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 9f91d808281..29211759011 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", @@ -21,7 +20,16 @@ module.exports = { "require-atomic-updates": "off", // This rule is so noisy and isn't useful: https://github.com/eslint/eslint/issues/11899 "require-jsdoc": "off", // This rule is deprecated and superseded by jsdoc/require-jsdoc. "valid-jsdoc": "off", // This is deprecated but included in recommended configs. - + "brikke/no-undeclared-imports": [ + "error", + { + excludedFilePatterns: ["**/scripts/**/*", `update-notifier-cjs.d.ts`], + excludedModules: [ + /node:/, + "express-serve-static-core", // We rely on just the types, and the package breaks our build. + ], + }, + ], "no-prototype-builtins": "warn", // TODO(bkendall): remove, allow to error. "no-useless-escape": "warn", // TODO(bkendall): remove, allow to error. "prefer-promise-reject-errors": "warn", // TODO(bkendall): remove, allow to error. @@ -32,8 +40,18 @@ module.exports = { rules: { "jsdoc/require-param-type": "off", "jsdoc/require-returns-type": "off", + + // Google style guide allows us to omit trivial parameters and returns + "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. + "@typescript-eslint/require-await": "off", // sometimes async functions don't do await stuff for valid reasons. "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. @@ -42,6 +60,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. @@ -56,8 +75,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. }, }, { @@ -68,6 +85,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", @@ -91,6 +109,10 @@ module.exports = { }, rules: {}, }, + { + files: ["src/mcp/tools/**/*.ts", "src/mcp/prompts/**/*.ts", "src/mcp/resources/**/*.ts"], + rules: { camelcase: "off" }, + }, ], globals: {}, parserOptions: { @@ -99,7 +121,7 @@ module.exports = { sourceType: "module", warnOnUnsupportedTypeScriptVersion: false, }, - plugins: ["prettier", "@typescript-eslint", "jsdoc"], + plugins: ["prettier", "@typescript-eslint", "jsdoc", "brikke"], settings: { jsdoc: { tagNamePreference: { @@ -108,4 +130,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 39a9bbea64c..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: "" --- @@ -38,4 +38,4 @@ https://firebase.google.com/support/ *Please avoid double posting across multiple channels!* --> -Please submit feature requests through our [support page](https://firebase.google.com/support/contact/bugs-features/). +Please submit feature requests through our [support page](https://firebase.google.com/support/troubleshooter/report/features/). diff --git a/.github/ISSUE_TEMPLATE/mcp.md b/.github/ISSUE_TEMPLATE/mcp.md new file mode 100644 index 00000000000..98af58290af --- /dev/null +++ b/.github/ISSUE_TEMPLATE/mcp.md @@ -0,0 +1,26 @@ +--- +name: "🤖 MCP Server" +about: Report bugs or request features for the Firebase MCP Server. +title: "[MCP]" +labels: "api: mcp" +assignees: "" +--- + + + +## Summary + + + +## Bug Info + +- **Affected Tool(s):** +- **MCP Client:** +- **Operating System:** + +### Steps to Reproduce + + 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 858a3b43ca5..fcb46816eb9 100644 --- a/.github/workflows/node-test.yml +++ b/.github/workflows/node-test.yml @@ -3,62 +3,154 @@ 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: - - 10.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: + runs-on: macos-latest + strategy: + matrix: + node-version: + - "20" + - "22" + 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 build + working-directory: firebase-vscode + - run: npm run test:unit + working-directory: firebase-vscode + - uses: codecov/codecov-action@v3 + if: matrix.node-version == '20' + + # vscode_integration: + # runs-on: macos-latest + # strategy: + # matrix: + # node-version: + # - "20" + + # env: + # FIREBASE_EMULATORS_PATH: ${{ github.workspace }}/emulator-cache + # # This overrides the binary which runs firebase commands in the extension tasks such as emulator start. + # # Currently, CI fails to start with npx so we change it to the global firebase binary. + # FIREBASE_BINARY: firebase + + # steps: + # - name: Setup Java JDK + # uses: actions/setup-java@v3.3.0 + # with: + # java-version: 17 + # distribution: temurin + + # - uses: actions/checkout@v4 + # - name: Setup Chrome + # uses: browser-actions/setup-chrome@v1.7.2 + # with: + # install-dependencies: true + # install-chromedriver: true + # - uses: actions/setup-node@v3 + # with: + # node-version: ${{ matrix.node-version }} + # cache: npm + # cache-dependency-path: firebase-vscode/package-lock.json + + # # TODO temporary workaround for GitHub Actions CI issue: + # # npm ERR! Your cache folder contains root-owned files, due to a bug in + # # npm ERR! previous versions of npm which has since been addressed. + # - run: sudo chown -R 501:20 "/Users/runner/.npm" || exit 1 + # - run: npm ci + # - run: npm install + # working-directory: firebase-vscode + # - run: npm run build + # working-directory: firebase-vscode + + # - run: npm i -g firebase-tools@latest + + # - uses: GabrielBB/xvfb-action@v1 + # with: + # run: npm run test:e2e + # working-directory: firebase-vscode + + # - uses: actions/upload-artifact@v4 + # if: failure() + # with: + # name: screenshots + # path: firebase-vscode/src/test/screenshots + + # - uses: codecov/codecov-action@v3 + # if: matrix.node-version == '20' unit: runs-on: ubuntu-latest strategy: matrix: node-version: - - 10.x - - 12.x - - 14.x + - "20" + - "22" 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' - runs-on: ubuntu-latest + if: contains(fromJSON('["push", "merge_group"]'), github.event_name) + runs-on: ubuntu-22.04 env: FIREBASE_EMULATORS_PATH: ${{ github.workspace }}/emulator-cache @@ -66,37 +158,49 @@ jobs: 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: - - 10.x + - "20" script: - - npm run test:hosting - npm run test:client-integration - npm run test:emulator - npm run test:extensions-emulator + - 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 + - npm run test:dataconnect-emulator 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 + cache: npm + cache-dependency-path: npm-shrinkwrap.json + - name: Setup Chrome + uses: browser-actions/setup-chrome@v1.7.2 with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} - + install-dependencies: true + install-chromedriver: true - 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 }} @@ -105,19 +209,127 @@ 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 # TODO (joehanley): Reenable this - it should be safe to run in parallel + # - npm run test:dataconnect-emulator # TODO (joehanley): Figure out why this is failing + - npm run test:frameworks + 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: dir "*.log" /s/b | type + check-package-lock: runs-on: ubuntu-latest strategy: matrix: node-version: - - 12.x + - "20" + - "22" + + 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" + - "22" + + 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: "(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 + + strategy: + matrix: + node-version: + - "20" + - "22" 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)" + 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..9857f71603c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,11 @@ +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 +scripts/functions-deploy-tests/**/package-lock.json +scripts/functions-discover-tests/**/**/package-lock.json +.dataconnect +*-debug.log + /.vscode node_modules /coverage @@ -6,6 +14,9 @@ node_modules firebase-debug.log firebase-debug.*.log npm-debug.log +ui-debug.log +test_output.log +scripts/emulator-tests/functions/index.js yarn.lock .npmrc @@ -17,3 +28,5 @@ scripts/*.json lib/ dev/ +clean/ +.gemini/ diff --git a/.mocharc.yml b/.mocharc.yml index 56e58b04e55..0e19f7e877f 100644 --- a/.mocharc.yml +++ b/.mocharc.yml @@ -6,3 +6,5 @@ file: - src/test/helpers/global-mock-auth.ts timeout: 1000 recursive: true +node-options: + - no-experimental-strip-types diff --git a/.prettierignore b/.prettierignore index 60f523e5f03..ec31db47c3d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,11 @@ /node_modules /lib/**/* /CONTRIBUTING.md +/scripts/frameworks-tests/vite-project/** +/scripts/webframeworks-deploy-tests/angular/** +/scripts/webframeworks-deploy-tests/nextjs/** +/src/frameworks/docs/** +/prompts + +# Intentionally invalid YAML file: +/src/test/fixtures/extension-yamls/invalid/extension.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 86988f737d1..e69de29bb2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +0,0 @@ -- Fixes native module issues by removing `fast-crc32c` (#3247, #3239). -- Adds retries on Quota Exceeded errors during functions deployment (#2606, #1372). -- Fixes Firestore Emulator wrong behavior for documents which are created and deleted in a single transaction. -- Fixes header parsing in Firestore Emulator causing permission denined errors with JS SDK v8.3.2 (#3258). -- Fixes an edge case with nextPageToken in batchGet in Auth Emulator (#3231). -- Removes unused dependencies (#3252). -- Adds support for multiple accounts via new commands `login:use`, `login:add` and `login:list`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ce9c273247..5da6eedac26 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,6 +53,7 @@ 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 ``` @@ -147,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 @@ -175,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 @@ -221,7 +217,7 @@ provide, the `Command.help` method accepts a long-form string to display for the #### Load the command -Next, go to `command/index.js`, then add a line to load the command, for +Next, go to `commands/index.ts`, then add a line to load the command, for example: ```javascript @@ -276,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 @@ -305,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/GEMINI.md b/GEMINI.md new file mode 100644 index 00000000000..d9e3212fd0e --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,57 @@ +# GEMINI.md + +This file provides guidance to Gemini CLI or other coding agents when working with code in this repository. It focuses on key conventions and best practices. For a comprehensive guide on the development setup and contribution process, see [`CONTRIBUTING.md`](CONTRIBUTING.md). + +## Essential Commands + +```bash +# Build the project +npm run build + +# Link the package - once this is run, you can manually test changes in your terminal +npm link + +npm test # Full test suite with linting and compilation +npm run mocha:fast # Quick unit tests only +npx mocha {testfile} # Quick unit test for a specific file + +# Linting and formatting +npm run lint # Check all code +npm run lint:changed-files # Lint changed files only (much faster) +npm run format # Auto-fix formatting issues +``` + +## Best Practices + +### Code Quality & Utilities + +- **Look for existing utilities first:** Before writing common helper functions (e.g., for logging, file system operations, promises, string manipulation), check `src/utils.ts` to see if a suitable function already exists. +- **Use the central `logger`** (`src/logger.ts`); never use `console.log()` for user-facing output. +- **Throw `FirebaseError`** (`src/error.ts`) for expected, user-facing errors. If the error is due to a violation of a precondition (e.g. something + that is null but should never be), specify a non-zero exit code. +- **API calls must use `apiv2.ts`** for authenticated requests. +- **Reduce nesting as much as possible** Code should avoid unnecessarily deep nesting or long periods of nesting. Handle edge cases early and exit + or fold them into the general case. Consider helper functions that can completely encapsulate branching, e.g. multiple ways a variable can be populated. + +### TypeScript + +- **Never use `any` or `unknown` as an escape hatch.** Define proper interfaces/types or use type guards. +- Use strict null checks and handle `undefined`/`null` explicitly. + +### Testing + +- **Avoid excessive mocking in unit tests.** If a test requires many mocks, it might be better as an integration test in `/scripts/[feature]-tests/`. +- **Unit tests (`*.spec.ts`) should be co-located with their source files.** +- Test error cases and edge conditions, not just the "happy path." + +## Git Workflow & Pull Requests + +1. **Lint and Test Before Committing:** Run `npm run lint:changed-files` for a quick check, and run the full `npm test` before submitting your PR to catch any issues. +2. **Structure Commit Messages for Pull Requests:** To streamline PR creation, format your commit messages to serve as both the commit and the PR description: + - **Subject Line:** A concise, imperative summary (e.g., `feat: add frobnicator support`). This will become the PR title. + - **Body:** After a blank line, structure the commit body to match the PR template. This will pre-populate the PR description. Include: + - `### Description` + - `### Scenarios Tested` + - `### Sample Commands` + - Reference issues with "Fixes #123" in the description. +3. **Update Changelog:** For any user-facing change (new features, bug fixes, deprecations), add a corresponding entry to `CHANGELOG.md`. diff --git a/README.md b/README.md index 9052db9d994..06e65e51f2e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Firebase CLI [![Actions Status][gh-actions-badge]][gh-actions] [![Node Version][node-badge]][npm] [![NPM version][npm-badge]][npm] +# Firebase CLI [![Actions Status][gh-actions-badge]][gh-actions] [![Node Version][node-badge]][npm] [![NPM version][npm-badge]][npm] [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=firebase&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImZpcmViYXNlLXRvb2xzIiwiZXhwZXJpbWVudGFsOm1jcCIsIi0tZGlyIiwiLiJdfQ==) The Firebase Command Line Interface (CLI) Tools can be used to test, manage, and deploy your Firebase project from the command line. @@ -117,15 +117,16 @@ Detailed doc is [here](https://firebase.google.com/docs/cli/auth). ### Extensions Commands -| Command | Description | -| ----------------- | ------------------------------------------------------------------------------------------- | -| **ext** | Display information on how to use ext commands and extensions installed to your project. | -| **ext:configure** | Configure an existing extension instance. | -| **ext:info** | Display information about an extension by name (extensionName@x.y.z for a specific version) | -| **ext:install** | Install an extension. | -| **ext:list** | List all the extensions that are installed in your Firebase project. | -| **ext:uninstall** | Uninstall an extension that is installed in your Firebase project by Instance ID. | -| **ext:update** | Update an existing extension instance to the latest version. | +| Command | Description | +| ------------------- | ------------------------------------------------------------------------------------------- | +| **ext** | Display information on how to use ext commands and extensions installed to your project. | +| **ext:configure** | Configure an existing extension instance. | +| **ext:info** | Display information about an extension by name (extensionName@x.y.z for a specific version) | +| **ext:install** | Install an extension. | +| **ext:sdk:install** | Install and SDK for an extension so you can define the extension in a functions codebase. | +| **ext:list** | List all the extensions that are installed in your Firebase project. | +| **ext:uninstall** | Uninstall an extension that is installed in your Firebase project by Instance ID. | +| **ext:update** | Update an existing extension instance to the latest version. | ### Cloud Firestore Commands @@ -136,15 +137,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 @@ -154,11 +161,17 @@ Detailed doc is [here](https://firebase.google.com/docs/cli/auth). ### Remote Config Commands -| Command | Description | -| ------------------------------ | ---------------------------------------------------------------------------------------------------------- | -| **remoteconfig:get** | Get a Firebase project's Remote Config template. | -| **remoteconfig:versions:list** | Get a list of the most recent Firebase Remote Config template versions that have been published. | -| **remoteconfig:rollback** | Roll back a project's published Remote Config template to the version provided by `--version_number` flag. | +| Command | Description | +| ----------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| **remoteconfig:get** | Get a Firebase project's Remote Config template. | +| **remoteconfig:versions:list** | Get a list of the most recent Firebase Remote Config template versions that have been published. | +| **remoteconfig:rollback** | Roll back a project's published Remote Config template to the version provided by `--version_number` flag. | +| **remoteconfig:experiments:get** | Get a Remote Config experiment. | +| **remoteconfig:experiments:list** | Get a list of Remote Config experiments | +| **remoteconfig:experiments:delete** | Delete a Remote Config experiment. | +| **remoteconfig:rollouts:get** | Get a Remote Config rollout. | +| **remoteconfig:rollouts:list** | Get a list of Remote Config rollouts. | +| **remoteconfig:rollouts:delete** | Delete a Remote Config rollout. | Use `firebase:deploy --only remoteconfig` to update and publish a project's Firebase Remote Config template. @@ -168,18 +181,18 @@ 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 By default `firebase login` sets a single global account for use on all projects. If you have multiple Google accounts which you use for Firebase projects you can -authorize multople accounts and use them on a per-project or per-command basis. +authorize multiple accounts and use them on a per-project or per-command basis. To authorize an additonal account for use with the CLI, run `firebase login:add`. You can view the list of authorized accounts with `firebase login:list`. @@ -218,21 +231,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..31f52f88a1b --- /dev/null +++ b/firebase-vscode/.gitignore @@ -0,0 +1,9 @@ +*.vsix +dist/ +*.scss.d.ts +resources/dist +.vscode-test +.wdio-vscode-service +logs +!*.tgz +prebuilt-extensions \ 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..5109cbc4aa0 --- /dev/null +++ b/firebase-vscode/.vscodeignore @@ -0,0 +1,24 @@ +.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/ +prebuilt-extensions/ \ No newline at end of file diff --git a/firebase-vscode/CHANGELOG.md b/firebase-vscode/CHANGELOG.md new file mode 100644 index 00000000000..1845fa2ffd3 --- /dev/null +++ b/firebase-vscode/CHANGELOG.md @@ -0,0 +1,385 @@ +## NEXT + +## 1.9.0 + +- [Added] Refine / Generate Operation Code Lens. +- [Added] Support run "firebase init" without login and project. +- Update internal `firebase-tools` dependency to 14.19.1 + +## 1.8.0 + +- [Changed] Gemini Code Assist is now optionally installed when using the "Build with AI" feature + +## 1.7.0 + +- Update internal `firebase-tools` dependency to 14.15.2 +- Fixed the projectless developer experience. There are "error linter", "run (local)" buttons. + +## 1.6.1 + +- Update internal `firebase-tools` dependency to 14.13.0 + +## 1.6.0 + +- Update internal `firebase-tools` dependency to 14.11.1 +- [Changed] Now integrates with GCA in its agentic mode, powered by the Gemini CLI. This brings the Firebase MCP Server directly into the VS Code environment, enabling developers to use natural language to generate application schemas and queries without manually invoking explicit tools. + +## 1.5.1 + +- Update internal `firebase-tools` dependency to 14.11.0 +- [Fixed] Language server now properly recognizes nested Dataconnect folders +- [Fixed] Add Data and Read Data now properly support enum and list types + +## 1.5.0 + +- Update internal `firebase-tools` dependency to 14.9.0 +- Update internal `graphql-language-server` dependency +- Update internal `graphql-language-service-server` dependency +- [Changed] Graphql Syntax Highlighter is now an extension dependency +- [Fixed] Language server now recognizes fragments in other files + +## 1.4.1 + +- Update internal `firebase-tools` dependency to 14.6.0 +- [Changed] Updated Gemini Tool name to @FirebaseDataConnect + +## 1.4.0 + +- Update internal `firebase-tools` dependency to 14.4.0 +- [Added] @data-connect tool callable from the Gemini Code Assist extension. + +## 1.3.1 + +- Updated internal `firebase-tools` dependency to 14.2.1 + +## 1.3.0 + +- [Fixed] Fixed an issue where adhoc operations would fail to execute + +## 1.2.0 + +- Updated internal `firebase-tools` dependency to 14.2.0 +- [Fixed] Fragments now properly validate for execution + +## 1.1.0 + +- Updated internal `firebase-tools` dependency to 14.1.0 +- [Fixed] User auth will now load without requiring extension sidebar to open + +## 1.0.0 + +- [Breaking] Updated minimum VSCode version requirement to 1.69.0 to ensure node 20 is used +- Updated internal `firebase-tools` dependency to 14.0.0 +- [Added] Added rerun execution button in variables context +- [Added] Provide default required variables during execution +- [Fixed] Fixed an issue where environment variables provided in `extraEnv` were not respected in some cases + +## 0.14.2 + +- Updated internal `firebase-tools` dependency to 13.34.0 + +## 0.14.1 + +- Updated internal `firebase-tools` dependency to 13.33.0 +- Updated introspection endpoint to V1 +- Allow unused variables in GraphQL queries and mutations. + +## 0.14.0 + +- Updated internal `firebase-tools` dependency to 13.32.0 +- [Fixed] Graphql Language Server support for Windows + +## 0.13.1 + +- Updated internal `firebase-tools` dependency to 13.31.2 + +## 0.13.0 + +- Updated internal `firebase-tools` dependency to 13.30.0 +- [Added] Added `extraEnv` setting to help extension development. +- [Added] Make Run Local button always present + +## 0.12.2 + +- Updated internal `firebase-tools` dependency to 13.29.3 +- [Fixed] Fixed a bug where results panel would break on API error + +## 0.12.1 + +- Updated internal `firebase-tools` dependency to 13.29.2 +- [Added] Added support for emulator import/export. +- [Added] Added `debug` setting to run commands with `--debug` +- [Fixed] Fixed a bug where emulator issues weren't being surfaced + +## 0.12.0 + +- Updated internal firebase-tools dependency to 13.29.1 +- [Fixed] Fixed firebase binary detection for analytics + +## 0.11.1 + +- [Fixed] Fixed IDX analytics issue + +## 0.11.0 + +- Updated internal firebase-tools dependency to 13.28.0 +- [Fixed] Fixed an issue where generating an ad-hoc file would break codelenses + +## 0.10.8 + +- Updated internal firebase-tools dependency to 13.25.0 +- [Fixed] Fixed an issue where the toolkit wouldn't start with misconfigured configs +- [Fixed] Fixed a visual bug when selecting a Firebase project in an empty folder + +## 0.10.7 + +- Updated internal firebase-tools dependency to 13.24.2 +- [Fixed] Fixed an issue where Add data and Read data would generate operations in the wrong folder +- [Fixed] Fixed an issue where firebase version check produced false positives on Windows (#7910) + +## 0.10.6 + +- Updated internal firebase-tools dependency to 13.23.1 +- [Added] Persist FIREBASE_BINARY env variable to settings. +- [Fixed] Fixed an issue where .firebaserc was being overwritten by the extension (#7861) + +## 0.10.5 + +- [Fixed] Fixed an issue where multiple instances of the extension would break the toolkit. + +## 0.10.4 + +- [Fixed] Fixed an issue where log files would be written to non-Firebase directories. + +## 0.10.3 + +- Updated internal firebase-tools dependency to 13.21.0 +- Updated default debug-log output to .firebase/logs directory +- [Fixed] Fixed an issue where emulator startup would hang +- Updated text for SDK configuration button + +## 0.10.2 + +- Updated internal firebase-tools dependency to 13.20.2 + +## 0.10.1 + +- [Fixed] Fixed an issue where commands would be executed against directory default project instead of the currently selected project. +- [Fixed] Fixed an issue where expired auth tokens would be used. +- [Fixed] Fixed an issue where Add Data wouldn't generate UUID types +- Updated README with feature descriptions + +## 0.10.0 + +- [Added] UI overhaul. +- [Added] Added View Docs button to see generated documentation for your schema and connectors. +- [Fixed] Improved detection for emulator start up and shut down. +- [Fixed] Improved error handling for variables pane. +- [Added] Added Firebase path setting, to control which Firebase dbinary is used when executing commands. + +## 0.9.1 + +- Updated internal firebase-tools dependency to 13.19.0 + +## 0.9.0 + +- Updated internal firebase-tools dependency to 13.18.0 + +## 0.8.0 + +- Updated internal firebase-tools dependency to 13.17.0 + +- [Fixed] Extension properly picks up firebase.json changes during Firebase Init flow + +## 0.7.0 + +- Updated internal firebase-tools dependency to 13.16.0 + +## 0.6.2 + +- Updated internal firebase-tools dependency to 13.15.4 + +## 0.6.1 + +- Updated internal firebase-tools dependency to 13.15.3 + +## 0.6.0 + +- Updated internal firebase-tools dependency to 13.15.2 + +- [Added] Support for configuring generated SDK +- Automatically pick up IDX project selection. + +## 0.5.4 + +- Updated internal firebase-tools dependency to 13.15.1 + +## 0.5.3 + +- Updated internal firebase-tools dependency to 13.15.0 + +## 0.5.2 + +- Updated internal firebase-tools dependency to 13.14.2 + +## 0.5.1 + +- Updated internal firebase-tools dependency to 13.14.1 + +## 0.5.0 + +- Updated internal firebase-tools dependency to 13.14.0 + +## 0.4.4 + +- [Fixed] Local execution now properly supports Vertex API + +## 0.4.3 + +- Updated internal firebase-tools dependency to 13.13.3 + +## 0.4.2 + +- Updated internal firebase-tools dependency to 13.13.2 + +## 0.4.1 + +- Updated internal firebase-tools dependency to 13.13.1 + +- IDX Auth is picked up by VSCode +- [Fixed] Data Connect emulator issues properly streamed on startup +- [Fixed] Data Connect schema reloads consistently + +## 0.4.0 + +- Updated internal firebase-tools dependency to 13.13.0 + +## 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..df29955fb58 --- /dev/null +++ b/firebase-vscode/README.md @@ -0,0 +1,37 @@ +# Firebase Data Connect for VSCode + +The Firebase Data Connect extension provides a suite of tools to assist developers in their Data Connect development workflow. + +![Extension Demo Gif](https://www.gstatic.com/mobilesdk/241004_mobilesdk/fdc_extension_readme.gif) + +## Language Features + +The extension runs a Graphql Language Server that checks for syntax and compile time errors in your Data Connect code. Additionally, it provides auto-complete suggestions specific to Data Connect. + +The extension will automatically generate GraphQL types based on your schema, viewable in your Explorer panel. + +## Query Execution + +Within your GraphQL files, you’ll see in-line Codelenses that can help you create and test operations. + +In your schema files, click on `Add Data` or `Read Data` to generate a corresponding operation to populate or read from your DB. + +To execute an operation, click on `Run Local` or `Run Production`. This will execute your operation against the emulators, or your production Data Connect instance. + +Note: You’ll need to start the Data Connect emulator in order to execute operations locally. + +## Strongly typed SDK Generation + +The extension can help you set-up SDK generation with a simple folder selection. Once you’ve selected an app folder of your choice, client code will start generating automatically. + +## Local Emulator + +You can start a local emulator to test your queries on your application. + +## Deploy to Production + +Once you’ve tested the schema and operations and ran the generated SDK in your app, deploy your schema, operation and data to your Cloud SQL instance in production. + +### Documentation + +Please see [Getting started with Firebase Data Connect](https://firebase.google.com/docs/data-connect/quickstart). 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/declarations.d.ts b/firebase-vscode/common/declarations.d.ts new file mode 100644 index 00000000000..7f267e4cf03 --- /dev/null +++ b/firebase-vscode/common/declarations.d.ts @@ -0,0 +1,2 @@ +// We need to tell TypeScript that when we write "import styles from './styles.scss' we mean to load a module (to look for a './styles.scss.d.ts'). +declare module "*.scss"; 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..266d643a406 --- /dev/null +++ b/firebase-vscode/common/messaging/broker.ts @@ -0,0 +1,105 @@ +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 as 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..6587794140b --- /dev/null +++ b/firebase-vscode/common/messaging/protocol.ts @@ -0,0 +1,202 @@ +/** + * @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 { EmulatorsStatus, RunningEmulatorInfo } from "./types"; +import { ExecutionResult } from "graphql"; +import { SerializedError } from "../error"; + +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; + getInitialFirebaseConfigList: void; + + addUser: {}; + logout: { email: string }; + + /* Emulator panel requests */ + getEmulatorUiSelections: void; + getEmulatorInfos: void; + + /** Notify extension that current user has been changed in UI. */ + requestChangeUser: { user: User | ServiceAccountUser }; + + /** Trigger project selection */ + selectProject: {}; + + /** When 2+ firebase.json are detected, the user can manually pick one */ + selectFirebaseConfig: string; + + /** + * Prompt user for text input + */ + promptUserForInput: { title: string; prompt: string }; + + /** Calls the `firebase init` CLI */ + runFirebaseInit: void; + + /** Calls the `firebase emulators:start` CLI */ + runStartEmulators: void; + + /** Calls the `firebase emulators:export` CLI */ + runEmulatorsExport: 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; + + /** Configures generated SDK */ + "fdc.configure-sdk": void; + + /** Opens generated docs */ + "fdc.open-docs": void; + + /** Opens settings page searching for Data Connect emualtor settings */ + "fdc.open-emulator-settings": void; + + /** Clears data from a running data connect emulator */ + "fdc.clear-emulator-data": void; + + "firebase.activate.gemini": void; + + // Initialize "result" tab. + getDataConnectResults: void; + + // execute terminal tasks + executeLogin: void; + + getDocsLink: void; + + openJSONFile: string; + + // called from execution panel + rerunExecution: void; + + /** Docs clicked for analytics */ + "docs.mcp.clicked": void; + "docs.tos.clicked": 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 */ + notifyEmulatorStateChanged: { + status: EmulatorsStatus; + infos?: RunningEmulatorInfo | undefined; + }; + + /** Lists all firebase.json in the workspace */ + notifyFirebaseConfigListChanged: { + values: string[]; + selected: string | undefined; + }; + + notifyEmulatorsHanging: boolean; + + /** 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 | null }; + + /** + * Notify webview of initial discovery or change in firebase.json or + * .firebaserc + */ + notifyFirebaseConfig: { + firebaseJson?: ValueOrError; + firebaseRC?: ValueOrError; + }; + /** Whether any dataconnect.yaml is present */ + notifyHasFdcConfigs: boolean; + + /** + * Return user-selected preview channel name + */ + notifyPreviewChannelResponse: { id: string }; + + // data connect specific + notifyDataConnectArgs: string; + + notifyDataConnectResults: DataConnectResults; + + notifyLastOperation: string; + + notifyIsLoadingUser: boolean; + + notifyDocksLink: 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..f750677e9b0 --- /dev/null +++ b/firebase-vscode/common/messaging/types.d.ts @@ -0,0 +1,22 @@ +import { EmulatorInfo } from "../emulator/types"; + +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 { + uiUrl: string; + displayInfo: EmulatorInfo[]; +} + +export type EmulatorsStatus = "running" | "stopped" | "starting" | "stopping"; 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.4.0.tgz b/firebase-vscode/graphql-language-service-5.4.0.tgz new file mode 100644 index 00000000000..5dd29c305fc Binary files /dev/null and b/firebase-vscode/graphql-language-service-5.4.0.tgz differ diff --git a/firebase-vscode/graphql-language-service-server-2.14.8.tgz b/firebase-vscode/graphql-language-service-server-2.14.8.tgz new file mode 100644 index 00000000000..6a99e623d14 Binary files /dev/null and b/firebase-vscode/graphql-language-service-server-2.14.8.tgz differ diff --git a/firebase-vscode/package-lock.json b/firebase-vscode/package-lock.json new file mode 100644 index 00000000000..515b478a397 --- /dev/null +++ b/firebase-vscode/package-lock.json @@ -0,0 +1,17743 @@ +{ + "name": "firebase-dataconnect-vscode", + "version": "1.9.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "firebase-dataconnect-vscode", + "version": "1.9.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.4.0.tgz", + "graphql-language-service-server": "file:graphql-language-service-server-2.14.8.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": "20.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": "^9.0.7", + "@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": "^6.1.1", + "webpack": "^5.75.0", + "webpack-cli": "^5.0.1", + "webpack-merge": "^5.8.0" + }, + "engines": { + "vscode": "^1.69.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/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", + "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.17.0.tgz", + "integrity": "sha512-62Vv8nC+uPId3j86XJ0WI+sBf0jlqTqPUFCBNrGtlaUeQUIXWV/D8GE5A1d+Qx8H7OQojn2WguC8kChD6v0shA==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.8.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.9.0", + "@azure/logger": "^1.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz", + "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz", + "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.5.0.tgz", + "integrity": "sha512-EknvVmtBuSIic47xkOqyNabAme0RYTw52BTMz8eBgU1ysTyMrD1uOoM+JdS0J/4Yfp98IBT3osqq3BfwSaNaGQ==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^3.26.1", + "@azure/msal-node": "^2.15.0", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.4.tgz", + "integrity": "sha512-4IXXzcCdLdlXuCG+8UKEwLA1T1NHqUfanhXYHiQTn+6sfWCZXduqbtXDGceg3Ce5QxTGo7EqmbV6Bi+aqKuClQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "3.26.1", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.26.1.tgz", + "integrity": "sha512-y78sr9g61aCAH9fcLO1um+oHFXc1/5Ap88RIsUSuzkm0BHzFnN+PXGaQeuM1h5Qf5dTnWNOd6JqkskkMPAhh7Q==", + "dependencies": { + "@azure/msal-common": "14.15.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "14.15.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.15.0.tgz", + "integrity": "sha512-ImAQHxmpMneJ/4S8BRFhjt1MZ3bppmpRPYYNyzeQPeFN288YKbb8TmmISQEbtfkQ1BPASvYZU5doIZOPBAqENQ==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.15.0.tgz", + "integrity": "sha512-gVPW8YLz92ZeCibQH2QUw96odJoiM3k/ZPH3f2HxptozmH6+OnyyvKXo/Egg39HAM230akarQKHf0W74UHlh0Q==", + "dependencies": { + "@azure/msal-common": "14.15.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", + "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", + "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.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "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==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "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.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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/@envelop/core": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@envelop/core/-/core-5.2.3.tgz", + "integrity": "sha512-KfoGlYD/XXQSc3BkM1/k15+JQbkQ4ateHazeZoWl9P71FsLTDXSjGy6j7QqfhpIDSbxNISqhPMfZHYSbDFOofQ==", + "dependencies": { + "@envelop/instrumentation": "^1.0.0", + "@envelop/types": "^5.2.1", + "@whatwg-node/promise-helpers": "^1.2.4", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@envelop/instrumentation": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@envelop/instrumentation/-/instrumentation-1.0.0.tgz", + "integrity": "sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==", + "dependencies": { + "@whatwg-node/promise-helpers": "^1.2.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@envelop/types": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@envelop/types/-/types-5.2.1.tgz", + "integrity": "sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==", + "dependencies": { + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "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.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz", + "integrity": "sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz", + "integrity": "sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==", + "dev": true, + "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.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/ajv/node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true + }, + "node_modules/@fastify/ajv-compiler/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/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@fastify/cors": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-9.0.1.tgz", + "integrity": "sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==", + "dev": true, + "dependencies": { + "fastify-plugin": "^4.0.0", + "mnemonist": "0.39.6" + } + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==", + "dev": true + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "dev": true, + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@fastify/send": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz", + "integrity": "sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==", + "dev": true, + "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/send/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/@fastify/static": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-7.0.4.tgz", + "integrity": "sha512-p2uKtaf8BMOZWLs6wu+Ihg7bWNBdjNgCwDza4MJtTqg+5ovKmcbgbR9Xs5/smZ1YISfzKOCNYmZV8LaCj+eJ1Q==", + "dev": true, + "dependencies": { + "@fastify/accept-negotiator": "^1.0.0", + "@fastify/send": "^2.0.0", + "content-disposition": "^0.5.3", + "fastify-plugin": "^4.0.0", + "fastq": "^1.17.0", + "glob": "^10.3.4" + } + }, + "node_modules/@fastify/static/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/@fastify/static/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/static/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@graphql-hive/signal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@graphql-hive/signal/-/signal-1.0.0.tgz", + "integrity": "sha512-RiwLMc89lTjvyLEivZ/qxAC5nBHoS2CtsWFSOsN35sxG9zoo5Z+JsFHM8MlvmO9yt+MJNIyC5MLE1rsbOphlag==", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@graphql-tools/batch-execute": { + "version": "9.0.17", + "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-9.0.17.tgz", + "integrity": "sha512-i7BqBkUP2+ex8zrQrCQTEt6nYHQmIey9qg7CMRRa1hXCY2X8ZCVjxsvbsi7gOLwyI/R3NHxSRDxmzZevE2cPLg==", + "dependencies": { + "@graphql-tools/utils": "^10.8.1", + "@whatwg-node/promise-helpers": "^1.3.0", + "dataloader": "^2.2.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.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.2.19", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-10.2.19.tgz", + "integrity": "sha512-aaCGAALTQmKctHwumbtz0c5XehGjYLSfoDx1IB2vdPt76Q0MKz2AiEDlENgzTVr4JHH7fd9YNrd+IO3D8tFlIg==", + "dependencies": { + "@graphql-tools/batch-execute": "^9.0.17", + "@graphql-tools/executor": "^1.4.7", + "@graphql-tools/schema": "^10.0.11", + "@graphql-tools/utils": "^10.8.1", + "@repeaterjs/repeater": "^3.0.6", + "@whatwg-node/promise-helpers": "^1.3.0", + "dataloader": "^2.2.3", + "dset": "^3.1.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-1.4.7.tgz", + "integrity": "sha512-U0nK9jzJRP9/9Izf1+0Gggd6K6RNRsheFo1gC/VWzfnsr0qjcOSS9qTjY0OTC5iTPt4tQ+W5Zpw/uc7mebI6aA==", + "dependencies": { + "@graphql-tools/utils": "^10.8.6", + "@graphql-typed-document-node/core": "^3.2.0", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/promise-helpers": "^1.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/executor-common": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-common/-/executor-common-0.0.4.tgz", + "integrity": "sha512-SEH/OWR+sHbknqZyROCFHcRrbZeUAyjCsgpVWCRjqjqRbiJiXq6TxNIIOmpXgkrXWW/2Ev4Wms6YSGJXjdCs6Q==", + "dependencies": { + "@envelop/core": "^5.2.3", + "@graphql-tools/utils": "^10.8.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-graphql-ws": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-2.0.5.tgz", + "integrity": "sha512-gI/D9VUzI1Jt1G28GYpvm5ckupgJ5O8mi5Y657UyuUozX34ErfVdZ81g6oVcKFQZ60LhCzk7jJeykK48gaLhDw==", + "dependencies": { + "@graphql-tools/executor-common": "^0.0.4", + "@graphql-tools/utils": "^10.8.1", + "@whatwg-node/disposablestack": "^0.0.6", + "graphql-ws": "^6.0.3", + "isomorphic-ws": "^5.0.0", + "tslib": "^2.8.1", + "ws": "^8.17.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor-http": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-1.3.3.tgz", + "integrity": "sha512-LIy+l08/Ivl8f8sMiHW2ebyck59JzyzO/yF9SFS4NH6MJZUezA1xThUXCDIKhHiD56h/gPojbkpcFvM2CbNE7A==", + "dependencies": { + "@graphql-hive/signal": "^1.0.0", + "@graphql-tools/executor-common": "^0.0.4", + "@graphql-tools/utils": "^10.8.1", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/fetch": "^0.10.4", + "@whatwg-node/promise-helpers": "^1.3.0", + "meros": "^1.2.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.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.1.17", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-1.1.17.tgz", + "integrity": "sha512-TvltY6eL4DY1Vt66Z8kt9jVmNcI+WkvVPQZrPbMCM3rv2Jw/sWvSwzUBezRuWX0sIckMifYVh23VPcGBUKX/wg==", + "dependencies": { + "@graphql-tools/utils": "^10.8.6", + "@types/ws": "^8.0.0", + "isomorphic-ws": "^5.0.0", + "tslib": "^2.4.0", + "ws": "^8.17.1" + }, + "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.20", + "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-8.0.20.tgz", + "integrity": "sha512-inds4My+JJxmg5mxKWYtMIJNRxa7MtX+XIYqqD/nu6G4LzQ5KGaBJg6wEl103KxXli7qNOWeVAUmEjZeYhwNEg==", + "dependencies": { + "@graphql-tools/import": "7.0.19", + "@graphql-tools/utils": "^10.8.6", + "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.19", + "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-7.0.19.tgz", + "integrity": "sha512-Xtku8G4bxnrr6I3hVf8RrBFGYIbQ1OYVjl7jgcy092aBkNZvy1T6EDmXmYXn5F+oLd9Bks3K3WaMm8gma/nM/Q==", + "dependencies": { + "@graphql-tools/utils": "^10.8.6", + "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.18", + "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-8.0.18.tgz", + "integrity": "sha512-JjjIxxewgk8HeMR3npR3YbOkB7fxmdgmqB9kZLWdkRKBxrRXVzhryyq+mhmI0Evzt6pNoHIc3vqwmSctG2sddg==", + "dependencies": { + "@graphql-tools/utils": "^10.8.6", + "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.1.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-8.1.0.tgz", + "integrity": "sha512-OGfOm09VyXdNGJS/rLqZ6ztCiG2g6AMxhwtET8GZXTbnjptFc17GtKwJ3Jv5w7mjJ8dn0BHydvIuEKEUK4ciYw==", + "dependencies": { + "@graphql-tools/schema": "^10.0.23", + "@graphql-tools/utils": "^10.8.6", + "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.24", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.0.24.tgz", + "integrity": "sha512-NzWx/Afl/1qHT3Nm1bghGG2l4jub28AdvtG11PoUlmjcIjnFBJMv4vqL0qnxWe8A82peWo4/TkVdjJRLXwgGEw==", + "dependencies": { + "@graphql-tools/utils": "^10.8.6", + "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.23", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.23.tgz", + "integrity": "sha512-aEGVpd1PCuGEwqTXCStpEkmheTHNdMayiIKH1xDWqYp9i8yKv9FRDgkGrY4RD8TNxnf7iII+6KOBGaJ3ygH95A==", + "dependencies": { + "@graphql-tools/merge": "^9.0.24", + "@graphql-tools/utils": "^10.8.6", + "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/url-loader": { + "version": "8.0.31", + "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-8.0.31.tgz", + "integrity": "sha512-QGP3py6DAdKERHO5D38Oi+6j+v0O3rkBbnLpyOo87rmIRbwE6sOkL5JeHegHs7EEJ279fBX6lMt8ry0wBMGtyA==", + "dependencies": { + "@graphql-tools/executor-graphql-ws": "^2.0.1", + "@graphql-tools/executor-http": "^1.1.9", + "@graphql-tools/executor-legacy-ws": "^1.1.17", + "@graphql-tools/utils": "^10.8.6", + "@graphql-tools/wrap": "^10.0.16", + "@types/ws": "^8.0.0", + "@whatwg-node/fetch": "^0.10.0", + "@whatwg-node/promise-helpers": "^1.0.0", + "isomorphic-ws": "^5.0.0", + "sync-fetch": "0.6.0-2", + "tslib": "^2.4.0", + "ws": "^8.17.1" + }, + "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.8.6", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.8.6.tgz", + "integrity": "sha512-Alc9Vyg0oOsGhRapfL3xvqh1zV8nKoFUdtLhXX7Ki4nClaIJXckrA86j+uxEuG3ic6j4jlM1nvcWXRn/71AVLQ==", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "dset": "^3.1.4", + "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.1.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-10.1.0.tgz", + "integrity": "sha512-M7QolM/cJwM2PNAJS1vphT2/PDVSKtmg5m+fxHrFfKpp2RRosJSvYPzUD/PVPqF2rXTtnCwkgh1s5KIsOPCz+w==", + "dependencies": { + "@graphql-tools/delegate": "^10.2.19", + "@graphql-tools/schema": "^10.0.11", + "@graphql-tools/utils": "^10.8.1", + "@whatwg-node/promise-helpers": "^1.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.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.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.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.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "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/@inquirer/checkbox": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-3.0.1.tgz", + "integrity": "sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-4.0.1.tgz", + "integrity": "sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", + "dev": true, + "dependencies": { + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/@types/node": { + "version": "22.8.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz", + "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@inquirer/core/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/@inquirer/editor": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-3.0.1.tgz", + "integrity": "sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-3.0.1.tgz", + "integrity": "sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.7.tgz", + "integrity": "sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-3.0.1.tgz", + "integrity": "sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-2.0.1.tgz", + "integrity": "sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-3.0.1.tgz", + "integrity": "sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-6.0.1.tgz", + "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==", + "dev": true, + "dependencies": { + "@inquirer/checkbox": "^3.0.1", + "@inquirer/confirm": "^4.0.1", + "@inquirer/editor": "^3.0.1", + "@inquirer/expand": "^3.0.1", + "@inquirer/input": "^3.0.1", + "@inquirer/number": "^2.0.1", + "@inquirer/password": "^3.0.1", + "@inquirer/rawlist": "^3.0.1", + "@inquirer/search": "^2.0.1", + "@inquirer/select": "^3.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-3.0.1.tgz", + "integrity": "sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-2.0.1.tgz", + "integrity": "sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-3.0.1.tgz", + "integrity": "sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dev": true, + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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-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/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/@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, + "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", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": 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/@jest/types/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/@jest/types/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/@jest/types/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/@jest/types/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/@jest/types/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/types/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/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "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.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/fast-element": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.14.0.tgz", + "integrity": "sha512-zXvuSOzvsu8zDTy9eby8ix8VqLop2rwKRgp++ZN2kTCsoB3+QJVoaGD2T/Cyso2ViZQFXNpiNCVKfnmxBvmWkQ==" + }, + "node_modules/@microsoft/fast-foundation": { + "version": "2.50.0", + "resolved": "https://registry.npmjs.org/@microsoft/fast-foundation/-/fast-foundation-2.50.0.tgz", + "integrity": "sha512-8mFYG88Xea1jZf2TI9Lm/jzZ6RWR8x29r24mGuLojNYqIR2Bl8+hnswoV6laApKdCbGMPKnsAL/O68Q0sRxeVg==", + "dependencies": { + "@microsoft/fast-element": "^1.14.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", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@microsoft/fast-react-wrapper": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@microsoft/fast-react-wrapper/-/fast-react-wrapper-0.3.25.tgz", + "integrity": "sha512-jKzmk2xJV93RL/jEFXEZgBvXlKIY4N4kXy3qrjmBfFpqNi3VjY+oUTWyMnHRMC5EUhIFxD+Y1VD4u9uIPX3jQw==", + "dependencies": { + "@microsoft/fast-element": "^1.14.0", + "@microsoft/fast-foundation": "^2.50.0" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, + "node_modules/@microsoft/fast-web-utilities": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/fast-web-utilities/-/fast-web-utilities-5.4.1.tgz", + "integrity": "sha512-ReWYncndjV3c8D8iq9tp7NcFNc1vbVHvcBFPME2nNFKNbS1XCesYZGlIlf3ot5EmuOXPlrzUHOWzQ2vFpIkqDg==", + "dependencies": { + "exenv-es6": "^1.1.1" + } + }, + "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==", + "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==", + "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==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dev": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true + }, + "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/@preact/signals-core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.8.0.tgz", + "integrity": "sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@preact/signals-react": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@preact/signals-react/-/signals-react-1.3.6.tgz", + "integrity": "sha512-jr/4lhcRo5W3hfieCJGDPbxq3YjfZDvpmTcisJ+lRhjWvnoYrgMKBoTiLsFPACO8VIEATaIBZXYiGAV08YvnfQ==", + "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/@promptbook/utils": { + "version": "0.69.5", + "resolved": "https://registry.npmjs.org/@promptbook/utils/-/utils-0.69.5.tgz", + "integrity": "sha512-xm5Ti/Hp3o4xHrsK9Yy3MS6KbDxYbq485hDsFvxqaNA7equHLPdo8H8faTitTeb14QCDfLW4iwCxdVYu5sn6YQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/webgptorg/promptbook/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "dependencies": { + "spacetrim": "0.11.59" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.4.0.tgz", + "integrity": "sha512-x8J1csfIygOwf6D6qUAZ0ASk3z63zPb7wkNeHRerCMh82qWKUrOgkuP005AJC8lDL6/evtXETGEJVcwykKT4/g==", + "dev": true, + "dependencies": { + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@repeaterjs/repeater": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.6.tgz", + "integrity": "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==" + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true + }, + "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 + }, + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "dev": true, + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@teamsupercell/typings-for-css-modules-loader": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@teamsupercell/typings-for-css-modules-loader/-/typings-for-css-modules-loader-2.5.2.tgz", + "integrity": "sha512-3sqH2B4itcm5XgV1IHENt4NOaW7bOC1CwJr63vrdKWWyKVxNxtBM+ABVhJZYFCCVAwNy7ulA64z6HyQqw96m4A==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "loader-utils": "^1.4.2", + "schema-utils": "^2.0.1" + }, + "optionalDependencies": { + "prettier": "*" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true + }, + "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==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + }, + "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==", + "dev": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true + }, + "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 + }, + "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, + "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, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "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/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "10.0.9", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.9.tgz", + "integrity": "sha512-sicdRoWtYevwxjOHNMPTl3vSfJM6oyW8o1wXeI7uww6b6xHg8eBznQDNSGBCDJmsE8UMxP05JgZRtsKbTqt//Q==", + "dev": true + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.17.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.5.tgz", + "integrity": "sha512-n8FYY/pRxu496441gIcAQFZPKXbhsd6VZygcq+PTSZ75eMh/Ke0hCAROdUa21qiFqKNsPPYic46yXDO1JGiPBQ==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", + "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, + "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 + }, + "node_modules/@types/vscode": { + "version": "1.95.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.95.0.tgz", + "integrity": "sha512-0LBD8TEiNbet3NvWsmn59zLzOFu/txSlGxnv5yAFHCrhG9WvAnR3IvfHzMOs2aeWqgvNjq9pO99IUw8d3n+unw==", + "dev": true + }, + "node_modules/@types/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", + "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", + "dev": true + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": 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 + }, + "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.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "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.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.4.tgz", + "integrity": "sha512-L95zIAkEuTDbUX1IsjRl+vyBSLh3PwLLgKpghl37aCK9Jvw0iP+wKwIFhfjdUtA2myLgjrG6VU6JCFLv8q/3Ww==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.4.tgz", + "integrity": "sha512-3Kab14fn/5QZRog5BPj6Rs8dc4B+mim27XaKWFWHWA87R56AKjHTGcBFKpvZKDzC4u5Wd0w/qKsUIio3KzWW4Q==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.4", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vscode/codicons": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.30.tgz", + "integrity": "sha512-/quu8pLXEyrShoDjTImQwJ2H28y1XhANigyw7E7JvN9NNWc3XCkoIWpcb/tUhdf7XQpopLVVYbkMjXpdPPuMXg==" + }, + "node_modules/@vscode/test-electron": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.1.tgz", + "integrity": "sha512-Gc6EdaLANdktQ1t+zozoBVRynfIsMKMc94Svu1QreOBC8y76x4tvaK32TljrLi1LI2+PK58sDVbL7ALdqf3VRQ==", + "dev": true, + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "jszip": "^3.10.1", + "ora": "^7.0.1", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vscode/vsce": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.32.0.tgz", + "integrity": "sha512-3EFJfsgrSftIqt3EtdRcAygy/OJ3hstyI1cDmIgkU9CFZW5C+3djr6mfosndCUqcVYuyjmxOK1xmFp/Bq7+NIg==", + "dependencies": { + "@azure/identity": "^4.1.0", + "@vscode/vsce-sign": "^2.0.0", + "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-sign": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.5.tgz", + "integrity": "sha512-GfYWrsT/vypTMDMgWDm75iDmAOMe7F71sZECJ+Ws6/xyIfmB3ELVnVN+LwMFAvmXY+e6eWhR2EzNGF/zAhWY3Q==", + "hasInstallScript": true, + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.2", + "@vscode/vsce-sign-alpine-x64": "2.0.2", + "@vscode/vsce-sign-darwin-arm64": "2.0.2", + "@vscode/vsce-sign-darwin-x64": "2.0.2", + "@vscode/vsce-sign-linux-arm": "2.0.2", + "@vscode/vsce-sign-linux-arm64": "2.0.2", + "@vscode/vsce-sign-linux-x64": "2.0.2", + "@vscode/vsce-sign-win32-arm64": "2.0.2", + "@vscode/vsce-sign-win32-x64": "2.0.2" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.2.tgz", + "integrity": "sha512-E80YvqhtZCLUv3YAf9+tIbbqoinWLCO/B3j03yQPbjT3ZIHCliKZlsy1peNc4XNZ5uIb87Jn0HWx/ZbPXviuAQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.2.tgz", + "integrity": "sha512-n1WC15MSMvTaeJ5KjWCzo0nzjydwxLyoHiMJHu1Ov0VWTZiddasmOQHekA47tFRycnt4FsQrlkSCTdgHppn6bw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.2.tgz", + "integrity": "sha512-rz8F4pMcxPj8fjKAJIfkUT8ycG9CjIp888VY/6pq6cuI2qEzQ0+b5p3xb74CJnBbSC0p2eRVoe+WgNCAxCLtzQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.2.tgz", + "integrity": "sha512-MCjPrQ5MY/QVoZ6n0D92jcRb7eYvxAujG/AH2yM6lI0BspvJQxp0o9s5oiAM9r32r9tkLpiy5s2icsbwefAQIw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.2.tgz", + "integrity": "sha512-Fkb5jpbfhZKVw3xwR6t7WYfwKZktVGNXdg1m08uEx1anO0oUPUkoQRsNm4QniL3hmfw0ijg00YA6TrxCRkPVOQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.2.tgz", + "integrity": "sha512-Ybeu7cA6+/koxszsORXX0OJk9N0GgfHq70Wqi4vv2iJCZvBrOWwcIrxKjvFtwyDgdeQzgPheH5nhLVl5eQy7WA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.2.tgz", + "integrity": "sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.2.tgz", + "integrity": "sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.2.tgz", + "integrity": "sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "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==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "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/webview-ui-toolkit": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vscode/webview-ui-toolkit/-/webview-ui-toolkit-1.4.0.tgz", + "integrity": "sha512-modXVHQkZLsxgmd5yoP3ptRC/G8NBDD+ob+ngPiWNQdlrH6H1xR/qgOBD85bfU3BhOB5sZzFWBwwhp9/SfoHww==", + "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.5.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.16.tgz", + "integrity": "sha512-AOQS2eaQOaaZQoL1u+2rCJIKDruNXVBZSiUD3chnUrsoX5ZTQMaCvXlWNIfxBJuU15r1o7+mpo5223KVtIhAgQ==", + "dependencies": { + "@babel/parser": "^7.27.2", + "@vue/shared": "3.5.16", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.16.tgz", + "integrity": "sha512-SSJIhBr/teipXiXjmWOVWLnxjNGo65Oj/8wTEQz0nqwQeP75jWZ0n4sF24Zxoht1cuJoWopwj0J0exYwCJ0dCQ==", + "dependencies": { + "@vue/compiler-core": "3.5.16", + "@vue/shared": "3.5.16" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.16.tgz", + "integrity": "sha512-rQR6VSFNpiinDy/DVUE0vHoIDUF++6p910cgcZoaAUm3POxgNOOdS/xgoll3rNdKYTYPnnbARDCZOyZ+QSe6Pw==", + "dependencies": { + "@babel/parser": "^7.27.2", + "@vue/compiler-core": "3.5.16", + "@vue/compiler-dom": "3.5.16", + "@vue/compiler-ssr": "3.5.16", + "@vue/shared": "3.5.16", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.17", + "postcss": "^8.5.3", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.16.tgz", + "integrity": "sha512-d2V7kfxbdsjrDSGlJE7my1ZzCXViEcqN6w14DOsDrUCHEA6vbnVCpRFfrc4ryCP/lCKzX2eS1YtnLE/BuC9f/A==", + "dependencies": { + "@vue/compiler-dom": "3.5.16", + "@vue/shared": "3.5.16" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.16.tgz", + "integrity": "sha512-FG5Q5ee/kxhIm1p2bykPpPwqiUBV3kFySsHEQha5BJvjXdZTUfmya7wP7zC39dFuZAcf/PD5S4Lni55vGLMhvA==", + "dependencies": { + "@vue/shared": "3.5.16" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.16.tgz", + "integrity": "sha512-bw5Ykq6+JFHYxrQa7Tjr+VSzw7Dj4ldR/udyBZbq73fCdJmyy5MPIFR9IX/M5Qs+TtTjuyUTCnmK3lWWwpAcFQ==", + "dependencies": { + "@vue/reactivity": "3.5.16", + "@vue/shared": "3.5.16" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.16.tgz", + "integrity": "sha512-T1qqYJsG2xMGhImRUV9y/RseB9d0eCYZQ4CWca9ztCuiPj/XWNNN+lkNBuzVbia5z4/cgxdL28NoQCvC0Xcfww==", + "dependencies": { + "@vue/reactivity": "3.5.16", + "@vue/runtime-core": "3.5.16", + "@vue/shared": "3.5.16", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.16.tgz", + "integrity": "sha512-BrX0qLiv/WugguGsnQUJiYOE0Fe5mZTwi6b7X/ybGB0vfrPH9z0gD/Y6WOR1sGCgX4gc25L1RYS5eYQKDMoNIg==", + "dependencies": { + "@vue/compiler-ssr": "3.5.16", + "@vue/shared": "3.5.16" + }, + "peerDependencies": { + "vue": "3.5.16" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.16.tgz", + "integrity": "sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg==" + }, + "node_modules/@wdio/cli": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-9.2.6.tgz", + "integrity": "sha512-+Vn2aLwx4M/LZ521yhkDgEXhTOrf8Gg1byZ+0pckWYaB3OmkZ+BGMFEn3a7n781Zcxhq7P/K2BY1lql50axW5g==", + "dev": true, + "dependencies": { + "@types/node": "^20.1.1", + "@vitest/snapshot": "^2.1.1", + "@wdio/config": "9.2.5", + "@wdio/globals": "9.2.6", + "@wdio/logger": "9.1.3", + "@wdio/protocols": "9.2.2", + "@wdio/types": "9.2.2", + "@wdio/utils": "9.2.5", + "async-exit-hook": "^2.0.1", + "chalk": "^5.2.0", + "chokidar": "^4.0.0", + "cli-spinners": "^3.0.0", + "dotenv": "^16.3.1", + "ejs": "^3.1.9", + "execa": "^9.2.0", + "import-meta-resolve": "^4.0.0", + "inquirer": "^11.0.1", + "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", + "tsx": "^4.7.2", + "webdriverio": "9.2.6", + "yargs": "^17.7.2" + }, + "bin": { + "wdio": "bin/wdio.js" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/cli/node_modules/@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@wdio/cli/node_modules/@wdio/repl": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.0.8.tgz", + "integrity": "sha512-3iubjl4JX5zD21aFxZwQghqC3lgu+mSs8c3NaiYYNCC+IT5cI/8QuKlgh9s59bu+N3gG988jqMJeCYlKuUv/iw==", + "dev": true, + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/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==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@wdio/cli/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/@wdio/cli/node_modules/chromium-bidi": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.3.tgz", + "integrity": "sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/@wdio/cli/node_modules/devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@wdio/cli/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@wdio/cli/node_modules/puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@wdio/cli/node_modules/webdriver": { + "version": "9.2.5", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.2.5.tgz", + "integrity": "sha512-7rmK6oD3oYq+6E0qa9FQ1/67Ajf2APTOghmqlcDnSBTnTGF51UPOWnzgy0x3yGozmCTVRe13O75JXRRWpvxOvA==", + "dev": true, + "dependencies": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.2.5", + "@wdio/logger": "9.1.3", + "@wdio/protocols": "9.2.2", + "@wdio/types": "9.2.2", + "@wdio/utils": "9.2.5", + "deepmerge-ts": "^7.0.3", + "ws": "^8.8.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/cli/node_modules/webdriverio": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.2.6.tgz", + "integrity": "sha512-GjQykwwYBXwqWAs0CUCU0Hg9nR5DonOwFkWQUcUpCfBVSTLdeH0fBEDikKVf7ohuDOUSOGo0wM9LN8ZTMroMXA==", + "dev": true, + "dependencies": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.2.5", + "@wdio/logger": "9.1.3", + "@wdio/protocols": "9.2.2", + "@wdio/repl": "9.0.8", + "@wdio/types": "9.2.2", + "@wdio/utils": "9.2.5", + "archiver": "^7.0.1", + "aria-query": "^5.3.0", + "cheerio": "^1.0.0-rc.12", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.3.0", + "import-meta-resolve": "^4.0.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "minimatch": "^9.0.3", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.3", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.2.5" + }, + "engines": { + "node": ">=18.20.0" + }, + "peerDependencies": { + "puppeteer-core": "^22.3.0" + }, + "peerDependenciesMeta": { + "puppeteer-core": { + "optional": true + } + } + }, + "node_modules/@wdio/config": { + "version": "9.2.5", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.2.5.tgz", + "integrity": "sha512-gqblHShjriGH3va2nSnQ2wktwargHK2oEDepNPG1LMPB5uWd997f+zyaR4s4vtqqUkdxwciIA1H4rDC3fIlesw==", + "dev": true, + "dependencies": { + "@wdio/logger": "9.1.3", + "@wdio/types": "9.2.2", + "@wdio/utils": "9.2.5", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/config/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/@wdio/config/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@wdio/config/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@wdio/globals": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-9.2.6.tgz", + "integrity": "sha512-aQqunfSZYqY2oI2YHf9qy9mab0C5Mk9Y904WIZAcEcMA1oojkutRp39btCEMY+twRJjswswWYFoBncTQY8vFDA==", + "dev": true, + "engines": { + "node": ">=18.20.0" + }, + "optionalDependencies": { + "expect-webdriverio": "^5.0.1", + "webdriverio": "9.2.6" + } + }, + "node_modules/@wdio/globals/node_modules/@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@wdio/globals/node_modules/@wdio/repl": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.0.8.tgz", + "integrity": "sha512-3iubjl4JX5zD21aFxZwQghqC3lgu+mSs8c3NaiYYNCC+IT5cI/8QuKlgh9s59bu+N3gG988jqMJeCYlKuUv/iw==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/globals/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/@wdio/globals/node_modules/chromium-bidi": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.3.tgz", + "integrity": "sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/@wdio/globals/node_modules/devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@wdio/globals/node_modules/expect-webdriverio": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/expect-webdriverio/-/expect-webdriverio-5.0.3.tgz", + "integrity": "sha512-0RHsFZX1856qCWZsXcvacFZpdZc7UAVD9wAglzf3KMWO1AoXt5EorjsNp1H9StGysxhJuVXJxRWKeXnD4LKtjQ==", + "dev": true, + "optional": true, + "dependencies": { + "@vitest/snapshot": "^2.0.5", + "expect": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "lodash.isequal": "^4.5.0" + }, + "engines": { + "node": ">=18 || >=20 || >=22" + }, + "peerDependencies": { + "@wdio/globals": "^9.0.0", + "@wdio/logger": "^9.0.0", + "webdriverio": "^9.0.0" + }, + "peerDependenciesMeta": { + "@wdio/globals": { + "optional": false + }, + "@wdio/logger": { + "optional": false + }, + "webdriverio": { + "optional": false + } + } + }, + "node_modules/@wdio/globals/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@wdio/globals/node_modules/puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@wdio/globals/node_modules/webdriver": { + "version": "9.2.5", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.2.5.tgz", + "integrity": "sha512-7rmK6oD3oYq+6E0qa9FQ1/67Ajf2APTOghmqlcDnSBTnTGF51UPOWnzgy0x3yGozmCTVRe13O75JXRRWpvxOvA==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.2.5", + "@wdio/logger": "9.1.3", + "@wdio/protocols": "9.2.2", + "@wdio/types": "9.2.2", + "@wdio/utils": "9.2.5", + "deepmerge-ts": "^7.0.3", + "ws": "^8.8.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/globals/node_modules/webdriverio": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.2.6.tgz", + "integrity": "sha512-GjQykwwYBXwqWAs0CUCU0Hg9nR5DonOwFkWQUcUpCfBVSTLdeH0fBEDikKVf7ohuDOUSOGo0wM9LN8ZTMroMXA==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.2.5", + "@wdio/logger": "9.1.3", + "@wdio/protocols": "9.2.2", + "@wdio/repl": "9.0.8", + "@wdio/types": "9.2.2", + "@wdio/utils": "9.2.5", + "archiver": "^7.0.1", + "aria-query": "^5.3.0", + "cheerio": "^1.0.0-rc.12", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.3.0", + "import-meta-resolve": "^4.0.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "minimatch": "^9.0.3", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.3", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.2.5" + }, + "engines": { + "node": ">=18.20.0" + }, + "peerDependencies": { + "puppeteer-core": "^22.3.0" + }, + "peerDependenciesMeta": { + "puppeteer-core": { + "optional": true + } + } + }, + "node_modules/@wdio/local-runner": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.40.6.tgz", + "integrity": "sha512-h2OvQ6kODNiEaM6JzYzTzPl4n9TiqM2KsQ+8x3QoMIHY7Z9JSc+cH9QcnNsm3Sqxk/LVYFGetza6/WTsHx1/zA==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0", + "@wdio/logger": "8.38.0", + "@wdio/repl": "8.40.3", + "@wdio/runner": "8.40.6", + "@wdio/types": "8.40.6", + "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": "22.8.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz", + "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@wdio/local-runner/node_modules/@wdio/logger": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", + "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", + "dev": true, + "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/local-runner/node_modules/@wdio/types": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.40.6.tgz", + "integrity": "sha512-ALftLri1BdsRuPrQkuW3evBNdOA5n4IkuoegOw6UE2z+R0f1YI5fHGSHNRWLnhtbOECbGyHXXqzbSxCEb+o+MA==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/local-runner/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/@wdio/logger": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.1.3.tgz", + "integrity": "sha512-cumRMK/gE1uedBUw3WmWXOQ7HtB6DR8EyKQioUz2P0IJtRRpglMBdZV7Svr3b++WWawOuzZHMfbTkJQmaVt8Gw==", + "dev": true, + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/logger/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/@wdio/mocha-framework": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.40.6.tgz", + "integrity": "sha512-LE7UrUHb3mNCsCdrteh3JbKqjOp6nW6b5K+BlSr1Aa/hygcauRez4NeXV+Fs0FGct0XxAWouovP5jNUroeyoKA==", + "dev": true, + "dependencies": { + "@types/mocha": "^10.0.0", + "@types/node": "^22.2.0", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.40.6", + "@wdio/utils": "8.40.6", + "mocha": "^10.0.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/mocha-framework/node_modules/@puppeteer/browsers": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.9.1.tgz", + "integrity": "sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==", + "dev": true, + "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/@wdio/mocha-framework/node_modules/@types/node": { + "version": "22.8.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz", + "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@wdio/mocha-framework/node_modules/@wdio/logger": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", + "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", + "dev": true, + "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/node_modules/@wdio/types": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.40.6.tgz", + "integrity": "sha512-ALftLri1BdsRuPrQkuW3evBNdOA5n4IkuoegOw6UE2z+R0f1YI5fHGSHNRWLnhtbOECbGyHXXqzbSxCEb+o+MA==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/mocha-framework/node_modules/@wdio/utils": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.40.6.tgz", + "integrity": "sha512-+TWfV6h+4f8gs7QiYUAWbWEylpZudQ+xkJPN34tRzPJK6dOBYEnIT/j6+1m3j39m1WPDehyYxIf1wCsrGKBxNQ==", + "dev": true, + "dependencies": { + "@puppeteer/browsers": "^1.6.0", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.40.6", + "decamelize": "^6.0.0", + "deepmerge-ts": "^5.1.0", + "edgedriver": "^5.5.0", + "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/@wdio/mocha-framework/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/@wdio/mocha-framework/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/@wdio/mocha-framework/node_modules/deepmerge-ts": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz", + "integrity": "sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==", + "dev": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@wdio/mocha-framework/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==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@wdio/mocha-framework/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/@wdio/mocha-framework/node_modules/proxy-agent": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", + "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", + "dev": true, + "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/@wdio/mocha-framework/node_modules/tar-fs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", + "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "dev": true, + "dependencies": { + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "node_modules/@wdio/protocols": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.2.2.tgz", + "integrity": "sha512-0GMUSHCbYm+J+rnRU6XPtaUgVCRICsiH6W5zCXpePm3wLlbmg/mvZ+4OnNErssbpIOulZuAmC2jNmut2AEfWSw==", + "dev": true + }, + "node_modules/@wdio/repl": { + "version": "8.40.3", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-8.40.3.tgz", + "integrity": "sha512-mWEiBbaC7CgxvSd2/ozpbZWebnRIc8KRu/J81Hlw/txUWio27S7IpXBlZGVvhEsNzq0+cuxB/8gDkkXvMPbesw==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/repl/node_modules/@types/node": { + "version": "22.8.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz", + "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@wdio/reporter": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.40.6.tgz", + "integrity": "sha512-DkmnwWLyz/CTkWQP9MWKYT5LDLKQxC1fIXZf6pZ1pKIhrxP39f5Rhhu6s+Ma3PhPbhyW/dqK3+nrtd+eD5nQOQ==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.40.6", + "diff": "^7.0.0", + "object-inspect": "^1.12.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/reporter/node_modules/@types/node": { + "version": "22.8.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz", + "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@wdio/reporter/node_modules/@wdio/logger": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", + "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", + "dev": true, + "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/reporter/node_modules/@wdio/types": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.40.6.tgz", + "integrity": "sha512-ALftLri1BdsRuPrQkuW3evBNdOA5n4IkuoegOw6UE2z+R0f1YI5fHGSHNRWLnhtbOECbGyHXXqzbSxCEb+o+MA==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/reporter/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/@wdio/runner": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.40.6.tgz", + "integrity": "sha512-dScN5mLia3YYctEgrbO4JG7pEttICfNENFd08t1T/hNJAWyzSbG/wvAL5k0HUGqp6Ukly6NUmhVsP5PVBz4ooA==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0", + "@wdio/config": "8.40.6", + "@wdio/globals": "8.40.6", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.40.6", + "@wdio/utils": "8.40.6", + "deepmerge-ts": "^5.1.0", + "expect-webdriverio": "^4.12.0", + "gaze": "^1.1.3", + "webdriver": "8.40.6", + "webdriverio": "8.40.6" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/runner/node_modules/@puppeteer/browsers": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.9.1.tgz", + "integrity": "sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==", + "dev": true, + "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/@wdio/runner/node_modules/@types/node": { + "version": "22.8.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz", + "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@wdio/runner/node_modules/@wdio/config": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.40.6.tgz", + "integrity": "sha512-rHCSmrhdJf7FlidcQPDvRKRPLYjklbrdxQa6J20BxHifTO4h2v23Wrq4OqqYIcq23gf9LpZvCA/PAMiET/QdVg==", + "dev": true, + "dependencies": { + "@wdio/logger": "8.38.0", + "@wdio/types": "8.40.6", + "@wdio/utils": "8.40.6", + "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/runner/node_modules/@wdio/globals": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.40.6.tgz", + "integrity": "sha512-37llSQk4ngM6wzn8diBQ1Xik2ZZtUCU29Hr7NbfyfXahQdzIApRxNtw0B2J5XrjGX9yuO6gdyg5Kj+UuaKIo7A==", + "dev": true, + "engines": { + "node": "^16.13 || >=18" + }, + "optionalDependencies": { + "expect-webdriverio": "^4.11.2", + "webdriverio": "8.40.6" + } + }, + "node_modules/@wdio/runner/node_modules/@wdio/logger": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", + "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", + "dev": true, + "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/runner/node_modules/@wdio/types": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.40.6.tgz", + "integrity": "sha512-ALftLri1BdsRuPrQkuW3evBNdOA5n4IkuoegOw6UE2z+R0f1YI5fHGSHNRWLnhtbOECbGyHXXqzbSxCEb+o+MA==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/runner/node_modules/@wdio/utils": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.40.6.tgz", + "integrity": "sha512-+TWfV6h+4f8gs7QiYUAWbWEylpZudQ+xkJPN34tRzPJK6dOBYEnIT/j6+1m3j39m1WPDehyYxIf1wCsrGKBxNQ==", + "dev": true, + "dependencies": { + "@puppeteer/browsers": "^1.6.0", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.40.6", + "decamelize": "^6.0.0", + "deepmerge-ts": "^5.1.0", + "edgedriver": "^5.5.0", + "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/@wdio/runner/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/@wdio/runner/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/@wdio/runner/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/@wdio/runner/node_modules/deepmerge-ts": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz", + "integrity": "sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==", + "dev": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@wdio/runner/node_modules/expect-webdriverio": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/expect-webdriverio/-/expect-webdriverio-4.15.4.tgz", + "integrity": "sha512-Op1xZoevlv1pohCq7g2Og5Gr3xP2NhY7MQueOApmopVxgweoJ/BqJxyvMNP0A//QsMg8v0WsN/1j81Sx2er9Wg==", + "dev": true, + "dependencies": { + "@vitest/snapshot": "^2.0.3", + "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/@wdio/runner/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@wdio/runner/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==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@wdio/runner/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@wdio/runner/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/@wdio/runner/node_modules/proxy-agent": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", + "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", + "dev": true, + "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/@wdio/runner/node_modules/tar-fs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", + "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "dev": true, + "dependencies": { + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "node_modules/@wdio/spec-reporter": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.40.6.tgz", + "integrity": "sha512-mr7cJl8rdyk4DimttPBPH//PCxTxmz/B5t6aFHYT5ds/oGbQKMh4lKCypXZnwjnMclaAa6xPS2MeF+2PwQbwUw==", + "dev": true, + "dependencies": { + "@wdio/reporter": "8.40.6", + "@wdio/types": "8.40.6", + "chalk": "^5.1.2", + "easy-table": "^1.2.0", + "pretty-ms": "^7.0.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/spec-reporter/node_modules/@types/node": { + "version": "22.8.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz", + "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@wdio/spec-reporter/node_modules/@wdio/types": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.40.6.tgz", + "integrity": "sha512-ALftLri1BdsRuPrQkuW3evBNdOA5n4IkuoegOw6UE2z+R0f1YI5fHGSHNRWLnhtbOECbGyHXXqzbSxCEb+o+MA==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/spec-reporter/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/@wdio/types": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.2.2.tgz", + "integrity": "sha512-nHZ9Ne9iRQFJ1TOYKUn4Fza69IshTTzk6RYmSZ51ImGs9uMZu0+S0Jm9REdly+VLN3FzxG6g2QSe0/F3uNVPdw==", + "dev": true, + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/utils": { + "version": "9.2.5", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.2.5.tgz", + "integrity": "sha512-QgBxscPVuqC/fP62ggssBfWLnonVs2/r1xaj5MgH+tLNez/OVS98pWx0/FYsFHVeTVO5vPnJIhoTo2CXz8tQzA==", + "dev": true, + "dependencies": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.1.3", + "@wdio/types": "9.2.2", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^5.6.1", + "geckodriver": "^4.3.3", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^0.1.2", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "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.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "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.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" + } + }, + "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.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@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.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@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.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "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/disposablestack": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz", + "integrity": "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==", + "dependencies": { + "@whatwg-node/promise-helpers": "^1.0.0", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@whatwg-node/fetch": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.10.8.tgz", + "integrity": "sha512-Rw9z3ctmeEj8QIB9MavkNJqekiu9usBCSMZa+uuAvM0lF3v70oQVCXNppMIqaV6OTZbdaHF1M2HLow58DEw+wg==", + "dependencies": { + "@whatwg-node/node-fetch": "^0.7.21", + "urlpattern-polyfill": "^10.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@whatwg-node/node-fetch": { + "version": "0.7.21", + "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.7.21.tgz", + "integrity": "sha512-QC16IdsEyIW7kZd77aodrMO7zAoDyyqRCTLg+qG4wqtP4JV9AA+p7/lgqMdD29XyiYdVvIdFrfI9yh7B1QvRvw==", + "dependencies": { + "@fastify/busboy": "^3.1.1", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/promise-helpers": "^1.3.2", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@whatwg-node/node-fetch/node_modules/@fastify/busboy": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz", + "integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==" + }, + "node_modules/@whatwg-node/promise-helpers": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@whatwg-node/promise-helpers/-/promise-helpers-1.3.2.tgz", + "integrity": "sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==", + "dependencies": { + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@xhmikosr/archive-type": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/archive-type/-/archive-type-7.0.0.tgz", + "integrity": "sha512-sIm84ZneCOJuiy3PpWR5bxkx3HaNt1pqaN+vncUBZIlPZCq8ASZH+hBVdu5H8znR7qYC6sKwx+ie2Q7qztJTxA==", + "dev": true, + "dependencies": { + "file-type": "^19.0.0" + }, + "engines": { + "node": "^14.14.0 || >=16.0.0" + } + }, + "node_modules/@xhmikosr/decompress": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress/-/decompress-10.0.1.tgz", + "integrity": "sha512-6uHnEEt5jv9ro0CDzqWlFgPycdE+H+kbJnwyxgZregIMLQ7unQSCNVsYG255FoqU8cP46DyggI7F7LohzEl8Ag==", + "dev": true, + "dependencies": { + "@xhmikosr/decompress-tar": "^8.0.1", + "@xhmikosr/decompress-tarbz2": "^8.0.1", + "@xhmikosr/decompress-targz": "^8.0.1", + "@xhmikosr/decompress-unzip": "^7.0.0", + "graceful-fs": "^4.2.11", + "make-dir": "^4.0.0", + "strip-dirs": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-tar": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-tar/-/decompress-tar-8.0.1.tgz", + "integrity": "sha512-dpEgs0cQKJ2xpIaGSO0hrzz3Kt8TQHYdizHsgDtLorWajuHJqxzot9Hbi0huRxJuAGG2qiHSQkwyvHHQtlE+fg==", + "dev": true, + "dependencies": { + "file-type": "^19.0.0", + "is-stream": "^2.0.1", + "tar-stream": "^3.1.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-tar/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/@xhmikosr/decompress-tarbz2": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-tarbz2/-/decompress-tarbz2-8.0.1.tgz", + "integrity": "sha512-OF+6DysDZP5YTDO8uHuGG6fMGZjc+HszFPBkVltjoje2Cf60hjBg/YP5OQndW1hfwVWOdP7f3CnJiPZHJUTtEg==", + "dev": true, + "dependencies": { + "@xhmikosr/decompress-tar": "^8.0.1", + "file-type": "^19.0.0", + "is-stream": "^2.0.1", + "seek-bzip": "^2.0.0", + "unbzip2-stream": "^1.4.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-tarbz2/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/@xhmikosr/decompress-targz": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-targz/-/decompress-targz-8.0.1.tgz", + "integrity": "sha512-mvy5AIDIZjQ2IagMI/wvauEiSNHhu/g65qpdM4EVoYHUJBAmkQWqcPJa8Xzi1aKVTmOA5xLJeDk7dqSjlHq8Mg==", + "dev": true, + "dependencies": { + "@xhmikosr/decompress-tar": "^8.0.1", + "file-type": "^19.0.0", + "is-stream": "^2.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-targz/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/@xhmikosr/decompress-unzip": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@xhmikosr/decompress-unzip/-/decompress-unzip-7.0.0.tgz", + "integrity": "sha512-GQMpzIpWTsNr6UZbISawsGI0hJ4KA/mz5nFq+cEoPs12UybAqZWKbyIaZZyLbJebKl5FkLpsGBkrplJdjvUoSQ==", + "dev": true, + "dependencies": { + "file-type": "^19.0.0", + "get-stream": "^6.0.1", + "yauzl": "^3.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/decompress-unzip/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/@xhmikosr/decompress-unzip/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/@xhmikosr/decompress-unzip/node_modules/yauzl": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.1.3.tgz", + "integrity": "sha512-JCCdmlJJWv7L0q/KylOekyRaUrdEoUxWkWVcgorosTROCFWiS9p2NNPE9Yb91ak7b1N5SxAZEliWpspbZccivw==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@xhmikosr/downloader": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@xhmikosr/downloader/-/downloader-15.0.1.tgz", + "integrity": "sha512-fiuFHf3Dt6pkX8HQrVBsK0uXtkgkVlhrZEh8b7VgoDqFf+zrgFBPyrwCqE/3nDwn3hLeNz+BsrS7q3mu13Lp1g==", + "dev": true, + "dependencies": { + "@xhmikosr/archive-type": "^7.0.0", + "@xhmikosr/decompress": "^10.0.1", + "content-disposition": "^0.5.4", + "defaults": "^3.0.0", + "ext-name": "^5.0.0", + "file-type": "^19.0.0", + "filenamify": "^6.0.0", + "get-stream": "^6.0.1", + "got": "^13.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@xhmikosr/downloader/node_modules/defaults": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-3.0.0.tgz", + "integrity": "sha512-RsqXDEAALjfRTro+IFNKpcPCt0/Cy2FqHSIlnomiJp9YGadpQnrtbRpSgN2+np21qHcIKiva4fiOQGjS9/qR/A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@xhmikosr/downloader/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/@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/@zip.js/zip.js": { + "version": "2.7.53", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.53.tgz", + "integrity": "sha512-G6Bl5wN9EXXVaTUIox71vIX5Z454zEBe+akKpV4m1tUboIctT5h7ID3QXCJd/Lfy2rSvmkTmZIucf1jGRR4f5A==", + "dev": true, + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=16.5.0" + } + }, + "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==", + "dev": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "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/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/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/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-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true + }, + "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==", + "dev": true + }, + "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/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-escapes/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/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/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/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "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/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "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==", + "dev": true, + "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/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/archiver-utils/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "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/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/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==", + "dev": true, + "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/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/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/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "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.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "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.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/avvio": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.4.0.tgz", + "integrity": "sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==", + "dev": true, + "dependencies": { + "@fastify/error": "^3.3.0", + "fastq": "^1.17.1" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "engines": { + "node": ">= 0.4" + } + }, + "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.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true + }, + "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/bare-events": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", + "dev": true, + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz", + "integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==", + "dev": true, + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^2.0.0" + } + }, + "node_modules/bare-os": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz", + "integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==", + "dev": true, + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "dev": true, + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.3.2.tgz", + "integrity": "sha512-EFZHSIBkDgSHIwj2l2QZfP4U5OcD4xFAOwhSb/vlr9PIqyGJGvB/nfClJbcnh3EY4jtPE4zsb5ztae96bVF79A==", + "dev": true, + "optional": true, + "dependencies": { + "streamx": "^2.20.0" + } + }, + "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==", + "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" + } + ] + }, + "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==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "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.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/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==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.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.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "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.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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/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==", + "dev": true, + "engines": { + "node": ">=8.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/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/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "dev": true, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "dev": true, + "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/cacheable-request/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/cacheable-request/node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "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.30001676", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001676.tgz", + "integrity": "sha512-Qz6zwGCiPghQXGJvgQAem79esjitvJ+CxSbSQkW9H/UX5hg8XM88d4lp2W+MEQ81j+Hip58Il+jGVdazk1z9cw==", + "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==", + "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/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/chromium-bidi": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.5.8.tgz", + "integrity": "sha512-blqh+1cEQbHBKmok3rVJkBlBxt9beKBgOsxbFgs7UJcoVbbeZ+K7+6liAsjgpc8l1Xd55cQUy14fXZdGSb4zIw==", + "dev": true, + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "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" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "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/cli-spinners": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.2.0.tgz", + "integrity": "sha512-pXftdQloMZzjCr3pCTIRniDcys6dDzgpgVhAHHk6TKBDbRuP1MkuetTF5KSv4YUutbOPa7+7ZrAJ2kVtbMqyXA==", + "dev": true, + "engines": { + "node": ">=18.20" + }, + "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/clipboardy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", + "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", + "dev": true, + "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", + "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/clipboardy/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/clipboardy/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/clipboardy/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/clipboardy/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/clipboardy/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/clipboardy/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/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/cliui/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/cliui/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/cliui/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/cliui/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/cliui/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/cliui/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/cliui/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/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "optional": 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/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "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", + "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/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/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/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/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "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/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/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==", + "dev": true, + "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/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/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==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "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==" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "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/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/copy-webpack-plugin/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/copy-webpack-plugin/node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true + }, + "node_modules/copy-webpack-plugin/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/copy-webpack-plugin/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/copy-webpack-plugin/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/copy-webpack-plugin/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/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/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "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==", + "dev": true, + "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/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/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dev": true, + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/cross-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==", + "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/cross-inspect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.1.tgz", + "integrity": "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.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, + "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", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "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.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "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.2", + "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.2.tgz", + "integrity": "sha512-C2AugXIpRGQTxaCW0N7n5jD/p5irUmCrwl03TrnMFBHDbdq44CFWR2zO7rK9xPN4Eo3pUxC4vQzQgbIpzrD1PQ==", + "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", + "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz", + "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==", + "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", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dataloader": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", + "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==" + }, + "node_modules/debounce-promise": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz", + "integrity": "sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg==" + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", + "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "devOptional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", + "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/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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.3.tgz", + "integrity": "sha512-qCSH6I0INPxd9Y1VtAiLpnYvz5O//6rCfJXKk0z66Up9/VOSr+1yS8XSKA5IWRxjocFGlzPyaZYe+jxq7OOLtQ==", + "dev": true, + "engines": { + "node": ">=16.0.0" + } + }, + "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, + "optional": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "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-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "engines": { + "node": ">=8" + } + }, + "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/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "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.1359167", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1359167.tgz", + "integrity": "sha512-f/9PeTaSH3weS/WAwrQb5/s9R3KMOeTGe+Jkhg5952yInub7iDPjdlzRdrDgpLZfxHbTrBuG9aUkAMM+ocVkXQ==", + "dev": true + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "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, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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==", + "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/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.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "engines": { + "node": ">=4" + } + }, + "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-table": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.2.0.tgz", + "integrity": "sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "optionalDependencies": { + "wcwidth": "^1.0.1" + } + }, + "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/edge-paths": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", + "integrity": "sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/edge-paths/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/edgedriver": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-5.6.1.tgz", + "integrity": "sha512-3Ve9cd5ziLByUdigw6zovVeWJjVs8QHVmqOB0sJ0WNeVPcwf4p18GnxMmVvlFmYRloUwf5suNuorea4QzwBIOA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@wdio/logger": "^8.38.0", + "@zip.js/zip.js": "^2.7.48", + "decamelize": "^6.0.0", + "edge-paths": "^3.0.5", + "fast-xml-parser": "^4.4.1", + "node-fetch": "^3.3.2", + "which": "^4.0.0" + }, + "bin": { + "edgedriver": "bin/edgedriver.js" + } + }, + "node_modules/edgedriver/node_modules/@wdio/logger": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", + "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", + "dev": true, + "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/edgedriver/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/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.5.50", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.50.tgz", + "integrity": "sha512-eMVObiUQ2LdgeO1F/ySTXsvqvxb6ZH2zPGaMYsWzRDdOddUa77tdmI0ltg+L16UpbWdhPmuF3wIQYyQq65WfZw==" + }, + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "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/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "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==", + "devOptional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "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.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "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", + "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-iterator-helpers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz", + "integrity": "sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.4", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.3", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "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/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "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==", + "dev": true + }, + "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/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "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.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "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.1", + "@humanwhocodes/config-array": "^0.13.0", + "@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.37.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", + "integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.1.0", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "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/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": "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-scope/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/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/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/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/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/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/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/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/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==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "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/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", + "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/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==", + "dev": true, + "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/execa": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.1.tgz", + "integrity": "sha512-QY5PPtSonnGwhhHDNI7+3RvY285c7iuJFFB+lU+oEzMY/gEGJ808owqJsrr8Otd1E/x07po1LkUBmdAc5duPAg==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/pretty-ms": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.1.0.tgz", + "integrity": "sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==", + "dev": true, + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exenv-es6": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exenv-es6/-/exenv-es6-1.1.1.tgz", + "integrity": "sha512-vlVu3N8d6yEMpMsEm+7sUBAI81aqYYuEvfK0jNqmdb/OPXzzH7QWDDnVjMvDSY47JdHEqx/dfC/q8WkfoTmpGQ==" + }, + "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", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": 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/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" + }, + "node_modules/ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dev": true, + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dev": true, + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "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/external-editor/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==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "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/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/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==", + "dev": true + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "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==", + "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==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "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==", + "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-json-stringify": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz", + "integrity": "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==", + "dev": true, + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^3.0.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.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/fast-json-stringify/node_modules/ajv/node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true + }, + "node_modules/fast-json-stringify/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/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/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dev": true, + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-uri": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", + "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==", + "dev": true + }, + "node_modules/fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastify": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.28.1.tgz", + "integrity": "sha512-kFWUtpNr4i7t5vY2EJPCN2KgMVpuqfU4NjnJNCgiNB900oiDeYqaNDRcAfeBbOF5hGixixxcKnOU4KN9z6QncQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "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.3.0", + "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": "^9.0.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", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "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/file-type": { + "version": "19.6.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-19.6.0.tgz", + "integrity": "sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==", + "dev": true, + "dependencies": { + "get-stream": "^9.0.1", + "strtok3": "^9.0.1", + "token-types": "^6.0.0", + "uint8array-extras": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/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/filelist/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, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filename-reserved-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", + "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/filenamify": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz", + "integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==", + "dev": true, + "dependencies": { + "filename-reserved-regex": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-my-way": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz", + "integrity": "sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^3.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "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": { + "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.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "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.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "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/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "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/fork-ts-checker-webpack-plugin": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.3.0.tgz", + "integrity": "sha512-IN+XTzusCjR5VgntYFgxbxVx3WraPRnKehBFrf00cMSrtUuW9MsG9dhL6MWpY6MkjC3wVwoujfCDgZZCQwbswA==", + "dev": true, + "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", + "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/fork-ts-checker-webpack-plugin/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/fork-ts-checker-webpack-plugin/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "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" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/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/fork-ts-checker-webpack-plugin/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/fork-ts-checker-webpack-plugin/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/fork-ts-checker-webpack-plugin/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/fork-ts-checker-webpack-plugin/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/fork-ts-checker-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/fork-ts-checker-webpack-plugin/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/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "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", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "dev": true, + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "optional": 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==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "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==" + }, + "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/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/gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "dependencies": { + "globule": "^1.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/geckodriver": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.5.1.tgz", + "integrity": "sha512-lGCRqPMuzbRNDWJOQcUqhNqPvNsIFu6yzXF8J/6K3WCYFd2r5ckbeF7h1cxsnjA7YLSEiWzERCt6/gjZ3tW0ug==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@wdio/logger": "^9.0.0", + "@zip.js/zip.js": "^2.7.48", + "decamelize": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^3.3.2", + "tar-fs": "^3.0.6", + "which": "^4.0.0" + }, + "bin": { + "geckodriver": "bin/geckodriver.js" + }, + "engines": { + "node": "^16.13 || >=18 || >=20" + } + }, + "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", + "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.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.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "dev": true, + "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/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==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "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==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "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", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "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": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "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==", + "dev": true + }, + "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==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/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, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "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/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "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==", + "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", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", + "integrity": "sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "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/globule/node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/got": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", + "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", + "dev": true, + "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": ">=16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/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/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/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/graphql": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", + "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", + "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.4.0", + "resolved": "file:graphql-language-service-5.4.0.tgz", + "integrity": "sha512-nseYyJbv9e+8oI5YYvMprFeV3halSBq35E93M8goF8fziuyf+lZoTIgQN6DXKYBu8PRiK25zc+6vDKXlqOPOGA==", + "dependencies": { + "debounce-promise": "^3.1.2", + "nullthrows": "^1.0.0", + "vscode-languageserver-types": "^3.17.1" + }, + "bin": { + "graphql": "dist/temp-bin.js" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-language-service-server": { + "version": "2.14.8", + "resolved": "file:graphql-language-service-server-2.14.8.tgz", + "integrity": "sha512-5ikSF9EVc3lBTkOWq9IosrAtFfmOMg7zavlE7o2nw6/uDilHcuZkXwUP0T6/IqKzTHxRTJsQfC9x2J7hMcNFaA==", + "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": "./graphql-language-service-5.4.0.tgz", + "lru-cache": "^10.2.0", + "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.2.19", + "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", + "vue": "^3.2.0" + }, + "peerDependencies": { + "graphql": "^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==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "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.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/graphql-ws": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.5.tgz", + "integrity": "sha512-HzYw057ch0hx2gZjkbgk1pur4kAtgljlWRP+Gccudqm3BRrTpExjWCQ9OHdIsq47Y6lHL++1lTvuQHhgRRcevw==", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@fastify/websocket": "^10 || ^11", + "crossws": "~0.3", + "graphql": "^15.10.1 || ^16", + "uWebSockets.js": "^20", + "ws": "^8" + }, + "peerDependenciesMeta": { + "@fastify/websocket": { + "optional": true + }, + "crossws": { + "optional": true + }, + "uWebSockets.js": { + "optional": true + }, + "ws": { + "optional": 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": "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/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.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "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-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/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/hosted-git-info/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/hosted-git-info/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/hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/htmlfy": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.3.2.tgz", + "integrity": "sha512-FsxzfpeDYRqn1emox9VpxMPfGjADoUmmup8D604q497R0VNxiXs4ZZTN2QzkaMA5C9aHGUoe1iQRVSm+HK9xuA==", + "dev": true + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "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.1.0", + "entities": "^4.5.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-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "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": "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/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "dev": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz", + "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "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==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "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, + "license": "ISC", + "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==", + "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" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true + }, + "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==", + "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==", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": 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/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "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": "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==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "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==", + "optional": true + }, + "node_modules/inquirer": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-11.1.0.tgz", + "integrity": "sha512-CmLAZT65GG/v30c+D2Fk8+ceP6pxD6RL+hIUOWAltCmeyEqWYwqu9v76q03OvjyZ3AB0C1Ala2stn1z/rMqGEw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/prompts": "^6.0.1", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "ansi-escapes": "^4.3.2", + "mute-stream": "^1.0.0", + "run-async": "^3.0.0", + "rxjs": "^7.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/inspect-with-kind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/inspect-with-kind/-/inspect-with-kind-1.0.5.tgz", + "integrity": "sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + } + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "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-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-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.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "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": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "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==", + "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-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-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==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "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==", + "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-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/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-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "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.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "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.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "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==", + "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/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/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/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/iterator.prototype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", + "integrity": "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==", + "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" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "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", + "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/jake/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/jake/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/jake/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/jake/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/jake/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/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, + "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", + "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/jest-diff/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/jest-diff/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/jest-diff/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/jest-diff/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-diff/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/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, + "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, + "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", + "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/jest-matcher-utils/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/jest-matcher-utils/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/jest-matcher-utils/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/jest-matcher-utils/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-matcher-utils/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/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, + "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", + "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/jest-message-util/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/jest-message-util/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/jest-message-util/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/jest-message-util/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-message-util/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/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, + "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", + "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/jest-util/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/jest-util/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/jest-util/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/jest-util/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-util/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/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.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "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==" + }, + "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==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "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==" + }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "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": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==" + }, + "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/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/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/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "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/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", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.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/ky": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.3.tgz", + "integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "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", + "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/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/light-my-request": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.14.0.tgz", + "integrity": "sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==", + "dev": true, + "dependencies": { + "cookie": "^0.7.0", + "process-warning": "^3.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "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==" + }, + "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/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": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "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", + "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/locate-app": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.5.0.tgz", + "integrity": "sha512-xIqbzPMBYArJRmPGUZD9CzV9wOqmVtQnaAn3wrj3s6WYW0bQvPI7x+sPYUGmDTYMHefVK//zc6HEYZ1qnxIK+Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/hejny/locate-app/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "dependencies": { + "@promptbook/utils": "0.69.5", + "type-fest": "4.26.0", + "userhome": "1.0.1" + } + }, + "node_modules/locate-app/node_modules/type-fest": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", + "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", + "dev": true, + "engines": { + "node": ">=16" + }, + "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", + "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==", + "dev": 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==", + "dev": true + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, + "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.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "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.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.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/lodash.pickby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", + "integrity": "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==", + "dev": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true + }, + "node_modules/lodash.zip": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==", + "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/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/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/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "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/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": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "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/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", + "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-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==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/meros": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/meros/-/meros-1.3.1.tgz", + "integrity": "sha512-eV7dRObfTrckdmAz4/n7pT1njIsIJXRIZkgCiX43xEsPNy4gjXQzOYYxmGcolAMtF7HyfqRuDBh3Lgs4hmhVEw==", + "engines": { + "node": ">=13" + }, + "peerDependencies": { + "@types/node": ">=13" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.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/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/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==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.1.tgz", + "integrity": "sha512-+Vyi+GCCOHnrJ2VPS+6aPoXN2k2jgUzDRhTFLjjTBn23qyXJXkjUWQgTL+mXpF5/A8ixLdCc6kWsoeOjKGejKQ==", + "dev": true, + "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.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/mini-css-extract-plugin/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/mini-css-extract-plugin/node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true + }, + "node_modules/mini-css-extract-plugin/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/mini-css-extract-plugin/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/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==", + "devOptional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, + "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", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "devOptional": true + }, + "node_modules/mnemonist": { + "version": "0.39.6", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.6.tgz", + "integrity": "sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==", + "dev": true, + "dependencies": { + "obliterator": "^2.0.1" + } + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/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/mocha/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/mocha/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "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" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/mocha/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/mocha/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/mocha/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/mocha/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/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/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-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/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/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/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/mocha/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/mocha/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/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/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/mocha/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/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/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.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/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", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "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/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/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "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.71.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz", + "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" + }, + "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", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "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", + "resolved": "https://registry.npmjs.org/node-loader/-/node-loader-2.0.0.tgz", + "integrity": "sha512-I5VN34NO4/5UYJaUBtkrODPWxbobrE4hgDqPrjB25yPkonFhCmZ146vTH+Zg417E9Iwoh1l/MbRs1apc5J295Q==", + "dev": true, + "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", + "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/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "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-url": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", + "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "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/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", + "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.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "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.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obliterator": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", + "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==", + "dev": true + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "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/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==", + "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/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "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.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz", + "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==", + "dev": true, + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.9.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.3.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "string-width": "^6.1.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/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/ora/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/ora/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/ora/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/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-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "dev": true, + "engines": { + "node": ">=12.20" + } + }, + "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-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.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "dev": true, + "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.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" + }, + "engines": { + "node": ">= 14" + } + }, + "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==", + "dev": true, + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "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==", + "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==", + "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", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", + "dev": true, + "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.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dependencies": { + "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", + "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==", + "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.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "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-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/peek-readable": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.3.1.tgz", + "integrity": "sha512-GVlENSDW6KHaXcd9zkZltB7tCLosKB/4Hg0fqBJkAoBgYG2Tn1xtMgXtSUuMU9AK/gCm/tTdT8mgAeF4YNeeqw==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "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==" + }, + "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.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "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/pino": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.5.0.tgz", + "integrity": "sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==", + "dev": true, + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "dev": true, + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "dev": true + }, + "node_modules/pino/node_modules/process-warning": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz", + "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==", + "dev": true + }, + "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/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz", + "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", + "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", + "dev": true, + "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", + "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/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "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, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "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", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss/node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "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/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/prebuild-install/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" + } + ], + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "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", + "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/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": 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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-ms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "dev": true, + "dependencies": { + "parse-ms": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "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==", + "dev": true + }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "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/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/prop-types/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/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==", + "dev": true, + "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==", + "dev": 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/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==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "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==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "devOptional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.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/puppeteer-core": { + "version": "21.11.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-21.11.0.tgz", + "integrity": "sha512-ArbnyA3U5SGHokEvkfWjW+O8hOxV1RSJxOgriX/3A4xZRqixt9ZFHD0yPgZQF05Qj0oAqi8H/7stDorjoHY90Q==", + "dev": true, + "dependencies": { + "@puppeteer/browsers": "1.9.1", + "chromium-bidi": "0.5.8", + "cross-fetch": "4.0.0", + "debug": "4.3.4", + "devtools-protocol": "0.0.1232444", + "ws": "8.16.0" + }, + "engines": { + "node": ">=16.13.2" + } + }, + "node_modules/puppeteer-core/node_modules/@puppeteer/browsers": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.9.1.tgz", + "integrity": "sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==", + "dev": true, + "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/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/devtools-protocol": { + "version": "0.0.1232444", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1232444.tgz", + "integrity": "sha512-pM27vqEfxSxRkTMnF+XCmxSEb6duO5R+t8A9DEEJgy4Wz2RVanje2mmj99B6A3zv2r/qGfYlOvYznUhuokizmg==", + "dev": true + }, + "node_modules/puppeteer-core/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==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "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/proxy-agent": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", + "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", + "dev": true, + "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/puppeteer-core/node_modules/tar-fs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", + "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "dev": true, + "dependencies": { + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "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.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "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", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "dev": true + }, + "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==", + "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==", + "dev": true + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "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", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-8.1.0.tgz", + "integrity": "sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-10.1.0.tgz", + "integrity": "sha512-aNtBq4jR8NawpKJQldrQcSW9y/d+KWH4v24HWkHljOZ7H0av+YTGANBzRh9A5pw7v/bLVsLVPpOhJ7gHNVy8lA==", + "dev": true, + "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", + "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/read-pkg-up/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/read-pkg-up/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/read-pkg-up/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/read-pkg-up/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/read-pkg-up/node_modules/type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "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.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-pkg/node_modules/lines-and-columns": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", + "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/read-pkg/node_modules/parse-json": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.1.1.tgz", + "integrity": "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "dev": true, + "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", + "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/readable-stream/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/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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "dev": true, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/rechoir/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/recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "dev": true, + "dependencies": { + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "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.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.2" + }, + "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", + "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/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/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, + "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, + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "dev": true, + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/resq/-/resq-1.11.0.tgz", + "integrity": "sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/resq/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "dev": true + }, + "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/restore-cursor/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/ret": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", + "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rgb2hex": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.2.5.tgz", + "integrity": "sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==", + "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==", + "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/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", + "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/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==", + "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==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safaridriver": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-0.1.2.tgz", + "integrity": "sha512-4R309+gWflJktzPXBQCobbWEHlzC4aK3a+Ov3tz2Ib2aBxiwd11phkdIBH1l0EO22x24CJMUQkpKFumRriCSRg==", + "dev": true + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "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", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "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/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex2": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz", + "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", + "dev": true, + "dependencies": { + "ret": "~0.4.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "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.80.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.5.tgz", + "integrity": "sha512-TQd2aoQl/+zsxRMEDSxVdpPIqeq9UFc6pr7PzkugiTx3VYCFPUaa3P4RrBQsqok4PO200Vkz0vXQBNlg7W907g==", + "dev": true, + "dependencies": { + "@parcel/watcher": "^2.4.1", + "chokidar": "^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", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.3.tgz", + "integrity": "sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==", + "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/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "dev": true + }, + "node_modules/seek-bzip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-2.0.0.tgz", + "integrity": "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==", + "dev": true, + "dependencies": { + "commander": "^6.0.0" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-error": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", + "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", + "dev": true, + "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", + "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/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "dev": true + }, + "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.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "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/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", + "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/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/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "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==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "dev": true, + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", + "dev": true, + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys/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": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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/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-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/spacetrim": { + "version": "0.11.59", + "resolved": "https://registry.npmjs.org/spacetrim/-/spacetrim-0.11.59.tgz", + "integrity": "sha512-lLYsktklSRKprreOm7NXReW8YiX2VBjbgmXYEziOoGf/qsJqAEACaDvoTtUOycwjpaSh+bT8eu0KrJn7UNxiCg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/hejny/spacetrim/blob/main/README.md#%EF%B8%8F-contributing" + } + ] + }, + "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.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "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.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "dev": true + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": 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, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "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/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/stream-buffers": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.3.tgz", + "integrity": "sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/streamx": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz", + "integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==", + "dev": true, + "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==", + "devOptional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/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==", + "devOptional": true + }, + "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", + "resolved": "https://registry.npmjs.org/string-replace-loader/-/string-replace-loader-3.1.0.tgz", + "integrity": "sha512-5AOMUZeX5HE/ylKDnEa/KKBqvlnFmRZudSOjVJHxhoJg9QYTwl1rECx7SLR8BBH7tfxb4Rp7EM2XVfQFxIhsbQ==", + "dev": true, + "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", + "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/string-replace-loader/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/string-width": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz", + "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^10.2.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/string-width-cjs/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/string-width-cjs/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/string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-3.0.0.tgz", + "integrity": "sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ==", + "dev": true, + "dependencies": { + "inspect-with-kind": "^1.0.5", + "is-plain-obj": "^1.1.0" + } + }, + "node_modules/strip-dirs/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": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "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/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "dev": true + }, + "node_modules/strtok3": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-9.0.1.tgz", + "integrity": "sha512-ERPW+XkvX9W2A+ov07iy+ZFJpVdik04GhDA4eVogiG9hpC97Kem2iucyzhFxbFRvQ5o2UckFtKZdp1hkGvnrEw==", + "dev": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.3.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "4.2.20", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.20.tgz", + "integrity": "sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==", + "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.39", + "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.7.39.tgz", + "integrity": "sha512-NX8a7eSqF1hr6WKArvXr7TV7DeE+y0kDFD7L5JP7TWqlwFidzGKaG415p992MHREiiEWOv2xIWXJ+mlONofs0A==", + "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/sync-fetch": { + "version": "0.6.0-2", + "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.6.0-2.tgz", + "integrity": "sha512-c7AfkZ9udatCuAy9RSfiGPpeOKKUAUK5e1cXadLOGUjasdxqYqAK0jTNkM/FSEyJ3a5Ra27j/tw/PS0qLmaF/A==", + "dependencies": { + "node-fetch": "^3.3.2", + "timeout-signal": "^2.0.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tabbable": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz", + "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==" + }, + "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-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "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==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/terser": { + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", + "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "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.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "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", + "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/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 + }, + "node_modules/text-decoder": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", + "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==", + "dev": true + }, + "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/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "dev": true, + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/timeout-signal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/timeout-signal/-/timeout-signal-2.0.0.tgz", + "integrity": "sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==", + "engines": { + "node": ">=16" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "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/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "dependencies": { + "tmp": "^0.2.0" + } + }, + "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/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", + "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "dev": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "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/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dev": true, + "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", + "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/ts-loader/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/ts-loader/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/ts-loader/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/ts-loader/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/ts-loader/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/ts-loader/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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "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/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "dev": true, + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "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", + "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.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "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", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "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/uint8array-extras": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/unbzip2-stream/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/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==" + }, + "node_modules/undici": { + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.20.1.tgz", + "integrity": "sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "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/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "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.2.0", + "picocolors": "^1.1.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/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/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.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/userhome": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.1.tgz", + "integrity": "sha512-5cnLm4gseXjAclKowC4IjByaGsjtAoV6PrOQOljplNB54ReUYJP8HdAFq2muHinSDAh09PPX/uXDPfdxRHvuSA==", + "dev": true, + "engines": { + "node": ">= 0.8.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==", + "devOptional": true + }, + "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/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/vscode-jsonrpc": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", + "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", + "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==", + "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", + "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/vscode-languageclient/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/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", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", + "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", + "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", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", + "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver-protocol/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==" + }, + "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", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" + }, + "node_modules/vue": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.16.tgz", + "integrity": "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==", + "dependencies": { + "@vue/compiler-dom": "3.5.16", + "@vue/compiler-sfc": "3.5.16", + "@vue/runtime-dom": "3.5.16", + "@vue/server-renderer": "3.5.16", + "@vue/shared": "3.5.16" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/wait-port": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", + "integrity": "sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==", + "dev": true, + "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", + "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/wait-port/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/wait-port/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/wait-port/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/wait-port/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/wait-port/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/wait-port/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/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.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, + "optional": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/wdio-vscode-service": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/wdio-vscode-service/-/wdio-vscode-service-6.1.2.tgz", + "integrity": "sha512-qOXpu8SwxPuBUPaZ+1d5j/RKU5JiZQeu5Kcp5iAxwaCj05h4v7TJhexeNYAhHgKqL/7C0uEFzjYWcB3afmDGpw==", + "dev": true, + "dependencies": { + "@fastify/cors": "^9.0.1", + "@fastify/static": "^7.0.1", + "@types/ws": "^8.5.10", + "@vscode/test-electron": "^2.3.9", + "@wdio/logger": "^8.28.0", + "@xhmikosr/downloader": "^15.0.1", + "clipboardy": "^3.0.0", + "decamelize": "6.0.0", + "fastify": "^4.26.1", + "get-port": "7.0.0", + "hpagent": "^1.2.0", + "slash": "^5.1.0", + "tmp-promise": "^3.0.3", + "undici": "^5.28.3", + "vscode-uri": "^3.0.8", + "ws": "^8.16.0", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": "^16.13 || >=18" + }, + "peerDependencies": { + "webdriverio": "^8.32.2" + }, + "peerDependenciesMeta": { + "webdriverio": { + "optional": true + } + } + }, + "node_modules/wdio-vscode-service/node_modules/@wdio/logger": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", + "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", + "dev": true, + "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-vscode-service/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/wdio-vscode-service/node_modules/get-port": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.0.0.tgz", + "integrity": "sha512-mDHFgApoQd+azgMdwylJrv2DX47ywGq1i5VFJE7fZ0dttNq3iQMfsU4IvEgBHojA3KqEudyu7Vq+oN8kNaNkWw==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wdio-vscode-service/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wdio-vscode-service/node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/wdio-vscode-service/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/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webdriver": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.40.6.tgz", + "integrity": "sha512-jkslwUvOmqhFfc1E21Tz48NgYD8ykiR+09iWZlVLtx3P43k4jOfS+CfasvQ+6hJiVck+N5dXjYfg6zDjpkIFRw==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0", + "@types/ws": "^8.5.3", + "@wdio/config": "8.40.6", + "@wdio/logger": "8.38.0", + "@wdio/protocols": "8.40.3", + "@wdio/types": "8.40.6", + "@wdio/utils": "8.40.6", + "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/@puppeteer/browsers": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.9.1.tgz", + "integrity": "sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==", + "dev": true, + "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/webdriver/node_modules/@types/node": { + "version": "22.8.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz", + "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/webdriver/node_modules/@wdio/config": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.40.6.tgz", + "integrity": "sha512-rHCSmrhdJf7FlidcQPDvRKRPLYjklbrdxQa6J20BxHifTO4h2v23Wrq4OqqYIcq23gf9LpZvCA/PAMiET/QdVg==", + "dev": true, + "dependencies": { + "@wdio/logger": "8.38.0", + "@wdio/types": "8.40.6", + "@wdio/utils": "8.40.6", + "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/webdriver/node_modules/@wdio/logger": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", + "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", + "dev": true, + "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/webdriver/node_modules/@wdio/protocols": { + "version": "8.40.3", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.40.3.tgz", + "integrity": "sha512-wK7+eyrB3TAei8RwbdkcyoNk2dPu+mduMBOdPJjp8jf/mavd15nIUXLID1zA+w5m1Qt1DsT1NbvaeO9+aJQ33A==", + "dev": true + }, + "node_modules/webdriver/node_modules/@wdio/types": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.40.6.tgz", + "integrity": "sha512-ALftLri1BdsRuPrQkuW3evBNdOA5n4IkuoegOw6UE2z+R0f1YI5fHGSHNRWLnhtbOECbGyHXXqzbSxCEb+o+MA==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/webdriver/node_modules/@wdio/utils": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.40.6.tgz", + "integrity": "sha512-+TWfV6h+4f8gs7QiYUAWbWEylpZudQ+xkJPN34tRzPJK6dOBYEnIT/j6+1m3j39m1WPDehyYxIf1wCsrGKBxNQ==", + "dev": true, + "dependencies": { + "@puppeteer/browsers": "^1.6.0", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.40.6", + "decamelize": "^6.0.0", + "deepmerge-ts": "^5.1.0", + "edgedriver": "^5.5.0", + "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/webdriver/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/webdriver/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/webdriver/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/webdriver/node_modules/deepmerge-ts": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz", + "integrity": "sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==", + "dev": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/webdriver/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/webdriver/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webdriver/node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "dev": true, + "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/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/webdriver/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webdriver/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/webdriver/node_modules/proxy-agent": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", + "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", + "dev": true, + "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/webdriver/node_modules/tar-fs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", + "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "dev": true, + "dependencies": { + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "node_modules/webdriverio": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.40.6.tgz", + "integrity": "sha512-hMFYRjVU5Nnk2e9Mi8kDx/IVFMWGaVyDCDpv/SeXXCP17DT9jAZtOWlwGhRaLVikN5JYYuHavHyatVa7gj6QTg==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0", + "@wdio/config": "8.40.6", + "@wdio/logger": "8.38.0", + "@wdio/protocols": "8.40.3", + "@wdio/repl": "8.40.3", + "@wdio/types": "8.40.6", + "@wdio/utils": "8.40.6", + "archiver": "^7.0.0", + "aria-query": "^5.0.0", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "devtools-protocol": "^0.0.1359167", + "grapheme-splitter": "^1.0.2", + "import-meta-resolve": "^4.0.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "minimatch": "^9.0.0", + "puppeteer-core": "^21.11.0", + "query-selector-shadow-dom": "^1.0.0", + "resq": "^1.9.1", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.1", + "webdriver": "8.40.6" + }, + "engines": { + "node": "^16.13 || >=18" + }, + "peerDependencies": { + "devtools": "^8.14.0" + }, + "peerDependenciesMeta": { + "devtools": { + "optional": true + } + } + }, + "node_modules/webdriverio/node_modules/@puppeteer/browsers": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.9.1.tgz", + "integrity": "sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==", + "dev": true, + "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/webdriverio/node_modules/@types/node": { + "version": "22.8.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz", + "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/webdriverio/node_modules/@wdio/config": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.40.6.tgz", + "integrity": "sha512-rHCSmrhdJf7FlidcQPDvRKRPLYjklbrdxQa6J20BxHifTO4h2v23Wrq4OqqYIcq23gf9LpZvCA/PAMiET/QdVg==", + "dev": true, + "dependencies": { + "@wdio/logger": "8.38.0", + "@wdio/types": "8.40.6", + "@wdio/utils": "8.40.6", + "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/webdriverio/node_modules/@wdio/logger": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", + "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", + "dev": true, + "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/webdriverio/node_modules/@wdio/protocols": { + "version": "8.40.3", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.40.3.tgz", + "integrity": "sha512-wK7+eyrB3TAei8RwbdkcyoNk2dPu+mduMBOdPJjp8jf/mavd15nIUXLID1zA+w5m1Qt1DsT1NbvaeO9+aJQ33A==", + "dev": true + }, + "node_modules/webdriverio/node_modules/@wdio/types": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.40.6.tgz", + "integrity": "sha512-ALftLri1BdsRuPrQkuW3evBNdOA5n4IkuoegOw6UE2z+R0f1YI5fHGSHNRWLnhtbOECbGyHXXqzbSxCEb+o+MA==", + "dev": true, + "dependencies": { + "@types/node": "^22.2.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/webdriverio/node_modules/@wdio/utils": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.40.6.tgz", + "integrity": "sha512-+TWfV6h+4f8gs7QiYUAWbWEylpZudQ+xkJPN34tRzPJK6dOBYEnIT/j6+1m3j39m1WPDehyYxIf1wCsrGKBxNQ==", + "dev": true, + "dependencies": { + "@puppeteer/browsers": "^1.6.0", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.40.6", + "decamelize": "^6.0.0", + "deepmerge-ts": "^5.1.0", + "edgedriver": "^5.5.0", + "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/webdriverio/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/webdriverio/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/webdriverio/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/webdriverio/node_modules/deepmerge-ts": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz", + "integrity": "sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==", + "dev": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/webdriverio/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webdriverio/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==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/webdriverio/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webdriverio/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/webdriverio/node_modules/proxy-agent": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", + "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", + "dev": true, + "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/webdriverio/node_modules/tar-fs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", + "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "dev": true, + "dependencies": { + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "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/webpack": { + "version": "5.96.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.0.tgz", + "integrity": "sha512-gvn84AfQ4f6vUeNWmFuRp3vGERyxK4epADKTaAo60K0EQbY/YBNQbXH3Ji/ZRK5M25O/XneAOuChF4xQZjQ4xA==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "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.11", + "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.1", + "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", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "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/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/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "engines": { + "node": ">=18" + } + }, + "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/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/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.4", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", + "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "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.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/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/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true + }, + "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/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-cjs/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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/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/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/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.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "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/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==" + }, + "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/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": "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/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.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/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/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/yargs/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/yargs/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/yargs/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/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yauzl/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==", + "engines": { + "node": "*" + } + }, + "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/yazl/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==", + "engines": { + "node": "*" + } + }, + "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/yoctocolors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", + "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "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==", + "dev": true, + "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/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==", + "dev": true, + "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/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/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, + "optional": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/firebase-vscode/package.json b/firebase-vscode/package.json new file mode 100644 index 00000000000..fb697cb0eca --- /dev/null +++ b/firebase-vscode/package.json @@ -0,0 +1,280 @@ +{ + "name": "firebase-dataconnect-vscode", + "displayName": "Firebase Data Connect", + "publisher": "GoogleCloudTools", + "icon": "./resources/firebase_dataconnect_logo.png", + "description": "Firebase Data Connect for VSCode", + "version": "1.9.0", + "engines": { + "vscode": "^1.69.0" + }, + "repository": "https://github.com/firebase/firebase-tools", + "sideEffects": false, + "extensionDependencies": [ + "graphql.vscode-graphql-syntax" + ], + "categories": [ + "Other" + ], + "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" + }, + { + "command": "firebase.refresh", + "title": "Firebase Data Connect Extension: Refresh" + } + ], + "configuration": { + "title": "%ext.config.title%", + "properties": { + "firebase.dataConnect.alwaysAllowMutationsInProduction": { + "type": "boolean", + "default": false, + "markdownDescription": "%ext.config.dataConnect.alwaysAllowMutationsInProduction%" + }, + "firebase.dataConnect.skipToAppFolderSelect": { + "type": "boolean", + "default": false, + "markdownDescription": "%ext.config.dataConnect.skipToAppFolderSelect%" + }, + "firebase.firebasePath": { + "type": "string", + "markdownDescription": "%ext.config.firebasePath%", + "scope": "machine-overridable" + }, + "firebase.hosting.useFrameworks": { + "type": "boolean", + "default": true, + "markdownDescription": "%ext.config.hosting.useFrameworks%" + }, + "firebase.npmPath": { + "type": "string", + "markdownDescription": "%ext.config.npmPath%" + }, + "firebase.idx.viewMetricNotice": { + "type": "boolean", + "default": true, + "markdownDescription": "%ext.config.idx.viewMetricNotice%", + "scope": "application" + }, + "firebase.emulators.importPath": { + "type": "string", + "markdownDescription": "%ext.config.emulators.importPath%" + }, + "firebase.emulators.exportPath": { + "type": "string", + "default": "./exportedData", + "markdownDescription": "%ext.config.emulators.exportPath%" + }, + "firebase.emulators.exportOnExit": { + "type": "boolean", + "default": false, + "markdownDescription": "%ext.config.emulators.exportOnExit%" + }, + "firebase.debug": { + "type": "boolean", + "default": false, + "markdownDescription": "%ext.config.debug%" + }, + "firebase.extraEnv": { + "type": "object", + "default": {}, + "markdownDescription": "%ext.config.extraEnv%" + } + } + }, + "viewsContainers": { + "activitybar": [ + { + "id": "firebase-data-connect", + "title": "Firebase Data Connect", + "icon": "resources/firebase_dataconnect_logo.svg" + } + ], + "panel": [ + { + "id": "firebase-data-connect-execution-view", + "title": "Data Connect Execution", + "icon": "$(gmp-data-connect)" + } + ] + }, + "icons": { + "mono-firebase": { + "description": "Firebase icon", + "default": { + "fontPath": "./resources/monicons.woff", + "fontCharacter": "\\F101" + } + }, + "gmp-data-connect": { + "description": "Data Connect icon", + "default": { + "fontPath": "./resources/GMPIcons.woff2", + "fontCharacter": "\\gmp_nav20_dataconnect" + } + } + }, + "views": { + "firebase-data-connect": [ + { + "type": "webview", + "id": "fdc_sidebar", + "name": "Studio", + "initialSize": 2 + }, + { + "id": "firebase.dataConnect.explorerView", + "name": "Schema explorer", + "when": "firebase-vscode.fdc.enabled", + "visibility": "collapsed", + "initialSize": 1 + } + ], + "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": "Resolve compilation errors 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 && node --max-old-space-size=4096 ./node_modules/webpack/bin/webpack.js --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", + "install:extensions": "code-server --install-extension graphql.vscode-graphql-syntax --extensions-dir ./prebuilt-extensions/", + "pretest:e2e": "curl -fsSL https://code-server.dev/install.sh | sh -s -- --edge && npm run install:extensions", + "pretest:unit": "npm run test-compile && 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.4.0.tgz", + "graphql-language-service-server": "file:graphql-language-service-server-2.14.8.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": "20.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": "^9.0.7", + "@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": "^6.1.1", + "webpack": "^5.75.0", + "webpack-cli": "^5.0.1", + "webpack-merge": "^5.8.0" + } +} diff --git a/firebase-vscode/package.nls.json b/firebase-vscode/package.nls.json new file mode 100644 index 00000000000..3d1f2c98443 --- /dev/null +++ b/firebase-vscode/package.nls.json @@ -0,0 +1,14 @@ +{ + "ext.config.dataConnect.alwaysAllowMutationsInProduction": "Always allow mutations in production. If false (default), trying to run a mutation in production will open a confirmation modal.", + "ext.config.dataConnect.skipToAppFolderSelect": "Skip app folder selection prompt. If false (default), trying to generate SDK config will open a confirmation modal.", + "ext.config.firebasePath": "Path to the `Firebase` module, e.g. `./node_modules/firebase`", + "ext.config.hosting.useFrameworks": "Enable web frameworks", + "ext.config.npmPath": "Path to NPM executable in local environment", + "ext.config.title": "Firebase Data Connect", + "ext.config.idx.viewMetricNotice": "Show data collection notice on next startup (IDX Only)", + "ext.config.emulators.importPath": "Path to import emulator data from", + "ext.config.emulators.exportPath": "Path to export emulator data to", + "ext.config.emulators.exportOnExit": "If true, data will be exported to exportPath when the emulator shuts down", + "ext.config.debug": "When true, add the --debug flag to any commands run by the extension", + "ext.config.extraEnv": "Extra environment variables to provide during deploy or emulation. Useful during extension development to enable experiments or hit non-production environments." +} 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_dataconnect_logo.png b/firebase-vscode/resources/firebase_dataconnect_logo.png new file mode 100644 index 00000000000..8172a0b89b8 Binary files /dev/null and b/firebase-vscode/resources/firebase_dataconnect_logo.png differ diff --git a/firebase-vscode/resources/firebase_dataconnect_logo.svg b/firebase-vscode/resources/firebase_dataconnect_logo.svg new file mode 100644 index 00000000000..8dc3321b84c --- /dev/null +++ b/firebase-vscode/resources/firebase_dataconnect_logo.svg @@ -0,0 +1,6 @@ + + + + + + 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..e157acabbfe --- /dev/null +++ b/firebase-vscode/src/analytics.ts @@ -0,0 +1,237 @@ +import vscode, { env, TelemetryLogger, TelemetrySender } from "vscode"; +import { pluginLogger } from "./logger-wrapper"; +import { AnalyticsParams, trackVSCode } from "../../src/track"; +import { env as monospaceEnv } from "../src/core/env"; +import { getSettings } from "./utils/settings"; + +export const IDX_METRIC_NOTICE = ` +When you use the Firebase Data Connect Extension, Google collects telemetry data such as usage statistics, error metrics, and crash reports. Telemetry helps us better understand how the Firebase Extension is performing, where improvements need to be made, and how features are being used. Firebase uses this data, consistent with our [Google Privacy Policy](https://policies.google.com/privacy?hl=en-US), to provide, improve, and develop Firebase products and services. +We take steps to protect your privacy as part of this process. This includes disconnecting your telemetry data from your Google Account, fully anonymizing it, and storing that data for up to 14 months. +Read more in our [Privacy Policy](https://policies.google.com/privacy?hl=en-US). +`; + +export enum DATA_CONNECT_EVENT_NAME { + EXTENSION_USED = "extension_used", + COMMAND_EXECUTION = "command_execution", + DEPLOY_ALL = "deploy_all", + DEPLOY_INDIVIDUAL = "deploy_individual", + IDX_LOGIN = "idx_login", + LOGIN = "login", + PROJECT_SELECT_CLICKED = "project_select_clicked", + PROJECT_SELECTED = "project_selected", + RUN_LOCAL = "run_local", + RUN_PROD = "run_prod", + RUN_PROD_MUTATION_WARNING = "run_prod_mutation_warning", + RUN_PROD_MUTATION_WARNING_REJECTED = "run_prod_mutation_warning_rejected", + RUN_PROD_MUTATION_WARNING_ACKED = "run_prod_mutation_warning_acked", + RUN_PROD_MUTATION_WARNING_ACKED_ALWAYS = "run_prod_mutation_warning_acked_always", + MISSING_VARIABLES = "missing_variables", + GENERATE_OPERATION = "generate_operation", + GIF_TOS_MODAL = "gif_tos_modal", + GIF_TOS_MODAL_ACKED = "gif_tos_modal_acked", + GIF_TOS_MODAL_CLICKED = "gif_tos_modal_clicked", + GIF_TOS_MODAL_REJECTED = "gif_tos_modal_rejected", + ADD_DATA = "add_data", + READ_DATA = "read_data", + MOVE_TO_CONNECTOR = "move_to_connector", + START_EMULATOR_FROM_EXECUTION = "start_emulator_from_execution", + INIT = "init", + INIT_SDK = "init_sdk", + INIT_SDK_CLI = "init_sdk_cli", + INIT_SDK_CODELENSE = "init_sdk_codelense", + START_EMULATORS = "start_emulators", + AUTO_COMPLETE = "auto_complete", + SESSION_CHAR_COUNT = "session_char_count", + EMULATOR_EXPORT = "emulator_export", + SETUP_FIREBASE_BINARY = "setup_firebase_binary", + TRY_FIREBASE_AGENT_CLICKED = "try_firebase_agent_in_gemini_clicked", + MCP_DOCS_CLICKED = "mcp_docs_clicked", + GIF_TOS_CLICKED = "gif_tos_clicked", +} + +export class AnalyticsLogger { + readonly logger: TelemetryLogger | IDXLogger; + private disposable: vscode.Disposable; + private sessionCharCount = 0; // Track total chars for the session + + constructor(context: vscode.ExtensionContext) { + this.logger = monospaceEnv.value.isMonospace + ? new IDXLogger(new GA4TelemetrySender(pluginLogger), context) + : env.createTelemetryLogger(new GA4TelemetrySender(pluginLogger)); + + let subscriptions: vscode.Disposable[] = [ + vscode.workspace.onDidChangeTextDocument( + this.trackWrittenCharacters, + this, + ), + vscode.workspace.onDidChangeTextDocument( + this.trackWrittenCharactersInSession, + this, + ), + vscode.commands.registerCommand( + "fdc.logCompletionItem", + this.listenForAutocompleteEvent, + ), + ]; + + this.disposable = vscode.Disposable.from(...subscriptions); + } + + onDispose() { + this.disposable.dispose(); + } + + private TYPING_TRACK_DURATION = 5000; + private typedCharCount = 0; + private timeoutHandle: NodeJS.Timeout | undefined | null; + + // Track manual typing during a session. + private trackWrittenCharactersInSession = ( + e: vscode.TextDocumentChangeEvent, + ) => { + e.contentChanges.forEach((change) => { + if (change.text === "") { + // Handle text deletion (backspace). + this.sessionCharCount = Math.max( + 0, + this.sessionCharCount - change.rangeLength, + ); + } else { + // Add the number of manually typed characters. + this.sessionCharCount += change.text.length; + } + }); + }; + + private trackWrittenCharacters = (e: vscode.TextDocumentChangeEvent) => { + e.contentChanges.forEach((change) => { + if (change.text === "") { + // Text deletion (backspace) + this.typedCharCount = Math.max( + 0, + this.typedCharCount - change.rangeLength, + ); + } else { + this.typedCharCount += change.text.length; + } + }); + }; + + listenForAutocompleteEvent = (label: string) => { + if (this.timeoutHandle) { + // Log the previously tracked session before resetting. + this.endAutocompleteTrackingSession(label); + } + + // Reset the count for tracking characters written after autocomplete. + this.typedCharCount = 0; + + // Start a new tracking session with a timeout. + this.timeoutHandle = setTimeout(() => { + this.endAutocompleteTrackingSession(label); + }, this.TYPING_TRACK_DURATION); + + this.logger.logUsage(DATA_CONNECT_EVENT_NAME.AUTO_COMPLETE, { + label, + }); + }; + + private endAutocompleteTrackingSession(label?: string) { + if (this.typedCharCount > 0) { + this.logger.logUsage(DATA_CONNECT_EVENT_NAME.AUTO_COMPLETE, { + autoCompleted: label, + charsCountAfterAutocomplete: this.typedCharCount, + }); + } + + // Reset the count and clear the timeout. + this.typedCharCount = 0; + if (this.timeoutHandle) { + clearTimeout(this.timeoutHandle); + this.timeoutHandle = null; + } + } + + endSession() { + this.logger.logUsage(DATA_CONNECT_EVENT_NAME.SESSION_CHAR_COUNT, { + totalChars: this.sessionCharCount, + }); + + this.sessionCharCount = 0; + } +} + +export class IDXLogger { + constructor( + private sender: GA4TelemetrySender, + private context: vscode.ExtensionContext, + ) {} + public logUsage(eventName: string, data?: any) { + const packageJson = this.context.extension.packageJSON; + data = { + ...data, + ...getAnalyticsContext(this.context), + isidx: "true", + }; + this.sender.sendEventData(eventName, data); + } + + public logError() { + // TODO + } +} + +class GA4TelemetrySender implements TelemetrySender { + private hasSentData = false; + constructor(readonly pluginLogger: { warn: (s: string) => void }) {} + + sendEventData( + eventName: string, + data?: Record | undefined, + ): void { + // telemetry logger adds prefixes to eventName and params that are disallowed in GA4 + eventName = eventName.replace( + "GoogleCloudTools.firebase-dataconnect-vscode/", + "", + ); + + // sanitize string as a fallback; numbers, letters, and underscore only + eventName = eventName.replace(/[^a-zA-Z0-9_]/g, ""); + + for (const key in data) { + if (key.includes("common.")) { + data[key.replace("common.", "")] = data[key]; + delete data[key]; + } + } + data = { ...data }; + data = addFirebaseBinaryMetadata(data); + if (!this.hasSentData) { + trackVSCode( + DATA_CONNECT_EVENT_NAME.EXTENSION_USED, + data as AnalyticsParams, + ); + this.hasSentData = true; + } + trackVSCode(eventName, data as AnalyticsParams); + } + + sendErrorData(error: Error, data?: Record | undefined): void { + // n/a + // TODO: Sanatize error messages for user data + } +} + +export function getAnalyticsContext(context: vscode.ExtensionContext) { + const packageJson = context.extension.packageJSON; + + return { + extversion: packageJson.version, + extname: monospaceEnv.value.isMonospace ? "idx" : "vscode", + }; +} + +function addFirebaseBinaryMetadata(data?: Record | undefined) { + const settings = getSettings(); + return { ...data, binary_kind: settings.firebaseBinaryKind }; +} 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..47ddf5835dd --- /dev/null +++ b/firebase-vscode/src/cli.ts @@ -0,0 +1,149 @@ +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, Tokens, User } from "../../src/types/auth"; +import { Options } from "../../src/options"; +import { currentOptions } from "./options"; +import { pluginLogger } from "./logger-wrapper"; +import { getAccessToken, setAccessToken } from "../../src/apiv2"; +import { EmulatorRegistry } from "../../src/emulator/registry"; +import { + DownloadableEmulatorDetails, + EmulatorInfo, + DownloadableEmulators, + Emulators, +} from "../../src/emulator/types"; +import { currentUser } from "./core/user"; +import { currentProjectId } from "./core/project"; +import { updateFirebaseRCProject } from "./core/config"; +export { Emulators }; +/** + * 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. + * + */ +export async function requireAuthWrapper( + showError: boolean = true, +): Promise { + // Try to get global default from configstore + pluginLogger.debug("requireAuthWrapper"); + let account = getGlobalDefaultAccount(); + // often overwritten when restarting the extension. + const accounts = getAllAccounts(); + + // helper to determine if VSCode options has the same account as global default + function isUserMatching(account: Account, options: Options) { + if (!options.user || !options.tokens) { + return false; + } + + const optionsUser = options.user as User; + const optionsTokens = options.tokens as Tokens; + return ( + account && + account.user?.email === optionsUser.email && + account.tokens.refresh_token === optionsTokens.refresh_token // Should check refresh token which is consistent, not access_token which is short lived. + ); + } + + // only add account options when vscode is missing account information + if (!isUserMatching(account!, currentOptions.value)) { + currentOptions.value = { ...currentOptions.value, ...account }; + } + + if (!account) { + // If nothing in configstore top level, grab the first "additionalAccount" + for (const additionalAccount of accounts) { + if (additionalAccount.user.email === currentUser.value!.email) { + account = additionalAccount; + setGlobalDefaultAccount(account); + } + } + } + // `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 optsCopy = currentOptions.value; + const userEmail = await requireAuth(optsCopy); // client email + // SetAccessToken is necessary here to ensure that access_tokens are available when: + // - we are using tokens from configstore (aka those set by firebase login), AND + // - we are calling CLI code that skips Command (where we normally call this) + currentOptions.value = optsCopy; + if (optsCopy.projectId) { + updateFirebaseRCProject({ + projectAlias: { alias: "default", projectId: optsCopy.projectId }, + }); + } + setAccessToken(await getAccessToken()); + if (userEmail) { + pluginLogger.debug("User found: ", userEmail); + + // VSCode only has the concept of a single user + return getGlobalDefaultAccount()!.user; + } + + pluginLogger.debug("No user found (this may be normal)"); + return null; + } catch (e: any) { + 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}`, + ); + } 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 null; + } +} + +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() { + return listFirebaseProjects(); +} + +export function listRunningEmulators(): EmulatorInfo[] { + return EmulatorRegistry.listRunningWithInfo(); +} + +export function getEmulatorUiUrl(): string | undefined { + try { + const url: URL = EmulatorRegistry.url(Emulators.UI); + return url.hostname === "unknown" ? undefined : url.toString(); + } catch { + return undefined; + } +} + +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..c5f5f218fbf --- /dev/null +++ b/firebase-vscode/src/core/config.ts @@ -0,0 +1,367 @@ +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 { ValueOrError } from "../../common/messaging/protocol"; +import { firstWhereDefined, onChange } from "../utils/signal"; +import { Result, ResultError, ResultValue } from "../result"; +import { FirebaseConfig } from "../firebaseConfig"; +import { computed, effect } from "@preact/signals-react"; + +const allFirebaseConfigsUris = globalSignal>([]); + +const selectedFirebaseConfigUri = globalSignal( + 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); + +const selectedRCUri = computed(() => { + const configUri = selectedFirebaseConfigUri.value; + if (!configUri) { + return undefined; + } + + const folderPath = path.dirname(configUri.fsPath); + return vscode.Uri.file(path.join(folderPath, ".firebaserc")); +}); + +/** + * 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, +); + +/** + * Write new default project to .firebaserc + */ +export async function updateFirebaseRCProject(values: { + projectAlias?: { + alias: string; + projectId: string; + }; +}) { + let didChange = false; + const newRCPath = path.join(currentOptions.value.cwd, ".firebaserc"); + const isNewRC = !firebaseRC.value?.tryReadValue; + + const rc = firebaseRC.value?.tryReadValue ?? new RC(newRCPath, {}); + + if ( + values.projectAlias && + rc.resolveAlias(values.projectAlias.alias) !== values.projectAlias.projectId + ) { + rc.addProjectAlias( + values.projectAlias.alias, + values.projectAlias.projectId, + ); + rc.save(); + if (isNewRC) { + firebaseRC.value = _readRC(vscode.Uri.file(newRCPath)); + } + } +} + +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 displayStringForUri(uri: vscode.Uri) { + return vscode.workspace.asRelativePath(uri); +} + +function notifyFirebaseConfigListChanged(broker: ExtensionBrokerImpl) { + broker.send("notifyFirebaseConfigListChanged", { + values: allFirebaseConfigsUris.value.map(displayStringForUri), + selected: selectedFirebaseConfigUri.value + ? displayStringForUri(selectedFirebaseConfigUri.value) + : undefined, + }); +} + +async function registerRc( + context: vscode.ExtensionContext, + broker: ExtensionBrokerImpl, +) { + context.subscriptions.push({ + dispose: effect(() => { + firebaseRC.value = undefined; + + const rcUri = selectedRCUri.value; + if (!rcUri) { + return; + } + + const watcher = workspace.value.createFileSystemWatcher(rcUri.fsPath); + + watcher.onDidChange(() => (firebaseRC.value = _readRC(rcUri))); + watcher.onDidCreate(() => (firebaseRC.value = _readRC(rcUri))); + // TODO handle deletion of .firebaserc/.firebase.json/firemat.yaml + watcher.onDidDelete(() => (firebaseRC.value = undefined)); + + firebaseRC.value = _readRC(rcUri); + + return () => { + watcher.dispose(); + }; + }), + }); + + context.subscriptions.push({ + dispose: onChange(firebaseRC, () => notifyFirebaseConfig(broker)), + }); + + context.subscriptions.push({ + dispose: effect(() => { + const rc = firebaseRC.value; + if (rc instanceof ResultError) { + vscode.window.showErrorMessage( + `Error reading .firebaserc:\n${rc.error}`, + ); + } + }), + }); +} + +async function registerFirebaseConfig( + context: vscode.ExtensionContext, + broker: ExtensionBrokerImpl, +) { + const firebaseJsonPattern = "**/firebase.json"; + allFirebaseConfigsUris.value = await findFiles(firebaseJsonPattern); + + const configWatcher = await _createWatcher(firebaseJsonPattern); + // Track the URI of any firebase.json in the project. + if (configWatcher) { + context.subscriptions.push(configWatcher); + + // We don't listen to changes here, as we'll only watch the selected config. + configWatcher.onDidCreate((addedUri) => { + allFirebaseConfigsUris.value = [ + ...allFirebaseConfigsUris.value, + addedUri, + ]; + }); + configWatcher.onDidDelete((deletedUri) => { + allFirebaseConfigsUris.value = allFirebaseConfigsUris.value.filter( + (uri) => uri.fsPath !== deletedUri.fsPath, + ); + }); + } + + context.subscriptions.push({ + dispose: onChange(firebaseConfig, () => notifyFirebaseConfig(broker)), + }); + + // When no config is selected, or the selected config is deleted, select the first one. + context.subscriptions.push({ + dispose: effect(() => { + const configUri = selectedFirebaseConfigUri.value; + // We watch all config URIs before selecting one, so that when deleting the selected + // config, the effect runs again and selects a new one. + const allConfigUris = allFirebaseConfigsUris.value; + if (configUri && fs.existsSync(configUri.fsPath)) { + return; + } + + if (allConfigUris[0] !== selectedFirebaseConfigUri.value) { + selectedFirebaseConfigUri.value = allConfigUris[0]; + } + }), + }); + + let disposable: Disposable | undefined; + context.subscriptions.push({ dispose: () => disposable?.dispose() }); + context.subscriptions.push({ + dispose: effect(() => { + disposable?.dispose(); + disposable = undefined; + firebaseRC.value = undefined; + + const configUri = selectedFirebaseConfigUri.value; + if (!configUri) { + return; + } + + disposable = configWatcher?.onDidChange((uri) => { + // ignore changes from firebase.json files that are not the selected one + if (uri.fsPath !== configUri.fsPath) { + firebaseConfig.value = _readFirebaseConfig(configUri); + } + }); + + firebaseConfig.value = _readFirebaseConfig(configUri); + }), + }); + + // Bind the list of URIs to webviews + context.subscriptions.push({ + dispose: effect(() => { + // Listen to changes + allFirebaseConfigsUris.value; + selectedFirebaseConfigUri.value; + + notifyFirebaseConfigListChanged(broker); + }), + }); + context.subscriptions.push({ + dispose: broker.on("getInitialFirebaseConfigList", () => { + notifyFirebaseConfigListChanged(broker); + }), + }); + context.subscriptions.push({ + dispose: broker.on("selectFirebaseConfig", (uri) => { + selectedFirebaseConfigUri.value = allFirebaseConfigsUris.value.find( + (u) => displayStringForUri(u) === uri, + ); + }), + }); +} + +export async function registerConfig( + context: vscode.ExtensionContext, + broker: ExtensionBrokerImpl, +) { + // On getInitialData, forcibly notifies the extension. + context.subscriptions.push({ + dispose: broker.on("getInitialData", () => { + notifyFirebaseConfig(broker); + }), + }); + + // Register configs before RC as the path to RC depends on the path to configs. + await registerFirebaseConfig(context, broker); + await registerRc(context, broker); +} + +/** @internal */ +export function _readRC(uri: vscode.Uri): Result { + return Result.guard(() => { + // RC.loadFile silences errors and returns a non-empty object if the rc file is + // missing. Let's load it ourselves. + + if (!fs.existsSync(uri.fsPath)) { + return undefined; + } + + const json = fs.readFileSync(uri.fsPath); + const data = JSON.parse(json.toString()); + + return new RC(uri.fsPath, data); + }); +} + +/** @internal */ +export function _readFirebaseConfig( + uri: vscode.Uri, +): Result | undefined { + const result = Result.guard(() => { + const config = Config.load({ configPath: uri.fsPath }); + 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 async function _createWatcher( + file: string, +): Promise { + const cwdSignal = computed(() => currentOptions.value.cwd); + const cwd = await firstWhereDefined(cwdSignal); + + return workspace.value.createFileSystemWatcher( + // Using RelativePattern enables tests to use watchers too. + new vscode.RelativePattern(vscode.Uri.file(cwd), file), + ); +} + +async function findFiles(file: string) { + return workspace.value.findFiles(file, "**/node_modules"); +} + +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..6a307c64166 --- /dev/null +++ b/firebase-vscode/src/core/emulators.ts @@ -0,0 +1,221 @@ +import vscode, { Disposable, ThemeColor } from "vscode"; +import { Emulators, getEmulatorUiUrl } from "../cli"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { firebaseRC } from "./config"; +import { EmulatorsStatus, RunningEmulatorInfo } from "../messaging/types"; +import { EmulatorHubClient } from "../../../src/emulator/hubClient"; +import { GetEmulatorsResponse } from "../../../src/emulator/hub"; +import { EmulatorInfo } from "../emulator/types"; +import { signal } from "@preact/signals-core"; +import { dataConnectConfigs } from "../data-connect/config"; +import { runEmulatorIssuesStream } from "../data-connect/emulator-stream"; +import { getSettings } from "../utils/settings"; +export class EmulatorsController implements Disposable { + constructor(private broker: ExtensionBrokerImpl) { + this.emulatorStatusItem.command = "firebase.openFirebaseRc"; + + // called by emulator UI + this.subscriptions.push( + broker.on("getEmulatorInfos", () => this.findRunningCliEmulators()), + ); + + // called by emulator UI + this.subscriptions.push( + broker.on("runStartEmulators", () => { + this.setEmulatorsStarting(); + }), + ); + + // Subscription to open up settings window + this.subscriptions.push( + broker.on("fdc.open-emulator-settings", () => { + vscode.commands.executeCommand( 'workbench.action.openSettings', 'firebase.emulators' ); + }) + ); + + // Subscription to trigger clear emulator data when button is clicked. + this.subscriptions.push( + broker.on("fdc.clear-emulator-data", () => { + vscode.commands.executeCommand("firebase.emulators.clearData"); + }), + ); + + // Subscription to trigger emulator exports when button is clicked. + this.subscriptions.push(broker.on("runEmulatorsExport", () => { + vscode.commands.executeCommand("firebase.emulators.exportData") + })); + } + + readonly emulatorStatusItem = vscode.window.createStatusBarItem("emulators"); + private currExecId = 0; + + public async startEmulators() { + this.setEmulatorsStarting(); + vscode.commands.executeCommand("firebase.emulators.start"); + } + // called by webhook + private readonly findRunningEmulatorsCommand = + vscode.commands.registerCommand( + "firebase.emulators.findRunning", + this.findRunningCliEmulators.bind(this), + ); + + // called by webhook + private readonly emulatorsStoppped = vscode.commands.registerCommand( + "firebase.emulators.stopped", + this.setEmulatorsStopped.bind(this), + ); + + private readonly clearEmulatorDataCommand = vscode.commands.registerCommand( + "firebase.emulators.clearData", + this.clearDataConnectData.bind(this), + ); + + + private readonly exportEmulatorDataCommand = vscode.commands.registerCommand( + "firebase.emulators.exportData", + this.exportEmulatorData.bind(this), + ); + + readonly emulators: { status: EmulatorsStatus; infos?: RunningEmulatorInfo } = + { + status: "stopped", + }; + + private readonly subscriptions: (() => void)[] = []; + + notifyEmulatorStateChanged() { + this.broker.send("notifyEmulatorStateChanged", this.emulators); + } + + // TODO: Move all api calls to CLI DataConnectEmulatorClient + public getLocalEndpoint = () => { + const emulatorInfos = this.emulators.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}`; + }; + + public setEmulatorsRunningInfo(info: EmulatorInfo[]) { + this.emulators.infos = { + uiUrl: getEmulatorUiUrl()!, + displayInfo: info, + }; + this.emulators.status = "running"; + this.notifyEmulatorStateChanged(); + + this.connectToEmulatorStream(); + } + + public setEmulatorsStarting() { + this.emulators.status = "starting"; + this.notifyEmulatorStateChanged(); + + this.currExecId += 1; + const execId = this.currExecId; + + // fallback in case we're stuck in a loading state + setTimeout(async () => { + if (this.emulators.status === "starting" && this.currExecId === execId) { + // notify UI to show reset + this.broker.send("notifyEmulatorsHanging", true); + } + }, 10000); // default 10 seconds spin up time + } + + public setEmulatorsStopping() { + this.emulators.status = "stopping"; + this.notifyEmulatorStateChanged(); + } + + public setEmulatorsStopped() { + this.emulators.status = "stopped"; + this.notifyEmulatorStateChanged(); + } + + async findRunningCliEmulators(): Promise< + { status: EmulatorsStatus; infos?: RunningEmulatorInfo } + > { + const hubClient = this.getHubClient(); + if (hubClient) { + const response: GetEmulatorsResponse = await hubClient.getEmulators(); + + if (Object.values(response)) { + this.setEmulatorsRunningInfo(Object.values(response)); + } else { + this.setEmulatorsStopped(); + } + } + return this.emulators; + } + + async clearDataConnectData(): Promise { + const hubClient = this.getHubClient(); + if (hubClient) { + await hubClient.clearDataConnectData(); + vscode.window.showInformationMessage(`Data Connect emulator data has been cleared.`); + } + } + + async exportEmulatorData(): Promise { + const settings = getSettings(); + const exportDir = settings.exportPath; + const hubClient = this.getHubClient(); + if (hubClient) { + // TODO: Make exportDir configurable + await hubClient.postExport({path: exportDir, initiatedBy: "Data Connect VSCode extension"}); + vscode.window.showInformationMessage(`Emulator Data exported to ${exportDir}`); + } + } + + private getHubClient(): EmulatorHubClient | undefined { + const projectId = firebaseRC.value?.tryReadValue?.projects?.default; + const hubClient = new EmulatorHubClient(projectId); + if (hubClient.foundHub()) { + return hubClient; + } else { + this.setEmulatorsStopped(); + } + } + + public async areEmulatorsRunning(): Promise { + if (this.emulators.status === "running") { + return true; + } + return (await this.findRunningCliEmulators())?.status === "running"; + } + + /** FDC specific functions */ + readonly isPostgresEnabled = signal(false); + private connectToEmulatorStream() { + const configs = dataConnectConfigs.value?.tryReadValue!; + + if (this.getLocalEndpoint()) { + // only if FDC emulator endpoint is found + runEmulatorIssuesStream( + configs, + this.getLocalEndpoint()!, + this.isPostgresEnabled, + ); + } + } + + dispose(): void { + this.subscriptions.forEach((subscription) => subscription()); + this.findRunningEmulatorsCommand.dispose(); + this.emulatorStatusItem.dispose(); + this.emulatorsStoppped.dispose(); + this.clearEmulatorDataCommand.dispose(); + this.exportEmulatorDataCommand.dispose(); + } +} diff --git a/firebase-vscode/src/core/env.ts b/firebase-vscode/src/core/env.ts new file mode 100644 index 00000000000..f8489d6be7a --- /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..4032ecf5286 --- /dev/null +++ b/firebase-vscode/src/core/index.ts @@ -0,0 +1,117 @@ +import vscode, { Disposable, ExtensionContext } from "vscode"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { getRootFolders, registerConfig } from "./config"; +import { EmulatorsController } from "./emulators"; +import { registerEnv } from "./env"; +import { pluginLogger, LogLevel } from "../logger-wrapper"; +import { getSettings } from "../utils/settings"; +import { setEnabled } from "../../../src/experiments"; +import { registerUser } from "./user"; +import { currentProjectId, registerProject } from "./project"; +import { registerQuickstart } from "./quickstart"; +import { registerOptions } from "../options"; +import { upsertFile } from "../data-connect/file-utils"; +import { registerWebhooks } from "./webhook"; +import { createE2eMockable } from "../utils/test_hooks"; +import { runTerminalTask } from "../data-connect/terminal"; +import { AnalyticsLogger, DATA_CONNECT_EVENT_NAME } from "../analytics"; +import { EmulatorHub } from "../../../src/emulator/hub"; + +export async function registerCore( + broker: ExtensionBrokerImpl, + context: ExtensionContext, + analyticsLogger: AnalyticsLogger, +): Promise<[EmulatorsController, vscode.Disposable]> { + const settings = getSettings(); + + // Wrap the runTerminalTask function to allow for e2e testing. + const initSpy = createE2eMockable( + async (...args: Parameters) => { + await runTerminalTask(...args); + }, + "init", + async () => {}, + ); + + if (settings.npmPath) { + process.env.PATH += `:${settings.npmPath}`; + } + + if (settings.useFrameworks) { + setEnabled("webframeworks", true); + } + + const sub1 = broker.on("writeLog", async ({ level, args }) => { + pluginLogger[level as LogLevel]("(Webview)", ...args); + }); + + const sub2 = broker.on( + "showMessage", + async ({ msg, options }: { msg: string; options?: any }) => { + 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 () => { + // Check if the user has a workspace open + if ( + !vscode.workspace.workspaceFolders || + vscode.workspace.workspaceFolders.length === 0 + ) { + vscode.window.showErrorMessage( + "You must have a workspace open to run firebase init.", + ); + return; + } + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.INIT); + const projectId = currentProjectId.value || EmulatorHub.MISSING_PROJECT_PLACEHOLDER; + const initCommand = `${settings.firebasePath} init dataconnect --project ${projectId}`; + initSpy.call("firebase init", initCommand, { focus: true }); + }); + + const emulatorsController = new EmulatorsController(broker); + + const openRcCmd = vscode.commands.registerCommand( + "firebase.openFirebaseRc", + () => { + for (const root of getRootFolders()) { + upsertFile(vscode.Uri.file(`${root}/.firebaserc`), () => ""); + } + }, + ); + + registerConfig(context, broker); + const refreshCmd = vscode.commands.registerCommand( + "firebase.refresh", + async () => { + await vscode.commands.executeCommand("workbench.action.closeSidebar"); + await vscode.commands.executeCommand( + "workbench.view.extension.firebase-data-connect", + ); + }, + ); + + return [ + emulatorsController, + Disposable.from( + openRcCmd, + refreshCmd, + emulatorsController, + initSpy, + registerOptions(context), + registerEnv(broker), + registerUser(broker, analyticsLogger), + registerProject(broker, analyticsLogger), + registerQuickstart(broker), + await registerWebhooks(), + { 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..7bca63c8b63 --- /dev/null +++ b/firebase-vscode/src/core/project.ts @@ -0,0 +1,166 @@ +import vscode, { Disposable } from "vscode"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { computed, effect, Signal } 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"; +import { DATA_CONNECT_EVENT_NAME, AnalyticsLogger } from "../analytics"; +/** Available projects */ +export const projects = globalSignal>( + {}, +); + +/** Currently selected project ID */ +export const currentProjectId = computed(() => { + const rc = firebaseRC.value?.tryReadValue; + + return rc?.projects.default; +}); + +const userScopedProjects = computed( + () => { + return projects.value[currentUser.value?.email ?? ""]; + }, +) as Signal; + +export function registerProject( + broker: ExtensionBrokerImpl, + analyticsLogger: AnalyticsLogger, +): Disposable { + // For testing purposes. + const demoProjectCommand = vscode.commands.registerCommand( + `fdc-graphql.mock.project`, + (projectId: string) => { + updateFirebaseRCProject({ + projectAlias: projectId + ? { + alias: "default", + projectId, + } + : undefined, + }); + broker.send("notifyProjectChanged", { projectId }); + }, + ); + + 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: currentProjectId.value ?? "", + }); + }); + + // Update .firebaserc with defined project ID + const sub3 = effect(() => { + const projectId = currentProjectId.value; + if (projectId) { + updateFirebaseRCProject({ + projectAlias: { alias: "default", projectId }, + }); + } + }); + + const sub5 = broker.on("getInitialData", () => { + let wantProjectId = + currentProjectId.value || + firebaseRC.value?.tryReadValue?.projects["default"]; + // Service accounts should only have one project + if (isServiceAccount.value) { + wantProjectId = userScopedProjects.value?.[0].projectId; + } + + broker.send("notifyProjectChanged", { + projectId: wantProjectId ?? "", + }); + }); + + // TODO: In IDX, should we just client.GetProject() from the metadata server? + // Should we instead hide this command entirely? + const command = vscode.commands.registerCommand( + "firebase.selectProject", + async () => { + if (isServiceAccount.value) { + return; + } else { + try { + analyticsLogger.logger.logUsage( + DATA_CONNECT_EVENT_NAME.PROJECT_SELECT_CLICKED, + ); + + const projects = firstWhereDefined(userScopedProjects); + + const projectId = + (await _promptUserForProject(projects)) ?? currentProjectId.value; + + updateFirebaseRCProject({ + projectAlias: projectId + ? { + alias: "default", + projectId, + } + : undefined, + }); + analyticsLogger.logger.logUsage( + DATA_CONNECT_EVENT_NAME.PROJECT_SELECTED, + ); + } catch (e: any) { + vscode.window.showErrorMessage(e.message); + } + } + }, + ); + + const sub6 = broker.on("selectProject", () => + vscode.commands.executeCommand("firebase.selectProject"), + ); + + return vscode.Disposable.from( + command, + demoProjectCommand, + { dispose: sub1 }, + { dispose: sub2 }, + { dispose: sub3 }, + { 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..f2e17263d28 --- /dev/null +++ b/firebase-vscode/src/core/user.ts @@ -0,0 +1,90 @@ +import { Signal, computed, effect } from "@preact/signals-react"; +import { Disposable } from "vscode"; +import { ServiceAccountUser } from "../types"; +import { User as AuthUser } from "../../../src/types/auth"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { login, logoutUser, requireAuthWrapper } from "../cli"; +import { globalSignal } from "../utils/globals"; +import { DATA_CONNECT_EVENT_NAME, AnalyticsLogger } from "../analytics"; +import * as vscode from "vscode"; + +type User = ServiceAccountUser | AuthUser; + +/** Currently selected user */ +export const currentUser = globalSignal(null); +const isLoadingUser = new Signal(false); + +export const isServiceAccount = computed(() => { + return (currentUser.value as ServiceAccountUser)?.type === "service_account"; +}); + +export async function checkLogin() { + return await requireAuthWrapper(); +} + +export function registerUser( + broker: ExtensionBrokerImpl, + analyticsLogger: AnalyticsLogger, +): Disposable { + // For testing purposes. + const userMockCommand = vscode.commands.registerCommand( + `fdc-graphql.mock.user`, + (user: User | null) => { + currentUser.value = user; + broker.send("notifyUserChanged", { user }); + }, + ); + + // For testing purposes. + const loadingUser = vscode.commands.registerCommand( + `fdc-graphql.user`, + () => { + return isLoadingUser.value; + }, + ); + + const getInitialData = async () => { + isLoadingUser.value = true; + currentUser.value = await checkLogin(); + isLoadingUser.value = false; + }; + + getInitialData(); + + const notifyUserChangedSub = effect(() => { + broker.send("notifyUserChanged", { user: currentUser.value }); + }); + + const getInitialDataSub = broker.on("getInitialData", async () => { + await getInitialData(); + }); + + const isLoadingSub = effect(() => { + broker.send("notifyIsLoadingUser", isLoadingUser.value); + }); + + const addUserSub = broker.on("addUser", async () => { + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.LOGIN); + const { user } = await login(); + currentUser.value = user; + }); + + const logoutSub = broker.on("logout", async ({ email }) => { + try { + await logoutUser(email); + currentUser.value = null; + } catch (e) { + // ignored + } + }); + + return Disposable.from( + { dispose: notifyUserChangedSub }, + { dispose: getInitialDataSub }, + { dispose: addUserSub }, + { dispose: logoutSub }, + { dispose: isLoadingSub }, + userMockCommand, + loadingUser, + ); +} diff --git a/firebase-vscode/src/core/webhook.ts b/firebase-vscode/src/core/webhook.ts new file mode 100644 index 00000000000..adc8753b2b2 --- /dev/null +++ b/firebase-vscode/src/core/webhook.ts @@ -0,0 +1,56 @@ +import * as vscode from "vscode"; +import express from "express"; +import { createServer } from "http"; +import bodyParser from "body-parser"; +import { VSCODE_MESSAGE, WebhookBody } from "../../../src/dataconnect/webhook"; +import { pluginLogger } from "../logger-wrapper"; +import { findOpenPort } from "../utils/port_utils"; +import { setTerminalEnvVars } from "../data-connect/terminal"; + +const DEFAULT_PORT = 40001; +export async function registerWebhooks() { + const app = express(); + app.use(bodyParser.json()); // for parsing application/json + + const server = createServer(app); + const port = await findOpenPort(DEFAULT_PORT); + + if (port !== DEFAULT_PORT) { + setTerminalEnvVars({"VSCODE_WEBHOOK_PORT": port.toString()}); + } + + server.listen(port, () => { + pluginLogger.debug(`VSCode notification server listening on port ${port}`); + }); + + app.post("/vscode/notify", (req, res) => { + const webhookData: WebhookBody = req.body; + // Notify extension through vscode commands + switch (webhookData.message) { + case VSCODE_MESSAGE.EMULATORS_STARTED: { + pluginLogger.debug( + "Received emulators started notification. Running detection.", + ); + vscode.commands.executeCommand("firebase.emulators.findRunning"); + break; + } + case VSCODE_MESSAGE.EMULATORS_SHUTDOWN: { + pluginLogger.debug("Received emulators shutdown notification."); + vscode.commands.executeCommand("firebase.emulators.stopped"); + break; + } + default: { + pluginLogger.debug("Received CLI notification."); + } + } + + // Send a response back to the webhook sender if needed + res.sendStatus(200); + }); + + return vscode.Disposable.from({ + dispose: () => { + server.close(); + }, + }); +} 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..ec3d6ada142 --- /dev/null +++ b/firebase-vscode/src/data-connect/ad-hoc-mutations.ts @@ -0,0 +1,310 @@ +import vscode, { Disposable } from "vscode"; +import { + DocumentNode, + GraphQLInputField, + Kind, + ObjectFieldNode, + ObjectTypeDefinitionNode, + OperationDefinitionNode, + OperationTypeNode, + ValueNode, + buildClientSchema, + getNamedType, + isEnumType, + isInputObjectType, + isListType, + print, +} from "graphql"; +import { upsertFile } from "./file-utils"; +import { DataConnectService } from "./service"; +import { DATA_CONNECT_EVENT_NAME } from "../analytics"; +import { dataConnectConfigs } from "./config"; +import { firstWhereDefined } from "../utils/signal"; +import { AnalyticsLogger } from "../analytics"; + +export function registerAdHoc( + dataConnectService: DataConnectService, + analyticsLogger: AnalyticsLogger, +): Disposable { + /** + * 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, + documentPath: string, + ) { + // TODO(hlshen): Revamp makeQuery to utilize live schema, and built out an AST instead of construction the query through string builder + const primitiveTypes = new Set([ + "String", + "Int", + "Int64", + "Boolean", + "Date", + "Timestamp", + "Float", + "Any", + ]); + + const configs = await firstWhereDefined(dataConnectConfigs); + const dataconnectConfig = + configs.tryReadValue?.findEnclosingServiceForPath(documentPath); + + const basePath = dataconnectConfig?.path; + 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 and ListType to obtain the actual type + let fieldType = field.type; + while ( + fieldType.kind === Kind.LIST_TYPE || + 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; + } + + const isEnum = document.definitions.some( + (def) => + def.kind === Kind.ENUM_TYPE_DEFINITION && + def.name.value === targetType.name.value, + ); + if (isEnum) { + 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}}`; + if (!hasField) { + return undefined; + } + return query; + } + + await upsertFile(filePath, () => { + const queryName = `${ast.name.value.charAt(0).toLowerCase()}${ast.name.value.slice(1)}s`; + + return `\n# This is a file for you to write an un-named query.\n# Only one un-named query is allowed per file.\nquery {\n ${queryName}${buildRecursiveObjectQuery(ast)!}\n}`; + }); + } + + /** + * 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, + documentPath: string, + ) { + // generate content for the file + const introspect = await dataConnectService.introspect(); + if (!introspect.data) { + vscode.window.showErrorMessage( + "Failed to generate mutation. Please check your compilation errors.", + ); + return; + } + const schema = buildClientSchema(introspect.data); + const dataType = schema.getType(`${ast.name.value}_Data`); + if (!isInputObjectType(dataType)) { + return; + } + + // get root where dataconnect.yaml lives + const configs = await firstWhereDefined(dataConnectConfigs); + const dataconnectConfig = + configs.tryReadValue?.findEnclosingServiceForPath(documentPath); + const basePath = dataconnectConfig?.path; + + const filePath = vscode.Uri.file( + `${basePath}/${ast.name.value}_insert.gql`, + ); + + await upsertFile(filePath, () => { + 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 = print( + makeAdHocMutation(Object.values(dataType.getFields()), ast.name.value), + ); + return [preamble, adhocMutation].join("\n"); + }); + } + + return Disposable.from( + vscode.commands.registerCommand( + "firebase.dataConnect.schemaAddData", + (ast, uri) => { + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.ADD_DATA); + schemaAddData(ast, uri); + }, + ), + vscode.commands.registerCommand( + "firebase.dataConnect.schemaReadData", + (document, ast, uri) => { + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.READ_DATA); + schemaReadData(document, ast, uri); + }, + ), + ); +} + +export function makeAdHocMutation( + fields: GraphQLInputField[], + singularName: string, +): OperationDefinitionNode { + const argumentFields: ObjectFieldNode[] = []; + + for (const field of fields) { + const type = getNamedType(field.type); + let defaultValue: ValueNode | undefined; + if (isEnumType(type)) { + const enumValues = type.getValues(); + if (enumValues.length > 0) { + defaultValue = { kind: Kind.ENUM, value: enumValues[0].name }; + } + } else { + defaultValue = getDefaultScalarValueNode(type.name); + } + if (!defaultValue) { + continue; + } + + // convert it back to a list + if (isListType(field.type)) { + defaultValue = { kind: Kind.LIST, values: [defaultValue] }; + } + + argumentFields.push({ + kind: Kind.OBJECT_FIELD, + name: { kind: Kind.NAME, value: field.name }, + value: defaultValue, + }); + } + + return { + kind: Kind.OPERATION_DEFINITION, + operation: OperationTypeNode.MUTATION, + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: `${singularName.charAt(0).toLowerCase()}${singularName.slice(1)}_insert`, + }, + arguments: [ + { + kind: Kind.ARGUMENT, + name: { kind: Kind.NAME, value: "data" }, + value: { + kind: Kind.OBJECT, + fields: argumentFields, + }, + }, + ], + }, + ], + }, + }; +} +function getDefaultScalarValueNode(type: string): ValueNode | undefined { + switch (type) { + case "Any": + return { kind: Kind.OBJECT, fields: [] }; + case "Boolean": + return { kind: Kind.BOOLEAN, value: false }; + case "Date": + return { + kind: Kind.STRING, + value: new Date().toISOString().substring(0, 10), + }; + case "Float": + return { kind: Kind.FLOAT, value: "0" }; + case "Int": + return { kind: Kind.INT, value: "0" }; + case "Int64": + return { kind: Kind.INT, value: "0" }; + case "String": + return { kind: Kind.STRING, value: "" }; + case "Timestamp": + return { kind: Kind.STRING, value: new Date().toISOString() }; + case "UUID": + return { kind: Kind.STRING, value: "11111111222233334444555555555555" }; + case "Vector": + return { kind: Kind.LIST, values: [] }; + default: + return undefined; + } +} + +export function getDefaultScalarValue(type: string): string { + switch (type) { + case "Boolean": + return "false"; + case "Date": + return new Date().toISOString().substring(0, 10); + case "Float": + return "0"; + case "Int": + return "0"; + case "Int64": + return "0"; + case "String": + return ""; + case "Timestamp": + return new Date().toISOString(); + case "UUID": + return "11111111222233334444555555555555"; + case "Vector": + return "[]"; + default: + return ""; + } +} diff --git a/firebase-vscode/src/data-connect/ai-tools/firebase-mcp.ts b/firebase-vscode/src/data-connect/ai-tools/firebase-mcp.ts new file mode 100644 index 00000000000..c170b75e79e --- /dev/null +++ b/firebase-vscode/src/data-connect/ai-tools/firebase-mcp.ts @@ -0,0 +1,99 @@ +import { gemini as geminiToolModule } from "../../../../src/init/features/aitools/gemini"; +import * as vscode from "vscode"; +import { firebaseConfig } from "../config"; +import { ExtensionBrokerImpl } from "../../extension-broker"; +import { AnalyticsLogger, DATA_CONNECT_EVENT_NAME } from "../../analytics"; +import { configstore } from "../../../../src/configstore"; + +const GEMINI_EXTENSION_ID = "google.geminicodeassist"; + +async function ensureGeminiExtension(): Promise { + let geminiExtension = vscode.extensions.getExtension(GEMINI_EXTENSION_ID); + + if (geminiExtension) { + if (!geminiExtension.isActive) { + await geminiExtension.activate(); + } + return true; + } + + const selection = await vscode.window.showInformationMessage( + "The Firebase Assistant requires the Gemini Code Assist extension. Do you want to install it?", + "Yes", + "No", + ); + + if (selection !== "Yes") { + vscode.window.showWarningMessage( + "Cannot open Firebase Assistant without the Gemini Code Assist extension.", + ); + return false; + } + + const disposable = vscode.extensions.onDidChange(async () => { + geminiExtension = vscode.extensions.getExtension(GEMINI_EXTENSION_ID); + if (geminiExtension) { + await openGeminiChat(); + disposable.dispose(); + } + }); + vscode.commands.executeCommand( + "workbench.extensions.installExtension", + GEMINI_EXTENSION_ID, + ); + + return false; +} + +// Writes MCP config, then opens up Gemini with a new chat +async function openGeminiChat() { + configstore.set("gemini", true); + writeToGeminiConfig(); + await vscode.commands.executeCommand("cloudcode.gemini.chatView.focus"); + await vscode.commands.executeCommand("geminicodeassist.agent.chat.new"); +} + +export function registerFirebaseMCP( + broker: ExtensionBrokerImpl, + analyticsLogger: AnalyticsLogger, +): vscode.Disposable { + const geminiActivateSub = broker.on("firebase.activate.gemini", async () => { + analyticsLogger.logger.logUsage( + DATA_CONNECT_EVENT_NAME.TRY_FIREBASE_AGENT_CLICKED, + ); + + const geminiReady = await ensureGeminiExtension(); + + if (!geminiReady) { + return; + } + await openGeminiChat(); + }); + + const mcpDocsSub = broker.on("docs.mcp.clicked", () => { + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.MCP_DOCS_CLICKED); + }); + const tosSub = broker.on("docs.tos.clicked", () => { + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.GIF_TOS_CLICKED); + }); + + return vscode.Disposable.from( + { dispose: geminiActivateSub }, + { dispose: mcpDocsSub }, + { dispose: tosSub }, + ); +} + +// Writes the Firebase MCP server to the gemini code assist config file +export function writeToGeminiConfig() { + const config = firebaseConfig.value?.tryReadValue; + if (!config) { + vscode.window.showErrorMessage("Could not read firebase.json"); + // TODO: Consider writing to HOME_DIR in case of this failure + return; + } + + geminiToolModule.configure(config, config.projectDir, [ + /** TODO: Create "dataconnect" .md file */ + ]); +} 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..fd101be8130 --- /dev/null +++ b/firebase-vscode/src/data-connect/code-lens-provider.ts @@ -0,0 +1,248 @@ +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 { GenerateOperationInput } from "./execution/execution"; +import { findCommentsBlocks } from "../utils/find_comments"; + +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(); + } + + refresh() { + this._onChangeCodeLensesEmitter.fire(); + } + + 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: EmulatorsController) { + 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 projectId = this.watch(firebaseRC)?.tryReadValue?.projects.default; + if (!fdcConfigs) { + 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) { + // startToken.line is 1-indexed, range is 0-indexed + 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) { + 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], + }), + ); + + if (projectId) { + codeLenses.push( + new vscode.CodeLens(range, { + title: `$(play) Run (Production – Project: ${projectId})`, + command: "firebase.dataConnect.executeOperation", + tooltip: "Execute the operation (⌘+enter or Ctrl+Enter)", + arguments: [x, operationLocation, InstanceType.PRODUCTION], + }), + ); + } + } + } + } + + const comments = findCommentsBlocks(documentText); + for (let i = 0; i < comments.length; i++) { + const c = comments[i]; + const range = new vscode.Range(c.startLine, 0, c.startLine, 0); + const queryDoc = documentNode.definitions.find((d) => + d.kind === Kind.OPERATION_DEFINITION && + // startToken.line is 1-indexed, endLine is 0-indexed + d.loc?.startToken.line === c.endLine + 2 + ); + const arg: GenerateOperationInput = { + projectId, + document: document, + description: c.text, + insertPosition: c.endIndex + 1, + existingQuery: queryDoc?.loc ? documentText.substring(c.endIndex + 1, queryDoc.loc.endToken.end) : '', + }; + codeLenses.push( + new vscode.CodeLens(range, { + title: queryDoc ? `$(sparkle) Refine Operation` : `$(sparkle) Generate Operation`, + command: "firebase.dataConnect.generateOperation", + tooltip: "Generate the operation (⌘+enter or Ctrl+Enter)", + arguments: [arg], + }), + ); + } + 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[] { + 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 documentPath = document.fileName; + + // Add only at top of document + // if (line === 0) { + // codeLenses.push( + // new vscode.CodeLens(range, { + // title: `Generate Schema`, + // command: "firebase.dataConnect.generateSchema", + // tooltip: "Generate a new schema", + // arguments: [document.getText(), documentPath], + // }), + // ); + // } + + 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, documentPath], + }), + ); + + 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, documentPath], + }), + ); + } + } + + return codeLenses; + } +} +/** + * CodeLensProvider for Configure SDK in Connector.yaml + */ +export class ConfigureSdkCodeLensProvider extends ComputedCodeLensProvider { + provideCodeLenses( + document: vscode.TextDocument, + token: vscode.CancellationToken, + ): vscode.CodeLens[] { + // Wait for configs to be loaded + const fdcConfigs = this.watch(dataConnectConfigs)?.tryReadValue; + if (!fdcConfigs) { + return []; + } + + const codeLenses: vscode.CodeLens[] = []; + const range = new vscode.Range(0, 0, 0, 0); + const serviceConfig = fdcConfigs.findEnclosingServiceForPath( + document.fileName, + ); + const connectorConfig = serviceConfig!.findEnclosingConnectorForPath( + document.fileName, + ); + if (serviceConfig) { + codeLenses.push( + new vscode.CodeLens(range, { + title: `$(tools) Configure Generated SDK`, + command: "fdc.connector.configure-sdk", + tooltip: "Configure a generated SDK for this connector", + arguments: [connectorConfig], + }), + ); + } + + 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..e6849786508 --- /dev/null +++ b/firebase-vscode/src/data-connect/config.ts @@ -0,0 +1,347 @@ +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/load"; +import { Config } from "../config"; +import { DataConnectMultiple } from "../firebaseConfig"; +import path from "path"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import * as fs from "fs"; +import { EmulatorHub } from "../../../src/emulator/hub"; + +export * from "../core/config"; + +export type DataConnectConfigsValue = ResolvedDataConnectConfigs | undefined; +export type DataConnectConfigsError = { + path?: string; + error: Error | unknown; + range: vscode.Range; +}; + +export const dataConnectConfigs = signal< + | Result + | undefined +>(undefined); + +export class ErrorWithPath extends Error { + constructor( + readonly path: string, + readonly error: unknown, + readonly range: vscode.Range, + ) { + super(error instanceof Error ? error.message : `${error}`); + } +} + +export async function registerDataConnectConfigs( + context: vscode.ExtensionContext, + broker: ExtensionBrokerImpl, +) { + function handleResult( + firebaseConfig: Result | undefined, + ): undefined | (() => void) { + // 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< + ResolvedDataConnectConfigs | undefined, + DataConnectConfigsError + >( + async (config) => { + const configs = await _readDataConnectConfigs( + readFdcFirebaseJson(config), + ); + + return new ResultValue< + ResolvedDataConnectConfigs | undefined, + DataConnectConfigsError + >(configs.requireValue); + }, + (err) => { + if (err instanceof ErrorWithPath) { + return { path: err.path, error: err.error, range: err.range }; + } + return { + path: undefined, + error: err, + range: new vscode.Range(0, 0, 0, 0), + }; + }, + ); + + const operation = + configs && + promise.cancelableThen(configs, (configs) => { + return (dataConnectConfigs.value = configs); + }); + + return operation?.cancel; + } + + context.subscriptions.push({ + dispose: effect(() => handleResult(firebaseConfig.value)), + }); + + const dataConnectWatcher = await createWatcher( + "**/{dataconnect,connector}.yaml", + ); + if (dataConnectWatcher) { + context.subscriptions.push(dataConnectWatcher); + + dataConnectWatcher.onDidChange(() => handleResult(firebaseConfig.value)); + dataConnectWatcher.onDidCreate(() => handleResult(firebaseConfig.value)); + dataConnectWatcher.onDidDelete(() => handleResult(firebaseConfig.value)); + } + + const hasConfigs = computed( + () => !!dataConnectConfigs.value?.tryReadValue?.values.length, + ); + + context.subscriptions.push({ + dispose: effect(() => { + broker.send("notifyHasFdcConfigs", hasConfigs.value); + }), + }); + + context.subscriptions.push({ + dispose: broker.on("getInitialHasFdcConfigs", () => { + broker.send("notifyHasFdcConfigs", hasConfigs.value); + }), + }); +} + +/** @internal */ +export async function _readDataConnectConfigs( + fdcConfig: DataConnectMultiple, +): Promise> { + async function mapConnector(connectorDirPath: string) { + const connectorYaml = await readConnectorYaml(connectorDirPath).catch( + (err: unknown) => { + const connectorPath = path.normalize( + path.join(connectorDirPath, "connector.yaml"), + ); + throw new ErrorWithPath( + connectorPath, + err, + new vscode.Range(0, 0, 0, 0), + ); + }, + ); + + return new ResolvedConnectorYaml(connectorDirPath, connectorYaml); + } + + async function mapDataConnect(absoluteLocation: string) { + const dataConnectYaml = await readDataConnectYaml(absoluteLocation); + const connectorDirs = dataConnectYaml.connectorDirs; + if (!Array.isArray(connectorDirs)) { + throw new ErrorWithPath( + path.join(absoluteLocation, "dataconnect.yaml"), + `Expected 'connectorDirs' to be an array, but got ${connectorDirs}`, + // TODO(rrousselGit): Decode Yaml using AST to have the error message point to the `connectorDirs:` line + new vscode.Range(0, 0, 0, 0), + ); + } + + const resolvedConnectors = await Promise.all( + connectorDirs.map((relativeConnector) => { + const absoluteConnector = asAbsolutePath( + relativeConnector, + absoluteLocation, + ); + const connectorPath = path.join(absoluteConnector, "connector.yaml"); + try { + // Check if the file exists + if (!fs.existsSync(connectorPath)) { + throw new ErrorWithPath( + path.join(absoluteLocation, "dataconnect.yaml"), + `No connector.yaml found at ${relativeConnector}`, + // TODO(rrousselGit): Decode Yaml using AST to have the error message point to the `connectorDirs:` line + new vscode.Range(0, 0, 0, 0), + ); + } + + return mapConnector(absoluteConnector); + } catch (error) { + if (error instanceof ErrorWithPath) { + throw error; + } + + throw new ErrorWithPath( + connectorPath, + error, + new vscode.Range(0, 0, 0, 0), + ); + } + }), + ); + + return new ResolvedDataConnectConfig( + absoluteLocation, + dataConnectYaml, + resolvedConnectors, + dataConnectYaml.location, + ); + } + + return Result.guard(async () => { + const dataConnects = await Promise.all( + fdcConfig + // Paths may be relative to the firebase.json file. + .map((relative) => asAbsolutePath(relative.source, getConfigPath()!)) + .map(async (absolutePath) => { + try { + return await mapDataConnect(absolutePath); + } catch (error) { + if (error instanceof ErrorWithPath) { + throw error; + } + + throw new ErrorWithPath( + path.join(absolutePath, "dataconnect.yaml"), + error, + new vscode.Range(0, 0, 0, 0), + ); + } + }), + ); + + 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: ResolvedConnectorYaml[], + readonly dataConnectLocation: string, + ) {} + + get connectorIds(): string[] { + const result: string[] = []; + + for (const connector of this.resolvedConnectors) { + const id = connector.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 { + if (!getConfigPath()) { + return this.path.split("/").pop()!; + } + return path.relative(getConfigPath()!, this.path); + } + + get relativeSchemaPath(): string { + return this.schemaDir.replace(".", this.relativePath); + } + + get relativeConnectorPaths(): string[] { + return this.connectorDirs.map((connectorDir) => + connectorDir.replace(".", this.relativePath), + ); + } + + findConnectorById(connectorId: string): ResolvedConnectorYaml | undefined { + return this.resolvedConnectors.find( + (connector) => connector.value.connectorId === connectorId, + ); + } + + containsPath(path: string) { + return isPathInside(path, this.path); + } + + findEnclosingConnectorForPath(filePath: string) { + return this.resolvedConnectors.find( + (connector) => connector?.containsPath(filePath) ?? false, + ); + } +} + +/** The fully resolved `dataconnect.yaml` and its connectors */ +export class ResolvedDataConnectConfigs { + constructor(readonly values: DeepReadOnly) {} + + get serviceIds(): string[] { + return this.values.map((config) => config.value.serviceId); + } + + get allConnectors(): ResolvedConnectorYaml[] { + return this.values.flatMap((dc) => dc.resolvedConnectors); + } + + findById(serviceId: string): ResolvedDataConnectConfig { + const dc = this.values.find((dc) => dc.value.serviceId === serviceId); + if (!dc) { + throw new Error(`No dataconnect.yaml with serviceId ${serviceId}. Available: ${this.serviceIds.join(", ")}`); + } + return dc; + } + + findEnclosingServiceForPath(filePath: string): ResolvedDataConnectConfig { + const dc = this.values.find((dc) => dc.containsPath(filePath)); + if (!dc) { + throw new Error(`No enclosing dataconnect.yaml found for path ${filePath}. Available Paths: ${this.values.map((dc) => dc.path).join(", ")}`); + } + return dc; + } + + getApiServicePathByPath(projectId: string | undefined, path: string): string { + const dataConnectConfig = this.findEnclosingServiceForPath(path); + const serviceId = dataConnectConfig?.value.serviceId; + const locationId = dataConnectConfig?.dataConnectLocation; + // FDC emulator can service multiple services keyed by serviceId. + // ${projectId} and ${locationId} aren't used to resolve emulator service. + projectId = projectId || EmulatorHub.MISSING_PROJECT_PLACEHOLDER; + 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..be4d3f5b816 --- /dev/null +++ b/firebase-vscode/src/data-connect/connectors.ts @@ -0,0 +1,525 @@ +import vscode, { + Disposable, + ExtensionContext, + InputBoxValidationMessage, + InputBoxValidationSeverity, +} 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, AnalyticsLogger } from "../analytics"; + +export function registerConnectors( + context: ExtensionContext, + broker: ExtensionBrokerImpl, + dataConnectService: DataConnectService, + analyticsLogger: AnalyticsLogger, +): 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.`, + }; + } + + return {} as InputBoxValidationMessage; + } + + 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) => { + analyticsLogger.logger.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..61826e2f502 --- /dev/null +++ b/firebase-vscode/src/data-connect/core-compiler.ts @@ -0,0 +1,143 @@ +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 } from "./config"; + +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: Record = {}; + const dcPath = configs.values[0].path; + for (const error of gqlErrors) { + if (error.message.includes("INSECURE")) { + // Don't surface insecure operation issues for now; we need to be able to compare with a deployed source for these to be accurately presented. + continue; + } + 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] || { line: 0, column: 0 }), + }); + perFileDiagnostics[absFilePath] = perFileDiagnostic; + } + return Object.keys(perFileDiagnostics).map((key) => { + return [ + Uri.file(key), + perFileDiagnostics[key], + ] 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: string) { + 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..e15877503e7 --- /dev/null +++ b/firebase-vscode/src/data-connect/deploy.ts @@ -0,0 +1,139 @@ +import * as vscode from "vscode"; +import { firstWhere, firstWhereDefined } from "../utils/signal"; +import { currentOptions } from "../options"; +import { dataConnectConfigs } from "./config"; +import { createE2eMockable } from "../utils/test_hooks"; +import { runCommand } from "./terminal"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { DATA_CONNECT_EVENT_NAME, AnalyticsLogger } from "../analytics"; +import { getSettings } from "../utils/settings"; + +function createDeployOnlyCommand(serviceConnectorMap: { + [key: string]: string[]; +}): string { + return ( + "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, + analyticsLogger: AnalyticsLogger, +): vscode.Disposable { + const settings = getSettings(); + + const deploySpy = createE2eMockable( + async (...args: Parameters) => { + // Have the "deploy" return "void" for easier mocking (no return value when spied). + runCommand(...args); + }, + "deploy", + async () => {}, + ); + + const deployAllCmd = vscode.commands.registerCommand("fdc.deploy-all", () => { + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.DEPLOY_ALL); + deploySpy.call(`${settings.firebasePath} deploy --only dataconnect`); + }); + + const deployCmd = vscode.commands.registerCommand("fdc.deploy", async () => { + analyticsLogger.logger.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)) ?? []; + } + + deploySpy.call( + `${settings.firebasePath} ${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<{ + picked: boolean; + label: string; + }>(options as any, { + title: `Select connectors to deploy for: ${serviceId}`, + canPickMany: true, + }); + + return picked?.filter((e) => e.picked).map((c) => c.label); +} diff --git a/firebase-vscode/src/data-connect/diagnostics.ts b/firebase-vscode/src/data-connect/diagnostics.ts new file mode 100644 index 00000000000..6f10038a40d --- /dev/null +++ b/firebase-vscode/src/data-connect/diagnostics.ts @@ -0,0 +1,46 @@ +import { effect, Signal } from "@preact/signals-core"; +import * as vscode from "vscode"; +import { Result } from "../result"; +import { + DataConnectConfigsError, + DataConnectConfigsValue, + ErrorWithPath, +} from "./config"; + +export function registerDiagnostics( + context: vscode.ExtensionContext, + dataConnectConfigs: Signal< + | Result + | undefined + >, +) { + const collection = + vscode.languages.createDiagnosticCollection("data-connect"); + context.subscriptions.push(collection); + + context.subscriptions.push({ + dispose: effect(() => { + collection.clear(); + + const fdcConfigsValue = dataConnectConfigs.value; + fdcConfigsValue?.switchCase( + (_) => { + // Value. No-op as we're only dealing with errors here + }, + (fdcError) => { + const error = fdcError.error; + + collection.set(vscode.Uri.file(fdcError.path!), [ + new vscode.Diagnostic( + error instanceof ErrorWithPath + ? error.range + : new vscode.Range(0, 0, 0, 0), + error instanceof Error ? error.message : `${error}`, + vscode.DiagnosticSeverity.Error, + ), + ]); + }, + ); + }), + }); +} 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..7a7bcb93a79 --- /dev/null +++ b/firebase-vscode/src/data-connect/emulator-stream.ts @@ -0,0 +1,152 @@ +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", + FILE_RELOAD = "FILE_RELOAD", +} +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"); + +// on schema reload, restart language server and run introspection again +function schemaReload() { + vscode.commands.executeCommand("fdc-graphql.restart"); + vscode.commands.executeCommand("firebase.dataConnect.executeIntrospection"); +} + +/** + * 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); + } + emulatorOutputChannel.appendLine(issueMessage); + + // special handlings + if (issue.kind === Kind.SQL_CONNECTION) { + isPostgresEnabled.value = false; + } + if (issue.kind === Kind.FILE_RELOAD) { + schemaReload(); + } +} + +/** + * 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/execution/execution-history-provider.ts b/firebase-vscode/src/data-connect/execution/execution-history-provider.ts new file mode 100644 index 00000000000..96cd6cdda79 --- /dev/null +++ b/firebase-vscode/src/data-connect/execution/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/execution-store.ts b/firebase-vscode/src/data-connect/execution/execution-store.ts new file mode 100644 index 00000000000..972b73a32d6 --- /dev/null +++ b/firebase-vscode/src/data-connect/execution/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/execution.ts b/firebase-vscode/src/data-connect/execution/execution.ts new file mode 100644 index 00000000000..35c85984079 --- /dev/null +++ b/firebase-vscode/src/data-connect/execution/execution.ts @@ -0,0 +1,518 @@ +import vscode, { + ConfigurationTarget, + Disposable, + ExtensionContext, +} 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, Signal } from "@preact/signals-core"; +import { + OperationDefinitionNode, + OperationTypeNode, + print, + buildClientSchema, + validate, + DocumentNode, + Kind, + TypeNode, + parse, +} from "graphql"; +import { DataConnectService } from "../service"; +import { DataConnectError, toSerializedError } from "../../../common/error"; +import { OperationLocation } from "../types"; +import { InstanceType } from "../code-lens-provider"; +import { DATA_CONNECT_EVENT_NAME, AnalyticsLogger } from "../../analytics"; +import { getDefaultScalarValue } from "../ad-hoc-mutations"; +import { EmulatorsController } from "../../core/emulators"; +import { getConnectorGQLText, insertQueryAt } from "../file-utils"; +import { pluginLogger } from "../../logger-wrapper"; +import * as gif from "../../../../src/gemini/fdcExperience"; +import { ensureGIFApiTos } from "../../../../src/dataconnect/ensureApis"; +import { configstore } from "../../../../src/configstore"; + +interface TypedInput { + varName: string; + type: string | null; +} + +interface ExecutionInput { + ast: OperationDefinitionNode; + location: OperationLocation; + instance: InstanceType; +} + +export interface GenerateOperationInput { + projectId?: string; + document: vscode.TextDocument; + description: string; + insertPosition: number; + existingQuery: string; +} + +export const lastExecutionInputSignal = new Signal(null); + +export function registerExecution( + context: ExtensionContext, + broker: ExtensionBrokerImpl, + dataConnectService: DataConnectService, + analyticsLogger: AnalyticsLogger, + emulatorsController: EmulatorsController, +): 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); + } + }); + + // re run called from execution panel; + const rerunExecutionBroker = broker.on("rerunExecution", () => { + if (!lastExecutionInputSignal.value) { + return; + } + executeOperation( + lastExecutionInputSignal.value.ast, + lastExecutionInputSignal.value.location, + lastExecutionInputSignal.value.instance, + ); + }); + + async function executeOperation( + ast: OperationDefinitionNode, + { document, documentPath, position }: OperationLocation, + instance: InstanceType, + ) { + analyticsLogger.logger.logUsage( + instance === InstanceType.LOCAL + ? DATA_CONNECT_EVENT_NAME.RUN_LOCAL + : DATA_CONNECT_EVENT_NAME.RUN_PROD, + ); + analyticsLogger.logger.logUsage( + instance === InstanceType.LOCAL + ? DATA_CONNECT_EVENT_NAME.RUN_LOCAL + `_${ast.operation}` + : DATA_CONNECT_EVENT_NAME.RUN_PROD + `_${ast.operation}`, + ); + await vscode.window.activeTextEditor?.document.save(); + + // hold last execution in memory, and send operation name to webview + lastExecutionInputSignal.value = { + ast, + location: { document, documentPath, position }, + instance, + }; + broker.send("notifyLastOperation", ast.name?.value ?? "anonymous"); + + // focus on execution panel immediately + vscode.commands.executeCommand( + "data-connect-execution-configuration.focus", + ); + + const configs = vscode.workspace.getConfiguration("firebase.dataConnect"); + + const alwaysExecuteMutationsInProduction = + "alwaysAllowMutationsInProduction"; + + // notify users that emulator is starting + if ( + instance === InstanceType.LOCAL && + !(await emulatorsController.areEmulatorsRunning()) + ) { + vscode.window.showWarningMessage( + "Automatically starting emulator... Please retry `Run local` execution after it's started.", + { modal: false }, + ); + analyticsLogger.logger.logUsage( + DATA_CONNECT_EVENT_NAME.START_EMULATOR_FROM_EXECUTION, + ); + emulatorsController.startEmulators(); + return; + } + + // Warn against using mutations in production. + if ( + instance !== InstanceType.LOCAL && + !configs.get(alwaysExecuteMutationsInProduction) && + ast.operation === OperationTypeNode.MUTATION + ) { + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.RUN_PROD_MUTATION_WARNING); + 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: !process.env.VSCODE_TEST_MODE }, + yes, + always, + ); + + switch (result) { + case yes: + analyticsLogger.logger.logUsage( + DATA_CONNECT_EVENT_NAME.RUN_PROD_MUTATION_WARNING_ACKED + ); + break; + case always: + // If the user selects "always", we update User settings. + configs.update( + alwaysExecuteMutationsInProduction, + true, + ConfigurationTarget.Global, + ); + analyticsLogger.logger.logUsage( + DATA_CONNECT_EVENT_NAME.RUN_PROD_MUTATION_WARNING_ACKED_ALWAYS + ); + break; + default: + analyticsLogger.logger.logUsage( + DATA_CONNECT_EVENT_NAME.RUN_PROD_MUTATION_WARNING_REJECTED + ); + return; + } + } + + // build schema + const introspect = await dataConnectService.introspect(); + if (!introspect.data) { + executionError("Please check your compilation errors"); + return undefined; + } + const schema = buildClientSchema(introspect.data); + + // get all gql files from connector and validate + const gqlText = await getConnectorGQLText(documentPath); + + // Adhoc mutation + if (!gqlText) { + pluginLogger.info("Executing adhoc operation. Skipping validation."); + } else { + try { + const connectorDocumentNode = parse(gqlText); + + const validationErrors = validate(schema, connectorDocumentNode); + + if (validationErrors.length > 0) { + executionError( + `Schema validation errors:`, + JSON.stringify(validationErrors), + ); + return; + } + } catch (error) { + executionError("Schema validation error", error as string); + return; + } + } + + + // if execution args is empty, reset to {} + if (!executionArgsJSON.value) { + executionArgsJSON.value = "{}"; + } + + // Check for missing arguments + const missingArgs = await verifyMissingArgs(ast, executionArgsJSON.value); + + // prompt user to continue execution or modify arguments + if (missingArgs.length > 0) { + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.MISSING_VARIABLES); + // open a modal with option to run anyway or edit args + const editArgs = { title: "Edit variables" }; + const continueExecution = { title: "Continue Execution" }; + const result = await vscode.window.showInformationMessage( + `Missing required variables. Would you like to modify them?`, + { modal: !process.env.VSCODE_TEST_MODE }, + editArgs, + continueExecution, + ); + + if (result === editArgs) { + const missingArgsJSON = getDefaultArgs(missingArgs); + + // combine w/ existing args, and send to webview + const newArgsJsonString = JSON.stringify({ + ...JSON.parse(executionArgsJSON.value), + ...missingArgsJSON, + }); + + broker.send("notifyDataConnectArgs", newArgsJsonString); + return; + } + } + + 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 compiled GQL from the whole connector to support fragments + // In the case of adhoc operation, just send the sole document + query: gqlText || 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), + }); + } + } + + async function generateOperation(arg: GenerateOperationInput) { + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.GENERATE_OPERATION); + if (!arg.projectId) { + vscode.window.showErrorMessage(`Connect a Firebase project to use Gemini in Firebase features.`); + return; + } + try { + const schema = await dataConnectService.schema(); + const prompt = `Generate a Data Connect operation to match this description: ${arg.description} +${arg.existingQuery ? `\n\nRefine this existing operation:\n${arg.existingQuery}` : ''} +${schema ? `\n\nUse the Data Connect Schema:\n\`\`\`graphql +${schema} +\`\`\`` : ""}`; + const serviceName = await dataConnectService.servicePath(arg.document.fileName); + if (!(await ensureGIFApiTos(arg.projectId))) { + if (!(await showGiFToSModal(arg.projectId))) { + return; // ToS isn't accepted. + } + } + const res = await gif.generateOperation(prompt, serviceName, arg.projectId); + await insertQueryAt(arg.document.uri, arg.insertPosition, arg.existingQuery, res); + } catch (e: any) { + vscode.window.showErrorMessage(`Failed to generate query: ${e.message}`); + } + } + + async function showGiFToSModal(projectId: string): Promise { + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.GIF_TOS_MODAL); + const tos = "Terms of Service"; + const enable = "Enable"; + const result = await vscode.window.showWarningMessage( + "Gemini in Firebase", + { + modal: !process.env.VSCODE_TEST_MODE, + detail: "Gemini in Firebase helps you write Data Connect queries.", + }, + enable, + tos, + ); + switch (result) { + case enable: + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.GIF_TOS_MODAL_ACKED); + configstore.set("gemini", true); + await ensureGIFApiTos(projectId); + return true; + case tos: + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.GIF_TOS_MODAL_CLICKED); + vscode.env.openExternal( + vscode.Uri.parse( + "https://firebase.google.com/docs/gemini-in-firebase#how-gemini-in-firebase-uses-your-data", + ), + ); + default: + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.GIF_TOS_MODAL_REJECTED); + break; + } + return false; + } + + const sub4 = broker.on( + "definedDataConnectArgs", + (value) => (executionArgsJSON.value = value), + ); + + return Disposable.from( + { dispose: sub1 }, + { dispose: sub2 }, + { dispose: sub3 }, + { dispose: sub4 }, + { dispose: rerunExecutionBroker }, + registerWebview({ + name: "data-connect-execution-configuration", + context, + broker, + }), + registerWebview({ + name: "data-connect-execution-results", + context, + broker, + }), + executionHistoryTreeView, + vscode.commands.registerCommand( + "firebase.dataConnect.executeOperation", + async (ast, location, instanceType: InstanceType) => { + await executeOperation(ast, location, instanceType); + }, + ), + vscode.commands.registerCommand( + "firebase.dataConnect.generateOperation", + async (arg: GenerateOperationInput) => { + await generateOperation(arg); + }, + ), + vscode.commands.registerCommand( + "firebase.dataConnect.selectExecutionResultToShow", + (executionId) => { + selectExecutionId(executionId); + }, + ), + vscode.commands.registerCommand( + "firebase.openJsonDocument", + async (content) => { + await vscode.workspace.openTextDocument({ language: "json", content }); + }, + ), + ); +} + +function executionError(message: string, error?: string) { + vscode.window.showErrorMessage( + `Failed to execute operation: ${message}: \n${JSON.stringify(error, undefined, 2)}`, + ); + throw new Error(error); +} + +function getArgsWithTypeFromOperation( + ast: OperationDefinitionNode, +): TypedInput[] { + if (!ast.variableDefinitions) { + return []; + } + return ast.variableDefinitions.map((variable) => { + const varName = variable.variable.name.value; + + const typeNode = variable.type; + + function getType(typeNode: TypeNode): string | null { + // Same as previous example + switch (typeNode.kind) { + case "NamedType": + return typeNode.name.value; + case "ListType": + const innerTypeName = getType(typeNode.type); + return `[${innerTypeName}]`; + case "NonNullType": + const nonNullTypeName = getType(typeNode.type); + return `${nonNullTypeName}!`; + default: + return null; + } + } + + const type = getType(typeNode); + + return { varName, type }; + }); +} + +// checks if required arguments are present in payload +async function verifyMissingArgs( + ast: OperationDefinitionNode, + jsonArgs: string, +): Promise { + let userArgs: { [key: string]: any }; + try { + userArgs = JSON.parse(jsonArgs); + } catch (e: any) { + executionError("Invalid JSON: ", e); + return []; + } + + const argsWithType = getArgsWithTypeFromOperation(ast); + if (!argsWithType) { + return []; + } + return argsWithType + .filter((arg) => arg.type?.includes("!")) + .filter((arg) => !userArgs[arg.varName]); +} + +function getDefaultArgs(args: TypedInput[]) { + return args.reduce((acc: { [key: string]: any }, arg) => { + const defaultValue = getDefaultScalarValue(arg.type as string); + + acc[arg.varName] = defaultValue; + return acc; + }, {}); +} 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..72c954a1bbf --- /dev/null +++ b/firebase-vscode/src/data-connect/explorer-provider.ts @@ -0,0 +1,234 @@ +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) { + throw new Error(`Expected field ${element} to be defined but was not.`); + } + + 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 || !this.typeSystem) { + return []; + } + // 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 field = this._field(element); + if (field) { + const unwrapped = this._baseType(field); + 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: any = 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..c021dbe58b8 --- /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..bf544d7261e --- /dev/null +++ b/firebase-vscode/src/data-connect/file-utils.ts @@ -0,0 +1,135 @@ +import vscode, { Uri } from "vscode"; +import path from "path"; +import * as fs from "fs"; + +import { dataConnectConfigs } from "./config"; +import { pluginLogger } from "../logger-wrapper"; + +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); + + // Have to write to file system first before opening + // otherwise we can't save it without closing it + if (!doesFileExist) { + vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(content())); + } + + // Opens existing text document + const doc = await vscode.workspace.openTextDocument(uri); + await vscode.window.showTextDocument(doc); +} + +export function getHighlightedText(): string { + const editor = vscode.window.activeTextEditor; + if (!editor) { + return ""; + } + const selection = editor.selection; + + const selectionRange = new vscode.Range( + selection.start.line, + selection.start.character, + selection.end.line, + selection.end.character, + ); + return editor.document.getText(selectionRange); +} + +export async function insertQueryAt(uri: vscode.Uri, at: number, existing: string, replace: string): Promise { + const doc = await vscode.workspace.openTextDocument(uri); + const text = doc.getText(); + if (!existing) { + if (text[at-1] !== "\n") { + replace = "\n" + replace; + } + const newText = text.slice(0, at) + replace + text.slice(at); + await vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(newText)); + return; + } + if (text.slice(at, at + existing.length) !== existing) { + throw new Error("The existing query was updated."); + } + const newText = text.slice(0, at) + replace + text.slice(at + existing.length); + await vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(newText)); +} + +// given a file path, compile all gql files for the associated connector +export async function getConnectorGqlFiles( + filePath: string, +): Promise { + const service = + dataConnectConfigs?.value?.tryReadValue?.findEnclosingServiceForPath( + filePath || "", + ); + + if (!service) { + // The entrypoint is not a codelens file, so we can't determine the service. + return []; + } + + const gqlFiles: string[] = []; + const activeDocumentConnector = service.findEnclosingConnectorForPath( + vscode.window.activeTextEditor?.document.uri.fsPath || "", + ); + + return await findGqlFiles(activeDocumentConnector?.path || ""); +} + +export async function getConnectorGQLText(filePath: string): Promise { + const files = await getConnectorGqlFiles(filePath); + return getTextFromFiles(files); +} + +export function getTextFromFiles(files: string[]): string { + return files.reduce((acc, filePath) => { + try { + return acc.concat(fs.readFileSync(filePath, "utf-8"), "\n"); + } catch (error) { + console.error(`${filePath} not found. Skipping file.`); + return acc; + } + }, ""); +} + +export async function findGqlFiles(dir: string): Promise { + try { + const entries = await fs.promises.readdir(dir, { withFileTypes: true }); + const files = entries + .filter( + (file) => + !file.isDirectory() && + (file.name.endsWith(".gql") || file.name.endsWith(".graphql")), + ) + .map((file) => path.join(dir, file.name)); + + const folders = entries.filter((folder) => folder.isDirectory()); + + for (const folder of folders) { + files.push(...(await findGqlFiles(path.join(dir, folder.name)))); + } + return files; + } catch (error) { + pluginLogger.error(`Failed to find GQL files: ${error}`); + return []; + } +} \ No newline at end of file diff --git a/firebase-vscode/src/data-connect/index.ts b/firebase-vscode/src/data-connect/index.ts new file mode 100644 index 00000000000..14856f0e6c4 --- /dev/null +++ b/firebase-vscode/src/data-connect/index.ts @@ -0,0 +1,267 @@ +import vscode, { Disposable, ExtensionContext } from "vscode"; +import { Signal, effect } from "@preact/signals-core"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { registerExecution } from "./execution/execution"; +import { registerExplorer } from "./explorer"; +import { registerAdHoc } from "./ad-hoc-mutations"; +import { DataConnectService as FdcService } from "./service"; +import { + ConfigureSdkCodeLensProvider, + 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 { DataConnectToolkit } from "./toolkit"; +import { registerFdcSdkGeneration } from "./sdk-generation"; +import { registerDiagnostics } from "./diagnostics"; +import { AnalyticsLogger } from "../analytics"; +import { registerFirebaseMCP } from "./ai-tools/firebase-mcp"; + +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 connector of enclosingService.resolvedConnectors) { + 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, + analyticsLogger: AnalyticsLogger, +): Disposable { + registerDiagnostics(context, dataConnectConfigs); + const dataConnectToolkit = new DataConnectToolkit(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, + dataConnectToolkit, + emulatorController, + context, + analyticsLogger, + ); + + // register codelens + const operationCodeLensProvider = new OperationCodeLensProvider( + emulatorController, + ); + const schemaCodeLensProvider = new SchemaCodeLensProvider(emulatorController); + const configureSdkCodeLensProvider = new ConfigureSdkCodeLensProvider(); + + // activate FDC toolkit + // 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, + ); + }); + + registerDataConnectConfigs(context, broker); + + return Disposable.from( + dataConnectToolkit, + codeActions, + selectedProjectStatus, + { dispose: sub1 }, + { + dispose: effect(() => { + selectedProjectStatus.text = `$(mono-firebase) ${ + currentProjectId.value ?? "" + }`; + selectedProjectStatus.show(); + }), + }, + registerExecution( + context, + broker, + fdcService, + analyticsLogger, + emulatorController, + ), + registerExplorer(context, broker, fdcService), + registerWebview({ name: "data-connect", context, broker }), + registerAdHoc(fdcService, analyticsLogger), + registerConnectors(context, broker, fdcService, analyticsLogger), + registerFdcDeploy(broker, analyticsLogger), + registerFdcSdkGeneration(broker, analyticsLogger), + registerTerminalTasks(broker, analyticsLogger), + registerFirebaseMCP(broker, analyticsLogger), + 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, + ), + vscode.languages.registerCodeLensProvider( + [{ scheme: "file", language: "yaml", pattern: "**/connector.yaml" }], + configureSdkCodeLensProvider, + ), + { + 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..1882cacd96e --- /dev/null +++ b/firebase-vscode/src/data-connect/language-client.ts @@ -0,0 +1,145 @@ +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: vscode.ExtensionContext, + 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"; + path.join(config.relativeSchemaPath, "**", "*.gql"); + let schemaPaths = [ + path.join(config.relativeSchemaPath, "**", "*.gql"), + path.join(config.relativePath, generatedPath, "**", "*.gql"), + ]; + let documentPaths = config.relativeConnectorPaths.map((connectorPath) => + path.join(connectorPath, "**", "*.gql"), + ); + + // make non windows paths relative + // TODO: figure out why relative paths are absolute on windows + if (process.platform !== "win32") { + schemaPaths = schemaPaths.map((schemaPath) => + path.join("..", schemaPath), + ); + documentPaths = documentPaths.map((documentPath) => + path.join("..", documentPath), + ); + } + + 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"); + }); + + 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/sdk-generation.ts b/firebase-vscode/src/data-connect/sdk-generation.ts new file mode 100644 index 00000000000..441257e4e59 --- /dev/null +++ b/firebase-vscode/src/data-connect/sdk-generation.ts @@ -0,0 +1,169 @@ +import * as vscode from "vscode"; +import { Uri } from "vscode"; +import { firstWhere, firstWhereDefined } from "../utils/signal"; +import { currentOptions } from "../options"; +import { dataConnectConfigs, ResolvedConnectorYaml } from "./config"; +import { runCommand, setTerminalEnvVars } from "./terminal"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { DATA_CONNECT_EVENT_NAME } from "../analytics"; +import { getSettings } from "../utils/settings"; +import { FDC_APP_FOLDER, } from "../../../src/init/features/dataconnect/sdk"; +import { createE2eMockable } from "../utils/test_hooks"; +import { AnalyticsLogger} from "../analytics"; + +export function registerFdcSdkGeneration( + broker: ExtensionBrokerImpl, + analyticsLogger: AnalyticsLogger, +): vscode.Disposable { + const settings = getSettings(); + + // For testing purposes. + const selectFolderSpy = createE2eMockable( + async () => { + return selectAppFolder(); + }, + "select-folder", + async () => { + return Promise.resolve("src/test/test_projects/fishfood/test-node-app"); + }, + ); + + const initSdkCmd = vscode.commands.registerCommand( + "fdc.init-sdk", + (args: { appFolder: string }) => { + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.INIT_SDK_CLI); + // Lets do it from the right directory + const e: Record = {} + e[FDC_APP_FOLDER] = args.appFolder; + setTerminalEnvVars(e); + runCommand(`${settings.firebasePath} init dataconnect:sdk`); + }, + ); + + // codelense from inside connector.yaml file + const configureSDKCodelense = vscode.commands.registerCommand( + "fdc.connector.configure-sdk", + async () => { + analyticsLogger.logger.logUsage( + DATA_CONNECT_EVENT_NAME.INIT_SDK_CODELENSE, + ); + await selectAppFolderAndRunInitSdk(); + }, + ); + + // Sidebar "configure generated sdk" button + const configureSDK = vscode.commands.registerCommand( + "fdc.configure-sdk", + async () => { + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.INIT_SDK); + await selectAppFolderAndRunInitSdk(); + }, + ); + + async function selectAppFolderAndRunInitSdk() { + const appFolder = await selectFolderSpy.call(); + if (appFolder) { + await runInitSdk(appFolder); + } + } + + async function selectAppFolder() { + // confirmation prompt for selecting app folder; skip if configured to skip + const configs = vscode.workspace.getConfiguration("firebase.dataConnect"); + const skipToAppFolderSelect = "skipToAppFolderSelect"; + if (!configs.get(skipToAppFolderSelect)) { + const result = await vscode.window.showInformationMessage( + "Please select your app folder to generate an SDK for.", + { modal: !process.env.VSCODE_TEST_MODE }, + "Yes", + "Don't show again", + ); + if (result !== "Yes" && result !== "Don't show again") { + return; + } + if (result === "Don't show again") { + configs.update( + skipToAppFolderSelect, + true, + vscode.ConfigurationTarget.Global, + ); + } + } + + // open app folder selector + const folderUris: Uri[] | undefined = await vscode.window.showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + title: "Select your app folder to link Data Connect to:", + openLabel: "Select app folder", + }); + if (!folderUris?.length) { + return; + } + return folderUris[0].fsPath; // can only pick one folder, but return type is an array + } + + async function runInitSdk(appFolder: string) { + vscode.commands.executeCommand("fdc.init-sdk", { appFolder }); + } + + const configureSDKSub = broker.on("fdc.configure-sdk", async () => + vscode.commands.executeCommand("fdc.configure-sdk"), + ); + return vscode.Disposable.from( + initSdkCmd, + configureSDK, + configureSDKCodelense, + { + dispose: configureSDKSub, + }, + ); +} + +async function pickService(serviceIds: string[]): Promise { + 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<{ label: string }>(options, { + title: "Select service", + canPickMany: false, + }); + return picked?.label; +} + +async function pickConnector( + connectorIds: string[] | undefined, +): Promise { + 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<{ label: string }>( + options as any, + { + title: `Select connector to generate SDK for.`, + canPickMany: false, + }, + ); + + return picked?.label; +} diff --git a/firebase-vscode/src/data-connect/service.ts b/firebase-vscode/src/data-connect/service.ts new file mode 100644 index 00000000000..797d8f2dc0d --- /dev/null +++ b/firebase-vscode/src/data-connect/service.ts @@ -0,0 +1,263 @@ +import fetch, { Response } from "node-fetch"; +import { ExtensionContext } from "vscode"; +import { + ExecutionResult, + IntrospectionQuery, + getIntrospectionQuery, +} from "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 { dataConnectConfigs } from "../data-connect/config"; + +import { firebaseRC } from "../core/config"; +import { + dataconnectDataplaneClient, + dataconnectOrigin, + executeGraphQL, + DATACONNECT_API_VERSION, +} from "../../../src/dataconnect/dataplaneClient"; + +import { + ExecuteGraphqlRequest, + GraphqlResponse, + GraphqlResponseError, + Impersonation, +} from "../dataconnect/types"; +import { Client, ClientResponse } from "../../../src/apiv2"; +import { InstanceType } from "./code-lens-provider"; +import { pluginLogger } from "../logger-wrapper"; +import { DataConnectToolkit } from "./toolkit"; +import { AnalyticsLogger, DATA_CONNECT_EVENT_NAME } from "../analytics"; + +/** + * DataConnect Emulator service + */ +export class DataConnectService { + constructor( + private authService: AuthService, + private dataConnectToolkit: DataConnectToolkit, + private emulatorsController: EmulatorsController, + private context: ExtensionContext, + private analyticsLogger: AnalyticsLogger, + ) {} + + async servicePath(path: string): Promise { + const dataConnectConfigsValue = await firstWhereDefined(dataConnectConfigs); + // TODO: avoid calling this here and in getApiServicePathByPath + const dcs = dataConnectConfigsValue?.tryReadValue; + if (!dcs) { + throw new Error("cannot find dataconnect.yaml in the project"); + } + const projectId = firebaseRC.value?.tryReadValue?.projects?.default; + return dcs?.getApiServicePathByPath(projectId, path); + } + + private async handleProdResponse( + response: ClientResponse, + ): Promise { + this.analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.RUN_PROD + `_${response.status}`); + if (!(response.status >= 200 && response.status < 300)) { + const errorResponse = response as ClientResponse; + throw new DataConnectError( + `Prod Request failed with status ${response.status}\nError Response: ${JSON.stringify(errorResponse?.body)}`, + ); + } + const successResponse = response as ClientResponse; + return successResponse.body; + } + + private async handleEmulatorResponse( + response: ClientResponse, + ): Promise { + this.analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.RUN_LOCAL + `_${response.status}`); + if (!(response.status >= 200 && response.status < 300)) { + const errorResponse = response as ClientResponse; + throw new DataConnectError( + `Emulator Request failed with status ${response.status}\nError Response: ${JSON.stringify(errorResponse?.body)}`, + ); + } + const successResponse = response as ClientResponse; + return successResponse.body; + } + + /** 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), includeDebugDetails: true } + : { unauthenticated: true, includeDebugDetails: 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 result: ", 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 }; + } + } + + // Fetch the local Data Connect Schema sources via the toolkit introspection service. + async schema(): Promise { + try { + const res = await this.executeGraphQLRead({ + query: `query { _service { schema } }`, + operationName: "", + variables: "{}", + }); + console.log("introspection schema result: ", res); + return (res as any)?.data?._service?.schema || ""; + } catch (e) { + // TODO: surface error that emulator is not connected + pluginLogger.error("error: ", e); + return ""; + } + } + + 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 this.dataConnectToolkit.getFDCToolkitURL()) + + `/v1/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); + if (!servicePath) { + throw new Error("No service found for path: " + params.path); + } + const prodBody: ExecuteGraphqlRequest = { + operationName: params.operationName, + variables: parseVariableString(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 client = dataconnectDataplaneClient(); + pluginLogger.info( + `ExecuteGraphQL (${dataconnectOrigin()}) request: ${JSON.stringify(prodBody, undefined, 4)}`, + ); + const resp = await executeGraphQL(client, servicePath, prodBody); + return this.handleProdResponse(resp); + } else { + const endpoint = this.emulatorsController.getLocalEndpoint(); + if (!endpoint) { + throw new DataConnectError( + `Emulator isn't running. Please start your emulator!`, + ); + } + const client = new Client({ + urlPrefix: endpoint, + apiVersion: DATACONNECT_API_VERSION, + }); + const resp = await executeGraphQL(client, servicePath, prodBody); + return this.handleEmulatorResponse(resp); + } + } + + docsLink() { + return this.dataConnectToolkit.getGeneratedDocsURL(); + } +} + +function parseVariableString(variables: string): Record { + if (!variables) { + return {}; + } + try { + return JSON.parse(variables); + } catch (e: any) { + throw new Error( + "Unable to parse variables as JSON. Double check that that there are no unmatched braces or quotes, or unqouted keys in the variables pane.", + ); + } +} diff --git a/firebase-vscode/src/data-connect/terminal.ts b/firebase-vscode/src/data-connect/terminal.ts new file mode 100644 index 00000000000..aff50552e3b --- /dev/null +++ b/firebase-vscode/src/data-connect/terminal.ts @@ -0,0 +1,136 @@ +import { ExtensionBrokerImpl } from "../extension-broker"; +import vscode, { Disposable, TelemetryLogger, TerminalOptions } from "vscode"; +import { checkLogin } from "../core/user"; +import { DATA_CONNECT_EVENT_NAME, AnalyticsLogger } from "../analytics"; +import { getSettings } from "../utils/settings"; +import { currentProjectId } from "../core/project"; + +let environmentVariables: Record = {}; + +const executionOptions: vscode.ShellExecutionOptions = { + env: environmentVariables, +}; + +export function setTerminalEnvVars(env: Record) { + environmentVariables = {...environmentVariables, ...env}; +} + +export function runCommand(command: string) { + const settings = getSettings(); + setTerminalEnvVars(settings.extraEnv ?? {}); + const terminalOptions: TerminalOptions = { + name: "Data Connect Terminal", + env: environmentVariables, + }; + const terminal = vscode.window.createTerminal(terminalOptions); + terminal.show(); + + // TODO: This fails if the interactive shell is not expecting a command, such + // as when oh-my-zsh asking for (Y/n) to updates during startup. + // Consider using an non-interactive shell. + if (currentProjectId.value) { + command = `${command} --project ${currentProjectId.value}`; + } + if (settings.debug) { + command = `${command} --debug`; + } + terminal.sendText(command); +} + +export function runTerminalTask( + taskName: string, + command: string, + presentationOptions: vscode.TaskPresentationOptions = { focus: true }, +): Promise { + const settings = getSettings(); + setTerminalEnvVars(settings.extraEnv ?? {}); + 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( + `{${e.exitCode}}: Failed to execute ${taskName} with command: ${command}`, + ), + ); + } + } + }); + const task = new vscode.Task( + { type }, + vscode.TaskScope.Workspace, + taskName, + "firebase", + new vscode.ShellExecution(`${command}${settings.debug ? " --debug" : ""}`, executionOptions), + ); + task.presentationOptions = presentationOptions; + await vscode.tasks.executeTask(task); + }); +} + +export function registerTerminalTasks( + broker: ExtensionBrokerImpl, + analyticsLogger: AnalyticsLogger, +): Disposable { + const settings = getSettings(); + + const loginTaskBroker = broker.on("executeLogin", () => { + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.IDX_LOGIN); + runTerminalTask( + "firebase login", + `${settings.firebasePath} login --no-localhost`, + ).then(() => { + checkLogin(); + }); + }); + + const startEmulatorsTask = () => { + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.START_EMULATORS); + + let cmd = `${settings.firebasePath} emulators:start`; + if (currentProjectId.value) { + cmd += ` --project ${currentProjectId.value}`; + } + if (settings.importPath) { + cmd += ` --import ${settings.importPath}`; + } + if (settings.exportOnExit) { + cmd += ` --export-on-exit ${settings.exportPath}`; + } + runTerminalTask( + "firebase emulators", + cmd, + { focus: true }, + ); + }; + const startEmulatorsTaskBroker = broker.on("runStartEmulators", () => { + startEmulatorsTask(); + }); + const startEmulatorsCommand = vscode.commands.registerCommand( + "firebase.emulators.start", + startEmulatorsTask, + ); + + return Disposable.from( + { dispose: loginTaskBroker }, + { dispose: startEmulatorsTaskBroker }, + startEmulatorsCommand, + vscode.commands.registerCommand( + "firebase.dataConnect.runTerminalTask", + (taskName, command) => { + analyticsLogger.logger.logUsage( + DATA_CONNECT_EVENT_NAME.COMMAND_EXECUTION, + { + commandName: command, + }, + ); + runTerminalTask(taskName, command); + }, + ), + ); +} diff --git a/firebase-vscode/src/data-connect/toolkit.ts b/firebase-vscode/src/data-connect/toolkit.ts new file mode 100644 index 00000000000..d01b06f7932 --- /dev/null +++ b/firebase-vscode/src/data-connect/toolkit.ts @@ -0,0 +1,87 @@ +import * as vscode from "vscode"; +import { ExtensionBrokerImpl } from "../extension-broker"; +import { effect } from "@preact/signals-core"; +import { dataConnectConfigs, firebaseConfig } from "./config"; +import { runDataConnectCompiler } from "./core-compiler"; +import { DataConnectToolkitController } from "../../../src/emulator/dataconnectToolkitController"; +import { DataConnectEmulatorArgs } from "../emulator/dataconnectEmulator"; +import { Config } from "../config"; +import { RC } from "../rc"; +import { findOpenPort } from "../utils/port_utils"; +import { pluginLogger } from "../logger-wrapper"; +import { getSettings } from "../utils/settings"; + +const DEFAULT_PORT = 50001; +/** FDC-specific emulator logic; Toolkit and emulator */ +export class DataConnectToolkit implements vscode.Disposable { + constructor(readonly broker: ExtensionBrokerImpl) { + this.subs.push( + effect(() => { + if (!this.isFDCToolkitRunning()) { + const config = firebaseConfig.value?.tryReadValue; + if (config) { + this.startFDCToolkit("./dataconnect", config).then(() => { + this.connectToToolkit(); + }); + } + } + }), + broker.on("getDocsLink", () => { + broker.send("notifyDocksLink", this.getGeneratedDocsURL()); + }), + ); + } + + // special function to start FDC emulator with special flags & port + async startFDCToolkit(configDir: string, config: Config) { + const port = await findOpenPort(DEFAULT_PORT); + const settings = getSettings(); + + const toolkitArgs: DataConnectEmulatorArgs = { + projectId: "toolkit", + listen: [{ address: "localhost", port, family: "IPv4" }], + config, + configDir, + autoconnectToPostgres: false, + enable_output_generated_sdk: true, + enable_output_schema_extensions: true, + extraEnv: settings.extraEnv, + }; + pluginLogger.info(`Starting Data Connect toolkit (version ${DataConnectToolkitController.getVersion()}) on port ${port}`); + return DataConnectToolkitController.start(toolkitArgs); + } + + async stopFDCToolkit() { + pluginLogger.info(`Stopping Data Connect toolkit`); + return DataConnectToolkitController.stop(); + } + + isFDCToolkitRunning() { + return DataConnectToolkitController.isRunning; + } + + getFDCToolkitURL() { + return DataConnectToolkitController.getUrl(); + } + + getGeneratedDocsURL() { + return this.getFDCToolkitURL() + "/docs"; + } + + private readonly subs: Array<() => void> = []; + + // Commands to run after the emulator is started successfully + private async connectToToolkit() { + vscode.commands.executeCommand("firebase.dataConnect.executeIntrospection"); + + const configs = dataConnectConfigs.value?.tryReadValue!; + runDataConnectCompiler(configs, this.getFDCToolkitURL()); + } + + dispose() { + for (const sub of this.subs) { + sub(); + } + this.stopFDCToolkit(); + } +} 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..f4daec6db29 --- /dev/null +++ b/firebase-vscode/src/extension.ts @@ -0,0 +1,131 @@ +import * as vscode from "vscode"; +import { spawnSync } from "child_process"; +import * as semver from "semver"; + +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, + setupFirebasePath, + updateIdxSetting, +} from "./utils/settings"; +import { registerFdc } from "./data-connect"; +import { AuthService } from "./auth/service"; +import { AnalyticsLogger, IDX_METRIC_NOTICE } from "./analytics"; +import { env } from "./core/env"; + +import { setIsVSCodeExtension } from "../../src/vsCodeUtils"; + +// This method is called when your extension is activated +export async function activate(context: vscode.ExtensionContext) { + const analyticsLogger = new AnalyticsLogger(context); + + + await setupFirebasePath(analyticsLogger); + const settings = getSettings(); + logSetup(); + pluginLogger.debug("Activating Firebase extension."); + + const broker = createBroker< + ExtensionToWebviewParamsMap, + WebviewToExtensionParamsMap, + vscode.Webview + >(new ExtensionBroker()); + + const authService = new AuthService(broker); + + // show IDX data collection notice + if (settings.shouldShowIdxMetricNotice && env.value.isMonospace) { + // don't await/block on this + vscode.window.showInformationMessage(IDX_METRIC_NOTICE, "Ok").then(() => { + updateIdxSetting(false); // don't show message again + }); + } + + await checkCLIInstallation(); + + const [emulatorsController, coreDisposable] = await registerCore( + broker, + context, + analyticsLogger, + ); + + context.subscriptions.push( + { dispose: analyticsLogger.endSession }, + { dispose: analyticsLogger.onDispose }, + coreDisposable, + registerWebview({ + name: "fdc_sidebar", + broker, + context, + }), + authService, + registerFdc( + context, + broker, + authService, + emulatorsController, + analyticsLogger, + ), + ); +} + +async function checkCLIInstallation(): Promise { + // This should never error out - it must be best effort. + let message = ""; + try { + // Fetch directly so that we don't need to rely on any tools being presnt on path. + const latestVersionRes = await fetch( + "https://registry.npmjs.org/firebase-tools", + ); + const latestVersion = (await latestVersionRes.json())?.["dist-tags"]?.[ + "latest" + ]; + setIsVSCodeExtension(true); + const env = { ...process.env, VSCODE_CWD: "" }; + const versionRes = spawnSync("firebase", ["--version"], { + env, + shell: process.platform === "win32", + }); + const currentVersion = semver.valid(versionRes.stdout?.toString()); + const npmVersionRes = spawnSync("npm", ["--version"], { + env, + shell: process.platform === "win32", + }); + const npmVersion = semver.valid(npmVersionRes.stdout?.toString()); + if (!currentVersion) { + message = `The Firebase CLI is not installed (or not available on $PATH). If you would like to install it, run ${ + npmVersion + ? "npm install -g firebase-tools" + : "curl -sL https://firebase.tools | bash" + }`; + } else if (semver.lt(currentVersion, latestVersion)) { + let installCommand = + "curl -sL https://firebase.tools | upgrade=true bash"; + if (npmVersion) { + // Despite the presence of npm, the existing command may be standalone. + // Run a special standalone-specific command to tell if it actually is. + const checkRes = spawnSync("firebase", ["--tool:setup-check"], { env }); + if (checkRes.status !== 0) { + installCommand = "npm install -g firebase-tools@latest"; + } + } + message = `There is an outdated version of the Firebase CLI installed on your system. We recommened updating to the latest verion by running ${installCommand}`; + } else { + pluginLogger.info(`Checked firebase-tools, is up to date!`); + } + } catch (err: any) { + pluginLogger.info(`Unable to check firebase-tools installation: ${err}`); + } + + if (message) { + vscode.window.showWarningMessage(message); + } +} diff --git a/firebase-vscode/src/logger-wrapper.ts b/firebase-vscode/src/logger-wrapper.ts new file mode 100644 index 00000000000..ec5b5ce0390 --- /dev/null +++ b/firebase-vscode/src/logger-wrapper.ts @@ -0,0 +1,90 @@ +import * as path from "path"; +import * as vscode from "vscode"; +import * as fs from "fs"; +import * as os from "os"; +import Transport from "winston-transport"; +import { stripVTControlCharacters } from "node:util"; +import { SPLAT } from "triple-beam"; +import { logger as cliLogger, useConsoleLoggers, useFileLogger, tryStringify } from "../../src/logger"; +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 as LogLevel] = (...args: any) => { + const prefixedArgs = ["[Firebase Plugin]", ...args]; + (cliLogger[logLevel as LogLevel] as any)(...prefixedArgs); + }; +} + +/** + * Logging setup for logging to console and to file. + */ +export function logSetup() { + // Log to console (use built in CLI functionality) + process.env.DEBUG = "true"; + useConsoleLoggers(); + + // Log to file + // Only log to file if firebase.debug extension setting is true. + // Re-implement file logger call from ../../src/bin/firebase.ts to not bring + // in the entire firebase.ts file + const rootFolders = getRootFolders(); + // Default to a central path, but write files to a local path if we're in a Firebase directory. + let filePath = path.join( + os.homedir(), + ".cache", + "firebase", + "logs", + "vsce-debug.log", + ); + if ( + rootFolders.length > 0 && + fs.existsSync(path.join(rootFolders[0], "firebase.json")) + ) { + filePath = path.join(rootFolders[0], ".firebase", "logs", "vsce-debug.log"); + } + pluginLogger.info("Logging to path", filePath); + useFileLogger(filePath); + 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: any) { + super(opts); + } + log(info: any, callback: any) { + setImmediate(() => { + this.emit("logged", info); + }); + const segments = [info.message, ...(info[SPLAT] || [])].map(tryStringify); + const text = `[${info.level}] ${stripVTControlCharacters(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..a829e6c9551 --- /dev/null +++ b/firebase-vscode/src/options.ts @@ -0,0 +1,109 @@ +import { RC } from "../../src/rc"; +import { Options } from "../../src/options"; +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: new RC(), + 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 as any, + 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 }, + ); +} diff --git a/firebase-vscode/src/result.ts b/firebase-vscode/src/result.ts new file mode 100644 index 00000000000..4d36b6f0f24 --- /dev/null +++ b/firebase-vscode/src/result.ts @@ -0,0 +1,128 @@ +/** 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 { + private static wrapError( + error: unknown, + onError?: (error: unknown) => ErrorT, + ): ResultError { + if (onError) { + try { + return new ResultError(onError(error)); + } catch (error) { + return Result.wrapError(error, onError); + } + } + + return new ResultError(error as ErrorT); + } + + /** 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: () => Promise, + onError?: (error: unknown) => ErrorT, + ): Result; + static guard( + cb: () => DataT, + onError?: (error: unknown) => ErrorT, + ): Result; + static guard( + cb: () => DataT | Promise, + onError?: (error: unknown) => ErrorT, + ): Result | Promise> { + try { + const value = cb(); + if (value instanceof Promise) { + return value + .then>((value) => new ResultValue(value)) + .catch((err) => Result.wrapError(err, onError)); + } + + return new ResultValue(value); + } catch (error: unknown) { + return Result.wrapError(error, onError); + } + } + + get tryReadValue(): DataT | undefined { + return this.switchCase( + (value) => value, + () => undefined, + ); + } + + get requireValue(): DataT { + return this.switchCase( + (value) => value, + (error) => { + throw error; + }, + ); + } + + switchCase( + value: (value: DataT) => NewT, + error: (error: ErrorT) => NewT, + ): NewT { + const that: unknown = this; + if (that instanceof ResultValue) { + return value(that.value); + } + + return error((that as ResultError).error); + } + + /** + * A `.then`-like method that guarantees to return a `Result` object. + * + * Any exception inside the callback will be caught and converted into an error + * result. + */ + follow( + cb: (prev: DataT) => Result, + onError?: (error: unknown) => ErrorT, + ): Result { + return this.switchCase( + (value) => cb(value), + (error) => Result.wrapError(error, onError), + ); + } + + /** + * A `.then`-like method that guarantees to return a `Result` object. + * It is the same as `follow`, but supports asynchronous callbacks. + */ + followAsync( + cb: (prev: DataT) => Promise>, + onError?: (error: unknown) => ErrorT, + ): Promise> { + return this.switchCase>>( + async (value) => { + try { + return await cb(value); + } catch (error) { + return Result.wrapError(error, onError); + } + }, + async (error) => Result.wrapError(error, onError), + ); + } +} + +export class ResultValue extends Result { + constructor(readonly value: DataT) { + super(); + } +} + +export class ResultError extends Result { + constructor(readonly error: ErrorT) { + 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..f736d15bdfa --- /dev/null +++ b/firebase-vscode/src/test/default_wdio.conf.ts @@ -0,0 +1,71 @@ +import * as path from "path"; +import * as fs from "fs"; + +import * as child_process from "child_process"; +import { Notifications } from "./utils/page_objects/editor"; + +process.env.VSCODE_TEST_MODE = "true"; +// used to preload extension dependencies +const prebuiltExtensionsDir = path.resolve(__dirname, "../../prebuilt-extensions"); +export const vscodeConfigs = { + browserName: "vscode", + browserVersion: "1.96.4", // also possible: "insiders" or a specific version e.g. "1.80.0" + "wdio:vscodeOptions": { + vscodeArgs: { + disableExtensions: false, + extensionsDir: prebuiltExtensionsDir, + }, + // points to directory where extension package.json is located + extensionPath: path.join(__dirname, "..", ".."), + // optional VS Code settings + userSettings: { + "editor.fontSize": 14, + }, + vscodeProxyOptions: { + commandTimeout: 60000, + }, + }, +}; + +export const config: WebdriverIO.Config = { + runner: "local", + autoCompileOpts: { + tsNodeOpts: { + project: "./tsconfig.test.json", + }, + }, + capabilities: [vscodeConfigs], + + // Redirect noisy chromedriver and browser logs to ./logs + outputDir: "./logs", + + logLevel: "debug", + + beforeTest: async function () { + await browser.pause(1000); // give some time for extension dependency to load + }, + + afterTest: async function (test) { + // 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`, + ); + // Only take a screenshot if the test failed + if (test.error !== undefined) { + const screenshotDir = path.join(__dirname, "screenshots"); + fs.mkdirSync(screenshotDir, { recursive: true }); + await browser.saveScreenshot( + path.join(screenshotDir, `${test.parent} - ${test.title}.png`), + ); + } + }, + + services: ["vscode"], + framework: "mocha", + reporters: ["spec"], + mochaOpts: { + ui: "tdd", + timeout: 120000, + }, +}; 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..359db2408f5 --- /dev/null +++ b/firebase-vscode/src/test/empty_wdio.conf.ts @@ -0,0 +1,16 @@ +import { merge } from "lodash"; +import { config as baseConfig, vscodeConfigs } from "./default_wdio.conf"; +import * as path from "path"; + +const emptyPath = path.resolve(process.cwd(), "src/test/test_projects/empty"); + +export const config: WebdriverIO.Config = { + ...baseConfig, + specs: ["./integration/empty/**/*.ts"], + maxInstances: 1, + capabilities: [ + merge(vscodeConfigs, { + "wdio:vscodeOptions": { workspacePath: emptyPath }, + }), + ], +}; 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..7868c2c441e --- /dev/null +++ b/firebase-vscode/src/test/fishfood_wdio.conf.ts @@ -0,0 +1,20 @@ +import { merge } from "lodash"; +import { config as baseConfig, vscodeConfigs } from "./default_wdio.conf"; +import * as path from "path"; + +const fishfoodPath = path.resolve( + process.cwd(), + "src/test/test_projects/fishfood", +); + +export const config: WebdriverIO.Config = { + ...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/init.ts b/firebase-vscode/src/test/integration/empty/init.ts new file mode 100644 index 00000000000..aad9c578e34 --- /dev/null +++ b/firebase-vscode/src/test/integration/empty/init.ts @@ -0,0 +1,124 @@ +import fs from "fs"; +import path from "path"; + +import { FirebaseCommands } from "../../utils/page_objects/commands"; +import { FirebaseSidebar } from "../../utils/page_objects/sidebar"; +import { + addTearDown, + firebaseSuite, + firebaseTest, +} from "../../utils/test_hooks"; +import { mockProject } from "../../utils/projects"; +import { mockUser } from "../../utils/user"; +import { e2eSpy, getE2eSpyCalls } from "../mock"; + +addTearDown(() => { + const emptyProjectPath = path.join( + __dirname, + "..", + "..", + "test_projects", + "empty", + ); + // Reset test_projects/empty to its original state. + // This is necessary because the test modifies the project. + fs.rmdirSync(emptyProjectPath, { recursive: true }); + // Recreate the empty project. + fs.mkdirSync(emptyProjectPath); +}); + +firebaseSuite("Init Firebase", async function () { + firebaseTest("calls init command in an empty project", async function () { + const workbench = await browser.getWorkbench(); + + const sidebar = new FirebaseSidebar(workbench); + await sidebar.openExtensionSidebar(); + + const commands = new FirebaseCommands(); + await commands.waitForUser(); + + await mockUser({ email: "test@gmail.com" }); + await mockProject("test-project"); + + await e2eSpy("init"); + + await sidebar.runInStudioContext(async (firebase) => { + await firebase.initFirebaseBtn.waitForExist(); + await firebase.initFirebaseBtn.waitForDisplayed(); + await firebase.initFirebaseBtn.click(); + }); + + const args = await getE2eSpyCalls("init"); + console.log("args", args); + + if (args[0].includes("firebase init")) { + upsertConfig(); + } + + await sidebar.runInStudioContext(async (studio) => { + expect(await studio.startEmulatorsBtn.waitForDisplayed()).toBeTruthy(); + }); + }); +}); + +async function upsertConfig() { + console.log("Upserting config"); + + // Upsert all config files. + const emptyProjectPath = path.join( + __dirname, + "..", + "..", + "test_projects", + "empty", + ); + const firebaseJsonPath = path.join(emptyProjectPath, "firebase.json"); + const dataconnectYamlPath = path.join( + emptyProjectPath, + "dataconnect", + "dataconnect.yaml", + ); + const connectorYamlPath = path.join( + emptyProjectPath, + "dataconnect", + "connector", + "connector.yaml" + ); + + + // Create the firebase.json file. + fs.writeFileSync( + firebaseJsonPath, + JSON.stringify({ + dataconnect: { + source: "dataconnect", + }, + }), + ); + + // Create the dataconnect directory. + fs.mkdirSync(path.join(emptyProjectPath, "dataconnect")); + fs.mkdirSync(path.join(emptyProjectPath, "dataconnect", "connector")); + // Create the dataconnect.yaml file. + fs.writeFileSync( + dataconnectYamlPath, + ` +specVersion: "v1" +serviceId: "s" +location: "asia-east1" +schema: + source: "./schema" + datasource: + postgresql: + database: "fdcdb" + cloudSql: + instanceId: "s-fdc" + # schemaValidation: "COMPATIBLE" +connectorDirs: ["./connector"] +`.trim(), + "utf8", + ); + + // create connector.yaml file + fs.writeFileSync(connectorYamlPath, ""); +} 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..f01ba6d9957 --- /dev/null +++ b/firebase-vscode/src/test/integration/empty/sidebar.ts @@ -0,0 +1,25 @@ +import { FirebaseCommands } from "../../utils/page_objects/commands"; +import { FirebaseSidebar } from "../../utils/page_objects/sidebar"; +import { firebaseSuite, firebaseTest } from "../../utils/test_hooks"; +import { mockUser } from "../../utils/user"; + +firebaseSuite("Supports opening empty projects", async function () { + firebaseTest("opens an empty project", async function () { + const workbench = await browser.getWorkbench(); + + const sidebar = new FirebaseSidebar(workbench); + const commands = new FirebaseCommands(); + + await sidebar.openExtensionSidebar(); + await commands.waitForUser(); + + await sidebar.runInStudioContext(async (firebase) => { + await firebase.signInWithGoogleLink.waitForExist(); + await firebase.signInWithGoogleLink.waitForDisplayed(); + + expect(await firebase.signInWithGoogleLink.getText()).toBe( + "Sign in with Google", + ); + }); + }); +}); diff --git a/firebase-vscode/src/test/integration/fishfood/adhoc-execution.ts b/firebase-vscode/src/test/integration/fishfood/adhoc-execution.ts new file mode 100644 index 00000000000..2fa2698447d --- /dev/null +++ b/firebase-vscode/src/test/integration/fishfood/adhoc-execution.ts @@ -0,0 +1,122 @@ +import { browser, expect } from "@wdio/globals"; +import { + ExecutionPanel, + HistoryItem, +} from "../../utils/page_objects/execution"; +import { firebaseSuite, firebaseTest } from "../../utils/test_hooks"; +import { EditorView } from "../../utils/page_objects/editor"; +import { + mockProject, + mutationsPath, + queriesPath, + queryWithFragmentPath, +} from "../../utils/projects"; +import { FirebaseCommands } from "../../utils/page_objects/commands"; +import { FirebaseSidebar } from "../../utils/page_objects/sidebar"; +import { mockUser } from "../../utils/user"; +import { Workbench, Notification } from "wdio-vscode-service"; +import { Notifications } from "../../utils/page_objects/notifications"; +import path from "path"; + +firebaseSuite("Execution", async function () { + firebaseTest( + "should be able to start emulator and execute operation", + async function () { + const workbench = await browser.getWorkbench(); + + const sidebar = new FirebaseSidebar(workbench); + await sidebar.openExtensionSidebar(); + + const commands = new FirebaseCommands(); + await commands.waitForUser(); + + await mockUser({ email: "test@gmail.com" }); + await mockProject("test-project"); + + const execution = new ExecutionPanel(workbench); + const editor = new EditorView(workbench); + + // Click run local while emulator is not started + await editor.openFile(mutationsPath); + await editor.runLocalButton.waitForDisplayed(); + await editor.runLocalButton.click(); + + // get start emulator notification + const notificationUtil = new Notifications(workbench); + const startEmulatorsNotif = + await notificationUtil.getStartEmulatorNotification(); + expect(startEmulatorsNotif).toExist(); + + console.log( + "Starting emulators from local execution. Waiting for emulators to start...", + ); + + await commands.waitForEmulators(); + + const current = await sidebar.currentEmulators(); + expect(current).toContain("dataconnect :9399"); + await browser.pause(4000); // strange case where emulators are showing before actually callable + + // Test 1 - Execute adhoc read data + + // Open the schema file + const schemaFilePath = path.join( + __dirname, + "..", + "..", + "test_projects", + "fishfood", + "dataconnect", + "schema", + "schema.gql", + ); + await editor.openFile(schemaFilePath); + + // Verify that inline Read Data button is displayed + const readDataButton = await editor.readDataButton; + await readDataButton.waitForDisplayed(); + + // Click the Read Data button + await readDataButton.click(); + + // Wait a bit for the query to be generated + await browser.pause(5000); + + // Verify the generated query + const activeEditor = await editor.getActiveEditor(); + const editorTitle = activeEditor?.document.fileName.split("/").pop(); + const editorContent = await editor.activeEditorContent(); + + expect(editorContent).toHaveText(`query { + posts{ + id + content + } +}`); + // file should be created, saved, then opened + expect(activeEditor?.document.isDirty).toBe(false); + + await editor.runLocalButton.waitForDisplayed(); + await editor.runLocalButton.click(); + + async function getExecutionStatus(name: string) { + await browser.pause(1000); + let item = await execution.history.getSelectedItem(); + let status = await item.getStatus(); + let label = await item.getLabel(); + while (status === "pending" && label !== name) { + await browser.pause(1000); + item = await execution.history.getSelectedItem(); + status = await item.getStatus(); + } + return item; + } + + // Check the history entry + const item3 = await getExecutionStatus("anonymous"); + expect(await item3.getLabel()).toBe("anonymous"); + expect(await item3.getStatus()).toBe("success"); + await editor.closeAllEditors(); + }, + ); +}); 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..12831e36551 --- /dev/null +++ b/firebase-vscode/src/test/integration/fishfood/deploy.ts @@ -0,0 +1,35 @@ +import { browser, expect } from "@wdio/globals"; +import { FirebaseSidebar } from "../../utils/page_objects/sidebar"; +import { firebaseSuite, firebaseTest } from "../../utils/test_hooks"; +import { e2eSpy, getE2eSpyCalls } from "../mock"; +import { mockUser } from "../../utils/user"; +import { FirebaseCommands } from "../../utils/page_objects/commands"; +import { mockProject } from "../../utils/projects"; + +firebaseSuite("Deployment", async function () { + firebaseTest("Can deploy services", async function () { + const workbench = await browser.getWorkbench(); + + const sidebar = new FirebaseSidebar(workbench); + const commands = new FirebaseCommands(); + + await sidebar.openExtensionSidebar(); + await commands.waitForUser(); + + await mockUser({ email: "test@gmail.com" }); + await mockProject("test-project"); + + await e2eSpy("deploy"); + + await sidebar.startDeploy(); + + const args = await getE2eSpyCalls("deploy"); + + const fbBinary = + process.env.FIREBASE_BINARY || "npx -y firebase-tools@latest"; + + expect(args.length).toBe(1); + expect(args[0].length).toBe(1); + expect(args[0][0]).toEqual(`${fbBinary} deploy --only dataconnect`); + }); +}); diff --git a/firebase-vscode/src/test/integration/fishfood/emulator.ts b/firebase-vscode/src/test/integration/fishfood/emulator.ts new file mode 100644 index 00000000000..4fe930b46dd --- /dev/null +++ b/firebase-vscode/src/test/integration/fishfood/emulator.ts @@ -0,0 +1,79 @@ +import { firebaseSuite, firebaseTest } from "../../utils/test_hooks"; +import { FirebaseCommands } from "../../utils/page_objects/commands"; +import { FirebaseSidebar } from "../../utils/page_objects/sidebar"; + +import { TerminalView } from "../../utils/page_objects/terminal"; +import { Notifications } from "../../utils/page_objects/notifications"; +import { mockUser } from "../../utils/user"; +import { mockProject, schemaPath } from "../../utils/projects"; +import { EditorView } from "../../utils/page_objects/editor"; + +firebaseSuite("Emulator", async function () { + firebaseTest( + "Data connect emulator has export, clear, and connect to stream", + async function () { + const workbench = await browser.getWorkbench(); + const sidebar = new FirebaseSidebar(workbench); + const commands = new FirebaseCommands(); + const terminal = new TerminalView(workbench); + const notifications = new Notifications(workbench); + + await sidebar.openExtensionSidebar(); + await commands.waitForUser(); + + await mockUser({ email: "test@gmail.com" }); + await mockProject("test-project"); + await sidebar.startEmulators(); + console.log("Waiting for emulators to start..."); + await commands.waitForEmulators(); + const current = await sidebar.currentEmulators(); + + expect(current).toContain("dataconnect :9399"); + + // Test 1: clear data button + console.log("Running test: clear data button"); + await sidebar.clearEmulatorData(); + + const text = await terminal.getTerminalText(); + expect( + text.includes("Clearing data from Data Connect data sources"), + ).toBeTruthy(); + + // Test 2: export data button + console.log("Running test: export button"); + + await sidebar.exportEmulatorData(); + const exportNotification = await notifications.getExportNotification(); + expect(exportNotification).toExist(); + + // Test 3: edit the schema to cause a migration error + console.log("Running test: migration error"); + const editor = new EditorView(workbench); + await editor.openFile(schemaPath); + + browser.executeWorkbench((vscode) => { + // necessary to get vscode type + editor.getActiveEditor().then((activeEditor) => { + activeEditor?.edit((editBuilder) => { + // replace String w/ Int + editBuilder.replace( + new vscode.Range( + new vscode.Position(8, 12), + new vscode.Position(8, 18), + ), + "Int", + ); + }); + }); + }); + + // look for a notification w/ sql_migration error + expect( + (await workbench.getNotifications()).find(async (notification) => { + const message = await notification.getMessage(); + return message.includes("Data Connect Emulator: SQL_MIGRATION"); + }), + ).toBeTruthy(); + }, + ); +}); 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..5f722c4abf7 --- /dev/null +++ b/firebase-vscode/src/test/integration/fishfood/execution.ts @@ -0,0 +1,141 @@ +import { browser, expect } from "@wdio/globals"; +import { + ExecutionPanel, + HistoryItem, +} from "../../utils/page_objects/execution"; +import { firebaseSuite, firebaseTest } from "../../utils/test_hooks"; +import { EditorView } from "../../utils/page_objects/editor"; +import { + mockProject, + mutationsPath, + queriesPath, + queryWithFragmentPath, +} from "../../utils/projects"; +import { FirebaseCommands } from "../../utils/page_objects/commands"; +import { FirebaseSidebar } from "../../utils/page_objects/sidebar"; +import { mockUser } from "../../utils/user"; +import { Workbench, Notification } from "wdio-vscode-service"; +import { Notifications } from "../../utils/page_objects/notifications"; + +firebaseSuite("Execution", async function () { + firebaseTest( + "should be able to start emulator and execute operation", + async function () { + const workbench = await browser.getWorkbench(); + + const sidebar = new FirebaseSidebar(workbench); + await sidebar.openExtensionSidebar(); + + const commands = new FirebaseCommands(); + await commands.waitForUser(); + + await mockUser({ email: "test@gmail.com" }); + await mockProject("test-project"); + + const execution = new ExecutionPanel(workbench); + const editor = new EditorView(workbench); + + // Click run local while emulator is not started + await editor.openFile(mutationsPath); + await editor.runLocalButton.waitForDisplayed(); + await editor.runLocalButton.click(); + + // get start emulator notification + const notificationUtil = new Notifications(workbench); + const startEmulatorsNotif = + await notificationUtil.getStartEmulatorNotification(); + expect(startEmulatorsNotif).toExist(); + + console.log( + "Starting emulators from local execution. Waiting for emulators to start...", + ); + + await commands.waitForEmulators(); + + const current = await sidebar.currentEmulators(); + expect(current).toContain("dataconnect :9399"); + await browser.pause(4000); // strange case where emulators are showing before actually callable + + // Test 1 - Execute mutation + console.log(`Running test: executing a mutation`); + + // Update arguments + await execution.open(); + await execution.setVariables(`{"id": "42", "content": "Hello, World!"}`); + + // Insert a post + await editor.openFile(mutationsPath); + + await editor.runLocalButton.waitForDisplayed(); + await editor.runLocalButton.click(); + + async function getExecutionStatus(name: string) { + await browser.pause(1000); + let item = await execution.history.getSelectedItem(); + let status = await item.getStatus(); + let label = await item.getLabel(); + while (status === "pending" && label !== name) { + await browser.pause(1000); + item = await execution.history.getSelectedItem(); + status = await item.getStatus(); + } + + return item; + } + + // Waiting for the execution to finish + let result = await getExecutionStatus("createPost"); + expect(await result.getLabel()).toBe("createPost"); + + // Test 2 - Execute mutation + console.log("Running test: executing a query"); + + await execution.setVariables(`{"id": "42"}`); + + // Execute query + await editor.openFile(queriesPath); + await editor.runLocalButton.waitForDisplayed(); + await editor.runLocalButton.click(); + + // Waiting for the new history entry to appear + await browser.waitUntil(async () => { + const selectedItem = await execution.history.getSelectedItem(); + return (await selectedItem.getLabel()) === "getPost"; + }); + + // Check the history entry + const item2 = await execution.history.getSelectedItem(); + + // Waiting for the execution to finish + await browser.waitUntil(async () => { + const status = await item2.getStatus(); + return status === "success"; + }); + + expect(await item2.getLabel()).toBe("getPost"); + expect(await item2.getDescription()).toHaveText( + 'Arguments: {"id": "42"}', + ); + + // Test 3: Execute operation with fragment + + console.log(`Running test: executing an operation with a fragment`); + + await execution.setVariables(`{}`); + await editor.openFile(queryWithFragmentPath); + await editor.runLocalButton.waitForDisplayed(); + await editor.runLocalButton.click(); + + // Waiting for the new history entry to appear + await browser.waitUntil(async () => { + const selectedItem = await execution.history.getSelectedItem(); + return (await selectedItem.getLabel()) === "fragmentTest"; + }); + + // Check the history entry + const item3 = await getExecutionStatus("fragmentTest"); + expect(await item3.getLabel()).toBe("fragmentTest"); + expect(await item3.getStatus()).toBe("success"); + }, + ); +}); diff --git a/firebase-vscode/src/test/integration/fishfood/exeuction-missing-variables.ts b/firebase-vscode/src/test/integration/fishfood/exeuction-missing-variables.ts new file mode 100644 index 00000000000..c2d5ddb86ed --- /dev/null +++ b/firebase-vscode/src/test/integration/fishfood/exeuction-missing-variables.ts @@ -0,0 +1,71 @@ +import { browser, expect } from "@wdio/globals"; +import { + ExecutionPanel, + HistoryItem, +} from "../../utils/page_objects/execution"; +import { firebaseSuite, firebaseTest } from "../../utils/test_hooks"; +import { EditorView } from "../../utils/page_objects/editor"; +import { mockProject, mutationsPath, queriesPath } from "../../utils/projects"; +import { FirebaseCommands } from "../../utils/page_objects/commands"; +import { FirebaseSidebar } from "../../utils/page_objects/sidebar"; +import { mockUser } from "../../utils/user"; +import { Notifications } from "../../utils/page_objects/notifications"; + +firebaseSuite("Execution", async function () { + firebaseTest("should ask user to add missing variables", async function () { + const workbench = await browser.getWorkbench(); + + const sidebar = new FirebaseSidebar(workbench); + await sidebar.openExtensionSidebar(); + + const commands = new FirebaseCommands(); + await commands.waitForUser(); + + const notification = new Notifications(workbench); + + await mockUser({ email: "test@gmail.com" }); + await mockProject("test-project"); + + const execution = new ExecutionPanel(workbench); + const editor = new EditorView(workbench); + + await sidebar.startEmulators(); + await commands.waitForEmulators(); + + // Update arguments + await execution.open(); + await execution.setVariables(`{"id": "42"}`); + + // Insert a post + await editor.openFile(mutationsPath); + await editor.runLocalButton.waitForDisplayed(); + await editor.runLocalButton.click(); + + const editVariablesNotif = await notification.getEditVariablesNotification(); + + if (!editVariablesNotif) { + throw(new Error("Edit Variables Notification not found")); + } + await notification.editVariablesFromNotification(editVariablesNotif); + + expect(await execution.getVariables()).toEqual(`{"id":"42","content":""}`); + + // click re-run button to continue + await execution.clickRerun(); + + async function getExecutionStatus() { + let item = await execution.history.getSelectedItem(); + let status = await item.getStatus(); + while (status === "pending") { + await browser.pause(1000); + item = await execution.history.getSelectedItem(); + status = await item.getStatus(); + } + + return item; + } + // Waiting for the execution to finish + let result = await getExecutionStatus(); + expect(await result.getLabel()).toBe("createPost"); + }); +}); diff --git a/firebase-vscode/src/test/integration/fishfood/gemini-install.ts b/firebase-vscode/src/test/integration/fishfood/gemini-install.ts new file mode 100644 index 00000000000..f74a310ec53 --- /dev/null +++ b/firebase-vscode/src/test/integration/fishfood/gemini-install.ts @@ -0,0 +1,48 @@ +import { browser, expect } from "@wdio/globals"; +import { firebaseSuite, firebaseTest } from "../../utils/test_hooks"; +import { FirebaseCommands } from "../../utils/page_objects/commands"; +import { Workbench, Notification } from "wdio-vscode-service"; +import { Notifications } from "../../utils/page_objects/notifications"; +import { FirebaseSidebar } from "../../utils/page_objects/sidebar"; + +firebaseSuite("Gemini Install", async function () { + firebaseTest( + "should prompt to install Gemini and open chat view", + async function () { + const workbench = await browser.getWorkbench(); + + const sidebar = new FirebaseSidebar(workbench); + await sidebar.openExtensionSidebar(); + + await sidebar.runInStudioContext(async (studio) => { + await studio.geminiButton.waitForDisplayed(); + await studio.geminiButton.click(); + }); + + const notificationUtil = new Notifications(workbench); + const installNotification = + await notificationUtil.getGeminiInstallNotification(); + expect(installNotification).toExist(); + + // Click "Yes" + await notificationUtil.clickYesFromGeminiInstallNotification( + installNotification!, // verified in expect statement above, + ); + + // Verify that the Gemini chat view is focused + const chatView = await workbench.getEditorView().webView$; + await chatView.waitForExist({ timeout: 50000 }); + const chatViewTitle = await chatView.getTitle(); + expect(chatViewTitle).toBe( + "[Extension Development Host] Gemini Code Assist - Welcome — fishfood", + ); + + await browser.executeWorkbench((vscode) => { + vscode.commands.executeCommand( + "workbench.extensions.uninstallExtension", + "google.geminicodeassist", + ); + }); + }, + ); +}); diff --git a/firebase-vscode/src/test/integration/fishfood/generated-sdk.ts b/firebase-vscode/src/test/integration/fishfood/generated-sdk.ts new file mode 100644 index 00000000000..20bba03bc3f --- /dev/null +++ b/firebase-vscode/src/test/integration/fishfood/generated-sdk.ts @@ -0,0 +1,72 @@ +import { + addTearDown, + firebaseSuite, + firebaseTest, +} from "../../utils/test_hooks"; +import { FirebaseSidebar } from "../../utils/page_objects/sidebar"; +import { EditorView } from "../../utils/page_objects/editor"; +import { mockUser } from "../../utils/user"; +import { FirebaseCommands } from "../../utils/page_objects/commands"; +import { mockProject } from "../../utils/projects"; +import { e2eSpy } from "../mock"; + +import * as fs from "fs"; +import * as path from "path"; + +addTearDown(() => { + const connectorYamlPath = path.join( + __dirname, + "..", + "..", + "test_projects", + "fishfood", + "dataconnect", + "connectors", + "a", + "connector.yaml", + ); + + // Cleanup. + fs.writeFileSync(connectorYamlPath, `connectorId: "a"`); +}); + +// TODO this test is blocked by the native file picker which can't show up in the test environment. +// Either find a way to mock the result of the file picker or find a way to bypass it. +// However, tried to mock the file picker but it didn't work. +firebaseSuite("Generated SDK", async function () { + firebaseTest( + "configuration should insert the correct path in the connector.yaml file", + async function () { + const workbench = await browser.getWorkbench(); + + const commands = new FirebaseCommands(); + await commands.waitForUser(); + + await mockUser({ email: "test@gmail.com" }); + await mockProject("test-project"); + + await e2eSpy("select-folder"); + await commands.setConfigToSkipFolderSelection(); + + const sidebar = new FirebaseSidebar(workbench); + await sidebar.openExtensionSidebar(); + + const editorView = new EditorView(workbench); + + await sidebar.runInStudioContext(async (config) => { + await config.addSdkToAppBtn.waitForDisplayed(); + await config.addSdkToAppBtn.click(); + }); + + const editorContent = await editorView.activeEditorContent(); + + expect(editorContent).toHaveText(` + generate: + javascriptSdk: + outputDir: ../../../test-node-app/dataconnect-generated/js/a-connector + package: "@firebasegen/a-connector" + packageJsonDir: ../../../test-node-app + `); + }, + ); +}); diff --git a/firebase-vscode/src/test/integration/fishfood/graphql.ts b/firebase-vscode/src/test/integration/fishfood/graphql.ts new file mode 100644 index 00000000000..28a2e03592f --- /dev/null +++ b/firebase-vscode/src/test/integration/fishfood/graphql.ts @@ -0,0 +1,293 @@ +import * as fs from "fs"; +import * as path from "path"; + +import { + addTearDown, + firebaseSuite, + firebaseTest, + addSetup, +} from "../../utils/test_hooks"; +import { + SchemaExplorerView, + FirebaseSidebar, +} from "../../utils/page_objects/sidebar"; +import { EditorView } from "../../utils/page_objects/editor"; +import { mockUser } from "../../utils/user"; +import { mockProject } from "../../utils/projects"; +import { FirebaseCommands } from "../../utils/page_objects/commands"; + +const queriesPath = path.join( + __dirname, + "..", + "..", + "test_projects", + "fishfood", + "dataconnect", + "connectors", + "a", + "queries.gql", +); + +addSetup(() => { + const queriesWithSyntaxError = fs.readFileSync( + path.join(__dirname, "..", "queries_with_error.gql"), + ); + // Write the file with error at ./queries.gql + fs.writeFileSync(queriesPath, queriesWithSyntaxError); +}); + +addTearDown(() => { + const originalQueries = fs.readFileSync(queriesPath); + // Delete the file with error at ./queries.gql + fs.writeFileSync(queriesPath, originalQueries); +}); + +firebaseSuite("GraphQL", async function () { + firebaseTest( + "GraphQL queries file with sytntax error should show the error", + async function () { + const workbench = await browser.getWorkbench(); + + const sidebar = new FirebaseSidebar(workbench); + await sidebar.openExtensionSidebar(); + + const commands = new FirebaseCommands(); + await commands.waitForUser(); + + await mockUser({ email: "test@gmail.com" }); + await mockProject("test-project"); + + const editorView = new EditorView(workbench); + await editorView.openFile(queriesPath); + + let diagnostics = await editorView.diagnoseFile(queriesPath); + + await browser.waitUntil( + async () => { + diagnostics = await editorView.diagnoseFile(queriesPath); + return diagnostics.length > 1; + }, + { timeout: 120000 }, + ); + + // Verify that the list of errors contains one from the FDC compiler source. + const fdcErrors = diagnostics.filter( + (diagnostic) => diagnostic.source === "Firebase Data Connect: Compiler", + ); + + // Check that there is at least one error. + expect(diagnostics.length).toBeGreaterThan(0); + expect(fdcErrors.length).toBeGreaterThan(0); + }, + ); + + firebaseTest( + "FDC Explorer should list all mutations and queries", + async function () { + const workbench = await browser.getWorkbench(); + + const sidebar = new FirebaseSidebar(workbench); + await sidebar.openExtensionSidebar(); + + const fdcView = new SchemaExplorerView(workbench); + await fdcView.focusFdcExplorer(); + + // Wait for the TreeView to load and its nodes to be displayed + await fdcView.waitForData(); + + const queries = await fdcView.getQueries(); + const mutations = await fdcView.getMutations(); + + // Verify that the queries and mutations are displayed + expect(queries.length).toBe(5); + expect(mutations.length).toBe(4); + }, + ); + + firebaseTest( + "GraphQL schema file should allow adding new data", + async function () { + const workbench = await browser.getWorkbench(); + + const sidebar = new FirebaseSidebar(workbench); + await sidebar.openExtensionSidebar(); + + const schemaFilePath = path.join( + __dirname, + "..", + "..", + "test_projects", + "fishfood", + "dataconnect", + "schema", + "schema.gql", + ); + + // Open the schema file + const editorView = new EditorView(workbench); + await editorView.openFile(schemaFilePath); + + // Verify that inline Add Data button is displayed + const addDataButton = await editorView.addDataButton; + await addDataButton.waitForDisplayed(); + + // Click the Add Data button + await addDataButton.click(); + + // Wait a bit for the mutation to be generated + await browser.pause(5000); + + // Verify the generated mutation + const activeEditor = await editorView.getActiveEditor(); + const editorTitle = activeEditor?.document.fileName.split("/").pop(); + const editorContent = await editorView.activeEditorContent(); + + expect(editorContent).toHaveText(`mutation { + post_insert(data: { + id: "" # String + content: "" # String + }) + }"`); + // file should be created, saved, then opened + expect(activeEditor?.document.isDirty).toBe(false); + await editorView.closeAllEditors(); + }, + ); + + firebaseTest( + "GraphQL schema file should allow reading new data", + async function () { + const workbench = await browser.getWorkbench(); + + const sidebar = new FirebaseSidebar(workbench); + await sidebar.openExtensionSidebar(); + + const schemaFilePath = path.join( + __dirname, + "..", + "..", + "test_projects", + "fishfood", + "dataconnect", + "schema", + "schema.gql", + ); + + // Open the schema file + const editorView = new EditorView(workbench); + await editorView.openFile(schemaFilePath); + + // Verify that inline Read Data button is displayed + const readDataButton = await editorView.readDataButton; + await readDataButton.waitForDisplayed(); + + // Click the Read Data button + await readDataButton.click(); + + // Wait a bit for the query to be generated + await browser.pause(5000); + + // Verify the generated query + const activeEditor = await editorView.getActiveEditor(); + const editorTitle = activeEditor?.document.fileName.split("/").pop(); + const editorContent = await editorView.activeEditorContent(); + + expect(editorContent).toHaveText(`query { + posts{ + id + content + } +}`); + // file should be created, saved, then opened + expect(activeEditor?.document.isDirty).toBe(false); + await editorView.closeAllEditors(); + }, + ); + + firebaseTest( + "Add Data should generate file in correct folder", + async function () { + const workbench = await browser.getWorkbench(); + + const sidebar = new FirebaseSidebar(workbench); + await sidebar.openExtensionSidebar(); + + const schemaFilePath = path.join( + __dirname, + "..", + "..", + "test_projects", + "fishfood", + "dataconnect", + "schema", + "schema.gql", + ); + + // Open the schema file + const editorView = new EditorView(workbench); + await editorView.openFile(schemaFilePath); + + // Verify that inline Add Data button is displayed + const addDataButton = await editorView.addDataButton; + await addDataButton.waitForDisplayed(); + + // Click the Add Data button + await addDataButton.click(); + + // Wait a bit for the mutation to be generated + await browser.pause(1500); + + // Verify the generated mutation file path + const activeEditor = await editorView.getActiveEditor(); + const filePath = activeEditor?.document.fileName; + expect(filePath).toContain( + "test_projects/fishfood/dataconnect/Post_insert.gql", + ); + + await editorView.closeAllEditors(); + }, + ); + + firebaseTest( + "Read Data should generate file in correct folder", + async function () { + const workbench = await browser.getWorkbench(); + + const sidebar = new FirebaseSidebar(workbench); + await sidebar.openExtensionSidebar(); + + const schemaFilePath = path.join( + __dirname, + "..", + "..", + "test_projects", + "fishfood", + "dataconnect", + "schema", + "schema.gql", + ); + + // Open the schema file + const editorView = new EditorView(workbench); + await editorView.openFile(schemaFilePath); + + // Verify that inline Read Data button is displayed + const readDataButton = await editorView.readDataButton; + await readDataButton.waitForDisplayed(); + + // Click the Read Data button + await readDataButton.click(); + + // Wait a bit for the query to be generated + await browser.pause(1500); + + // Verify the generated query file path + const activeEditor = await editorView.getActiveEditor(); + const filePath = activeEditor?.document.fileName; + expect(filePath).toContain( + "test_projects/fishfood/dataconnect/Post_read.gql", + ); + await editorView.closeAllEditors(); + }, + ); +}); diff --git a/firebase-vscode/src/test/integration/mock.ts b/firebase-vscode/src/test/integration/mock.ts new file mode 100644 index 00000000000..d21132bdad9 --- /dev/null +++ b/firebase-vscode/src/test/integration/mock.ts @@ -0,0 +1,41 @@ +import { addTearDown } from "../utils/test_hooks"; +import { deploy as cliDeploy } from "../../../../src/deploy"; +import * as vscode from "vscode"; +import { runTerminalTask } from "../../data-connect/terminal"; + +export async function e2eSpy(key: string): Promise { + addTearDown(async () => { + await callBrowserSpyCommand(key, { spy: false }); + }); + + await callBrowserSpyCommand(key, { spy: true }); +} + +export function getE2eSpyCalls( + key: "deploy" | "init", +): Promise< + | Array> + | Array> +>; +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( + async (vs: typeof vscode, key, args) => { + return await vs.commands.executeCommand(key, args); + }, + `fdc-graphql.spy.${key}`, + args, + ); + + return result as Array>; +} diff --git a/firebase-vscode/src/test/integration/queries_with_error.gql b/firebase-vscode/src/test/integration/queries_with_error.gql new file mode 100644 index 00000000000..670424a3c3f --- /dev/null +++ b/firebase-vscode/src/test/integration/queries_with_error.gql @@ -0,0 +1,24 @@ +# This file is used to test the codelens error through the extension, +# it contains a syntax error in the query. +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/runTest.ts b/firebase-vscode/src/test/runTest.ts new file mode 100644 index 00000000000..25b13fe4573 --- /dev/null +++ b/firebase-vscode/src/test/runTest.ts @@ -0,0 +1,26 @@ +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/index"); + + // Download VS Code, unzip it and run the integration test + await runTests({ + extensionDevelopmentPath, + extensionTestsPath, + }); + } catch (err) { + console.error("Failed to run tests"); + process.exit(1); + } +} + +main(); diff --git a/firebase-vscode/src/test/suite/index.ts b/firebase-vscode/src/test/suite/index.ts new file mode 100644 index 00000000000..a621be8bb97 --- /dev/null +++ b/firebase-vscode/src/test/suite/index.ts @@ -0,0 +1,39 @@ +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, + require: ["ts-node/register", "@babel/register"], + }); + + 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/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..c371a649f24 --- /dev/null +++ b/firebase-vscode/src/test/suite/src/core/config.test.ts @@ -0,0 +1,615 @@ +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 { + addTearDown, + firebaseSuite, + firebaseTest, +} from "../../../utils/test_hooks"; +import { createFake, createFakeContext, 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(); + const path = createFile( + dir, + "firebase.json", + JSON.stringify(expectedConfig), + ); + + const config = _readFirebaseConfig(vscode.Uri.parse(path)); + assert.deepEqual(config?.requireValue!.data, expectedConfig); + }); + + firebaseTest("returns undefined if firebase.json is not found", () => { + const dir = createTemporaryDirectory(); + + const config = _readFirebaseConfig( + vscode.Uri.parse(`${dir}/firebase.json`), + ); + assert.deepEqual(config, undefined); + }); + + firebaseTest("throws if firebase.json is invalid", () => { + const logs = spyLogs(); + const dir = createTemporaryDirectory(); + const path = createFile(dir, "firebase.json", "invalid json"); + + assert.equal(logs.error.length, 0); + + assert.throws( + () => _readFirebaseConfig(vscode.Uri.parse(path)), + (thrown) => + thrown! + .toString() + .startsWith(`FirebaseError: There was an error loading ${path}:`), + ); + + 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(); + const path = createFile(dir, ".firebaserc", JSON.stringify(expectedConfig)); + + const config = _readRC(vscode.Uri.parse(path)); + assert.deepEqual( + config?.requireValue!.data.projects, + expectedConfig.projects, + ); + }); + + firebaseTest("returns undefined if .firebaserc is not found", () => { + const dir = createTemporaryDirectory(); + + const config = _readRC(vscode.Uri.parse(`${dir}/.firebaserc`)); + assert.deepEqual(config, undefined); + }); + + firebaseTest("throws if .firebaserc is invalid", () => { + const logs = spyLogs(); + const dir = createTemporaryDirectory(); + const path = createFile(dir, ".firebaserc", "invalid json"); + + assert.equal(logs.error.length, 0); + + assert.throws( + () => _readRC(vscode.Uri.parse(path)), + (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 = await _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 context = createFakeContext(); + registerConfig(context, broker); + + // 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 context = createFakeContext(); + registerConfig(context, broker); + + 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 context = createFakeContext(); + registerConfig(context, broker); + + assert.deepEqual(broker.sentLogs, []); + + fs.writeFileSync( + workspaces.byIndex(0)!.firebaseConfigPath, + JSON.stringify(newConfig), + ); + firebaseConfig.value = _readFirebaseConfig( + vscode.Uri.parse(workspaces.byIndex(0)!.firebaseConfigPath), + )!; + + 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 context = createFakeContext(); + registerConfig(context, broker); + + // Should not throw. + }); + + firebaseTest("disposes of the watchers when disposed", async () => { + const broker = createTestBroker(); + const dir = createTemporaryDirectory(); + + const pendingWatchers: any = []; + 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 context = createFakeContext(); + registerConfig(context, broker); + + assert.equal(pendingWatchers.length, 3); + assert.deepEqual(Object.keys(broker.onListeners), ["getInitialData"]); + + context.subscriptions.forEach((sub) => sub.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: any) => { + 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 context = createFakeContext(); + registerConfig(context, broker); + + 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 context = createFakeContext(); + registerConfig(context, broker); + + 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/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/ad-hoc-mutation.test.ts b/firebase-vscode/src/test/suite/src/dataconnect/ad-hoc-mutation.test.ts new file mode 100644 index 00000000000..237bc708c0c --- /dev/null +++ b/firebase-vscode/src/test/suite/src/dataconnect/ad-hoc-mutation.test.ts @@ -0,0 +1,153 @@ +import assert from "assert"; +import { + GraphQLString, + GraphQLNonNull, + GraphQLList, + GraphQLEnumType, + print, + GraphQLInputField, +} from "graphql"; +import { makeAdHocMutation } from "../../../../data-connect/ad-hoc-mutations"; +import { firebaseSuite } from "../../../utils/test_hooks"; + +firebaseSuite("makeAdHocMutation", () => { + // The `makeAdHocMutation` function expects an array of `GraphQLInputField` objects. + // We create plain objects that conform to the structure of `GraphQLInputField` for our tests. + const mockFields = { + string: { + name: "field1", + type: GraphQLString, + description: "", + defaultValue: null, + extensions: {}, + isDeprecated: false, + deprecationReason: null, + astNode: undefined, + } as GraphQLInputField, + listString: { + name: "field2", + type: new GraphQLList(GraphQLString), + description: "", + defaultValue: null, + extensions: {}, + isDeprecated: false, + deprecationReason: null, + astNode: undefined, + } as GraphQLInputField, + enum: { + name: "field3", + type: new GraphQLEnumType({ + name: "TestEnum", + values: { + VALUE1: { value: "VALUE1" }, + VALUE2: { value: "VALUE2" }, + }, + }), + description: "", + defaultValue: null, + extensions: {}, + isDeprecated: false, + deprecationReason: null, + astNode: undefined, + } as GraphQLInputField, + nonNullString: { + name: "field4", + type: new GraphQLNonNull(GraphQLString), + description: "", + defaultValue: null, + extensions: {}, + isDeprecated: false, + deprecationReason: null, + astNode: undefined, + } as GraphQLInputField, + listEnum: { + name: "field5", + type: new GraphQLList( + new GraphQLEnumType({ + name: "TestEnum2", + values: { + VALUEA: { value: "VALUEA" }, + VALUEB: { value: "VALUEB" }, + }, + }), + ), + description: "", + defaultValue: null, + extensions: {}, + isDeprecated: false, + deprecationReason: null, + astNode: undefined, + } as GraphQLInputField, + }; + + test("should generate a mutation with a single scalar field", () => { + const fields = [mockFields.string]; + const singularName = "Test"; + const result = makeAdHocMutation(fields, singularName); + const printedResult = print(result).replace(/\s/g, ""); + + const expected = 'mutation{test_insert(data:{field1:""})}'; + assert.strictEqual(printedResult, expected); + }); + + test("should handle list types", () => { + const fields = [mockFields.listString]; + const singularName = "Test"; + const result = makeAdHocMutation(fields, singularName); + const printedResult = print(result).replace(/\s/g, ""); + + const expected = 'mutation{test_insert(data:{field2:[""]})}'; + assert.strictEqual(printedResult, expected); + }); + + test("should handle enum types, selecting the first enum value", () => { + const fields = [mockFields.enum]; + const singularName = "Test"; + const result = makeAdHocMutation(fields, singularName); + const printedResult = print(result).replace(/\s/g, ""); + + const expected = "mutation{test_insert(data:{field3:VALUE1})}"; + assert.strictEqual(printedResult, expected); + }); + + test("should handle list of enum types", () => { + const fields = [mockFields.listEnum]; + const singularName = "Test"; + const result = makeAdHocMutation(fields, singularName); + const printedResult = print(result).replace(/\s/g, ""); + + const expected = "mutation{test_insert(data:{field5:[VALUEA]})}"; + assert.strictEqual(printedResult, expected); + }); + + test("should handle non-null types", () => { + const fields = [mockFields.nonNullString]; + const singularName = "Test"; + const result = makeAdHocMutation(fields, singularName); + const printedResult = print(result).replace(/\s/g, ""); + + const expected = 'mutation{test_insert(data:{field4:""})}'; + assert.strictEqual(printedResult, expected); + }); + + test("should generate a mutation with a mix of field types", () => { + const fields = [mockFields.string, mockFields.listString, mockFields.enum]; + const singularName = "Test"; + const result = makeAdHocMutation(fields, singularName); + const printedResult = print(result).replace(/\s/g, ""); + + const expected = + 'mutation{test_insert(data:{field1:"",field2:[""],field3:VALUE1})}'; + assert.strictEqual(printedResult, expected); + }); + + test("should generate a mutation with a different singular name", () => { + const fields = [mockFields.string]; + const singularName = "Item"; + const result = makeAdHocMutation(fields, singularName); + const printedResult = print(result).replace(/\s/g, ""); + + const expected = 'mutation{item_insert(data:{field1:""})}'; + assert.strictEqual(printedResult, expected); + }); +}); 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..60c0b49e792 --- /dev/null +++ b/firebase-vscode/src/test/suite/src/dataconnect/config.test.ts @@ -0,0 +1,116 @@ +import assert from "assert"; +import { createTestBroker } from "../../../utils/broker"; +import { firebaseSuite } from "../../../utils/test_hooks"; +import { setupMockTestWorkspaces } from "../../../utils/workspace"; +import { + dataConnectConfigs, + registerDataConnectConfigs, +} from "../../../../data-connect/config"; +import { createTemporaryDirectory } from "../../../utils/fs"; +import { createFake, createFakeContext, 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 context = createFakeContext(); + registerDataConnectConfigs(context, broker); + + 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: any) => { + 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 context = createFakeContext(); + const broker = createTestBroker(); + registerDataConnectConfigs(context, broker); + + 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/empty/README b/firebase-vscode/src/test/test_projects/empty/README new file mode 100644 index 00000000000..18f69b6d33e --- /dev/null +++ b/firebase-vscode/src/test/test_projects/empty/README @@ -0,0 +1 @@ +DO NOT DELETE - Used for testing \ 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..36fd8c67a70 --- /dev/null +++ b/firebase-vscode/src/test/test_projects/fishfood/.firebaserc @@ -0,0 +1,8 @@ +{ + "projects": { + "default": "test-project" + }, + "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..a53b46eb456 --- /dev/null +++ b/firebase-vscode/src/test/test_projects/fishfood/dataconnect/connectors/a/connector.yaml @@ -0,0 +1 @@ +connectorId: "a" 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..1294b848196 --- /dev/null +++ b/firebase-vscode/src/test/test_projects/fishfood/dataconnect/connectors/a/mutations.gql @@ -0,0 +1,14 @@ +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 }) +} + +fragment CommentContent on Comment { + 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/connectors/a/queryWithFragment.gql b/firebase-vscode/src/test/test_projects/fishfood/dataconnect/connectors/a/queryWithFragment.gql new file mode 100644 index 00000000000..23e4a44654c --- /dev/null +++ b/firebase-vscode/src/test/test_projects/fishfood/dataconnect/connectors/a/queryWithFragment.gql @@ -0,0 +1,5 @@ +query fragmentTest @auth(level: PUBLIC) { + comments { + ...CommentContent + } +} 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..d51277dc6c7 --- /dev/null +++ b/firebase-vscode/src/test/test_projects/fishfood/dataconnect/dataconnect.yaml @@ -0,0 +1,11 @@ +specVersion: "v1alpha" +serviceId: "us-east" +location: "europe-north1" +schema: + source: "./schema" + datasource: + postgresql: + database: "emulator" + cloudSql: + instanceId: "dataconnect-test" +connectorDirs: ["./connectors/a"] 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..62116e480bb --- /dev/null +++ b/firebase-vscode/src/test/test_projects/fishfood/firebase.json @@ -0,0 +1,14 @@ +{ + "dataconnect": { + "source": "./dataconnect" + }, + "emulators": { + "dataconnect": { + "port": 9399 + }, + "ui": { + "enabled": false + }, + "singleProjectMode": true + } +} diff --git a/firebase-vscode/src/test/test_projects/fishfood/test-node-app/package.json b/firebase-vscode/src/test/test_projects/fishfood/test-node-app/package.json new file mode 100644 index 00000000000..18ec65486af --- /dev/null +++ b/firebase-vscode/src/test/test_projects/fishfood/test-node-app/package.json @@ -0,0 +1,14 @@ +{ + "name": "test-node-app", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@firebasegen/a-connector": "file:dataconnect-generated/js/a-connector" + } +} diff --git a/firebase-vscode/src/test/tsconfig.test.json b/firebase-vscode/src/test/tsconfig.test.json new file mode 100644 index 00000000000..6ea3cb7059e --- /dev/null +++ b/firebase-vscode/src/test/tsconfig.test.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2020", + "lib": ["es2020", "dom"], + "strict": true /* enable all strict type-checking options */, + "allowJs": true, + "checkJs": false, + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "outDir": "../../dist/test", + "rootDirs": ["../", "../../common/", "../../../src/"], + "jsx": "react", + "types": [ + "node", + "@wdio/mocha-framework", + "@wdio/globals/types", + "@types/mocha", + "wdio-vscode-service" + ] + }, + "exclude": ["**/*.test.ts"], + "include": ["../**/*", "../../common/**/*", "../../../src/types/**/*"] +} diff --git a/firebase-vscode/src/test/utils/broker.ts b/firebase-vscode/src/test/utils/broker.ts new file mode 100644 index 00000000000..48c732f1721 --- /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 as any); + + return () => { + const index = listenersForMessage.indexOf(listener as any); + 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/install-extensions.ts b/firebase-vscode/src/test/utils/install-extensions.ts new file mode 100644 index 00000000000..b178f505952 --- /dev/null +++ b/firebase-vscode/src/test/utils/install-extensions.ts @@ -0,0 +1,25 @@ +import { execSync } from "child_process"; + +async function installExtensions() { + // List of extensions to install + const extensions: string[] = ["graphql.vscode-graphql-syntax"]; + + // Install each extension + extensions.forEach((extension) => { + try { + console.log(`Installing ${extension}...`); + execSync(`code --install-extension ${extension}`, { + stdio: "inherit", + }); + console.log(`${extension} installed successfully.`); + } catch (error) { + console.error(`Failed to install ${extension}:`, error); + process.exit(1); + } + }); +} + +// Ensure this script runs as part of the WebDriverIO setup +export default async function setupVscodeEnv() { + await installExtensions(); +} diff --git a/firebase-vscode/src/test/utils/logs.ts b/firebase-vscode/src/test/utils/logs.ts new file mode 100644 index 00000000000..bd362f395c9 --- /dev/null +++ b/firebase-vscode/src/test/utils/logs.ts @@ -0,0 +1,34 @@ +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) { + if (key in allLogs) { + pluginLogger[key as LogLevel] = function (...args: any[]) { + const logs = allLogs[key as LogLevel]; + + 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..a71a3bdb5e3 --- /dev/null +++ b/firebase-vscode/src/test/utils/mock.ts @@ -0,0 +1,50 @@ +import { Ref } from "../../utils/test_hooks"; +import { addTearDown } from "./test_hooks"; +import * as vscode from "vscode"; + +/** 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; +} + +export function createFakeContext(): vscode.ExtensionContext { + const context = createFake({ + subscriptions: [], + }); + + addTearDown(() => { + context.subscriptions.forEach((sub) => sub.dispose()); + }); + + return context; +} 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..f08ed5d4c61 --- /dev/null +++ b/firebase-vscode/src/test/utils/page_objects/commands.ts @@ -0,0 +1,64 @@ +import * as vscode from "vscode"; +import { EmulatorsStatus, RunningEmulatorInfo } from "../../../messaging/types"; +import { waitForTaskCompletion } from "../task"; +// import { browser } from "@wdio/globals"; +export class FirebaseCommands { + private async getEmulatorsStatus() { + return browser.executeWorkbench(async (vs: typeof vscode) => { + const emulators = await vs.commands.executeCommand( + "firebase.emulators.findRunning", + ); + return emulators as + | { status: EmulatorsStatus; infos?: RunningEmulatorInfo } + | undefined; + }); + } + + async waitForEmulators(): Promise { + return browser.waitUntil( + async () => { + const emulators = await this.getEmulatorsStatus(); + await browser.pause(1000); + console.log("Emulators status", emulators); + return emulators?.status === "running"; + }, + { timeout: 60000 }, + ); + } + + async waitForEmulatorsStopped(): Promise { + return browser.waitUntil( + async () => { + const emulators = await this.getEmulatorsStatus(); + await browser.pause(1000); + console.log("Emulators status", emulators); + return emulators?.status === "stopped"; + }, + { timeout: 10000 }, + ); + } + + async waitForUser(): Promise { + return browser.waitUntil(async () => { + return browser.executeWorkbench(async (vs: typeof vscode) => { + const isLoading = await vs.commands.executeCommand("fdc-graphql.user"); + console.log("User loading", isLoading); + return true; + }); + }); + } + + async setConfigToSkipFolderSelection(): Promise { + await browser.executeWorkbench(async (vs: typeof vscode) => { + // Retrieve the existing configuration for "firebase.dataConnect" + const configs = vs.workspace.getConfiguration("firebase.dataConnect"); + + // Update the configuration with new values + await configs.update( + "skipToAppFolderSelect", + true, + vs.ConfigurationTarget.Workspace, + ); + }); + } +} 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..b645b8a5cd7 --- /dev/null +++ b/firebase-vscode/src/test/utils/page_objects/editor.ts @@ -0,0 +1,161 @@ +import vscode from "vscode"; +import { Workbench } from "wdio-vscode-service"; + +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"); + } + + get runLocalButton() { + return this.editorView.elem.$('//a[contains(text(), "Run (local)")]'); + } + + async openFile(path: string) { + return browser.executeWorkbench(async (vs: typeof vscode, path) => { + const doc = await vs.workspace.openTextDocument(path); + return vs.window.showTextDocument(doc, 1, false); + }, path); + } + + async closeAllEditors() { + return browser.executeWorkbench(async (vs: typeof vscode) => { + await vs.commands.executeCommand("workbench.action.closeAllEditors"); + }); + } + + async closeCurrentEditor() { + return browser.executeWorkbench(async (vs: typeof vscode) => { + await vs.commands.executeCommand("workbench.action.closeActiveEditor"); + }); + } + + async getActiveEditor() { + return browser.executeWorkbench(async (vs: typeof vscode) => { + return vs.window.activeTextEditor; + }); + } + + async activeEditorContent() { + const editorContentElement = await browser.$(".view-lines"); + return editorContentElement.getText(); + } + + /** + * + * @param path The path of the file to diagnose. + * @returns An array of vscode.Diagnostic objects. + */ + async diagnoseFile(path: string): Promise { + const diagnostics = await browser.executeWorkbench( + async (vs: typeof vscode, queriesPath) => { + const uri = vs.Uri.file(queriesPath); + let diagnostics = vs.languages.getDiagnostics(uri); + + // Timeout if no diagnostics are found after 10 seconds. + let timeout = 0; + while (diagnostics.length === 0 && timeout < 10) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + diagnostics = vs.languages.getDiagnostics(uri); + timeout++; + } + + return diagnostics.map((diagnostic) => ({ + message: diagnostic.message, + range: diagnostic.range, + severity: diagnostic.severity, + source: diagnostic.source, + })); + }, + path, + ); + + return diagnostics; + } + + get addDataButton() { + return $('a[title="Generate a mutation to add data of this type"]'); + } + + get readDataButton() { + return $('a[title="Generate a query to read data of this type"]'); + } +} + +export class Notifications { + constructor(readonly workbench: Workbench) {} + + /** + * Wait for the extension recommendation pop-up and click the Install button + */ + async installRecommendedExtension({ + extensionId, + message, + }: { + extensionId: string; + message: string; + }): Promise { + let installed: vscode.Extension | undefined; + + console.log(`Installing extension ${extensionId}`); + let foundNotification: WebdriverIO.Element | undefined; + + while (!foundNotification) { + try { + let notifications = await browser.$$( + ".monaco-workbench .notification-list-item", + ); + + await browser.waitUntil( + async () => { + notifications = await browser.$$( + ".monaco-workbench .notification-list-item", + ); + + foundNotification = await notifications.find(async (notification) => + (await notification.getText()).includes(message), + ); + + return foundNotification; + }, + { + timeout: 10000, + timeoutMsg: "No notifications found", + }, + ); + } catch (e) { + return; + } + } + + // Locate and click the "Install" button in the notification + const installButton = await foundNotification.$("a.monaco-button=Install"); + await installButton.waitForClickable(); + await installButton.click(); + + console.log(`Installing extension ${extensionId}`); + + // Wait for the extension to be installed + while (!installed) { + installed = await browser.executeWorkbench( + async (vs: typeof vscode, extensionId) => { + return vs.extensions.getExtension(extensionId); + }, + extensionId, + ); + console.log(`Extension ${extensionId} not installed yet`); + await browser.pause(1000); + } + + if (installed) { + console.log(`Extension ${extensionId} installed`); + } + } +} 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..2f4abb9f140 --- /dev/null +++ b/firebase-vscode/src/test/utils/page_objects/execution.ts @@ -0,0 +1,133 @@ +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 browser.keys("F1"); + await this.workbench.executeCommand( + "data-connect-execution-configuration.focus", + ); + } + + async getVariables(): Promise { + return this.runInConfigurationContext(async (configs) => { + return configs.variablesTextarea.getValue(); + }); + } + + async setVariables(variables: string): Promise { + // TODO revert to the original value after test + + await this.runInConfigurationContext(async (configs) => { + await configs.variablesTextarea.setValue(variables); + }); + } + + async clickRerun(): Promise { + return this.runInConfigurationContext(async (configs) => { + const rerunButton = await configs.rerunButton; + await rerunButton.waitForClickable(); + await rerunButton.doubleClick(); // double click first transitions focus to window instead of notifs + }); + } + + 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"); + } + + get rerunButton() { + return this.variablesView.$("vscode-button"); + } +} + +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/notifications.ts b/firebase-vscode/src/test/utils/page_objects/notifications.ts new file mode 100644 index 00000000000..52bbc82cd16 --- /dev/null +++ b/firebase-vscode/src/test/utils/page_objects/notifications.ts @@ -0,0 +1,63 @@ +import { Workbench, Notification } from "wdio-vscode-service"; + +export class Notifications { + constructor(readonly workbench: Workbench) {} + + async getExportNotification() { + const notifications = await this.workbench.getNotifications(); + return notifications.find(async (n) => { + const message = await n.getMessage(); + return message.includes("Emulator Data exported to"); + }); + } + + async getStartEmulatorNotification() { + const notifications = await this.workbench.getNotifications(); + return notifications.find(async (notif) => { + return ( + (await notif.getMessage()) === + "Automatically starting emulator... Please retry `Run local` execution after it's started." + ); + }); + } + + // Edit Variables Notification + async getEditVariablesNotification() { + await browser.pause(250); + const notifications = await this.workbench.getNotifications(); + return notifications.find(async (n) => { + const message = await n.getMessage(); + return message.includes("Missing required variables"); + }); + } + + async editVariablesFromNotification(notification: Notification) { + // takeAction doesn't work in wdio vscode + const editButton = await notification.elem.$( + ".monaco-button=Edit variables", + ); + if (editButton) { + await editButton.click(); + } + } + + async getGeminiInstallNotification() { + const notifications = await this.workbench.getNotifications(); + return notifications.find(async (n) => { + const message = await n.getMessage(); + return message.includes( + "The Firebase Assistant requires the Gemini Code Assist extension", + ); + }); + } + + async clickYesFromGeminiInstallNotification(notification: Notification) { + // takeAction doesn't work in wdio vscode + const yesButton = await notification.elem.$( + ".monaco-button=Yes", + ); + if (yesButton) { + await yesButton.click(); + } + } +} 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..6cc482be786 --- /dev/null +++ b/firebase-vscode/src/test/utils/page_objects/sidebar.ts @@ -0,0 +1,204 @@ +import vscode from "vscode"; + +import { Workbench } from "wdio-vscode-service"; +import { findWebviewWithTitle, runInFrame } from "../webviews"; +import { TEXT } from "../../../../webviews/globals/ux-text"; + +export class FirebaseSidebar { + constructor(readonly workbench: Workbench) {} + + async openExtensionSidebar() { + const sidebar = await $(`a[aria-label="Firebase Data Connect"]`); + await sidebar.waitForDisplayed(); + await sidebar.click(); + await this.refresh(); + + // single retry to work around Syntax Highlighter download + try { + (await browser.$(".monaco-workbench .part.sidebar")).waitForExist({ + timeout: 2000, + }); + } catch (e) { + await this.open(); + } + + await browser.pause(1000); // visual loading delay + } + + async waitForSidebar() { + const sidebar = await browser.$$(".monaco-workbench .part.sidebar"); + } + + async refresh() { + await browser.executeWorkbench((vs: typeof vscode) => { + return vs.commands.executeCommand("firebase.refresh"); + }); + } + async open() { + await browser.executeWorkbench((vs: typeof vscode) => { + return vs.commands.executeCommand("fdc_sidebar.focus"); + }); + } + + get hostBtn() { + return $("vscode-button=Host your Web App"); + } + + /** + * 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() { + try { + await this.runInStudioContext(async (studio) => { + await studio.startEmulatorsBtn.waitForDisplayed(); + await studio.startEmulatorsBtn.click(); + }); + } catch (e) { + console.error("Error starting emulators", e); + await this.startEmulators(); + } + } + + async currentEmulators() { + return this.runInStudioContext(async (studio) => { + const items = await studio.emulatorsList; + const texts = items.map((item) => item.getText()); + return texts; + }); + } + + async clearEmulatorData() { + return this.runInStudioContext(async (studio) => { + const btn = await studio.clearEmulatorDataBtn; + return btn.click(); + }); + } + + async exportEmulatorData() { + return this.runInStudioContext(async (studio) => { + const btn = await studio.exportEmulatorDataBtn; + return btn.click(); + }); + } + + async startDeploy() { + return this.runInStudioContext(async (studio) => { + await studio.fdcDeployElement.waitForDisplayed(); + await studio.fdcDeployElement.click(); + }); + } + + /** Runs the callback in the context of the Firebase view, within the sidebar */ + async runInStudioContext( + cb: (firebase: StudioView) => Promise, + ): Promise { + const [a, b] = await findWebviewWithTitle("Studio"); + return runInFrame(a, () => + runInFrame(b, () => cb(new StudioView(this.workbench))), + ); + } +} + +export class StudioView { + constructor(readonly workbench: Workbench) {} + + get userIconElement() { + return $(".codicon-account"); + } + + get signInWithGoogleLink() { + return $(`vscode-link=${TEXT.GOOGLE_SIGN_IN}`); + } + + get initFirebaseBtn() { + return $("vscode-button=Run firebase init"); + } + + get startEmulatorsBtn() { + return $("vscode-button=Start emulators"); + } + + get clearEmulatorDataBtn() { + return $("vscode-button=Clear Data Connect data"); + } + + get exportEmulatorDataBtn() { + return $("vscode-button=Export emulator data"); + } + + get addSdkToAppBtn() { + return $("vscode-button=Add SDK to app"); + } + + get emulatorsList() { + return $("ul[class^='list-']").$$(`li span`); + } + + get fdcDeployElement() { + return $(`vscode-button=${TEXT.DEPLOY_FDC_ENABLED}`); + } + + get geminiButton() { + return $("vscode-button=Build your schema and queries with AI"); + } +} + +export class SchemaExplorerView { + constructor(readonly workbench: Workbench) {} + + get schemaExplorerView() { + return $('div[aria-label="Schema explorer"] .monaco-list-rows'); + } + + async focusFdcExplorer() { + await browser.executeWorkbench((vs: typeof vscode) => { + return vs.commands.executeCommand( + "firebase.dataConnect.explorerView.focus", + ); + }); + } + + async waitForData() { + await this.schemaExplorerView.waitForDisplayed(); + } + + async getQueries() { + const explorerView = await this.schemaExplorerView; + const query = await explorerView.$( + `div.monaco-list-row[aria-label*="query"]`, + ); + + // Select all the queries + await query.waitForDisplayed(); + await query.click(); + + const queries = explorerView.$$(`div.monaco-list-row[aria-level="2"]`); + await browser.pause(500); + + // Close the query list + await query.click(); + + return queries; + } + + async getMutations() { + const explorerView = await this.schemaExplorerView; + const mutation = await explorerView.$( + `div.monaco-list-row[aria-label*="mutation"]`, + ); + + // Select all the queries + await mutation.waitForDisplayed(); + await mutation.click(); + const mutations = explorerView.$$(`div.monaco-list-row[aria-level="2"]`); + await browser.pause(500); + + // Close the mutation list + await mutation.click(); + + return mutations; + } +} 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/page_objects/terminal.ts b/firebase-vscode/src/test/utils/page_objects/terminal.ts new file mode 100644 index 00000000000..df627e0a87f --- /dev/null +++ b/firebase-vscode/src/test/utils/page_objects/terminal.ts @@ -0,0 +1,16 @@ +import { Workbench } from "wdio-vscode-service"; + +export class TerminalView { + constructor(readonly workbench: Workbench) {} + + private readonly bottomBar = this.workbench.getBottomBar(); + async getTerminalText() { + const tv = await this.bottomBar.openTerminalView(); + /** + * SEE: https://github.com/webdriverio-community/wdio-vscode-service/blob/e4ef4d5a1da194e9a6195fad881733c3aa6720d8/src/pageobjects/workbench/Workbench.ts#L183 + * The code recognizes our webview, and chooses to send `F1` as an input instead of as a keystroke. + */ + await browser.keys("F1"); + return await tv.getText(); + } +} diff --git a/firebase-vscode/src/test/utils/projects.ts b/firebase-vscode/src/test/utils/projects.ts new file mode 100644 index 00000000000..61504175897 --- /dev/null +++ b/firebase-vscode/src/test/utils/projects.ts @@ -0,0 +1,46 @@ +import * as path from "path"; +import * as vscode from "vscode"; + +export const schemaPath = path.resolve( + process.cwd(), + "src/test/test_projects/fishfood/dataconnect/schema/schema.gql", +); + + +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", +); + +export const queryWithFragmentPath = path.resolve( + process.cwd(), + "src/test/test_projects/fishfood/dataconnect/connectors/a/queryWithFragment.gql", +); + +export const firebaseRcPath = path.resolve( + process.cwd(), + "src/test/test_projects/empty/.firebaserc", +); + +export const firebaseLogsPath = path.resolve( + process.cwd(), + "src/test/test_projects/empty/firebase-debug.log", +); + +export async function mockProject(project: string): Promise { + return browser.executeWorkbench( + async (vs: typeof vscode, project: string) => { + const promise = vs.commands.executeCommand( + "fdc-graphql.mock.project", + project, + ); + return promise; + }, + project, + ); +} 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/task.ts b/firebase-vscode/src/test/utils/task.ts new file mode 100644 index 00000000000..7da974c90b6 --- /dev/null +++ b/firebase-vscode/src/test/utils/task.ts @@ -0,0 +1,60 @@ +import * as vscode from "vscode"; + +export async function waitForTaskCompletion( + taskName: string, +): Promise { + return browser.executeWorkbench>( + (vs: typeof vscode, taskName: string) => { + return new Promise((resolve, reject) => { + let taskCompleted = false; + + const startListener = vs.tasks.onDidStartTask((e) => { + if (e.execution.task.name === taskName) { + console.log(`Task "${taskName}" started.`); + } + }); + + const endListener = vs.tasks.onDidEndTask((e) => { + if (e.execution.task.name === taskName) { + console.log(`Task "${taskName}" completed.`); + taskCompleted = true; + resolve(taskCompleted); // Resolve the promise when the task finishes + startListener.dispose(); // Clean up the listeners + endListener.dispose(); + } + }); + + setTimeout(() => { + reject(new Error(`Task "${taskName}" did not complete in time.`)); + startListener.dispose(); // Clean up in case of timeout + endListener.dispose(); + }, 60000); // Set a timeout (e.g., 60 seconds) to prevent hanging + }); + }, + taskName, + ); +} + +export async function waitForTaskStart(taskName: string): Promise { + return browser.executeWorkbench>( + (vs: typeof vscode, taskName: string) => { + return new Promise((resolve, reject) => { + let taskStarted = false; + + const startListener = vs.tasks.onDidStartTask((e) => { + if (e.execution.task.name === taskName) { + console.log(`Task "${taskName}" started.`); + taskStarted = true; + resolve(taskStarted); + } + }); + + setTimeout(() => { + reject(new Error(`Task "${taskName}" did not start in time.`)); + startListener.dispose(); + }, 60000); + }); + }, + taskName, + ); +} 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..f1f5b358c8b --- /dev/null +++ b/firebase-vscode/src/test/utils/test_hooks.ts @@ -0,0 +1,100 @@ +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 addSetup(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: () => 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]; + // 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; + + setup(async function () { + await runGuarded(testSetups); + }); + + teardown(async function () { + await runGuarded(testTearDowns.reverse()); + tearDowns = previousTearDowns; + }); + + test(description, async function () { + await cb(); + }); +} + +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 as Error; + } + } + + if (firstError) { + throw firstError; + } +} diff --git a/firebase-vscode/src/test/utils/user.ts b/firebase-vscode/src/test/utils/user.ts new file mode 100644 index 00000000000..d383731da6c --- /dev/null +++ b/firebase-vscode/src/test/utils/user.ts @@ -0,0 +1,12 @@ +import { User } from "../../types/auth"; +import * as vscode from "vscode"; + +export async function mockUser(user: User | undefined): Promise { + return browser.executeWorkbench( + async (vs: typeof vscode, user: User) => { + const promise = vs.commands.executeCommand("fdc-graphql.mock.user", user); + return promise; + }, + user, + ); +} 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..2af35cb3612 --- /dev/null +++ b/firebase-vscode/src/test/webpack.test.js @@ -0,0 +1,59 @@ +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", + parallelism: 20, + 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..4d2a6da47cb --- /dev/null +++ b/firebase-vscode/src/utils/env.ts @@ -0,0 +1,2 @@ +// Set by the `package.json` file +export const isTest = !!process.env.TEST; diff --git a/firebase-vscode/src/utils/find_comments.ts b/firebase-vscode/src/utils/find_comments.ts new file mode 100644 index 00000000000..69094745560 --- /dev/null +++ b/firebase-vscode/src/utils/find_comments.ts @@ -0,0 +1,37 @@ +interface Comment { + text: string; + startLine: number; + endLine: number; + endIndex: number; +} + +export function findCommentsBlocks(text: string): Comment[] { + const lineEnds: number[] = []; + let searchIndex: number = -1; + while ((searchIndex = text.indexOf('\n', searchIndex + 1)) !== -1) { + lineEnds.push(searchIndex); + } + lineEnds.push(text.length); + const comments: Comment[] = []; + for (let i = 0; i < lineEnds.length; i++) { + const lineStart = i === 0 ? 0 : lineEnds[i - 1] + 1; + const lineText = text.substring(lineStart, lineEnds[i]).trim(); + if (lineText.startsWith('#')) { + comments.push({ startLine: i, endLine: i, text: lineText.substring(1).trim(), endIndex: lineEnds[i] }); + } + } + const commentBlocks: Comment[] = []; + for (let i = 0; i < comments.length; i++) { + const current = comments[i]; + if (i === 0 || current.startLine > comments[i - 1].endLine + 1) { + commentBlocks.push({ ...current }); + } else { + // Continuation of the previous block + const lastBlock = commentBlocks[commentBlocks.length - 1]; + lastBlock.endLine = current.endLine; + lastBlock.endIndex = current.endIndex; + lastBlock.text += '\n' + current.text; + } + } + return commentBlocks; +} 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/port_utils.ts b/firebase-vscode/src/utils/port_utils.ts new file mode 100644 index 00000000000..8fcf91e002d --- /dev/null +++ b/firebase-vscode/src/utils/port_utils.ts @@ -0,0 +1,39 @@ +import * as net from "net"; + +export async function findOpenPort(startPort: number): Promise { + return new Promise((resolve, reject) => { + let server: net.Server | null = null; + + server = net.createServer(); + server.on("error", (err: any) => { + if (err.code === "EADDRINUSE") { + // Port is in use, try the next one + if (server) { + server.close(() => + findOpenPort(startPort + 1) + .then(resolve) + .catch(reject), + ); + } else { + reject(new Error("Server is null while handling EADDRINUSE")); + } + } else { + reject(err); + } + }); + + server.listen(startPort, "127.0.0.1", () => { + const address = server?.address(); + if (address && typeof address === "object" && "port" in address) { + const port = address.port; + if (server) { + server.close(() => resolve(port)); + } else { + reject(new Error("Server is null after successful listen")); + } + } else { + reject(new Error("Invalid address returned from server")); + } + }); + }); +} 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..216a2d085c4 --- /dev/null +++ b/firebase-vscode/src/utils/settings.ts @@ -0,0 +1,72 @@ +import { ConfigurationTarget, workspace } from "vscode"; +import { DATA_CONNECT_EVENT_NAME, AnalyticsLogger } from "../analytics"; + +export interface Settings { + readonly firebasePath: string; + readonly firebaseBinaryKind: string; + readonly npmPath: string; + readonly useFrameworks: boolean; + readonly shouldShowIdxMetricNotice: boolean; + readonly importPath?: string; + readonly exportPath: string; + readonly exportOnExit: boolean; + readonly debug: boolean; + readonly extraEnv: Record; +} + +// TODO: Temporary fallback for bashing, this should probably point to the global firebase binary on the system +const DEFAULT_FIREBASE_BINARY = "npx -y firebase-tools@latest"; + +export function getSettings(): Settings { + const config = workspace.getConfiguration("firebase"); + const firebasePath = + config.get("firebasePath") || DEFAULT_FIREBASE_BINARY; + + let firebaseBinaryKind = "unknown"; // Used for analytics. + if (firebasePath === DEFAULT_FIREBASE_BINARY) { + firebaseBinaryKind = "npx"; + } else if (firebasePath.endsWith("/.local/bin/firebase")) { + // https://firebase.tools/dataconnect defaults to $HOME/.local/bin + firebaseBinaryKind = "firepit-local"; + } else if (firebasePath.endsWith("/local/bin/firebase")) { + // https://firebase.tools/ defaults to /usr/local/bin + firebaseBinaryKind = "firepit-global"; + } + + const extraEnv = config.get>("extraEnv", {}) + process.env = { ...process.env, ...extraEnv }; + + return { + firebasePath, + firebaseBinaryKind, + npmPath: config.get("npmPath", "npm"), + useFrameworks: config.get("hosting.useFrameworks", false), + shouldShowIdxMetricNotice: config.get( + "idx.viewMetricNotice", + true, + ), + importPath: config.get("emulators.importPath"), + exportPath: config.get("emulators.exportPath", "./exportedData"), + exportOnExit: config.get("emulators.exportOnExit", false), + debug: config.get("debug", false), + extraEnv, + }; +} + +export function updateIdxSetting(shouldShow: boolean) { + const config = workspace.getConfiguration("firebase"); + config.update("idx.viewMetricNotice", shouldShow, ConfigurationTarget.Global); +} + +// Persist env var as path setting when path setting doesn't exist +export function setupFirebasePath(analyticsLogger: AnalyticsLogger) { + const config = workspace.getConfiguration("firebase"); + if (process.env.FIREBASE_BINARY && !config.get("firebasePath")) { + config.update( + "firebasePath", + process.env.FIREBASE_BINARY, + ConfigurationTarget.Global, + ); + } + analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.SETUP_FIREBASE_BINARY); +} diff --git a/firebase-vscode/src/utils/signal.ts b/firebase-vscode/src/utils/signal.ts new file mode 100644 index 00000000000..a7b14b773c8 --- /dev/null +++ b/firebase-vscode/src/utils/signal.ts @@ -0,0 +1,46 @@ +import { ReadonlySignal, Signal } from "@preact/signals-react"; + +/** Waits for a signal value to not be undefined */ +export async function firstWhereDefined( + signal: Signal | ReadonlySignal, +): 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 | ReadonlySignal, + 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..26373bcb43d --- /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..df1a621793e --- /dev/null +++ b/firebase-vscode/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "strict": true /* enable all strict type-checking options */, + "allowJs": true, + "checkJs": false, + "sourceMap": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "jsx": "react", + "outDir": "dist", + "module": "ES2022", + "target": "ES2017", + "lib": ["ES2020", "DOM"], + "moduleResolution": "node", + "rootDirs": ["src", "../src", "common"], + "types": [ + "node", + "@wdio/mocha-framework", + "@wdio/globals/types", + "@types/mocha" + ] + }, + "ts-node": { + "esm": true + }, + "include": ["src/**/*", "common/**/*", "../src/types/**/*"] +} diff --git a/firebase-vscode/webpack.common.js b/firebase-vscode/webpack.common.js new file mode 100644 index 00000000000..2d975169f78 --- /dev/null +++ b/firebase-vscode/webpack.common.js @@ -0,0 +1,322 @@ +//@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", ".json"], // needed to handle a node_module dependency emojilib, which requires json without ext. + alias: { + // "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"), + }, + 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, + /** @param match {any} */ + 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, + /** @param match {any} */ + 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, + /** @param match {any} */ + 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: "../prompts", + to: "./prompts", + }, + { + from: "../templates", + to: "./templates", + }, + { + from: "../schema", + to: "./schema", + }, + // TODO(hlshen): Sanity check if these should be fixed or removed. AFIACT, they exist for functions and hosting deploys, which are not relevant anymore. + // 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 + }, +}; + +/** @param entryName {any} */ +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 { + /** @param compiler {any} */ + 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..2fbb684504a --- /dev/null +++ b/firebase-vscode/webpack.prod.js @@ -0,0 +1,22 @@ +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/, + }, + parallel: 2, + }), + "...", + ], + }, + }), +); diff --git a/firebase-vscode/webviews/SidebarApp.tsx b/firebase-vscode/webviews/SidebarApp.tsx new file mode 100644 index 00000000000..318a34f7f95 --- /dev/null +++ b/firebase-vscode/webviews/SidebarApp.tsx @@ -0,0 +1,289 @@ +import React, { useEffect } from "react"; +import { Spacer } from "./components/ui/Spacer"; +import { broker, brokerSignal, useBroker } from "./globals/html-broker"; +import { AccountSection } from "./components/AccountSection"; +import { ProjectSection } from "./components/ProjectSection"; +import { + VSCodeButton, + VSCodeDropdown, + VSCodeOption, + VSCodeProgressRing, +} from "@vscode/webview-ui-toolkit/react"; +import { Body, Label } from "./components/ui/Text"; +import { PanelSection } from "./components/ui/PanelSection"; +import { EmulatorPanel as Emulators } from "./components/EmulatorPanel"; +import { App } from "./globals/app"; +import { signal, useComputed } from "@preact/signals-react"; +import { Icon } from "./components/ui/Icon"; +import { ExternalLink } from "./components/ui/ExternalLink"; + +const user = brokerSignal("notifyUserChanged", { + initialRequest: "getInitialData", +}); +const isLoadingUser = brokerSignal("notifyIsLoadingUser"); +const project = brokerSignal("notifyProjectChanged"); +const env = brokerSignal("notifyEnv"); +const configs = brokerSignal("notifyFirebaseConfig", { + initialRequest: "getInitialData", +}); +const hasFdcConfigs = brokerSignal("notifyHasFdcConfigs", { + initialRequest: "getInitialHasFdcConfigs", +}); +const emulatorsRunningInfo = brokerSignal("notifyEmulatorStateChanged", { + initialRequest: "getEmulatorInfos", +}); +const docsLink = brokerSignal("notifyDocksLink", { + initialRequest: "getDocsLink", +}); + +const showResetPanel = brokerSignal("notifyEmulatorsHanging"); + +function Welcome() { + const configLabel = useComputed(() => { + return !hasFdcConfigs.value ? "dataconnect.yaml" : "firebase.json"; + }); + + return ( + <> + + No {configLabel.value} detected in this project + + + { + broker.send("runFirebaseInit"); + }} + > + Run firebase init + + + ); +} + +function EmulatorsPanel() { + if (emulatorsRunningInfo.value?.status === "starting") { + const runningPanel = ( + <> + + + + ); + + if (showResetPanel.value) { + return ( + <> + + + + + { + broker.send("getEmulatorInfos"); + showResetPanel.value = false; + }} + > + Refresh Emulator View + + + ); + } + return runningPanel; + } + + return emulatorsRunningInfo.value?.infos && + emulatorsRunningInfo.value?.status === "running" ? ( + + ) : ( + <> + broker.send("runStartEmulators")} + > + Start emulators + + + + + + ); +} + +const deployMenu = signal(false); + +function DataConnect() { + return ( + <> + {docsLink.value && ( + <> + + }> + View reference docs + + + + + )} + + broker.send("fdc.configure-sdk")} + appearance="secondary" + > + Add SDK to app + + + + + + broker.send("fdc.deploy-all")}> + Deploy to production + + + + + + + { + broker.send("firebase.activate.gemini"); + }} + > + Build your schema and queries with AI + + + + + + ); +} +function Content() { + useEffect(() => { + broker.send("getDocsLink"); + }, []); + + return ( + <> + + + + + + + + ); +} + +function ConfigPicker() { + const configs = useBroker("notifyFirebaseConfigListChanged", { + initialRequest: "getInitialFirebaseConfigList", + }); + + if (!configs || configs.values.length < 2) { + // Only show the picker when there are multiple configs + return <>; + } + + return ( + <> + + + + + broker.send("selectFirebaseConfig", (e.target as any).value) + } + > + {configs.values.map((uri) => ( + {uri} + ))} + + + ); +} + +export function SidebarApp() { + const isInitialized = useComputed(() => { + return !!configs.value?.firebaseJson?.value && hasFdcConfigs.value; + }); + + if (isLoadingUser.value) { + return Loading...; + } + + return ( + + + + {user.value?.user && project.value && ( + + )} + + + + {isInitialized.value ? ( + + ) : ( + + + + )} + + ); +} diff --git a/firebase-vscode/webviews/components/AccountSection.scss b/firebase-vscode/webviews/components/AccountSection.scss new file mode 100644 index 00000000000..35361c1b0e5 --- /dev/null +++ b/firebase-vscode/webviews/components/AccountSection.scss @@ -0,0 +1,19 @@ +.account-row { + display: flex; + justify-content: space-between; + 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..99e79318866 --- /dev/null +++ b/firebase-vscode/webviews/components/AccountSection.tsx @@ -0,0 +1,124 @@ +import { + VSCodeLink, + 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, + isMonospace, + isLoadingUser, +}: { + user: UserWithType | ServiceAccountUser | null; + isMonospace: boolean; + isLoadingUser: boolean; +}) { + const [userDropdownVisible, toggleUserDropdown] = useState(false); + + // Default: initial users check hasn't completed + let currentUserElement: ReactElement | string = (<>{TEXT.LOGIN_IN_PROGRESS}); + if (!isLoadingUser) { + if (!user) { + // Users loaded but no user was found + if (isMonospace) { + // Monospace: this is an error, should have found a workspace + // service account + currentUserElement = TEXT.MONOSPACE_LOGIN_FAIL; + } else { + // VS Code: prompt user to log in with Google account + currentUserElement = ( + broker.send("addUser")}> + {TEXT.GOOGLE_SIGN_IN} + + ); + } + } else if (user) { + // Users loaded, at least one user was found + if (user.type === "service_account") { + if (isMonospace) { + currentUserElement = TEXT.MONOSPACE_LOGGED_IN; + } else { + currentUserElement = TEXT.VSCE_SERVICE_ACCOUNT_LOGGED_IN; + } + } else { + currentUserElement = user.email; + } + } + } + + let userBoxElement = ( + + ); + if (user?.type === "service_account" && isMonospace) { + userBoxElement = ( + + ); + } + return ( +
+ {userBoxElement} + { + // Logout menu. Can't logout in monospace + + user && !isMonospace && ( + <> + toggleUserDropdown(!userDropdownVisible)} + /> + {userDropdownVisible ? ( + toggleUserDropdown(false)} + /> + ) : null} + + ) + } +
+ ); +} + +function LogoutMenu({ + user, + onClose, +}: { + user: UserWithType | ServiceAccountUser; + onClose: Function; +}) { + return ( + <> + + <> + { + broker.send("logout", { email: user.email }); + onClose(); + }} + > + Sign Out {user.email} + + + + + ); +} diff --git a/firebase-vscode/webviews/components/EmulatorPanel.scss b/firebase-vscode/webviews/components/EmulatorPanel.scss new file mode 100644 index 00000000000..be56cdcef9a --- /dev/null +++ b/firebase-vscode/webviews/components/EmulatorPanel.scss @@ -0,0 +1,19 @@ +@import "../globals/index.scss"; + +.list { + list-style: none; +} + +.fullWidth { + width: 100%; +} + +.list-item { + align-items: center; + display: flex; + gap: 4px; +} + +.running-indicator { + color: var(--vscode-testing-runAction, green); +} diff --git a/firebase-vscode/webviews/components/EmulatorPanel.tsx b/firebase-vscode/webviews/components/EmulatorPanel.tsx new file mode 100644 index 00000000000..45971d1c5bf --- /dev/null +++ b/firebase-vscode/webviews/components/EmulatorPanel.tsx @@ -0,0 +1,89 @@ +import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"; +import React from "react"; +import { Spacer } from "./ui/Spacer"; +import type { EmulatorInfo } from "../../../src/emulator/types"; +import { RunningEmulatorInfo } from "../messaging/types"; +import { Body, Label } from "./ui/Text"; +import { broker } from "../globals/html-broker"; +import styles from "./EmulatorPanel.scss"; +import { ExternalLink } from "./ui/ExternalLink"; +import { Icon } from "./ui/Icon"; + +/** + * Emulator info display component for the VSCode extension. + */ +export function EmulatorPanel({ + emulatorInfo, +}: { + emulatorInfo: RunningEmulatorInfo; +}) { + return ( + <> + + + + + {!!emulatorInfo.uiUrl && ( + <> + + + View them in the Emulator Suite UI + + + )} + + + + + } + > + View emulator docs + + + + ); +} + +// Make it pretty for the screen. Filter out the logging emulator since it's +// an implementation detail. +function FormatEmulatorRunningInfo({ infos }: { infos: EmulatorInfo[] }) { + return ( +
    + {infos + .filter((info) => info.name !== "logging") + .map((info, index) => ( +
  • + + + {info.name} + + +
  • + ))} +
+ ); +} + +function RunningEmulatorControlButtons({ infos }: { infos: EmulatorInfo[] }) { + return ( + <> + broker.send("fdc.clear-emulator-data")} + appearance="secondary" + > + Clear Data Connect data + + + broker.send("runEmulatorsExport")} + appearance="secondary" + > + Export emulator data + + + ); +} diff --git a/firebase-vscode/webviews/components/ProjectSection.tsx b/firebase-vscode/webviews/components/ProjectSection.tsx new file mode 100644 index 00000000000..a8bc71c09c8 --- /dev/null +++ b/firebase-vscode/webviews/components/ProjectSection.tsx @@ -0,0 +1,93 @@ +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 (!userEmail || (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: !process.env.VSCODE_TEST_MODE, + 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} + + {TEXT.CONSOLE_LINK_DESCRIPTION} + + + ); +} diff --git a/firebase-vscode/webviews/components/ui/ButtonGroup.scss b/firebase-vscode/webviews/components/ui/ButtonGroup.scss new file mode 100644 index 00000000000..52c48e45ebe --- /dev/null +++ b/firebase-vscode/webviews/components/ui/ButtonGroup.scss @@ -0,0 +1,11 @@ +.button-group { + vscode-button + vscode-button { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + vscode-button:has(+ vscode-button) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } +} diff --git a/firebase-vscode/webviews/components/ui/ButtonGroup.tsx b/firebase-vscode/webviews/components/ui/ButtonGroup.tsx new file mode 100644 index 00000000000..a590e70b49c --- /dev/null +++ b/firebase-vscode/webviews/components/ui/ButtonGroup.tsx @@ -0,0 +1,6 @@ +import React, { ReactNode } from "react"; +import styles from "./ButtonGroup.scss"; + +export function ButtonGroup({ children }: { children: ReactNode }) { + return
{children}
; +} diff --git a/firebase-vscode/webviews/components/ui/ExternalLink.scss b/firebase-vscode/webviews/components/ui/ExternalLink.scss new file mode 100644 index 00000000000..b6da07cab15 --- /dev/null +++ b/firebase-vscode/webviews/components/ui/ExternalLink.scss @@ -0,0 +1,10 @@ +.link-content { + align-items: center; + display: inline-flex; + gap: 4px; +} + +.link:hover .link-text, +.link:focus .link-text { + text-decoration: underline; +} diff --git a/firebase-vscode/webviews/components/ui/ExternalLink.tsx b/firebase-vscode/webviews/components/ui/ExternalLink.tsx new file mode 100644 index 00000000000..f4e089fdbe4 --- /dev/null +++ b/firebase-vscode/webviews/components/ui/ExternalLink.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"; +import styles from "./ExternalLink.scss"; + +export function ExternalLink({ + href, + children, + prefix, +}: { + href: string; + children: string; + prefix?: JSX.Element; +}) { + return ( + + + {prefix} + {children} + + + ); +} diff --git a/firebase-vscode/webviews/components/ui/Icon.scss b/firebase-vscode/webviews/components/ui/Icon.scss new file mode 100644 index 00000000000..7abdec349c4 --- /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: inherit; + 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..526bc8c9c77 --- /dev/null +++ b/firebase-vscode/webviews/components/ui/PanelSection.scss @@ -0,0 +1,25 @@ +.panel { + display: flex; + flex-direction: column; +} + +.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..4442cf2435b --- /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..c00d9d11298 --- /dev/null +++ b/firebase-vscode/webviews/data-connect/DataConnectExecutionResultsApp.tsx @@ -0,0 +1,154 @@ +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 = ( + <> + + + + ); + } + } 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({ errors }: { errors: readonly GraphQLError[] }) { + let pathDisplay: JSX.Element | undefined; + // update path + const errorsWithPathDisplay = errors.map((error) => { + if (error.path) { + // Renders the path as a series of kbd elements separated by commas + return { + ...error, + pathDisplay: ( + <> + {error.path?.map((path, index) => { + const item = {path}; + + return index === 0 ? item : <>, {item}; + })}{" "} + + ), + }; + } + return error; + }); + + return ( + <> + {errorsWithPathDisplay.map((error, index) => { + 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..c2f6257282e --- /dev/null +++ b/firebase-vscode/webviews/data-connect/data-connect-execution-configuration.entry.scss @@ -0,0 +1,69 @@ +@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: flex; + align-items: stretch; + flex-direction: column; + justify-content: stretch; +} + +.variableInput { + flex: 1; +} + +.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; +} + +vscode-button { + flex:0; + justify-self: flex-end; +} \ No newline at end of file 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..c991a703f6f --- /dev/null +++ b/firebase-vscode/webviews/data-connect/data-connect-execution-configuration.entry.tsx @@ -0,0 +1,151 @@ +import React, { useEffect, useRef, useState } from "react"; +import { createRoot } from "react-dom/client"; +import style from "./data-connect-execution-configuration.entry.scss"; +import { + VSCodeButton, + VSCodeDropdown, + VSCodeOption, + VSCodePanels, + VSCodePanelTab, + VSCodePanelView, + VSCodeTextArea, +} from "@vscode/webview-ui-toolkit/react"; +import { broker, useBroker } 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) { + setText(e.target.value); + broker.send("definedDataConnectArgs", e.target.value); + } + + const lastOperation = useBroker("notifyLastOperation"); + const textareaRef = useRef(null); + const [textareaVariables, setText] = useState("{}"); + + + const updateText = broker.on("notifyDataConnectArgs" , (newArgs: string) => { + setText(newArgs); + if (textareaRef.current) { + textareaRef.current.focus(); + textareaRef.current.setSelectionRange(0, 1); + } + }) + + const sendRerun = () => { + broker.send("rerunExecution"); + }; + + // 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 + + + + {lastOperation && ( + + Rerun last execution: {lastOperation} + + )} + + + + + + ); +} + +function AuthUserMockForm() { + const [selectedKind, setSelectedMockKind] = useState( + UserMockKind.ADMIN, + ); + const [claims, setClaims] = useState( + `{\n "email_verified": true,\n "sub": "exampleUserId"\n}`, + ); + + useEffect(() => { + broker.send( + "notifyAuthUserMockChange", + selectedKind === UserMockKind.AUTHENTICATED + ? { + kind: selectedKind, + claims: claims, + } + : { + kind: selectedKind, + }, + ); + }, [selectedKind, claims]); + + let expandedForm: JSX.Element | undefined; + if (selectedKind === UserMockKind.AUTHENTICATED) { + expandedForm = ( + <> + + Claim and values + setClaims((event.target as any).value)} + /> + + ); + } + + return ( + <> + Run as + setSelectedMockKind((event.target as any).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..664160f52d3 --- /dev/null +++ b/firebase-vscode/webviews/data-connect/data-connect.entry.tsx @@ -0,0 +1,85 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import { + VSCodeButton, + VSCodeProgressRing, +} from "@vscode/webview-ui-toolkit/react"; +import { Spacer } from "../components/ui/Spacer"; +import styles from "../globals/index.scss"; +import { broker, useBroker } from "../globals/html-broker"; +import { PanelSection } from "../components/ui/PanelSection"; +import { EmulatorPanel } from "../components/EmulatorPanel"; + +const root = createRoot(document.getElementById("root")!); +root.render(); + +function DataConnect() { + const emulatorsRunningInfo = useBroker("notifyEmulatorStateChanged", { + initialRequest: "getEmulatorInfos", + }); + + const user = useBroker("notifyUserChanged", { + initialRequest: "getInitialData", + })?.user; + + if (emulatorsRunningInfo?.status === "starting") { + return ( + <> + + + + ); + } + + return ( +
    + + + {emulatorsRunningInfo?.status === "running" ? ( + <> + + + + ) : ( + broker.send("runStartEmulators")}> + Start emulators + + )} + +

    + Configure a generated SDK. +

    + See also:{" "} + + Working with generated SDKs + +

    + broker.send("fdc.configure-sdk")}> + Configure Generated SDK + + +
    + +

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

    + + broker.send("fdc.deploy-all")}> + Deploy + + + broker.send("fdc.deploy")} + > + Deploy Individual + +
    +
    + ); +} diff --git a/firebase-vscode/webviews/fdc_sidebar.entry.scss b/firebase-vscode/webviews/fdc_sidebar.entry.scss new file mode 100644 index 00000000000..462fa84fd36 --- /dev/null +++ b/firebase-vscode/webviews/fdc_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/fdc_sidebar.entry.tsx b/firebase-vscode/webviews/fdc_sidebar.entry.tsx new file mode 100644 index 00000000000..2e75e04b56e --- /dev/null +++ b/firebase-vscode/webviews/fdc_sidebar.entry.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import { SidebarApp } from "./SidebarApp"; +import { App } from "./globals/app"; +import style from "./fdc_sidebar.entry.scss"; + +// Prevent scss tree shaking +style; + +const root = createRoot(document.getElementById("root")!); +root.render( + + + , +); diff --git a/firebase-vscode/webviews/globals/app.tsx b/firebase-vscode/webviews/globals/app.tsx new file mode 100644 index 00000000000..299ce06c853 --- /dev/null +++ b/firebase-vscode/webviews/globals/app.tsx @@ -0,0 +1,11 @@ +import React, { ReactNode, StrictMode } from "react"; +import styles from "./index.scss"; + +/** 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/html-broker.ts b/firebase-vscode/webviews/globals/html-broker.ts new file mode 100644 index 00000000000..b24f1732919 --- /dev/null +++ b/firebase-vscode/webviews/globals/html-broker.ts @@ -0,0 +1,114 @@ +import { useEffect, useState } from "react"; +import { Broker, createBroker } from "../../common/messaging/broker"; +import { + ExtensionToWebviewParamsMap, + WebviewToExtensionParamsMap, +} from "../../common/messaging/protocol"; +import { webLogger } from "./web-logger"; +import { signal, Signal } from "@preact/signals-react"; + +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 function brokerSignal< + MessageT extends keyof ExtensionToWebviewParamsMap, +>( + message: Extract, + options?: { + initialRequest: keyof WebviewToExtensionParamsMap; + }, +): Signal { + const data = signal( + undefined, + ); + + broker.on(message, (value) => { + data.value = value; + }); + + if (options?.initialRequest) { + broker.send(options.initialRequest); + } + + return data; +} + +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..46d837ec582 --- /dev/null +++ b/firebase-vscode/webviews/globals/ux-text.ts @@ -0,0 +1,42 @@ +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", + + LOGIN_IN_PROGRESS: "Login in progress", + + 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..741d87640a8 --- /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: Partial = {}; + +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/tsconfig.json b/firebase-vscode/webviews/tsconfig.json new file mode 100644 index 00000000000..a9723730ca4 --- /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": true /* 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..9aa64561c77 --- /dev/null +++ b/npm-shrinkwrap.json @@ -0,0 +1,38043 @@ +{ + "name": "firebase-tools", + "version": "14.19.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "firebase-tools", + "version": "14.19.1", + "license": "MIT", + "dependencies": { + "@apphosting/build": "^0.1.6", + "@apphosting/common": "^0.0.8", + "@electric-sql/pglite": "^0.3.3", + "@electric-sql/pglite-tools": "^0.2.8", + "@google-cloud/cloud-sql-connector": "^1.3.3", + "@google-cloud/pubsub": "^4.5.0", + "@inquirer/prompts": "^7.4.0", + "@modelcontextprotocol/sdk": "^1.10.2", + "abort-controller": "^3.0.0", + "ajv": "^8.17.1", + "ajv-formats": "3.0.1", + "archiver": "^7.0.0", + "async-lock": "1.4.1", + "body-parser": "^1.19.0", + "chokidar": "^3.6.0", + "cjson": "^0.3.1", + "cli-table3": "0.6.5", + "colorette": "^2.0.19", + "commander": "^5.1.0", + "configstore": "^5.0.1", + "cors": "^2.8.5", + "cross-env": "^7.0.3", + "cross-spawn": "^7.0.5", + "csv-parse": "^5.0.4", + "deep-equal-in-any-order": "^2.0.6", + "exegesis": "^4.2.0", + "exegesis-express": "^4.0.0", + "express": "^4.16.4", + "filesize": "^6.1.0", + "form-data": "^4.0.1", + "fs-extra": "^10.1.0", + "fuzzy": "^0.1.3", + "gaxios": "^6.7.0", + "glob": "^10.4.1", + "google-auth-library": "^9.11.0", + "ignore": "^7.0.4", + "js-yaml": "^3.14.1", + "jsonwebtoken": "^9.0.0", + "leven": "^3.1.0", + "libsodium-wrappers": "^0.7.10", + "lodash": "^4.17.21", + "lsofi": "1.0.0", + "marked": "^13.0.2", + "marked-terminal": "^7.0.0", + "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", + "pg-gateway": "^0.3.0-beta.4", + "pglite-2": "npm:@electric-sql/pglite@0.2.17", + "portfinder": "^1.0.32", + "progress": "^2.0.3", + "proxy-agent": "^6.3.0", + "retry": "^0.13.1", + "semver": "^7.5.2", + "sql-formatter": "^15.3.0", + "stream-chain": "^2.2.4", + "stream-json": "^1.7.3", + "superstatic": "^9.2.0", + "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.5.10", + "yaml": "^2.4.1", + "zod": "^3.24.3", + "zod-to-json-schema": "^3.24.5" + }, + "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.4.2", + "@types/body-parser": "^1.17.0", + "@types/chai": "^4.3.0", + "@types/chai-as-promised": "^7.1.4", + "@types/cjson": "^0.5.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/lsofi": "1.0.2", + "@types/marked-terminal": "^6.1.1", + "@types/mocha": "^9.0.0", + "@types/mock-fs": "4.13.4", + "@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-brikke": "^2.2.2", + "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": "^11.7.1", + "mock-fs": "5.2.0", + "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", + "openapi3-ts": "^3.2.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.65.1", + "vite": "^4.2.1" + }, + "engines": { + "node": ">=20.0.0 || >=22.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.17.1", + "ajv-formats": "3.0.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/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "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/@apphosting/build": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@apphosting/build/-/build-0.1.6.tgz", + "integrity": "sha512-nXK1wsR1tehaq9uSRDCGQmN+Dp0xbyGohssYd7g4W8ZbzHfUiab+Pabv34pHVTS03VaSVkjdNcR1g9hezi6s8g==", + "license": "Apache-2.0", + "dependencies": { + "@apphosting/common": "^0.0.8", + "@npmcli/promise-spawn": "^3.0.0", + "colorette": "^2.0.20", + "commander": "^11.1.0", + "npm-pick-manifest": "^9.0.0", + "ts-node": "^10.9.1" + }, + "bin": { + "apphosting-local-build": "dist/bin/localbuild.js" + } + }, + "node_modules/@apphosting/build/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/@apphosting/common": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@apphosting/common/-/common-0.0.8.tgz", + "integrity": "sha512-RJu5gXs2HYV7+anxpVPpp04oXeuHbV3qn402AdXVlnuYM/uWo7aceqmngpfp6Bi376UzRqGjfpdwFHxuwsEGXQ==", + "license": "Apache-2.0" + }, + "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==", + "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==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@electric-sql/pglite": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.3.tgz", + "integrity": "sha512-JrvHOx9q0yvKEby0bK8qzGTVw6K+yEg8enxDWb2IwNKr5XZxRrBb+GNIqoAIP7yXyhRg5jcENWmdHmtnAT87vA==", + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.8.tgz", + "integrity": "sha512-MBWelYjUZThOBrktPU4beuuX4hrUdIPRgfLbTgltLMT6Chh2R7ATxHsT9Nr7L9fXUSYlZCyoIf+n8pis3uoiiw==", + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.3.3" + } + }, + "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/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/@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/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "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/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/@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.3.3", + "resolved": "https://registry.npmjs.org/@google-cloud/cloud-sql-connector/-/cloud-sql-connector-1.3.3.tgz", + "integrity": "sha512-Z/6haHca3bnaf1I2t/lmRgU5pCzGQTK6u9hMnD6a6sCL46QB4JRiBvRI5QMSPjnG8VYr1R7Wp1ZawvQJodEY6g==", + "dependencies": { + "@googleapis/sqladmin": "^19.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.5.0", + "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-4.5.0.tgz", + "integrity": "sha512-ptRLLDrAp1rStD1n3ZrG8FdAfpccqI6M5rCaceF6PL7DU3hqJbvQ2Y91G8MKG7c7zK+jiWv655Qf5r2IvjTzwA==", + "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.8.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.3", + "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.11", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.11.tgz", + "integrity": "sha512-3RaoxOqkHHN2c05bwtBNVJmOf/UwMam0rZYtdl7dsRpsvDwcNpv6LkGgzltQ7xVf822LzBoKEPRvf4D7+xeIDw==", + "license": "Apache-2.0", + "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": "^14.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": "19.0.0", + "resolved": "https://registry.npmjs.org/@googleapis/sqladmin/-/sqladmin-19.0.0.tgz", + "integrity": "sha512-65zgEpQLhpTZqUic+pm4BbdDByN9NsHkphfCIwzpx3fccHPc6OuKsW0XexYCq9oTUtTC4QRjFisBDLV9fChRtg==", + "dependencies": { + "googleapis-common": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@googleapis/sqladmin/node_modules/googleapis-common": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", + "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.7.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/@inquirer/checkbox": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.4.tgz", + "integrity": "sha512-d30576EZdApjAMceijXA5jDzRQHT/MygbC+J8I7EqA6f/FRpYxlRtRJbHF8gHeWYeSdOuTEJqonn7QLB1ELezA==", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/checkbox/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/checkbox/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/confirm": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.8.tgz", + "integrity": "sha512-dNLWCYZvXDjO3rnQfk2iuJNL4Ivwz/T2+C3+WnNfJKsNGSuOs3wAo2F6e0p946gtSAk31nZMfW+MRmYaplPKsg==", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.9.tgz", + "integrity": "sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw==", + "dependencies": { + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/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/core/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/core/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==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@inquirer/core/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/core/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/core/node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@inquirer/core/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/@inquirer/core/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/core/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/@inquirer/editor": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.9.tgz", + "integrity": "sha512-8HjOppAxO7O4wV1ETUlJFg6NDjp/W2NP5FB9ZPAcinAlNT4ZIWOLe2pUVwmmPRSV0NMdI5r/+lflN55AwZOKSw==", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.11.tgz", + "integrity": "sha512-OZSUW4hFMW2TYvX/Sv+NnOZgO8CHT2TU1roUCUIF2T+wfw60XFRRp9MRUPCT06cRnKL+aemt2YmTWwt7rOrNEA==", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", + "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.8.tgz", + "integrity": "sha512-WXJI16oOZ3/LiENCAxe8joniNp8MQxF6Wi5V+EBbVA0ZIOpFcL4I9e7f7cXse0HJeIPCWO8Lcgnk98juItCi7Q==", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.11.tgz", + "integrity": "sha512-pQK68CsKOgwvU2eA53AG/4npRTH2pvs/pZ2bFvzpBhrznh8Mcwt19c+nMO7LHRr3Vreu1KPhNBF3vQAKrjIulw==", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.11.tgz", + "integrity": "sha512-dH6zLdv+HEv1nBs96Case6eppkRggMe8LoOTl30+Gq5Wf27AO/vHFgStTVz4aoevLdNXqwE23++IXGw4eiOXTg==", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password/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/password/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/prompts": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.4.0.tgz", + "integrity": "sha512-EZiJidQOT4O5PYtqnu1JbF0clv36oW2CviR66c7ma4LsupmmQlUwmdReGKRp456OWPWMz3PdrPiYg3aCk3op2w==", + "dependencies": { + "@inquirer/checkbox": "^4.1.4", + "@inquirer/confirm": "^5.1.8", + "@inquirer/editor": "^4.2.9", + "@inquirer/expand": "^4.0.11", + "@inquirer/input": "^4.1.8", + "@inquirer/number": "^3.0.11", + "@inquirer/password": "^4.0.11", + "@inquirer/rawlist": "^4.0.11", + "@inquirer/search": "^3.0.11", + "@inquirer/select": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.11.tgz", + "integrity": "sha512-uAYtTx0IF/PqUAvsRrF3xvnxJV516wmR6YVONOmCWJbbt87HcDHLfL9wmBQFbNJRv5kCjdYKrZcavDkH3sVJPg==", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.11.tgz", + "integrity": "sha512-9CWQT0ikYcg6Ls3TOa7jljsD7PgjcsYEM0bYE+Gkz+uoW9u8eaJCRHJKkucpRE5+xKtaaDbrND+nPDoxzjYyew==", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.1.0.tgz", + "integrity": "sha512-z0a2fmgTSRN+YBuiK1ROfJ2Nvrpij5lVN3gPDkQGhavdvIVGHGW29LwYZfM/j42Ai2hUghTI/uoBuTbrJk42bA==", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select/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/select/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/type": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz", + "integrity": "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": 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==", + "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.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "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/@modelcontextprotocol/sdk": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.2.tgz", + "integrity": "sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.3", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/@npmcli/promise-spawn": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-3.0.0.tgz", + "integrity": "sha512-s9SgS+p3a9Eohe68cSI3fi+hpcZUmXq5P7w0kMlAsWVtR7XbK3ptkZqKT2cK1zLDObJ3sR+8P59sJE0w/KTL1g==", + "license": "ISC", + "dependencies": { + "infer-owner": "^1.0.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", + "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/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "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/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==" + }, + "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==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==" + }, + "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==" + }, + "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.4.2", + "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.4.2.tgz", + "integrity": "sha512-HlZ6Dcr205BmNhwkdXqrg2vkFMN2PluI7Lgr8In3B3wE5PiQHhjRqtW/lGdVU9gw+sM0JcIDx2AN+cW8oSWIcw==", + "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/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha512-/xCVwg8lWvahHsV2wXZt4i64H1sdL+sN1Uoq7fAc8/FA6uYHjuIveDwPwvGUYp4VZiv85dVl6J/Bum3NDAOm8g==", + "dev": true + }, + "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/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/lsofi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/lsofi/-/lsofi-1.0.2.tgz", + "integrity": "sha512-AWzMJsDEsXj6dH+7rxe6RzLtNkW2tGqmJkjIaga76xeQORglf6VcMX5Xwv/jvZ/rfpXdFO0YAREWJEssAC6HWw==", + "dev": true + }, + "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-terminal": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/marked-terminal/-/marked-terminal-6.1.1.tgz", + "integrity": "sha512-DfoUqkmFDCED7eBY9vFUhJ9fW8oZcMAK5EwRDQ9drjTbpQa+DnBTQQCwWhTFVf4WsZ6yYcJTI8D91wxTWXRZZQ==", + "dev": true, + "dependencies": { + "@types/cardinal": "^2.1", + "@types/node": "*", + "chalk": "^5.3.0", + "marked": ">=6.0.0 <12" + } + }, + "node_modules/@types/marked-terminal/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/@types/marked-terminal/node_modules/marked": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-11.2.0.tgz", + "integrity": "sha512-HR0m3bvu0jAPYiIvLUUQtdg1g6D247//lvcekpHO1WMvbwDlwSkZAX9Lw4F4YHE1T0HaaNve0tuAWuV1UJ6vtw==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "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/mock-fs": { + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz", + "integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.2.tgz", + "integrity": "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==", + "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.6", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz", + "integrity": "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==", + "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.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.2.tgz", + "integrity": "sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/@types/request/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/@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/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "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/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==", + "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==", + "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": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "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-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "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/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, + "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==" + }, + "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/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.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==" + }, + "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.1.0", + "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.1.0.tgz", + "integrity": "sha512-rKcWjfiRZ3p5WS9e5q6msXa07s6DaFAMXoyowV+mb2xQG+oYdw2QEUyKi0Xp95JvXzShlM+oGy5QuqSK6TfC1Q==", + "dependencies": { + "tsscmp": "^1.0.6" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/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.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.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/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-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.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/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/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==", + "engines": { + "node": ">=10" + } + }, + "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.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "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" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "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-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/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/cli-highlight/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/cli-highlight/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/cli-highlight/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/cli-highlight/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/cli-highlight/node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" + }, + "node_modules/cli-highlight/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/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-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "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==", + "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.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "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/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": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "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/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==" + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "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.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "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/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/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.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/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==" + }, + "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/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-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/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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=", + "dev": true, + "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-brikke": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-brikke/-/eslint-plugin-brikke-2.2.2.tgz", + "integrity": "sha512-m/nruSg/LxVvyjIZBQ3hJBK2cE4Lxc+klaX+P+yd1FwDDjj+/ul+JJz9fw/zrttcFx7WsA6gtAWdfV5S5yiGSQ==", + "dev": true, + "dependencies": { + "minimatch": "^3.0.4", + "read-pkg-up": "^7.0.1" + }, + "engines": { + "node": ">=0.10.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/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/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/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/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "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/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/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/eventsource": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", + "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", + "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "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.2.0", + "resolved": "https://registry.npmjs.org/exegesis/-/exegesis-4.2.0.tgz", + "integrity": "sha512-MOzRyqhvl+hTA4+W4p0saWRIPlu0grIx4ykjMEYgGLiqr/z9NCIlwSq2jF0gyxNjPZD3xyHgmkW6BSaLVUdctg==", + "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-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/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/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/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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==", + "license": "MIT", + "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==", + "license": "MIT", + "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.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": "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-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "license": "BSD-3-Clause" + }, + "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/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.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "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.7.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.0.tgz", + "integrity": "sha512-DSrkyMTfAnAm4ks9Go20QGOcXEyW/NmZhvTYBU2rb4afBB393WIMQPWPEDMl/k8xqiNN9HYq2zao3oWXsdl2Tg==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^10.0.0" + }, + "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/gaxios/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "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.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.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-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "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/globby/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "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.11.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.11.0.tgz", + "integrity": "sha512-epX3ww/mNnhl6tL45EQ/oixsY8JLEgUFoT4A5E/5iAR4esld9Kqv6IJGk7EmGuOgDvaarwF95hU2+v7Irql9lw==", + "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.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/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-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "engines": { + "node": "*" + } + }, + "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": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "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==" + }, + "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/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-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "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==", + "license": "MIT", + "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-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "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==", + "dev": true, + "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/js-yaml/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/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/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "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/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "optional": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "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": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "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/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/lsofi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lsofi/-/lsofi-1.0.0.tgz", + "integrity": "sha512-MKr9vM1MSm+TSKfI05IYxpKV1NCxpJaBLnELyIf784zYJ5KV9lGCE1EvpA2DtXDNM3fCuFeCwXUzim/fyQRi+A==", + "dependencies": { + "is-number": "^2.1.0", + "through2": "^2.0.1" + } + }, + "node_modules/lsofi/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "node_modules/lsofi/node_modules/is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lsofi/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==" + }, + "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-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": "13.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.2.tgz", + "integrity": "sha512-J6CPjP8pS5sgrRqxVRvkCIkZ6MFdRIjDkwUwgJ9nL2fbmM6qGQeB2C16hi8Cc9BOzj6xXzy0jyi0iPIfnMHYzA==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/marked-terminal": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.1.0.tgz", + "integrity": "sha512-+pvwa14KZL74MVXjYdPR3nSInhGhNvPce/3mqLVZT2oUvt654sL1XImFuLZ1pkA866IYZ3ikDTOFUIC7XzpZZg==", + "dependencies": { + "ansi-escapes": "^7.0.0", + "chalk": "^5.3.0", + "cli-highlight": "^2.1.11", + "cli-table3": "^0.6.5", + "node-emoji": "^2.1.3", + "supports-hyperlinks": "^3.0.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "marked": ">=1 <14" + } + }, + "node_modules/marked-terminal/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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": "11.7.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.1.tgz", + "integrity": "sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/mocha/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, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "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/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": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "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/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/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/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, + "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/mocha/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, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/mock-fs": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz", + "integrity": "sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "license": "MIT", + "optional": true + }, + "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/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": "2.1.3", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz", + "integrity": "sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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-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==", + "license": "BSD-2-Clause", + "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==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/npm-pick-manifest": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.1.0.tgz", + "integrity": "sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==", + "license": "ISC", + "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-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.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "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/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/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "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==" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dependencies": { + "parse5": "^6.0.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-equal": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/path-equal/-/path-equal-1.2.5.tgz", + "integrity": "sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g==", + "dev": true + }, + "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-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.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/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.12.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", + "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", + "dependencies": { + "pg-connection-string": "^2.6.4", + "pg-pool": "^3.6.2", + "pg-protocol": "^1.6.1", + "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.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" + }, + "node_modules/pg-gateway": { + "version": "0.3.0-beta.4", + "resolved": "https://registry.npmjs.org/pg-gateway/-/pg-gateway-0.3.0-beta.4.tgz", + "integrity": "sha512-CTjsM7Z+0Nx2/dyZ6r8zRsc3f9FScoD5UAOlfUx1Fdv/JOIWvRbF7gou6l6vP+uypXQVoYPgw8xZDXgMGvBa4Q==", + "license": "MIT" + }, + "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.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", + "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" + }, + "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/pglite-2": { + "name": "@electric-sql/pglite", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.2.17.tgz", + "integrity": "sha512-qEpKRT2oUaWDH6tjRxLHjdzMqRUGYDnGZlKrnL4dJ77JVMcP2Hpo3NYnOSPKdZdeec57B6QPprCUFg0picx5Pw==", + "license": "Apache-2.0" + }, + "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.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "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/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "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/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "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.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "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.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "license": "MIT", + "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/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/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": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "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/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/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "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.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "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.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.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/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": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "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/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.2.0", + "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-9.2.0.tgz", + "integrity": "sha512-QrJAJIpAij0jJT1nEwYTB0SzDi4k0wYygu6GxK0ko8twiQgfgaOAZ7Hu99p02MTAsGho753zhzSvsw8We4PBEQ==", + "dependencies": { + "basic-auth-connect": "^1.1.0", + "commander": "^10.0.0", + "compression": "^1.7.0", + "connect": "^3.7.0", + "destroy": "^1.0.4", + "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.9.0", + "router": "^2.0.0", + "update-notifier-cjs": "^5.1.6" + }, + "bin": { + "superstatic": "lib/bin/server.js" + }, + "engines": { + "node": "18 || 20 || 22" + }, + "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": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "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.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "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": "3.0.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz", + "integrity": "sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "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/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/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/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/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==", + "license": "MIT", + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "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==", + "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==", + "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/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "engines": { + "node": ">=0.6.x" + } + }, + "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==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/typescript-json-schema": { + "version": "0.65.1", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.65.1.tgz", + "integrity": "sha512-tuGH7ff2jPaUYi6as3lHyHcKpSmXIqN7/mu50x3HlYn0EHzLpmt3nplZ7EuhUkO0eqDRc9GqWNkfjgBPIS9kxg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@types/node": "^18.11.9", + "glob": "^7.1.7", + "path-equal": "^1.2.5", + "safe-stable-stringify": "^2.2.0", + "ts-node": "^10.9.1", + "typescript": "~5.5.0", + "yargs": "^17.1.1" + }, + "bin": { + "typescript-json-schema": "bin/typescript-json-schema" + } + }, + "node_modules/typescript-json-schema/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/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/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-json-schema/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/typescript-json-schema/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/typescript-json-schema/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/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/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "engines": { + "node": ">=4" + } + }, + "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.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/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==" + }, + "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/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "license": "ISC", + "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": "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": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "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": "14.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz", + "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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": "9.3.3", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.3.tgz", + "integrity": "sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==", + "dev": true, + "license": "Apache-2.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/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.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "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.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "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==", + "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==", + "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==", + "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==", + "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/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "engines": { + "node": ">=18" + }, + "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.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, + "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.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "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": { + "ajv": "^8.17.1", + "ajv-formats": "3.0.1", + "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" + } + }, + "@apphosting/build": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@apphosting/build/-/build-0.1.6.tgz", + "integrity": "sha512-nXK1wsR1tehaq9uSRDCGQmN+Dp0xbyGohssYd7g4W8ZbzHfUiab+Pabv34pHVTS03VaSVkjdNcR1g9hezi6s8g==", + "requires": { + "@apphosting/common": "^0.0.8", + "@npmcli/promise-spawn": "^3.0.0", + "colorette": "^2.0.20", + "commander": "^11.1.0", + "npm-pick-manifest": "^9.0.0", + "ts-node": "^10.9.1" + }, + "dependencies": { + "commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==" + } + } + }, + "@apphosting/common": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@apphosting/common/-/common-0.0.8.tgz", + "integrity": "sha512-RJu5gXs2HYV7+anxpVPpp04oXeuHbV3qn402AdXVlnuYM/uWo7aceqmngpfp6Bi376UzRqGjfpdwFHxuwsEGXQ==" + }, + "@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==", + "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==", + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, + "@electric-sql/pglite": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.3.tgz", + "integrity": "sha512-JrvHOx9q0yvKEby0bK8qzGTVw6K+yEg8enxDWb2IwNKr5XZxRrBb+GNIqoAIP7yXyhRg5jcENWmdHmtnAT87vA==" + }, + "@electric-sql/pglite-tools": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.8.tgz", + "integrity": "sha512-MBWelYjUZThOBrktPU4beuuX4hrUdIPRgfLbTgltLMT6Chh2R7ATxHsT9Nr7L9fXUSYlZCyoIf+n8pis3uoiiw==", + "requires": {} + }, + "@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": { + "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.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" + } + }, + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "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" + } + }, + "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 + }, + "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.3.3", + "resolved": "https://registry.npmjs.org/@google-cloud/cloud-sql-connector/-/cloud-sql-connector-1.3.3.tgz", + "integrity": "sha512-Z/6haHca3bnaf1I2t/lmRgU5pCzGQTK6u9hMnD6a6sCL46QB4JRiBvRI5QMSPjnG8VYr1R7Wp1ZawvQJodEY6g==", + "requires": { + "@googleapis/sqladmin": "^19.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.5.0", + "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-4.5.0.tgz", + "integrity": "sha512-ptRLLDrAp1rStD1n3ZrG8FdAfpccqI6M5rCaceF6PL7DU3hqJbvQ2Y91G8MKG7c7zK+jiWv655Qf5r2IvjTzwA==", + "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.8.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.3", + "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.11", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.11.tgz", + "integrity": "sha512-3RaoxOqkHHN2c05bwtBNVJmOf/UwMam0rZYtdl7dsRpsvDwcNpv6LkGgzltQ7xVf822LzBoKEPRvf4D7+xeIDw==", + "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": "^14.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": "19.0.0", + "resolved": "https://registry.npmjs.org/@googleapis/sqladmin/-/sqladmin-19.0.0.tgz", + "integrity": "sha512-65zgEpQLhpTZqUic+pm4BbdDByN9NsHkphfCIwzpx3fccHPc6OuKsW0XexYCq9oTUtTC4QRjFisBDLV9fChRtg==", + "requires": { + "googleapis-common": "^7.0.0" + }, + "dependencies": { + "googleapis-common": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", + "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", + "requires": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.7.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 + }, + "@inquirer/checkbox": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.4.tgz", + "integrity": "sha512-d30576EZdApjAMceijXA5jDzRQHT/MygbC+J8I7EqA6f/FRpYxlRtRJbHF8gHeWYeSdOuTEJqonn7QLB1ELezA==", + "requires": { + "@inquirer/core": "^10.1.9", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "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==" + } + } + }, + "@inquirer/confirm": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.8.tgz", + "integrity": "sha512-dNLWCYZvXDjO3rnQfk2iuJNL4Ivwz/T2+C3+WnNfJKsNGSuOs3wAo2F6e0p946gtSAk31nZMfW+MRmYaplPKsg==", + "requires": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5" + } + }, + "@inquirer/core": { + "version": "10.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.9.tgz", + "integrity": "sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw==", + "requires": { + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "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" + } + }, + "cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==" + }, + "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==" + }, + "mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==" + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + }, + "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/editor": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.9.tgz", + "integrity": "sha512-8HjOppAxO7O4wV1ETUlJFg6NDjp/W2NP5FB9ZPAcinAlNT4ZIWOLe2pUVwmmPRSV0NMdI5r/+lflN55AwZOKSw==", + "requires": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "external-editor": "^3.1.0" + } + }, + "@inquirer/expand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.11.tgz", + "integrity": "sha512-OZSUW4hFMW2TYvX/Sv+NnOZgO8CHT2TU1roUCUIF2T+wfw60XFRRp9MRUPCT06cRnKL+aemt2YmTWwt7rOrNEA==", + "requires": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/figures": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", + "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==" + }, + "@inquirer/input": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.8.tgz", + "integrity": "sha512-WXJI16oOZ3/LiENCAxe8joniNp8MQxF6Wi5V+EBbVA0ZIOpFcL4I9e7f7cXse0HJeIPCWO8Lcgnk98juItCi7Q==", + "requires": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5" + } + }, + "@inquirer/number": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.11.tgz", + "integrity": "sha512-pQK68CsKOgwvU2eA53AG/4npRTH2pvs/pZ2bFvzpBhrznh8Mcwt19c+nMO7LHRr3Vreu1KPhNBF3vQAKrjIulw==", + "requires": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5" + } + }, + "@inquirer/password": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.11.tgz", + "integrity": "sha512-dH6zLdv+HEv1nBs96Case6eppkRggMe8LoOTl30+Gq5Wf27AO/vHFgStTVz4aoevLdNXqwE23++IXGw4eiOXTg==", + "requires": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2" + }, + "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==" + } + } + }, + "@inquirer/prompts": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.4.0.tgz", + "integrity": "sha512-EZiJidQOT4O5PYtqnu1JbF0clv36oW2CviR66c7ma4LsupmmQlUwmdReGKRp456OWPWMz3PdrPiYg3aCk3op2w==", + "requires": { + "@inquirer/checkbox": "^4.1.4", + "@inquirer/confirm": "^5.1.8", + "@inquirer/editor": "^4.2.9", + "@inquirer/expand": "^4.0.11", + "@inquirer/input": "^4.1.8", + "@inquirer/number": "^3.0.11", + "@inquirer/password": "^4.0.11", + "@inquirer/rawlist": "^4.0.11", + "@inquirer/search": "^3.0.11", + "@inquirer/select": "^4.1.0" + } + }, + "@inquirer/rawlist": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.11.tgz", + "integrity": "sha512-uAYtTx0IF/PqUAvsRrF3xvnxJV516wmR6YVONOmCWJbbt87HcDHLfL9wmBQFbNJRv5kCjdYKrZcavDkH3sVJPg==", + "requires": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/search": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.11.tgz", + "integrity": "sha512-9CWQT0ikYcg6Ls3TOa7jljsD7PgjcsYEM0bYE+Gkz+uoW9u8eaJCRHJKkucpRE5+xKtaaDbrND+nPDoxzjYyew==", + "requires": { + "@inquirer/core": "^10.1.9", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/select": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.1.0.tgz", + "integrity": "sha512-z0a2fmgTSRN+YBuiK1ROfJ2Nvrpij5lVN3gPDkQGhavdvIVGHGW29LwYZfM/j42Ai2hUghTI/uoBuTbrJk42bA==", + "requires": { + "@inquirer/core": "^10.1.9", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "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==" + } + } + }, + "@inquirer/type": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz", + "integrity": "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==", + "requires": {} + }, + "@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==" + }, + "@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.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "@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 + }, + "@modelcontextprotocol/sdk": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.2.tgz", + "integrity": "sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==", + "requires": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.3", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "dependencies": { + "accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "requires": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + } + }, + "body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "requires": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + } + }, + "content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + }, + "cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" + }, + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "requires": { + "ms": "^2.1.3" + } + }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "requires": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + } + }, + "finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "requires": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + } + }, + "fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" + }, + "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==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" + }, + "merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" + }, + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "requires": { + "mime-db": "^1.54.0" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" + }, + "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" + } + }, + "qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "requires": { + "side-channel": "^1.1.0" + } + }, + "raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.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==" + }, + "send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "requires": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + } + }, + "serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "requires": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "requires": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + } + } + } + }, + "@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" + } + } + } + }, + "@npmcli/promise-spawn": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-3.0.0.tgz", + "integrity": "sha512-s9SgS+p3a9Eohe68cSI3fi+hpcZUmXq5P7w0kMlAsWVtR7XbK3ptkZqKT2cK1zLDObJ3sR+8P59sJE0w/KTL1g==", + "requires": { + "infer-owner": "^1.0.4" + } + }, + "@opentelemetry/api": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==" + }, + "@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 + } + } + }, + "@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==" + }, + "@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/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==" + }, + "@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==" + }, + "@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==" + }, + "@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==" + }, + "@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.4.2", + "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.4.2.tgz", + "integrity": "sha512-HlZ6Dcr205BmNhwkdXqrg2vkFMN2PluI7Lgr8In3B3wE5PiQHhjRqtW/lGdVU9gw+sM0JcIDx2AN+cW8oSWIcw==", + "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/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@types/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha512-/xCVwg8lWvahHsV2wXZt4i64H1sdL+sN1Uoq7fAc8/FA6uYHjuIveDwPwvGUYp4VZiv85dVl6J/Bum3NDAOm8g==", + "dev": true + }, + "@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/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/lsofi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/lsofi/-/lsofi-1.0.2.tgz", + "integrity": "sha512-AWzMJsDEsXj6dH+7rxe6RzLtNkW2tGqmJkjIaga76xeQORglf6VcMX5Xwv/jvZ/rfpXdFO0YAREWJEssAC6HWw==", + "dev": true + }, + "@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-terminal": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/marked-terminal/-/marked-terminal-6.1.1.tgz", + "integrity": "sha512-DfoUqkmFDCED7eBY9vFUhJ9fW8oZcMAK5EwRDQ9drjTbpQa+DnBTQQCwWhTFVf4WsZ6yYcJTI8D91wxTWXRZZQ==", + "dev": true, + "requires": { + "@types/cardinal": "^2.1", + "@types/node": "*", + "chalk": "^5.3.0", + "marked": ">=6.0.0 <12" + }, + "dependencies": { + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true + }, + "marked": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-11.2.0.tgz", + "integrity": "sha512-HR0m3bvu0jAPYiIvLUUQtdg1g6D247//lvcekpHO1WMvbwDlwSkZAX9Lw4F4YHE1T0HaaNve0tuAWuV1UJ6vtw==", + "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/mock-fs": { + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz", + "integrity": "sha512-mXmM0o6lULPI8z3XNnQCpL0BGxPwx1Ul1wXYEPBGl4efShyxW2Rln0JOPEWGyZaYZMM6OVXM/15zUuFMY52ljg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@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.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.2.tgz", + "integrity": "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==", + "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.6", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz", + "integrity": "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==", + "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.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.2.tgz", + "integrity": "sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12", + "safe-buffer": "^5.2.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==" + } + } + }, + "@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" + } + }, + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "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 + } + } + }, + "@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/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==" + }, + "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==" + }, + "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": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "requires": { + "ajv": "^8.17.1" + } + }, + "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-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "requires": { + "environment": "^1.0.0" + } + }, + "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" + } + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, + "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==" + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "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.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==" + }, + "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.1.0", + "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.1.0.tgz", + "integrity": "sha512-rKcWjfiRZ3p5WS9e5q6msXa07s6DaFAMXoyowV+mb2xQG+oYdw2QEUyKi0Xp95JvXzShlM+oGy5QuqSK6TfC1Q==", + "requires": { + "tsscmp": "^1.0.6" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "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.13.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.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "requires": { + "fill-range": "^7.1.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 + }, + "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-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.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 + }, + "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" + } + }, + "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==" + }, + "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.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "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-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "requires": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.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": "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==" + }, + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" + }, + "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" + } + } + } + }, + "cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==" + }, + "cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "requires": { + "@colors/colors": "1.5.0", + "string-width": "^4.2.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==", + "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==", + "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.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" + }, + "colornames": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", + "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=" + }, + "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": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" + }, + "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": { + "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==" + }, + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "requires": { + "cross-spawn": "^7.0.1" + } + }, + "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.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "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" + } + }, + "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 + }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "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==" + }, + "emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==" + }, + "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==" + }, + "environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==" + }, + "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-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "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 + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.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-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=", + "dev": true + }, + "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": { + "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-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.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 + }, + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "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" + } + }, + "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 + }, + "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-brikke": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-brikke/-/eslint-plugin-brikke-2.2.2.tgz", + "integrity": "sha512-m/nruSg/LxVvyjIZBQ3hJBK2cE4Lxc+klaX+P+yd1FwDDjj+/ul+JJz9fw/zrttcFx7WsA6gtAWdfV5S5yiGSQ==", + "dev": true, + "requires": { + "minimatch": "^3.0.4", + "read-pkg-up": "^7.0.1" + } + }, + "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==" + }, + "eventsource": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", + "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", + "requires": { + "eventsource-parser": "^3.0.1" + } + }, + "eventsource-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", + "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==" + }, + "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.2.0", + "resolved": "https://registry.npmjs.org/exegesis/-/exegesis-4.2.0.tgz", + "integrity": "sha512-MOzRyqhvl+hTA4+W4p0saWRIPlu0grIx4ykjMEYgGLiqr/z9NCIlwSq2jF0gyxNjPZD3xyHgmkW6BSaLVUdctg==", + "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-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" + } + } + } + }, + "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.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, + "finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "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==" + } + } + }, + "express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "requires": {} + }, + "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.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": "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-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==" + }, + "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==" + }, + "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.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "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.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "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.7.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.0.tgz", + "integrity": "sha512-DSrkyMTfAnAm4ks9Go20QGOcXEyW/NmZhvTYBU2rb4afBB393WIMQPWPEDMl/k8xqiNN9HYq2zao3oWXsdl2Tg==", + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^10.0.0" + }, + "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": "^14.0.0" + } + }, + "uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==" + } + } + }, + "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.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.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-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "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" + }, + "dependencies": { + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true + } + } + }, + "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.11.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.11.0.tgz", + "integrity": "sha512-epX3ww/mNnhl6tL45EQ/oixsY8JLEgUFoT4A5E/5iAR4esld9Kqv6IJGk7EmGuOgDvaarwF95hU2+v7Irql9lw==", + "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.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" + }, + "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" + } + }, + "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-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" + }, + "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.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "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 + }, + "highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" + }, + "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": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==" + }, + "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==" + }, + "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==" + }, + "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-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "requires": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + } + } + }, + "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-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "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==", + "dev": true + }, + "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" + }, + "dependencies": { + "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" + } + } + } + }, + "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" + } + }, + "jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "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 + }, + "marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "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": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "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" + } + }, + "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 + } + } + }, + "lsofi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lsofi/-/lsofi-1.0.0.tgz", + "integrity": "sha512-MKr9vM1MSm+TSKfI05IYxpKV1NCxpJaBLnELyIf784zYJ5KV9lGCE1EvpA2DtXDNM3fCuFeCwXUzim/fyQRi+A==", + "requires": { + "is-number": "^2.1.0", + "through2": "^2.0.1" + }, + "dependencies": { + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==", + "requires": { + "kind-of": "^3.0.2" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "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==" + }, + "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" + } + }, + "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": "13.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.2.tgz", + "integrity": "sha512-J6CPjP8pS5sgrRqxVRvkCIkZ6MFdRIjDkwUwgJ9nL2fbmM6qGQeB2C16hi8Cc9BOzj6xXzy0jyi0iPIfnMHYzA==" + }, + "marked-terminal": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.1.0.tgz", + "integrity": "sha512-+pvwa14KZL74MVXjYdPR3nSInhGhNvPce/3mqLVZT2oUvt654sL1XImFuLZ1pkA866IYZ3ikDTOFUIC7XzpZZg==", + "requires": { + "ansi-escapes": "^7.0.0", + "chalk": "^5.3.0", + "cli-highlight": "^2.1.11", + "cli-table3": "^0.6.5", + "node-emoji": "^2.1.3", + "supports-hyperlinks": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==" + } + } + }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "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.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" + }, + "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": "11.7.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.1.tgz", + "integrity": "sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==", + "dev": true, + "requires": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "requires": { + "readdirp": "^4.0.1" + } + }, + "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.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "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 + }, + "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": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.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==", + "dev": true + }, + "readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "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" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "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 + } + } + }, + "mock-fs": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz", + "integrity": "sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw==", + "dev": true + }, + "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=" + }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "requires": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "optional": 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 + } + } + }, + "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": "2.1.3", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz", + "integrity": "sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==", + "requires": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + } + }, + "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": "^14.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-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==", + "requires": { + "semver": "^7.1.1" + } + }, + "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==" + }, + "npm-package-arg": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", + "requires": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "requires": { + "lru-cache": "^10.0.1" + } + }, + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + } + } + }, + "npm-pick-manifest": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.1.0.tgz", + "integrity": "sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==", + "requires": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + } + }, + "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.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" + }, + "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": { + "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" + } + }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, + "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==" + }, + "parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "requires": { + "parse5": "^6.0.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-equal": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/path-equal/-/path-equal-1.2.5.tgz", + "integrity": "sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g==", + "dev": true + }, + "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-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.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "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.12.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", + "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", + "requires": { + "pg-cloudflare": "^1.1.1", + "pg-connection-string": "^2.6.4", + "pg-pool": "^3.6.2", + "pg-protocol": "^1.6.1", + "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.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" + }, + "pg-gateway": { + "version": "0.3.0-beta.4", + "resolved": "https://registry.npmjs.org/pg-gateway/-/pg-gateway-0.3.0-beta.4.tgz", + "integrity": "sha512-CTjsM7Z+0Nx2/dyZ6r8zRsc3f9FScoD5UAOlfUx1Fdv/JOIWvRbF7gou6l6vP+uypXQVoYPgw8xZDXgMGvBa4Q==" + }, + "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.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", + "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "requires": {} + }, + "pg-protocol": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" + }, + "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" + } + }, + "pglite-2": { + "version": "npm:@electric-sql/pglite@0.2.17", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.2.17.tgz", + "integrity": "sha512-qEpKRT2oUaWDH6tjRxLHjdzMqRUGYDnGZlKrnL4dJ77JVMcP2Hpo3NYnOSPKdZdeec57B6QPprCUFg0picx5Pw==" + }, + "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.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "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 + }, + "pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==" + }, + "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 + }, + "proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==" + }, + "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.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + }, + "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.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "requires": { + "side-channel": "^1.0.6" + } + }, + "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" + } + }, + "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 + }, + "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": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "requires": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "dependencies": { + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "requires": { + "ms": "^2.1.3" + } + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==" + } + } + }, + "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" + } + }, + "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==" + }, + "safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true + }, + "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.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "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.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "requires": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } + } + }, + "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 + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "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.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + } + }, + "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 + }, + "skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "requires": { + "unicode-emoji-modifier-base": "^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 + }, + "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.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "requires": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + } + }, + "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": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "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" + } + }, + "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.2.0", + "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-9.2.0.tgz", + "integrity": "sha512-QrJAJIpAij0jJT1nEwYTB0SzDi4k0wYygu6GxK0ko8twiQgfgaOAZ7Hu99p02MTAsGho753zhzSvsw8We4PBEQ==", + "requires": { + "basic-auth-connect": "^1.1.0", + "commander": "^10.0.0", + "compression": "^1.7.0", + "connect": "^3.7.0", + "destroy": "^1.0.4", + "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.9.0", + "re2": "^1.17.7", + "router": "^2.0.0", + "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": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "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.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "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": "3.0.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz", + "integrity": "sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==", + "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 + }, + "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 + }, + "thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "requires": { + "any-promise": "^1.0.0" + } + }, + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "requires": { + "thenify": ">= 3.1.0 < 4" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + }, + "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" + } + } + } + }, + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "requires": { + "punycode": "^2.3.1" + } + }, + "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==", + "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==" + } + } + }, + "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 + }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" + }, + "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==" + }, + "typescript-json-schema": { + "version": "0.65.1", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.65.1.tgz", + "integrity": "sha512-tuGH7ff2jPaUYi6as3lHyHcKpSmXIqN7/mu50x3HlYn0EHzLpmt3nplZ7EuhUkO0eqDRc9GqWNkfjgBPIS9kxg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@types/node": "^18.11.9", + "glob": "^7.1.7", + "path-equal": "^1.2.5", + "safe-stable-stringify": "^2.2.0", + "ts-node": "^10.9.1", + "typescript": "~5.5.0", + "yargs": "^17.1.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" + } + }, + "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" + } + }, + "typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "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.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "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 + } + } + }, + "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 + }, + "unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==" + }, + "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.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" + } + }, + "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==" + }, + "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" + } + }, + "validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==" + }, + "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": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" + }, + "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": "14.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz", + "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==", + "requires": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.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": "9.3.3", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.3.tgz", + "integrity": "sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==", + "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.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "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.6.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", + "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==" + }, + "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" + }, + "dependencies": { + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + } + } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==" + }, + "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==" + }, + "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==" + }, + "yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==" + }, + "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.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==" + }, + "zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "requires": {} + }, + "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 c3643678ccb..00000000000 --- a/package-lock.json +++ /dev/null @@ -1,10716 +0,0 @@ -{ - "name": "firebase-tools", - "version": "9.9.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@apidevtools/json-schema-ref-parser": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz", - "integrity": "sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg==", - "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" - } - } - } - }, - "@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/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": "3.4.5", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.5.tgz", - "integrity": "sha512-MNL15wC3EKyw1VLF+RoVO4hJJdk9t/Hlv3rt1OL65Qvuadm4BYo6g9ZJQqoq7X8NBFSsQXgAujWciovh2lpVjA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@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": "*" - } - }, - "@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.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-align": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", - "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", - "requires": { - "string-width": "^3.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==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.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": "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.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" - } - } - }, - "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==" - }, - "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.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.0.tgz", - "integrity": "sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==" - }, - "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.1", - "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", - "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", - "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.3", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", - "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", - "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": "2.2.0", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-2.2.0.tgz", - "integrity": "sha512-aiQcQowF01RxFI4ZLFMpzyotbQonhNpBao6dkI8JPk5a+hmSjR5ErHp2CQySmQe8os3VBqLCIh87nDBgZXvsmg==", - "requires": { - "ast-types": "^0.13.2", - "escodegen": "^1.8.1", - "esprima": "^4.0.0" - } - }, - "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=" - }, - "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==" - }, - "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": { - "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" - } - }, - "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.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "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.6", - "resolved": "https://registry.npmjs.org/exegesis/-/exegesis-2.5.6.tgz", - "integrity": "sha512-e+YkH/zZTN2njiwrV8tY6tHGDsFu3LyR/YbrqdWvDZaAJ5YGWaBYyd3oX/Y26iGqQc+7jLEKLDTv2UPzjAYL8w==", - "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": "^1.3.1", - "json-schema-traverse": "^0.4.1", - "lodash": "^4.17.11", - "openapi3-ts": "^1.2.0", - "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" - } - }, - "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==" - }, - "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" - } - } - } - }, - "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" - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" - }, - "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": { - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "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" - } - }, - "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.11.0", - "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-3.11.0.tgz", - "integrity": "sha512-i1uMhZ/M6i5SCI3ulKo7EWX0/LD+I5o6N+sk0HbOWfzyWfOl0iJTvQkR3BVDcjrlhPVC4xG1bDTLxd+DTkLqaw==", - "dev": true, - "requires": { - "@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": "0.23.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.23.1.tgz", - "integrity": "sha1-ZhHbpq3yq43Jxp+rN83fiBgVfj0=", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0", - "path-is-absolute": "^1.0.0", - "rimraf": "^2.2.8" - }, - "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" - } - } - } - }, - "fs-minipass": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz", - "integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==", - "requires": { - "minipass": "^2.2.1" - } - }, - "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.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "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" - } - }, - "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" - } - }, - "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.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", - "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==", - "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" - } - }, - "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.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", - "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": "1.3.2", - "resolved": "https://registry.npmjs.org/json-ptr/-/json-ptr-1.3.2.tgz", - "integrity": "sha512-tFH40YQ+lG7mgYYM1kGZOhQngO4SbOEHZJlA4W+NtetWZ20EUU3BPU+30uWRKumuAJoSo5eqrsXD2h72ioS8ew==", - "requires": { - "tslib": "^2.0.0" - }, - "dependencies": { - "tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" - } - } - }, - "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-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": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" - }, - "jsonpath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.0.2.tgz", - "integrity": "sha512-rmzlgFZiQPc6q4HDyK8s9Qb4oxBnI5sF61y/Co5PV0lc3q2bIuRsNdueVbhoSHdKM4fxeimphOAtfz47yjCfeA==", - "dev": true, - "requires": { - "esprima": "1.2.2", - "static-eval": "2.0.2", - "underscore": "1.7.0" - }, - "dependencies": { - "esprima": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", - "integrity": "sha1-dqD9Zvz+FU/SkmZ9wmQBl1CxZXs=", - "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.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" - }, - "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": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "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.3.5", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", - "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - }, - "dependencies": { - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" - } - } - }, - "minizlib": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", - "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", - "requires": { - "minipass": "^2.2.1" - } - }, - "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" - } - }, - "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" - } - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "requires": { - "is-glob": "^4.0.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 - }, - "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" - } - } - } - }, - "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.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.1.tgz", - "integrity": "sha512-gB8eG6ubxz67c7O2gaGiyWdRUIbH61q7anjgueDqCC9kvIs/b4CTtCMaQKeJbv1/Y7FT19I4zKwYmjnjInRQsg==" - }, - "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.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", - "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", - "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" - } - }, - "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.0.5", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz", - "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==", - "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.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", - "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" - }, - "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": "1.4.0", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-1.4.0.tgz", - "integrity": "sha512-8DmE2oKayvSkIR3XSZ4+pRliBsx19bSNeIzkTPswY8r4wvjX86bMxsORdqwAwMxE8PefOcSAT2auvi/0TZe9yA==" - }, - "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": "4.1.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-4.1.0.tgz", - "integrity": "sha512-ejNgYm2HTXSIYX9eFlkvqFp8hyJ374uDf0Zq5YUAifiSh1D6fo+iBivQZirGvVv8dCYUsLhmLBRhlAYvBKI5+Q==", - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4", - "get-uri": "3", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "5", - "pac-resolver": "^4.1.0", - "raw-body": "^2.2.0", - "socks-proxy-agent": "5" - }, - "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==" - } - } - }, - "pac-resolver": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-4.2.0.tgz", - "integrity": "sha512-rPACZdUyuxT5Io/gFKUeeZFfE5T7ve7cAkE5TUZRRfuKP0u5Hocwe48X7ZEm6mYB+bTB0Qf+xlVlA/RM/i6RCQ==", - "requires": { - "degenerator": "^2.2.0", - "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.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "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 - }, - "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": "4.0.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-4.0.0.tgz", - "integrity": "sha512-8P0Y2SkwvKjiGU1IkEfYuTteioMIDFxPL4/j49zzt5Mz3pG1KO+mIrDG1qH0PQUHTTczjwGcYl+EzfXiFj5vUQ==", - "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": "^4.1.0", - "proxy-from-env": "^1.0.0", - "socks-proxy-agent": "^5.0.0" - }, - "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" - } - }, - "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.0.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", - "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==", - "requires": { - "escape-goat": "^2.0.0" - } - }, - "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==" - } - } - }, - "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": { - "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.1.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", - "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==" - }, - "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.5.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.5.1.tgz", - "integrity": "sha512-oZCsJJxapULAYJaEYBSzMcz8m3jqgGrHaGhkmU/o/PQfFWYWxkAaA0UMGImb6s6tEXfKi959X6VJjMMQ3P6TTQ==", - "requires": { - "ip": "^1.1.5", - "smart-buffer": "^4.1.0" - } - }, - "socks-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.0.tgz", - "integrity": "sha512-lEpa1zsWCChxiynk+lCycKuC502RxDWLKJZoIhnxrWNjLSDGYRFflHA1/228VkRcnv9TIb8w98derGbpKxJRgA==", - "requires": { - "agent-base": "6", - "debug": "4", - "socks": "^2.3.3" - }, - "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==" - } - } - }, - "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" - } - }, - "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.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "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.10", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz", - "integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==", - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.5", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - }, - "dependencies": { - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" - } - } - }, - "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.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz", - "integrity": "sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==", - "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 - }, - "underscore": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", - "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", - "dev": true - }, - "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==" - } - } - }, - "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": "4.1.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.0.tgz", - "integrity": "sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew==", - "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.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==" - }, - "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.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" - } - } - } - }, - "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.6", - "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.6.tgz", - "integrity": "sha512-eSXsXZ2jLvGWeLYlQA3Gh36BcjF+0amo92+wHPyN1mdR8Nxf75fuEuYTd9c0a+m/vhCjRK0ESlE9YNLW+E1VEw==", - "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": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "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" - } - }, - "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.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "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.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz", - "integrity": "sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==" - }, - "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==", - "dev": true - }, - "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 - } - } - }, - "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 be47517ea58..3ab1a6b8f3e 100644 --- a/package.json +++ b/package.json @@ -1,39 +1,58 @@ { "name": "firebase-tools", - "version": "9.9.0", + "version": "14.19.1", "description": "Command-Line Interface for Firebase", "main": "./lib/index.js", "bin": { "firebase": "./lib/bin/firebase.js" }, "scripts": { - "build": "tsc", - "build:watch": "tsc --watch", - "clean": "rimraf lib dev", + "build": "tsc && npm run copyfiles", + "build:publish": "tsc --build tsconfig.publish.json && npm run copyfiles", + "build:watch": "npm run build && tsc --watch", + "clean": "node -e \"fs.rmSync('lib', { recursive: true, force: true }); fs.rmSync('dev', { recursive: true, force: true });\"", + "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", "generate:auth-api": "ts-node scripts/gen-auth-api-spec.ts", + "generate:json-schema": "typescript-json-schema --strictNullChecks --required --noExtraProps --ignoreErrors src/firebaseConfig.ts FirebaseConfig > schema/firebase-config.json", "lint": "npm run lint:ts && npm run lint:other", "lint:changed-files": "ts-node ./scripts/lint-changed-files.ts", "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:fast": "mocha 'src/**/*.spec.{ts,js}'", + "mocha": "nyc --reporter=html 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:apptesting": "npm run lint:quiet && npm run test:compile && nyc mocha 'src/apptesting/*.spec.{ts,js}'", + "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-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:management": "npm run lint:quiet && npm run test:compile && nyc mocha 'src/management/*.spec.{ts,js}'", + "test:dataconnect-deploy": "bash ./scripts/dataconnect-test/run.sh", + "test:dataconnect-emulator": "bash ./scripts/dataconnect-emulator-tests/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", - "templates", - "standalone" + "prompts", + "schema", + "standalone", + "templates" ], "repository": { "type": "git", @@ -52,7 +71,7 @@ ], "preferGlobal": true, "engines": { - "node": ">= 10.13" + "node": ">=20.0.0 || >=22.0.0" }, "author": "Firebase (https://firebase.google.com/)", "license": "MIT", @@ -77,132 +96,184 @@ ".ts" ], "exclude": [ + "src/**/*.spec.*", + "src/**/testing/**/*", "src/test/**/*" ] }, "dependencies": { - "@google-cloud/pubsub": "^2.7.0", - "@types/archiver": "^5.1.0", - "JSONStream": "^1.2.1", + "@apphosting/build": "^0.1.6", + "@apphosting/common": "^0.0.8", + "@electric-sql/pglite": "^0.3.3", + "@electric-sql/pglite-tools": "^0.2.8", + "@google-cloud/cloud-sql-connector": "^1.3.3", + "@google-cloud/pubsub": "^4.5.0", + "@inquirer/prompts": "^7.4.0", + "@modelcontextprotocol/sdk": "^1.10.2", "abort-controller": "^3.0.0", - "archiver": "^5.0.0", + "ajv": "^8.17.1", + "ajv-formats": "3.0.1", + "archiver": "^7.0.0", + "async-lock": "1.4.1", "body-parser": "^1.19.0", - "chokidar": "^3.0.2", + "chokidar": "^3.6.0", "cjson": "^0.3.1", - "cli-color": "^1.2.0", - "cli-table": "^0.3.1", - "commander": "^4.0.1", + "cli-table3": "0.6.5", + "colorette": "^2.0.19", + "commander": "^5.1.0", "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.6", - "exegesis-express": "^2.0.0", - "exit-code": "^1.0.2", + "cross-env": "^7.0.3", + "cross-spawn": "^7.0.5", + "csv-parse": "^5.0.4", + "deep-equal-in-any-order": "^2.0.6", + "exegesis": "^4.2.0", + "exegesis-express": "^4.0.0", "express": "^4.16.4", "filesize": "^6.1.0", - "fs-extra": "^0.23.1", - "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.1", + "fs-extra": "^10.1.0", + "fuzzy": "^0.1.3", + "gaxios": "^6.7.0", + "glob": "^10.4.1", + "google-auth-library": "^9.11.0", + "ignore": "^7.0.4", + "js-yaml": "^3.14.1", + "jsonwebtoken": "^9.0.0", "leven": "^3.1.0", - "lodash": "^4.17.19", - "marked": "^0.7.0", - "marked-terminal": "^3.3.0", + "libsodium-wrappers": "^0.7.10", + "lodash": "^4.17.21", + "lsofi": "1.0.0", + "marked": "^13.0.2", + "marked-terminal": "^7.0.0", + "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", + "pg-gateway": "^0.3.0-beta.4", + "pglite-2": "npm:@electric-sql/pglite@0.2.17", + "portfinder": "^1.0.32", "progress": "^2.0.3", - "proxy-agent": "^4.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", + "semver": "^7.5.2", + "sql-formatter": "^15.3.0", + "stream-chain": "^2.2.4", + "stream-json": "^1.7.3", + "superstatic": "^9.2.0", + "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": "^4.1.0", - "uuid": "^3.0.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.5.10", + "yaml": "^2.4.1", + "zod": "^3.24.3", + "zod-to-json-schema": "^3.24.5" }, "devDependencies": { - "@manifoldco/swagger-to-ts": "^2.0.0", + "@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.4.2", "@types/body-parser": "^1.17.0", - "@types/chai": "^4.2.12", - "@types/chai-as-promised": "^7.1.3", - "@types/cli-color": "^0.3.29", - "@types/cli-table": "^0.3.0", + "@types/chai": "^4.3.0", + "@types/chai-as-promised": "^7.1.4", + "@types/cjson": "^0.5.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/lsofi": "1.0.2", + "@types/marked-terminal": "^6.1.1", + "@types/mocha": "^9.0.0", + "@types/mock-fs": "4.13.4", "@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/uuid": "^3.4.4", - "@types/winston": "^2.4.4", + "@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": "^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.11.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-brikke": "^2.2.2", + "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": "^11.7.1", + "mock-fs": "5.2.0", + "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", + "openapi3-ts": "^3.2.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": "^3.3.0", - "swagger2openapi": "^6.0.3", - "ts-node": "^9.1.1", - "typescript": "^3.9.5" + "supertest": "^6.2.3", + "swagger2openapi": "^7.0.8", + "ts-node": "^10.4.0", + "typescript": "^4.5.4", + "typescript-json-schema": "^0.65.1", + "vite": "^4.2.1" + }, + "overrides": { + "@angular-devkit/core": { + "ajv-formats": "3.0.1", + "ajv": "^8.17.1" + }, + "node-fetch": { + "whatwg-url": "^14.0.0" + } } } diff --git a/prompts/FIREBASE.md b/prompts/FIREBASE.md new file mode 100644 index 00000000000..b7410b3c174 --- /dev/null +++ b/prompts/FIREBASE.md @@ -0,0 +1,121 @@ +# Firebase CLI Context + + +``` +project/ +├── firebase.json # Main configuration +├── .firebaserc # Project aliases +├── firestore.rules # Security rules +├── functions/ # Cloud Functions +├── public/ # Hosting files +└── firebase-debug.log # Created when CLI commands fail +``` + + +## Common Commands + + +```bash +# Initialize new features +firebase init hosting +firebase init functions +firebase init firestore + +# Deploy everything or specific services + +firebase deploy +firebase deploy --only hosting +firebase deploy --only functions:processOrder,functions:sendEmail +firebase deploy --except functions + +# Switch between projects + +firebase use staging +firebase use production +``` + + +## Local Development + + +```bash +# Start all emulators +firebase emulators:start + +# Start specific emulators +firebase emulators:start --only functions,firestore + +# Common emulator URLs +# Emulator UI: http://localhost:4000 +# Functions: http://localhost:5001 +# Firestore: http://localhost:8080 +# Hosting: http://localhost:5000 +```` + + + +## Debugging Failed Commands + + +```bash +# When any firebase command fails +cat firebase-debug.log # Contains detailed error traces + +# Common fixes for errors in debug log + +firebase login --reauth # Fix authentication errors +firebase use # Fix wrong project errors + +```` + + +## Complete Workflow Example + + +```bash +# Clone and setup a Firebase project +git clone https://github.com/example/my-app +cd my-app + +# Initialize Firebase in existing project +firebase init + +# Start local development +firebase emulators:start + +# Make changes, then deploy to staging +firebase use staging +firebase deploy + +# Deploy to production +firebase use production +firebase deploy --only hosting,firestore +```` + + + +## Service Detection in firebase.json + + +```json +{ + "hosting": { + "public": "dist", + "ignore": ["firebase.json", "**/.*", "**/node_modules/**"] + }, + "functions": { + "source": "functions", + "runtime": "nodejs20" + }, + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + }, + "emulators": { + "functions": { "port": 5001 }, + "firestore": { "port": 8080 }, + "hosting": { "port": 5000 } + } +} +``` + diff --git a/prompts/FIREBASE_FUNCTIONS.md b/prompts/FIREBASE_FUNCTIONS.md new file mode 100644 index 00000000000..478a856f510 --- /dev/null +++ b/prompts/FIREBASE_FUNCTIONS.md @@ -0,0 +1,221 @@ +# Firebase Functions Context (SDK 6.0.0+) + +Always use v2 functions for new development. Use v1 only for Analytics, basic Auth, and Test Lab triggers. + +For SDK versions before 6.0.0, add `/v2` to import paths (e.g., `firebase-functions/v2/https`). + +## Function Imports (SDK 6.0.0+) + + +```typescript +// HTTPS functions +import {onRequest, onCall} from 'firebase-functions/https'; + +// Firestore triggers +import {onDocumentCreated, onDocumentUpdated, onDocumentDeleted} from 'firebase-functions/firestore'; + +// RTDB triggers +import {onValueCreated, onValueWritten, onValueUpdated, onValueDeleted} from 'firebase-functions/database'; + +// Scheduled functions +import {onSchedule} from 'firebase-functions/scheduler'; + +// Storage triggers +import {onObjectFinalized, onObjectDeleted} from 'firebase-functions/storage'; + +// Pub/Sub triggers +import {onMessagePublished} from 'firebase-functions/pubsub'; + +// Blocking Auth triggers +import {beforeUserCreated, beforeUserSignedIn} from 'firebase-functions/identity'; + +// Test Lab triggers +import {onTestMatrixCompleted} from 'firebase-functions/testLab'; + +// Deferred initialization +import {onInit} from 'firebase-functions'; + +// Structured logging +import {logger} from 'firebase-functions'; + +// Configuration +import {defineString, defineInt, defineSecret} from 'firebase-functions/params'; +import * as params from 'firebase-functions/params'; + +// Note: For SDK versions before 6.0.0, add /v2 to import paths: +// import {onRequest} from 'firebase-functions/v2/https'; + +```` + + +## v1 Functions (Analytics & Basic Auth Only) + + +```typescript +// Use v1 ONLY for these triggers +import * as functionsV1 from 'firebase-functions/v1'; +import {logger} from 'firebase-functions'; + +// Analytics triggers (v1 only) +export const onPurchase = functionsV1.analytics.event('purchase').onLog((event) => { + logger.info('Purchase event', { + value: event.params?.value, + currency: event.params?.currency + }); +}); + +// Basic Auth triggers (v1 only) +export const onUserCreate = functionsV1.auth.user().onCreate((user) => { + logger.info('User created', { uid: user.uid, email: user.email }); + // Initialize user profile... +}); + +export const onUserDelete = functionsV1.auth.user().onDelete((user) => { + logger.info('User deleted', { uid: user.uid }); + // Cleanup user data... +}); +```` + + + +## Environment Configuration + + +```typescript +import {defineString, defineInt, defineSecret} from 'firebase-functions/params'; +import * as params from 'firebase-functions/params'; +import {onRequest} from 'firebase-functions/https'; +import {logger} from 'firebase-functions'; + +// Built-in params available automatically +const projectId = params.projectID; +const databaseUrl = params.databaseURL; +const bucket = params.storageBucket; +const gcpProject = params.gcloudProject; + +// Custom params +const apiUrl = defineString('API_URL', { + default: 'https://api.example.com' +}); + +const environment = defineString('ENVIRONMENT', { + default: 'dev' +}); + +const apiKey = defineSecret('STRIPE_KEY'); + +// Using params directly in runtime configuration +export const processPayment = onRequest({ + secrets: [apiKey], + memory: defineString('PAYMENT_MEMORY', { default: '1GiB' }), + minInstances: environment.equals('production').thenElse(5, 0), + maxInstances: environment.equals('production').thenElse(1000, 10) +}, async (req, res) => { + logger.info('Processing payment', { + project: projectId.value(), + bucket: bucket.value(), + env: environment.value() + }); + + const key = apiKey.value(); + const url = apiUrl.value(); + // Process payment... +}); + +```` + + +## Deferred Initialization + + +```typescript +import {onInit} from 'firebase-functions'; +import {onRequest} from 'firebase-functions/https'; + +let heavyClient: HeavySDK; + +onInit(async () => { + const {HeavySDK} = await import('./lib/heavy-sdk'); + heavyClient = new HeavySDK({ + // Expensive initialization... + }); +}); + +export const useHeavyClient = onRequest(async (req, res) => { + const result = await heavyClient.process(req.body); + res.json(result); +}); +```` + + + +## Structured Logging + + +```typescript +import {logger} from 'firebase-functions'; +import {onRequest} from 'firebase-functions/https'; + +interface OrderRequest { + orderId: string; + userId: string; + amount: number; +} + +export const processOrder = onRequest(async (req, res) => { + const {orderId, userId, amount} = req.body as OrderRequest; + + logger.info("Processing order", { + orderId, + userId, + amount + }); + + try { + // Process... + logger.log("Order complete", { orderId }); + res.json({ success: true }); + } catch (error) { + logger.error("Order failed", { + orderId, + error: error instanceof Error ? error.message : 'Unknown error', + stack: error instanceof Error ? error.stack : undefined + }); + res.status(500).json({ error: "Processing failed" }); + } +}); + +```` + + +## Development Commands + + +```bash +# TypeScript development +cd functions +npm install +npm run build # Compile TypeScript + +# Local development + +firebase emulators:start --only functions + +# Testing functions + +npm test # Run unit tests +npm run serve # TypeScript watch + emulators + +# Deployment + +firebase deploy --only functions +firebase deploy --only functions:api,functions:onUserCreate + +# Debugging + +firebase functions:log +firebase functions:log --only api --lines=50 + +```` + + diff --git a/schema/apptesting-yaml.json b/schema/apptesting-yaml.json new file mode 100644 index 00000000000..aa587391bfa --- /dev/null +++ b/schema/apptesting-yaml.json @@ -0,0 +1,64 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "testConfig": { + "additionalProperties": false, + "type": "object", + "properties": { + "route": { + "type": "string", + "description": "URL route appended to the test target base URL at which to start the test" + } + } + }, + "testStep": { + "additionalProperties": false, + "properties": { + "goal": { + "type": "string", + "description": "The goal of the test step" + }, + "hint": { + "type": "string", + "description": "A hint to provide extra context to accomplish the goal" + }, + "successCriteria": { + "type": "string", + "description": "Crtieria that the agent can use determine if the goal is complete" + } + } + }, + "test": { + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "A descriptive name of the test" + }, + "testConfig": { + "$ref": "#/definitions/testConfig", + "description": "Configs to apply to the specific test" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/testStep" + } + } + } + } + }, + "properties": { + "defaultConfig": { + "$ref": "#/definitions/testConfig", + "description": "Default config to apply to each test within the file" + }, + "tests": { + "type": "array", + "items": { + "$ref": "#/definitions/test" + } + } + } +} diff --git a/schema/connector-yaml.json b/schema/connector-yaml.json new file mode 100644 index 00000000000..729a747d128 --- /dev/null +++ b/schema/connector-yaml.json @@ -0,0 +1,138 @@ +{ + "$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." + } + } + }, + "dartSdk": { + "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." + } + } + }, + "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." + } + } + }, + "llmTools": { + "additionalProperties": true, + "type": "object", + "properties": { + "outputFile": { + "type": "string", + "description": "Path where the JSON LLM tool definitions file should be generated." + } + } + } + }, + "properties": { + "connectorId": { + "type": "string", + "description": "The ID of the Firebase Data Connect connector." + }, + "generate": { + "type": "object", + "additionalProperties": false, + "properties": { + "javascriptSdk": { + "oneOf": [ + { "$ref": "#/definitions/javascriptSdk" }, + { + "type": "array", + "items": { + "$ref": "#/definitions/javascriptSdk" + } + } + ], + "description": "Configuration for a generated Javascript SDK" + }, + "dartSdk": { + "oneOf": [ + { "$ref": "#/definitions/dartSdk" }, + { + "type": "array", + "items": { + "$ref": "#/definitions/dartSdk" + } + } + ], + "description": "Configuration for a generated Dart SDK" + }, + "kotlinSdk": { + "oneOf": [ + { "$ref": "#/definitions/kotlinSdk" }, + { + "type": "array", + "items": { + "$ref": "#/definitions/kotlinSdk" + } + } + ], + "description": "Configuration for a generated Kotlin SDK" + }, + "swiftSdk": { + "oneOf": [ + { "$ref": "#/definitions/swiftSdk" }, + { + "type": "array", + "items": { + "$ref": "#/definitions/swiftSdk" + } + } + ], + "description": "Configuration for a generated Swift SDK" + }, + "llmTools": { + "oneOf": [ + { "$ref": "#/definitions/llmTools" }, + { "type": "array", "items": { "$ref": "#/definitions/llmTools" } } + ] + } + } + } + } +} diff --git a/schema/dataconnect-yaml.json b/schema/dataconnect-yaml.json new file mode 100644 index 00000000000..cc68ac5c24f --- /dev/null +++ b/schema/dataconnect-yaml.json @@ -0,0 +1,81 @@ +{ + "$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" + }, + "schemaValidation": { + "type": "string", + "enum": ["COMPATIBLE", "STRICT"], + "description": "Schema validation mode for schema migrations" + } + } + } + } + }, + "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..261da16c4a9 --- /dev/null +++ b/schema/extension-yaml.json @@ -0,0 +1,445 @@ +{ + "$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|select|multiSelect|secret|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": "array", + "description": "Filters that further limit the events to listen to.", + "items": { + "$ref": "#/definitions/eventFilter" + } + }, + "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" + } + } + }, + "eventFilter": { + "type": "object", + "properties": { + "attribute": { + "type": "string", + "description": "The event attribute to filter on", + "value": "The value to filter for" + } + } + } + }, + "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 new file mode 100644 index 00000000000..2a1ef0a06d7 --- /dev/null +++ b/schema/firebase-config.json @@ -0,0 +1,1880 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "definitions": { + "ActiveRuntime": { + "enum": [ + "nodejs18", + "nodejs20", + "nodejs22", + "python310", + "python311", + "python312", + "python313" + ], + "type": "string" + }, + "DataConnectSingle": { + "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" + }, + "DatabaseSingle": { + "additionalProperties": false, + "properties": { + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + } + }, + "required": [ + "rules" + ], + "type": "object" + }, + "ExtensionsConfig": { + "additionalProperties": false, + "type": "object" + }, + "FirestoreSingle": { + "additionalProperties": false, + "properties": { + "database": { + "type": "string" + }, + "edition": { + "type": "string" + }, + "indexes": { + "type": "string" + }, + "location": { + "type": "string" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + } + }, + "type": "object" + }, + "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": [ + { + "const": "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": { + "const": "public", + "description": "Invoker to set access control on https functions.", + "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" + }, + "FunctionConfig": { + "anyOf": [ + { + "$ref": "#/definitions/LocalFunctionConfig" + }, + { + "$ref": "#/definitions/RemoteFunctionConfig" + } + ] + }, + "HostingHeaders": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "glob": { + "type": "string" + }, + "headers": { + "items": { + "additionalProperties": false, + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "key", + "value" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "glob", + "headers" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "headers": { + "items": { + "additionalProperties": false, + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "key", + "value" + ], + "type": "object" + }, + "type": "array" + }, + "source": { + "type": "string" + } + }, + "required": [ + "headers", + "source" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "headers": { + "items": { + "additionalProperties": false, + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "key", + "value" + ], + "type": "object" + }, + "type": "array" + }, + "regex": { + "type": "string" + } + }, + "required": [ + "headers", + "regex" + ], + "type": "object" + } + ] + }, + "HostingRedirects": { + "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" + } + ] + }, + "HostingRewrites": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "glob": { + "type": "string" + } + }, + "required": [ + "destination", + "glob" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "type": "string" + }, + "glob": { + "type": "string" + }, + "region": { + "type": "string" + } + }, + "required": [ + "function", + "glob" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + } + }, + "required": [ + "functionId" + ], + "type": "object" + }, + "glob": { + "type": "string" + } + }, + "required": [ + "function", + "glob" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "glob": { + "type": "string" + }, + "run": { + "additionalProperties": false, + "properties": { + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + }, + "serviceId": { + "type": "string" + } + }, + "required": [ + "serviceId" + ], + "type": "object" + } + }, + "required": [ + "glob", + "run" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "dynamicLinks": { + "type": "boolean" + }, + "glob": { + "type": "string" + } + }, + "required": [ + "dynamicLinks", + "glob" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "required": [ + "destination", + "source" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "type": "string" + }, + "region": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "required": [ + "function", + "source" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + } + }, + "required": [ + "functionId" + ], + "type": "object" + }, + "source": { + "type": "string" + } + }, + "required": [ + "function", + "source" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "run": { + "additionalProperties": false, + "properties": { + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + }, + "serviceId": { + "type": "string" + } + }, + "required": [ + "serviceId" + ], + "type": "object" + }, + "source": { + "type": "string" + } + }, + "required": [ + "run", + "source" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "dynamicLinks": { + "type": "boolean" + }, + "source": { + "type": "string" + } + }, + "required": [ + "dynamicLinks", + "source" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "destination": { + "type": "string" + }, + "regex": { + "type": "string" + } + }, + "required": [ + "destination", + "regex" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "type": "string" + }, + "regex": { + "type": "string" + }, + "region": { + "type": "string" + } + }, + "required": [ + "function", + "regex" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "function": { + "additionalProperties": false, + "properties": { + "functionId": { + "type": "string" + }, + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + } + }, + "required": [ + "functionId" + ], + "type": "object" + }, + "regex": { + "type": "string" + } + }, + "required": [ + "function", + "regex" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "regex": { + "type": "string" + }, + "run": { + "additionalProperties": false, + "properties": { + "pinTag": { + "type": "boolean" + }, + "region": { + "type": "string" + }, + "serviceId": { + "type": "string" + } + }, + "required": [ + "serviceId" + ], + "type": "object" + } + }, + "required": [ + "regex", + "run" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "dynamicLinks": { + "type": "boolean" + }, + "regex": { + "type": "string" + } + }, + "required": [ + "dynamicLinks", + "regex" + ], + "type": "object" + } + ] + }, + "HostingSingle": { + "additionalProperties": false, + "properties": { + "appAssociation": { + "enum": [ + "AUTO", + "NONE" + ], + "type": "string" + }, + "cleanUrls": { + "type": "boolean" + }, + "frameworksBackend": { + "$ref": "#/definitions/FrameworksBackendOptions" + }, + "headers": { + "items": { + "$ref": "#/definitions/HostingHeaders" + }, + "type": "array" + }, + "i18n": { + "additionalProperties": false, + "properties": { + "root": { + "type": "string" + } + }, + "required": [ + "root" + ], + "type": "object" + }, + "ignore": { + "items": { + "type": "string" + }, + "type": "array" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "public": { + "type": "string" + }, + "redirects": { + "items": { + "$ref": "#/definitions/HostingRedirects" + }, + "type": "array" + }, + "rewrites": { + "items": { + "$ref": "#/definitions/HostingRewrites" + }, + "type": "array" + }, + "site": { + "type": "string" + }, + "source": { + "type": "string" + }, + "target": { + "type": "string" + }, + "trailingSlash": { + "type": "boolean" + } + }, + "type": "object" + }, + "LocalFunctionConfig": { + "additionalProperties": false, + "properties": { + "codebase": { + "type": "string" + }, + "configDir": { + "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" + } + ] + }, + "prefix": { + "type": "string" + }, + "runtime": { + "enum": [ + "nodejs18", + "nodejs20", + "nodejs22", + "python310", + "python311", + "python312", + "python313" + ], + "type": "string" + }, + "source": { + "type": "string" + } + }, + "required": [ + "source" + ], + "type": "object" + }, + "Record": { + "additionalProperties": false, + "type": "object" + }, + "RemoteConfigConfig": { + "additionalProperties": false, + "properties": { + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "template": { + "type": "string" + } + }, + "required": [ + "template" + ], + "type": "object" + }, + "RemoteFunctionConfig": { + "additionalProperties": false, + "properties": { + "codebase": { + "type": "string" + }, + "configDir": { + "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" + } + ] + }, + "prefix": { + "type": "string" + }, + "remoteSource": { + "additionalProperties": false, + "properties": { + "dir": { + "type": "string" + }, + "ref": { + "type": "string" + }, + "repository": { + "type": "string" + } + }, + "required": [ + "ref", + "repository" + ], + "type": "object" + }, + "runtime": { + "$ref": "#/definitions/ActiveRuntime" + } + }, + "required": [ + "remoteSource", + "runtime" + ], + "type": "object" + }, + "StorageSingle": { + "additionalProperties": false, + "properties": { + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "required": [ + "rules" + ], + "type": "object" + } + }, + "properties": { + "$schema": { + "format": "uri", + "type": "string" + }, + "apphosting": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "alwaysDeployFromSource": { + "type": "boolean" + }, + "backendId": { + "type": "string" + }, + "ignore": { + "items": { + "type": "string" + }, + "type": "array" + }, + "localBuild": { + "type": "boolean" + }, + "rootDir": { + "type": "string" + } + }, + "required": [ + "backendId", + "ignore", + "rootDir" + ], + "type": "object" + }, + { + "items": { + "additionalProperties": false, + "properties": { + "alwaysDeployFromSource": { + "type": "boolean" + }, + "backendId": { + "type": "string" + }, + "ignore": { + "items": { + "type": "string" + }, + "type": "array" + }, + "localBuild": { + "type": "boolean" + }, + "rootDir": { + "type": "string" + } + }, + "required": [ + "backendId", + "ignore", + "rootDir" + ], + "type": "object" + }, + "type": "array" + } + ] + }, + "database": { + "anyOf": [ + { + "$ref": "#/definitions/DatabaseSingle" + }, + { + "items": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "instance": { + "type": "string" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "required": [ + "instance", + "rules" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "instance": { + "type": "string" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "required": [ + "rules", + "target" + ], + "type": "object" + } + ] + }, + "type": "array" + } + ] + }, + "dataconnect": { + "anyOf": [ + { + "$ref": "#/definitions/DataConnectSingle" + }, + { + "items": { + "$ref": "#/definitions/DataConnectSingle" + }, + "type": "array" + } + ] + }, + "emulators": { + "additionalProperties": false, + "properties": { + "apphosting": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + }, + "rootDirectory": { + "type": "string" + }, + "startCommand": { + "type": "string" + }, + "startCommandOverride": { + "type": "string" + } + }, + "type": "object" + }, + "auth": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "database": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "dataconnect": { + "additionalProperties": false, + "properties": { + "dataDir": { + "type": "string" + }, + "host": { + "type": "string" + }, + "port": { + "type": "number" + }, + "postgresHost": { + "type": "string" + }, + "postgresPort": { + "type": "number" + } + }, + "type": "object" + }, + "eventarc": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "extensions": { + "properties": {}, + "type": "object" + }, + "firestore": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + }, + "websocketPort": { + "type": "number" + } + }, + "type": "object" + }, + "functions": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "hosting": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "hub": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "logging": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "pubsub": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "singleProjectMode": { + "type": "boolean" + }, + "storage": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "tasks": { + "additionalProperties": false, + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + }, + "ui": { + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "host": { + "type": "string" + }, + "port": { + "type": "number" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "extensions": { + "$ref": "#/definitions/ExtensionsConfig" + }, + "firestore": { + "anyOf": [ + { + "$ref": "#/definitions/FirestoreSingle" + }, + { + "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": [ + { + "$ref": "#/definitions/LocalFunctionConfig" + }, + { + "$ref": "#/definitions/RemoteFunctionConfig" + }, + { + "items": { + "$ref": "#/definitions/FunctionConfig" + }, + "type": "array" + } + ] + }, + "hosting": { + "anyOf": [ + { + "$ref": "#/definitions/HostingSingle" + }, + { + "items": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "appAssociation": { + "enum": [ + "AUTO", + "NONE" + ], + "type": "string" + }, + "cleanUrls": { + "type": "boolean" + }, + "frameworksBackend": { + "$ref": "#/definitions/FrameworksBackendOptions" + }, + "headers": { + "items": { + "$ref": "#/definitions/HostingHeaders" + }, + "type": "array" + }, + "i18n": { + "additionalProperties": false, + "properties": { + "root": { + "type": "string" + } + }, + "required": [ + "root" + ], + "type": "object" + }, + "ignore": { + "items": { + "type": "string" + }, + "type": "array" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "public": { + "type": "string" + }, + "redirects": { + "items": { + "$ref": "#/definitions/HostingRedirects" + }, + "type": "array" + }, + "rewrites": { + "items": { + "$ref": "#/definitions/HostingRewrites" + }, + "type": "array" + }, + "site": { + "type": "string" + }, + "source": { + "type": "string" + }, + "target": { + "type": "string" + }, + "trailingSlash": { + "type": "boolean" + } + }, + "required": [ + "target" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "appAssociation": { + "enum": [ + "AUTO", + "NONE" + ], + "type": "string" + }, + "cleanUrls": { + "type": "boolean" + }, + "frameworksBackend": { + "$ref": "#/definitions/FrameworksBackendOptions" + }, + "headers": { + "items": { + "$ref": "#/definitions/HostingHeaders" + }, + "type": "array" + }, + "i18n": { + "additionalProperties": false, + "properties": { + "root": { + "type": "string" + } + }, + "required": [ + "root" + ], + "type": "object" + }, + "ignore": { + "items": { + "type": "string" + }, + "type": "array" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "public": { + "type": "string" + }, + "redirects": { + "items": { + "$ref": "#/definitions/HostingRedirects" + }, + "type": "array" + }, + "rewrites": { + "items": { + "$ref": "#/definitions/HostingRewrites" + }, + "type": "array" + }, + "site": { + "type": "string" + }, + "source": { + "type": "string" + }, + "target": { + "type": "string" + }, + "trailingSlash": { + "type": "boolean" + } + }, + "required": [ + "site" + ], + "type": "object" + } + ] + }, + "type": "array" + } + ] + }, + "remoteconfig": { + "$ref": "#/definitions/RemoteConfigConfig" + }, + "storage": { + "anyOf": [ + { + "$ref": "#/definitions/StorageSingle" + }, + { + "items": { + "additionalProperties": false, + "properties": { + "bucket": { + "type": "string" + }, + "postdeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "predeploy": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ] + }, + "rules": { + "type": "string" + }, + "target": { + "type": "string" + } + }, + "required": [ + "bucket", + "rules" + ], + "type": "object" + }, + "type": "array" + } + ] + } + }, + "type": "object" +} + diff --git a/scripts/assets/functions_to_test_minimal.js b/scripts/assets/functions_to_test_minimal.js new file mode 100644 index 00000000000..2c5eaf7fbd4 --- /dev/null +++ b/scripts/assets/functions_to_test_minimal.js @@ -0,0 +1,5 @@ +var functions = require("firebase-functions"); + +exports.httpsAction = functions.https.onRequest(function (req, res) { + res.send(req.body); +}); diff --git a/scripts/build/Dockerfile b/scripts/build/Dockerfile index 30e2fbe10cb..1ab5d0253df 100644 --- a/scripts/build/Dockerfile +++ b/scripts/build/Dockerfile @@ -1,12 +1,14 @@ -FROM node:10 +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 - -# Install npm at 6.10.2. -RUN npm install --global npm@6.10.2 + 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.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 +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..dcc9f2a5fa3 --- /dev/null +++ b/scripts/clean-install.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -ex + +function cleanup() { + echo "Cleaning up artifacts..." + rm -rf ./clean + echo "Artifacts deleted." +} + +trap cleanup EXIT + +rm -rf ./clean || true +echo "Running clean-publish@5.0.0 --without-publish, as we would before publishing to npm..." +npx --yes clean-publish@5.0.0 --without-publish --before-script ./scripts/clean-shrinkwrap.sh --temp-dir clean +echo "Ran clean-publish@5.0.0 --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 5e328cb6fa6..8630d1cc93b 100644 --- a/scripts/client-integration-tests/tests.ts +++ b/scripts/client-integration-tests/tests.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { join } from "path"; import { readFileSync, writeFileSync, unlinkSync } from "fs"; -import * as uuid from "uuid"; +import { v4 as uuidv4 } from "uuid"; import * as tmp from "tmp"; import firebase = require("../../src"); @@ -93,12 +93,12 @@ describe("apps:sdkconfig", () => { describe("database:set|get|remove", () => { it("should be able to interact with the database", async () => { const opts = { project: process.env.FBTOOLS_TARGET_PROJECT }; - const path = `/${uuid()}`; + const path = `/${uuidv4()}`; const data = { foo: "bar" }; await client.database.set( path, - Object.assign({ data: JSON.stringify(data), confirm: true }, opts) + Object.assign({ data: JSON.stringify(data), force: true }, opts), ); // Have to read to a file in order to get data. @@ -107,7 +107,7 @@ describe("database:set|get|remove", () => { await client.database.get(path, Object.assign({ output: file.name }, opts)); expect(JSON.parse(readFileSync(file.name).toString())).to.deep.equal(data); - await client.database.remove(path, Object.assign({ confirm: true }, opts)); + await client.database.remove(path, Object.assign({ force: true }, opts)); await client.database.get(path, Object.assign({ output: file.name }, opts)); expect(JSON.parse(readFileSync(file.name, "utf-8"))).to.equal(null); diff --git a/scripts/dataconnect-emulator-tests/fdc-test/connector/connector.yaml b/scripts/dataconnect-emulator-tests/fdc-test/connector/connector.yaml new file mode 100644 index 00000000000..68215053ca4 --- /dev/null +++ b/scripts/dataconnect-emulator-tests/fdc-test/connector/connector.yaml @@ -0,0 +1,2 @@ +connectorId: "connectorId" +authMode: "PUBLIC" diff --git a/scripts/dataconnect-emulator-tests/fdc-test/connector/queries.gql b/scripts/dataconnect-emulator-tests/fdc-test/connector/queries.gql new file mode 100644 index 00000000000..51e05570ca5 --- /dev/null +++ b/scripts/dataconnect-emulator-tests/fdc-test/connector/queries.gql @@ -0,0 +1,3 @@ +mutation createOrder($name: String!) { + order_insert(data : {name: $name}) +} diff --git a/scripts/dataconnect-emulator-tests/fdc-test/dataconnect.yaml b/scripts/dataconnect-emulator-tests/fdc-test/dataconnect.yaml new file mode 100644 index 00000000000..6a2d2fe1c2a --- /dev/null +++ b/scripts/dataconnect-emulator-tests/fdc-test/dataconnect.yaml @@ -0,0 +1,11 @@ +specVersion: "v1beta" +serviceId: "fake-service" +location: "us-central1" +schema: + source: "./schema" + datasource: + postgresql: + database: "postgres" + cloudSql: + instanceId: "dataconnect-test" +connectorDirs: ["./connector"] diff --git a/scripts/dataconnect-emulator-tests/fdc-test/schema/schema.gql b/scripts/dataconnect-emulator-tests/fdc-test/schema/schema.gql new file mode 100644 index 00000000000..b6ea799498c --- /dev/null +++ b/scripts/dataconnect-emulator-tests/fdc-test/schema/schema.gql @@ -0,0 +1,3 @@ +type Order @table { + name: String! +} diff --git a/scripts/dataconnect-emulator-tests/firebase.json b/scripts/dataconnect-emulator-tests/firebase.json new file mode 100644 index 00000000000..bb70b69cbe2 --- /dev/null +++ b/scripts/dataconnect-emulator-tests/firebase.json @@ -0,0 +1,5 @@ +{ + "dataconnect": { + "source": "fdc-test" + } +} diff --git a/scripts/dataconnect-emulator-tests/run.sh b/scripts/dataconnect-emulator-tests/run.sh new file mode 100644 index 00000000000..fcb35afa786 --- /dev/null +++ b/scripts/dataconnect-emulator-tests/run.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -ex # Immediately exit on failure +# Globally link the CLI for the testing framework +./scripts/clean-install.sh +source scripts/set-default-credentials.sh + +echo "Running in ${CWD}" +echo "Running with node: $(which node)" +echo "Running with npm: $(which npm)" +echo "Running with Application Creds: ${GOOGLE_APPLICATION_CREDENTIALS}" + +cd scripts/dataconnect-emulator-tests +firebase emulators:exec "cd ." --only dataconnect -P demo-test +# rm -rf ../../clean 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/cases.ts b/scripts/dataconnect-test/cases.ts new file mode 100644 index 00000000000..da731d21902 --- /dev/null +++ b/scripts/dataconnect-test/cases.ts @@ -0,0 +1,76 @@ +export interface TestCase { + description: string; + sequence: Step[]; +} + +export interface Step { + schemaGQL: string; + connectorGQL: string; + expectErr: boolean; +} + +export const cases: TestCase[] = [ + { + description: "Schema migration: adding a field", + sequence: [ + { + schemaGQL: `type Order @table { + name: String! + }`, + connectorGQL: `mutation createOrder($name: String!) { + order_insert(data : {name: $name}) + }`, + expectErr: false, + }, + { + schemaGQL: `type Order @table { + name: String! + price: Int! + }`, + connectorGQL: `mutation createOrder($name: String!) { + order_insert(data : {name: $name, price: 1}) + }`, + expectErr: false, + }, + ], + }, + { + description: "Schema migration: removing a field", + sequence: [ + { + schemaGQL: `type Order @table { + name: String! + price: Int! + }`, + connectorGQL: `mutation createOrder($name: String!) { + order_insert(data : {name: $name, price: 1}) + }`, + expectErr: false, + }, + { + schemaGQL: `type Order @table { + name: String! + }`, + connectorGQL: `mutation createOrder($name: String!) { + order_insert(data : {name: $name}) + }`, + expectErr: false, + }, + ], + }, + { + description: "Vector embeddings", + sequence: [ + { + schemaGQL: `type Order @table { + name: String! + v: Vector! @col(size:768) + }`, + connectorGQL: `mutation createOrder($name: String!) { + order_insert(data : {name: $name, v_embed: {model: "textembedding-gecko@001", text: $name}}) + }`, + expectErr: false, + }, + ], + }, +]; diff --git a/scripts/dataconnect-test/firebase.json b/scripts/dataconnect-test/firebase.json new file mode 100644 index 00000000000..bb70b69cbe2 --- /dev/null +++ b/scripts/dataconnect-test/firebase.json @@ -0,0 +1,5 @@ +{ + "dataconnect": { + "source": "fdc-test" + } +} diff --git a/scripts/dataconnect-test/run.sh b/scripts/dataconnect-test/run.sh new file mode 100644 index 00000000000..86227385363 --- /dev/null +++ b/scripts/dataconnect-test/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 + +echo "Running in ${CWD}" +echo "Running with node: $(which node)" +echo "Running with npm: $(which npm)" +echo "Running with Application Creds: ${GOOGLE_APPLICATION_CREDENTIALS}" + +mocha scripts/dataconnect-test/tests.ts +rm -rf ../../clean \ No newline at end of file diff --git a/scripts/dataconnect-test/templates/connector.yaml b/scripts/dataconnect-test/templates/connector.yaml new file mode 100644 index 00000000000..68215053ca4 --- /dev/null +++ b/scripts/dataconnect-test/templates/connector.yaml @@ -0,0 +1,2 @@ +connectorId: "connectorId" +authMode: "PUBLIC" diff --git a/scripts/dataconnect-test/templates/dataconnect.yaml b/scripts/dataconnect-test/templates/dataconnect.yaml new file mode 100644 index 00000000000..10abc3d509c --- /dev/null +++ b/scripts/dataconnect-test/templates/dataconnect.yaml @@ -0,0 +1,11 @@ +specVersion: "v1beta" +serviceId: "__serviceId__" +location: "us-central1" +schema: + source: "./schema" + datasource: + postgresql: + database: "__databaseId__" + cloudSql: + instanceId: "dataconnect-test" +connectorDirs: ["./connector"] diff --git a/scripts/dataconnect-test/tests.ts b/scripts/dataconnect-test/tests.ts new file mode 100644 index 00000000000..c50e58feab6 --- /dev/null +++ b/scripts/dataconnect-test/tests.ts @@ -0,0 +1,180 @@ +import * as fs from "fs"; +import * as path from "path"; +import { expect } from "chai"; + +import * as cli from "../functions-deploy-tests/cli"; +import { cases, Step } from "./cases"; +import * as client from "../../src/dataconnect/client"; +import { deleteDatabase } from "../../src/gcp/cloudsql/cloudsqladmin"; +import { requireAuth } from "../../src/requireAuth"; + +const FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT || ""; +const FIREBASE_DEBUG = process.env.FIREBASE_DEBUG || ""; + +function expected( + serviceId: string, + databaseId: string, + schemaUpdateTime: string, + connectorLastUpdated: string, +) { + return { + serviceId, + location: "us-central1", + datasource: `CloudSQL Instance: dataconnect-test\nDatabase: ${databaseId}`, + schemaUpdateTime, + connectors: [ + { + connectorId: "connectorId", + connectorLastUpdated, + }, + ], + }; +} + +async function cleanUpService(projectId: string, serviceId: string, databaseId: string) { + await client.deleteService(`projects/${projectId}/locations/us-central1/services/${serviceId}`); + await deleteDatabase(projectId, "dataconnect-test", databaseId); +} + +async function list() { + return await cli.exec( + "dataconnect:services:list", + FIREBASE_PROJECT, + ["--json"], + __dirname, + /** quiet=*/ false, + { + FIREBASE_CLI_EXPERIMENTS: "dataconnect", + }, + ); +} + +async function migrate(force: boolean) { + const args = force ? ["--force"] : []; + if (FIREBASE_DEBUG) { + args.push("--debug"); + } + return await cli.exec( + "dataconnect:sql:migrate", + FIREBASE_PROJECT, + args, + __dirname, + /** quiet=*/ false, + { + FIREBASE_CLI_EXPERIMENTS: "dataconnect", + }, + ); +} + +async function deploy(force: boolean) { + const args = ["--only", "dataconnect"]; + if (force) { + args.push("--force"); + } + if (FIREBASE_DEBUG) { + args.push("--debug"); + } + return await cli.exec("deploy", FIREBASE_PROJECT, args, __dirname, /** quiet=*/ false, { + FIREBASE_CLI_EXPERIMENTS: "dataconnect", + }); +} + +function toPath(p: string) { + return path.join(__dirname, p); +} + +function getRandomString(length: number): string { + const SUFFIX_CHAR_SET = "abcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + for (let i = 0; i < length; i++) { + result += SUFFIX_CHAR_SET.charAt(Math.floor(Math.random() * SUFFIX_CHAR_SET.length)); + } + return result; +} +const fdcTest = toPath("fdc-test"); + +// Each test run should use a random serviceId and databaseId. +function newTestRun(): { serviceId: string; databaseId: string } { + const id = getRandomString(6); + const serviceId = `cli-e2e-service-${id}`; + const databaseId = `cli-e2e-database-${id}`; + + const dataconnectYamlTemplate = fs.readFileSync(toPath("templates/dataconnect.yaml")).toString(); + const connectorYamlTemplate = fs.readFileSync(toPath("templates/connector.yaml")).toString(); + const subbedDataconnectYaml = dataconnectYamlTemplate + .replace("__serviceId__", serviceId) + .replace("__databaseId__", databaseId); + if (!fs.existsSync(fdcTest)) { + fs.mkdirSync(fdcTest); + } + if (!fs.existsSync(toPath("fdc-test/connector"))) { + fs.mkdirSync(toPath("fdc-test/connector")); + } + if (!fs.existsSync(toPath("fdc-test/schema"))) { + fs.mkdirSync(toPath("fdc-test/schema")); + } + fs.writeFileSync(toPath("fdc-test/dataconnect.yaml"), subbedDataconnectYaml, { + mode: 420 /* 0o644 */, + }); + fs.writeFileSync(toPath("fdc-test/connector/connector.yaml"), connectorYamlTemplate, { + mode: 420 /* 0o644 */, + }); + return { serviceId, databaseId }; +} + +function prepareStep(step: Step) { + fs.writeFileSync(toPath("fdc-test/schema/schema.gql"), step.schemaGQL, { mode: 420 /* 0o644 */ }); + fs.writeFileSync(toPath("fdc-test/connector/connector.gql"), step.connectorGQL, { + mode: 420 /* 0o644 */, + }); +} + +describe("firebase deploy", () => { + let serviceId: string; + let databaseId: string; + + beforeEach(async function (this) { + this.timeout(10000); + expect(FIREBASE_PROJECT).not.to.equal("", "No FBTOOLS_TARGET_PROJECT env var set."); + const info = newTestRun(); + serviceId = info.serviceId; + databaseId = info.databaseId; + await requireAuth({}); + }); + + afterEach(async function (this) { + this.timeout(10000); + fs.rmSync(fdcTest, { recursive: true, force: true }); + await cleanUpService(FIREBASE_PROJECT, serviceId, databaseId); + }); + + for (const c of cases) { + it(c.description, async () => { + for (const step of c.sequence) { + prepareStep(step); + try { + await deploy(false); + await migrate(true); + await deploy(true); + } catch (err: any) { + expect(err.expectErr, `Unexpected error: ${err.message}`).to.be.true; + } + expect(step.expectErr).to.be.false; + const result = await list(); + const out = JSON.parse(result.stdout); + expect(out?.status).to.equal("success"); + expect(out?.result?.services?.length).to.gte(1); + const service = out.result.services.find((s: any) => s.serviceId === serviceId); + // Don't need to check update times. + expect(service).to.deep.equal( + expected( + serviceId, + databaseId, + service["schemaUpdateTime"], + service["connectors"]?.[0]?.["connectorLastUpdated"], + ), + ); + } + }).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..55c6f8b86bf --- /dev/null +++ b/scripts/emulator-import-export-tests/tests.ts @@ -0,0 +1,579 @@ +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!"); + + await importCLI.stop(); + }); +}); 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 39ca14ae3fb..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,21 +30,8 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = }, }, }, - triggerId: "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: { @@ -80,21 +56,8 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = }, }, }, - triggerId: "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: { @@ -119,21 +82,8 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = }, }, }, - triggerId: "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: { @@ -170,23 +120,9 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = timestamp: "2019-05-15T16:21:15.148831Z", }, }, - triggerId: "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: "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..4ef87b4050e --- /dev/null +++ b/scripts/emulator-tests/functions/package-lock.json @@ -0,0 +1,5060 @@ +{ + "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": "^5.1.0" + }, + "engines": { + "node": "20" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "optional": true, + "dependencies": { + "@babel/types": "^7.25.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "optional": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.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.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/firestore/node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "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==", + "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/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.22", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.22.tgz", + "integrity": "sha512-oAjDdN7fzbUi+4hZjKG96MR6KTEubAeMpQEb+77qy+3r0Ua5xTFuie6JOLr4ZZgl5g+W5/uRTS2M1V8mVAFPuA==", + "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.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "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/@jsdoc/salty": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", + "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", + "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/@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": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "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": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "optional": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "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.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/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==", + "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.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.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/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.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "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.1", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", + "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==", + "optional": true, + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "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.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==", + "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/fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "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/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.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": "5.1.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.1.1.tgz", + "integrity": "sha512-KkyKZE98Leg/C73oRyuUYox04PQeeBThdygMfeX+7t1cmKWYKa/ZieYa89U8GHgED+0mF7m7wfNZOfbURYxIKg==", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^11.10.0 || ^12.0.0" + } + }, + "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.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/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==", + "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": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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-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==", + "deprecated": "Package is no longer maintained", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "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.3", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.3.tgz", + "integrity": "sha512-Nu7Sf35kXJ1MWDZIMAuATRQTg1iIPdzh7tqJ6jjvaU/GfDf+qi5UV8zJR3Mo+/pYFvm8mzay4+6O5EWigaQBQw==", + "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/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": "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==", + "optional": true, + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "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": "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.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": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "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==", + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "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.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": 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/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-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.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.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/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/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "optional": true + }, + "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==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "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.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==", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/retry-request/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true + }, + "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.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "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/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==", + "optional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/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.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "optional": true, + "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==", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "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==", + "optional": true + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "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": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "optional": true + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "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.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/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==", + "optional": true + }, + "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==", + "optional": true, + "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/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "optional": true + }, + "@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "optional": true + }, + "@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "optional": true, + "requires": { + "@babel/types": "^7.25.6" + } + }, + "@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "optional": true, + "requires": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + } + }, + "@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.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "requires": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "@firebase/database": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz", + "integrity": "sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==", + "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" + } + }, + "@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==", + "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" + } + }, + "@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==", + "requires": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "@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.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@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, + "requires": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.7", + "protobufjs": "^7.2.5" + }, + "dependencies": { + "protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "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==", + "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.12.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.12.0.tgz", + "integrity": "sha512-78nNAY7iiZ4O/BouWMWTD/oSF2YtYgYB3GZirn0To6eBOugjXVoK+GXgUXOl+HlqbAOyHxAVXOlsj3snfbQ1dw==", + "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", + "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" + }, + "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.22", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.22.tgz", + "integrity": "sha512-oAjDdN7fzbUi+4hZjKG96MR6KTEubAeMpQEb+77qy+3r0Ua5xTFuie6JOLr4ZZgl5g+W5/uRTS2M1V8mVAFPuA==", + "optional": true, + "requires": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + } + }, + "@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==", + "optional": true, + "requires": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "dependencies": { + "protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "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" + } + } + } + }, + "@jsdoc/salty": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", + "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", + "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==" + }, + "@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.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": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "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": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "optional": true, + "requires": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "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.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "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" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.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" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", + "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==", + "optional": true, + "requires": { + "punycode": "^1.4.1" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "optional": true + }, + "escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "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.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 + }, + "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, + "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 + }, + "fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "optional": true, + "requires": { + "strnum": "^1.0.5" + } + }, + "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.11.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.11.1.tgz", + "integrity": "sha512-UyEbq+3u6jWzCYbUntv/HuJiTixwh36G1R9j0v71mSvGAx/YZEWEW7uSGLYxBYE6ckVRQoKMr40PYUEzrm/4dg==", + "requires": { + "@fastify/busboy": "^1.2.1", + "@firebase/database-compat": "^0.3.4", + "@firebase/database-types": "^0.10.4", + "@google-cloud/firestore": "^6.8.0", + "@google-cloud/storage": "^6.9.5", + "@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": "5.1.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.1.1.tgz", + "integrity": "sha512-KkyKZE98Leg/C73oRyuUYox04PQeeBThdygMfeX+7t1cmKWYKa/ZieYa89U8GHgED+0mF7m7wfNZOfbURYxIKg==", + "requires": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "protobufjs": "^7.2.2" + } + }, + "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.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "optional": true, + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + } + }, + "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, + "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.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", + "integrity": "sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==", + "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.3.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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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.3", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.3.tgz", + "integrity": "sha512-Nu7Sf35kXJ1MWDZIMAuATRQTg1iIPdzh7tqJ6jjvaU/GfDf+qi5UV8zJR3Mo+/pYFvm8mzay4+6O5EWigaQBQw==", + "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" + } + }, + "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": "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==", + "optional": true, + "requires": { + "uc.micro": "^2.0.0" + } + }, + "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": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "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": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "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" + } + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "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.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "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-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.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, + "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" + } + }, + "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==" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "optional": true + }, + "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==", + "optional": true + }, + "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.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true + } + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "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 + }, + "strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "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.3", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.3.tgz", + "integrity": "sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww==", + "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.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "optional": true + }, + "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==", + "optional": true + }, + "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==", + "optional": true + }, + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "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": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "optional": true + }, + "uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "optional": true + }, + "underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "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.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + }, + "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==", + "optional": true + }, + "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==", + "optional": true, + "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..c27dd4a0099 --- /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": "^5.1.0" + }, + "engines": { + "node": "20" + } +} diff --git a/scripts/emulator-tests/functionsEmulator.spec.ts b/scripts/emulator-tests/functionsEmulator.spec.ts index 4e10165a5c5..df5cb1ef92f 100644 --- a/scripts/emulator-tests/functionsEmulator.spec.ts +++ b/scripts/emulator-tests/functionsEmulator.spec.ts @@ -1,526 +1,1169 @@ +import * as fs from "fs"; +import * as fsp from "fs/promises"; +import * as os from "os"; +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 { EmulatedTriggerType } from "../../src/emulator/functionsEmulatorShared"; -import { FunctionsEmulator, InvokeRuntimeOpts } from "../../src/emulator/functionsEmulator"; -import { RuntimeWorker } from "../../src/emulator/functionsRuntimeWorker"; -import { TIMEOUT_LONG, MODULE_ROOT } from "./fixtures"; -import { logger } from "../../src/logger"; import * as winston from "winston"; import * as logform from "logform"; -if ((process.env.DEBUG || "").toLowerCase().indexOf("spec") >= 0) { +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 secretManager from "../../src/gcp/secretManager"; + +if ((process.env.DEBUG || "").toLowerCase().includes("spec")) { const dropLogLevels = (info: logform.TransformableInfo) => info.message; logger.add( new winston.transports.Console({ 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([ - { - name: "function_id", - entryPoint: "function_id", - httpsTrigger: {}, - labels: {}, - }, - { - name: "callable_function_id", - entryPoint: "callable_function_id", - httpsTrigger: {}, - labels: { - "deployment-callable": "true", - }, - }, - { - name: "nested-function_id", - 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, - triggerType: EmulatedTriggerType, - proto?: any, - runtimeOpts?: InvokeRuntimeOpts - ): RuntimeWorker => { - return startFunctionRuntime(triggerId, 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, dir?: string) { + const envFiles: string[] = []; + const envDir = dir || FUNCTIONS_DIR; + for (const [filename, data] of Object.entries(envs)) { + const envPath = path.join(envDir, filename); + await fsp.writeFile(path.join(envDir, 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/: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 }); - } - ), - }; - }); +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); + }; +} - 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/: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 }); - } - ), - }; +interface UseFunctionOptions { + regions?: string[]; + backend?: EmulatableBackend; + triggerOverrides?: Partial; +} + +async function useFunction( + emu: FunctionsEmulator, + triggerName: string, + triggerSource: () => {}, + options: UseFunctionOptions = {}, +): Promise { + const { regions = ["us-central1"], backend = TEST_BACKEND, triggerOverrides } = options; + + 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, 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 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 }); - } - ), - }; +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/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( + 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 }); - } + }, ), + }; + }); + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/functionId`) + .expect(200) + .then((res) => { + expect(res.body.path).to.deep.equal("/"); + }); + }); + + 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 }); + }), + }; }, - }; + { regions: ["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/nested-function_id") - .expect(200) - .then((res) => { - expect(res.body.path).to.deep.equal("/"); - }); - }).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 }); - } - ), - }; + 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 }); + }), + }; + }, + { regions: ["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_id/a/b") - .expect(200) - .then((res) => { - expect(res.body.path).to.deep.equal("/a/b"); - }); - }).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 }); - } - ), - }; + 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("/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 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 }); + }, + ), + }; + }); + + 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/sub/route/a") - .expect(200) - .then((res) => { - expect(res.body.path).to.eq("/sub/route/a"); - }); - }).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, - }); - } - ), - }; + 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 }); + }, + ), + }, + }; + }); + + 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("/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("/"); - }); - }).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, - }); - } - ), - }; + 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?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" }); - }); - }).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, - }); - } - ), - }; + 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 }); + }, + ), + }; + }); + + await supertest(emu.createHubServer()).get("/foo/bar/baz").expect(404); }); - 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"); - }); - }).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, - }); - }), - }; + 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 }); + }, + ), + }; + }); + + 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/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("/"); - }); - }).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); - } - ), - }; + 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, + }); + }, + ), + }; + }); + + 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()) - .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" }); - }); - }).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); - } - ), - }; + 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, + }); + }, + ), + }; + }); + + 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/us-central1/function_id/sub/route/a?hello=world") - .expect(200) - .then((res) => { - expect(res.body).to.deep.equal({ hello: "world" }); + 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 override 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/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 { - auth: ctx.auth, + 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, + }); + }), }; - }), - }; + }, + { regions: ["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" }); + }); }); - // 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 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); + }, + ), + }; + }); + + 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" }); + }); + }); + + 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); + }, + ), + }; + }); + + 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" }); + }); + }); + + 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 { + 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, + }); + }, + ), + }; + }); + + 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 { - auth: ctx.auth, + 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( + `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 { + 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); + }); + + it("should support multiple codebases with the same source and apply prefixes", async () => { + const backend1: EmulatableBackend = { + ...TEST_BACKEND, + codebase: "one", + prefix: "prefix-one", + }; + const backend2: EmulatableBackend = { + ...TEST_BACKEND, + codebase: "two", + prefix: "prefix-two", }; + + const prefixEmu = new FunctionsEmulator({ + projectId: TEST_PROJECT_ID, + projectDir: MODULE_ROOT, + emulatableBackends: [backend1, backend2], + verbosity: "QUIET", + debugPort: false, + }); + + await writeSource(() => { + return { + functionId: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ path: req.path }); + }, + ), + }; + }); + + try { + await registry.EmulatorRegistry.start(prefixEmu); + await prefixEmu.connect(); + + await supertest(prefixEmu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/prefix-one-functionId`) + .expect(200); + + await supertest(prefixEmu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/prefix-two-functionId`) + .expect(200); + } finally { + await registry.EmulatorRegistry.stop(Emulators.FUNCTIONS); + } }); - // 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", + 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, + }); }, - }, - }, + ), + }; }); + + 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" }); + }); }); - }).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) => { + 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 { - header: ctx.rawRequest.headers["authorization"], + dotenv: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ + FOO: process.env.FOO, + }); + }, + ), }; - }), - }; + }); + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/dotenv`) + .expect(200) + .then((res) => { + expect(res.body).to.deep.equal({ FOO: "goo" }); + }); + }); + + 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", + }); + + await useFunction(emu, "dotenv", () => { + return { + dotenv: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ + FOO: process.env.FOO, + }); + }, + ), + }; + }); + + await supertest(emu.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/dotenv`) + .expect(200) + .then((res) => { + expect(res.body).to.deep.equal({ FOO: "hoo" }); + }); + }); + + context("when configDir is provided", () => { + let emuWithConfigDir: FunctionsEmulator; + let configDir: string; + let cleanupEnvFiles: () => Promise; + + before(async () => { + configDir = fs.mkdtempSync(path.join(os.tmpdir(), "configdir-")); + cleanupEnvFiles = await setupEnvFiles({ ".env": "FOO=foo\nBAR=bar" }, configDir); + + const backend: EmulatableBackend = { + ...TEST_BACKEND, + configDir: configDir, + }; + + emuWithConfigDir = new FunctionsEmulator({ + projectId: TEST_PROJECT_ID, + projectDir: MODULE_ROOT, + emulatableBackends: [backend], + verbosity: "QUIET", + debugPort: false, + }); + + await useFunction( + emuWithConfigDir, + "dotenv", + () => { + return { + dotenv: require("firebase-functions").https.onRequest( + (req: express.Request, res: express.Response) => { + res.json({ + FOO: process.env.FOO, + BAR: process.env.BAR, + }); + }, + ), + }; + }, + { backend }, + ); + }); + + after(async () => { + await emuWithConfigDir.stop(); + await cleanupEnvFiles(); + }); + + it("should load environment variables from that directory", async () => { + await supertest(emuWithConfigDir.createHubServer()) + .get(`/${TEST_PROJECT_ID}/us-central1/dotenv`) + .expect(200) + .then((res) => { + expect(res.body).to.deep.equal({ FOO: "foo", BAR: "bar" }); + }); + }); + }); }); - 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("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"); + }); + + afterEach(() => { + readFileSyncStub.restore(); + accessSecretVersionStub.restore(); + }); + + 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 }); + }, + ), + }; }, - }); + { + triggerOverrides: { + 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("local"); + }); + }); + + 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 }); + }, + ), + }; + }, + { + triggerOverrides: { + 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"); + }); }); - }).timeout(TIMEOUT_LONG); + }); + }); + + 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("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", + }); + + 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 { + 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); + }); + }), + }; + }, + { + triggerOverrides: { + timeoutSeconds: 1, + }, + }, + ); + + 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 7d08eac8400..1ca2ad0535b 100644 --- a/scripts/emulator-tests/functionsEmulatorRuntime.spec.ts +++ b/scripts/emulator-tests/functionsEmulatorRuntime.spec.ts @@ -1,124 +1,224 @@ -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 express from "express"; -import { EmulatorLog } from "../../src/emulator/types"; -import { FunctionRuntimeBundles, TIMEOUT_LONG, TIMEOUT_MED, MODULE_ROOT } from "./fixtures"; -import { FunctionsRuntimeBundle } from "../../src/emulator/functionsEmulatorShared"; -import { InvokeRuntimeOpts, FunctionsEmulator } from "../../src/emulator/functionsEmulator"; -import { RuntimeWorker } from "../../src/emulator/functionsRuntimeWorker"; +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 { 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"; -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.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 invokeRuntimeWithFunctions( - frb: FunctionsRuntimeBundle, - triggers: () => {}, - opts?: InvokeRuntimeOpts -): RuntimeWorker { - const serializedTriggers = triggers.toString(); +async function startRuntime( + triggerName: string, + signatureType: SignatureType, + 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); - opts = opts || { nodeBinary: process.execPath }; - opts.ignore_warnings = true; - opts.serializedTriggers = serializedTriggers; + 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"], + }); - return functionsEmulator.invokeRuntime(frb, opts); + 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(...)", () => { +async function sendDebugBundle(runtime: Runtime, debug: FunctionsRuntimeBundle["debug"]) { + return new Promise((resolve) => { + runtime.proc.send(JSON.stringify(debug), resolve); + }); +} + +describe("FunctionsEmulator-Runtime", function () { + // eslint-disable-next-line @typescript-eslint/no-invalid-this + this.timeout(TIMEOUT_LONG); + + let runtime: Runtime | undefined; + + afterEach(() => { + runtime?.proc.kill(9); + runtime = undefined; + }); + + describe("Stubs, Mocks, and Helpers", () => { + describe("_InitializeNetworkFiltering", () => { it("should log outgoing unknown HTTP requests via 'http'", async () => { - const worker = invokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => { + runtime = await startRuntime("functionId", "event", () => { require("firebase-admin").initializeApp(); return { - function_id: require("firebase-functions") + functionId: 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); }); }), }; }); - - const logs = await countLogEntries(worker); - expect(logs["unidentified-network-access"]).to.gte(1); - }).timeout(TIMEOUT_LONG); + await sendEvent(runtime, FunctionRuntimeBundles.onCreate.proto); + expect(runtime.sysMsg["unidentified-network-access"]?.length).to.gte(1); + }); it("should log outgoing unknown HTTP requests via 'https'", async () => { - const worker = invokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => { + runtime = await startRuntime("functionId", "event", () => { require("firebase-admin").initializeApp(); return { - function_id: require("firebase-functions") + functionId: require("firebase-functions") .firestore.document("test/test") .onCreate(async () => { await new Promise((resolve) => { @@ -127,17 +227,15 @@ describe("FunctionsEmulator-Runtime", () => { }), }; }); - - const logs = await countLogEntries(worker); - - expect(logs["unidentified-network-access"]).to.gte(1); - }).timeout(TIMEOUT_LONG); + await sendEvent(runtime, FunctionRuntimeBundles.onCreate.proto); + expect(runtime.sysMsg["unidentified-network-access"]?.length).to.gte(1); + }); it("should log outgoing Google API requests", async () => { - const worker = invokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => { + runtime = await startRuntime("functionId", "event", () => { require("firebase-admin").initializeApp(); return { - function_id: require("firebase-functions") + functionId: require("firebase-functions") .firestore.document("test/test") .onCreate(async () => { await new Promise((resolve) => { @@ -146,511 +244,309 @@ describe("FunctionsEmulator-Runtime", () => { }), }; }); - - const logs = await countLogEntries(worker); - - expect(logs["googleapis-network-access"]).to.gte(1); - }).timeout(TIMEOUT_LONG); + 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 = invokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => { + runtime = await startRuntime("functionId", "event", () => { require("firebase-admin").initializeApp(); return { - function_id: require("firebase-functions") + functionId: require("firebase-functions") .firestore.document("test/test") - .onCreate(DO_NOTHING), + .onCreate(() => { + console.log("hello world"); + }), }; }); - - const logs = await countLogEntries(worker); - expect(logs["default-admin-app-used"]).to.eq(1); - }).timeout(TIMEOUT_MED); + 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 = invokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => { - require("firebase-admin").initializeApp({ - custom: true, - }); + runtime = await startRuntime("functionId", "event", () => { + require("firebase-admin").initializeApp({ custom: true }); return { - function_id: require("firebase-functions") + functionId: require("firebase-functions") .firestore.document("test/test") - .onCreate(DO_NOTHING), + .onCreate(() => { + console.log("hello world"); + }), }; }); - - 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 } }); - }); - - 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 = invokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => { + runtime = await startRuntime("functionId", "event", () => { require("firebase-admin").initializeApp(); // We still need to initialize default for snapshots require("firebase-admin").initializeApp({}, "non-default"); return { - function_id: require("firebase-functions") + functionId: require("firebase-functions") .firestore.document("test/test") - .onCreate(DO_NOTHING), + .onCreate(() => { + console.log("hello world"); + }), }; }); - const logs = await countLogEntries(worker); - expect(logs["non-default-admin-app-used"]).to.eq(1); - }).timeout(TIMEOUT_MED); + await sendEvent(runtime, FunctionRuntimeBundles.onCreate.proto); + expect(runtime.sysMsg["non-default-admin-app-used"]?.length).to.gte(1); + }); it("should route all sub-fields accordingly", async () => { - const worker = invokeRuntimeWithFunctions(FunctionRuntimeBundles.onCreate, () => { + runtime = await startRuntime("functionId", "event", () => { require("firebase-admin").initializeApp(); return { - function_id: require("firebase-functions") + functionId: require("firebase-functions") .firestore.document("test/test") .onCreate(() => { console.log( - JSON.stringify(require("firebase-admin").firestore.FieldValue.increment(4)) + JSON.stringify(require("firebase-admin/firestore").FieldValue.increment(4)), ); return Promise.resolve(); }), }; }); - - worker.runtime.events.on("log", (el: EmulatorLog) => { - if (el.level !== "USER") { - return; - } - - expect(JSON.parse(el.text)).to.deep.eq({ operand: 4 }); - }); - - const logs = await countLogEntries(worker); - expect(logs["function-log"]).to.eq(1); - }).timeout(TIMEOUT_MED); + await sendEvent(runtime, FunctionRuntimeBundles.onCreate.proto); + expect(runtime.stdout.join(" ")).to.match(/{"operand":4}/); + }); it("should expose Firestore prod when the emulator is not running", async () => { - const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest); - frb.emulators = {}; - - const worker = invokeRuntimeWithFunctions(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(); }), }; }); - - 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.be.undefined; expect(info.port).to.be.undefined; - }).timeout(TIMEOUT_MED); - - it("should set FIRESTORE_EMULATOR_HOST when the emulator is running", async () => { - const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest); - frb.emulators = { - firestore: { - host: "localhost", - port: 9090, - }, - }; - - const worker = invokeRuntimeWithFunctions(frb, () => { - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - res.json({ - var: process.env.FIRESTORE_EMULATOR_HOST, - }); - return Promise.resolve(); - }), - }; - }); - - const data = await callHTTPSFunction(worker, frb); - const res = JSON.parse(data); - - expect(res.var).to.eql("localhost:9090"); - }).timeout(TIMEOUT_MED); + }); it("should expose a stubbed Firestore when the emulator is running", async () => { - const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest); - frb.emulators = { - firestore: { - host: "localhost", - port: 9090, + 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 worker = invokeRuntimeWithFunctions(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); - return Promise.resolve(); - }), - }; - }); - - const data = await callHTTPSFunction(worker, frb); + { FIRESTORE_EMULATOR_HOST: "localhost:9090" }, + ); + 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 = _.cloneDeep(FunctionRuntimeBundles.onRequest); - frb.emulators = {}; - - const worker = invokeRuntimeWithFunctions(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(), }); - return Promise.resolve(); }), }; }); - - const data = await callHTTPSFunction(worker, frb); + 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 set FIREBASE_DATABASE_EMULATOR_HOST when the emulator is running", async () => { - const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest); - frb.emulators = { - database: { - host: "localhost", - port: 9000, - }, - }; - - const worker = invokeRuntimeWithFunctions(frb, () => { - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - res.json({ - var: process.env.FIREBASE_DATABASE_EMULATOR_HOST, - }); - }), - }; - }); - - const data = await callHTTPSFunction(worker, frb); - const res = JSON.parse(data); - - expect(res.var).to.eql("localhost:9000"); - }).timeout(TIMEOUT_MED); + }); it("should expose a stubbed RTDB when the emulator is running", async () => { - const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest); - frb.emulators = { - database: { - host: "localhost", - port: 9090, + 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 worker = invokeRuntimeWithFunctions(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(), - }); - }), - }; - }); - - 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 = _.cloneDeep(FunctionRuntimeBundles.onRequest); - frb.emulators = { - database: { - host: "localhost", - port: 9090, + { + FIREBASE_DATABASE_EMULATOR_HOST: "localhost:9090", }, - }; - - const worker = invokeRuntimeWithFunctions(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!)); - }), - }; - }); - - const data = await callHTTPSFunction(worker, frb); + ); + const data = await sendReq(runtime); 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 = invokeRuntimeWithFunctions(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!)); - }), - }; - }); + expect(info.url).to.eql("http://localhost:9090/"); + }); + }); + }); + describe("_InitializeFunctionsConfigHelper()", () => { + const cfgPath = path.join(FUNCTIONS_DIR, ".runtimeconfig.json"); - const data = await callHTTPSFunction(worker, frb); - const info = JSON.parse(data); - expect(info.databaseURL).to.eql(frb.adminSdkConfig.databaseURL!); - }).timeout(TIMEOUT_MED); + before(async () => { + await fs.writeFile(cfgPath, '{"real":{"exist":"already exists" }}'); }); - it("should set FIREBASE_AUTH_EMULATOR_HOST when the emulator is running", async () => { - const frb = _.cloneDeep(FunctionRuntimeBundles.onRequest); - frb.emulators = { - auth: { - host: "localhost", - port: 9099, - }, - }; + after(async () => { + await fs.unlink(cfgPath); + }); - const worker = invokeRuntimeWithFunctions(frb, () => { + it("should tell the user if they've accessed a non-existent function field", async () => { + runtime = await startRuntime("functionId", "event", () => { + require("firebase-admin").initializeApp(); return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - res.json({ - var: process.env.FIREBASE_AUTH_EMULATOR_HOST, - }); - }), + 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); + }), }; }); - - const data = await callHTTPSFunction(worker, frb); - const res = JSON.parse(data); - - expect(res.var).to.eql("localhost:9099"); - }).timeout(TIMEOUT_MED); - }); - - describe("_InitializeFunctionsConfigHelper()", () => { - it("should tell the user if they've accessed a non-existent function field", async () => { - const worker = invokeRuntimeWithFunctions( - 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); - }), - }; - }, - { - nodeBinary: process.execPath, - env: { - CLOUD_RUNTIME_CONFIG: JSON.stringify({ - real: { exist: "already exists" }, - }), - }, - } - ); - - const logs = await countLogEntries(worker); - expect(logs["functions-config-missing-value"]).to.eq(2); - }).timeout(TIMEOUT_MED); + 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 = invokeRuntimeWithFunctions(frb, () => { + runtime = await startRuntime("functionId", "http", () => { require("firebase-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({ from_trigger: true }); }), }; }); - const data = await callHTTPSFunction(worker, frb); - + 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 = invokeRuntimeWithFunctions(frb, () => { - require("firebase-admin").initializeApp(); + runtime = await startRuntime("functionId", "http", () => { return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { + 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 = invokeRuntimeWithFunctions(frb, () => { - require("firebase-admin").initializeApp(); + runtime = await startRuntime("functionId", "http", () => { return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { + 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 = invokeRuntimeWithFunctions(frb, () => { - require("firebase-admin").initializeApp(); + runtime = await startRuntime("functionId", "http", () => { return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { + 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 = invokeRuntimeWithFunctions(frb, () => { - require("firebase-admin").initializeApp(); + runtime = await startRuntime("functionId", "http", () => { return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { + 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 = invokeRuntimeWithFunctions(frb, () => { - require("firebase-admin").initializeApp(); + runtime = await startRuntime("functionId", "http", () => { return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { + 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 = invokeRuntimeWithFunctions(frb, () => { - require("firebase-admin").initializeApp(); + runtime = await startRuntime("functionId", "http", () => { const app = require("express")(); app.all("/", (req: express.Request, res: express.Response) => { res.json({ @@ -658,244 +554,245 @@ describe("FunctionsEmulator-Runtime", () => { }); }); return { - function_id: require("firebase-functions").https.onRequest(app), + 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 = invokeRuntimeWithFunctions(frb, () => { - require("firebase-admin").initializeApp(); + runtime = await startRuntime("functionId", "http", () => { return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { + 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 = invokeRuntimeWithFunctions(frb, () => { - return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { - const now = new Date(); - res.json({ offset: now.getTimezoneOffset() }); - }), - }; - }); - - 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 = invokeRuntimeWithFunctions(FunctionRuntimeBundles.onWrite, () => { + runtime = await startRuntime("functionId", "event", () => { require("firebase-admin").initializeApp(); return { - function_id: require("firebase-functions") + 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(); }), }; }); - 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 }); - }); - - 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 = invokeRuntimeWithFunctions(FunctionRuntimeBundles.onUpdate, () => { + runtime = await startRuntime("functionId", "event", () => { require("firebase-admin").initializeApp(); return { - function_id: require("firebase-functions") + 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(); }), }; }); - 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 }); - }); - - 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(/{"before_exists":true,"after_exists":true}/); + }); - it("should provide DocumentSnapshot for firestore.onDelete()", async () => { - const worker = invokeRuntimeWithFunctions(FunctionRuntimeBundles.onDelete, () => { + it("should provide Change for firestore.onDelete()", async () => { + runtime = await startRuntime("functionId", "event", () => { require("firebase-admin").initializeApp(); return { - function_id: require("firebase-functions") + functionId: require("firebase-functions") .firestore.document("test/test") .onDelete((snap: DocumentSnapshot) => { console.log( JSON.stringify({ snap_exists: snap.exists, - }) + }), ); return Promise.resolve(); }), }; }); - worker.runtime.events.on("log", (el: EmulatorLog) => { - if (el.level !== "USER") { - return; - } - expect(JSON.parse(el.text)).to.deep.eq({ snap_exists: true }); - }); - - const logs = await countLogEntries(worker); - expect(logs["function-log"]).to.eq(1); - }).timeout(TIMEOUT_MED); + await sendEvent(runtime, FunctionRuntimeBundles.onDelete.proto); + expect(runtime.stdout.join(" ")).to.match(/{"snap_exists":true}/); + }); - it("should provide DocumentSnapshot for firestore.onCreate()", async () => { - const worker = invokeRuntimeWithFunctions(FunctionRuntimeBundles.onWrite, () => { + it("should provide Change for firestore.onCreate()", async () => { + runtime = await startRuntime("functionId", "event", () => { require("firebase-admin").initializeApp(); return { - function_id: require("firebase-functions") + functionId: require("firebase-functions") .firestore.document("test/test") .onCreate((snap: DocumentSnapshot) => { console.log( JSON.stringify({ snap_exists: snap.exists, - }) + }), ); return Promise.resolve(); }), }; }); - worker.runtime.events.on("log", (el: EmulatorLog) => { - if (el.level !== "USER") { - return; - } - expect(JSON.parse(el.text)).to.deep.eq({ snap_exists: true }); - }); - - 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 = invokeRuntimeWithFunctions(frb, () => { - require("firebase-admin").initializeApp(); + runtime = await startRuntime("functionId", "http", () => { return { - function_id: require("firebase-functions").https.onRequest((req: any, res: any) => { + functionId: require("firebase-functions").https.onRequest(() => { throw new Error("not a thing"); }), }; }); - - const logs = countLogEntries(worker); - 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 = invokeRuntimeWithFunctions(frb, () => { - require("firebase-admin").initializeApp(); + runtime = await startRuntime("functionId", "http", () => { 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")); - } - ), + functionId: require("firebase-functions").https.onRequest(async () => { + return Promise.reject(new Error("not a thing")); + }), }; }); - - const logs = countLogEntries(worker); - 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 = invokeRuntimeWithFunctions(frb, () => { - require("firebase-admin").initializeApp(); + runtime = await startRuntime("functionId", "http", () => { return { - function_id: require("firebase-functions") + functionId: require("firebase-functions") .runWith({}) - .https.onRequest(async (req: any, res: any) => { - await Promise.resolve(); // Required `await` for `async`. + .https.onRequest(async () => { return Promise.reject(new Error("not a thing")); }), }; }); - - const logs = countLogEntries(worker); - 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/tsconfig.dev.json b/scripts/emulator-tests/tsconfig.dev.json index ff58bdaa17d..37459f4aa3d 100644 --- a/scripts/emulator-tests/tsconfig.dev.json +++ b/scripts/emulator-tests/tsconfig.dev.json @@ -7,6 +7,8 @@ }, "include": [ "../../src/**/*", - "*", - ] + "../../src/*", + "./*", + ], + "exclude": [], } 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/.firebaserc b/scripts/extensions-deploy-tests/.firebaserc new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/scripts/extensions-deploy-tests/.firebaserc @@ -0,0 +1 @@ +{} diff --git a/scripts/extensions-deploy-tests/extensions/test-instance1.env b/scripts/extensions-deploy-tests/extensions/test-instance1.env new file mode 100644 index 00000000000..f027a8d197e --- /dev/null +++ b/scripts/extensions-deploy-tests/extensions/test-instance1.env @@ -0,0 +1,6 @@ +TABLE_ID=posts +TABLE_PARTITIONING=NONE +LOCATION=us-east1 +DATASET_LOCATION=us +COLLECTION_PATH=posters +DATASET_ID=firestore_export \ No newline at end of file diff --git a/scripts/extensions-deploy-tests/extensions/test-instance2.env b/scripts/extensions-deploy-tests/extensions/test-instance2.env new file mode 100644 index 00000000000..a007fa2ce7f --- /dev/null +++ b/scripts/extensions-deploy-tests/extensions/test-instance2.env @@ -0,0 +1,5 @@ +IMG_BUCKET=joehanley-public.appspot.com +IMG_SIZES=100x100 +DELETE_ORIGINAL_FILE=false +IMAGE_TYPE=jpeg +LOCATION=us-central1 \ No newline at end of file diff --git a/scripts/extensions-deploy-tests/firebase.json b/scripts/extensions-deploy-tests/firebase.json new file mode 100644 index 00000000000..ada161d0a7a --- /dev/null +++ b/scripts/extensions-deploy-tests/firebase.json @@ -0,0 +1,6 @@ +{ + "extensions": { + "test-instance1": "firebase/firestore-bigquery-export@^0.1.18", + "test-instance2": "firebase/storage-resize-images@^0.1.22" + } +} diff --git a/scripts/extensions-deploy-tests/run.sh b/scripts/extensions-deploy-tests/run.sh new file mode 100755 index 00000000000..a02c539c814 --- /dev/null +++ b/scripts/extensions-deploy-tests/run.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e # Immediately exit on failure + +# Globally link the CLI for the testing framework +./scripts/clean-install.sh + +mocha scripts/extensions-deploy-tests/tests.ts diff --git a/scripts/extensions-deploy-tests/tests.ts b/scripts/extensions-deploy-tests/tests.ts new file mode 100644 index 00000000000..5ed1c53e5a6 --- /dev/null +++ b/scripts/extensions-deploy-tests/tests.ts @@ -0,0 +1,59 @@ +import { expect } from "chai"; + +import { CLIProcess } from "../integration-helpers/cli"; + +const FIREBASE_PROJECT = process.env.FBTOOLS_TARGET_PROJECT || ""; + +const TEST_SETUP_TIMEOUT_MS = 10000; +const TEST_TIMEOUT_MS = 600000; + +describe("firebase deploy --only extensions", () => { + let cli: CLIProcess; + before(function (this) { + this.timeout(TEST_SETUP_TIMEOUT_MS); + expect(FIREBASE_PROJECT).to.exist.and.not.be.empty; + cli = new CLIProcess("default", __dirname); + }); + + after(() => { + cli.stop(); + }); + + it("should have deployed the expected extensions", async function (this) { + this.timeout(TEST_TIMEOUT_MS); + + await cli.start( + "deploy", + FIREBASE_PROJECT, + ["--only", "extensions", "--non-interactive", "--force"], + (data: any) => { + if (`${data}`.match(/Deploy complete/)) { + return true; + } + }, + ); + let output: any; + await cli.start("ext:list", FIREBASE_PROJECT, ["--json"], (data: any) => { + output = JSON.parse(data); + return true; + }); + + expect(output.result.length).to.eq(2); + expect( + output.result.some( + (i: any) => + i.instanceId === "test-instance1" && + i.extension === "firebase/firestore-bigquery-export" && + 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", + ), + ).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..603f3934c97 --- /dev/null +++ b/scripts/extensions-emulator-tests/functions/package.json @@ -0,0 +1,14 @@ +{ + "name": "functions", + "description": "Cloud Functions for Firebase", + "scripts": {}, + "engines": { + "node": "20" + }, + "dependencies": { + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.1.0", + "fs-extra": "^5.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 5a5cb23ae56..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.3.0" - }, - "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..ef9861cbb66 --- 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 { rmSync } from "node:fs"; 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) + ) { + rmSync(process.env.FIREBASE_EXTENSIONS_CACHE_PATH, { recursive: true }); + } +} 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/firepit-builder/Dockerfile b/scripts/firepit-builder/Dockerfile index f04522d09e1..e4fdd363785 100644 --- a/scripts/firepit-builder/Dockerfile +++ b/scripts/firepit-builder/Dockerfile @@ -1,4 +1,4 @@ -FROM node:12 +FROM node:20 # 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 f2dc85ef312..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" } @@ -186,9 +632,9 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "rechoir": { "version": "0.6.2", @@ -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", @@ -270,9 +723,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" }, "yargs": { "version": "13.3.0", 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..6db700cec3a --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/bundled/dist/package.json @@ -0,0 +1,4 @@ +{ + "name": "dist", + "version": "0.0.1" +} 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..70ac76246a7 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/bundled/firebase.json @@ -0,0 +1,6 @@ +{ + "functions": { + "source": "dist", + "runtime": "nodejs22" + } +} \ 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..fe8d33b546b --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/bundled/package.json @@ -0,0 +1,7 @@ +{ + "name": "dist", + "version": "0.0.1", + "dependencies": { + "firebase-functions": "^6.4.0" + } +} \ 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..0f3ad5a31fe --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/codebases/firebase.json @@ -0,0 +1,26 @@ +{ + "functions": [ + { + "source": "v1", + "codebase": "v1", + "runtime": "nodejs22", + "ignore": [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log" + ] + }, + { + "source": "v2", + "codebase": "v2", + "runtime": "nodejs22", + "ignore": [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log" + ] + } + ] +} 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..ab4598111e0 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/codebases/v1/package.json @@ -0,0 +1,6 @@ +{ + "name": "codebase-v1", + "dependencies": { + "firebase-functions": "^6.4.0" + } +} 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..6d91da2c4ee --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/codebases/v2/package.json @@ -0,0 +1,7 @@ +{ + "name": "codebase-v2", + "type": "module", + "dependencies": { + "firebase-functions": "^6.4.0" + } +} 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..5d2ac9afea3 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/esm/firebase.json @@ -0,0 +1,6 @@ +{ + "functions": { + "source": "functions", + "runtime": "nodejs22" + } +} 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..092ed5e70c0 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/esm/functions/package.json @@ -0,0 +1,7 @@ +{ + "name": "esm", + "type": "module", + "dependencies": { + "firebase-functions": "^6.4.0" + } +} 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..5d2ac9afea3 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/pnpm/firebase.json @@ -0,0 +1,6 @@ +{ + "functions": { + "source": "functions", + "runtime": "nodejs22" + } +} 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..7ddca3681b9 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/pnpm/functions/package.json @@ -0,0 +1,6 @@ +{ + "name": "pnpm", + "dependencies": { + "firebase-functions": "^6.4.0" + } +} 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..5d2ac9afea3 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/simple/firebase.json @@ -0,0 +1,6 @@ +{ + "functions": { + "source": "functions", + "runtime": "nodejs22" + } +} 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..712c92b37e1 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/simple/functions/package.json @@ -0,0 +1,6 @@ +{ + "name": "simple", + "dependencies": { + "firebase-functions": "^6.4.0" + } +} 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/stress-test/firebase.json b/scripts/functions-discover-tests/fixtures/stress-test/firebase.json new file mode 100644 index 00000000000..85ebba0e1d9 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/stress-test/firebase.json @@ -0,0 +1,6 @@ +{ + "functions": { + "source": "functions", + "runtime": "nodejs22" + } +} \ No newline at end of file diff --git a/scripts/functions-discover-tests/fixtures/stress-test/functions/index.js b/scripts/functions-discover-tests/fixtures/stress-test/functions/index.js new file mode 100644 index 00000000000..5d6f5f6586d --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/stress-test/functions/index.js @@ -0,0 +1,8 @@ +const { onRequest } = require("firebase-functions/v2/https"); + +// Generate 20 functions for stress testing +for (let i = 1; i <= 20; i++) { + exports[`stressFunction${i}`] = onRequest((request, response) => { + response.send(`Hello from stress function ${i}!`); + }); +} diff --git a/scripts/functions-discover-tests/fixtures/stress-test/functions/package.json b/scripts/functions-discover-tests/fixtures/stress-test/functions/package.json new file mode 100644 index 00000000000..26b8c354c2a --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/stress-test/functions/package.json @@ -0,0 +1,6 @@ +{ + "name": "stress-test", + "dependencies": { + "firebase-functions": "^6.4.0" + } +} \ No newline at end of file diff --git a/scripts/functions-discover-tests/fixtures/stress-test/install.sh b/scripts/functions-discover-tests/fixtures/stress-test/install.sh new file mode 100755 index 00000000000..b60d01c6103 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/stress-test/install.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euxo pipefail # bash strict mode +IFS=$'\n\t' + +cd functions && npm i \ No newline at end of file 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..823f2bea7e8 --- /dev/null +++ b/scripts/functions-discover-tests/fixtures/yarn-workspaces/firebase.json @@ -0,0 +1,6 @@ +{ + "functions": { + "source": "packages/functions", + "runtime": "nodejs22" + } +} \ 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..702d28f9545 --- /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": "^6.4.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..2f961f63c2a --- /dev/null +++ b/scripts/functions-discover-tests/tests.ts @@ -0,0 +1,166 @@ +import * as path from "node:path"; +import * as os from "node:os"; +import * as fs from "node:fs/promises"; + +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[]; + }[]; +} + +async function runDiscoveryTest( + projectDir: string, + testcase: Testcase, + env?: Record, +): Promise { + const cli = new CLIProcess("default", projectDir); + + let outputBuffer = ""; + let output: any; + await cli.start( + "internaltesting:functions:discover", + FIREBASE_PROJECT, + ["--json"], + (data: any) => { + outputBuffer += data; + try { + output = JSON.parse(outputBuffer); + return true; + } catch (e) { + // Not complete JSON yet, continue buffering + return false; + } + }, + env, + ); + + expect(output.status).to.equal("success"); + for (const e of testcase.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(); +} + +describe("Function discovery test", function (this) { + this.timeout(2000_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"], + }, + ], + }, + { + name: "stress-test", + projectDir: "stress-test", + expects: [ + { + codebase: "default", + endpoints: Array.from({ length: 20 }, (_, i) => `stressFunction${i + 1}`), + }, + ], + }, + ]; + + describe("detectFromPort", () => { + for (const tc of testCases) { + it(`discovers functions using HTTP in a ${tc.name} project`, async () => { + await runDiscoveryTest(path.join(FIXTURES, tc.projectDir), tc); + }); + } + }); + + describe("detectFromOutputPath", () => { + for (const tc of testCases) { + it(`discovers functions using file-based discovery in a ${tc.name} project`, async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "firebase-test-")); + try { + await runDiscoveryTest(path.join(FIXTURES, tc.projectDir), tc, { + FIREBASE_FUNCTIONS_DISCOVERY_OUTPUT_PATH: tempDir, + }); + } finally { + // Clean up the temp directory + await fs.rm(tempDir, { recursive: true, force: true }).catch(() => { + // Ignore cleanup errors + }); + } + }); + } + }); +}); diff --git a/scripts/gen-auth-api-spec.ts b/scripts/gen-auth-api-spec.ts index 12d13bb84d8..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. @@ -28,11 +25,6 @@ import swaggerToTS from "@manifoldco/swagger-to-ts"; // @ts-ignore import * as googleDiscoveryToSwagger from "google-discovery-to-swagger"; -// Swagger and OpenAPI 3.0 formats do not support RFC 6570 Level 2 features -// (like {+var}) which are present in API Discovery. Setting this flag in this -// library makes it produce Level 1-only URL templates and fully spec-compliant. -googleDiscoveryToSwagger.setStrict(true); - async function main(): Promise { const [v1Disc, v2Disc, tokenDisc] = await Promise.all([ fetchJson("https://identitytoolkit.googleapis.com/$discovery/rest?version=v1"), @@ -72,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); } @@ -105,7 +98,7 @@ function fetchJson(url: string): any { const OPENAPI_HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"]; -async function toOpenapi3(discovery: any): Promise { +async function toOpenapi3(discovery: Discovery): Promise { // Error format query param, not supported in emulator and pollutes defs. delete discovery.parameters["$.xgafv"]; @@ -113,23 +106,139 @@ async function toOpenapi3(discovery: any): Promise { const apiKeyDescription = discovery.parameters.key.description; delete discovery.parameters.key; + // Preprocess and replace paths with flatPaths + replaceWithFlatPath(discovery.resources); + // We first convert the discovery document to Swagger (a.k.a. OpenAPI 2.0) and // then to OpenAPI 3.0 because there is tool that does direct conversion. Some // 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(/\/$/, ""); }); - patchSecurity(openapi3, apiKeyDescription); + patchSecurity(openapi3, apiKeyDescription!); return openapi3; } +interface Discovery { + kind: string; + name: string; + version: string; + title: string; + description: string; + protocol: string; + rootUrl: string; + servicePath: string; + parameters: Parameters; + resources: Resources; +} + +interface Parameters { + [paramName: string]: Parameter; +} + +interface Parameter { + type: string; + required: boolean; + location: string; + description?: string; + pattern?: string; +} + +interface Methods { + [methodName: string]: Method; +} + +interface Method { + id: string; + path: string; + flatPath: string; + httpMethod: string; + description: string; + response: { $ref: string }; + parameters: Parameters; + parameterOrder: string[]; + scopes: string[]; +} + +interface Resource { + methods: Methods; + resources?: Resources; +} + +interface Resources { + [resourceName: string]: Resource | Resources; +} + +const pathParamsForFlatPathParam = new Map([ + ["{projectsId}", "{targetProjectId}"], + ["{tenantsId}", "{tenantId}"], +]); + +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: + // path = v2/{name} + // flatPath = v2/projects/{projectsId}/defaultSupportedIdpConfigs/{defaultSupportedIdpConfigsId} + // + // transform v2/projects/{projectsId}/defaultSupportedIdpConfigs/{defaultSupportedIdpConfigsId} + // --> v2/projects/{targetProjectId}/defaultSupportedIdpConfigs/{defaultSupportedIdpConfigsId} + let flatPath = method.flatPath; + pathParamsForFlatPathParam.forEach((pathParam, flatPathParam) => { + flatPath = flatPath.replace(flatPathParam, pathParam); + }); + + const cleanedParams: Parameters = {}; + + // Get all param names in path + // e.g. ["projectsId", "defaultSupportedIdpConfigsId"] + const paramsInPath = [...flatPath.matchAll(paramPattern)].map((match) => match[1]); + + // Remove method path parameters that don't appear in the path + // e.g. remove parameter "name" that appears in original path + const params = method.parameters; + Object.entries(params).forEach(([name, paramObj]) => { + // Compiler complains that paramObj is unknown, cast explicitly + if ((paramObj as Parameter).location !== "path" || paramsInPath.includes(name)) { + cleanedParams[name] = paramObj as Parameter; + } + }); + + // Add params that are in path but are not in the parameters object + // e.g. add "targetProjectId" and "defaultSupportedIdpConfigsId" + paramsInPath.forEach((param) => { + if (!Object.keys(cleanedParams).some((name) => name === param)) { + cleanedParams[param] = { + location: "path", + required: true, + type: "string", + }; + } + }); + + method.parameters = cleanedParams; + method.parameterOrder = paramsInPath; + method.path = flatPath; + }); + if (discovery.resources) { + replaceWithFlatPath(discovery.resources); + } + return; + } + Object.values(discovery).forEach((val) => replaceWithFlatPath(val)); +} + function patchSecurity(openapi3: any, apiKeyDescription: string): void { // OpenAPI v3 now supports putting multiple flows in one single OAuth object, // so let's remove the "Oauth2c" workaround and merge it into "Oauth2". @@ -141,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) { @@ -160,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: [] }); }); } @@ -229,6 +345,46 @@ function addEmulatorOperations(openapi3: any): void { tags: ["emulator"], }, }; + openapi3.paths["/emulator/v1/projects/{targetProjectId}/tenants/{tenantId}/accounts"] = { + parameters: [ + { + name: "targetProjectId", + in: "path", + description: "The ID of the Google Cloud project that the accounts belong to.", + required: true, + schema: { + type: "string", + }, + }, + { + name: "tenantId", + in: "path", + description: + "The ID of the Identity Platform tenant the accounts belongs to. If not specified, accounts on the Identity Platform project are returned.", + required: true, + schema: { type: "string" }, + }, + ], + servers: [{ url: "" }], + delete: { + description: "Remove all accounts in the project, regardless of state.", + operationId: "emulator.projects.accounts.delete", + responses: { + "200": { + description: "Successful response", + content: { + "application/json": { + schema: { + type: "object", + }, + }, + }, + }, + }, + security: [], + tags: ["emulator"], + }, + }; openapi3.paths["/emulator/v1/projects/{targetProjectId}/config"] = { parameters: [ { @@ -332,6 +488,46 @@ function addEmulatorOperations(openapi3: any): void { tags: ["emulator"], }, }; + openapi3.paths["/emulator/v1/projects/{targetProjectId}/tenants/{tenantId}/oobCodes"] = { + parameters: [ + { + name: "targetProjectId", + in: "path", + description: "The ID of the Google Cloud project that the confirmation codes belongs to.", + required: true, + schema: { + type: "string", + }, + }, + { + name: "tenantId", + in: "path", + description: + "The ID of the Identity Platform tenant the accounts belongs to. If not specified, accounts on the Identity Platform project are returned.", + required: true, + schema: { type: "string" }, + }, + ], + servers: [{ url: "" }], + get: { + description: "List all pending confirmation codes for the project.", + operationId: "emulator.projects.oobCodes.list", + responses: { + "200": { + description: "Successful response", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/EmulatorV1ProjectsOobCodes", + }, + }, + }, + }, + }, + security: [], + tags: ["emulator"], + }, + }; openapi3.components.schemas.EmulatorV1ProjectsOobCodes = { type: "object", description: "Details of all pending confirmation codes.", @@ -382,6 +578,46 @@ function addEmulatorOperations(openapi3: any): void { tags: ["emulator"], }, }; + openapi3.paths["/emulator/v1/projects/{targetProjectId}/tenants/{tenantId}/verificationCodes"] = { + parameters: [ + { + name: "targetProjectId", + in: "path", + description: "The ID of the Google Cloud project that the verification codes belongs to.", + required: true, + schema: { + type: "string", + }, + }, + { + name: "tenantId", + in: "path", + description: + "The ID of the Identity Platform tenant the accounts belongs to. If not specified, accounts on the Identity Platform project are returned.", + required: true, + schema: { type: "string" }, + }, + ], + servers: [{ url: "" }], + get: { + description: "List all pending phone verification codes for the project.", + operationId: "emulator.projects.verificationCodes.list", + responses: { + "200": { + description: "Successful response", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/EmulatorV1ProjectsOobCodes", + }, + }, + }, + }, + }, + security: [], + tags: ["emulator"], + }, + }; openapi3.components.schemas.EmulatorV1ProjectsVerificationCodes = { type: "object", description: "Details of all pending verification codes.", @@ -409,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..0b44c58dcca 100644 --- a/scripts/integration-helpers/cli.ts +++ b/scripts/integration-helpers/cli.ts @@ -1,15 +1,20 @@ -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, + env?: Record, ): Promise { const args = [cmd, "--project", project]; @@ -17,17 +22,20 @@ export class CLIProcess { args.push(...additionalArgs); } - const p = subprocess.spawn("firebase", args, { cwd: this.workdir }); + const p = spawn("firebase", args, { + cwd: this.workdir, + env: env ? { ...process.env, ...env } : process.env, + }); 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 +45,19 @@ 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); + p.stderr?.on("data", (data) => { + console.error(`[${this.name} stderr]`, data.toString()); + }); }); } else { started = new Promise((resolve) => { @@ -66,7 +77,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 ebc12749a5c..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"; @@ -8,11 +9,43 @@ const FIREBASE_PROJECT_ZONE = "us-central1"; * Markers this test looks for in the emulator process stdout * as one test for whether a cloud function was triggered. */ +/* 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 =========="; +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 =========="; 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 =========="; 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; @@ -21,6 +54,7 @@ interface ConnectionInfo { export interface FrameworkOptions { emulators?: { + hub: ConnectionInfo; database: ConnectionInfo; firestore: ConnectionInfo; functions: ConnectionInfo; @@ -30,39 +64,127 @@ 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; pubsubTriggerCount = 0; authTriggerCount = 0; + storageArchivedTriggerCount = 0; + storageDeletedTriggerCount = 0; + storageFinalizedTriggerCount = 0; + storageMetadataTriggerCount = 0; + storageBucketArchivedTriggerCount = 0; + storageBucketDeletedTriggerCount = 0; + storageBucketFinalizedTriggerCount = 0; + storageBucketMetadataTriggerCount = 0; + authBlockingCreateV1TriggerCount = 0; + authBlockingSignInV1TriggerCount = 0; + + /* Functions V2 */ + pubsubV2TriggerCount = 0; + storageV2ArchivedTriggerCount = 0; + storageV2DeletedTriggerCount = 0; + storageV2FinalizedTriggerCount = 0; + storageV2MetadataTriggerCount = 0; + storageBucketV2ArchivedTriggerCount = 0; + 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 */ + this.firestoreTriggerCount = 0; + this.rtdbTriggerCount = 0; + this.pubsubTriggerCount = 0; + this.authTriggerCount = 0; + this.storageArchivedTriggerCount = 0; + this.storageDeletedTriggerCount = 0; + this.storageFinalizedTriggerCount = 0; + this.storageMetadataTriggerCount = 0; + this.storageBucketArchivedTriggerCount = 0; + this.storageBucketDeletedTriggerCount = 0; + this.storageBucketFinalizedTriggerCount = 0; + this.storageBucketMetadataTriggerCount = 0; + this.authBlockingCreateV1TriggerCount = 0; + this.authBlockingSignInV1TriggerCount = 0; + + /* Functions V2 */ + this.pubsubV2TriggerCount = 0; + this.storageV2ArchivedTriggerCount = 0; + this.storageV2DeletedTriggerCount = 0; + this.storageV2FinalizedTriggerCount = 0; + this.storageV2MetadataTriggerCount = 0; + this.storageBucketV2ArchivedTriggerCount = 0; + this.storageBucketV2DeletedTriggerCount = 0; + this.storageBucketV2FinalizedTriggerCount = 0; + this.storageBucketV2MetadataTriggerCount = 0; + this.authBlockingCreateV2TriggerCount = 0; + this.authBlockingSignInV2TriggerCount = 0; + this.rtdbV2TriggerCount = 0; + this.firestoreV2TriggerCount = 0; } /* @@ -78,16 +200,12 @@ 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++; } @@ -100,10 +218,74 @@ export class TriggerEndToEndTest { if (data.includes(AUTH_FUNCTION_LOG)) { this.authTriggerCount++; } + if (data.includes(STORAGE_FUNCTION_ARCHIVED_LOG)) { + this.storageArchivedTriggerCount++; + } + if (data.includes(STORAGE_FUNCTION_DELETED_LOG)) { + this.storageDeletedTriggerCount++; + } + if (data.includes(STORAGE_FUNCTION_FINALIZED_LOG)) { + this.storageFinalizedTriggerCount++; + } + if (data.includes(STORAGE_FUNCTION_METADATA_LOG)) { + this.storageMetadataTriggerCount++; + } + if (data.includes(STORAGE_BUCKET_FUNCTION_ARCHIVED_LOG)) { + this.storageBucketArchivedTriggerCount++; + } + if (data.includes(STORAGE_BUCKET_FUNCTION_DELETED_LOG)) { + this.storageBucketDeletedTriggerCount++; + } + if (data.includes(STORAGE_BUCKET_FUNCTION_FINALIZED_LOG)) { + this.storageBucketFinalizedTriggerCount++; + } + if (data.includes(STORAGE_BUCKET_FUNCTION_METADATA_LOG)) { + this.storageBucketMetadataTriggerCount++; + } + + /* Functions V2 */ + if (data.includes(PUBSUB_FUNCTION_V2_LOG)) { + this.pubsubV2TriggerCount++; + } + if (data.includes(STORAGE_FUNCTION_V2_ARCHIVED_LOG)) { + this.storageV2ArchivedTriggerCount++; + } + if (data.includes(STORAGE_FUNCTION_V2_DELETED_LOG)) { + this.storageV2DeletedTriggerCount++; + } + if (data.includes(STORAGE_FUNCTION_V2_FINALIZED_LOG)) { + this.storageV2FinalizedTriggerCount++; + } + if (data.includes(STORAGE_FUNCTION_V2_METADATA_LOG)) { + this.storageV2MetadataTriggerCount++; + } + if (data.includes(STORAGE_BUCKET_FUNCTION_V2_ARCHIVED_LOG)) { + this.storageBucketV2ArchivedTriggerCount++; + } + if (data.includes(STORAGE_BUCKET_FUNCTION_V2_DELETED_LOG)) { + this.storageBucketV2DeletedTriggerCount++; + } + if (data.includes(STORAGE_BUCKET_FUNCTION_V2_FINALIZED_LOG)) { + this.storageBucketV2FinalizedTriggerCount++; + } + 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 { @@ -113,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"); } @@ -155,10 +373,34 @@ export class TriggerEndToEndTest { return this.invokeHttpFunction("writeToScheduledPubsub"); } + writeToDefaultStorage(): Promise { + return this.invokeHttpFunction("writeToDefaultStorage"); + } + + writeToSpecificStorageBucket(): Promise { + return this.invokeHttpFunction("writeToSpecificStorageBucket"); + } + + updateMetadataDefaultStorage(): Promise { + return this.invokeHttpFunction("updateMetadataFromDefaultStorage"); + } + + updateMetadataSpecificStorageBucket(): Promise { + return this.invokeHttpFunction("updateMetadataFromSpecificStorageBucket"); + } + + updateDeleteFromDefaultStorage(): Promise { + return this.invokeHttpFunction("updateDeleteFromDefaultStorage"); + } + + updateDeleteFromSpecificStorageBucket(): Promise { + return this.invokeHttpFunction("updateDeleteFromSpecificStorageBucket"); + } + waitForCondition( conditionFn: () => boolean, timeout: number, - callback: (err?: Error) => void + callback: (err?: Error) => void, ): void { let elapsed = 0; const interval = 10; @@ -176,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/mcp-tests/gemini-smoke-test.ts b/scripts/mcp-tests/gemini-smoke-test.ts new file mode 100644 index 00000000000..3ad89316e7f --- /dev/null +++ b/scripts/mcp-tests/gemini-smoke-test.ts @@ -0,0 +1,70 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; + +if (!process.env.GEMINI_API_KEY) { + console.error("Must set GEMINI_API_KEY to run this smoke test."); + process.exit(1); +} + +const client = new Client({ + name: "firebase-mcp-smoke-tester", + version: "0.0.1", +}); + +await client.connect( + new StdioClientTransport({ + command: "../../lib/bin/firebase.js", + args: [ + "mcp", + "--only", + "firestore,dataconnect,messaging,remoteconfig,crashlytics,auth,storage,apphosting", + ], + }), +); + +const { tools } = await client.listTools(); + +const geminiTools = tools.map((tool) => { + return { + name: tool.name, + description: tool.description, + parameters: tool.inputSchema, + }; +}); + +const response = await fetch( + `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${process.env.GEMINI_API_KEY}`, + { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ + toolConfig: { + functionCallingConfig: { mode: "auto" }, + }, + tools: [{ functionDeclarations: geminiTools }], + contents: [ + { + parts: [{ text: "Call the firebase_list_apps tool." }], + }, + ], + }), + }, +); + +if (response.status === 200) { + console.dir(await response.json(), { depth: null }); + + console.log("✅ Passed smoke test!"); + process.exit(0); +} else { + const rtext = await response.text(); + try { + console.dir(JSON.parse(rtext), { depth: null }); + } catch (e) { + console.log(rtext); + } + console.error("ERROR: Got non-200 response from smoke test."); + process.exit(1); +} diff --git a/scripts/mcp-tests/package-lock.json b/scripts/mcp-tests/package-lock.json new file mode 100644 index 00000000000..f642b185e6f --- /dev/null +++ b/scripts/mcp-tests/package-lock.json @@ -0,0 +1,1553 @@ +{ + "name": "mcp-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-tests", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.11.5", + "tsx": "^4.19.4" + }, + "devDependencies": { + "typescript": "^5.8.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.5.tgz", + "integrity": "sha512-gS7Q7IHpKxjVaNLMUZyTtatZ63ca3h418zPPntAhu/MvG5yfz/8HMcDAOpvpQfx3V3dsw9QQxk8RuFNrQhLlgA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.2.tgz", + "integrity": "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.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==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "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==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.19.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", + "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.17", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.17.tgz", + "integrity": "sha512-8hQzQ/kMOIFbwOgPrm9Sf9rtFHpFUMy4HvN0yEB0spw14aYi0uT5xG5CE2DB9cd51GWNsz+DNO7se1kztHMKnw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/scripts/mcp-tests/package.json b/scripts/mcp-tests/package.json new file mode 100644 index 00000000000..722d9297df1 --- /dev/null +++ b/scripts/mcp-tests/package.json @@ -0,0 +1,20 @@ +{ + "name": "mcp-tests", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "scripts": { + "test": "tsx ./gemini-smoke-test.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.11.5", + "tsx": "^4.19.4" + }, + "devDependencies": { + "typescript": "^5.8.3" + } +} diff --git a/scripts/mcp-tests/tsconfig.json b/scripts/mcp-tests/tsconfig.json new file mode 100644 index 00000000000..5ac568d3134 --- /dev/null +++ b/scripts/mcp-tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "NodeNext", + "skipLibCheck": true + } +} 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.sh b/scripts/publish.sh old mode 100644 new mode 100755 index 70c6e361a30..54575e83428 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -2,18 +2,28 @@ set -e printusage() { - echo "publish.sh " + echo "publish.sh [branch]" 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 " version: 'patch', 'minor', 'major', 'artifactsOnly', or 'preview'" + echo " branch: required if version is 'preview'" } VERSION=$1 +BRANCH=$2 if [[ $VERSION == "" ]]; then printusage exit 1 +elif [[ $VERSION == "artifactsOnly" ]]; then + echo "Skipping npm package publish since VERSION is artifactsOnly." + exit 0 +elif [[ $VERSION == "preview" ]]; then + if [[ $BRANCH == "" ]]; then + printusage + exit 1 + fi elif [[ ! ($VERSION == "patch" || $VERSION == "minor" || $VERSION == "major") ]]; then printusage exit 1 @@ -42,13 +52,6 @@ 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 - ERR -echo "Checked for Twitter credentials..." 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 @@ -65,6 +68,11 @@ echo "Moved to temporary directory." echo "Cloning repository..." git clone "git@github.com:${REPOSITORY_ORG}/${REPOSITORY_NAME}.git" cd "${REPOSITORY_NAME}" +if [[ $VERSION == "preview" ]]; then + echo "Checking out branch $BRANCH..." + git checkout "$BRANCH" + echo "Checked out branch $BRANCH." +fi echo "Cloned repository." echo "Making sure there is a changelog..." @@ -82,10 +90,18 @@ echo "Running tests..." npm test echo "Ran tests." -echo "Making a $VERSION version..." -npm version $VERSION -NEW_VERSION=$(jq -r ".version" package.json) -echo "Made a $VERSION version." +if [[ $VERSION == "preview" ]]; then + echo "Making a preview version..." + sanitized_branch=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9]/-/g') + npm version prerelease --preid=${sanitized_branch} + NEW_VERSION=$(jq -r ".version" package.json) + echo "Made a preview version." +else + echo "Making a $VERSION version..." + npm version $VERSION + NEW_VERSION=$(jq -r ".version" package.json) + echo "Made a $VERSION version." +fi echo "Making the release notes..." RELEASE_NOTES_FILE=$(mktemp) @@ -96,25 +112,25 @@ cat CHANGELOG.md >> "${RELEASE_NOTES_FILE}" echo "Made the release notes." echo "Publishing to npm..." -npm publish +npx clean-publish@5.0.0 --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 -echo "Cleaned up release notes." - -echo "Pushing to GitHub..." -git push origin master --tags -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." +if [[ $VERSION != "preview" ]]; then + echo "Updating package-lock.json for Docker image..." + npm --prefix ./scripts/publish/firebase-docker-image install + echo "Updated package-lock.json for Docker image." + + 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 scripts/publish/firebase-docker-image/package-lock.json + echo "Cleaned up release notes." + + echo "Pushing to GitHub..." + git push origin master --tags + echo "Pushed to GitHub." + + echo "Publishing release notes..." + hub release create --file "${RELEASE_NOTES_FILE}" "v${NEW_VERSION}" + echo "Published release notes." +fi diff --git a/scripts/publish/cloudbuild.yaml b/scripts/publish/cloudbuild.yaml index 4113b5875f6..1fcb230880e 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,24 +98,74 @@ steps: # Publish the package. - name: "gcr.io/$PROJECT_ID/package-builder" dir: "${_REPOSITORY_NAME}" - args: ["bash", "./scripts/publish.sh", "${_VERSION}"] + entrypoint: bash + args: + - -c + - | + if [ "${_VERSION}" == "preview" ]; then + ./scripts/publish.sh "${_VERSION}" "${_BRANCH}" + else + ./scripts/publish.sh "${_VERSION}" + fi env: - "REPOSITORY_ORG=${_REPOSITORY_ORG}" - "REPOSITORY_NAME=${_REPOSITORY_NAME}" # Wait a bit of time for npm to catch up. - name: "gcr.io/$PROJECT_ID/package-builder" - args: ["sleep", "60"] + args: ["sleep", "240"] # Set up the hub credentials for firepit-builder. - name: "gcr.io/$PROJECT_ID/firepit-builder" entrypoint: "bash" - args: ["-c", "mkdir -vp ~/.config && cp -v hub ~/.config/hub"] + args: + - "-c" + - | + if [ "${_VERSION}" != "preview" ]; then + mkdir -vp ~/.config && cp -v hub ~/.config/hub + else + echo "Skipping hub credentials for firepit-builder for preview." + fi # Publish the firepit builds. - name: "gcr.io/$PROJECT_ID/firepit-builder" - entrypoint: "node" - args: ["/usr/src/app/pipeline.js", "--package=firebase-tools@latest", "--publish"] + entrypoint: "bash" + args: + - "-c" + - | + if [ "${_VERSION}" != "preview" ]; then + node /usr/src/app/pipeline.js --package=firebase-tools@latest --publish + else + echo "Skipping firepit build for preview version." + fi + + # Grab the latest version, store in workspace + - id: "Read New Version Number from npm" + name: "node" + entrypoint: "sh" + args: + - "-c" + - | + if [ "${_VERSION}" != "preview" ]; then + npm view firebase-tools version > /workspace/version_number.txt + else + echo "Skipping version lookup for preview version." + fi + + # Publish the Firebase docker image + - name: "gcr.io/cloud-builders/docker" + entrypoint: "bash" + args: + - "-c" + - | + if [ "${_VERSION}" != "preview" ]; then + docker build -t us-docker.pkg.dev/${_ARTIFACT_REGISTRY_PROJECT}/us/firebase:$(cat /workspace/version_number.txt) -t us-docker.pkg.dev/${_ARTIFACT_REGISTRY_PROJECT}/us/firebase:latest -f ./firebase-docker-image/Dockerfile ./firebase-docker-image + else + echo "Skipping docker build for preview version." + fi + +images: + - "us-docker.pkg.dev/${_ARTIFACT_REGISTRY_PROJECT}/us/firebase" timeout: 1200s # 20 minutes @@ -122,7 +176,9 @@ options: substitutions: _VERSION: "" + _BRANCH: "" _KEY_RING: "cloud-build-ring" _KEY_NAME: "publish" _REPOSITORY_ORG: "firebase" _REPOSITORY_NAME: "firebase-tools" + _ARTIFACT_REGISTRY_PROJECT: "firebase-cli" diff --git a/scripts/publish/firebase-docker-image/Dockerfile b/scripts/publish/firebase-docker-image/Dockerfile new file mode 100644 index 00000000000..0455e95e313 --- /dev/null +++ b/scripts/publish/firebase-docker-image/Dockerfile @@ -0,0 +1,27 @@ +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 && \ + apk update && \ + apk upgrade + + +RUN mkdir -p /usr/local/node_packages/ +COPY package.json /usr/local/node_packages/ +COPY package-lock.json /usr/local/node_packages/ + +WORKDIR /usr/local/node_packages/ +RUN npm install +ENV PATH="/usr/local/node_packages/node_modules/.bin:${PATH}" + +WORKDIR / + +RUN firebase setup:emulators:database && \ + firebase setup:emulators:firestore && \ + firebase setup:emulators:pubsub && \ + firebase setup:emulators:storage && \ + firebase setup:emulators:dataconnect && \ + firebase setup:emulators:ui && \ + rm -rf /var/cache/apk/* + +ENTRYPOINT [ "firebase" ] \ No newline at end of file diff --git a/scripts/publish/firebase-docker-image/cloudbuild.yaml b/scripts/publish/firebase-docker-image/cloudbuild.yaml new file mode 100644 index 00000000000..0bfdbbc1ab4 --- /dev/null +++ b/scripts/publish/firebase-docker-image/cloudbuild.yaml @@ -0,0 +1,19 @@ +steps: + # Grab the latest version, store in workspace + - id: "Read New Version Number from npm" + name: "node" + entrypoint: "sh" + args: + [ + "-c", + "npm view firebase-tools version > /workspace/version_number.txt && cat /workspace/version_number.txt", + ] + # Publish the Firebase docker image + - name: "gcr.io/cloud-builders/docker" + entrypoint: "sh" + args: + - "-c" + - "docker build -t us-docker.pkg.dev/$PROJECT_ID/us/firebase:$(cat /workspace/version_number.txt) -t us-docker.pkg.dev/$PROJECT_ID/us/firebase:latest -t us-docker.pkg.dev/$PROJECT_ID/us/firebase:public-image-$(cat /workspace/version_number.txt) -f ./Dockerfile ." + +images: + - "us-docker.pkg.dev/$PROJECT_ID/us/firebase" diff --git a/scripts/publish/firebase-docker-image/package-lock.json b/scripts/publish/firebase-docker-image/package-lock.json new file mode 100644 index 00000000000..82344aa8e15 --- /dev/null +++ b/scripts/publish/firebase-docker-image/package-lock.json @@ -0,0 +1,8142 @@ +{ + "name": "firebase-docker-image", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "firebase-tools": "latest" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser/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==", + "license": "Python-2.0" + }, + "node_modules/@apidevtools/json-schema-ref-parser/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==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@electric-sql/pglite": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.3.tgz", + "integrity": "sha512-JrvHOx9q0yvKEby0bK8qzGTVw6K+yEg8enxDWb2IwNKr5XZxRrBb+GNIqoAIP7yXyhRg5jcENWmdHmtnAT87vA==", + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.8.tgz", + "integrity": "sha512-MBWelYjUZThOBrktPU4beuuX4hrUdIPRgfLbTgltLMT6Chh2R7ATxHsT9Nr7L9fXUSYlZCyoIf+n8pis3uoiiw==", + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.3.3" + } + }, + "node_modules/@google-cloud/cloud-sql-connector": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@google-cloud/cloud-sql-connector/-/cloud-sql-connector-1.8.1.tgz", + "integrity": "sha512-BhPZB/pR6VTjfwS0S5EyjUdHod98rlcJs8tCin33z4yw8V9r2+N8JvmMdoJrbOnhEHgjcFnEx49iZe6XBOnYNg==", + "license": "Apache-2.0", + "dependencies": { + "@googleapis/sqladmin": "^29.0.0", + "gaxios": "^6.1.1", + "google-auth-library": "^9.2.0", + "p-throttle": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "license": "Apache-2.0", + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/pubsub": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-4.11.0.tgz", + "integrity": "sha512-xWxJAlyUGd6OPp97u8maMcI3xVXuHjxfwh6Dr7P/P+6NK9o446slJobsbgsmK0xKY4nTK8m5uuJrhEKapfZSmQ==", + "license": "Apache-2.0", + "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.9.0", + "@opentelemetry/semantic-conventions": "~1.30.0", + "arrify": "^2.0.0", + "extend": "^3.0.2", + "google-auth-library": "^9.3.0", + "google-gax": "^4.3.3", + "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/@googleapis/sqladmin": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/@googleapis/sqladmin/-/sqladmin-29.0.0.tgz", + "integrity": "sha512-gBbr+fTtZg1EElFsXgEIqrldSu/U9M8iOUKC5ob3Sjhd/dYw1HkWblwhrS9lvnTUi2B7vXrP9RJpRcDfkH0dxw==", + "license": "Apache-2.0", + "dependencies": { + "googleapis-common": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", + "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "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/@inquirer/checkbox": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.8.tgz", + "integrity": "sha512-d/QAsnwuHX2OPolxvYcgSj7A9DO9H6gVOy2DvBTx+P2LH2iRTo/RSGV3iwCzW024nP9hw98KIuDmdyhZQj1UQg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.12", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.12.tgz", + "integrity": "sha512-dpq+ielV9/bqgXRUbNH//KsY6WEw9DrGPmipkpmgC1Y46cwuBTNx7PXFWTjc3MQ+urcc0QxoVHcMI0FW4Ok0hg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.13", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.13.tgz", + "integrity": "sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==", + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.13.tgz", + "integrity": "sha512-WbicD9SUQt/K8O5Vyk9iC2ojq5RHoCLK6itpp2fHsWe44VxxcA9z3GTWlvjSTGmMQpZr+lbVmrxdHcumJoLbMA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.15.tgz", + "integrity": "sha512-4Y+pbr/U9Qcvf+N/goHzPEXiHH8680lM3Dr3Y9h9FFw4gHS+zVpbj8LfbKWIb/jayIB4aSO4pWiBTrBYWkvi5A==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", + "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.12.tgz", + "integrity": "sha512-xJ6PFZpDjC+tC1P8ImGprgcsrzQRsUh9aH3IZixm1lAZFK49UGHxM3ltFfuInN2kPYNfyoPRh+tU4ftsjPLKqQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.15.tgz", + "integrity": "sha512-xWg+iYfqdhRiM55MvqiTCleHzszpoigUpN5+t1OMcRkJrUrw7va3AzXaxvS+Ak7Gny0j2mFSTv2JJj8sMtbV2g==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.15.tgz", + "integrity": "sha512-75CT2p43DGEnfGTaqFpbDC2p2EEMrq0S+IRrf9iJvYreMy5mAWj087+mdKyLHapUEPLjN10mNvABpGbk8Wdraw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.5.3.tgz", + "integrity": "sha512-8YL0WiV7J86hVAxrh3fE5mDCzcTDe1670unmJRz6ArDgN+DBK1a0+rbnNWp4DUB5rPMwqD5ZP6YHl9KK1mbZRg==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.8", + "@inquirer/confirm": "^5.1.12", + "@inquirer/editor": "^4.2.13", + "@inquirer/expand": "^4.0.15", + "@inquirer/input": "^4.1.12", + "@inquirer/number": "^3.0.15", + "@inquirer/password": "^4.0.15", + "@inquirer/rawlist": "^4.1.3", + "@inquirer/search": "^3.0.15", + "@inquirer/select": "^4.2.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.3.tgz", + "integrity": "sha512-7XrV//6kwYumNDSsvJIPeAqa8+p7GJh7H5kRuxirct2cgOcSWwwNGoXDRgpNFbY/MG2vQ4ccIWCi8+IXXyFMZA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.15.tgz", + "integrity": "sha512-YBMwPxYBrADqyvP4nNItpwkBnGGglAvCLVW8u4pRmmvOsHUtCAUIMbUrLX5B3tFL1/WsLGdQ2HNzkqswMs5Uaw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.3.tgz", + "integrity": "sha512-OAGhXU0Cvh0PhLz9xTF/kx6g6x+sP+PcyTiLvCrewI99P3BBeexD+VbuwkNDvqGkk3y2h5ZiWLeRP7BFlhkUDg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", + "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": 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==", + "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", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "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", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "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==", + "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", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "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/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.13.1.tgz", + "integrity": "sha512-8q6+9aF0yA39/qWT/uaIj6zTpC+Qu07DnN/lb9mjoquCJsAh6l3HyYqc9O3t2j7GilseOQOQimLg7W3By6jqvg==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/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==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "license": "ISC", + "optional": 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.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "license": "ISC", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.30.0.tgz", + "integrity": "sha512-4VlGgo32k2EQ2wcCY3vEU28A0O13aOtHz3Xt2/2U5FAh9EfhD6t6DqL5Z6yAnRCntbTFDU4YfbpyzSlHNWycPw==", + "license": "Apache-2.0", + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/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==", + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", + "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@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": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.0.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.4.tgz", + "integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "license": "MIT", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz", + "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT" + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "license": "ISC", + "optional": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "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==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-styles/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ansi-styles/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "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==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "license": "MIT", + "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==", + "license": "MIT", + "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/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "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": "sha512-1Sd1LrodN0XYxYeZcN1J4xYZvmvTwD5tDWaPUGPIzH1mFsmzsPnVtd2exWhecMjtZk/wYWjNZJiD3b1SLCeJqg==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.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==", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth-connect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.1.0.tgz", + "integrity": "sha512-rKcWjfiRZ3p5WS9e5q6msXa07s6DaFAMXoyowV+mb2xQG+oYdw2QEUyKi0Xp95JvXzShlM+oGy5QuqSK6TfC1Q==", + "license": "MIT", + "dependencies": { + "tsscmp": "^1.0.6" + } + }, + "node_modules/basic-auth/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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", + "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "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", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "license": "MIT", + "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/boxen/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/boxen/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/boxen/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==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "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/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.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==", + "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-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==", + "license": "MIT", + "engines": { + "node": ">=8.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==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^4.0.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": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "optional": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "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" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "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==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/cjson": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/cjson/-/cjson-0.3.3.tgz", + "integrity": "sha512-yKNcXi/Mvi5kb1uK0sahubYiyfUO2EUgOp4NcY9+8NX5Xmc+4yeNogZuLFkpLBBj7/QI9MjRUIuXrV9XOw5kVg==", + "license": "MIT", + "dependencies": { + "json-parse-helpfulerror": "^1.0.3" + }, + "engines": { + "node": ">= 0.3.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/cli-highlight/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==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cli-highlight/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "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/cli-highlight/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "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/cli-highlight/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==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "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==", + "license": "ISC", + "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==", + "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-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "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", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "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==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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==", + "license": "MIT", + "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/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", + "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "license": "BSD-2-Clause", + "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/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "license": "MIT", + "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/connect/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "license": "MIT", + "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/connect/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==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/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==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "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==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/csv-parse": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.6.0.tgz", + "integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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": "sha512-Z+z8HiAvsGwmjqlphnHW5oz6yWlOwu6EQfFTjmeTWlDeda3FS2yv3jhq35TX/ewmsnqB+RX2IdsIOyjJCQN5tg==", + "license": "public domain" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "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", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/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==", + "license": "MIT" + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "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==", + "license": "MIT", + "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": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "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/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", + "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/exegesis": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/exegesis/-/exegesis-4.3.0.tgz", + "integrity": "sha512-V90IJQ4XYO1SfH5qdJTOijXkQTF3hSpSHHqlf7MstUMDKP22iAvi63gweFLtPZ4Gj3Wnh8RgJX5TGu0WiwTyDQ==", + "license": "MIT", + "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", + "qs": "^6.6.0", + "raw-body": "^2.3.3", + "semver": "^7.0.0" + }, + "engines": { + "node": ">=10.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==", + "license": "MIT", + "dependencies": { + "exegesis": "^4.1.0" + }, + "engines": { + "node": ">=6.0.0", + "npm": ">5.0.0" + } + }, + "node_modules/exegesis/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==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/exegesis/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/filesize": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.4.0.tgz", + "integrity": "sha512-mjFIpOHC4jbfcTfoh4rkWpI31mF7viw9ikj/JyLoKzqlwG/YsefKfvYlYhdYdg/9mtK2z1AzgN/0LvVQ3zdlSQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/firebase-tools": { + "version": "14.8.0", + "resolved": "https://registry.npmjs.org/firebase-tools/-/firebase-tools-14.8.0.tgz", + "integrity": "sha512-fz1m5LXs5e4WjS9bqe8yFUxz+grUtfnCEXCtRYRdgoofeY4av0DxW1PSxwLq5soPEBXj5ZR9CeSE6SrBui0WzQ==", + "license": "MIT", + "dependencies": { + "@electric-sql/pglite": "^0.3.3", + "@electric-sql/pglite-tools": "^0.2.8", + "@google-cloud/cloud-sql-connector": "^1.3.3", + "@google-cloud/pubsub": "^4.5.0", + "@inquirer/prompts": "^7.4.0", + "@modelcontextprotocol/sdk": "^1.10.2", + "abort-controller": "^3.0.0", + "ajv": "^8.17.1", + "ajv-formats": "3.0.1", + "archiver": "^7.0.0", + "async-lock": "1.4.1", + "body-parser": "^1.19.0", + "chokidar": "^3.6.0", + "cjson": "^0.3.1", + "cli-table3": "0.6.5", + "colorette": "^2.0.19", + "commander": "^5.1.0", + "configstore": "^5.0.1", + "cors": "^2.8.5", + "cross-env": "^7.0.3", + "cross-spawn": "^7.0.5", + "csv-parse": "^5.0.4", + "deep-equal-in-any-order": "^2.0.6", + "exegesis": "^4.2.0", + "exegesis-express": "^4.0.0", + "express": "^4.16.4", + "filesize": "^6.1.0", + "form-data": "^4.0.1", + "fs-extra": "^10.1.0", + "fuzzy": "^0.1.3", + "gaxios": "^6.7.0", + "glob": "^10.4.1", + "google-auth-library": "^9.11.0", + "ignore": "^7.0.4", + "js-yaml": "^3.14.1", + "jsonwebtoken": "^9.0.0", + "leven": "^3.1.0", + "libsodium-wrappers": "^0.7.10", + "lodash": "^4.17.21", + "lsofi": "1.0.0", + "marked": "^13.0.2", + "marked-terminal": "^7.0.0", + "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", + "pg-gateway": "^0.3.0-beta.4", + "pglite-2": "npm:@electric-sql/pglite@0.2.17", + "portfinder": "^1.0.32", + "progress": "^2.0.3", + "proxy-agent": "^6.3.0", + "retry": "^0.13.1", + "semver": "^7.5.2", + "sql-formatter": "^15.3.0", + "stream-chain": "^2.2.4", + "stream-json": "^1.7.3", + "superstatic": "^9.2.0", + "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.5.10", + "yaml": "^2.4.1", + "zod": "^3.24.3", + "zod-to-json-schema": "^3.24.5" + }, + "bin": { + "firebase": "lib/bin/firebase.js" + }, + "engines": { + "node": ">=20.0.0 || >=22.0.0" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/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==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-uri": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.4.tgz", + "integrity": "sha512-E1b1lFFLvLgak2whF2xDBcOy6NLVGZBqqjJjsIhvopKfWWEi64pLVTWWehV8KlLerZkfNTA95sTe2OdJKm1OzQ==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/get-uri/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "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==", + "license": "ISC", + "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": "sha512-ZwFh34WZhZX28ntCMAP1mwyAJkn8+Omagvt/GvA+JQM/qgT0+MR2NPF3vhvgdshfdvDyGZXs8fPXW84K32Wjuw==", + "license": "MIT" + }, + "node_modules/glob-slasher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glob-slasher/-/glob-slasher-1.0.1.tgz", + "integrity": "sha512-5MUzqFiycIKLMD1B0dYOE4hGgLLUZUNGGYO4BExdwT32wUwW3DBOE7lMQars7vB1q43Fb3Tyt+HmgLKsJhDYdg==", + "license": "MIT", + "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.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "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==", + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz", + "integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.10.9", + "@grpc/proto-loader": "^0.7.13", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.7.0", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis-common": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", + "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.7.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis-common/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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==", + "license": "ISC" + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/heap-js": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.6.0.tgz", + "integrity": "sha512-trFMIq3PATiFRiQmNNeHtsrkwYRByIXUbYNbotiY9RLVfMkdwZdd2eQ38mGt7BRiCKBaj1DyBAIHmm7mmXPuuw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-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==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/install-artifact-from-github": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.4.0.tgz", + "integrity": "sha512-+y6WywKZREw5rq7U2jvr2nmZpT7cbWbQQ0N/qfcseYnzHFz2cZz1Et52oY+XttYuYeTkI8Y+R2JNWj68MpQFSg==", + "license": "BSD-3-Clause", + "optional": true, + "bin": { + "install-from-cache": "bin/install-from-cache.js", + "save-to-github-cache": "bin/save-to-github-cache.js" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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==", + "license": "MIT" + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/is2": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.9.tgz", + "integrity": "sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==", + "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", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "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": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "license": "MIT" + }, + "node_modules/join-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/join-path/-/join-path-1.1.1.tgz", + "integrity": "sha512-jnt9OC34sLXMLJ6YfPQ2ZEKrR9mB5ZbSnQb4LPaOx1c5rTzxpR33L18jjp0r75mGGTJmsil3qwN1B5IBeTnSSA==", + "license": "MIT", + "dependencies": { + "as-array": "^2.0.0", + "url-join": "0.0.1", + "valid-url": "^1" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-parse-helpfulerror": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", + "integrity": "sha512-XgP0FGR77+QhUxjXkwOMkC94k3WtqEBfcnjWqhRd82qTat4SWKRE+9kUnynz/shm3I4ea2+qISvTIeGTNU7kJg==", + "license": "MIT", + "dependencies": { + "jju": "^1.1.0" + } + }, + "node_modules/json-ptr": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-ptr/-/json-ptr-3.1.1.tgz", + "integrity": "sha512-SiSJQ805W1sDUCD1+/t1/1BIrveq2Fe9HJqENxZmMCILmrPI7WhS/pePpIOx85v6/H2z1Vy7AI08GV2TzfXocg==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.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==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "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==", + "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/lazystream/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==", + "license": "MIT" + }, + "node_modules/lazystream/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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/libsodium": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz", + "integrity": "sha512-sZwRknt/tUpE2AwzHq3jEyUU5uvIZHtSssktXq7owd++3CSgn8RGrv6UZJJBpP7+iBghBqe7Z06/2M31rI2NKw==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.15.tgz", + "integrity": "sha512-E4anqJQwcfiC6+Yrl01C1m8p99wEhLmJSs0VQqST66SbQXXBoaJY0pF4BNjRYa/sOQAxx6lXAaAFIlx+15tXJQ==", + "license": "ISC", + "dependencies": { + "libsodium": "^0.7.15" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash._objecttypes": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz", + "integrity": "sha512-XpqGh1e7hhkOzftBfWE7zt+Yn9mVHFkDhicVttvKLsoCMLVVL+xTQjfjB4X4vtznauxv0QZ5ZAeqjvat0dh62Q==", + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isobject": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz", + "integrity": "sha512-sTebg2a1PoicYEZXD5PBdQcTlIJ6hUslrlWr7iV0O7n+i4596s2NQ9I5CaZ5FbXSfya/9WQsrYLANUJv9paYVA==", + "license": "MIT", + "dependencies": { + "lodash._objecttypes": "~2.4.1" + } + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, + "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==", + "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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/lsofi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lsofi/-/lsofi-1.0.0.tgz", + "integrity": "sha512-MKr9vM1MSm+TSKfI05IYxpKV1NCxpJaBLnELyIf784zYJ5KV9lGCE1EvpA2DtXDNM3fCuFeCwXUzim/fyQRi+A==", + "license": "MIT", + "dependencies": { + "is-number": "^2.1.0", + "through2": "^2.0.1" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/marked": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz", + "integrity": "sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/marked-terminal": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.3.0.tgz", + "integrity": "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "ansi-regex": "^6.1.0", + "chalk": "^5.4.1", + "cli-highlight": "^2.1.11", + "cli-table3": "^0.6.5", + "node-emoji": "^2.2.0", + "supports-hyperlinks": "^3.1.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "marked": ">=1 <16" + } + }, + "node_modules/marked-terminal/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "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==", + "license": "ISC", + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "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==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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==", + "license": "ISC", + "optional": 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==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.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==", + "license": "ISC", + "optional": 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==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.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==", + "license": "ISC", + "optional": 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==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", + "license": "BSD-3-Clause" + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", + "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/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==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nan": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", + "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", + "license": "MIT", + "optional": true + }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.2.0.tgz", + "integrity": "sha512-T0S1zqskVUSxcsSTkAsLc7xCycrRYmtDHadDinzocrThjyQCn5kMlEBSj6H4qDbgsIOSLmmlRIeb0lZXj+UArA==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "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==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "optional": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "yaml": "^2.2.1" + } + }, + "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==", + "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-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "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/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-throttle": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/p-throttle/-/p-throttle-7.0.0.tgz", + "integrity": "sha512-aio0v+S0QVkH1O+9x4dHtD4dgCExACcL+3EtNaGqC01GBudS9ijMuUsmN8OVScyV4OOp0jqdLShZFuSlbL/AsA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/pac-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/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==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "license": "MIT" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "BlueOak-1.0.0", + "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-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.2.tgz", + "integrity": "sha512-OtLWF0mKLmpxelOt9BqVq83QV6bTfsS0XLegIeAKqKjurRnRKie1Dc1iL89MugmSLhftxw6NNCyZhm1yQFLMEQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.2", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.6" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.6.tgz", + "integrity": "sha512-uxmJAnmIgmYgnSFzgOf2cqGQBzwnRYcrEgXuFjJNEkpedEIPBSEzxY7ph4uA9k1mI+l/GR0HjPNS6FKNZe8SBQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-gateway": { + "version": "0.3.0-beta.4", + "resolved": "https://registry.npmjs.org/pg-gateway/-/pg-gateway-0.3.0-beta.4.tgz", + "integrity": "sha512-CTjsM7Z+0Nx2/dyZ6r8zRsc3f9FScoD5UAOlfUx1Fdv/JOIWvRbF7gou6l6vP+uypXQVoYPgw8xZDXgMGvBa4Q==", + "license": "MIT" + }, + "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==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.2.tgz", + "integrity": "sha512-Ci7jy8PbaWxfsck2dwZdERcDG2A0MG8JoQILs+uZNjABFuBuItAZCWUNz8sXRDMoui24rJw7WlXqgpMdBSN/vQ==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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/pglite-2": { + "name": "@electric-sql/pglite", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.2.17.tgz", + "integrity": "sha512-qEpKRT2oUaWDH6tjRxLHjdzMqRUGYDnGZlKrnL4dJ77JVMcP2Hpo3NYnOSPKdZdeec57B6QPprCUFg0picx5Pw==", + "license": "Apache-2.0" + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/portfinder": { + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.37.tgz", + "integrity": "sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==", + "license": "MIT", + "dependencies": { + "async": "^3.2.6", + "debug": "^4.3.6" + }, + "engines": { + "node": ">= 10.12" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz", + "integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/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==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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==", + "license": "CC0-1.0" + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "license": "MIT", + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "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/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/re2": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.22.1.tgz", + "integrity": "sha512-E4J0EtgyNLdIr0wTg0dQPefuiqNY29KaLacytiUAYYRzxCG+zOkWoUygt1rI+TA1LrhN49/njrfSO1DHtVC5Vw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "install-artifact-from-github": "^1.4.0", + "nan": "^2.22.2", + "node-gyp": "^11.2.0" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "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/readable-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" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "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==", + "license": "ISC", + "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==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/registry-auth-token": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.0.tgz", + "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==", + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/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==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.12" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "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==", + "license": "MIT", + "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==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz", + "integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/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==", + "license": "MIT", + "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==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "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": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/sql-formatter": { + "version": "15.6.5", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.6.5.tgz", + "integrity": "sha512-fr4TyM1udCSrOHOmouotwUi8dxIDhSLpYNmPePGFVzxq8/i8jd828IapE49QXG7Gzkswxo5WwdAGnYX4YpKoTg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "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==", + "license": "Python-2.0" + }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "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==", + "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/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "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", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/superstatic": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/superstatic/-/superstatic-9.2.0.tgz", + "integrity": "sha512-QrJAJIpAij0jJT1nEwYTB0SzDi4k0wYygu6GxK0ko8twiQgfgaOAZ7Hu99p02MTAsGho753zhzSvsw8We4PBEQ==", + "license": "MIT", + "dependencies": { + "basic-auth-connect": "^1.1.0", + "commander": "^10.0.0", + "compression": "^1.7.0", + "connect": "^3.7.0", + "destroy": "^1.0.4", + "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.9.0", + "router": "^2.0.0", + "update-notifier-cjs": "^5.1.6" + }, + "bin": { + "superstatic": "lib/bin/server.js" + }, + "engines": { + "node": "18 || 20 || 22" + }, + "optionalDependencies": { + "re2": "^1.17.7" + } + }, + "node_modules/superstatic/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/superstatic/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "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": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "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==", + "license": "ISC", + "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.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "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==", + "license": "ISC", + "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-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/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==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "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==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/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==", + "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/through2/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==", + "license": "MIT" + }, + "node_modules/through2/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==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "license": "MIT", + "optional": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/to-regex-range/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==", + "license": "MIT", + "engines": { + "node": ">=0.12.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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.10" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, + "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==", + "license": "(MIT OR CC0-1.0)", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "license": "MIT" + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=12.18.2" + } + }, + "node_modules/universal-analytics/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/universal-analytics/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-notifier-cjs": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/update-notifier-cjs/-/update-notifier-cjs-5.1.7.tgz", + "integrity": "sha512-eZWTh8F+VCEoC4UIh0pKmh8h4izj65VvLhCpJpVefUxdYe0fU3GBrC4Sbh1AoWA/miNPAb6UVlp2fUQNsfp+3g==", + "license": "BSD-2-Clause", + "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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-join": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-0.0.1.tgz", + "integrity": "sha512-H6dnQ/yPAAVzMQRvEvyz01hhfQL5qRWSEt7BX8t9DqnPw9BjMb64fjIRq76Uvf1hkHp+mTZvEVJ5guXOT0Xqaw==", + "license": "MIT" + }, + "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==", + "license": "BSD" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "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", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "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-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "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==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/write-file-atomic/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==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "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==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zod": { + "version": "3.25.67", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", + "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/scripts/publish/firebase-docker-image/package.json b/scripts/publish/firebase-docker-image/package.json new file mode 100644 index 00000000000..ac50a6e0c1f --- /dev/null +++ b/scripts/publish/firebase-docker-image/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "firebase-tools": "latest" + } +} diff --git a/scripts/publish/firebase-docker-image/run.sh b/scripts/publish/firebase-docker-image/run.sh new file mode 100644 index 00000000000..19f0deb2a01 --- /dev/null +++ b/scripts/publish/firebase-docker-image/run.sh @@ -0,0 +1,6 @@ +## Script for testing Docker image creation without running a full release. +PROJECT_ID=$1 +npm i +gcloud --project $PROJECT_ID \ + builds \ + submit \ No newline at end of file diff --git a/scripts/publish/run.sh b/scripts/publish/run.sh index 4adb35b7206..8737cdc9ea4 100755 --- a/scripts/publish/run.sh +++ b/scripts/publish/run.sh @@ -2,21 +2,33 @@ set -e printusage() { - echo "run.sh " + echo "run.sh [branch]" echo "" echo "Arguments:" - echo " version: 'patch', 'minor', or 'major'." + echo " version: 'patch', 'minor', 'major', 'artifactsOnly', or 'preview'" + echo " branch: required if version is 'preview'" } VERSION=$1 +BRANCH=$2 if [[ $VERSION == "" ]]; then printusage exit 1 -elif [[ ! ($VERSION == "patch" || $VERSION == "minor" || $VERSION == "major") ]]; then +elif [[ $VERSION == "preview" ]]; then + if [[ $BRANCH == "" ]]; then + printusage + exit 1 + fi +elif [[ ! ($VERSION == "patch" || $VERSION == "minor" || $VERSION == "major" || $VERSION == "artifactsOnly") ]]; then printusage exit 1 fi +SUBSTITUTIONS="_VERSION=$VERSION" +if [[ $VERSION == "preview" ]]; then + SUBSTITUTIONS="$SUBSTITUTIONS,_BRANCH=$BRANCH" +fi + THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" cd "$THIS_DIR" @@ -24,5 +36,6 @@ cd "$THIS_DIR" gcloud --project fir-tools-builds \ builds \ submit \ - --substitutions=_VERSION=$VERSION \ + --machine-type=e2-highcpu-8 \ + --substitutions=$SUBSTITUTIONS \ . \ 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..31f7e2b1926 --- /dev/null +++ b/scripts/storage-emulator-integration/conformance/persistence.test.ts @@ -0,0 +1,146 @@ +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", + timeout: TEST_SETUP_TIMEOUT - 5000, + }); + 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..1b19adf5fef 100755 --- a/scripts/storage-emulator-integration/run.sh +++ b/scripts/storage-emulator-integration/run.sh @@ -2,13 +2,29 @@ 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 + +# Brief sleep between tests to make sure emulators shut down fully. +sleep 5 + +mocha scripts/storage-emulator-integration/rules/*.test.ts + +sleep 5 + +mocha scripts/storage-emulator-integration/import/tests.ts + +sleep 5 + +mocha scripts/storage-emulator-integration/multiple-targets/tests.ts + +sleep 5 + +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 def01c47149..b1a51eb9d22 100644 --- a/scripts/storage-emulator-integration/storage.rules +++ b/scripts/storage-emulator-integration/storage.rules @@ -1,8 +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, write; } } } diff --git a/scripts/storage-emulator-integration/tests.ts b/scripts/storage-emulator-integration/tests.ts deleted file mode 100644 index 95a2183544a..00000000000 --- a/scripts/storage-emulator-integration/tests.ts +++ /dev/null @@ -1,1052 +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"]); - - admin.initializeApp({ - credential: admin.credential.applicationDefault(), - }); - } else { - admin.initializeApp({ - credential: admin.credential.cert(readJson(SERVICE_ACCOUNT_KEY)), - }); - } - - 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 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, - }); - }); - - // Skipping large upload test for now. - 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()", () => { - 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("#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", - }); - }); - }); - - 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 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 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: string = ""; - 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", - }); - }); - - it("#setMetadata()", 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("#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.deep.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).to.deep.equal(tokens[1].toString()); - }); - }); - - 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).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..f42000b7f7c --- /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 = 120000; +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 74fea903e19..9ae3e3f4249 100644 --- a/scripts/test-functions-deploy.js +++ b/scripts/test-functions-deploy.js @@ -20,10 +20,11 @@ var cloudfunctions = require("../lib/gcp/cloudfunctions"); var api = require("../lib/api"); var scopes = require("../lib/scopes"); var { configstore } = require("../lib/configstore"); -var extractTriggers = require("../lib/extractTriggers"); +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 () { @@ -95,9 +96,9 @@ var postTest = function (errored) { var checkFunctionsListMatch = function (expectedFunctions) { var deployedFunctions; return cloudfunctions - .list(projectId, region) + .listFunctions(projectId, region) .then(function (result) { - deployedFunctions = _.map(result, "functionName"); + 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,25 +155,7 @@ var testDeleteWithFilter = function () { console.log(stdout); expect(err).to.be.null; resolve(checkFunctionsListMatch(["httpsAction"])); - } - ); - }); -}; - -var testUnknownFilter = function () { - return new Promise(function (resolve) { - exec( - "> functions/index.js &&" + - `${localFirebase} deploy --only functions:unknownFilter --project=${projectId}`, - { cwd: tmpDir }, - function (err, stdout) { - console.log(stdout); - expect(stdout).to.contain( - "the following filters were specified but do not match any functions in the project: unknownFilter" - ); - expect(err).to.be.null; - resolve(); - } + }, ); }); }; @@ -209,10 +192,11 @@ var writeToDB = function (path) { }; var sendHttpRequest = function (message) { + const url = new URL(httpsTrigger); return api - .request("POST", httpsTrigger, { + .request("POST", url.pathname, { data: message, - origin: "", + origin: url.origin, }) .then(function (resp) { expect(resp.status).to.equal(200); @@ -222,7 +206,7 @@ var sendHttpRequest = function (message) { var publishPubsub = function (topic) { var uuid = getUuid(); - var message = new Buffer(uuid).toString("base64"); + var message = Buffer.from(uuid).toString("base64"); return api .request("POST", `/v1/projects/${projectId}/topics/${topic}:publish`, { auth: true, @@ -289,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; }); @@ -329,13 +313,9 @@ var main = function () { console.log(clc.green("\u2713 Test passed: creating functions with filters")); return testDeleteWithFilter(); }) - .then(function () { - console.log(clc.green("\u2713 Test passed: deleting functions with filters")); - return testUnknownFilter(); - }) .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 new file mode 100755 index 00000000000..201fb09c954 --- /dev/null +++ b/scripts/test-functions-env.js @@ -0,0 +1,109 @@ +#!/usr/bin/env node +"use strict"; + +/** + * Integration test for functions env var support. Run: + * node ./test-functions-env.js + * + * If parameter ommited: + * - projectId defaults to `functions-integration-test` + * - region defaults to `us-central1` + */ + +const expect = require("chai").expect; +const execSync = require("child_process").execSync; +const fs = require("fs-extra"); +const path = require("path"); +const tmp = require("tmp"); + +const api = require("../lib/api"); +const cloudfunctions = require("../lib/gcp/cloudfunctions"); +const { configstore } = require("../lib/configstore"); +const scopes = require("../lib/scopes"); + +const source = __dirname + "/assets/functions_to_test_minimal.js"; +const functionTarget = "httpsAction"; +const projectId = process.argv[2] || "functions-integration-test"; +const region = process.argv[3] || "us-central1"; +const localFirebase = __dirname + "/../lib/bin/firebase.js"; +const projectDir = __dirname + "/test-project"; + +let tmpDir; +let functionsSource; + +function preTest() { + const dir = tmp.dirSync({ prefix: "envtest_" }); + tmpDir = dir.name; + functionsSource = tmpDir + "/functions"; + fs.copySync(projectDir, tmpDir); + execSync("npm install", { cwd: functionsSource, stdio: "ignore", stderr: "ignore" }); + api.setRefreshToken(configstore.get("tokens").refresh_token); + api.setScopes(scopes.CLOUD_PLATFORM); + fs.copySync(source, functionsSource + "/index.js"); + execSync(`${localFirebase} --open-sesame dotenv`, { cwd: tmpDir }); + console.log("Done pretest prep."); +} + +function postTest() { + fs.remove(tmpDir); + execSync(`${localFirebase} functions:delete ${functionTarget} --project=${projectId} -f`); + console.log("Done post-test cleanup."); +} + +async function expectEnvs(envs) { + const fns = await cloudfunctions.listFunctions(projectId, region); + const fn = fns.find((fn) => fn.name.includes(functionTarget)); + + const gotEnvs = fn.environmentVariables; + // Remove system-provided environment variables. + delete gotEnvs.GCLOUD_PROJECT; + delete gotEnvs.FIREBASE_CONFIG; + + expect(gotEnvs).to.be.deep.equals(envs); + console.log("PASS"); +} + +async function deployAndCompare(expected) { + execSync(`${localFirebase} deploy --only functions --project=${projectId}`, { cwd: tmpDir }); + await expectEnvs(expected); +} + +async function runTest(description, envFiles, expected) { + console.log("============================"); + console.log(`Running test: ${description}`); + + const toCleanup = []; + for (const [targetFile, data] of Object.entries(envFiles)) { + const fullPath = path.join(functionsSource, targetFile); + fs.writeFileSync(fullPath, data); + toCleanup.push(fullPath); + } + try { + await deployAndCompare(expected); + } finally { + for (const f of toCleanup) { + fs.unlinkSync(f); + } + } +} + +(async () => { + try { + preTest(); + await runTest( + "Inject environment variables from .env", + { ".env": "FOO=foo\nBAR=bar\nCAR=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" }, + ); + console.log("Success"); + } catch (err) { + console.log("Error: " + err); + } finally { + postTest(); + } +})(); 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 6e241eacd6d..e37ee130128 100644 --- a/scripts/triggers-end-to-end-tests/firebase.json +++ b/scripts/triggers-end-to-end-tests/firebase.json @@ -3,7 +3,24 @@ "firestore": { "rules": "firestore.rules" }, - "functions": {}, + "storage": { + "rules": "storage.rules" + }, + "functions": [ + { + "codebase": "triggers", + "source": "triggers" + }, + { + "codebase": "v1", + "source": "v1" + }, + { + "codebase": "v2", + "source": "v2" + } + + ], "emulators": { "hub": { "port": 4000 @@ -22,6 +39,9 @@ }, "auth": { "port": 9099 + }, + "storage": { + "port": 9199 } } } 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 efbb927bb9b..00000000000 --- a/scripts/triggers-end-to-end-tests/functions/index.js +++ /dev/null @@ -1,134 +0,0 @@ -const admin = require("firebase-admin"); -const functions = require("firebase-functions"); -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. - */ -const RTDB_FUNCTION_LOG = "========== RTDB FUNCTION =========="; -const FIRESTORE_FUNCTION_LOG = "========== FIRESTORE FUNCTION =========="; -const PUBSUB_FUNCTION_LOG = "========== PUBSUB FUNCTION =========="; -const AUTH_FUNCTION_LOG = "========== AUTH FUNCTION =========="; - -/* - * 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 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.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; -}); 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 ba56a99b0cf..00000000000 --- a/scripts/triggers-end-to-end-tests/functions/package.json +++ /dev/null @@ -1,17 +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.11.0" - }, - "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 57fe153926b..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 -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/storage.rules b/scripts/triggers-end-to-end-tests/storage.rules new file mode 100644 index 00000000000..a7db6961cad --- /dev/null +++ b/scripts/triggers-end-to-end-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/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 174b35185d1..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,7 +38,7 @@ function readConfig(): FrameworkOptions { return JSON.parse(data); } -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; @@ -55,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, }); @@ -84,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( @@ -94,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( @@ -103,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); @@ -113,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); }); @@ -126,444 +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); + describe("https triggers", () => { + it("should handle parallel requests", async function (this) { + this.timeout(TEST_SETUP_TIMEOUT); - const response = await test.writeToRtdb(); - expect(response.status).to.equal(200); - }); + const [resp1, resp2] = await Promise.all([ + test.invokeHttpFunction("httpsv2reaction"), + test.invokeHttpFunction("httpsv2reaction"), + ]); - 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)); + expect(resp1.status).to.eq(200); + expect(resp2.status).to.eq(200); + }); }); - 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("database and firestore emulator triggers", () => { + it("should write to the database emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); -describe("pubsub emulator function triggers", () => { - let test: TriggerEndToEndTest; + const response = await test.writeToRtdb(); + expect(response.status).to.equal(200); + }); - before(async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); + it("should write to the firestore emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT * 2); - expect(FIREBASE_PROJECT).to.exist.and.not.be.empty; + const response = await test.writeToFirestore(); + expect(response.status).to.equal(200); - const config = readConfig(); - test = new TriggerEndToEndTest(FIREBASE_PROJECT, __dirname, config); - await test.startEmulators(["--only", "functions,pubsub"]); - }); + /* + * 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)); + }); - after(async function (this) { - this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); - await test.stopEmulators(); + 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 write to the pubsub emulator", async function (this) { - this.timeout(EMULATOR_TEST_TIMEOUT); + describe("pubsub emulator triggered functions", () => { + it("should write to the pubsub emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); - const response = await test.writeToPubsub(); - expect(response.status).to.equal(200); - await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); - }); + const response = await test.writeToPubsub(); + 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(1); - }); + 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); + 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)); - }); + 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); + it("should have have triggered cloud functions", () => { + expect(test.pubsubTriggerCount).to.equal(2); + }); }); -}); -describe("auth emulator function triggers", () => { - let test: TriggerEndToEndTest; + 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)); + }); - before(async function (this) { - this.timeout(TEST_SETUP_TIMEOUT); + it("should have have triggered cloud functions", () => { + expect(test.authTriggerCount).to.equal(1); + }); - expect(FIREBASE_PROJECT).to.exist.and.not.be.empty; + 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)); + }); - const config = readConfig(); - test = new TriggerEndToEndTest(FIREBASE_PROJECT, __dirname, config); - await test.startEmulators(["--only", "functions,auth"]); - }); + 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); + }); - after(async function (this) { - this.timeout(EMULATORS_SHUTDOWN_DELAY_MS); - await test.stopEmulators(); - }); + 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 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 triggered cloud functions", () => { + expect(test.authBlockingSignInV2TriggerCount).to.equal(2); + }); }); - it("should have have triggered cloud functions", () => { - expect(test.authTriggerCount).to.equal(1); - }); -}); + describe("storage emulator triggered functions", () => { + it("should write to the default bucket of storage emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); -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); - } - ); + const response = await test.writeToDefaultStorage(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + }); - // 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 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(); + }); - await importCLI.stop(); + it("should write to a specific bucket of storage emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); - expect(true).to.be.true; - }); + const response = await test.writeToSpecificStorageBucket(); + expect(response.status).to.equal(200); + await new Promise((resolve) => setTimeout(resolve, EMULATORS_WRITE_DELAY_MS)); + }); - 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 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(); + }); - // 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" - ); + it("should write and update metadata from the default bucket of the storage emulator", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); - // 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); - } - ); + const response = await test.updateMetadataDefaultStorage(); + 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 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(); + }); - // 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 update metadata from a specific 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.updateMetadataSpecificStorageBucket(); + 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 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(); + }); + + it("should write and delete from the default bucket of the 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 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)); + describe("callable functions", () => { + it("should make a call to v1 callable function", async function (this) { + this.timeout(EMULATOR_TEST_TIMEOUT); + + 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" }); + }); - // 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 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, - }, - }); - - 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 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 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, - }, + describe("disable/enableBackgroundTriggers", () => { + before(() => { + test.resetCounts(); }); - 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); - } - ); + 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); + }); - await importCLI.stop(); + 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..9a990de04b6 --- /dev/null +++ b/scripts/triggers-end-to-end-tests/triggers/package-lock.json @@ -0,0 +1,6788 @@ +{ + "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": "^5.1.0" + }, + "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.2.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.2.1.tgz", + "integrity": "sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==", + "license": "MIT", + "dependencies": { + "text-decoding": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "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.8.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.8.0.tgz", + "integrity": "sha512-JRpk06SmZXLGz0pNx1x7yU3YhkUXheKgH5hbDZ4kMsdhtfV5qPLJLRI4wv69K0cZorIk+zTMOwptue7hizo0eA==", + "license": "Apache-2.0", + "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/firestore/node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@google-cloud/firestore/node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "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.12.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.12.0.tgz", + "integrity": "sha512-78nNAY7iiZ4O/BouWMWTD/oSF2YtYgYB3GZirn0To6eBOugjXVoK+GXgUXOl+HlqbAOyHxAVXOlsj3snfbQ1dw==", + "license": "Apache-2.0", + "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/@google-cloud/paginator": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.7.tgz", + "integrity": "sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "MIT", + "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/@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==", + "license": "MIT", + "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.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "license": "MIT", + "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==", + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/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/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.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==", + "license": "MIT", + "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==", + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", + "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "MIT", + "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/fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "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.11.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.11.1.tgz", + "integrity": "sha512-UyEbq+3u6jWzCYbUntv/HuJiTixwh36G1R9j0v71mSvGAx/YZEWEW7uSGLYxBYE6ckVRQoKMr40PYUEzrm/4dg==", + "license": "Apache-2.0", + "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-admin/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==", + "license": "Apache-2.0" + }, + "node_modules/firebase-admin/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==", + "license": "Apache-2.0" + }, + "node_modules/firebase-admin/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==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-admin/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==", + "license": "Apache-2.0", + "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-admin/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==", + "license": "Apache-2.0", + "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-admin/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==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-functions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.1.1.tgz", + "integrity": "sha512-KkyKZE98Leg/C73oRyuUYox04PQeeBThdygMfeX+7t1cmKWYKa/ZieYa89U8GHgED+0mF7m7wfNZOfbURYxIKg==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^11.10.0 || ^12.0.0" + } + }, + "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-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==", + "license": "Apache-2.0" + }, + "node_modules/firebase-functions/node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/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==", + "license": "MIT", + "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==", + "license": "MIT", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "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": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/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": "3.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "license": "MIT", + "dependencies": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "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==", + "license": "MIT" + }, + "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.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/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==", + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "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==", + "license": "MIT", + "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/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT", + "optional": true + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "license": "MIT", + "optional": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT", + "optional": true + }, + "node_modules/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.3", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.3.tgz", + "integrity": "sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww==", + "license": "Apache-2.0", + "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==", + "license": "MIT" + }, + "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.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/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==", + "license": "MIT", + "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.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/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.8.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.8.0.tgz", + "integrity": "sha512-JRpk06SmZXLGz0pNx1x7yU3YhkUXheKgH5hbDZ4kMsdhtfV5qPLJLRI4wv69K0cZorIk+zTMOwptue7hizo0eA==", + "optional": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.7", + "protobufjs": "^7.2.5" + }, + "dependencies": { + "long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "optional": true + }, + "protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "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.12.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.12.0.tgz", + "integrity": "sha512-78nNAY7iiZ4O/BouWMWTD/oSF2YtYgYB3GZirn0To6eBOugjXVoK+GXgUXOl+HlqbAOyHxAVXOlsj3snfbQ1dw==", + "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", + "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" + }, + "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==" + }, + "@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.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "@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": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "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/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "requires": { + "@types/mime": "^1", + "@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.1", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", + "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==", + "optional": true, + "requires": { + "punycode": "^1.4.1" + } + }, + "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==" + }, + "fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "optional": true, + "requires": { + "strnum": "^1.0.5" + } + }, + "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.11.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.11.1.tgz", + "integrity": "sha512-UyEbq+3u6jWzCYbUntv/HuJiTixwh36G1R9j0v71mSvGAx/YZEWEW7uSGLYxBYE6ckVRQoKMr40PYUEzrm/4dg==", + "requires": { + "@fastify/busboy": "^1.2.1", + "@firebase/database-compat": "^0.3.4", + "@firebase/database-types": "^0.10.4", + "@google-cloud/firestore": "^6.8.0", + "@google-cloud/storage": "^6.9.5", + "@types/node": ">=12.12.47", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.0.1", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + }, + "dependencies": { + "@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.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "requires": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "@firebase/database": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz", + "integrity": "sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==", + "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" + } + }, + "@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==", + "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" + } + }, + "@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==", + "requires": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "@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.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "requires": { + "tslib": "^2.1.0" + } + } + } + }, + "firebase-functions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.1.1.tgz", + "integrity": "sha512-KkyKZE98Leg/C73oRyuUYox04PQeeBThdygMfeX+7t1cmKWYKa/ZieYa89U8GHgED+0mF7m7wfNZOfbURYxIKg==", + "requires": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "protobufjs": "^7.2.2" + }, + "dependencies": { + "long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "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" + } + } + } + }, + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==" + }, + "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": "3.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "requires": { + "@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" + }, + "dependencies": { + "@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "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.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "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" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "optional": true + }, + "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==" + }, + "strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "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==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "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, + "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.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + }, + "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..0c36db5c2b6 --- /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": "^5.1.0" + }, + "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..fb3d0589d2d --- /dev/null +++ b/scripts/triggers-end-to-end-tests/v1/package-lock.json @@ -0,0 +1,5597 @@ +{ + "name": "functions", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "functions", + "dependencies": { + "@firebase/database-compat": "0.1.2", + "firebase-admin": "^11.0.0", + "firebase-functions": "^5.1.0" + }, + "devDependencies": { + "firebase-functions-test": "^0.2.0" + }, + "engines": { + "node": "20" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/types": "^7.25.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.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==", + "license": "MIT", + "dependencies": { + "text-decoding": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "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.8.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.8.0.tgz", + "integrity": "sha512-JRpk06SmZXLGz0pNx1x7yU3YhkUXheKgH5hbDZ4kMsdhtfV5qPLJLRI4wv69K0cZorIk+zTMOwptue7hizo0eA==", + "license": "Apache-2.0", + "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/firestore/node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.8.22", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.22.tgz", + "integrity": "sha512-oAjDdN7fzbUi+4hZjKG96MR6KTEubAeMpQEb+77qy+3r0Ua5xTFuie6JOLr4ZZgl5g+W5/uRTS2M1V8mVAFPuA==", + "license": "Apache-2.0", + "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.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "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/@jsdoc/salty": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", + "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", + "license": "Apache-2.0", + "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/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.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.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/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==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@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==", + "license": "MIT", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0", + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", + "optional": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "optional": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "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.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/ent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", + "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "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==", + "license": "MIT", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "Apache-2.0", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "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.11.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.11.1.tgz", + "integrity": "sha512-UyEbq+3u6jWzCYbUntv/HuJiTixwh36G1R9j0v71mSvGAx/YZEWEW7uSGLYxBYE6ckVRQoKMr40PYUEzrm/4dg==", + "license": "Apache-2.0", + "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-admin/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==", + "license": "Apache-2.0" + }, + "node_modules/firebase-admin/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==", + "license": "Apache-2.0" + }, + "node_modules/firebase-admin/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==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-admin/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==", + "license": "Apache-2.0", + "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-admin/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==", + "license": "Apache-2.0", + "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-admin/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==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-admin/node_modules/@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/firebase-functions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.1.1.tgz", + "integrity": "sha512-KkyKZE98Leg/C73oRyuUYox04PQeeBThdygMfeX+7t1cmKWYKa/ZieYa89U8GHgED+0mF7m7wfNZOfbURYxIKg==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^11.10.0 || ^12.0.0" + } + }, + "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==", + "license": "ISC", + "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==", + "license": "MIT", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "ISC", + "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==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "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.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", + "integrity": "sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "deprecated": "Package is no longer maintained", + "license": "MIT", + "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==", + "license": "ISC", + "optional": true + }, + "node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "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==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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": "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "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==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "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==", + "license": "Unlicense", + "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==", + "license": "MIT", + "optional": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "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==", + "license": "ISC", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": 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/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.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==", + "license": "ISC", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.1.tgz", + "integrity": "sha512-AwAuY4g9nxx0u52DnSMkqqgyLHaW/XaPLtaAo3y/ZCfeaQB/g4YDH4kb8Wc/mWzWvu0YjOznVnfn373MVZZrgw==", + "license": "Apache-2.0", + "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==", + "license": "BSD-3-Clause", + "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/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/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT", + "optional": true + }, + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/retry-request/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/retry-request/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/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==", + "license": "BSD-3-Clause", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT", + "optional": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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==", + "license": "MIT", + "optional": 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==", + "license": "MIT", + "optional": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT", + "optional": true + }, + "node_modules/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==", + "license": "MIT", + "optional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "Apache-2.0", + "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==", + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "license": "MIT", + "optional": true, + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "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==", + "license": "MIT", + "optional": true + }, + "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==", + "license": "MIT", + "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": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT", + "optional": true + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "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" + ], + "license": "MIT", + "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==", + "license": "BSD-2-Clause", + "optional": true + }, + "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==", + "license": "MIT", + "optional": true, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "optional": true + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "optional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "optional": true + }, + "@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "optional": true + }, + "@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "optional": true, + "requires": { + "@babel/types": "^7.25.6" + } + }, + "@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "optional": true, + "requires": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + } + }, + "@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": { + "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.8.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-6.8.0.tgz", + "integrity": "sha512-JRpk06SmZXLGz0pNx1x7yU3YhkUXheKgH5hbDZ4kMsdhtfV5qPLJLRI4wv69K0cZorIk+zTMOwptue7hizo0eA==", + "optional": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.7", + "protobufjs": "^7.2.5" + }, + "dependencies": { + "protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "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==", + "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.12.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.12.0.tgz", + "integrity": "sha512-78nNAY7iiZ4O/BouWMWTD/oSF2YtYgYB3GZirn0To6eBOugjXVoK+GXgUXOl+HlqbAOyHxAVXOlsj3snfbQ1dw==", + "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", + "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" + }, + "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.22", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.22.tgz", + "integrity": "sha512-oAjDdN7fzbUi+4hZjKG96MR6KTEubAeMpQEb+77qy+3r0Ua5xTFuie6JOLr4ZZgl5g+W5/uRTS2M1V8mVAFPuA==", + "optional": true, + "requires": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + } + }, + "@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==", + "optional": true, + "requires": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "dependencies": { + "protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "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" + } + } + } + }, + "@jsdoc/salty": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", + "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", + "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==" + }, + "@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.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "@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.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "requires": { + "@types/node": "*" + } + }, + "@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==", + "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": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "optional": true, + "requires": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "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/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "requires": { + "@types/mime": "^1", + "@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.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "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" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.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" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", + "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==", + "optional": true, + "requires": { + "punycode": "^1.4.1" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "optional": true + }, + "escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "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.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 + }, + "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, + "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 + }, + "fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "optional": true, + "requires": { + "strnum": "^1.0.5" + } + }, + "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.11.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.11.1.tgz", + "integrity": "sha512-UyEbq+3u6jWzCYbUntv/HuJiTixwh36G1R9j0v71mSvGAx/YZEWEW7uSGLYxBYE6ckVRQoKMr40PYUEzrm/4dg==", + "requires": { + "@fastify/busboy": "^1.2.1", + "@firebase/database-compat": "^0.3.4", + "@firebase/database-types": "^0.10.4", + "@google-cloud/firestore": "^6.8.0", + "@google-cloud/storage": "^6.9.5", + "@types/node": ">=12.12.47", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.0.1", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + }, + "dependencies": { + "@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.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "requires": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "@firebase/database": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz", + "integrity": "sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==", + "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" + } + }, + "@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==", + "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" + } + }, + "@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==", + "requires": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "@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.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "requires": { + "tslib": "^2.1.0" + } + } + } + }, + "firebase-functions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.1.1.tgz", + "integrity": "sha512-KkyKZE98Leg/C73oRyuUYox04PQeeBThdygMfeX+7t1cmKWYKa/ZieYa89U8GHgED+0mF7m7wfNZOfbURYxIKg==", + "requires": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "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.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "optional": true, + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + } + }, + "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, + "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.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", + "integrity": "sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==", + "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.3.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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==" + }, + "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.3", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.3.tgz", + "integrity": "sha512-Nu7Sf35kXJ1MWDZIMAuATRQTg1iIPdzh7tqJ6jjvaU/GfDf+qi5UV8zJR3Mo+/pYFvm8mzay4+6O5EWigaQBQw==", + "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" + } + }, + "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.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "requires": { + "@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" + }, + "dependencies": { + "@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "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": "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==", + "optional": true, + "requires": { + "uc.micro": "^2.0.0" + } + }, + "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": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "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.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "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" + } + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "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.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "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-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.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, + "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" + } + }, + "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" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "optional": true + }, + "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==", + "optional": true + }, + "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.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true + } + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "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 + }, + "strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "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.3", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.3.tgz", + "integrity": "sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww==", + "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.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "optional": true + }, + "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==", + "optional": true + }, + "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==", + "optional": true + }, + "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": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "optional": true + }, + "uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "optional": true + }, + "underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "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.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + }, + "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==", + "optional": true + }, + "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==", + "optional": true, + "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..446f4cf351e --- /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": "^5.1.0" + }, + "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..60a8218ba2d --- /dev/null +++ b/scripts/triggers-end-to-end-tests/v2/package-lock.json @@ -0,0 +1,5259 @@ +{ + "name": "functions", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "functions", + "dependencies": { + "firebase-admin": "^11.0.0", + "firebase-functions": "^5.1.0" + }, + "devDependencies": { + "firebase-functions-test": "^0.2.0" + }, + "engines": { + "node": "20" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/types": "^7.25.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.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==", + "license": "MIT", + "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==", + "license": "Apache-2.0" + }, + "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==", + "license": "Apache-2.0" + }, + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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/firestore/node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.8.22", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.22.tgz", + "integrity": "sha512-oAjDdN7fzbUi+4hZjKG96MR6KTEubAeMpQEb+77qy+3r0Ua5xTFuie6JOLr4ZZgl5g+W5/uRTS2M1V8mVAFPuA==", + "license": "Apache-2.0", + "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.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "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/@jsdoc/salty": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", + "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", + "license": "Apache-2.0", + "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/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.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.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/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==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@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==", + "license": "MIT", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0", + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", + "optional": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "optional": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "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.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/ent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", + "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "optional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "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==", + "license": "MIT", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "Apache-2.0", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "BSD-2-Clause", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/finalhandler": { + "version": "1.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.11.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.11.1.tgz", + "integrity": "sha512-UyEbq+3u6jWzCYbUntv/HuJiTixwh36G1R9j0v71mSvGAx/YZEWEW7uSGLYxBYE6ckVRQoKMr40PYUEzrm/4dg==", + "license": "Apache-2.0", + "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": "5.1.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.1.1.tgz", + "integrity": "sha512-KkyKZE98Leg/C73oRyuUYox04PQeeBThdygMfeX+7t1cmKWYKa/ZieYa89U8GHgED+0mF7m7wfNZOfbURYxIKg==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^11.10.0 || ^12.0.0" + } + }, + "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==", + "license": "ISC", + "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==", + "license": "MIT", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "ISC", + "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==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "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.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", + "integrity": "sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "deprecated": "Package is no longer maintained", + "license": "MIT", + "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==", + "license": "ISC", + "optional": true + }, + "node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "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==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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": "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "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==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "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==", + "license": "Unlicense", + "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==", + "license": "MIT", + "optional": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "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==", + "license": "ISC", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": 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/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.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==", + "license": "ISC", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.1.tgz", + "integrity": "sha512-AwAuY4g9nxx0u52DnSMkqqgyLHaW/XaPLtaAo3y/ZCfeaQB/g4YDH4kb8Wc/mWzWvu0YjOznVnfn373MVZZrgw==", + "license": "Apache-2.0", + "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==", + "license": "BSD-3-Clause", + "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/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/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT", + "optional": true + }, + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^4.1.1", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/retry-request/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/retry-request/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/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==", + "license": "BSD-3-Clause", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT", + "optional": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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==", + "license": "MIT", + "optional": 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==", + "license": "MIT", + "optional": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT", + "optional": true + }, + "node_modules/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==", + "license": "MIT", + "optional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "Apache-2.0", + "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==", + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "license": "MIT", + "optional": true, + "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==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "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==", + "license": "MIT", + "optional": true + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "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==", + "license": "MIT", + "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": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT", + "optional": true + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "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" + ], + "license": "MIT", + "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==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "optional": true, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "optional": true + }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "optional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "optional": true + }, + "@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "optional": true + }, + "@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "optional": true, + "requires": { + "@babel/types": "^7.25.6" + } + }, + "@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "optional": true, + "requires": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + } + }, + "@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.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "requires": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "@firebase/database": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz", + "integrity": "sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==", + "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" + } + }, + "@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==", + "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" + } + }, + "@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==", + "requires": { + "@firebase/app-types": "0.9.0", + "@firebase/util": "1.9.3" + } + }, + "@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.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@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, + "requires": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^3.5.7", + "protobufjs": "^7.2.5" + }, + "dependencies": { + "protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "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==", + "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.12.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-6.12.0.tgz", + "integrity": "sha512-78nNAY7iiZ4O/BouWMWTD/oSF2YtYgYB3GZirn0To6eBOugjXVoK+GXgUXOl+HlqbAOyHxAVXOlsj3snfbQ1dw==", + "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", + "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" + }, + "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.22", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.22.tgz", + "integrity": "sha512-oAjDdN7fzbUi+4hZjKG96MR6KTEubAeMpQEb+77qy+3r0Ua5xTFuie6JOLr4ZZgl5g+W5/uRTS2M1V8mVAFPuA==", + "optional": true, + "requires": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + } + }, + "@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==", + "optional": true, + "requires": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "dependencies": { + "protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "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" + } + } + } + }, + "@jsdoc/salty": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", + "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", + "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==" + }, + "@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.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "@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.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "requires": { + "@types/node": "*" + } + }, + "@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==", + "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": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "optional": true, + "requires": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "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/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "requires": { + "@types/mime": "^1", + "@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.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "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" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.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" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", + "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==", + "optional": true, + "requires": { + "punycode": "^1.4.1" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "optional": true + }, + "escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "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.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 + }, + "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, + "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 + }, + "fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "optional": true, + "requires": { + "strnum": "^1.0.5" + } + }, + "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.11.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-11.11.1.tgz", + "integrity": "sha512-UyEbq+3u6jWzCYbUntv/HuJiTixwh36G1R9j0v71mSvGAx/YZEWEW7uSGLYxBYE6ckVRQoKMr40PYUEzrm/4dg==", + "requires": { + "@fastify/busboy": "^1.2.1", + "@firebase/database-compat": "^0.3.4", + "@firebase/database-types": "^0.10.4", + "@google-cloud/firestore": "^6.8.0", + "@google-cloud/storage": "^6.9.5", + "@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": "5.1.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.1.1.tgz", + "integrity": "sha512-KkyKZE98Leg/C73oRyuUYox04PQeeBThdygMfeX+7t1cmKWYKa/ZieYa89U8GHgED+0mF7m7wfNZOfbURYxIKg==", + "requires": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "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.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "optional": true, + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + } + }, + "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, + "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.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.9.0.tgz", + "integrity": "sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==", + "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.3.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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==" + }, + "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.3", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.3.tgz", + "integrity": "sha512-Nu7Sf35kXJ1MWDZIMAuATRQTg1iIPdzh7tqJ6jjvaU/GfDf+qi5UV8zJR3Mo+/pYFvm8mzay4+6O5EWigaQBQw==", + "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" + } + }, + "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.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "requires": { + "@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" + }, + "dependencies": { + "@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "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": "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==", + "optional": true, + "requires": { + "uc.micro": "^2.0.0" + } + }, + "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": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "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.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "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" + } + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "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.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "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-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.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, + "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" + } + }, + "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" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "optional": true + }, + "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==", + "optional": true + }, + "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.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "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.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "optional": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true + } + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "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 + }, + "strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "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.3", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-8.0.3.tgz", + "integrity": "sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww==", + "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.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "optional": true + }, + "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==", + "optional": true + }, + "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==", + "optional": true + }, + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, + "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": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "optional": true + }, + "uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "optional": true + }, + "underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "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.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + }, + "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==", + "optional": true + }, + "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==", + "optional": true, + "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..29b6c533614 --- /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": "^5.1.0" + }, + "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..4c3e462eda2 --- /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..286895bcf73 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/app/app/api/static/route.ts @@ -0,0 +1,11 @@ +export const dynamic = 'force-static' + +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..111aac54d3a --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/next.config.js @@ -0,0 +1,39 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + experimental: { + serverActions: {}, + }, + 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..7376883d55f --- /dev/null +++ b/scripts/webframeworks-deploy-tests/nextjs/package-lock.json @@ -0,0 +1,7257 @@ +{ + "name": "hosting", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "hosting", + "version": "0.1.0", + "dependencies": { + "next": "15.0.0", + "react": "19.0.0-rc-65a56d0e-20241020", + "react-dom": "19.0.0-rc-65a56d0e-20241020" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^9", + "eslint-config-next": "15.0.0", + "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/@emnapi/runtime": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", + "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "optional": true, + "dependencies": { + "tslib": "^2.4.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.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", + "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.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": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", + "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.1.tgz", + "integrity": "sha512-HFZ4Mp26nbWk9d/BpvP0YNL6W4UoZF0VFcTw/aPPA8RpOxeFQgK+ClABGgAUXs9Y/RGX/l1vOmrqz1MQt9MNuw==", + "dev": true, + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", + "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", + "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.0", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.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/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.0.0.tgz", + "integrity": "sha512-Mcv8ZVmEgTO3bePiH/eJ7zHqQEs2gCqZ0UId2RxHmDDc7Pw6ngfSrOFlxG8XDpaex+n2G+TKPsQAf28MO+88Gw==" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.0.0.tgz", + "integrity": "sha512-UG/Gnsq6Sc4wRhO9qk+vc/2v4OfRXH7GEH6/TGlNF5eU/vI9PIO7q+kgd65X2DxJ+qIpHWpzWwlPLmqMi1FE9A==", + "dev": true, + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/eslint-plugin-next/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/@next/eslint-plugin-next/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/@next/swc-darwin-arm64": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.0.0.tgz", + "integrity": "sha512-Gjgs3N7cFa40a9QT9AEHnuGKq69/bvIOn0SLGDV+ordq07QOP4k1GDOVedMHEjVeqy1HBLkL8rXnNTuMZIv79A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.0.tgz", + "integrity": "sha512-BUtTvY5u9s5berAuOEydAUlVMjnl6ZjXS+xVrMt317mglYZ2XXjY8YRDCaz9vYMjBNPXH8Gh75Cew5CMdVbWTw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.0.tgz", + "integrity": "sha512-sbCoEpuWUBpYoLSgYrk0CkBv8RFv4ZlPxbwqRHr/BWDBJppTBtF53EvsntlfzQJ9fosYX12xnS6ltxYYwsMBjg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.0.tgz", + "integrity": "sha512-JAw84qfL81aQCirXKP4VkgmhiDpXJupGjt8ITUkHrOVlBd+3h5kjfPva5M0tH2F9KKSgJQHEo3F5S5tDH9h2ww==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.0.tgz", + "integrity": "sha512-r5Smd03PfxrGKMewdRf2RVNA1CU5l2rRlvZLQYZSv7FUsXD5bKEcOZ/6/98aqRwL7diXOwD8TCWJk1NbhATQHg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.0.tgz", + "integrity": "sha512-fM6qocafz4Xjhh79CuoQNeGPhDHGBBUbdVtgNFJOUM8Ih5ZpaDZlTvqvqsh5IoO06CGomxurEGqGz/4eR/FaMQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.0.tgz", + "integrity": "sha512-ZOd7c/Lz1lv7qP/KzR513XEa7QzW5/P0AH3A5eR1+Z/KmDOvMucht0AozccPc0TqhdV1xaXmC0Fdx0hoNzk6ng==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.0.tgz", + "integrity": "sha512-2RVWcLtsqg4LtaoJ3j7RoKpnWHgcrz5XvuUGE7vBYU2i6M2XeD9Y8RlLaF770LEIScrrl8MdWsp6odtC6sZccg==", + "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/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", + "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", + "dev": true + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, + "node_modules/@swc/helpers": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz", + "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "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/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/eslint-plugin": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz", + "integrity": "sha512-KhGn2LjW1PJT2A/GfDpiyOfS4a8xHQv2myUagTM5+zsormOmBlYsnQ6pobJ8XxJmh6hnHwa2Mbe3fPrDJoDhbA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/type-utils": "8.11.0", + "@typescript-eslint/utils": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.11.0.tgz", + "integrity": "sha512-lmt73NeHdy1Q/2ul295Qy3uninSqi6wQI18XwSpm8w0ZbQXUpjCAWP1Vlv/obudoBiIjJVjlztjQ+d/Md98Yxg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/typescript-estree": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.11.0.tgz", + "integrity": "sha512-Uholz7tWhXmA4r6epo+vaeV7yjdKy5QFCERMjs1kMVsLRKIrSdM6o21W2He9ftp5PP6aWOVpD5zvrvuHZC0bMQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.11.0.tgz", + "integrity": "sha512-ItiMfJS6pQU0NIKAaybBKkuVzo6IdnAhPFZA/2Mba/uBjuPQPet/8+zh5GtLHwmuFRShZx+8lhIs7/QeDHflOg==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.11.0", + "@typescript-eslint/utils": "8.11.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.11.0.tgz", + "integrity": "sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.11.0.tgz", + "integrity": "sha512-yHC3s1z1RCHoCz5t06gf7jH24rr3vns08XXhfEqzYpd6Hll3z/3g23JRi0jM8A47UFKNc3u/y5KIMx8Ynbjohg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.11.0.tgz", + "integrity": "sha512-CYiX6WZcbXNJV7UNB4PLDIBtSdRmRI/nb0FMyqHPTQD1rMjA0foPLaPUV39C/MxkTd/QKSeX+Gb34PPsDVC35g==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/typescript-estree": "8.11.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.11.0.tgz", + "integrity": "sha512-EaewX6lxSjRJnc+99+dqzTeoDZUfyrA52d2/HRrkI830kgovWsmIiTfmr0NZorzqic7ga+1bS60lRBUgR3n/Bw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.11.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", + "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-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.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "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.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "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.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "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-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/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.1.tgz", + "integrity": "sha512-qPC9o+kD8Tir0lzNGLeghbOrWMr3ZJpaRlCIb6Uobt/7N4FiEDvqUMnxzCHRHmg8vOg14kr5gVNyScRmbMaJ9g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "engines": { + "node": ">= 0.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==", + "dev": true + }, + "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/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.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "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", + "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.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "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": "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": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.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==", + "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/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "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/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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==", + "dev": true, + "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", + "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/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/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/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.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "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==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz", + "integrity": "sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.4", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.3", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "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": "9.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz", + "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.7.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.13.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.0.0.tgz", + "integrity": "sha512-HFeTwCR2lFEUWmdB00WZrzaak2CvMvxici38gQknA6Bu2HPizSE4PNFGaFzr5GupjBt+SBJ/E0GIP57ZptOD3g==", + "dev": true, + "dependencies": { + "@next/eslint-plugin-next": "15.0.0", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.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.6.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz", + "integrity": "sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==", + "dev": true, + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.5", + "enhanced-resolve": "^5.15.0", + "eslint-module-utils": "^2.8.1", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", + "is-glob": "^4.0.3" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-typescript/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/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "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.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "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.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "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/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.10.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.1.tgz", + "integrity": "sha512-zHByM9WTUMnfsDTafGXRiqxp6lFtNoSOWBY6FonVRn3A+BUwN1L/tdBXT40BcBJi0cZjOGTXZ0eD/rTG9fEJ0g==", + "dev": true, + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "es-iterator-helpers": "^1.1.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz", + "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz", + "integrity": "sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==", + "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 || ^9.0.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": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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/eslint/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "dev": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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/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.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "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": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.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": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "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/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.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "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-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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/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.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "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.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "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/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "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.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "optional": true + }, + "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-bun-module": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.2.1.tgz", + "integrity": "sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q==", + "dev": true, + "dependencies": { + "semver": "^7.6.3" + } + }, + "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.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "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-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-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "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-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.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "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.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", + "integrity": "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==", + "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" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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": "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-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "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/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "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==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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/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": "15.0.0", + "resolved": "https://registry.npmjs.org/next/-/next-15.0.0.tgz", + "integrity": "sha512-/ivqF6gCShXpKwY9hfrIQYh8YMge8L3W+w1oRLv/POmK4MOQnh+FscZ8a0fRFTSQWE+2z9ctNYvELD9vP2FV+A==", + "dependencies": { + "@next/env": "15.0.0", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.13", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.18.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.0.0", + "@next/swc-darwin-x64": "15.0.0", + "@next/swc-linux-arm64-gnu": "15.0.0", + "@next/swc-linux-arm64-musl": "15.0.0", + "@next/swc-linux-x64-gnu": "15.0.0", + "@next/swc-linux-x64-musl": "15.0.0", + "@next/swc-win32-arm64-msvc": "15.0.0", + "@next/swc-win32-x64-msvc": "15.0.0", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-65a56d0e-20241020", + "react-dom": "^18.2.0 || 19.0.0-rc-65a56d0e-20241020", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "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.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-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/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/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "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": "19.0.0-rc-65a56d0e-20241020", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0-rc-65a56d0e-20241020.tgz", + "integrity": "sha512-rZqpfd9PP/A97j9L1MR6fvWSMgs3khgIyLd0E+gYoCcLrxXndj+ySPRVlDPDC3+f7rm8efHNL4B6HeapqU6gzw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.0.0-rc-65a56d0e-20241020", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0-rc-65a56d0e-20241020.tgz", + "integrity": "sha512-OrsgAX3LQ6JtdBJayK4nG1Hj5JebzWyhKSsrP/bmkeFxulb0nG2LaPloJ6kBkAxtgjiwRyGUciJ4+Qu64gy/KA==", + "dependencies": { + "scheduler": "0.25.0-rc-65a56d0e-20241020" + }, + "peerDependencies": { + "react": "19.0.0-rc-65a56d0e-20241020" + } + }, + "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.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "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.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.2" + }, + "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/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "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-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "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.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.25.0-rc-65a56d0e-20241020", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-65a56d0e-20241020.tgz", + "integrity": "sha512-HxWcXSy0sNnf+TKRkMwyVD1z19AAVQ4gUub8m7VxJUUfSu3J4lr1T+AagohKEypiW5dbQhJuCtAumPY6z9RQ1g==" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "devOptional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true, + "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.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "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.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "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/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "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.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-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.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.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/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/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/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "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/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/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "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/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/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.4", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", + "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "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.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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 + }, + "@emnapi/runtime": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", + "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "optional": true, + "requires": { + "tslib": "^2.4.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.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true + }, + "@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "requires": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + } + }, + "@eslint/core": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", + "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.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": "9.13.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", + "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", + "dev": true + }, + "@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true + }, + "@eslint/plugin-kit": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.1.tgz", + "integrity": "sha512-HFZ4Mp26nbWk9d/BpvP0YNL6W4UoZF0VFcTw/aPPA8RpOxeFQgK+ClABGgAUXs9Y/RGX/l1vOmrqz1MQt9MNuw==", + "dev": true, + "requires": { + "levn": "^0.4.1" + } + }, + "@humanfs/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", + "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "dev": true + }, + "@humanfs/node": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", + "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "dev": true, + "requires": { + "@humanfs/core": "^0.19.0", + "@humanwhocodes/retry": "^0.3.0" + } + }, + "@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/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true + }, + "@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "optional": true, + "requires": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "optional": true, + "requires": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "optional": true + }, + "@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "optional": true + }, + "@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "optional": true + }, + "@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "optional": true + }, + "@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "optional": true + }, + "@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "optional": true + }, + "@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "optional": true + }, + "@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "optional": true + }, + "@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "optional": true, + "requires": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "optional": true, + "requires": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "optional": true, + "requires": { + "@emnapi/runtime": "^1.2.0" + } + }, + "@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "optional": true + }, + "@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "optional": true + }, + "@next/env": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.0.0.tgz", + "integrity": "sha512-Mcv8ZVmEgTO3bePiH/eJ7zHqQEs2gCqZ0UId2RxHmDDc7Pw6ngfSrOFlxG8XDpaex+n2G+TKPsQAf28MO+88Gw==" + }, + "@next/eslint-plugin-next": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.0.0.tgz", + "integrity": "sha512-UG/Gnsq6Sc4wRhO9qk+vc/2v4OfRXH7GEH6/TGlNF5eU/vI9PIO7q+kgd65X2DxJ+qIpHWpzWwlPLmqMi1FE9A==", + "dev": true, + "requires": { + "fast-glob": "3.3.1" + }, + "dependencies": { + "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, + "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" + } + }, + "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" + } + } + } + }, + "@next/swc-darwin-arm64": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.0.0.tgz", + "integrity": "sha512-Gjgs3N7cFa40a9QT9AEHnuGKq69/bvIOn0SLGDV+ordq07QOP4k1GDOVedMHEjVeqy1HBLkL8rXnNTuMZIv79A==", + "optional": true + }, + "@next/swc-darwin-x64": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.0.tgz", + "integrity": "sha512-BUtTvY5u9s5berAuOEydAUlVMjnl6ZjXS+xVrMt317mglYZ2XXjY8YRDCaz9vYMjBNPXH8Gh75Cew5CMdVbWTw==", + "optional": true + }, + "@next/swc-linux-arm64-gnu": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.0.tgz", + "integrity": "sha512-sbCoEpuWUBpYoLSgYrk0CkBv8RFv4ZlPxbwqRHr/BWDBJppTBtF53EvsntlfzQJ9fosYX12xnS6ltxYYwsMBjg==", + "optional": true + }, + "@next/swc-linux-arm64-musl": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.0.tgz", + "integrity": "sha512-JAw84qfL81aQCirXKP4VkgmhiDpXJupGjt8ITUkHrOVlBd+3h5kjfPva5M0tH2F9KKSgJQHEo3F5S5tDH9h2ww==", + "optional": true + }, + "@next/swc-linux-x64-gnu": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.0.tgz", + "integrity": "sha512-r5Smd03PfxrGKMewdRf2RVNA1CU5l2rRlvZLQYZSv7FUsXD5bKEcOZ/6/98aqRwL7diXOwD8TCWJk1NbhATQHg==", + "optional": true + }, + "@next/swc-linux-x64-musl": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.0.tgz", + "integrity": "sha512-fM6qocafz4Xjhh79CuoQNeGPhDHGBBUbdVtgNFJOUM8Ih5ZpaDZlTvqvqsh5IoO06CGomxurEGqGz/4eR/FaMQ==", + "optional": true + }, + "@next/swc-win32-arm64-msvc": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.0.tgz", + "integrity": "sha512-ZOd7c/Lz1lv7qP/KzR513XEa7QzW5/P0AH3A5eR1+Z/KmDOvMucht0AozccPc0TqhdV1xaXmC0Fdx0hoNzk6ng==", + "optional": true + }, + "@next/swc-win32-x64-msvc": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.0.tgz", + "integrity": "sha512-2RVWcLtsqg4LtaoJ3j7RoKpnWHgcrz5XvuUGE7vBYU2i6M2XeD9Y8RlLaF770LEIScrrl8MdWsp6odtC6sZccg==", + "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" + } + }, + "@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true + }, + "@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, + "@rushstack/eslint-patch": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", + "integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==", + "dev": true + }, + "@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, + "@swc/helpers": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz", + "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==", + "requires": { + "tslib": "^2.4.0" + } + }, + "@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "@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 + }, + "@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/eslint-plugin": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz", + "integrity": "sha512-KhGn2LjW1PJT2A/GfDpiyOfS4a8xHQv2myUagTM5+zsormOmBlYsnQ6pobJ8XxJmh6hnHwa2Mbe3fPrDJoDhbA==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/type-utils": "8.11.0", + "@typescript-eslint/utils": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + } + }, + "@typescript-eslint/parser": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.11.0.tgz", + "integrity": "sha512-lmt73NeHdy1Q/2ul295Qy3uninSqi6wQI18XwSpm8w0ZbQXUpjCAWP1Vlv/obudoBiIjJVjlztjQ+d/Md98Yxg==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/typescript-estree": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.11.0.tgz", + "integrity": "sha512-Uholz7tWhXmA4r6epo+vaeV7yjdKy5QFCERMjs1kMVsLRKIrSdM6o21W2He9ftp5PP6aWOVpD5zvrvuHZC0bMQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.11.0.tgz", + "integrity": "sha512-ItiMfJS6pQU0NIKAaybBKkuVzo6IdnAhPFZA/2Mba/uBjuPQPet/8+zh5GtLHwmuFRShZx+8lhIs7/QeDHflOg==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "8.11.0", + "@typescript-eslint/utils": "8.11.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + } + }, + "@typescript-eslint/types": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.11.0.tgz", + "integrity": "sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.11.0.tgz", + "integrity": "sha512-yHC3s1z1RCHoCz5t06gf7jH24rr3vns08XXhfEqzYpd6Hll3z/3g23JRi0jM8A47UFKNc3u/y5KIMx8Ynbjohg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/visitor-keys": "8.11.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.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==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, + "@typescript-eslint/utils": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.11.0.tgz", + "integrity": "sha512-CYiX6WZcbXNJV7UNB4PLDIBtSdRmRI/nb0FMyqHPTQD1rMjA0foPLaPUV39C/MxkTd/QKSeX+Gb34PPsDVC35g==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.11.0", + "@typescript-eslint/types": "8.11.0", + "@typescript-eslint/typescript-estree": "8.11.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.11.0.tgz", + "integrity": "sha512-EaewX6lxSjRJnc+99+dqzTeoDZUfyrA52d2/HRrkI830kgovWsmIiTfmr0NZorzqic7ga+1bS60lRBUgR3n/Bw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "8.11.0", + "eslint-visitor-keys": "^3.4.3" + } + }, + "acorn": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", + "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-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.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true + }, + "array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + } + }, + "array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + } + }, + "array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + } + }, + "arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "requires": { + "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" + } + }, + "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 + }, + "available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "requires": { + "possible-typed-array-names": "^1.0.0" + } + }, + "axe-core": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.1.tgz", + "integrity": "sha512-qPC9o+kD8Tir0lzNGLeghbOrWMr3ZJpaRlCIb6Uobt/7N4FiEDvqUMnxzCHRHmg8vOg14kr5gVNyScRmbMaJ9g==", + "dev": true + }, + "axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "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==", + "dev": true + }, + "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" + } + }, + "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.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "requires": { + "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" + } + }, + "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.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==" + }, + "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": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "optional": true, + "requires": { + "color-convert": "^2.0.1", + "color-string": "^1.9.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==", + "devOptional": 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==", + "devOptional": true + }, + "color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "optional": true, + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "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 + }, + "data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "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 + }, + "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==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "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" + } + }, + "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 + }, + "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" + } + }, + "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.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + } + }, + "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==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.4" + } + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true + }, + "es-iterator-helpers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz", + "integrity": "sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.4", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.3", + "safe-array-concat": "^1.1.2" + } + }, + "es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "requires": { + "es-errors": "^1.3.0" + } + }, + "es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + } + }, + "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": "9.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz", + "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.7.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.13.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "text-table": "^0.2.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true + } + } + }, + "eslint-config-next": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.0.0.tgz", + "integrity": "sha512-HFeTwCR2lFEUWmdB00WZrzaak2CvMvxici38gQknA6Bu2HPizSE4PNFGaFzr5GupjBt+SBJ/E0GIP57ZptOD3g==", + "dev": true, + "requires": { + "@next/eslint-plugin-next": "15.0.0", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react-hooks": "^5.0.0" + } + }, + "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.6.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz", + "integrity": "sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==", + "dev": true, + "requires": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.3.5", + "enhanced-resolve": "^5.15.0", + "eslint-module-utils": "^2.8.1", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", + "is-glob": "^4.0.3" + }, + "dependencies": { + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "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 + } + } + }, + "eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "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.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "requires": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "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.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "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" + } + }, + "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.10.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.1.tgz", + "integrity": "sha512-zHByM9WTUMnfsDTafGXRiqxp6lFtNoSOWBY6FonVRn3A+BUwN1L/tdBXT40BcBJi0cZjOGTXZ0eD/rTG9fEJ0g==", + "dev": true, + "requires": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "es-iterator-helpers": "^1.1.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + } + }, + "eslint-plugin-react": { + "version": "7.37.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz", + "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==", + "dev": true, + "requires": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" + }, + "dependencies": { + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz", + "integrity": "sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==", + "dev": true, + "requires": {} + }, + "eslint-scope": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "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": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "dev": true, + "requires": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true + } + } + }, + "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 + }, + "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.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "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": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "requires": { + "flat-cache": "^4.0.0" + } + }, + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "requires": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + } + }, + "flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "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" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "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-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + } + }, + "get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "requires": { + "resolve-pkg-maps": "^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" + } + }, + "globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true + }, + "globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "requires": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + } + }, + "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==", + "dev": true + }, + "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.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "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.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.3" + } + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "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 + }, + "internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + } + }, + "is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + } + }, + "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==", + "optional": true + }, + "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-bun-module": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.2.1.tgz", + "integrity": "sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q==", + "dev": true, + "requires": { + "semver": "^7.6.3" + } + }, + "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.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "requires": { + "hasown": "^2.0.2" + } + }, + "is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "requires": { + "is-typed-array": "^1.1.13" + } + }, + "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-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-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "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-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.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7" + } + }, + "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.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.14" + } + }, + "is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "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.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", + "integrity": "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==", + "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==", + "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" + } + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "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==", + "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" + } + }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "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==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^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" + } + }, + "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": "15.0.0", + "resolved": "https://registry.npmjs.org/next/-/next-15.0.0.tgz", + "integrity": "sha512-/ivqF6gCShXpKwY9hfrIQYh8YMge8L3W+w1oRLv/POmK4MOQnh+FscZ8a0fRFTSQWE+2z9ctNYvELD9vP2FV+A==", + "requires": { + "@next/env": "15.0.0", + "@next/swc-darwin-arm64": "15.0.0", + "@next/swc-darwin-x64": "15.0.0", + "@next/swc-linux-arm64-gnu": "15.0.0", + "@next/swc-linux-arm64-musl": "15.0.0", + "@next/swc-linux-x64-gnu": "15.0.0", + "@next/swc-linux-x64-musl": "15.0.0", + "@next/swc-win32-arm64-msvc": "15.0.0", + "@next/swc-win32-x64-msvc": "15.0.0", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.13", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "sharp": "^0.33.5", + "styled-jsx": "5.1.6" + } + }, + "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.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + } + }, + "object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + } + }, + "object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.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-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 + }, + "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 + }, + "possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "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": "19.0.0-rc-65a56d0e-20241020", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0-rc-65a56d0e-20241020.tgz", + "integrity": "sha512-rZqpfd9PP/A97j9L1MR6fvWSMgs3khgIyLd0E+gYoCcLrxXndj+ySPRVlDPDC3+f7rm8efHNL4B6HeapqU6gzw==" + }, + "react-dom": { + "version": "19.0.0-rc-65a56d0e-20241020", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0-rc-65a56d0e-20241020.tgz", + "integrity": "sha512-OrsgAX3LQ6JtdBJayK4nG1Hj5JebzWyhKSsrP/bmkeFxulb0nG2LaPloJ6kBkAxtgjiwRyGUciJ4+Qu64gy/KA==", + "requires": { + "scheduler": "0.25.0-rc-65a56d0e-20241020" + } + }, + "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.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + } + }, + "regexp.prototype.flags": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.2" + } + }, + "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 + }, + "resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "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 + }, + "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.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + } + }, + "safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + } + }, + "scheduler": { + "version": "0.25.0-rc-65a56d0e-20241020", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-65a56d0e-20241020.tgz", + "integrity": "sha512-HxWcXSy0sNnf+TKRkMwyVD1z19AAVQ4gUub8m7VxJUUfSu3J4lr1T+AagohKEypiW5dbQhJuCtAumPY6z9RQ1g==" + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "devOptional": true + }, + "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==", + "dev": true, + "requires": { + "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" + } + }, + "set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + } + }, + "sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "optional": true, + "requires": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5", + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + } + }, + "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.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + } + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "optional": true, + "requires": { + "is-arrayish": "^0.3.1" + } + }, + "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.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + } + }, + "string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + } + }, + "string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + } + }, + "string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } + }, + "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-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.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "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 + }, + "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 + }, + "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" + } + }, + "ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "requires": {} + }, + "tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "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==" + }, + "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" + } + }, + "typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + } + }, + "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 + }, + "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" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", + "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", + "dev": true, + "requires": { + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "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.2", + "which-typed-array": "^1.1.15" + } + }, + "which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "requires": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + } + }, + "which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + } + }, + "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..7d338ca1dd7 --- /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": { + "next": "15.0.0", + "react": "19.0.0-rc-65a56d0e-20241020", + "react-dom": "19.0.0-rc-65a56d0e-20241020" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^9", + "eslint-config-next": "15.0.0", + "typescript": "^5" + } +} 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..c65b65d06e9 --- /dev/null +++ b/scripts/webframeworks-deploy-tests/tests.ts @@ -0,0 +1,532 @@ +import { expect, use } from "chai"; +import { glob } from "glob"; +import { join, normalize, relative } from "path"; +import { readFileSync } from "fs"; +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 { IS_WINDOWS } from "../../src/utils"; +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`; +const PATH_SEPARATOR = IS_WINDOWS ? `\\\\` : `\/`; + +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-nextjs-stale-time", + value: "4294967294", + }, + { + key: "x-nextjs-prerender", + value: "1", + }, + { + 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", + }, + { + source: "/base/_not-found", + headers: [ + { + key: "x-nextjs-stale-time", + value: "4294967294", + }, + { + key: "x-nextjs-prerender", + value: "1", + }, + { + key: "x-next-cache-tags", + value: + "_N_T_/layout,_N_T_/_not-found/layout,_N_T_/_not-found/page,_N_T_/_not-found", + }, + ], + }, + { + headers: [ + { + key: "x-nextjs-stale-time", + value: "4294967294", + }, + { + key: "x-nextjs-prerender", + value: "1", + }, + { + 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-nextjs-stale-time", + value: "4294967294", + }, + { + key: "x-nextjs-prerender", + value: "1", + }, + { + 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: join(".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: join(".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: join(".firebase", "demo-nextjs", "functions"), + }, + { + codebase: "firebase-frameworks-demo-angular", + source: join(".firebase", "demo-angular", "functions"), + }, + ], + }); + }); + }); + + describe("next.js", () => { + describe("app directory", () => { + it("should have working static routes", async () => { + const apiStaticJSON = JSON.parse( + readFileSync( + join(NEXT_OUTPUT_PATH, "hosting", NEXT_BASE_PATH, "app", "api", "static"), + ).toString(), + ); + const apiStaticResponse = await fetch(`${NEXTJS_HOST}/app/api/static`); + + const jsonResponse = await apiStaticResponse.json(); + + expect(apiStaticResponse.ok).to.be.true; + + // TODO(leoortizz|jamesdaniels): Figure out why custom headers aren't working with emulators on Windows + if (!IS_WINDOWS) { + expect(apiStaticResponse.headers.get("content-type")).to.eql("application/json"); + expect(apiStaticResponse.headers.get("custom-header")).to.eql("custom-value"); + } + + expect(jsonResponse).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( + join(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 + ? [1, 2].map((num) => + join( + NEXT_BASE_PATH, + "_next", + "data", + buildId, + locale, + "pages", + "fallback", + `${num}.json`, + ), + ) + : [ + join(NEXT_BASE_PATH, "_next", "data", buildId, "pages", "ssg.json"), + join(NEXT_BASE_PATH, "_next", "static", buildId, "_buildManifest.js"), + join(NEXT_BASE_PATH, "_next", "static", buildId, "_ssgManifest.js"), + join(NEXT_BASE_PATH, "app", "api", "static"), + join(NEXT_BASE_PATH, "app", "image.html"), + join(NEXT_BASE_PATH, "app", "ssg.html"), + join(NEXT_BASE_PATH, "404.html"), + ]), + join(I18N_BASE, locale, NEXT_BASE_PATH, "pages", "fallback", "1.html"), + join(I18N_BASE, locale, NEXT_BASE_PATH, "pages", "fallback", "2.html"), + join(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`, + join(I18N_BASE, locale, NEXT_BASE_PATH, "500.html"), + join(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.filter(Boolean).join(PATH_SEPARATOR))); + + 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) => [ + join(I18N_BASE, locale, ANGULAR_BASE_PATH, "index.html"), + join(I18N_BASE, locale, ANGULAR_BASE_PATH, "3rdpartylicenses.txt"), + join(I18N_BASE, locale, ANGULAR_BASE_PATH, "favicon.ico"), + join(I18N_BASE, locale, ANGULAR_BASE_PATH, "index.original.html"), + join(I18N_BASE, locale, ANGULAR_BASE_PATH, "3rdpartylicenses.txt"), + ]) + .map(normalize); + + 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((it) => new RegExp(it.filter(Boolean).join(PATH_SEPARATOR))); + + 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 35cee6e6d7c..00000000000 --- a/src/api.js +++ /dev/null @@ -1,320 +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" - ), - 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") - ), - 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" - ), - functionsUploadRegion: utils.envOverride("FIREBASE_FUNCTIONS_UPLOAD_REGION", "us-central1"), - cloudschedulerOrigin: utils.envOverride( - "FIREBASE_CLOUDSCHEDULER_URL", - "https://cloudscheduler.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"), - 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..c7118000dde --- /dev/null +++ b/src/api.ts @@ -0,0 +1,195 @@ +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 apphostingGitHubAppInstallationURL = () => + utils.envOverride( + "FIREBASE_APPHOSTING_GITHUB_INSTALLATION_URL", + "https://github.com/apps/firebase-app-hosting/installations/new", + ); + +export const authOrigin = () => + utils.envOverride("FIREBASE_AUTH_URL", "https://accounts.google.com"); +export const authManagementOrigin = () => + utils.envOverride("FIREBASE_AUTH_MANAGEMENT_URL", "https://identitytoolkit.googleapis.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 messagingApiOrigin = () => + utils.envOverride("FIREBASE_MESSAGING_CONFIG_URL", "https://fcm.googleapis.com"); +export const crashlyticsApiOrigin = () => + utils.envOverride("FIREBASE_CRASHLYTICS_URL", "https://firebasecrashlytics.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 studioApiOrigin = () => + utils.envOverride("FIREBASE_STUDIO_URL", "https://monospace-pa.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 dataconnectP4SADomain = () => + utils.envOverride( + "FIREBASE_DATACONNECT_P4SA_DOMAIN", + "gcp-sa-firebasedataconnect.iam.gserviceaccount.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"); +export const cloudAiCompanionOrigin = () => + utils.envOverride("CLOUD_AI_COMPANION_URL", "https://cloudaicompanion.googleapis.com"); + +export const appTestingOrigin = () => + utils.envOverride("FIREBASE_APP_TESTING_URL", "https://firebaseapptesting.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 65513522fe6..880c08aa246 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.CLI_VERSION; +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,21 @@ export function setAccessToken(token = ""): void { accessToken = token; } +/** + * Gets a singleton access token + * @returns An access token + */ +export async function getAccessToken(): Promise { + const valid = auth.haveValidTokens(refreshToken, []); + const usingADC = !auth.loggedIn(); + if (accessToken && (valid || usingADC)) { + return accessToken; + } + + const data = await auth.getAccessToken(refreshToken, []); + return data.access_token; +} + function proxyURIFromEnv(): string | undefined { return ( process.env.HTTPS_PROXY || @@ -105,7 +151,6 @@ export type ClientOptions = { urlPrefix: string; apiVersion?: string; auth?: boolean; - proxy?: string; }; export class Client { @@ -129,7 +174,7 @@ export class Client { post( path: string, json?: ReqT, - options: ClientVerbOptions = {} + options: ClientVerbOptions = {}, ): Promise> { const reqOptions: ClientRequestOptions = Object.assign(options, { method: "POST", @@ -142,7 +187,7 @@ export class Client { patch( path: string, json?: ReqT, - options: ClientVerbOptions = {} + options: ClientVerbOptions = {}, ): Promise> { const reqOptions: ClientRequestOptions = Object.assign(options, { method: "PATCH", @@ -155,7 +200,7 @@ export class Client { put( path: string, json?: ReqT, - options: ClientVerbOptions = {} + options: ClientVerbOptions = {}, ): Promise> { const reqOptions: ClientRequestOptions = Object.assign(options, { method: "PUT", @@ -173,6 +218,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 +253,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 +268,7 @@ export class Client { } try { return await this.doRequest(internalReqOptions); - } catch (thrown) { + } catch (thrown: any) { if (thrown instanceof FirebaseError) { throw thrown; } @@ -232,24 +284,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.responseType === "json") { - reqOptions.headers.set("Content-Type", "application/json"); + 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(); @@ -258,33 +319,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; @@ -313,12 +360,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) { @@ -340,54 +383,116 @@ 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") { - // 204 statuses have no content. Don't try to `json` it. - if (res.status === 204) { - body = (undefined as unknown) as ResT; - } else { - body = (await res.json()) 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 (res.status === 401 && this.opts.auth) { + // If we get a 401, access token is expired or otherwise invalid. + // Throw it away and get a new one. We check for validity before using + // tokens, so this should not happen. + logger.debug( + "Got a 401 Unauthenticated error for a call that required authentication. Refreshing tokens.", + ); + setAccessToken(); + setAccessToken(await getAccessToken()); + } + if (options.retryCodes?.includes(res.status)) { + const err = responseToError({ statusCode: res.status }, body, fetchURL) || undefined; + if (operation.retry(err)) { + return; + } + } + if (!options.resolveOnHTTPError) { + return reject(responseToError({ statusCode: res.status }, body, fetchURL)); + } + } + + resolve({ + status: res.status, + response: res, + body, + }); + }); + }); } private logRequest(options: InternalClientRequestOptions): void { @@ -403,6 +508,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) { @@ -415,7 +528,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}`); @@ -428,7 +541,7 @@ export class Client { } function isLocalInsecureRequest(urlPrefix: string): boolean { - const u = parse(urlPrefix); + const u = new URL(urlPrefix); return u.protocol === "http:"; } @@ -446,5 +559,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/appUtils.spec.ts b/src/appUtils.spec.ts new file mode 100644 index 00000000000..3289df025df --- /dev/null +++ b/src/appUtils.spec.ts @@ -0,0 +1,791 @@ +import * as mockfs from "mock-fs"; +import { expect } from "chai"; +import { + extractAppIdentifiersFlutter, + extractAppIdentifierIos, + extractAppIdentifiersAndroid, + Platform, + getPlatformsFromFolder, + detectApps, + App, +} from "./appUtils"; + +const FLUTTER_CONFIG = ` +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'FAKE_WEB_API_KEY', + appId: '1:123456789012:web:abcdef1234567890abcdef', + messagingSenderId: '123456789012', + projectId: 'fake-project-web', + authDomain: 'fake-project-web.firebaseapp.com', + storageBucket: 'fake-project-web.firebasestorage.app', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'FAKE_ANDROID_API_KEY', + appId: '1:123456789012:android:abcdef1234567890abcdef', + messagingSenderId: '123456789012', + projectId: 'fake-project-android', + storageBucket: 'fake-project-android.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'FAKE_IOS_API_KEY', + appId: '1:123456789012:ios:abcdef1234567890abcdef', + messagingSenderId: '123456789012', + projectId: 'fake-project-ios', + storageBucket: 'fake-project-ios.firebasestorage.app', + iosBundleId: 'com.example.fakeTestsFlutter', + ); +} +`; + +const IOS_CONFIG_1 = ` + + + + API_KEY + FAKE_API_KEY + GCM_SENDER_ID + FAKE_GCM_SENDER_ID + PLIST_VERSION + 1 + BUNDLE_ID + com.fake.ios.app + PROJECT_ID + fake-project-id + STORAGE_BUCKET + fake-project-id.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:123456789012:ios:abcdef1234567890abcdef + +`; + +const IOS_CONFIG_2 = ` + + + + API_KEY + FAKE_API_KEY + GCM_SENDER_ID + FAKE_GCM_SENDER_ID + PLIST_VERSION + 1 + BUNDLE_ID + com.fake.ios.app.debug + PROJECT_ID + fake-project-id-debug + STORAGE_BUCKET + fake-project-id.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:123456789012:ios:abcdef0987654321abcdef + +`; + +const ANDROID_CONFIG_1 = ` +{ + "project_info": { + "project_number": "FAKE_PROJECT_NUMBER", + "project_id": "fake-project-android", + "storage_bucket": "fake-project-android.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:123456789012:android:fakeapp1id", + "android_client_info": { + "package_name": "com.fake.app1" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "FAKE_API_KEY_1" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +}`; + +const ANDROID_CONFIG_2 = ` +{ + "project_info": { + "project_number": "FAKE_PROJECT_NUMBER", + "project_id": "fake-project-android", + "storage_bucket": "fake-project-android.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:123456789012:android:fakeapp2id", + "android_client_info": { + "package_name": "com.fake.app1.debug" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "FAKE_API_KEY_2" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +}`; + +const ANDROID_CONFIG_COMBINED = `{ + "project_info": { + "project_number": "FAKE_PROJECT_NUMBER", + "project_id": "fake-project-android", + "storage_bucket": "fake-project-android.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:123456789012:android:fakeapp1id", + "android_client_info": { + "package_name": "com.fake.app1" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "FAKE_API_KEY_1" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:123456789012:android:fakeapp2id", + "android_client_info": { + "package_name": "com.fake.app1.debug" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "FAKE_API_KEY_2" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +}`; + +function cleanUndefinedFields(apps: App[]): App[] { + return apps.map((app) => { + const leanApp = Object.fromEntries( + Object.entries(app).filter((entry) => entry[1] !== undefined), + ) as App; + return leanApp; + }); +} + +describe("appUtils", () => { + describe("getPlatformsFromFolder", () => { + const testDir = "test-dir"; + + afterEach(() => { + mockfs.restore(); + }); + + it("should return WEB if package.json exists", async () => { + mockfs({ [testDir]: { "package.json": "{}" } }); + const platforms = await getPlatformsFromFolder(testDir); + expect(platforms).to.have.deep.members([Platform.WEB]); + }); + + it("should return ANDROID if src/main exists", async () => { + mockfs({ + [testDir]: { src: { main: {} } }, + }); + const platforms = await getPlatformsFromFolder(testDir); + expect(platforms).to.have.deep.members([Platform.ANDROID]); + }); + + it("should return IOS if .xcodeproj exists", async () => { + mockfs({ + [testDir]: { "a.xcodeproj": {} }, + }); + const platforms = await getPlatformsFromFolder(testDir); + expect(platforms).to.have.deep.members([Platform.IOS]); + }); + + it("should return FLUTTER if pubspec.yaml exists", async () => { + mockfs({ + [testDir]: { "pubspec.yaml": "name: test" }, + }); + const platforms = await getPlatformsFromFolder(testDir); + expect(platforms).to.have.deep.members([Platform.FLUTTER]); + }); + + it("should return FLUTTER and WEB if both identifiers exist", async () => { + mockfs({ [testDir]: { "package.json": "{}", "pubspec.yaml": "name: test" } }); + const platforms = await getPlatformsFromFolder(testDir); + expect(platforms).to.have.deep.members([Platform.FLUTTER, Platform.WEB]); + }); + + it("should return an empty array if no identifiers exist", async () => { + mockfs({ [testDir]: {} }); + const platforms = await getPlatformsFromFolder(testDir); + expect(platforms).to.be.empty; + }); + }); + + describe("detectApps", () => { + const testDir = "test-dir"; + + afterEach(() => { + mockfs.restore(); + }); + + it("should detect a web app", async () => { + mockfs({ [testDir]: { "package.json": "{}" } }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.WEB, + directory: ".", + frameworks: [], + }, + ]); + }); + + it("should detect an android app", async () => { + mockfs({ [testDir]: { src: { main: {} } } }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.ANDROID, + directory: ".", + }, + ]); + }); + + it("should detect an android app with Firebase", async () => { + mockfs({ + [testDir]: { + src: { main: {} }, + "google-services.json": ANDROID_CONFIG_1, + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.ANDROID, + directory: ".", + appId: "1:123456789012:android:fakeapp1id", + bundleId: "com.fake.app1", + }, + ]); + }); + + it("should detect an android app with a suffix", async () => { + mockfs({ + [testDir]: { + src: { main: {} }, + "google-services.json.example": ANDROID_CONFIG_1, + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.ANDROID, + directory: ".", + appId: "1:123456789012:android:fakeapp1id", + bundleId: "com.fake.app1", + }, + ]); + }); + + it("should detect an android app with extra words", async () => { + mockfs({ + [testDir]: { + src: { main: {} }, + "google-services-example.json": ANDROID_CONFIG_1, + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.ANDROID, + directory: ".", + appId: "1:123456789012:android:fakeapp1id", + bundleId: "com.fake.app1", + }, + ]); + }); + + it("should detect android app with multiple variants", async () => { + mockfs({ + [testDir]: { + src: { + main: {}, + release: { + "google-services.json": ANDROID_CONFIG_1, + }, + debug: { + "google-services.json": ANDROID_CONFIG_2, + }, + }, + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.ANDROID, + directory: ".", + appId: "1:123456789012:android:fakeapp1id", + bundleId: "com.fake.app1", + }, + { + platform: Platform.ANDROID, + directory: ".", + appId: "1:123456789012:android:fakeapp2id", + bundleId: "com.fake.app1.debug", + }, + ]); + }); + + it("should detect android app with multiple app ids in the same file", async () => { + mockfs({ + [testDir]: { + src: { main: {} }, + "google-services.json": ANDROID_CONFIG_COMBINED, + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.ANDROID, + directory: ".", + appId: "1:123456789012:android:fakeapp1id", + bundleId: "com.fake.app1", + }, + { + platform: Platform.ANDROID, + directory: ".", + appId: "1:123456789012:android:fakeapp2id", + bundleId: "com.fake.app1.debug", + }, + ]); + }); + + it("should detect an ios app", async () => { + mockfs({ + [testDir]: { + "a.xcodeproj": {}, + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.IOS, + directory: ".", + }, + ]); + }); + + it("should detect an ios app with Firebase", async () => { + mockfs({ + [testDir]: { + "a.xcodeproj": {}, + "GoogleService-Info.plist": IOS_CONFIG_1, + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.IOS, + directory: ".", + appId: "1:123456789012:ios:abcdef1234567890abcdef", + bundleId: "com.fake.ios.app", + }, + ]); + }); + + it("should detect an ios app with different suffix", async () => { + mockfs({ + [testDir]: { + "a.xcodeproj": {}, + "GoogleService-Info.plist.example": IOS_CONFIG_1, + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.IOS, + directory: ".", + appId: "1:123456789012:ios:abcdef1234567890abcdef", + bundleId: "com.fake.ios.app", + }, + ]); + }); + + it("should detect an ios app replacing Info", async () => { + mockfs({ + [testDir]: { + "a.xcodeproj": {}, + "GoogleService-Prod.plist": IOS_CONFIG_1, + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.IOS, + directory: ".", + appId: "1:123456789012:ios:abcdef1234567890abcdef", + bundleId: "com.fake.ios.app", + }, + ]); + }); + + it("should detect an ios app with multiple plist files", async () => { + mockfs({ + [testDir]: { + "a.xcodeproj": {}, + Configs: { + Dev: { + "GoogleService-Info.plist": IOS_CONFIG_1, + }, + Prod: { + "GoogleService-Info.plist": IOS_CONFIG_2, + }, + }, + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.IOS, + directory: ".", + appId: "1:123456789012:ios:abcdef1234567890abcdef", + bundleId: "com.fake.ios.app", + }, + { + platform: Platform.IOS, + directory: ".", + appId: "1:123456789012:ios:abcdef0987654321abcdef", + bundleId: "com.fake.ios.app.debug", + }, + ]); + }); + + it("should detect a flutter app", async () => { + mockfs({ + [testDir]: { "pubspec.yaml": "name: test" }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.FLUTTER, + directory: ".", + }, + ]); + }); + + it("should detect a flutter app with Firebase", async () => { + mockfs({ + [testDir]: { + "pubspec.yaml": "name: test", + lib: { "firebase_options.dart": FLUTTER_CONFIG }, + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.FLUTTER, + directory: ".", + appId: "1:123456789012:web:abcdef1234567890abcdef", + }, + { + platform: Platform.FLUTTER, + directory: ".", + appId: "1:123456789012:android:abcdef1234567890abcdef", + }, + { + platform: Platform.FLUTTER, + directory: ".", + appId: "1:123456789012:ios:abcdef1234567890abcdef", + bundleId: "com.example.fakeTestsFlutter", + }, + ]); + }); + it("should detect multiple apps", async () => { + mockfs({ + [testDir]: { + web: { "package.json": "{}" }, + android: { + src: { main: {} }, + "google-services.json": ANDROID_CONFIG_1, + }, + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.WEB, + directory: `web`, + frameworks: [], + }, + { + platform: Platform.ANDROID, + directory: `android`, + appId: "1:123456789012:android:fakeapp1id", + bundleId: "com.fake.app1", + }, + ]); + }); + + it("should detect the react framework", async () => { + mockfs({ + [testDir]: { + "package.json": JSON.stringify({ + dependencies: { + react: "1.0.0", + }, + }), + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.WEB, + directory: ".", + frameworks: ["react"], + }, + ]); + }); + + it("should detect angular web framework", async () => { + mockfs({ + [testDir]: { + "package.json": JSON.stringify({ + dependencies: { + "@angular/core": "1.0.0", + }, + }), + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.WEB, + directory: ".", + frameworks: ["angular"], + }, + ]); + }); + + it("should detect the next framework", async () => { + mockfs({ + [testDir]: { + "package.json": JSON.stringify({ + dependencies: { + next: "1.0.0", + }, + }), + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.WEB, + directory: ".", + frameworks: ["react"], + }, + ]); + }); + + it("should detect the angular framework", async () => { + mockfs({ + [testDir]: { + "package.json": JSON.stringify({ + dependencies: { + "@angular/core": "1.0.0", + }, + }), + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.WEB, + directory: ".", + frameworks: ["angular"], + }, + ]); + }); + + it("should detect a nested web app", async () => { + mockfs({ + [testDir]: { + web: { + frontend: { "package.json": "{}" }, + }, + }, + }); + const apps = cleanUndefinedFields(await detectApps(testDir)); + expect(apps).to.have.deep.members([ + { + platform: Platform.WEB, + directory: "web/frontend", + frameworks: [], + }, + ]); + }); + + it("should detect multiple top-level and nested apps", async () => { + mockfs({ + [testDir]: { + web: { + "package.json": "{}", + }, + android: { + src: { main: {} }, + "google-services.json": ANDROID_CONFIG_1, + }, + ios: { + "a.xcodeproj": {}, + "GoogleService-Info.plist": IOS_CONFIG_1, + }, + }, + }); + + const apps = cleanUndefinedFields(await detectApps(testDir)); + const expected = [ + { + platform: Platform.ANDROID, + directory: "android", + appId: "1:123456789012:android:fakeapp1id", + bundleId: "com.fake.app1", + }, + { + platform: Platform.IOS, + directory: "ios", + appId: "1:123456789012:ios:abcdef1234567890abcdef", + bundleId: "com.fake.ios.app", + }, + { + platform: Platform.WEB, + directory: "web", + frameworks: [], + }, + ]; + expect(apps).to.have.deep.members(expected); + }); + }); + + describe("extractAppIdentifiers", () => { + it("should extract all app IDs and bundle IDs from a firebase_options.dart file", () => { + const expectedIdentifiers = [ + { + appId: "1:123456789012:web:abcdef1234567890abcdef", + bundleId: undefined, + }, + { + appId: "1:123456789012:android:abcdef1234567890abcdef", + bundleId: undefined, + }, + { + appId: "1:123456789012:ios:abcdef1234567890abcdef", + bundleId: "com.example.fakeTestsFlutter", + }, + ]; + + const result = extractAppIdentifiersFlutter(FLUTTER_CONFIG); + expect(result).to.deep.equal(expectedIdentifiers); + }); + + it("should extract the GOOGLE_APP_ID and BUNDLE_ID from a GoogleService-Info.plist file", () => { + const expectedIdentifier = [ + { + appId: "1:123456789012:ios:abcdef1234567890abcdef", + bundleId: "com.fake.ios.app", + }, + ]; + + const result = extractAppIdentifierIos(IOS_CONFIG_1); + expect(result).to.deep.equal(expectedIdentifier); + }); + + it("should extract all mobilesdk_app_id and package_name from a google-services.json file", () => { + const expectedIdentifiers = [ + { + appId: "1:123456789012:android:fakeapp1id", + bundleId: "com.fake.app1", + }, + { + appId: "1:123456789012:android:fakeapp2id", + bundleId: "com.fake.app1.debug", + }, + ]; + + const result = extractAppIdentifiersAndroid(ANDROID_CONFIG_COMBINED); + expect(result).to.deep.equal(expectedIdentifiers); + }); + }); +}); diff --git a/src/appUtils.ts b/src/appUtils.ts new file mode 100644 index 00000000000..a366885f260 --- /dev/null +++ b/src/appUtils.ts @@ -0,0 +1,314 @@ +import * as fs from "fs-extra"; +import * as path from "path"; +import { glob } from "glob"; +import { PackageJSON } from "./frameworks/compose/discover/runtime/node"; + +/** + * Supported application platforms. + */ +export enum Platform { + ANDROID = "ANDROID", + WEB = "WEB", + IOS = "IOS", + FLUTTER = "FLUTTER", +} + +/** + * Supported web frameworks. + */ +export enum Framework { + REACT = "react", + ANGULAR = "angular", +} + +interface AppIdentifier { + appId: string; + bundleId?: string; +} + +/** + * Represents a detected application. + */ +export interface App { + platform: Platform; + directory: string; + appId?: string; + bundleId?: string; + frameworks?: Framework[]; +} + +/** Returns a string description of the app */ +export function appDescription(a: App): string { + return `${a.directory} (${a.platform.toLowerCase()})`; +} + +/** + * Given a directory, determine the platform type. + * @param dirPath The directory to scan. + * @return A list of platforms detected. + */ +export async function getPlatformsFromFolder(dirPath: string): Promise { + const apps = await detectApps(dirPath); + return [...new Set(apps.map((app) => app.platform))]; +} + +/** + * Detects the apps in a given directory. + * @param dirPath The current working directory to scan. + * @return A list of apps detected. + */ +export async function detectApps(dirPath: string): Promise { + const packageJsonFiles = await detectFiles(dirPath, "package.json"); + const pubSpecYamlFiles = await detectFiles(dirPath, "pubspec.yaml"); + const srcMainFolders = await detectFiles(dirPath, "src/main/"); + const xCodeProjects = await detectFiles(dirPath, "*.xcodeproj/"); + const webApps = await Promise.all(packageJsonFiles.map((p) => packageJsonToWebApp(dirPath, p))); + + const flutterAppPromises = await Promise.all( + pubSpecYamlFiles.map((f) => processFlutterDir(dirPath, f)), + ); + const flutterApps = flutterAppPromises.flat(); + + const androidAppPromises = await Promise.all( + srcMainFolders.map((f) => processAndroidDir(dirPath, f)), + ); + const androidApps = androidAppPromises + .flat() + .filter((a) => !flutterApps.some((f) => isPathInside(f.directory, a.directory))); + + const iosAppPromises = await Promise.all(xCodeProjects.map((f) => processIosDir(dirPath, f))); + const iosApps = iosAppPromises + .flat() + .filter((a) => !flutterApps.some((f) => isPathInside(f.directory, a.directory))); + return [...webApps, ...flutterApps, ...androidApps, ...iosApps]; +} + +async function processIosDir(dirPath: string, filePath: string): Promise { + // Search for apps in the parent directory + const iosDir = path.dirname(filePath); + const iosAppIds = await detectAppIdsForPlatform(dirPath, Platform.IOS); + if (iosAppIds.length === 0) { + return [ + { + platform: Platform.IOS, + directory: iosDir, + }, + ]; + } + const iosApps = await Promise.all( + iosAppIds.map((app) => ({ + platform: Platform.IOS, + directory: iosDir, + appId: app.appId, + bundleId: app.bundleId, + })), + ); + return iosApps.flat(); +} + +async function processAndroidDir(dirPath: string, filePath: string): Promise { + // Search for apps in the parent directory, not in the src/main directory + const androidDir = path.dirname(path.dirname(filePath)); + const androidAppIds = await detectAppIdsForPlatform(dirPath, Platform.ANDROID); + + if (androidAppIds.length === 0) { + return [ + { + platform: Platform.ANDROID, + directory: androidDir, + }, + ]; + } + + const androidApps = await Promise.all( + androidAppIds.map((app) => ({ + platform: Platform.ANDROID, + directory: androidDir, + appId: app.appId, + bundleId: app.bundleId, + })), + ); + return androidApps.flat(); +} + +async function processFlutterDir(dirPath: string, filePath: string): Promise { + const flutterDir = path.dirname(filePath); + const flutterAppIds = await detectAppIdsForPlatform(dirPath, Platform.FLUTTER); + + if (flutterAppIds.length === 0) { + return [ + { + platform: Platform.FLUTTER, + directory: flutterDir, + }, + ]; + } + + const flutterApps = await Promise.all( + flutterAppIds.map((app) => { + const flutterApp: App = { + platform: Platform.FLUTTER, + directory: flutterDir, + appId: app.appId, + bundleId: app.bundleId, + }; + return flutterApp; + }), + ); + + return flutterApps.flat(); +} + +function isPathInside(parent: string, child: string): boolean { + const relativePath = path.relative(parent, child); + return !relativePath.startsWith(`..`); +} + +async function packageJsonToWebApp(dirPath: string, packageJsonFile: string): Promise { + const fullPath = path.join(dirPath, packageJsonFile); + const packageJson = JSON.parse((await fs.readFile(fullPath)).toString()) as PackageJSON; + return { + platform: Platform.WEB, + directory: path.dirname(packageJsonFile), + frameworks: getFrameworksFromPackageJson(packageJson), + }; +} + +const WEB_FRAMEWORKS: Framework[] = Object.values(Framework); +const WEB_FRAMEWORKS_SIGNALS: { [key in Framework]: string[] } = { + react: ["react", "next"], + angular: ["@angular/core"], +}; + +async function detectAppIdsForPlatform( + dirPath: string, + platform: Platform, +): Promise { + let appIdFiles; + let extractFunc: (fileContent: string) => AppIdentifier[]; + switch (platform) { + // Leaving web out of the mix for now because we have no strong conventions + // around where to put Firebase config. It could be anywhere in your codebase. + case Platform.ANDROID: + appIdFiles = await detectFiles(dirPath, "google-services*.json*"); + extractFunc = extractAppIdentifiersAndroid; + break; + case Platform.IOS: + appIdFiles = await detectFiles(dirPath, "GoogleService-*.plist*"); + extractFunc = extractAppIdentifierIos; + break; + case Platform.FLUTTER: + appIdFiles = await detectFiles(dirPath, "firebase_options.dart"); + extractFunc = extractAppIdentifiersFlutter; + break; + default: + return []; + } + + const allAppIds = await Promise.all( + appIdFiles.map(async (file) => { + const fileContent = (await fs.readFile(path.join(dirPath, file))).toString(); + return extractFunc(fileContent); + }), + ); + return allAppIds.flat(); +} + +function getFrameworksFromPackageJson(packageJson: PackageJSON): Framework[] { + const devDependencies = Object.keys(packageJson.devDependencies ?? {}); + const dependencies = Object.keys(packageJson.dependencies ?? {}); + const allDeps = Array.from(new Set([...devDependencies, ...dependencies])); + return WEB_FRAMEWORKS.filter((framework) => + WEB_FRAMEWORKS_SIGNALS[framework].find((dep) => allDeps.includes(dep)), + ); +} + +/** + * Reads a firebase_options.dart file and extracts all appIds and bundleIds. + * @param fileContent content of the dart file. + * @return a list of appIds and bundleIds. + */ +export function extractAppIdentifiersFlutter(fileContent: string): AppIdentifier[] { + const optionsRegex = /FirebaseOptions\(([^)]*)\)/g; + const appIdRegex = /appId: '([^']*)'/; + const bundleIdRegex = /iosBundleId: '([^']*)'/; + const matches = fileContent.matchAll(optionsRegex); + const identifiers: AppIdentifier[] = []; + for (const match of matches) { + const optionsContent = match[1]; + const appIdMatch = appIdRegex.exec(optionsContent); + const bundleIdMatch = bundleIdRegex.exec(optionsContent); + if (appIdMatch?.[1]) { + identifiers.push({ + appId: appIdMatch[1], + bundleId: bundleIdMatch?.[1], + }); + } + } + + return identifiers; +} + +/** + * Reads a GoogleService-Info.plist file and extracts the GOOGLE_APP_ID and BUNDLE_ID. + * @param fileContent content of the plist file. + * @return The GOOGLE_APP_ID and BUNDLE_ID or an empty array. + */ +export function extractAppIdentifierIos(fileContent: string): AppIdentifier[] { + const appIdRegex = /GOOGLE_APP_ID<\/key>\s*([^<]*)<\/string>/; + const bundleIdRegex = /BUNDLE_ID<\/key>\s*([^<]*)<\/string>/; + const appIdMatch = fileContent.match(appIdRegex); + const bundleIdMatch = fileContent.match(bundleIdRegex); + if (appIdMatch?.[1]) { + return [ + { + appId: appIdMatch[1], + bundleId: bundleIdMatch?.[1], + }, + ]; + } + return []; +} + +/** + * Reads a google-services.json file and extracts all mobilesdk_app_id and package_name values. + * @param fileContent content of the google-services.json file. + * @return a list of mobilesdk_app_id and package_name values. + */ +export function extractAppIdentifiersAndroid(fileContent: string): AppIdentifier[] { + const identifiers: AppIdentifier[] = []; + try { + const config = JSON.parse(fileContent); + if (config.client && Array.isArray(config.client)) { + for (const client of config.client) { + if (client.client_info?.mobilesdk_app_id) { + identifiers.push({ + appId: client.client_info.mobilesdk_app_id, + bundleId: client.client_info.android_client_info?.package_name, + }); + } + } + } + } catch (e) { + // Handle parsing errors if necessary + console.error("Error parsing google-services.json:", e); + } + return identifiers; +} + +async function detectFiles(dirPath: string, filePattern: string): Promise { + const options = { + cwd: dirPath, + ignore: [ + "**/dataconnect*/**", + "**/node_modules/**", // Standard dependency directory + "**/dist/**", // Common build output + "**/build/**", // Common build output + "**/out/**", // Another common build output + "**/.next/**", // Next.js build directory + "**/coverage/**", // Test coverage reports + ], + absolute: false, + }; + return glob(`**/${filePattern}`, options); +} diff --git a/src/appdistribution/client.spec.ts b/src/appdistribution/client.spec.ts new file mode 100644 index 00000000000..87ae1323485 --- /dev/null +++ b/src/appdistribution/client.spec.ts @@ -0,0 +1,512 @@ +import { expect } from "chai"; +import { join } from "path"; +import * as fs from "fs-extra"; +import * as nock from "nock"; +import { rmSync } from "node:fs"; +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(() => { + rmSync(tempdir.name, { recursive: true }); + }); + + 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("listTesters", () => { + it("should throw error if request fails", async () => { + nock(appDistributionOrigin()) + .get(`/v1/${projectName}/testers`) + .reply(400, { error: { status: "FAILED_PRECONDITION" } }); + await expect(appDistributionClient.listTesters(projectName)).to.be.rejectedWith( + FirebaseError, + "Client request failed to list testers", + ); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with ListTestersResponse when request succeeds - no filter", async () => { + const testerListing = [ + { + name: "tester_1", + displayName: "Tester 1", + groups: [], + lastActivityTime: new Date("2024-08-27T02:37:19.539865Z"), + }, + { + name: "tester_2", + displayName: "Tester 2", + groups: [`${projectName}/groups/beta-team`, `${projectName}/groups/alpha-team`], + lastActivityTime: new Date("2024-08-26T02:37:19Z"), + }, + ]; + + nock(appDistributionOrigin()).get(`/v1/${projectName}/testers`).reply(200, { + testers: testerListing, + }); + await expect(appDistributionClient.listTesters(projectName)).to.eventually.deep.eq({ + testers: [ + { + name: "tester_1", + displayName: "Tester 1", + groups: [], + lastActivityTime: new Date("2024-08-27T02:37:19.539865Z"), + }, + { + name: "tester_2", + displayName: "Tester 2", + groups: [`${projectName}/groups/beta-team`, `${projectName}/groups/alpha-team`], + lastActivityTime: new Date("2024-08-26T02:37:19Z"), + }, + ], + }); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve when request succeeds - with filter", async () => { + const testerListing = [ + { + name: "tester_2", + displayName: "Tester 2", + groups: [`${projectName}/groups/beta-team`], + lastActivityTime: new Date("2024-08-26T02:37:19Z"), + }, + ]; + + const filterQuery = encodeURI(`groups=${projectName}/groups/beta-team`); + + nock(appDistributionOrigin()) + .get(`/v1/${projectName}/testers?filter=${filterQuery}`) + .reply(200, { + testers: testerListing, + }); + await expect( + appDistributionClient.listTesters(projectName, "beta-team"), + ).to.eventually.deep.eq({ + testers: [ + { + name: "tester_2", + displayName: "Tester 2", + groups: [`${projectName}/groups/beta-team`], + lastActivityTime: new Date("2024-08-26T02:37:19Z"), + }, + ], + }); + expect(nock.isDone()).to.be.true; + }); + + it("should gracefully handle no testers", async () => { + nock(appDistributionOrigin()).get(`/v1/${projectName}/testers`).reply(200, {}); + + await expect(appDistributionClient.listTesters(projectName)).to.eventually.deep.eq({ + testers: [], + }); + 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("listGroups", () => { + it("should throw error if request fails", async () => { + nock(appDistributionOrigin()) + .get(`/v1/${projectName}/groups`) + .reply(400, { error: { status: "FAILED_PRECONDITION" } }); + await expect(appDistributionClient.listGroups(projectName)).to.be.rejectedWith( + FirebaseError, + "Client failed to list groups", + ); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with ListGroupsResponse when request succeeds", async () => { + const groupListing: Group[] = [ + { + name: "group_1", + displayName: "Group 1", + testerCount: 5, + releaseCount: 2, + inviteLinkCount: 10, + }, + { + name: "group_2", + displayName: "Group 2", + }, + ]; + + nock(appDistributionOrigin()).get(`/v1/${projectName}/groups`).reply(200, { + groups: groupListing, + }); + await expect(appDistributionClient.listGroups(projectName)).to.eventually.deep.eq({ + groups: groupListing, + }); + 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 91733348e4e..07af4b456c8 100644 --- a/src/appdistribution/client.ts +++ b/src/appdistribution/client.ts @@ -1,199 +1,306 @@ -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 { FirebaseError, getErrMsg } from "../error"; +import { Client, ClientResponse } from "../apiv2"; +import { appDistributionOrigin } from "../api"; -// tslint:disable-next-line:no-var-requires -const pkg = require("../../package.json"); +import { + AabInfo, + BatchRemoveTestersResponse, + Group, + ListGroupsResponse, + ListTestersResponse, + LoginCredential, + mapDeviceToExecution, + ReleaseTest, + TestDevice, + UploadReleaseResponse, +} from "./types"; /** - * Helper interface for an app that is provisioned with App Distribution - */ -export interface AppDistributionApp { - projectNumber: string; - appId: string; - platform: string; - bundleId: string; - contactEmail: string; - aabState: AabState; - aabCertificate: AabCertificate | null; -} - -export interface AabCertificate { - certificateHashMd5: string; - certificateHashSha1: string; - certificateHashSha256: string; -} - -export enum UploadStatus { - SUCCESS = "SUCCESS", - IN_PROGRESS = "IN_PROGRESS", - ERROR = "ERROR", -} - -/** Enum representing the App Bundles state for the App */ -export enum AabState { - AAB_STATE_UNSPECIFIED = "AAB_STATE_UNSPECIFIED", - ACTIVE = "ACTIVE", - 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 interface UploadStatusResponse { - status: UploadStatus; - message: string; - errorCode: string; - release: { - id: string; - }; -} - -/** Enum for app_view parameter for getApp requests */ -export enum AppView { - BASIC = "BASIC", - FULL = "FULL", -} - -/** - * Proxies HTTPS requests to the App Distribution server backend. + * Makes RPCs to the App Distribution server backend. */ export class AppDistributionClient { - static MAX_POLLING_RETRIES = 60; - static POLLING_INTERVAL_MS = 2000; - - constructor(private readonly appId: string) {} - - async getApp(appView = AppView.BASIC): Promise { - const apiResponse = await api.request("GET", `/v1alpha/apps/${this.appId}?appView=${appView}`, { - origin: api.appDistributionOrigin, - auth: true, - }); + appDistroV1Client = new Client({ + urlPrefix: appDistributionOrigin(), + apiVersion: "v1", + }); + appDistroV1AlphaClient = new Client({ + urlPrefix: appDistributionOrigin(), + apiVersion: "v1alpha", + }); - return _.get(apiResponse, "body"); + async getAabInfo(appName: string): Promise { + const apiResponse = await this.appDistroV1Client.get(`/${appName}/aabInfo`); + return apiResponse.body; } - async uploadDistribution(distribution: Distribution): Promise { - const apiResponse = await api.request("POST", `/app-binary-uploads?app_id=${this.appId}`, { - auth: true, - origin: api.appDistributionOrigin, + async uploadRelease(appName: string, distribution: Distribution): Promise { + const client = new Client({ urlPrefix: appDistributionOrigin() }); + const apiResponse = await client.request({ + method: "POST", + path: `/upload/v1/${appName}/releases:upload`, headers: { - "X-APP-DISTRO-API-CLIENT-ID": pkg.name, - "X-APP-DISTRO-API-CLIENT-TYPE": distribution.platform(), - "X-APP-DISTRO-API-CLIENT-VERSION": pkg.version, - "X-GOOG-UPLOAD-FILE-NAME": distribution.getFileName(), - "X-GOOG-UPLOAD-PROTOCOL": "raw", + "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), "token"); - } - - async pollUploadStatus(binaryName: string, retryCount = 0): Promise { - const uploadStatus = await this.getUploadStatus(binaryName); - if (uploadStatus.status === UploadStatus.IN_PROGRESS) { - if (retryCount >= AppDistributionClient.MAX_POLLING_RETRIES) { - throw new FirebaseError( - "it took longer than expected to process your binary, please try again", - { exit: 1 } - ); - } - await new Promise((resolve) => - setTimeout(resolve, AppDistributionClient.POLLING_INTERVAL_MS) - ); - return this.pollUploadStatus(binaryName, retryCount + 1); - } else if (uploadStatus.status === UploadStatus.SUCCESS) { - return uploadStatus.release.id; - } else { - throw new FirebaseError( - `error processing your binary: ${uploadStatus.message} (Code: ${uploadStatus.errorCode})` - ); - } + return apiResponse.body.name; } - async getUploadStatus(binaryName: string): Promise { - const encodedBinaryName = encodeURIComponent(binaryName); - const apiResponse = await api.request( - "GET", - `/v1alpha/apps/${this.appId}/upload_status/${encodedBinaryName}`, - { - origin: api.appDistributionOrigin, - auth: true, - } - ); - - return _.get(apiResponse, "body"); + async pollUploadStatus(operationName: string): Promise { + return operationPoller.pollOperation({ + pollerName: "App Distribution Upload Poller", + apiOrigin: appDistributionOrigin(), + apiVersion: "v1", + operationResourceName: operationName, + masterTimeout: 5 * 60 * 1000, + backoff: 1000, + maxBackoff: 10 * 1000, + }); } - async addReleaseNotes(releaseId: string, releaseNotes: string): Promise { + async updateReleaseNotes(releaseName: string, releaseNotes: string): Promise { if (!releaseNotes) { utils.logWarning("no release notes specified, skipping"); return; } - utils.logBullet("adding release notes..."); + utils.logBullet("updating release notes..."); const data = { + name: releaseName, releaseNotes: { - releaseNotes, + text: releaseNotes, }, }; + const queryParams = { updateMask: "release_notes.text" }; try { - await api.request("POST", `/v1alpha/apps/${this.appId}/releases/${releaseId}/notes`, { - origin: api.appDistributionOrigin, - auth: true, - data, - }); - } catch (err) { - throw new FirebaseError(`failed to add release notes with ${err.message}`, { exit: 1 }); + await this.appDistroV1Client.patch(`/${releaseName}`, data, { queryParams }); + } catch (err: unknown) { + throw new FirebaseError(`failed to update release notes with ${getErrMsg(err)}`); } utils.logSuccess("added release notes successfully"); } - async enableAccess( - releaseId: string, - emails: string[] = [], - groupIds: string[] = [] + async distribute( + releaseName: string, + testerEmails: string[] = [], + groupAliases: string[] = [], ): Promise { - if (emails.length === 0 && groupIds.length === 0) { + if (testerEmails.length === 0 && groupAliases.length === 0) { utils.logWarning("no testers or groups specified, skipping"); return; } - utils.logBullet("adding testers/groups..."); + utils.logBullet("distributing to testers/groups..."); const data = { - emails, - groupIds, + testerEmails, + groupAliases, }; try { - await api.request("POST", `/v1alpha/apps/${this.appId}/releases/${releaseId}/enable_access`, { - 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, + }); + } + + utils.logSuccess("distributed to testers/groups successfully"); + } + + async listTesters(projectName: string, groupName?: string): Promise { + const listTestersResponse: ListTestersResponse = { + testers: [], + }; + + const client = this.appDistroV1Client; + + let pageToken: string | undefined; + + const filter = groupName ? `groups=${projectName}/groups/${groupName}` : null; + + do { + const queryParams: Record = pageToken ? { pageToken } : {}; + if (filter != null) { + queryParams["filter"] = filter; + } + + let apiResponse: ClientResponse; + try { + apiResponse = await client.get(`${projectName}/testers`, { + queryParams, + }); + } catch (err) { + throw new FirebaseError(`Client request failed to list testers ${err}`); + } + + for (const t of apiResponse.body.testers ?? []) { + listTestersResponse.testers.push({ + name: t.name, + displayName: t.displayName, + groups: t.groups, + lastActivityTime: new Date(t.lastActivityTime), + }); + } + + pageToken = apiResponse.body.nextPageToken; + } while (pageToken); + return listTestersResponse; + } + + async addTesters(projectName: string, emails: string[]): Promise { + try { + await this.appDistroV1Client.request({ + method: "POST", + path: `${projectName}/testers:batchAdd`, + body: { emails: emails }, + }); + } catch (err: unknown) { + throw new FirebaseError(`Failed to add testers ${getErrMsg(err)}`); + } + + utils.logSuccess(`Testers created successfully`); + } + + async removeTesters(projectName: string, emails: string[]): Promise { + let apiResponse; + try { + apiResponse = await this.appDistroV1Client.request< + { emails: string[] }, + BatchRemoveTestersResponse + >({ + method: "POST", + path: `${projectName}/testers:batchRemove`, + body: { emails: emails }, + }); + } catch (err: unknown) { + throw new FirebaseError(`Failed to remove testers ${getErrMsg(err)}`); + } + return apiResponse.body; + } + + async listGroups(projectName: string): Promise { + const listGroupsResponse: ListGroupsResponse = { + groups: [], + }; + + const client = this.appDistroV1Client; + + let pageToken: string | undefined; + + do { + const queryParams: Record = pageToken ? { pageToken } : {}; + try { + const apiResponse = await client.get(`${projectName}/groups`, { + queryParams, + }); + listGroupsResponse.groups.push(...(apiResponse.body.groups ?? [])); + pageToken = apiResponse.body.nextPageToken; + } catch (err) { + throw new FirebaseError(`Client failed to list groups ${err}`); } - throw new FirebaseError(`failed to add testers/groups: ${errorMessage}`, { exit: 1 }); + } while (pageToken); + return listGroupsResponse; + } + + 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: unknown) { + throw new FirebaseError(`Failed to create group ${getErrMsg(err)}`); } + return apiResponse.body; + } + + async deleteGroup(groupName: string): Promise { + try { + await this.appDistroV1Client.request({ + method: "DELETE", + path: groupName, + }); + } catch (err: unknown) { + throw new FirebaseError(`Failed to delete group ${getErrMsg(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: unknown) { + throw new FirebaseError(`Failed to add testers to group ${getErrMsg(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: unknown) { + throw new FirebaseError(`Failed to remove testers from group ${getErrMsg(err)}`); + } + + utils.logSuccess(`Testers removed from group successfully`); + } + + async createReleaseTest( + releaseName: string, + devices: TestDevice[], + loginCredential?: LoginCredential, + testCaseName?: string, + ): Promise { + try { + const response = await this.appDistroV1AlphaClient.request({ + method: "POST", + path: `${releaseName}/tests`, + body: { + deviceExecutions: devices.map(mapDeviceToExecution), + loginCredential, + testCase: testCaseName, + }, + }); + return response.body; + } catch (err: unknown) { + throw new FirebaseError(`Failed to create release test ${getErrMsg(err)}`); + } + } - utils.logSuccess("added testers/groups successfully"); + 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 50de89a201d..3f03d7ea35b 100644 --- a/src/appdistribution/distribution.ts +++ b/src/appdistribution/distribution.ts @@ -1,7 +1,5 @@ import * as fs from "fs-extra"; -import { FirebaseError } from "../error"; -import * as crypto from "crypto"; -import { AppDistributionApp } from "./client"; +import { FirebaseError, getErrMsg } from "../error"; import { logger } from "../logger"; import * as pathUtil from "path"; @@ -12,7 +10,7 @@ export enum DistributionFileType { } /** - * Object representing an APK or IPa file. Used for uploading app distributions. + * Object representing an APK, AAB or IPA file. Used for uploading app distributions. */ export class Distribution { private readonly fileType: DistributionFileType; @@ -20,7 +18,7 @@ export class Distribution { constructor(private readonly path: string) { if (!path) { - throw new FirebaseError("must specify a distribution file"); + throw new FirebaseError("must specify a release binary file"); } const distributionType = path.split(".").pop(); @@ -29,16 +27,18 @@ export class Distribution { distributionType !== DistributionFileType.APK && distributionType !== DistributionFileType.AAB ) { - throw new FirebaseError("Unsupported distribution file format, should be .ipa, .apk or .aab"); + throw new FirebaseError("Unsupported file format, should be .ipa, .apk or .aab"); } + let stat; try { - fs.ensureFileSync(path); - } catch (err) { - logger.info(err); - throw new FirebaseError( - `${path} is not a file. Verify that it points to a distribution binary.` - ); + stat = fs.statSync(path); + } catch (err: unknown) { + logger.info(getErrMsg(err)); + throw new FirebaseError(`File ${path} does not exist: verify that file points to a binary`); + } + if (!stat.isFile()) { + throw new FirebaseError(`${path} is not a file. Verify that it points to a binary.`); } this.path = path; @@ -54,42 +54,7 @@ export class Distribution { return fs.createReadStream(this.path); } - platform(): string { - switch (this.fileType) { - case DistributionFileType.IPA: - return "ios"; - case DistributionFileType.AAB: - case DistributionFileType.APK: - return "android"; - default: - throw new FirebaseError( - "Unsupported distribution file format, should be .ipa, .apk or .aab" - ); - } - } - getFileName(): string { return this.fileName; } - - /** - * Returns the binary name in the format: - * projects//apps//releases/-/binaries/ - * - * This is used to check the distribution upload status. - */ - binaryName(app: AppDistributionApp): Promise { - return new Promise((resolve) => { - const hash = crypto.createHash("sha256"); - const stream = this.readStream(); - stream.on("data", (data) => hash.update(data)); - stream.on("end", () => { - return resolve( - `projects/${app.projectNumber}/apps/${app.appId}/releases/-/binaries/${hash.digest( - "hex" - )}` - ); - }); - }); - } } diff --git a/src/appdistribution/options-parser-util.spec.ts b/src/appdistribution/options-parser-util.spec.ts new file mode 100644 index 00000000000..1e983a73cb8 --- /dev/null +++ b/src/appdistribution/options-parser-util.spec.ts @@ -0,0 +1,205 @@ +import { expect } from "chai"; +import { getLoginCredential, parseTestDevices } from "./options-parser-util"; +import { FirebaseError } from "../error"; +import * as fs from "fs-extra"; +import { rmSync } from "node:fs"; +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(() => { + rmSync(tempdir.name, { recursive: true }); + }); + + describe("getTestDevices", () => { + it("parses a test device", () => { + const optionValue = "model=modelname,version=123,orientation=landscape,locale=en_US"; + + const result = parseTestDevices(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 = parseTestDevices(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 = parseTestDevices(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(() => parseTestDevices(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(() => parseTestDevices(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 new file mode 100644 index 00000000000..1d8d83e5fcf --- /dev/null +++ b/src/appdistribution/options-parser-util.ts @@ -0,0 +1,177 @@ +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- or newline-separated + * file and converts the input into an string[]. + * Value takes precedent over file. + */ +export function parseIntoStringArray(value: string, file: string): string[] { + // 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"); + } + + // The value is split into a string[] + if (value) { + return splitter(value); + } + return []; +} + +/** + * Takes in a string[] or a path to a comma- or newline-separated file of + * testers emails and returns a string[] of emails. + */ +export function getEmails(emails: string[], file: string): string[] { + if (emails.length === 0) { + ensureFileExists(file); + const readFile = fs.readFileSync(file, "utf8"); + return splitter(readFile); + } + return emails; +} + +// Ensures a the file path that the user input is valid +export function ensureFileExists(file: string, message = ""): void { + if (!fs.existsSync(file)) { + throw new FirebaseError(`File ${file} does not exist: ${message}`); + } +} + +// Splits string by either comma or new line +function splitter(value: string): string[] { + return value + .split(/[,\n]/) + .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); + return `projects/${projectNumber}`; +} + +// Gets app name from appId +export function getAppName(options: any): string { + if (!options.app) { + throw new FirebaseError("set the --app option to a valid Firebase app id and try again"); + } + 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 parseTestDevices(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..f90155a4cd2 --- /dev/null +++ b/src/appdistribution/types.ts @@ -0,0 +1,130 @@ +/** + * 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 ListGroupsResponse { + groups: Group[]; + nextPageToken?: string; +} + +export interface Group { + name: string; + displayName: string; + testerCount?: number; + releaseCount?: number; + inviteLinkCount?: number; +} + +export interface ListTestersResponse { + testers: Tester[]; + nextPageToken?: string; +} + +export interface Tester { + name: string; + displayName?: string; + groups?: string[]; + lastActivityTime: Date; +} + +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; + testCase?: string; +} 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/backend.spec.ts b/src/apphosting/backend.spec.ts new file mode 100644 index 00000000000..13495dabd2d --- /dev/null +++ b/src/apphosting/backend.spec.ts @@ -0,0 +1,477 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; + +import * as promptImport 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, + chooseBackends, + getBackendForAmbiguousLocation, + getBackend, +} from "./backend"; +import * as deploymentTool from "../deploymentTool"; +import { FirebaseError } from "../error"; + +describe("apphosting setup functions", () => { + const projectId = "projectId"; + const location = "us-central1"; + const backendId = "backendId"; + + let promptStub: sinon.SinonStubbedInstance; + 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(() => { + promptStub = sinon.stub(promptImport); + promptStub.input.throws("Unexpected input call"); + promptStub.confirm.throws("Unexpected confirm call"); + promptStub.select.throws("Unexpected select call"); + promptStub.checkbox.throws("Unepxected checkbox 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, + "custom-service-account", + cloudBuildConnRepo, + 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", + }, + }); + }); + }); + + describe("ensureAppHostingComputeServiceAccount", () => { + const serviceAccount = "hello@example.com"; + + it("should succeed if the user has permissions for the service account", async () => { + testResourceIamPermissionsStub.resolves(); + createServiceAccountStub.resolves(); + addServiceAccountToRolesStub.resolves(); + + await expect(ensureAppHostingComputeServiceAccount(projectId, serviceAccount)).to.be + .fulfilled; + + expect(testResourceIamPermissionsStub).to.be.calledOnce; + }); + + it("should still add permissions even if the service account already exists", async () => { + testResourceIamPermissionsStub.resolves(); + createServiceAccountStub.rejects(new FirebaseError("error occurred", { status: 409 })); + addServiceAccountToRolesStub.resolves(); + + await expect(ensureAppHostingComputeServiceAccount(projectId, serviceAccount)).to.be + .fulfilled; + + expect(addServiceAccountToRolesStub).to.be.calledOnce; + }); + + 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; + }); + + it("should throw an unexpected error", async () => { + testResourceIamPermissionsStub.rejects( + new FirebaseError("Unexpected error", { status: 500 }), + ); + + await expect( + ensureAppHostingComputeServiceAccount(projectId, serviceAccount), + ).to.be.rejectedWith("Unexpected error"); + + expect(testResourceIamPermissionsStub).to.be.calledOnce; + expect(createServiceAccountStub).to.not.be.called; + 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); + promptStub.select.resolves(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(promptStub.select).to.be.calledWith({ + default: "us-central1", + message: "Please select a location:", + 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(promptStub.select).to.not.be.called; + }); + }); + + describe("chooseBackends", () => { + const backendChickenAsia = { + name: `projects/${projectId}/locations/asia-east1/backends/chicken`, + labels: {}, + createTime: "0", + updateTime: "1", + uri: "https://placeholder.com", + }; + + const backendChickenEurope = { + name: `projects/${projectId}/locations/europe-west4/backends/chicken`, + labels: {}, + createTime: "0", + updateTime: "1", + uri: "https://placeholder.com", + }; + + const backendChickenUS = { + name: `projects/${projectId}/locations/us-central1/backends/chicken`, + labels: {}, + createTime: "0", + updateTime: "1", + uri: "https://placeholder.com", + }; + + const backendCow = { + name: `projects/${projectId}/locations/asia-east1/backends/cow`, + labels: {}, + createTime: "0", + updateTime: "1", + uri: "https://placeholder.com", + }; + + const allBackends = [backendChickenAsia, backendChickenEurope, backendChickenUS, backendCow]; + + it("returns backend if only one is found", async () => { + listBackendsStub.resolves({ + backends: allBackends, + }); + + await expect(chooseBackends(projectId, "cow", /* prompt= */ "")).to.eventually.deep.equal([ + backendCow, + ]); + }); + + it("throws if --force is used when multiple backends are found", async () => { + listBackendsStub.resolves({ + backends: allBackends, + }); + + await expect( + chooseBackends(projectId, "chicken", /* prompt= */ "", /* force= */ true), + ).to.be.rejectedWith( + "Force cannot be used because multiple backends were found with ID chicken.", + ); + }); + + it("throws if no backend is found", async () => { + listBackendsStub.resolves({ + backends: allBackends, + }); + + await expect(chooseBackends(projectId, "farmer", /* prompt= */ "")).to.be.rejectedWith( + 'No backend named "farmer" found.', + ); + }); + + it("lets user choose backends when more than one share a name", async () => { + listBackendsStub.resolves({ + backends: allBackends, + }); + promptStub.checkbox.resolves(["chicken(asia-east1)", "chicken(europe-west4)"]); + + await expect(chooseBackends(projectId, "chicken", /* prompt= */ "")).to.eventually.deep.equal( + [backendChickenAsia, backendChickenEurope], + ); + }); + }); + + 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] }); + promptStub.select.resolves(location); + + await expect( + getBackendForAmbiguousLocation( + projectId, + "foo", + "Please select the location of the backend you'd like to delete:", + ), + ).to.eventually.equal(backendFoo); + + expect(promptStub.select).to.be.calledWith({ + message: "Please select the location of the backend you'd like to delete:", + choices: [location, "otherRegion"], + }); + }); + }); + + describe("getBackend", () => { + const backendChickenAsia = { + name: `projects/${projectId}/locations/asia-east1/backends/chicken`, + labels: {}, + createTime: "0", + updateTime: "1", + uri: "https://placeholder.com", + }; + + const backendChickenEurope = { + name: `projects/${projectId}/locations/europe-west4/backends/chicken`, + labels: {}, + createTime: "0", + updateTime: "1", + uri: "https://placeholder.com", + }; + + const backendCow = { + name: `projects/${projectId}/locations/us-central1/backends/cow`, + labels: {}, + createTime: "0", + updateTime: "1", + uri: "https://placeholder.com", + }; + + const allBackends = [backendChickenAsia, backendChickenEurope, backendCow]; + + it("throws if more than one backend is found", async () => { + listBackendsStub.resolves({ backends: allBackends }); + + await expect(getBackend(projectId, "chicken")).to.be.rejectedWith( + "You have multiple backends with the same chicken ID in regions: " + + "asia-east1, europe-west4. " + + "This is not allowed until we can support more locations. " + + "Please delete and recreate any backends that share an ID with another backend.", + ); + }); + + it("throws if no backend is found", async () => { + listBackendsStub.resolves({ backends: allBackends }); + + await expect(getBackend(projectId, "farmer")).to.be.rejectedWith( + "No backend named farmer found.", + ); + }); + + it("returns backend", async () => { + listBackendsStub.resolves({ backends: allBackends }); + + await expect(getBackend(projectId, "cow")).to.eventually.equal(backendCow); + }); + }); +}); diff --git a/src/apphosting/backend.ts b/src/apphosting/backend.ts new file mode 100644 index 00000000000..c3320c9cf12 --- /dev/null +++ b/src/apphosting/backend.ts @@ -0,0 +1,659 @@ +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 } from "../gcp/apphosting"; +import { addServiceAccountToRoles } from "../gcp/resourceManager"; +import * as iam from "../gcp/iam"; +import { FirebaseError, getErrStatus, getError } from "../error"; +import { input, confirm, select, checkbox, search, Choice } 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"; +import { orchestrateRollout } from "./rollout"; +import * as fuzzy from "fuzzy"; + +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 }; code: string }; + if ( + /HANDSHAKE_FAILURE/.test(maybeNodeError?.cause?.code) || + "EPROTO" === maybeNodeError?.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, + nonInteractive: boolean, + webAppName?: string, + backendId?: string, + serviceAccount?: string, + primaryRegion?: string, + rootDir?: string, +): Promise { + await ensureRequiredApisEnabled(projectId); + + // 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 ? serviceAccount : null); + + // TODO(https://github.com/firebase/firebase-tools/issues/8283): The "primary region" + // is still "locations" in the V1 API. This will change in the V2 API and we may need to update + // the variables and API methods we're calling under the hood when fetching "primary region". + let location = primaryRegion; + let gitRepositoryLink: GitRepositoryLink | undefined; + let branch: string | undefined; + if (nonInteractive) { + if (!backendId || !primaryRegion) { + throw new FirebaseError("nonInteractive mode requires a backendId and primaryRegion"); + } + } else { + if (!location) { + location = await promptLocation(projectId, "Select a primary region to host your backend:\n"); + } + if (!backendId) { + logBullet(`${clc.yellow("===")} Set up your backend`); + backendId = await promptNewBackendId(projectId, location); + logSuccess(`Name set to ${backendId}\n`); + } + if (!rootDir) { + rootDir = await input({ + default: "/", + message: "Specify your app's root directory relative to your repository", + }); + } + + gitRepositoryLink = await githubConnections.linkGitHubRepository(projectId, location); + // 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:? + branch = await githubConnections.promptGitHubBranch(gitRepositoryLink); + logSuccess(`Repo linked successfully!\n`); + } + // Confirm both backendId and location are set at this point + if (!location || !backendId) { + // This should not happen based on the logic above, but it satisfies the type checker. + throw new FirebaseError("Internal error: location or backendId is not defined."); + } + + const webApp = await webApps.getOrCreateWebApp( + projectId, + webAppName ? webAppName : null, + backendId, + ); + if (!webApp) { + logWarning(`Firebase web app not set`); + } + + const createBackendSpinner = ora("Creating your new backend...").start(); + const backend = await createBackend( + projectId, + location, + backendId, + serviceAccount ? serviceAccount : null, + gitRepositoryLink, + webApp?.id, + rootDir, + ); + createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`); + + // In non-interactive mode, we never connected the backend to a github repo. Return + // early and skip the rollout and setting default traffic policy. + if (nonInteractive) { + return; + } + + if (!branch) { + throw new FirebaseError("Branch was not set while connecting to a github repo."); + } + + await setDefaultTrafficPolicy(projectId, location, backendId, branch); + + const confirmRollout = await confirm({ + 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, + buildInput: { + source: { + codebase: { + branch, + }, + }, + }, + isFirstRollout: true, + }); + 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}`); +} + +/** + * Setup up a new App Hosting backend to deploy from source. + */ +export async function doSetupSourceDeploy( + projectId: string, + backendId: string, +): Promise<{ backend: Backend; location: string }> { + const location = await promptLocation( + projectId, + "Select a primary region to host your backend:\n", + ); + const webAppSpinner = ora("Creating a new web app...\n").start(); + const webApp = await webApps.getOrCreateWebApp(projectId, null, backendId); + if (!webApp) { + logWarning(`Firebase web app not set`); + } + webAppSpinner.stop(); + + const createBackendSpinner = ora("Creating your new backend...").start(); + const backend = await createBackend(projectId, location, backendId, null, undefined, webApp?.id); + createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`); + return { + backend, + location, + }; +} + +/** + * Check that all GCP APIs required for App Hosting are enabled. + */ +export async function ensureRequiredApisEnabled(projectId: string): 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), + ]); +} + +/** + * Set up a new App Hosting-type Developer Connect GitRepoLink, optionally with a specific connection ID + */ +export async function createGitRepoLink( + projectId: string, + location: string | null, + connectionId?: string, +): Promise { + await Promise.all([ + ensure(projectId, developerConnectOrigin(), "apphosting", true), + ensure(projectId, secretManagerOrigin(), "apphosting", true), + ensure(projectId, iamOrigin(), "apphosting", true), + ]); + + 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 for your GitRepoLink's connection:\n")); + + await githubConnections.linkGitHubRepository(projectId, location, connectionId); +} + +/** + * 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. Otherwise, it 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 === 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 }, + ); + } else if (err.status !== 404) { + throw new FirebaseError( + "Unexpected error occurred while testing for IAM service account permissions", + { original: err }, + ); + } + } + await provisionDefaultComputeServiceAccount(projectId); +} + +/** + * Prompts the user for a backend id and verifies that it doesn't match a pre-existing backend. + */ +export async function promptNewBackendId(projectId: string, location: string): Promise { + while (true) { + const backendId = await input({ + default: "my-web-app", + message: "Provide a name for your backend [1-30 characters]", + validate: (s) => s.length >= 1 && s.length <= 30, + }); + try { + await apphosting.getBackend(projectId, location, backendId); + } catch (err: unknown) { + if (getErrStatus(err) === 404) { + return backendId; + } + throw new FirebaseError( + `Failed to check if backend with id ${backendId} already exists in ${location}`, + { original: getError(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, + serviceAccount: string | null, + repository: GitRepositoryLink | undefined, + webAppId: string | undefined, + rootDir = "/", +): Promise { + const defaultServiceAccount = defaultComputeServiceAccountEmail(projectId); + const backendReqBody: Omit = { + servingLocality: "GLOBAL_ACCESS", + codebase: repository + ? { + repository: `${repository.name}`, + rootDirectory: rootDir, + } + : undefined, + 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: unknown) { + // 409 Already Exists errors can safely be ignored. + if (getErrStatus(err) !== 409) { + throw err; + } + } + try { + await addServiceAccountToRoles( + projectId, + defaultComputeServiceAccountEmail(projectId), + [ + "roles/firebaseapphosting.computeRunner", + "roles/firebase.sdkAdminServiceAgent", + "roles/developerconnect.readTokenAccessor", + "roles/storage.objectViewer", + ], + /* skipAccountLookup= */ true, + ); + } catch (err: unknown) { + if (getErrStatus(err) === 400) { + logWarning( + "Your App Hosting compute service account is still being provisioned in the background. If you encounter an error, please try again after a few moments.", + ); + } else { + throw err; + } + } +} + +/** + * 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, + }, + }; + const op = await apphosting.updateTraffic(projectId, location, backendId, traffic); + await poller.pollOperation({ + ...apphostingPollerOptions, + pollerName: `updateTraffic-${projectId}-${location}-${backendId}`, + operationResourceName: op.name, + }); +} + +/** + * 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 = "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 select({ + default: DEFAULT_LOCATION, + message: prompt, + choices: allowedLocations, + }); + + logSuccess(`Location set to ${location}.\n`); + + return location; +} + +/** + * Fetches a backend from the server in the specified region (location). + */ +export async function getBackendForLocation( + projectId: string, + location: string, + backendId: string, +): Promise { + try { + return await apphosting.getBackend(projectId, location, backendId); + } catch (err: unknown) { + throw new FirebaseError(`No backend named "${backendId}" found in ${location}.`, { + original: getError(err), + }); + } +} + +/** + * Prompts users to select an existing backend. + * @param projectId the user's project ID + * @param promptMessage prompt message to display to the user + * @return the selected backend ID + */ +export async function promptExistingBackend( + projectId: string, + promptMessage: string, +): Promise { + const { backends } = await apphosting.listBackends(projectId, "-"); + const backendId: string = await search({ + message: promptMessage, + source: (input = ""): Promise[]> => { + return new Promise((resolve) => + resolve([ + ...fuzzy + .filter(input, backends, { + extract: (backend) => apphosting.parseBackendName(backend.name).id, + }) + .map((result) => { + return { + name: apphosting.parseBackendName(result.original.name).id, + value: apphosting.parseBackendName(result.original.name).id, + }; + }), + ]), + ); + }, + }); + return backendId; +} + +/** + * Fetches backends of the given backendId and lets the user choose if more than one is found. + */ +export async function chooseBackends( + projectId: string, + backendId: string, + chooseBackendPrompt: string, + force?: boolean, +): Promise { + let { unreachable, backends } = await apphosting.listBackends(projectId, "-"); + if (unreachable && unreachable.length !== 0) { + logWarning( + `The following locations are currently unreachable: ${unreachable.join(",")}.\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; + } + + if (force) { + throw new FirebaseError( + `Force cannot be used because multiple backends were found with ID ${backendId}.`, + ); + } + const backendsByDisplay = new Map(); + backends.forEach((backend) => { + const { location, id } = apphosting.parseBackendName(backend.name); + backendsByDisplay.set(`${id}(${location})`, backend); + }); + const chosenBackendDisplays = await checkbox({ + message: chooseBackendPrompt, + choices: Array.from(backendsByDisplay.keys(), (name) => { + return { + checked: false, + name: name, + value: name, + }; + }), + }); + const chosenBackends: apphosting.Backend[] = []; + chosenBackendDisplays.forEach((backendDisplay) => { + const backend = backendsByDisplay.get(backendDisplay); + if (backend !== undefined) { + chosenBackends.push(backend); + } + }); + return chosenBackends; +} + +/** + * Fetches a backend from the server. If there are multiple backends with that name (ie multi-regional backends), + * prompts the user to disambiguate. If the force option is specified and multiple backends have the same name, + * it throws an error. + */ +export async function getBackendForAmbiguousLocation( + projectId: string, + backendId: string, + locationDisambugationPrompt: string, + force?: boolean, +): Promise { + let { unreachable, backends } = await apphosting.listBackends(projectId, "-"); + if (unreachable && unreachable.length !== 0) { + logWarning( + `The following locations are currently unreachable: ${unreachable.join(", ")}.\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]; + } + if (force) { + throw new FirebaseError( + `Multiple backends found with ID ${backendId}. Please specify the region of your target backend.`, + ); + } + + const backendsByLocation = new Map(); + backends.forEach((backend) => + backendsByLocation.set(apphosting.parseBackendName(backend.name).location, backend), + ); + const location = await select({ + message: locationDisambugationPrompt, + choices: [...backendsByLocation.keys()], + }); + return backendsByLocation.get(location)!; +} + +/** + * Fetches a backend from the server. If there are multiple backends with the name, it will throw an error + * telling the user that there are other backends with the same name that need to be deleted. + */ +export async function getBackend( + projectId: string, + backendId: string, +): Promise { + let { unreachable, backends } = await apphosting.listBackends(projectId, "-"); + backends = backends.filter( + (backend) => apphosting.parseBackendName(backend.name).id === backendId, + ); + if (backends.length > 1) { + const locations = backends.map((b) => apphosting.parseBackendName(b.name).location); + throw new FirebaseError( + `You have multiple backends with the same ${backendId} ID in regions: ${locations.join(", ")}. This is not allowed until we can support more locations. ` + + "Please delete and recreate any backends that share an ID with another backend.", + ); + } + if (backends.length === 1) { + return backends[0]; + } + if (unreachable && unreachable.length !== 0) { + logWarning( + `Backends with the following primary regions are unreachable: ${unreachable.join(", ")}.\n` + + "If your backend is in one of these regions, please try again later.", + ); + } + throw new FirebaseError(`No backend named ${backendId} found.`); +} diff --git a/src/apphosting/config.spec.ts b/src/apphosting/config.spec.ts new file mode 100644 index 00000000000..b4e87a15488 --- /dev/null +++ b/src/apphosting/config.spec.ts @@ -0,0 +1,475 @@ +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 csmImport from "../gcp/secretManager"; +import * as promptImport from "../prompt"; +import * as dialogs from "./secrets/dialogs"; +import * as config from "./config"; +import { NodeType } from "yaml/dist/nodes/Node"; +import { AppHostingYamlConfig, toEnvList } from "./yaml"; +import { FirebaseError } from "../error"; + +describe("config", () => { + describe("discoverBackendRoot", () => { + let fs: sinon.SinonStubbedInstance; + + beforeEach(() => { + fs = sinon.stub(fsImport); + }); + + afterEach(() => { + sinon.verifyAndRestore(); + }); + + it("finds apphosting.yaml at cwd", () => { + fs.listFiles.withArgs("/parent/cwd").returns(["apphosting.yaml"]); + expect(config.discoverBackendRoot("/parent/cwd")).equals("/parent/cwd"); + }); + + it("finds apphosting.yaml in a parent directory", () => { + fs.listFiles.withArgs("/parent/cwd").returns(["random_file.txt"]); + fs.listFiles.withArgs("/parent").returns(["apphosting.yaml"]); + + expect(config.discoverBackendRoot("/parent/cwd")).equals("/parent"); + }); + + it("returns null if it finds firebase.json without finding apphosting.yaml", () => { + fs.listFiles.withArgs("/parent/cwd").returns([]); + fs.listFiles.withArgs("/parent").returns(["firebase.json"]); + + expect(config.discoverBackendRoot("/parent/cwd")).equals(null); + }); + + it("returns if it reaches the fs root", () => { + fs.listFiles.withArgs("/parent/cwd").returns([]); + fs.listFiles.withArgs("/parent").returns(["random_file.txt"]); + fs.listFiles.withArgs("/").returns([]); + + expect(config.discoverBackendRoot("/parent/cwd")).equals(null); + }); + + it("discovers backend root from any apphosting yaml file", () => { + fs.listFiles.withArgs("/parent/cwd").returns(["apphosting.staging.yaml"]); + + expect(config.discoverBackendRoot("/parent/cwd")).equals("/parent/cwd"); + }); + }); + + 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 discoverBackendRoot: 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); + discoverBackendRoot = sinon.stub(config, "discoverBackendRoot"); + 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("{}"); + discoverBackendRoot.returns("CWD"); + load.returns(doc); + findEnv.withArgs(doc, "SECRET").returns({ variable: "SECRET", secret: "SECRET" }); + + await config.maybeAddSecretToYaml("SECRET"); + + expect(discoverBackendRoot).to.have.been.called; + expect(load).to.have.been.calledWith("CWD/apphosting.yaml"); + expect(prompt.confirm).to.not.have.been.called; + expect(prompt.input).to.not.have.been.called; + }); + + it("inserts into an existing doc", async () => { + const doc = yaml.parseDocument("{}"); + discoverBackendRoot.returns("CWD"); + 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(discoverBackendRoot).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.input).to.not.have.been.called; + }); + + it("inserts into an new doc", async () => { + const doc = new yaml.Document(); + discoverBackendRoot.returns(null); + findEnv.withArgs(doc, "SECRET").returns(undefined); + prompt.confirm.resolves(true); + prompt.input.resolves("CWD"); + envVarForSecret.resolves("SECRET_VARIABLE"); + + await config.maybeAddSecretToYaml("SECRET"); + + expect(discoverBackendRoot).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.input).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); + }); + }); + + describe("listAppHostingFilesInPath", () => { + let fs: sinon.SinonStubbedInstance; + + beforeEach(() => { + fs = sinon.stub(fsImport); + }); + + afterEach(() => { + sinon.verifyAndRestore(); + }); + + it("only returns valid App Hosting YAML files", () => { + fs.listFiles + .withArgs("/parent/cwd") + .returns([ + "test1.js", + "test2.js", + "apphosting.yaml", + "test4.js", + "apphosting.staging.yaml", + ]); + + const apphostingYamls = config.listAppHostingFilesInPath("/parent/cwd"); + expect(apphostingYamls).to.deep.equal([ + "/parent/cwd/apphosting.yaml", + "/parent/cwd/apphosting.staging.yaml", + ]); + }); + }); + + describe("maybeGenerateEmulatorsYaml", () => { + let discoverBackendRoot: sinon.SinonStub; + let overrideChosenEnv: sinon.SinonStub; + let loadFromFile: sinon.SinonStub; + let store: sinon.SinonStub; + let fs: sinon.SinonStubbedInstance; + let prompt: sinon.SinonStubbedInstance; + + const existingYaml = AppHostingYamlConfig.empty(); + existingYaml.env = { + VAR: { value: "value" }, + API_KEY: { secret: "api-key" }, + API_KEY2: { secret: "api-key2" }, + }; + + beforeEach(() => { + discoverBackendRoot = sinon.stub(config, "discoverBackendRoot"); + overrideChosenEnv = sinon.stub(config, "overrideChosenEnv"); + store = sinon.stub(config, "store"); + loadFromFile = sinon.stub(AppHostingYamlConfig, "loadFromFile"); + fs = sinon.stub(fsImport); + prompt = sinon.stub(promptImport); + }); + + afterEach(() => { + sinon.verifyAndRestore(); + }); + + it("noops if emulators.yaml already exists", async () => { + discoverBackendRoot.withArgs("/project").returns("/project"); + fs.fileExistsSync.withArgs(`/project/${config.APPHOSTING_EMULATORS_YAML_FILE}`).returns(true); + + await config.maybeGenerateEmulatorYaml("projectId", "/project"); + + expect(prompt.confirm).to.not.have.been.called; + expect(store).to.not.have.been.called; + }); + + // This allows us to prompt to give devs access to prod keys + it("returns existing config even if the user does not create apphosting.emulator.yaml", async () => { + discoverBackendRoot.withArgs("/project").returns("/project"); + fs.fileExistsSync + .withArgs(`/project/${config.APPHOSTING_EMULATORS_YAML_FILE}`) + .returns(false); + // Do not create emulator file + prompt.confirm.resolves(false); + loadFromFile.resolves(existingYaml); + + await expect( + config.maybeGenerateEmulatorYaml("projectId", "/project"), + ).to.eventually.deep.equal(toEnvList(existingYaml.env)); + }); + + it("returns overwritten config", async () => { + discoverBackendRoot.withArgs("/project").returns("/project"); + fs.fileExistsSync + .withArgs(`/project/${config.APPHOSTING_EMULATORS_YAML_FILE}`) + .returns(false); + loadFromFile.resolves(existingYaml); + // Create emulator file + prompt.confirm.resolves(true); + overrideChosenEnv.resolves({ + API_KEY2: { secret: "test-api-key2" }, + }); + store.resolves(); + + await expect( + config.maybeGenerateEmulatorYaml("projectId", "/project"), + ).to.eventually.deep.equal([ + { variable: "VAR", value: "value" }, + { variable: "API_KEY", secret: "api-key" }, + { variable: "API_KEY2", secret: "test-api-key2" }, + ]); + + expect(overrideChosenEnv.firstCall.args[1]).to.deep.equal({ + VAR: { value: "value" }, + API_KEY: { secret: "api-key" }, + API_KEY2: { secret: "api-key2" }, + }); + expect(store).to.have.been.called; + const emulatorYaml = store.firstCall.args[1] as yaml.Document; + expect(emulatorYaml.toJSON()).to.deep.equal({ + env: [{ variable: "API_KEY2", secret: "test-api-key2" }], + }); + }); + }); + + describe("overrideChosenEnv", () => { + let csm: sinon.SinonStubbedInstance; + let prompt: sinon.SinonStubbedInstance; + + beforeEach(() => { + csm = sinon.stub(csmImport); + prompt = sinon.stub(promptImport); + }); + + afterEach(() => { + sinon.verifyAndRestore(); + }); + + it("noops with no envs", async () => { + await expect(config.overrideChosenEnv(undefined, {})).to.eventually.deep.equal({}); + + expect(promptImport.checkbox).to.not.have.been.called; + expect(csmImport.getSecret).to.not.have.been.called; + }); + + it("noops with no selected envs", async () => { + const originalEnv: Record> = { + VARIABLE: { value: "value" }, + API_KEY: { secret: "api-key" }, + }; + + prompt.checkbox.onFirstCall().resolves([]); + + await expect(config.overrideChosenEnv(undefined, originalEnv)).to.eventually.deep.equal({}); + + expect(prompt.checkbox).to.have.been.calledOnce; + expect(csm.secretExists).to.not.have.been.called; + }); + + it("can override plaintext values", async () => { + const originalEnv: Record = { + VARIABLE: { variable: "VARIABLE", value: "value" }, + VARIABLE2: { variable: "VARIABLE2", value: "value2" }, + }; + + prompt.checkbox.onFirstCall().resolves(["VARIABLE2"]); + prompt.input.onFirstCall().resolves("new-value2"); + + await expect(config.overrideChosenEnv(undefined, originalEnv)).to.eventually.deep.equal({ + VARIABLE2: { variable: "VARIABLE2", value: "new-value2" }, + }); + + expect(prompt.checkbox).to.have.been.calledOnce; + expect(prompt.input).to.have.been.calledOnce; + expect(csmImport.secretExists).to.not.have.been.called; + }); + + it("throws when trying to overwrite secrets without knowing the project", async () => { + const originalEnv: Record = { + API_KEY: { variable: "API_KEY", secret: "api-key" }, + }; + + prompt.checkbox.onFirstCall().resolves(["API_KEY"]); + + await expect(config.overrideChosenEnv(undefined, originalEnv)).to.be.rejectedWith( + FirebaseError, + /Need a project ID to overwrite a secret./, + ); + }); + + it("can create new secrets", async () => { + const originalEnv: Record = { + API_KEY: { variable: "API_KEY", secret: "api-key" }, + }; + + prompt.checkbox.onFirstCall().resolves(["API_KEY"]); + prompt.input.onFirstCall().resolves("test-api-key"); + csm.secretExists.withArgs("project", "test-api-key").resolves(false); + prompt.password.onFirstCall().resolves("plaintext secret value"); + + await expect(config.overrideChosenEnv("project", originalEnv)).to.eventually.deep.equal({ + API_KEY: { variable: "API_KEY", secret: "test-api-key" }, + }); + + expect(prompt.checkbox).to.have.been.calledOnce; + expect(prompt.input).to.have.been.calledOnce; + expect(prompt.password).to.have.been.calledOnce; + expect(csm.secretExists).to.have.been.calledOnce; + expect(csm.createSecret).to.have.been.calledOnce; + expect(csm.addVersion).to.have.been.calledOnce; + expect(csm.addVersion.getCall(0).args[2]).to.equal("plaintext secret value"); + }); + + it("can create new secrets after warning about reuse", async () => { + const originalEnv: Record = { + API_KEY: { variable: "API_KEY", secret: "api-key" }, + }; + + prompt.checkbox.onFirstCall().resolves(["API_KEY"]); + prompt.input.onFirstCall().resolves("test-api-key"); + csm.secretExists.withArgs("project", "test-api-key").resolves(true); + prompt.select.onFirstCall().resolves("pick-new"); + prompt.input.onSecondCall().resolves("test-api-key2"); + prompt.password.resolves("plaintext secret value"); + + await expect(config.overrideChosenEnv("project", originalEnv)).to.eventually.deep.equal({ + API_KEY: { variable: "API_KEY", secret: "test-api-key2" }, + }); + + expect(prompt.checkbox).to.have.been.calledOnce; + expect(prompt.input).to.have.been.calledTwice; + expect(prompt.select).to.have.been.calledOnce; + expect(prompt.password).to.have.been.calledOnce; + expect(csm.secretExists).to.have.been.calledTwice; + expect(csm.createSecret).to.have.been.calledOnce; + expect(csm.addVersion).to.have.been.calledOnce; + expect(csm.addVersion.getCall(0).args[2]).to.equal("plaintext secret value"); + }); + + it("can reuse secrets", async () => { + const originalEnv: Record = { + API_KEY: { variable: "API_KEY", secret: "api-key" }, + }; + + prompt.checkbox.onFirstCall().resolves(["API_KEY"]); + prompt.input.onFirstCall().resolves("test-api-key"); + csm.secretExists.withArgs("project", "test-api-key").resolves(true); + prompt.select.onFirstCall().resolves("reuse"); + + await expect(config.overrideChosenEnv("project", originalEnv)).to.eventually.deep.equal({ + API_KEY: { variable: "API_KEY", secret: "test-api-key" }, + }); + + expect(prompt.checkbox).to.have.been.calledOnce; + expect(prompt.input).to.have.been.calledOnce; + expect(prompt.select).to.have.been.calledOnce; + expect(csm.secretExists).to.have.been.calledOnce; + expect(csm.createSecret).to.not.have.been.called; + expect(csm.addVersion).to.not.have.been.called; + }); + + it("suggests test key names", () => { + expect(config.suggestedTestKeyName("GOOGLE_GENAI_API_KEY")).to.equal( + "test-google-genai-api-key", + ); + }); + }); +}); diff --git a/src/apphosting/config.ts b/src/apphosting/config.ts new file mode 100644 index 00000000000..c8487626980 --- /dev/null +++ b/src/apphosting/config.ts @@ -0,0 +1,351 @@ +import { join, dirname } from "path"; +import { writeFileSync } from "fs"; +import * as yaml from "yaml"; +import * as clc from "colorette"; + +import * as fs from "../fsutils"; +import { NodeType } from "yaml/dist/nodes/Node"; +import * as prompt from "../prompt"; +import * as dialogs from "./secrets/dialogs"; +import { AppHostingYamlConfig, EnvMap, toEnvList } from "./yaml"; +import { logger } from "../logger"; +import * as csm from "../gcp/secretManager"; +import { FirebaseError, getError } from "../error"; + +// Common config across all environments +export const APPHOSTING_BASE_YAML_FILE = "apphosting.yaml"; + +// Modern version of local configuration that is intended to be safe to commit. +// In order to ensure safety, values that are secret environment variables in +// apphosting.yaml cannot be made plaintext in apphosting.emulators.yaml +export const APPHOSTING_EMULATORS_YAML_FILE = "apphosting.emulator.yaml"; + +// Legacy/undocumented version of local configuration that is allowed to store +// values that are secrets in preceeding files as plaintext. It is not safe +// to commit to SCM +export const APPHOSTING_LOCAL_YAML_FILE = "apphosting.local.yaml"; + +export const APPHOSTING_YAML_FILE_REGEX = /^apphosting(\.[a-z0-9_]+)?\.yaml$/; + +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[]; +} + +/** + * Returns the absolute path for an app hosting backend root. + * + * Backend root is determined by looking for an apphosting.yaml + * file. + */ +export function discoverBackendRoot(cwd: string): string | null { + let dir = cwd; + + while (true) { + const files = fs.listFiles(dir); + if (files.some((file) => APPHOSTING_YAML_FILE_REGEX.test(file))) { + return dir; + } + + // We've hit project root + if (files.includes("firebase.json")) { + return null; + } + + const parent = dirname(dir); + // We've hit the filesystem root + if (parent === dir) { + return null; + } + dir = parent; + } +} + +/** + * Returns paths of apphosting config files in the given path + */ +export function listAppHostingFilesInPath(path: string): string[] { + return fs + .listFiles(path) + .filter((file) => APPHOSTING_YAML_FILE_REGEX.test(file)) + .map((file) => join(path, file)); +} + +/** + * Load an apphosting yaml file if it exists. + * Throws if the file exists but is malformed. + * Returns an empty document if the file does not exist. + */ +export function load(yamlPath: string): yaml.Document { + let raw: string; + try { + raw = fs.readFile(yamlPath); + } catch (err: any) { + if (err.code !== "ENOENT") { + throw new FirebaseError(`Unexpected error trying to load ${yamlPath}`, { + original: getError(err), + }); + } + return new yaml.Document(); + } + 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); +} + +// We must go through the exports object for stubbing to work in tests. +const dynamicDispatch = exports as { + discoverBackendRoot: typeof discoverBackendRoot; + load: typeof load; + findEnv: typeof findEnv; + upsertEnv: typeof upsertEnv; + store: typeof store; + overrideChosenEnv: typeof overrideChosenEnv; +}; + +/** + * Given a secret name, guides the user whether they want to add that secret to the specified apphosting yaml file. + * If an the file exists and includes the secret already is used as a variable name, exist early. + * If the file 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, + fileName: string = APPHOSTING_BASE_YAML_FILE, +): Promise { + // Note: The API proposal suggested that we would check if the env exists. This is stupidly hard because the YAML may not exist yet. + const backendRoot = dynamicDispatch.discoverBackendRoot(process.cwd()); + let path: string | undefined; + let projectYaml: yaml.Document; + if (backendRoot) { + path = join(backendRoot, fileName); + 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 ${fileName}?`, + default: true, + }); + if (!addToYaml) { + return; + } + if (!path) { + path = await prompt.input({ + message: `It looks like you don't have an ${fileName} yet. Where would you like to store it?`, + default: process.cwd(), + }); + path = join(path, fileName); + } + const envName = await dialogs.envVarForSecret( + secretName, + /* trimTestPrefix= */ fileName === APPHOSTING_EMULATORS_YAML_FILE, + ); + dynamicDispatch.upsertEnv(projectYaml, { + variable: envName, + secret: secretName, + }); + dynamicDispatch.store(path, projectYaml); +} + +/** + * Generates an apphosting.emulator.yaml if the user chooses to do so. + * Returns the resolved env that an emulator would see so that future code can + * grant access. + */ +export async function maybeGenerateEmulatorYaml( + projectId: string | undefined, + backendRoot: string, +): Promise { + // Even if the app is in /project/app, the user might have their apphosting.yaml file in /project/apphosting.yaml. + // Walk up the tree to see if we find other local files so that we can put apphosting.emulator.yaml in the right place. + const basePath = dynamicDispatch.discoverBackendRoot(backendRoot) || backendRoot; + if (fs.fileExistsSync(join(basePath, APPHOSTING_EMULATORS_YAML_FILE))) { + logger.debug( + "apphosting.emulator.yaml already exists, skipping generation and secrets access prompt", + ); + return null; + } + + let baseConfig: AppHostingYamlConfig; + try { + baseConfig = await AppHostingYamlConfig.loadFromFile(join(basePath, APPHOSTING_BASE_YAML_FILE)); + } catch { + baseConfig = AppHostingYamlConfig.empty(); + } + const createFile = await prompt.confirm({ + message: + "The App Hosting emulator uses a file called apphosting.emulator.yaml to override " + + "values in apphosting.yaml for local testing. This codebase does not have one, would you like " + + "to create it?", + default: true, + }); + if (!createFile) { + return toEnvList(baseConfig.env); + } + + const newEnv = await dynamicDispatch.overrideChosenEnv(projectId, baseConfig.env || {}); + // Ensures we don't write 'null' if there are no overwritten env. + const envList = Object.entries(newEnv); + if (envList.length) { + const newYaml = new yaml.Document(); + for (const [variable, env] of envList) { + // N.B. This is a bit weird. We're not defensively assuring that the key of the variable name is used, + // but this ensures that the generated YAML shows "variable" before "value" or "secret", which is what + // docs canonically show. + dynamicDispatch.upsertEnv(newYaml, { variable, ...env }); + } + dynamicDispatch.store(join(basePath, APPHOSTING_EMULATORS_YAML_FILE), newYaml); + } else { + // The yaml library _always_ stringifies empty objects and arrays as {} and [] and there is + // no setting on toString to change this, so we'll craft the YAML file manually. + const sample = + "env:\n" + + "#- variable: ENV_VAR_NAME\n" + + "# value: plaintext value\n" + + "#- variable: SECRET_ENV_VAR_NAME\n" + + "# secret: cloud-secret-manager-id\n"; + writeFileSync(join(basePath, APPHOSTING_EMULATORS_YAML_FILE), sample); + } + return toEnvList({ ...baseConfig.env, ...newEnv }); +} + +/** + * Prompts a user which env they'd like to override and then asks them for the new values. + * Values cannot change between plaintext and secret while overriding them. Users are warned/asked to confirm + * if they choose to reuse an existing secret value. Secret reference IDs are suggested with a test- prefix to suggest + * a design pattern. + * Returns a map of modified environment variables. + */ +export async function overrideChosenEnv( + projectId: string | undefined, + env: EnvMap, +): Promise { + const names = Object.keys(env); + if (!names.length) { + return {}; + } + + const toOverwrite = await prompt.checkbox({ + message: "Which environment variables would you like to override?", + choices: names, + }); + + if (!projectId && toOverwrite.some((name) => "secret" in env[name])) { + throw new FirebaseError( + `Need a project ID to overwrite a secret. Either use ${clc.bold("firebase use")} or pass the ${clc.bold("--project")} flag`, + ); + } + + const newEnv: Record = {}; + for (const name of toOverwrite) { + if ("value" in env[name]) { + const newValue = await prompt.input(`What new value would you like for plaintext ${name}?`); + newEnv[name] = { variable: name, value: newValue }; + continue; + } + + let secretRef: string; + let action: "reuse" | "create" | "pick-new" = "pick-new"; + while (action === "pick-new") { + secretRef = await prompt.input({ + message: `What would you like to name the secret reference for ${name}?`, + default: suggestedTestKeyName(name), + }); + + if (await csm.secretExists(projectId!, secretRef)) { + action = await prompt.select<"reuse" | "pick-new">({ + message: + "This secret reference already exists, would you like to reuse it or create a new one?", + choices: [ + { name: "Reuse it", value: "reuse" }, + { name: "Create a new one", value: "pick-new" }, + ], + }); + } else { + action = "create"; + } + } + + newEnv[name] = { variable: name, secret: secretRef! }; + if (action === "reuse") { + continue; + } + + const secretValue = await prompt.password( + `What new value would you like for secret ${name} [input is hidden]?`, + ); + // TODO: Do we need to support overriding locations? Inferring them from the original? + await csm.createSecret(projectId!, secretRef!, { [csm.FIREBASE_MANAGED]: "apphosting" }); + await csm.addVersion(projectId!, secretRef!, secretValue); + } + + return newEnv; +} + +export function suggestedTestKeyName(variable: string): string { + return "test-" + variable.replace(/_/g, "-").toLowerCase(); +} 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..d0c41dd178b --- /dev/null +++ b/src/apphosting/githubConnections.spec.ts @@ -0,0 +1,753 @@ +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(); + const knownConnectionId = "apphosting-github-conn-test123"; + + let promptStub: sinon.SinonStubbedInstance; + 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 listAllLinkableGitRepositoriesStub: sinon.SinonStub; + let getProjectNumberStub: sinon.SinonStub; + let openInBrowserPopupStub: sinon.SinonStub; + let listConnectionsStub: sinon.SinonStub; + let fetchGitHubInstallationsStub: sinon.SinonStub; + + beforeEach(() => { + promptStub = sandbox.stub(prompt); + promptStub.input.throws("Unexpected input call"); + promptStub.search.throws("Unexpected search call"); + promptStub.confirm.throws("Unexpected confirm 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"); + listAllLinkableGitRepositoriesStub = 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"); + listConnectionsStub = sandbox + .stub(devconnect, "listAllConnections") + .throws("Unexpected listAllConnections call"); + fetchGitHubInstallationsStub = sandbox + .stub(devconnect, "fetchGitHubInstallations") + .throws("Unexpected fetchGitHubInstallations call"); + sandbox.stub(repo, "generateConnectionId").returns(knownConnectionId); + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + const op = { + name: `projects/${projectId}/locations/${location}/connections/${knownConnectionId}`, + done: true, + }; + const pendingConn = { + name: `projects/${projectId}/locations/${location}/connections/${knownConnectionId}`, + 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/${knownConnectionId}`, + disabled: false, + createTime: "0", + updateTime: "1", + installationState: { + stage: "COMPLETE", + message: "complete", + actionUri: "https://google.com", + }, + reconciling: false, + githubConfig: { + authorizerCredential: { + oauthTokenSecretVersion: "secret", + username: "test-user", + }, + }, + }; + 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); + promptStub.input.onFirstCall().resolves("any key"); + + await repo.getOrCreateConnection(projectId, location, knownConnectionId); + expect(createConnectionStub).to.be.calledWith(projectId, location, knownConnectionId); + }); + + it("checks if secret manager admin role is granted for developer connect P4SA when creating an oauth connection", async () => { + listConnectionsStub.returns([]); // Mock a situation where the oauth connection does not exist. + createConnectionStub.resolves(op); + pollOperationStub.resolves(pendingConn); + getConnectionStub.onFirstCall().resolves(completeConn); + promptStub.input.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); + listAllLinkableGitRepositoriesStub.resolves(repos.repositories); + promptStub.search.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, + knownConnectionId, + repos.repositories[0].remoteUri, + ); + expect(createRepositoryStub).to.be.calledWith( + projectId, + location, + knownConnectionId, + "test-repo0", + repos.repositories[0].remoteUri, + ); + }); + + it("links a github repository without an existing oauth connection", async () => { + // linkGitHubRepository() + // -getOrCreateFullyInstalledGithubConnection() + // --getOrCreateOauthConnection + listConnectionsStub.onFirstCall().resolves([]); // Oauth connection does not yet exist. + createConnectionStub.onFirstCall().resolves({ name: "op" }); // Poll on createsConnection(). + pollOperationStub.onFirstCall().resolves(completeConn); // Polling returns the connection created. + getProjectNumberStub.onFirstCall().resolves(projectId); // Verifies the secret manager grant. + + // -getOrCreateFullyInstalledGithubConnection() + // promptGitHubInstallation fetches the installations. + fetchGitHubInstallationsStub.resolves([ + { + id: "installationID", + name: "main-user", + type: "user", + }, + ]); + + promptStub.search.onFirstCall().resolves("installationID"); // Uses existing Github Account installation. + listConnectionsStub.onSecondCall().resolves([completeConn]); // getConnectionForInstallation() returns sentinel connection. + + // -- createFullyInstalledConnection + createConnectionStub.onSecondCall().resolves({ name: "op" }); // Poll on createsConnection(). + pollOperationStub.onSecondCall().resolves(pendingConn); // Polling returns the connection created. + promptStub.input.onFirstCall().resolves("enter"); // Enter to signal setup finished. + getConnectionStub.onFirstCall().resolves(completeConn); // getConnection() returns a completed connection. + + // linkGitHubRepository() + // -promptCloneUri() + listAllLinkableGitRepositoriesStub.resolves(repos.repositories); // fetchRepositoryCloneUris() returns repos + promptStub.search.onSecondCall().resolves(repos.repositories[0].remoteUri); // promptCloneUri() returns repo's clone uri. + + // linkGitHubRepository() + getConnectionStub.onSecondCall().resolves(completeConn); // getOrCreateConnection() returns a completed connection. + + // -getOrCreateRepository() + getRepositoryStub.rejects(new FirebaseError("error", { status: 404 })); // Repo not yet created. + createRepositoryStub.resolves({ name: "op" }); // Poll on createGitRepositoryLink(). + pollOperationStub.resolves(repos.repositories[0]); // Polling returns the gitRepoLink. + + const r = await repo.linkGitHubRepository(projectId, location); + expect(getConnectionStub).to.be.calledWith(projectId, location, knownConnectionId); + expect(createConnectionStub).to.be.calledWith(projectId, location, knownConnectionId); + + expect(r).to.be.deep.equal(repos.repositories[0]); // Returns the correct repo. + }); + + it("links a github repository using an existing oauth connection", async () => { + // linkGitHubRepository() + // -getOrCreateFullyInstalledGithubConnection() + listConnectionsStub.onFirstCall().resolves([completeConn]); // getOrCreateOauthConnection() Fetches a completed connection. + + // promptGitHubInstallation fetches the installations. + fetchGitHubInstallationsStub.resolves([ + { + id: "installationID", + name: "main-user", + type: "user", + }, + ]); + + promptStub.search.onFirstCall().resolves("installationID"); // Uses existing Github Account installation. + listConnectionsStub.onSecondCall().resolves([completeConn]); // getConnectionForInstallation() returns sentinel connection. + createConnectionStub.onFirstCall().resolves({ name: "op" }); // Poll on createsConnection(). + pollOperationStub.onFirstCall().resolves(completeConn); // Polling returns the oauth stub connection created. + + // linkGitHubRepository() + // -promptCloneUri() + listAllLinkableGitRepositoriesStub.resolves(repos.repositories); // fetchRepositoryCloneUris() returns repos + promptStub.search.onSecondCall().resolves(repos.repositories[0].remoteUri); // promptCloneUri() returns repo's clone uri. + + // linkGitHubRepository() + getConnectionStub.onFirstCall().resolves(completeConn); // getOrCreateConnection() returns a completed connection. + + // -getOrCreateRepository() + getRepositoryStub.rejects(new FirebaseError("error", { status: 404 })); // Repo not yet created. + createRepositoryStub.resolves({ name: "op" }); // Poll on createGitRepositoryLink(). + pollOperationStub.onSecondCall().resolves(repos.repositories[0]); // Polling returns the gitRepoLink. + + const r = await repo.linkGitHubRepository(projectId, location); + expect(getConnectionStub).to.be.calledWith(projectId, location, knownConnectionId); + expect(createConnectionStub).to.be.calledOnce; + expect(createConnectionStub).to.be.calledWith(projectId, location, knownConnectionId); + + expect(r).to.be.deep.equal(repos.repositories[0]); // Returns the correct repo. + }); + + it("links a github repository with a new named connection", async () => { + const namedConnectionId = `apphosting-named-${location}`; + + const namedCompleteConn = { + name: `projects/${projectId}/locations/${location}/connections/${namedConnectionId}`, + disabled: false, + createTime: "0", + updateTime: "1", + installationState: { + stage: "COMPLETE", + message: "complete", + actionUri: "https://google.com", + }, + reconciling: false, + }; + + // linkGitHubRepository() + // -getOrCreateFullyInstalledGithubConnection() + getConnectionStub.onFirstCall().rejects(new FirebaseError("error", { status: 404 })); // Named connection does not exist. + getConnectionStub.onSecondCall().resolves(completeConn); // Fetches oauth sentinel. + // promptGitHubInstallation fetches the installations. + fetchGitHubInstallationsStub.resolves([ + { + id: "installationID", + name: "main-user", + type: "user", + }, + ]); + promptStub.search.onFirstCall().resolves("installationID"); // Uses existing Github Account installation. + listConnectionsStub.resolves([completeConn]); // Installation has sentinel connection but not the named one. + + // --createFullyInstalledConnection + createConnectionStub.onFirstCall().resolves({ name: "op" }); // Poll on createsConnection(). + pollOperationStub.onFirstCall().resolves(namedCompleteConn); // Polling returns the connection created. + + // linkGitHubRepository() + // -promptCloneUri() + listAllLinkableGitRepositoriesStub.resolves(repos.repositories); // fetchRepositoryCloneUris() returns repos + promptStub.search.onSecondCall().resolves(repos.repositories[0].remoteUri); // promptCloneUri() returns repo's clone uri. + + // linkGitHubRepository() + getConnectionStub.onThirdCall().resolves(namedCompleteConn); // getOrCreateConnection() returns a completed connection. + + // -getOrCreateRepository() + getRepositoryStub.rejects(new FirebaseError("error", { status: 404 })); // Repo not yet created. + createRepositoryStub.resolves({ name: "op" }); // Poll on createGitRepositoryLink(). + pollOperationStub.onSecondCall().resolves(repos.repositories[0]); // Polling returns the gitRepoLink. + + const r = await repo.linkGitHubRepository(projectId, location, namedConnectionId); + + expect(r).to.be.deep.equal(repos.repositories[0]); + expect(getConnectionStub).to.be.calledWith(projectId, location, namedConnectionId); + expect(createConnectionStub).to.be.calledWith(projectId, location, namedConnectionId, { + appInstallationId: "installationID", + authorizerCredential: completeConn.githubConfig.authorizerCredential, + }); + }); + + it("reuses an existing named connection to link github repo", async () => { + const namedConnectionId = `apphosting-named-${location}`; + + const namedCompleteConn = { + name: `projects/${projectId}/locations/${location}/connections/${namedConnectionId}`, + disabled: false, + createTime: "0", + updateTime: "1", + installationState: { + stage: "COMPLETE", + message: "complete", + actionUri: "https://google.com", + }, + reconciling: false, + }; + + // linkGitHubRepository() + // -getOrCreateFullyInstalledGithubConnection() + getConnectionStub.onFirstCall().resolves(namedCompleteConn); // Named connection already exists. + + // -promptCloneUri() + listAllLinkableGitRepositoriesStub.resolves(repos.repositories); // fetchRepositoryCloneUris() returns repos + promptStub.search.onFirstCall().resolves(repos.repositories[0].remoteUri); // Selects the repo's clone uri. + + // linkGitHubRepository() + getConnectionStub.onSecondCall().resolves(namedCompleteConn); // getOrCreateConnection() returns a completed connection. + + // -getOrCreateRepository() + getRepositoryStub.rejects(new FirebaseError("error", { status: 404 })); // Repo not yet created. + createRepositoryStub.resolves({ name: "op" }); // Poll on createGitRepositoryLink(). + pollOperationStub.resolves(repos.repositories[0]); // Polling returns the gitRepoLink. + + const r = await repo.linkGitHubRepository(projectId, location, namedConnectionId); + + expect(r).to.be.deep.equal(repos.repositories[0]); + expect(getConnectionStub).to.be.calledWith(projectId, location, namedConnectionId); + expect(getConnectionStub).to.not.be.calledWith(projectId, location, knownConnectionId); + expect(listConnectionsStub).to.not.be.called; + expect(createConnectionStub).to.not.be.called; + }); + + it("re-uses existing repository it already exists", async () => { + getConnectionStub.resolves(completeConn); + listAllLinkableGitRepositoriesStub.resolves(repos.repositories); + promptStub.search.onFirstCall().resolves(repos.repositories[0].remoteUri); + getRepositoryStub.resolves(repos.repositories[0]); + + const r = await repo.getOrCreateRepository( + projectId, + location, + knownConnectionId, + repos.repositories[0].remoteUri, + ); + expect(r).to.be.deep.equal(repos.repositories[0]); + }); + }); + + describe("fetchRepositoryCloneUris", () => { + 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 repo0 = mockRepo("repo-0"); + const repo1 = mockRepo("repo-1"); + listAllLinkableGitRepositoriesStub.onFirstCall().resolves([repo0, repo1]); + + const repos = await repo.fetchRepositoryCloneUris(projectId, conn0); + + expect(repos.length).to.equal(2); + expect(repos).to.deep.equal([repo0.cloneUri, repo1.cloneUri]); + }); + }); + + 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, location); + 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("listValidInstallations", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + let fetchGitHubInstallationsStub: sinon.SinonStub; + + beforeEach(() => { + fetchGitHubInstallationsStub = sandbox + .stub(devconnect, "fetchGitHubInstallations") + .throws("Unexpected fetchGitHubInstallations call"); + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + it("only lists organizations and authorizer github account", async () => { + const conn = mockConn("1"); + conn.githubConfig = { + authorizerCredential: { + oauthTokenSecretVersion: "blah", + username: "main-user", + }, + }; + + fetchGitHubInstallationsStub.resolves([ + { + id: "1", + name: "main-user", + type: "user", + }, + { + id: "2", + name: "org-1", + type: "organization", + }, + { + id: "3", + name: "org-3", + type: "organization", + }, + { + id: "4", + name: "some-other-user", + type: "user", + }, + { + id: "5", + name: "org-4", + type: "organization", + }, + ]); + + const installations = await repo.listValidInstallations(projectId, location, conn); + expect(installations).to.deep.equal([ + { + id: "1", + name: "main-user", + type: "user", + }, + { + id: "2", + name: "org-1", + type: "organization", + }, + { + id: "3", + name: "org-3", + type: "organization", + }, + { + id: "5", + name: "org-4", + type: "organization", + }, + ]); + }); + }); + + describe("getConnectionForInstallation", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + let listConnectionsStub: sinon.SinonStub; + + beforeEach(() => { + listConnectionsStub = sandbox + .stub(devconnect, "listAllConnections") + .throws("Unexpected listAllConnections call"); + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + it("finds the matching connection for a given installation", async () => { + const mockConn1 = mockConn("apphosting-github-conn-1"); + const mockConn2 = mockConn("apphosting-github-conn-2"); + const mockConn3 = mockConn("apphosting-github-conn-3"); + const mockConn4 = mockConn("random-conn"); + + const installationToMatch = "installation-1"; + + mockConn1.githubConfig = { + appInstallationId: installationToMatch, + }; + + mockConn2.githubConfig = { + appInstallationId: "installation-2", + }; + + mockConn3.githubConfig = { + appInstallationId: "installation-3", + }; + + listConnectionsStub.onFirstCall().resolves([mockConn1, mockConn2, mockConn3, mockConn4]); + + const matchingConnection = await repo.getConnectionForInstallation( + projectId, + location, + installationToMatch, + ); + expect(matchingConnection).to.deep.equal(mockConn1); + }); + + it("returns null if there is no matching connection for a given installation", async () => { + const mockConn1 = mockConn("apphosting-github-conn-1"); + const mockConn2 = mockConn("apphosting-github-conn-2"); + + const installationToMatch = "random-installation"; + + mockConn1.githubConfig = { + appInstallationId: "installation-1", + }; + + mockConn2.githubConfig = { + appInstallationId: "installation-2", + }; + + listConnectionsStub.onFirstCall().resolves([mockConn1, mockConn2]); + + const matchingConnection = await repo.getConnectionForInstallation( + projectId, + location, + installationToMatch, + ); + expect(matchingConnection).to.be.null; + }); + }); + + describe("ensureSecretManagerAdminGrant", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + + let confirmStub: sinon.SinonStub; + let serviceAccountHasRolesStub: sinon.SinonStub; + let addServiceAccountToRolesStub: sinon.SinonStub; + let generateP4SAStub: sinon.SinonStub; + + beforeEach(() => { + confirmStub = sandbox.stub(prompt, "confirm").throws("Unexpected confirm 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(confirmStub).to.not.be.called; + }); + + it("prompts user if the developer connect P4SA does not have secretmanager.admin permissions", async () => { + serviceAccountHasRolesStub.resolves(false); + confirmStub.resolves(true); + addServiceAccountToRolesStub.resolves(); + + await repo.ensureSecretManagerAdminGrant(projectId); + + expect(serviceAccountHasRolesStub).calledWith( + projectId, + `service-${projectId}@gcp-sa-devconnect.iam.gserviceaccount.com`, + ["roles/secretmanager.admin"], + ); + + expect(confirmStub).to.be.called; + }); + + it("tries to generate developer connect P4SA if adding role throws an error", async () => { + serviceAccountHasRolesStub.resolves(false); + confirmStub.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(confirmStub).to.be.called; + }); + }); + describe("promptGitHubBranch", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + + let searchStub: sinon.SinonStub; + let listAllBranchesStub: sinon.SinonStub; + + beforeEach(() => { + searchStub = sandbox.stub(prompt, "search").throws("Unexpected search call"); + listAllBranchesStub = sandbox + .stub(devconnect, "listAllBranches") + .throws("Unexpected listAllBranches call"); + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + it("prompts user for branch", async () => { + listAllBranchesStub.returns(new Set(["main", "test1"])); + + searchStub.onFirstCall().returns("main"); + const testRepoLink = { + name: "test", + cloneUri: "/test", + createTime: "", + updateTime: "", + deleteTime: "", + reconciling: false, + uid: "", + }; + await expect(repo.promptGitHubBranch(testRepoLink)).to.eventually.equal("main"); + }); + }); +}); diff --git a/src/apphosting/githubConnections.ts b/src/apphosting/githubConnections.ts new file mode 100644 index 00000000000..d97669e13d8 --- /dev/null +++ b/src/apphosting/githubConnections.ts @@ -0,0 +1,656 @@ +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 { Choice, input, search, confirm, Separator } from "../prompt"; +import { getProjectNumber } from "../getProjectNumber"; +import { + apphostingGitHubAppInstallationURL, + developerConnectOrigin, + githubApiOrigin, +} from "../api"; + +import * as fuzzy from "fuzzy"; +import { Client } from "../apiv2"; + +const githubApiClient = new Client({ urlPrefix: githubApiOrigin(), auth: false }); + +export interface GitHubBranchInfo { + commit: GitHubCommitInfo; +} + +export interface GitHubCommitInfo { + sha: string; + commit: GitHubCommit; +} + +interface GitHubCommit { + message: string; +} + +interface ConnectionNameParts { + projectId: string; + location: string; + id: string; +} + +const APPHOSTING_CONN_PATTERN = /.+\/apphosting-github-conn-.+$/; +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("/", "-"); +} + +export const generateConnectionId = (): string => { + const randomHash = Math.random().toString(36).slice(6); + return `apphosting-github-conn-${randomHash}`; +}; + +const ADD_ACCOUNT_CHOICE = "@ADD_ACCOUNT"; +const MANAGE_INSTALLATION_CHOICE = "@MANAGE_INSTALLATION"; + +/** + * Prompts the user to create a GitHub connection. + */ +export async function getOrCreateFullyInstalledGithubConnection( + projectId: string, + location: string, + createConnectionId?: string, +): Promise { + utils.logBullet(clc.bold(`${clc.yellow("===")} Import a GitHub repository`)); + + if (createConnectionId) { + // Check if the connection already exists. + try { + const connection = await devConnect.getConnection(projectId, location, createConnectionId); + utils.logBullet(`Reusing existing connection ${createConnectionId}`); + return connection; + } catch (err: unknown) { + // A 404 is expected if the connection doesn't exist. Otherwise, continue to throw the err. + if ((err as any).status !== 404) { + throw err; + } + } + } + + // Just fetch a fully installed App Hosting connection as it would have the oauth credentials required. + const oauthConn = await getOrCreateOauthConnection(projectId, location); + let installationId = await promptGitHubInstallation(projectId, location, oauthConn); + + while (installationId === ADD_ACCOUNT_CHOICE) { + utils.logBullet( + "Install the Firebase App Hosting GitHub app on a new account to enable access to those repositories", + ); + + const apphostingGitHubInstallationURL = apphostingGitHubAppInstallationURL(); + utils.logBullet(apphostingGitHubInstallationURL); + await utils.openInBrowser(apphostingGitHubInstallationURL); + await input( + "Press Enter once you have installed or configured the Firebase App Hosting GitHub app to access your GitHub repo.", + ); + installationId = await promptGitHubInstallation(projectId, location, oauthConn); + } + + const connectionMatchingInstallation = await getConnectionForInstallation( + projectId, + location, + installationId, + ); + if (connectionMatchingInstallation) { + const { id: matchingConnectionId } = parseConnectionName(connectionMatchingInstallation.name)!; + + if (!createConnectionId) { + utils.logBullet(`Reusing matching connection ${matchingConnectionId}`); + return connectionMatchingInstallation; + } + } + if (!createConnectionId) { + createConnectionId = generateConnectionId(); + } + + const connection = await createFullyInstalledConnection( + projectId, + location, + createConnectionId, + oauthConn, + installationId, + ); + + return connection; +} + +/** + * Prompts the user to link their backend to a GitHub repository. + */ +export async function linkGitHubRepository( + projectId: string, + location: string, + createConnectionId?: string, +): Promise { + const connection = await getOrCreateFullyInstalledGithubConnection( + projectId, + location, + createConnectionId, + ); + + let repoCloneUri: string | undefined; + + do { + if (repoCloneUri === MANAGE_INSTALLATION_CHOICE) { + await manageInstallation(connection); + } + + repoCloneUri = await promptCloneUri(projectId, connection); + } while (repoCloneUri === MANAGE_INSTALLATION_CHOICE); + + 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 installationId represents an installation of the Firebase App Hosting GitHub app on a GitHub account / org + */ +async function createFullyInstalledConnection( + projectId: string, + location: string, + connectionId: string, + oauthConn: devConnect.Connection, + installationId: string, +): Promise { + let conn = await createConnection(projectId, location, connectionId, { + appInstallationId: installationId, + 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 input( + "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; +} + +async function manageInstallation(connection: devConnect.Connection): Promise { + utils.logBullet( + "Manage the Firebase App Hosting GitHub app to enable access to GitHub repositories", + ); + const targetUri = connection.githubConfig?.installationUri; + if (!targetUri) { + throw new FirebaseError("Failed to get Installation URI. Please try again."); + } + + utils.logBullet(targetUri); + await utils.openInBrowser(targetUri); + await input( + "Press Enter once you have installed or configured the Firebase App Hosting GitHub app to access your GitHub repo.", + ); +} + +/** + * Gets the oldest matching Dev Connect connection resource for a GitHub app installation. + */ +export async function getConnectionForInstallation( + projectId: string, + location: string, + installationId: string, +): Promise { + const connections = await listAppHostingConnections(projectId, location); + const connectionsMatchingInstallation = connections.filter( + (conn) => conn.githubConfig?.appInstallationId === installationId, + ); + if (connectionsMatchingInstallation.length === 0) { + return null; + } + + if (connectionsMatchingInstallation.length > 1) { + /** + * In the Firebase Console and previous versions of the CLI we create a + * connection and then choose an installation, which makes it possible for + * there to be more than one connection for the same installation. + * + * To handle this case gracefully we return the oldest matching connection. + */ + const sorted = devConnect.sortConnectionsByCreateTime(connectionsMatchingInstallation); + return sorted[0]; + } + + return connectionsMatchingInstallation[0]; +} + +/** + * Prompts the user to select which GitHub account to install the GitHub app. + */ +export async function promptGitHubInstallation( + projectId: string, + location: string, + connection: devConnect.Connection, +): Promise { + const installations = await listValidInstallations(projectId, location, connection); + + const installationName = await search({ + message: "Which GitHub account do you want to use?", + source: (input: string | undefined = ""): Array> => [ + new Separator(), + { + name: "Missing an account? Select this option to add a GitHub account", + value: ADD_ACCOUNT_CHOICE, + }, + new Separator(), + ...fuzzy + .filter(input, installations, { + extract: (installation) => installation.name || "", + }) + .map((result) => { + return { + name: result.original.name || "", + value: result.original.id, + }; + }), + ], + }); + + return installationName; +} + +/** + * A "valid" installation is either the user's account itself or any orgs they + * have access to that the GitHub app has been installed on. + */ +export async function listValidInstallations( + projectId: string, + location: string, + connection: devConnect.Connection, +): Promise { + const { id: connId } = parseConnectionName(connection.name)!; + let installations = await devConnect.fetchGitHubInstallations(projectId, location, connId); + + installations = installations.filter((installation) => { + return ( + (installation.type === "user" && + installation.name === connection.githubConfig?.authorizerCredential?.username) || + installation.type === "organization" + ); + }); + + return installations; +} + +/** + * Gets or creates the fully installed 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; + const completedConnections = await listAppHostingConnections(projectId, location); + if (completedConnections.length > 0) { + /** + * any valid app hosting connection can be used, we just want the associated + * oauth credential, don't care about the connection itself. + * */ + return completedConnections[0]; + } + + await ensureSecretManagerAdminGrant(projectId); + conn = await createConnection(projectId, location, generateConnectionId()); + + 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 input("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, + connection: devConnect.Connection, +): Promise { + const cloneUris = await fetchRepositoryCloneUris(projectId, connection); + const cloneUri = await search({ + message: "Which GitHub repo do you want to deploy?", + source: (input = ""): Array | Separator> => [ + new Separator(), + { + name: "Missing a repo? Select this option to configure your GitHub connection settings", + value: MANAGE_INSTALLATION_CHOICE, + }, + new Separator(), + ...fuzzy + .filter(input, cloneUris, { + extract: (uri) => extractRepoSlugFromUri(uri) || "", + }) + .map((result) => { + return { + name: extractRepoSlugFromUri(result.original) || "", + value: result.original, + }; + }), + ], + }); + + return cloneUri; +} + +/** + * Prompts the user for a GitHub branch and validates that the given branch + * actually exists. User is re-prompted until they enter a valid branch. + */ +export async function promptGitHubBranch(repoLink: devConnect.GitRepositoryLink): Promise { + const branches = await devConnect.listAllBranches(repoLink.name); + const branch = await search({ + message: "Pick a branch for continuous deployment", + source: (input = ""): Array | Separator> => [ + ...fuzzy.filter(input, Array.from(branches)).map((result) => { + return { + name: result.original, + value: result.original, + }; + }), + ], + }); + + return branch; +} + +/** + * 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 confirm("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; +} + +/** + * Gets or creates a new Developer Connect Connection resource. Will typically need some initialization + * 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; +} + +/** + * Gets or creates a new Developer Connect GitRepositoryLink resource on a Developer Connect connection. + * 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; +} + +/** + * Lists all App Hosting Developer Connect Connections + * not including the OAuth Connection + * + * Exported for unit testing. + */ +export async function listAppHostingConnections( + projectId: string, + location: string, +): Promise { + const conns = await devConnect.listAllConnections(projectId, location); + return conns.filter( + (conn) => + APPHOSTING_CONN_PATTERN.test(conn.name) && + conn.installationState.stage === "COMPLETE" && + !conn.disabled, + ); +} + +/** + * Fetch the git clone url using a Developer Connect GitRepositoryLink. + * + * Exported for unit testing. + */ +export async function fetchRepositoryCloneUris( + projectId: string, + connection: devConnect.Connection, +): Promise { + const { location, id } = parseConnectionName(connection.name)!; + const connectionRepos = await devConnect.listAllLinkableGitRepositories(projectId, location, id); + const cloneUris = connectionRepos.map((conn) => conn.cloneUri); + + return cloneUris; +} + +/** + * Gets the details of a GitHub branch from the GitHub REST API. + */ +export async function getGitHubBranch( + owner: string, + repo: string, + branch: string, + readToken: string, +): Promise { + const headers = { Authorization: `Bearer ${readToken}`, "User-Agent": "Firebase CLI" }; + const { body } = await githubApiClient.get( + `/repos/${owner}/${repo}/branches/${branch}`, + { + headers, + }, + ); + return body; +} + +/** + * Gets the details of a GitHub commit from the GitHub REST API. + */ +export async function getGitHubCommit( + owner: string, + repo: string, + ref: string, + readToken: string, +): Promise { + const headers = { Authorization: `Bearer ${readToken}`, "User-Agent": "Firebase CLI" }; + const { body } = await githubApiClient.get( + `/repos/${owner}/${repo}/commits/${ref}`, + { + headers, + }, + ); + return body; +} diff --git a/src/apphosting/index.ts b/src/apphosting/index.ts new file mode 100644 index 00000000000..0ecc6703ff5 --- /dev/null +++ b/src/apphosting/index.ts @@ -0,0 +1,3 @@ +import { doSetup } from "./backend"; + +export { doSetup as setupBackend }; diff --git a/src/apphosting/localbuilds.spec.ts b/src/apphosting/localbuilds.spec.ts new file mode 100644 index 00000000000..7fff66afcde --- /dev/null +++ b/src/apphosting/localbuilds.spec.ts @@ -0,0 +1,47 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; +import * as localBuildModule from "@apphosting/build"; +import { localBuild } from "./localbuilds"; + +describe("localBuild", () => { + afterEach(() => { + sinon.restore(); + }); + + it("returns the expected output", async () => { + const bundleConfig = { + version: "v1" as const, + runConfig: { + runCommand: "npm run build:prod", + }, + metadata: { + adapterPackageName: "@apphosting/angular-adapter", + adapterVersion: "14.1", + framework: "nextjs", + }, + outputFiles: { + serverApp: { + include: ["./next/standalone"], + }, + }, + }; + const expectedAnnotations = { + adapterPackageName: "@apphosting/angular-adapter", + adapterVersion: "14.1", + framework: "nextjs", + }; + const expectedOutputFiles = ["./next/standalone"]; + const expectedBuildConfig = { + runCommand: "npm run build:prod", + env: [], + }; + const localApphostingBuildStub: sinon.SinonStub = sinon + .stub(localBuildModule, "localBuild") + .resolves(bundleConfig); + const { outputFiles, annotations, buildConfig } = await localBuild("./", "nextjs"); + expect(annotations).to.deep.equal(expectedAnnotations); + expect(buildConfig).to.deep.equal(expectedBuildConfig); + expect(outputFiles).to.deep.equal(expectedOutputFiles); + sinon.assert.calledWith(localApphostingBuildStub, "./", "nextjs"); + }); +}); diff --git a/src/apphosting/localbuilds.ts b/src/apphosting/localbuilds.ts new file mode 100644 index 00000000000..ed8bef0d077 --- /dev/null +++ b/src/apphosting/localbuilds.ts @@ -0,0 +1,37 @@ +import { BuildConfig, Env } from "../gcp/apphosting"; +import { localBuild as localAppHostingBuild } from "@apphosting/build"; + +/** + * Triggers a local apphosting build. + */ +export async function localBuild( + projectRoot: string, + framework: string, +): Promise<{ + outputFiles: string[]; + annotations: Record; + buildConfig: BuildConfig; +}> { + const apphostingBuildOutput = await localAppHostingBuild(projectRoot, framework); + + const annotations: Record = Object.fromEntries( + Object.entries(apphostingBuildOutput.metadata).map(([key, value]) => [key, String(value)]), + ); + + const env: Env[] | undefined = apphostingBuildOutput.runConfig.environmentVariables?.map( + ({ variable, value, availability }) => ({ + variable, + value, + availability, + }), + ); + + return { + outputFiles: apphostingBuildOutput.outputFiles?.serverApp.include ?? [], + annotations, + buildConfig: { + runCommand: apphostingBuildOutput.runConfig.runCommand, + env: env ?? [], + }, + }; +} diff --git a/src/apphosting/repo.spec.ts b/src/apphosting/repo.spec.ts new file mode 100644 index 00000000000..78864d0451d --- /dev/null +++ b/src/apphosting/repo.spec.ts @@ -0,0 +1,328 @@ +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 promptStub: sinon.SinonStubbedInstance; + 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(() => { + promptStub = sandbox.stub(prompt); + promptStub.input.throws("Unexpected input call"); + promptStub.search.throws("Unexpected search call"); + promptStub.confirm.throws("Unexpected confirm 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); + promptStub.input.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); + promptStub.input.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); + promptStub.search.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); + promptStub.search.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..c9683bf362b --- /dev/null +++ b/src/apphosting/repo.ts @@ -0,0 +1,382 @@ +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 { Choice, input, search, Separator, confirm } from "../prompt"; +import { getProjectNumber } from "../getProjectNumber"; + +import * as fuzzy from "fuzzy"; + +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 input( + "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 input("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 search({ + message: "Which GitHub repo do you want to deploy?", + source: (input: string | undefined): Array> => [ + new Separator(), + { + name: "Missing a repo? Select this option to configure your GitHub connection settings", + value: ADD_CONN_CHOICE, + }, + new 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 confirm("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/rollout.spec.ts b/src/apphosting/rollout.spec.ts new file mode 100644 index 00000000000..3646c80c07e --- /dev/null +++ b/src/apphosting/rollout.spec.ts @@ -0,0 +1,271 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; +import { createRollout, orchestrateRollout } from "./rollout"; +import * as devConnect from "../gcp/devConnect"; +import * as githubConnections from "../apphosting/githubConnections"; +import * as apphosting from "../gcp/apphosting"; +import * as backend from "./backend"; +import { FirebaseError } from "../error"; +import * as poller from "../operation-poller"; +import * as utils from "../utils"; + +describe("apphosting rollouts", () => { + const user = "user"; + const repo = "repo"; + const commitSha = "0123456"; + const branchId = "main"; + + const projectId = "projectId"; + const location = "us-central1"; + const backendId = "backendId"; + const connectionId = "apphosting-github-conn-a1b2c3"; + const gitRepoLinkId = `${user}-${repo}`; + const buildAndRolloutId = "build-2024-10-01-001"; + + let getBackend: sinon.SinonStub; + let getRepoDetailsFromBackendStub: sinon.SinonStub; + let listAllBranchesStub: sinon.SinonStub; + let getGitHubBranchStub: sinon.SinonStub; + let getGitHubCommitStub: sinon.SinonStub; + let getNextRolloutIdStub: sinon.SinonStub; + let createBuildStub: sinon.SinonStub; + let createRolloutStub: sinon.SinonStub; + let pollOperationStub: sinon.SinonStub; + let promptGitHubBranchStub: sinon.SinonStub; + let sleepStub: sinon.SinonStub; + + beforeEach(() => { + getBackend = sinon.stub(backend, "getBackend").throws("unexpected getBackend call"); + getRepoDetailsFromBackendStub = sinon + .stub(devConnect, "getRepoDetailsFromBackend") + .throws("unexpected getRepoDetailsFromBackend call"); + listAllBranchesStub = sinon + .stub(devConnect, "listAllBranches") + .throws("unexpected listAllBranches call"); + getGitHubBranchStub = sinon + .stub(githubConnections, "getGitHubBranch") + .throws("unexpected getGitHubBranch call"); + getGitHubCommitStub = sinon + .stub(githubConnections, "getGitHubCommit") + .throws("unexpected getGitHubCommit call"); + getNextRolloutIdStub = sinon + .stub(apphosting, "getNextRolloutId") + .throws("unexpected getNextRolloutId call"); + createBuildStub = sinon.stub(apphosting, "createBuild").throws("unexpected createBuild call"); + createRolloutStub = sinon + .stub(apphosting, "createRollout") + .throws("unexpected createRollout call"); + pollOperationStub = sinon.stub(poller, "pollOperation").throws("unexpected pollOperation call"); + promptGitHubBranchStub = sinon + .stub(githubConnections, "promptGitHubBranch") + .throws("unexpected promptGitHubBranch call"); + sleepStub = sinon.stub(utils, "sleep").throws("unexpected sleep call"); + }); + + afterEach(() => { + sinon.verifyAndRestore(); + }); + + describe("apphosting rollouts", () => { + const repoLinkId = `projects/${projectId}/location/${location}/connections/${connectionId}/gitRepositoryLinks/${gitRepoLinkId}`; + + const backend = { + name: `projects/${projectId}/locations/${location}/backends/${backendId}`, + labels: {}, + createTime: "0", + updateTime: "1", + uri: "https://placeholder.com", + codebase: { + repository: repoLinkId, + rootDirectory: "/", + }, + }; + + const buildInput = { + source: { + codebase: { + commit: commitSha, + }, + }, + }; + + const repoLinkDetails = { + repoLink: { + name: repoLinkId, + cloneUri: `https://github.com/${user}/${repo}.git`, + createTime: "create-time", + updateTime: "update-time", + reconciling: true, + uid: "00000000", + }, + owner: user, + repo: repo, + readToken: { + token: "read-token", + expirationTime: "some-time", + gitUsername: user, + }, + }; + + const branches = new Set(); + branches.add(branchId); + + const commitInfo = { + sha: commitSha, + commit: { + message: "new commit", + }, + }; + + const branchInfo = { + commit: commitInfo, + }; + + const buildOp = { + name: "build-op", + done: true, + }; + + const rolloutOp = { + name: "rollout-op", + done: true, + }; + + const build = { + name: buildAndRolloutId, + state: "READY", + }; + + const rollout = { + name: buildAndRolloutId, + state: "READY", + }; + + describe("createRollout", () => { + it("should create a new rollout from user-specified branch", async () => { + getBackend.resolves(backend); + getRepoDetailsFromBackendStub.resolves(repoLinkDetails); + listAllBranchesStub.resolves(branches); + getGitHubBranchStub.resolves(branchInfo); + getNextRolloutIdStub.resolves(buildAndRolloutId); + createBuildStub.resolves(buildOp); + createRolloutStub.resolves(rolloutOp); + pollOperationStub.onFirstCall().resolves(rollout); + pollOperationStub.onSecondCall().resolves(build); + + await createRollout(backendId, projectId, branchId, undefined, true); + + expect(createBuildStub).to.be.called; + expect(createRolloutStub).to.be.called; + expect(pollOperationStub).to.be.called; + }); + + it("should create a new rollout from user-specified commit", async () => { + getBackend.resolves(backend); + getRepoDetailsFromBackendStub.resolves(repoLinkDetails); + getGitHubCommitStub.resolves(commitInfo); + getNextRolloutIdStub.resolves(buildAndRolloutId); + createBuildStub.resolves(buildOp); + createRolloutStub.resolves(rolloutOp); + pollOperationStub.onFirstCall().resolves(rollout); + pollOperationStub.onSecondCall().resolves(build); + + await createRollout(backendId, projectId, undefined, commitSha, true); + + expect(createBuildStub).to.be.called; + expect(createRolloutStub).to.be.called; + expect(pollOperationStub).to.be.called; + }); + + it("should prompt user for a branch if branch or commit ID is not specified", async () => { + getBackend.resolves(backend); + getRepoDetailsFromBackendStub.resolves(repoLinkDetails); + promptGitHubBranchStub.resolves(branchId); + getGitHubBranchStub.resolves(branchInfo); + getNextRolloutIdStub.resolves(buildAndRolloutId); + createBuildStub.resolves(buildOp); + createRolloutStub.resolves(rolloutOp); + pollOperationStub.onFirstCall().resolves(rollout); + pollOperationStub.onSecondCall().resolves(build); + + await createRollout(backendId, projectId, undefined, undefined, false); + + expect(promptGitHubBranchStub).to.be.called; + expect(createBuildStub).to.be.called; + expect(createRolloutStub).to.be.called; + expect(pollOperationStub).to.be.called; + }); + + it("should throw an error if GitHub branch is not found", async () => { + getBackend.resolves(backend); + getRepoDetailsFromBackendStub.resolves(repoLinkDetails); + listAllBranchesStub.resolves(branches); + + await expect( + createRollout(backendId, projectId, "invalid-branch", undefined, true), + ).to.be.rejectedWith(/Unrecognized git branch/); + }); + + it("should throw an error if GitHub commit is not found", async () => { + getBackend.resolves(backend); + getRepoDetailsFromBackendStub.resolves(repoLinkDetails); + getGitHubCommitStub.rejects(new FirebaseError("error", { status: 422 })); + + await expect( + createRollout(backendId, projectId, undefined, commitSha, true), + ).to.be.rejectedWith(/Unrecognized git commit/); + }); + + it("should throw an error if --force flag is specified but --git-branch and --git-commit are missing", async () => { + getBackend.resolves(backend); + getRepoDetailsFromBackendStub.resolves(repoLinkDetails); + + await expect( + createRollout(backendId, projectId, undefined, undefined, true), + ).to.be.rejectedWith(/Failed to create rollout with --force option/); + }); + }); + + describe("orchestrateRollout", () => { + it("should successfully create build and rollout", async () => { + getNextRolloutIdStub.resolves(buildAndRolloutId); + createBuildStub.resolves(buildOp); + createRolloutStub.resolves(rolloutOp); + pollOperationStub.onFirstCall().resolves(rollout); + pollOperationStub.onSecondCall().resolves(build); + sleepStub.resolves(); + + await orchestrateRollout({ + projectId, + location, + backendId, + buildInput, + }); + + expect(createBuildStub).to.be.called; + expect(createRolloutStub).to.be.called; + }); + + it("should retry createRollout call on HTTP 400 errors", async () => { + getNextRolloutIdStub.resolves(buildAndRolloutId); + createBuildStub.resolves(buildOp); + createRolloutStub.onFirstCall().rejects(new FirebaseError("error", { status: 400 })); + createRolloutStub.resolves(rolloutOp); + pollOperationStub.onFirstCall().resolves(rollout); + pollOperationStub.onSecondCall().resolves(build); + sleepStub.resolves(); + + await orchestrateRollout({ + projectId, + location, + backendId, + buildInput, + }); + + expect(createBuildStub).to.be.called; + expect(createRolloutStub).to.be.calledThrice; + expect(pollOperationStub).to.be.called; + }); + }); + }); +}); diff --git a/src/apphosting/rollout.ts b/src/apphosting/rollout.ts new file mode 100644 index 00000000000..f4c47c4d0fc --- /dev/null +++ b/src/apphosting/rollout.ts @@ -0,0 +1,214 @@ +import * as apphosting from "../gcp/apphosting"; +import { FirebaseError } from "../error"; +import * as ora from "ora"; +import { getRepoDetailsFromBackend, listAllBranches } from "../gcp/devConnect"; +import { + getGitHubBranch, + getGitHubCommit, + GitHubCommitInfo, + promptGitHubBranch, +} from "../apphosting/githubConnections"; +import * as poller from "../operation-poller"; + +import { logBullet, sleep } from "../utils"; +import { apphostingOrigin, consoleOrigin } from "../api"; +import { DeepOmit } from "../metaprogramming"; +import { getBackend } from "./backend"; + +const apphostingPollerOptions: Omit = { + apiOrigin: apphostingOrigin(), + apiVersion: apphosting.API_VERSION, + masterTimeout: 25 * 60 * 1_000, + maxBackoff: 10_000, +}; + +const GIT_COMMIT_SHA_REGEX = /^(?:[0-9a-f]{40}|[0-9a-f]{7})$/; + +/** + * Create a new App Hosting rollout for a backend. + * Implements core logic for apphosting:rollouts:create command. + */ +export async function createRollout( + backendId: string, + projectId: string, + branch?: string, + commit?: string, + force?: boolean, +): Promise { + const backend = await getBackend(projectId, backendId); + + if (!backend.codebase || !backend.codebase.repository) { + throw new FirebaseError( + `Backend ${backendId} is missing a connected repository. If you would like to deploy from a branch or commit of a GitHub repository, you can connect one through the Firebase Console. If you would like to deploy from local source, run 'firebase deploy'.`, + ); + } + + const { location } = apphosting.parseBackendName(backend.name); + const { repoLink, owner, repo, readToken } = await getRepoDetailsFromBackend( + projectId, + location, + backend.codebase.repository, + ); + + let targetCommit: GitHubCommitInfo; + if (branch) { + const branches = await listAllBranches(repoLink.name); + if (!branches.has(branch)) { + throw new FirebaseError( + `Unrecognized git branch ${branch}. Please double-check your branch name and try again.`, + ); + } + const branchInfo = await getGitHubBranch(owner, repo, branch, readToken.token); + targetCommit = branchInfo.commit; + } else if (commit) { + if (!GIT_COMMIT_SHA_REGEX.test(commit)) { + throw new FirebaseError(`Invalid git commit ${commit}. Must be a valid SHA1 hash.`); + } + try { + const commitInfo = await getGitHubCommit(owner, repo, commit, readToken.token); + targetCommit = commitInfo; + } catch (err: unknown) { + // 422 HTTP status code returned by GitHub indicates it was unable to find the commit. + if ((err as FirebaseError).status === 422) { + throw new FirebaseError( + `Unrecognized git commit ${commit}. Please double-check your commit hash and try again.`, + ); + } + throw err; + } + } else { + if (force) { + throw new FirebaseError( + `Failed to create rollout with --force option because no target branch or commit was specified. Please specify which branch or commit to roll out with the --git-branch or --git-commit flag.`, + ); + } + branch = await promptGitHubBranch(repoLink); + const branchInfo = await getGitHubBranch(owner, repo, branch, readToken.token); + targetCommit = branchInfo.commit; + } + + logBullet( + `You are about to deploy [${targetCommit.sha.substring(0, 7)}]: ${targetCommit.commit.message}`, + ); + logBullet( + `You may also track this rollout at:\n\t${consoleOrigin()}/project/${projectId}/apphosting`, + ); + + const createRolloutSpinner = ora( + "Starting a new rollout; this may take a few minutes. It's safe to exit now.", + ).start(); + + try { + await orchestrateRollout({ + projectId, + location, + backendId, + buildInput: { + source: { + codebase: { + commit: targetCommit.sha, + }, + }, + }, + }); + } catch (err: unknown) { + createRolloutSpinner.fail("Rollout failed."); + throw err; + } + createRolloutSpinner.succeed("Successfully created a new rollout!"); +} + +interface OrchestrateRolloutArgs { + projectId: string; + location: string; + backendId: string; + buildInput: DeepOmit; + // Used to determine if a rollout ID needs to be computed. + // If we know this is the first rollout for a backend, + // we can avoid multiple API calls and default to: + // build-{year}-{month}-{day}-001. + isFirstRollout?: boolean; +} + +/** + * Creates a new build and rollout and polls both to completion. + */ +export async function orchestrateRollout( + args: OrchestrateRolloutArgs, +): Promise<{ rollout: apphosting.Rollout; build: apphosting.Build }> { + const { projectId, location, backendId, buildInput, isFirstRollout } = args; + + const buildId = await apphosting.getNextRolloutId( + projectId, + location, + backendId, + isFirstRollout ? 1 : undefined, + ); + 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 }; +} diff --git a/src/apphosting/secrets/dialogs.spec.ts b/src/apphosting/secrets/dialogs.spec.ts new file mode 100644 index 00000000000..9b88d881b4e --- /dev/null +++ b/src/apphosting/secrets/dialogs.spec.ts @@ -0,0 +1,481 @@ +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", async () => { + // Note: passing in out of order to verify the results are sorted. + const metadata = await 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", async () => { + const metadata = await dialogs.toMetadata("number", [modernA, legacy]); + + expect(metadata).to.deep.equal([ + { + location: "l", + id: "legacy", + ...(await secrets.serviceAccountsForBackend("number", legacy)), + }, + { location: "l", id: "modernA", buildServiceAccount: "a", runServiceAccount: "a" }, + ]); + }); + + it("sorts by location first and id second", async () => { + const metadata = await dialogs.toMetadata("number", [legacy, modernA, modernA2]); + expect(metadata).to.deep.equal([ + { + location: "l", + id: "legacy", + ...(await 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", async () => { + const table = dialogs.tableForBackends( + await 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 account", async () => { + const table = dialogs.tableForBackends(await dialogs.toMetadata("number", [legacy, modernA])); + const legacyAccounts = await 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 = await 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 = await 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.checkbox.resolves(["a", "b"]); + const legacyAccounts = await secrets.serviceAccountsForBackend("number", legacy); + + await expect( + dialogs.selectBackendServiceAccounts("number", "id", {}), + ).to.eventually.deep.equal({ buildServiceAccounts: ["a", "b"], runServiceAccounts: [] }); + + expect(prompt.checkbox).to.have.been.calledWith({ + 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.checkbox.resolves([]); + const legacyAccounts = await secrets.serviceAccountsForBackend("number", legacy); + + await expect( + dialogs.selectBackendServiceAccounts("number", "id", {}), + ).to.eventually.deep.equal(emptyMulti); + + expect(prompt.checkbox).to.have.been.calledWith({ + 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.input).to.not.have.been.called; + }); + + it("suggests a valid upper case name", async () => { + prompt.input.resolves("SECRET_VALUE"); + + await expect(dialogs.envVarForSecret("secret-value")).to.eventually.equal("SECRET_VALUE"); + expect(prompt.input).to.have.been.calledWithMatch({ + message: "What environment variable name would you like to use?", + default: "SECRET_VALUE", + }); + }); + + it("prevents invalid keys", async () => { + prompt.input.onFirstCall().resolves("secret-value"); + prompt.input.onSecondCall().resolves("SECRET_VALUE"); + + await expect(dialogs.envVarForSecret("secret-value")).to.eventually.equal("SECRET_VALUE"); + expect(prompt.input).to.have.been.calledWithMatch({ + message: "What environment variable name would you like to use?", + default: "SECRET_VALUE", + }); + expect(prompt.input).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.input.onFirstCall().resolves("PORT"); + prompt.input.onSecondCall().resolves("SECRET_VALUE"); + + await expect(dialogs.envVarForSecret("secret-value")).to.eventually.equal("SECRET_VALUE"); + expect(prompt.input).to.have.been.calledWithMatch({ + message: "What environment variable name would you like to use?", + default: "SECRET_VALUE", + }); + expect(prompt.input).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.input.onFirstCall().resolves("X_GOOGLE_SECRET"); + prompt.input.onSecondCall().resolves("SECRET_VALUE"); + + await expect(dialogs.envVarForSecret("secret-value")).to.eventually.equal("SECRET_VALUE"); + expect(prompt.input).to.have.been.calledWithMatch({ + message: "What environment variable name would you like to use?", + default: "SECRET_VALUE", + }); + expect(prompt.input).to.have.been.calledTwice; + expect(utils.logLabeledError).to.have.been.calledWithMatch( + "apphosting", + /Key X_GOOGLE_SECRET starts with a reserved prefix/, + ); + }); + + it("can trim test prefixes", async () => { + prompt.input.resolves("SECRET"); + + await expect( + dialogs.envVarForSecret("test-secret", /* trimTestPrefix=*/ true), + ).to.eventually.equal("SECRET"); + expect(prompt.input).to.have.been.calledWithMatch({ + message: "What environment variable name would you like to use?", + default: "SECRET", + }); + }); + }); +}); diff --git a/src/apphosting/secrets/dialogs.ts b/src/apphosting/secrets/dialogs.ts new file mode 100644 index 00000000000..50e8e74269c --- /dev/null +++ b/src/apphosting/secrets/dialogs.ts @@ -0,0 +1,239 @@ +import * as clc from "colorette"; +import * as Table from "cli-table3"; + +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 async function toMetadata( + projectNumber: string, + backends: apphosting.Backend[], +): Promise { + const metadata: BackendMetadata[] = []; + for (const backend of backends) { + // Splits format projects//locations//backends/ + const [, , , location, , id] = backend.name.split("/"); + metadata.push({ location, id, ...(await 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(await serviceAccountsForBackend(projectNumber, listBackends.backends[0])); + } + utils.logBullet(GRANT_ACCESS_IN_FUTURE); + return { buildServiceAccounts: [], runServiceAccounts: [] }; + } + + const metadata: BackendMetadata[] = await 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"] }, + }); + table.push(...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.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, + trimTestPrefix: boolean = false, +): Promise { + let upper = toUpperSnakeCase(secret); + if (trimTestPrefix && upper.startsWith("TEST_")) { + upper = upper.substring("TEST_".length); + } + if (upper === secret) { + try { + env.validateKey(secret); + return secret; + } catch { + // fallthrough + } + } + + do { + const test = await prompt.input({ + 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..810f38e6439 --- /dev/null +++ b/src/apphosting/secrets/index.spec.ts @@ -0,0 +1,421 @@ +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"; + +import { Secret } from "../yaml"; +import { FirebaseError } from "../../error"; + +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", async () => { + const backend = { + serviceAccount: "sa", + } as any as apphosting.Backend; + expect(await secrets.serviceAccountsForBackend("number", backend)).to.deep.equal({ + buildServiceAccount: "sa", + runServiceAccount: "sa", + }); + }); + + it("has a fallback for legacy SAs", async () => { + const backend = {} as any as apphosting.Backend; + expect(await secrets.serviceAccountsForBackend("number", backend)).to.deep.equal({ + buildServiceAccount: gcb.getDefaultServiceAccount("number"), + runServiceAccount: await 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); + }); + }); + + describe("grantEmailsSecretAccess", () => { + const secret = { + projectId: "projectId", + name: "secret", + }; + const secret2 = { + projectId: "projectId", + name: "secret2", + }; + const existingPolicy: iam.Policy = { + version: 1, + etag: "tag", + bindings: [ + { + role: "roles/viewer", + members: ["serviceAccount:existingSA"], + }, + ], + }; + + it("should brute force its way to success", async () => { + gcsm.getIamPolicy.resolves(existingPolicy); + gcsm.setIamPolicy + .onFirstCall() + .rejects( + new FirebaseError( + 'Principal mygroup@mydomain.com is of type "group". The principal should appear as "group:mygroup@mydomain.com"', + ), + ); + gcsm.setIamPolicy.onSecondCall().resolves(); + + await secrets.grantEmailsSecretAccess( + secret.projectId, + [secret.name], + ["user@mydomain.com", "mygroup@mydomain.com"], + ); + + expect(gcsm.getIamPolicy).to.be.calledWithMatch(secret); + expect(gcsm.setIamPolicy.firstCall).to.be.calledWithMatch(secret, [ + { + role: "roles/viewer", + members: ["serviceAccount:existingSA"], + }, + { + role: "roles/secretmanager.secretAccessor", + members: ["user:user@mydomain.com", "user:mygroup@mydomain.com"], + }, + ]); + expect(gcsm.setIamPolicy.secondCall).to.be.calledWithMatch(secret, [ + { + role: "roles/viewer", + members: ["serviceAccount:existingSA"], + }, + { + role: "roles/secretmanager.secretAccessor", + members: ["user:user@mydomain.com", "group:mygroup@mydomain.com"], + }, + ]); + }); + + it("Should remember what it learns while brute forcing across multiple secrets", async () => { + gcsm.getIamPolicy.resolves(existingPolicy); + gcsm.setIamPolicy + .onFirstCall() + .rejects( + new FirebaseError( + 'Principal mygroup@mydomain.com is of type "group". The principal should appear as "group:mygroup@mydomain.com"', + ), + ); + gcsm.setIamPolicy.onSecondCall().resolves(); + gcsm.setIamPolicy.onThirdCall().resolves(); + + await secrets.grantEmailsSecretAccess( + secret.projectId, + [secret.name, secret2.name], + ["user@mydomain.com", "mygroup@mydomain.com"], + ); + + expect(gcsm.getIamPolicy).to.be.calledWithMatch(secret); + expect(gcsm.setIamPolicy).to.be.calledThrice; + expect(gcsm.setIamPolicy.firstCall).to.be.calledWithMatch(secret, [ + { + role: "roles/viewer", + members: ["serviceAccount:existingSA"], + }, + { + role: "roles/secretmanager.secretAccessor", + members: ["user:user@mydomain.com", "user:mygroup@mydomain.com"], + }, + ]); + expect(gcsm.setIamPolicy.secondCall).to.be.calledWithMatch(secret, [ + { + role: "roles/viewer", + members: ["serviceAccount:existingSA"], + }, + { + role: "roles/secretmanager.secretAccessor", + members: ["user:user@mydomain.com", "group:mygroup@mydomain.com"], + }, + ]); + expect(gcsm.setIamPolicy.thirdCall).to.be.calledWithMatch(secret2, [ + { + role: "roles/viewer", + members: ["serviceAccount:existingSA"], + }, + { + role: "roles/secretmanager.secretAccessor", + members: ["user:user@mydomain.com", "group:mygroup@mydomain.com"], + }, + ]); + }); + + it("Should fail if the error is not specifically a principal type error", async () => { + gcsm.getIamPolicy.resolves(existingPolicy); + gcsm.setIamPolicy.rejects(new FirebaseError("Some other error")); + + await expect( + secrets.grantEmailsSecretAccess(secret.projectId, [secret.name], ["user@mydomain.com"]), + ).to.eventually.be.rejectedWith(/Failed to set IAM bindings/); + }); + }); + + describe("fetchSecrets", () => { + const projectId = "randomProject"; + it("correctly attempts to fetch secret and it's version", async () => { + const secretSource: Secret[] = [ + { + variable: "PINNED_API_KEY", + secret: "myApiKeySecret@5", + }, + ]; + + gcsm.accessSecretVersion.returns(Promise.resolve("some-value")); + await secrets.fetchSecrets(projectId, secretSource); + + expect(gcsm.accessSecretVersion).calledOnce; + expect(gcsm.accessSecretVersion).calledWithExactly(projectId, "myApiKeySecret", "5"); + }); + + it("fetches latest version if version not explicitely provided", async () => { + const secretSource: Secret[] = [ + { + variable: "VERBOSE_API_KEY", + secret: "projects/test-project/secrets/secretID", + }, + ]; + + gcsm.accessSecretVersion.returns(Promise.resolve("some-value")); + await secrets.fetchSecrets(projectId, secretSource); + + expect(gcsm.accessSecretVersion).calledOnce; + expect(gcsm.accessSecretVersion).calledWithExactly( + projectId, + "projects/test-project/secrets/secretID", + "latest", + ); + }); + }); +}); diff --git a/src/apphosting/secrets/index.ts b/src/apphosting/secrets/index.ts new file mode 100644 index 00000000000..54b14380f49 --- /dev/null +++ b/src/apphosting/secrets/index.ts @@ -0,0 +1,273 @@ +import { FirebaseError, getErrStatus, getError } 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"; +import { Secret } from "../yaml"; + +/** 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 async function serviceAccountsForBackend( + projectNumber: string, + backend: apphosting.Backend, +): Promise { + if (backend.serviceAccount) { + return { + buildServiceAccount: backend.serviceAccount, + runServiceAccount: backend.serviceAccount, + }; + } + return { + buildServiceAccount: gcb.getDefaultServiceAccount(projectNumber), // TOOD: Look this up via API + runServiceAccount: await 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: unknown) { + throw new FirebaseError( + `Failed to get IAM bindings on secret: ${secretName}. Ensure you have the permissions to do so and try again.`, + { original: getError(err) }, + ); + } + + const updatedBindings = existingBindings.concat(newBindings); + try { + await gcsm.setIamPolicy({ projectId, name: secretName }, updatedBindings); + } catch (err: unknown) { + 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. ` + + "For more information visit https://cloud.google.com/secret-manager/docs/manage-access-to-secrets#required-roles", + { original: getError(err) }, + ); + } + + utils.logSuccess(`Successfully set IAM bindings on secret ${secretName}.\n`); +} + +/** + * Grants the following users or groups access to the provided secret. + */ +export async function grantEmailsSecretAccess( + projectId: string, + secretNames: string[], + emails: string[], +): Promise { + // This feels like a hack, but it's actually sorta taking advantage of an escalation of privilege in Google IAM. + // The correct way to determine if an email address is a user or group is to use the Google Admin API + // (GET e.g. admin.googleapis.com/admin/directory/v1/users/ or GET admin.googleapis.com/admin/driectory/v1/groups/) + // but that would require us to have admin permissions on GMail for example. Fortunately, IAM seems to give us well formed errors + // that dictate what type of role the email address should have been bound with. This seems... like a design mistake. If they knew + // already, why not just accept the value without leaking its type? + // Note: we keep typeGuesses outside of the loop so that we learn the type of principal an email is once across all secrets. + const typeGuesses = Object.fromEntries(emails.map((email) => [email, "user"])); + for (const secretName of secretNames) { + let existingBindings; + try { + existingBindings = (await gcsm.getIamPolicy({ projectId, name: secretName })).bindings || []; + } catch (err: unknown) { + throw new FirebaseError( + `Failed to get IAM bindings on secret: ${secretName}. Ensure you have the permissions to do so and try again. ` + + "For more information visit https://cloud.google.com/secret-manager/docs/manage-access-to-secrets#required-roles", + { original: getError(err) }, + ); + } + + do { + try { + const newBindings: iam.Binding[] = [ + { + role: "roles/secretmanager.secretAccessor", + members: Object.entries(typeGuesses).map(([email, type]) => `${type}:${email}`), + }, + ]; + const updatedBindings = existingBindings.concat(newBindings); + await gcsm.setIamPolicy({ projectId, name: secretName }, updatedBindings); + break; + } catch (err: any) { + if (!(err instanceof FirebaseError)) { + throw new FirebaseError( + `Unexpected error updating IAM bindings on secret: ${secretName}`, + { + original: getError(err), + }, + ); + } + const match = /Principal (.*) is of type "([^"]+)"/.exec(err.message); + if (!match) { + throw new FirebaseError( + `Failed to set IAM bindings on secret: ${secretName}. Ensure you have the permissions to do so and try again.`, + { original: getError(err) }, + ); + } + typeGuesses[match[1]] = match[2]; + continue; + } + } while (true); + + 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: unknown) { + if (getErrStatus(err) !== 404) { + throw new FirebaseError("Unexpected error loading secret", { original: getError(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; +} + +/** + * Fetches secrets from Google Secret Manager and returns their values in plain text. + */ +export async function fetchSecrets( + projectId: string, + secrets: Secret[], +): Promise> { + let secretsKeyValuePairs: Map; + + try { + const secretPromises: Promise<[string, string]>[] = secrets.map(async (secretConfig) => { + const [name, version] = getSecretNameParts(secretConfig.secret!); + + const value = await gcsm.accessSecretVersion(projectId, name, version); + return [secretConfig.variable, value] as [string, string]; + }); + + const secretEntries = await Promise.all(secretPromises); + secretsKeyValuePairs = new Map(secretEntries); + } catch (e: any) { + throw new FirebaseError(`Error exporting secrets`, { + original: e, + }); + } + + return secretsKeyValuePairs; +} + +/** + * secret expected to be in format "myApiKeySecret@5", + * "projects/test-project/secrets/secretID", or + * "projects/test-project/secrets/secretID/versions/5" + */ +export function getSecretNameParts(secret: string): [string, string] { + let [name, version] = secret.split("@"); + if (!version) { + version = "latest"; + } + + return [name, version]; +} diff --git a/src/apphosting/utils.spec.ts b/src/apphosting/utils.spec.ts new file mode 100644 index 00000000000..591f4b67826 --- /dev/null +++ b/src/apphosting/utils.spec.ts @@ -0,0 +1,54 @@ +import { expect } from "chai"; + +import * as utils from "./utils"; +import * as promptImport from "../prompt"; +import * as sinon from "sinon"; + +describe("utils", () => { + describe("getEnvironmentName", () => { + it("should throw an error if environment can't be found", () => { + expect(utils.getEnvironmentName.bind(utils.getEnvironmentName, "apphosting.yaml")).to.throw( + "Invalid apphosting environment file", + ); + }); + + it("should return the environment if valid environment specific apphosting file is given", () => { + expect(utils.getEnvironmentName("apphosting.staging.yaml")).to.equal("staging"); + }); + }); + + describe("promptForAppHostingYaml", () => { + let prompt: sinon.SinonStubbedInstance; + + beforeEach(() => { + prompt = sinon.stub(promptImport); + }); + afterEach(() => { + sinon.verifyAndRestore(); + }); + it("should prompt with the correct options", async () => { + const apphostingFileNameToPathMap = new Map([ + ["apphosting.yaml", "/parent/cwd/apphosting.yaml"], + ["apphosting.staging.yaml", "/parent/apphosting.staging.yaml"], + ]); + + prompt.select.returns(Promise.resolve()); + + await utils.promptForAppHostingYaml(apphostingFileNameToPathMap); + + expect(prompt.select).to.have.been.calledWith({ + message: "Please select an App Hosting config:", + choices: [ + { + name: "base (apphosting.yaml)", + value: "/parent/cwd/apphosting.yaml", + }, + { + name: "staging (apphosting.yaml + apphosting.staging.yaml)", + value: "/parent/apphosting.staging.yaml", + }, + ], + }); + }); + }); +}); diff --git a/src/apphosting/utils.ts b/src/apphosting/utils.ts new file mode 100644 index 00000000000..35524a5d1a7 --- /dev/null +++ b/src/apphosting/utils.ts @@ -0,0 +1,55 @@ +import { FirebaseError } from "../error"; +import { APPHOSTING_BASE_YAML_FILE, APPHOSTING_YAML_FILE_REGEX } from "./config"; +import * as prompt from "../prompt"; + +/** + * Returns given an apphosting..yaml file + */ +export function getEnvironmentName(apphostingYamlFileName: string): string { + const found = apphostingYamlFileName.match(APPHOSTING_YAML_FILE_REGEX); + if (!found || found.length < 2 || !found[1]) { + throw new FirebaseError("Invalid apphosting environment file"); + } + + return found[1].replaceAll(".", ""); +} + +/** + * Prompts user for an App Hosting yaml file + * + * Given a map of App Hosting yaml file names and their paths + * (e.g: "apphosting.staging.yaml" => "/cwd/apphosting.staging.yaml"), this function + * will prompt the user to choose an App Hosting configuration. It returns the path + * of the chosen App Hosting configuration. + */ +export async function promptForAppHostingYaml( + apphostingFileNameToPathMap: Map, + promptMessage = "Please select an App Hosting config:", +): Promise { + const fileNames = Array.from(apphostingFileNameToPathMap.keys()); + + const baseFilePath = apphostingFileNameToPathMap.get(APPHOSTING_BASE_YAML_FILE); + const listOptions = fileNames.map((fileName) => { + if (fileName === APPHOSTING_BASE_YAML_FILE) { + return { + name: `base (${APPHOSTING_BASE_YAML_FILE})`, + value: baseFilePath!, + }; + } + + const environment = getEnvironmentName(fileName); + return { + name: baseFilePath + ? `${environment} (${APPHOSTING_BASE_YAML_FILE} + ${fileName})` + : `${environment} (${fileName})`, + value: apphostingFileNameToPathMap.get(fileName)!, + }; + }); + + const fileToExportPath = await prompt.select({ + message: promptMessage, + choices: listOptions, + }); + + return fileToExportPath; +} diff --git a/src/apphosting/yaml.spec.ts b/src/apphosting/yaml.spec.ts new file mode 100644 index 00000000000..4db263e34ad --- /dev/null +++ b/src/apphosting/yaml.spec.ts @@ -0,0 +1,52 @@ +import { expect } from "chai"; +import { AppHostingYamlConfig } from "./yaml"; + +describe("merge", () => { + it("merges incoming apphosting yaml config with precendence", () => { + const apphostingYaml = AppHostingYamlConfig.empty(); + apphostingYaml.env = { + ENV_1: { value: "env_1" }, + ENV_2: { value: "env_2" }, + SECRET: { secret: "secret_1" }, + }; + + const incomingAppHostingYaml = AppHostingYamlConfig.empty(); + incomingAppHostingYaml.env = { + ENV_1: { value: "incoming_env_1" }, + ENV_3: { value: "incoming_env_3" }, + SECRET_2: { value: "incoming_secret_2" }, + }; + + apphostingYaml.merge(incomingAppHostingYaml); + expect(apphostingYaml.env).to.deep.equal({ + ENV_1: { value: "incoming_env_1" }, + ENV_2: { value: "env_2" }, + ENV_3: { value: "incoming_env_3" }, + SECRET: { secret: "secret_1" }, + SECRET_2: { value: "incoming_secret_2" }, + }); + }); + + it("conditionally allows secrets to become plaintext", () => { + const apphostingYaml = AppHostingYamlConfig.empty(); + apphostingYaml.env = { + API_KEY: { secret: "api_key" }, + }; + + const incomingYaml = AppHostingYamlConfig.empty(); + incomingYaml.env = { + API_KEY: { value: "plaintext" }, + }; + + expect(() => + apphostingYaml.merge(incomingYaml, /* alllowSecretsToBecomePlaintext */ false), + ).to.throw("Cannot convert secret to plaintext in apphosting yaml"); + + expect(() => + apphostingYaml.merge(incomingYaml, /* alllowSecretsToBecomePlaintext */ true), + ).to.not.throw(); + expect(apphostingYaml.env).to.deep.equal({ + API_KEY: { value: "plaintext" }, + }); + }); +}); diff --git a/src/apphosting/yaml.ts b/src/apphosting/yaml.ts new file mode 100644 index 00000000000..10ff529e6fa --- /dev/null +++ b/src/apphosting/yaml.ts @@ -0,0 +1,108 @@ +import { basename, dirname } from "path"; +import { readFileFromDirectory, wrappedSafeLoad } from "../utils"; +import { Config, Env, store } from "./config"; +import * as yaml from "yaml"; +import * as jsYaml from "js-yaml"; +import * as path from "path"; +import { fileExistsSync } from "../fsutils"; +import { FirebaseError } from "../error"; + +export type Secret = Omit; +export type EnvMap = Record>; + +/** + * AppHostingYamlConfig is an object representing an apphosting.yaml configuration + * present in the user's codebase (i.e 'apphosting.yaml', 'apphosting.staging.yaml', etc). + */ +export class AppHostingYamlConfig { + // Holds the basename of the file (e.g. apphosting.yaml vs apphosting.staging.yaml) + public filename: string | undefined; + public env: EnvMap = {}; + + /** + * Reads in the App Hosting yaml file found in filePath, parses the secrets and + * environment variables, and returns an object that makes it easier to + * programatically read or manipulate the App Hosting config. + */ + static async loadFromFile(filePath: string): Promise { + if (!fileExistsSync(filePath)) { + throw new FirebaseError(`Cannot load ${filePath} from given path, it doesn't exist`); + } + const config = new AppHostingYamlConfig(); + + const file = await readFileFromDirectory(dirname(filePath), basename(filePath)); + config.filename = path.basename(filePath); + const loadedAppHostingYaml = (await wrappedSafeLoad(file.source)) ?? {}; + + if (loadedAppHostingYaml.env) { + config.env = toEnvMap(loadedAppHostingYaml.env); + } + + return config; + } + + /** + * Simply returns an empty AppHostingYamlConfig (no environment variables + * or secrets). + */ + static empty() { + return new AppHostingYamlConfig(); + } + + /** + * Merges this AppHostingYamlConfig with another config, the incoming config + * has precedence if there are any conflicting configurations. + * */ + merge(other: AppHostingYamlConfig, allowSecretsToBecomePlaintext: boolean = true) { + if (!allowSecretsToBecomePlaintext) { + const wereSecrets = Object.entries(this.env) + .filter(([, env]) => env.secret) + .map(([key]) => key); + if (wereSecrets.some((key) => other.env[key]?.value)) { + throw new FirebaseError( + `Cannot convert secret to plaintext in ${other.filename ?? "apphosting yaml"}`, + ); + } + } + + this.env = { + ...this.env, + ...other.env, + }; + } + + /** + * Loads the given file if it exists and updates it. If + * it does not exist a new file will be created. + */ + async upsertFile(filePath: string) { + let yamlConfigToWrite: Config = {}; + + if (fileExistsSync(filePath)) { + const file = await readFileFromDirectory(dirname(filePath), basename(filePath)); + yamlConfigToWrite = await wrappedSafeLoad(file.source); + } + + yamlConfigToWrite.env = toEnvList(this.env); + + store(filePath, yaml.parseDocument(jsYaml.dump(yamlConfigToWrite))); + } +} + +// TODO: generalize into a utility function and remove the key from the array type. +export function toEnvMap(envs: Env[]): EnvMap { + return Object.fromEntries( + envs.map((env) => { + const variable = env.variable; + const tmp = { ...env }; + delete (env as any).variable; + return [variable, tmp]; + }), + ); +} + +export function toEnvList(envs: EnvMap): Env[] { + return Object.entries(envs).map(([variable, env]) => { + return { ...env, variable }; + }); +} diff --git a/src/apptesting/ensureProjectConfigured.spec.ts b/src/apptesting/ensureProjectConfigured.spec.ts new file mode 100644 index 00000000000..217c23a7c2e --- /dev/null +++ b/src/apptesting/ensureProjectConfigured.spec.ts @@ -0,0 +1,160 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; +import * as ensureApiEnabled from "../ensureApiEnabled"; +import * as iam from "../gcp/iam"; +import * as rm from "../gcp/resourceManager"; +import * as prompt from "../prompt"; +import * as utils from "../utils"; +import { FirebaseError } from "../error"; +import * as apptesting from "./ensureProjectConfigured"; + +describe("ensureProjectConfigured", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + + let serviceAccountHasRolesStub: sinon.SinonStub; + let confirmStub: sinon.SinonStub; + let ensureApiEnabledStub: sinon.SinonStub; + let createServiceAccountStub: sinon.SinonStub; + let addServiceAccountToRolesStub: sinon.SinonStub; + let logWarningStub: sinon.SinonStub; + + beforeEach(() => { + serviceAccountHasRolesStub = sandbox.stub(rm, "serviceAccountHasRoles"); + confirmStub = sandbox.stub(prompt, "confirm"); + ensureApiEnabledStub = sandbox.stub(ensureApiEnabled, "ensure"); + createServiceAccountStub = sandbox.stub(iam, "createServiceAccount"); + addServiceAccountToRolesStub = sandbox.stub(rm, "addServiceAccountToRoles"); + logWarningStub = sandbox.stub(utils, "logWarning"); + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + const projectId = "test-project"; + const serviceAccount = "firebaseapptesting-test-runner@test-project.iam.gserviceaccount.com"; + const TEST_RUNNER_ROLE = "roles/firebaseapptesting.testRunner"; + + it("should ensure all necessary APIs are enabled", async () => { + serviceAccountHasRolesStub.resolves(true); + ensureApiEnabledStub.resolves(); + + await apptesting.ensureProjectConfigured(projectId); + + expect(ensureApiEnabledStub).to.be.callCount(4); + expect(ensureApiEnabledStub).to.be.calledWith( + projectId, + "https://firebaseapptesting.googleapis.com", + "Firebase App Testing", + false, + ); + expect(ensureApiEnabledStub).to.be.calledWith( + projectId, + "https://run.googleapis.com", + "Cloud Run", + false, + ); + expect(ensureApiEnabledStub).to.be.calledWith( + projectId, + "https://storage.googleapis.com", + "Cloud Storage", + false, + ); + expect(ensureApiEnabledStub).to.be.calledWith( + projectId, + "https://artifactregistry.googleapis.com", + "Artifact Registry", + false, + ); + }); + + it("should do nothing if service account is already configured", async () => { + ensureApiEnabledStub.resolves(); + serviceAccountHasRolesStub.resolves(true); + + await apptesting.ensureProjectConfigured(projectId); + + expect(serviceAccountHasRolesStub).to.be.calledWith( + projectId, + serviceAccount, + [TEST_RUNNER_ROLE], + true, + ); + expect(confirmStub).to.not.have.been.called; + expect(createServiceAccountStub).to.not.have.been.called; + expect(addServiceAccountToRolesStub).to.not.have.been.called; + }); + + it("should provision service account if user confirms", async () => { + ensureApiEnabledStub.resolves(); + serviceAccountHasRolesStub.resolves(false); + confirmStub.resolves(true); + createServiceAccountStub.resolves(); + addServiceAccountToRolesStub.resolves(); + + await apptesting.ensureProjectConfigured(projectId); + + expect(serviceAccountHasRolesStub).to.be.calledWith( + projectId, + serviceAccount, + [TEST_RUNNER_ROLE], + true, + ); + expect(confirmStub).to.be.calledOnce; + expect(createServiceAccountStub).to.be.calledWith( + projectId, + "firebaseapptesting-test-runner", + sinon.match.string, + sinon.match.string, + ); + expect(addServiceAccountToRolesStub).to.be.calledWith( + projectId, + serviceAccount, + [TEST_RUNNER_ROLE], + true, + ); + }); + + it("should throw error if user denies service account creation", async () => { + ensureApiEnabledStub.resolves(); + serviceAccountHasRolesStub.resolves(false); + confirmStub.resolves(false); + + await expect(apptesting.ensureProjectConfigured(projectId)).to.be.rejectedWith( + FirebaseError, + /Firebase App Testing requires a service account/, + ); + + expect(confirmStub).to.be.calledOnce; + expect(createServiceAccountStub).to.not.have.been.called; + expect(addServiceAccountToRolesStub).to.not.have.been.called; + }); + + it("should handle service account already exists error", async () => { + ensureApiEnabledStub.resolves(); + serviceAccountHasRolesStub.resolves(false); + confirmStub.resolves(true); + createServiceAccountStub.rejects(new FirebaseError("Already exists", { status: 409 })); + addServiceAccountToRolesStub.resolves(); + + await apptesting.ensureProjectConfigured(projectId); + + expect(createServiceAccountStub).to.be.calledOnce; + expect(addServiceAccountToRolesStub).to.be.calledOnce; + }); + + it("should handle addServiceAccountToRoles 400 error", async () => { + ensureApiEnabledStub.resolves(); + serviceAccountHasRolesStub.resolves(false); + confirmStub.resolves(true); + createServiceAccountStub.resolves(); + addServiceAccountToRolesStub.rejects(new FirebaseError("Bad request", { status: 400 })); + + await apptesting.ensureProjectConfigured(projectId); + + expect(addServiceAccountToRolesStub).to.be.calledOnce; + expect(logWarningStub).to.be.calledWith( + `Your App Testing runner service account, "${serviceAccount}", is still being provisioned in the background. If you encounter an error, please try again after a few moments.`, + ); + }); +}); diff --git a/src/apptesting/ensureProjectConfigured.ts b/src/apptesting/ensureProjectConfigured.ts new file mode 100644 index 00000000000..1b5fa964cd2 --- /dev/null +++ b/src/apptesting/ensureProjectConfigured.ts @@ -0,0 +1,78 @@ +import { addServiceAccountToRoles, serviceAccountHasRoles } from "../gcp/resourceManager"; +import { ensure } from "../ensureApiEnabled"; +import { appTestingOrigin, artifactRegistryDomain, cloudRunApiOrigin, storageOrigin } from "../api"; +import { logBullet, logWarning } from "../utils"; +import { FirebaseError, getErrStatus } from "../error"; +import * as iam from "../gcp/iam"; +import { confirm } from "../prompt"; + +const TEST_RUNNER_ROLE = "roles/firebaseapptesting.testRunner"; +const TEST_RUNNER_SERVICE_ACCOUNT_NAME = "firebaseapptesting-test-runner"; + +export async function ensureProjectConfigured(projectId: string) { + await ensure(projectId, appTestingOrigin(), "Firebase App Testing", false); + await ensure(projectId, cloudRunApiOrigin(), "Cloud Run", false); + await ensure(projectId, storageOrigin(), "Cloud Storage", false); + await ensure(projectId, artifactRegistryDomain(), "Artifact Registry", false); + const serviceAccount = runnerServiceAccount(projectId); + + const serviceAccountExistsAndIsRunner = await serviceAccountHasRoles( + projectId, + serviceAccount, + [TEST_RUNNER_ROLE], + true, + ); + if (!serviceAccountExistsAndIsRunner) { + const grant = await confirm( + `Firebase App Testing runs tests in Cloud Run using a service account, provision an account, "${serviceAccount}", with the role "${TEST_RUNNER_ROLE}"?`, + ); + if (!grant) { + logBullet( + "You, or your project administrator, should run the following command to grant the required role:\n\n" + + `\tgcloud projects add-iam-policy-binding ${projectId} \\\n` + + `\t --member="serviceAccount:${serviceAccount}" \\\n` + + `\t --role="${TEST_RUNNER_ROLE}"\n`, + ); + throw new FirebaseError( + `Firebase App Testing requires a service account named "${serviceAccount}" with the "${TEST_RUNNER_ROLE}" role to execute tests using Cloud Run`, + ); + } + await provisionServiceAccount(projectId, serviceAccount); + } +} + +async function provisionServiceAccount(projectId: string, serviceAccount: string): Promise { + try { + await iam.createServiceAccount( + projectId, + TEST_RUNNER_SERVICE_ACCOUNT_NAME, + "Service Account used in Cloud Run, responsible for running tests", + "Firebase App Testing Test Runner", + ); + } catch (err: unknown) { + // 409 Already Exists errors can safely be ignored. + if (getErrStatus(err) !== 409) { + throw err; + } + } + try { + await addServiceAccountToRoles( + projectId, + serviceAccount, + [TEST_RUNNER_ROLE], + /* skipAccountLookup= */ true, + ); + } catch (err: unknown) { + if (getErrStatus(err) === 400) { + logWarning( + `Your App Testing runner service account, "${serviceAccount}", is still being provisioned in the background. If you encounter an error, please try again after a few moments.`, + ); + } else { + throw err; + } + } +} + +function runnerServiceAccount(projectId: string): string { + return `${TEST_RUNNER_SERVICE_ACCOUNT_NAME}@${projectId}.iam.gserviceaccount.com`; +} diff --git a/src/apptesting/invokeTests.spec.ts b/src/apptesting/invokeTests.spec.ts new file mode 100644 index 00000000000..558f8f2cd9b --- /dev/null +++ b/src/apptesting/invokeTests.spec.ts @@ -0,0 +1,152 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import { appTestingOrigin } from "../api"; +import { invokeTests, pollInvocationStatus } from "./invokeTests"; +import { FirebaseError } from "../error"; +import { Browser } from "./types"; + +describe("invokeTests", () => { + describe("invokeTests", () => { + const projectNumber = "123456789"; + const appId = `1:${projectNumber}:ios:abc123def456`; + + it("throws FirebaseError if invocation request fails", async () => { + nock(appTestingOrigin()) + .post(`/v1alpha/projects/${projectNumber}/apps/${appId}/testInvocations:invokeTestCases`) + .reply(400, { error: {} }); + await expect(invokeTests(appId, "https://www.example.com", [])).to.be.rejectedWith( + FirebaseError, + "Test invocation failed", + ); + expect(nock.isDone()).to.be.true; + }); + + it("returns operation when successful", async () => { + nock(appTestingOrigin()) + .post(`/v1alpha/projects/${projectNumber}/apps/${appId}/testInvocations:invokeTestCases`) + .reply(200, { name: "foo/bar/biz" }); + const operation = await invokeTests(appId, "https://www.example.com", []); + expect(operation).to.eql({ name: "foo/bar/biz" }); + expect(nock.isDone()).to.be.true; + }); + + it("builds the correct request", async () => { + let requestBody; + nock(appTestingOrigin()) + .post( + `/v1alpha/projects/${projectNumber}/apps/${appId}/testInvocations:invokeTestCases`, + (r) => { + requestBody = r; + return true; + }, + ) + .reply(200, { name: "foo/bar/biz" }); + + await invokeTests(appId, "https://www.example.com", [ + { + testCase: { + startUri: "https://www.example.com", + displayName: "testName1", + instructions: { steps: [{ goal: "test this app", hint: "try clicking the button" }] }, + }, + testExecution: [{ config: { browser: Browser.CHROME } }], + }, + { + testCase: { + startUri: "https://www.example.com", + displayName: "testName2", + instructions: { steps: [{ goal: "retest it", successCriteria: "a dialog appears" }] }, + }, + testExecution: [{ config: { browser: Browser.CHROME } }], + }, + ]); + + expect(requestBody).to.eql({ + resource: { + testCaseInvocations: [ + { + testCase: { + displayName: "testName1", + instructions: { + steps: [ + { + goal: "test this app", + hint: "try clicking the button", + }, + ], + }, + startUri: "https://www.example.com", + }, + testExecution: [ + { + config: { + browser: "CHROME", + }, + }, + ], + }, + { + testCase: { + displayName: "testName2", + instructions: { + steps: [ + { + goal: "retest it", + successCriteria: "a dialog appears", + }, + ], + }, + startUri: "https://www.example.com", + }, + testExecution: [ + { + config: { + browser: "CHROME", + }, + }, + ], + }, + ], + testInvocation: {}, + }, + }); + }); + }); + + describe("pollInvocationStatus", () => { + const operationName = "operations/foo/bar"; + + beforeEach(() => { + nock(appTestingOrigin()) + .get(`/v1alpha/${operationName}`) + .reply(200, { done: false, metadata: { count: 1 } }); + nock(appTestingOrigin()) + .get(`/v1alpha/${operationName}`) + .reply(200, { done: false, metadata: { count: 2 } }); + nock(appTestingOrigin()) + .get(`/v1alpha/${operationName}`) + .reply(200, { done: true, metadata: { count: 3 }, response: { foo: "12" } }); + }); + + it("calls poll callback with metadata on each poll", async () => { + const pollResponses: { [k: string]: any }[] = []; + await pollInvocationStatus( + operationName, + (op) => { + pollResponses.push(op.metadata!); + }, + /* backoff= */ 1, + ); + + expect(pollResponses).to.eql([{ count: 1 }, { count: 2 }, { count: 3 }]); + expect(nock.isDone()).to.be.true; + }); + + it("returns the response", async () => { + const response = await pollInvocationStatus(operationName, () => null, /* backoff= */ 1); + + expect(response).to.eql({ foo: "12" }); + expect(nock.isDone()).to.be.true; + }); + }); +}); diff --git a/src/apptesting/invokeTests.ts b/src/apptesting/invokeTests.ts new file mode 100644 index 00000000000..4d6c288a2cf --- /dev/null +++ b/src/apptesting/invokeTests.ts @@ -0,0 +1,57 @@ +import { Client } from "../apiv2"; +import { appTestingOrigin } from "../api"; +import { + InvokedTestCases, + InvokeTestCasesRequest, + TestCaseInvocation, + TestInvocation, +} from "./types"; +import * as operationPoller from "../operation-poller"; +import { FirebaseError, getError } from "../error"; + +const apiClient = new Client({ urlPrefix: appTestingOrigin(), apiVersion: "v1alpha" }); + +export async function invokeTests(appId: string, startUri: string, testDefs: TestCaseInvocation[]) { + const appResource = `projects/${appId.split(":")[1]}/apps/${appId}`; + try { + const invocationResponse = await apiClient.post< + InvokeTestCasesRequest, + operationPoller.LongRunningOperation + >(`${appResource}/testInvocations:invokeTestCases`, buildInvokeTestCasesRequest(testDefs)); + return invocationResponse.body; + } catch (err: unknown) { + throw new FirebaseError("Test invocation failed", { original: getError(err) }); + } +} + +function buildInvokeTestCasesRequest( + testCaseInvocations: TestCaseInvocation[], +): InvokeTestCasesRequest { + return { + resource: { + testInvocation: {}, + testCaseInvocations, + }, + }; +} + +interface InvocationOperation { + resource: InvokedTestCases; +} + +export async function pollInvocationStatus( + operationName: string, + onPoll: (invocation: operationPoller.OperationResult) => void, + backoff = 30 * 1000, +): Promise { + return operationPoller.pollOperation({ + pollerName: "App Testing Invocation Poller", + apiOrigin: appTestingOrigin(), + apiVersion: "v1alpha", + operationResourceName: operationName, + masterTimeout: 30 * 60 * 1000, // 30 minutes + backoff, + maxBackoff: 15 * 1000, // 30 seconds + onPoll, + }); +} diff --git a/src/apptesting/parseTestFiles.spec.ts b/src/apptesting/parseTestFiles.spec.ts new file mode 100644 index 00000000000..3a1aa13f9ed --- /dev/null +++ b/src/apptesting/parseTestFiles.spec.ts @@ -0,0 +1,207 @@ +import * as tmp from "tmp"; +import { expect } from "chai"; +import { rmSync } from "node:fs"; +import { join } from "node:path"; +import * as fs from "fs-extra"; +import { stringify } from "yaml"; +import { parseTestFiles } from "./parseTestFiles"; +import { readTemplateSync } from "../templates"; +import { Browser } from "./types"; +import { FirebaseError } from "../error"; + +describe("parseTestFiles", () => { + let tempdir: tmp.DirResult; + + beforeEach(() => { + tempdir = tmp.dirSync(); + }); + + afterEach(() => { + rmSync(tempdir.name, { recursive: true }); + }); + + function writeFile(filename: string, content: string) { + const file = join(tempdir.name, filename); + fs.writeFileSync(file, content); + } + + describe("parsing", () => { + it("throws an error for invalid targetUri", async () => { + writeFile( + "my_test.yaml", + stringify({ + defaultConfig: { route: "/mypage" }, + tests: [{ testName: "my test", steps: [{ goal: "click a button" }] }], + }), + ); + await expect(parseTestFiles(tempdir.name, "foo.com")).to.be.rejectedWith( + FirebaseError, + "Invalid URL", + ); + }); + + it("ignores invalid files", async () => { + writeFile( + "my_test.yaml", + stringify({ + defaultConfig: { route: "/mypage" }, + tests: [{ testName: "my test", steps: [{ goal: "click a button" }] }], + }), + ); + writeFile("my_test2.yaml", "foo"); + const tests = await parseTestFiles(tempdir.name, "http://www.foo.com"); + expect(tests).to.eql([ + { + testCase: { + displayName: "my test", + startUri: "http://www.foo.com/mypage", + instructions: { + steps: [ + { + goal: "click a button", + }, + ], + }, + }, + testExecution: [{ config: { browser: Browser.CHROME } }], + }, + ]); + }); + + it("parses the sample test case file", async () => { + writeFile("smoke_test.yaml", readTemplateSync("init/apptesting/smoke_test.yaml")); + const tests = await parseTestFiles(tempdir.name, "http://www.foo.com"); + expect(tests).to.eql([ + { + testCase: { + displayName: "Smoke test", + startUri: "http://www.foo.com", + instructions: { + steps: [ + { + goal: "View the provided application", + hint: "No additional actions should be necessary", + successCriteria: "The application should load with no obvious errors", + }, + ], + }, + }, + testExecution: [{ config: { browser: Browser.CHROME } }], + }, + ]); + }); + + it("parses multiple test case files", async () => { + writeFile( + "my_test.yaml", + stringify({ tests: [{ testName: "my test", steps: [{ goal: "click a button" }] }] }), + ); + writeFile( + "my_test2.yaml", + stringify({ + defaultConfig: { browsers: ["CHROME"] }, + tests: [ + { testName: "my second test", steps: [{ goal: "click a button" }] }, + { + testName: "my third test", + testConfig: { browsers: ["firefox"], route: "/mypage" }, + steps: [{ goal: "type something" }], + }, + ], + }), + ); + + const tests = await parseTestFiles(tempdir.name, "https://www.foo.com"); + expect(tests).to.eql([ + { + testCase: { + displayName: "my test", + startUri: "https://www.foo.com", + instructions: { + steps: [ + { + goal: "click a button", + }, + ], + }, + }, + testExecution: [{ config: { browser: "CHROME" } }], + }, + { + testCase: { + displayName: "my second test", + startUri: "https://www.foo.com", + instructions: { + steps: [ + { + goal: "click a button", + }, + ], + }, + }, + testExecution: [{ config: { browser: "CHROME" } }], + }, + + { + testCase: { + displayName: "my third test", + startUri: "https://www.foo.com/mypage", + instructions: { + steps: [ + { + goal: "type something", + }, + ], + }, + }, + testExecution: [{ config: { browser: "firefox" } }], + }, + ]); + }); + }); + + describe("filtering", () => { + function createBasicTest(testNames: string[]) { + return stringify({ + tests: testNames.map((testName) => ({ + testName, + steps: [{ goal: "do something" }], + })), + }); + } + + async function getTestCaseNames(filenameFilter = "", testCaseFilter = "") { + const tests = await parseTestFiles( + tempdir.name, + "https://www.foo.com", + filenameFilter, + testCaseFilter, + ); + return tests.map((t) => t.testCase.displayName); + } + + it("returns an empty list if no match", async () => { + writeFile("aaa", createBasicTest(["axx", "ayy", "azz"])); + writeFile("bbb", createBasicTest(["bxx", "byy", "bzz"])); + expect(await getTestCaseNames("yyy")).to.eql([]); + }); + + it("filters on filename", async () => { + writeFile("aaa", createBasicTest(["axx", "ayy", "azz"])); + writeFile("bbb", createBasicTest(["bxx", "byy", "bzz"])); + expect(await getTestCaseNames("aaa")).to.eql(["axx", "ayy", "azz"]); + }); + + it("filters on test case name", async () => { + writeFile("aaa", createBasicTest(["axx", "ayy", "azz"])); + writeFile("bbb", createBasicTest(["bxx", "byy", "bzz"])); + expect(await getTestCaseNames("", ".xx")).to.eql(["axx", "bxx"]); + }); + + it("filters on filename and test case name", async () => { + writeFile("aaa", createBasicTest(["axx", "ayy", "azz"])); + writeFile("bbb", createBasicTest(["bxx", "byy", "bzz"])); + expect(await getTestCaseNames("a$", "xx")).to.eql(["axx"]); + }); + }); +}); diff --git a/src/apptesting/parseTestFiles.ts b/src/apptesting/parseTestFiles.ts new file mode 100644 index 00000000000..f366ed4af03 --- /dev/null +++ b/src/apptesting/parseTestFiles.ts @@ -0,0 +1,77 @@ +import { dirExistsSync, fileExistsSync, listFiles } from "../fsutils"; +import { join } from "path"; +import { logger } from "../logger"; +import { Browser, TestCaseInvocation } from "./types"; +import { readFileFromDirectory, wrappedSafeLoad } from "../utils"; +import { FirebaseError, getErrMsg, getError } from "../error"; + +function createFilter(pattern?: string) { + const regex = pattern ? new RegExp(pattern) : undefined; + return (s: string) => !regex || regex.test(s); +} + +export async function parseTestFiles( + dir: string, + targetUri: string, + filePattern?: string, + namePattern?: string, +): Promise { + try { + new URL(targetUri); + } catch (ex) { + const errMsg = "Invalid URL" + (targetUri.startsWith("http") ? "" : " (must include protocol)"); + throw new FirebaseError(errMsg, { original: getError(ex) }); + } + const fileFilterFn = createFilter(filePattern); + const nameFilterFn = createFilter(namePattern); + + async function parseTestFilesRecursive(testDir: string): Promise { + const items = listFiles(testDir); + const results = []; + for (const item of items) { + const path = join(testDir, item); + if (dirExistsSync(path)) { + results.push(...(await parseTestFilesRecursive(path))); + } else if (fileFilterFn(path) && fileExistsSync(path)) { + try { + const file = await readFileFromDirectory(testDir, item); + const parsedFile = wrappedSafeLoad(file.source); + const tests = parsedFile.tests; + const defaultConfig = parsedFile.defaultConfig; + if (!tests || !tests.length) { + logger.info(`No tests found in ${path}. Ignoring.`); + continue; + } + for (const rawTestDef of parsedFile.tests) { + if (!nameFilterFn(rawTestDef.testName)) continue; + const testDef = toTestDef(rawTestDef, targetUri, defaultConfig); + results.push(testDef); + } + } catch (ex) { + const errMsg = getErrMsg(ex); + const errDetails = errMsg ? `Error details: \n${errMsg}` : ""; + logger.info(`Unable to parse test file ${path}. Ignoring.${errDetails}`); + continue; + } + } + } + return results; + } + + return parseTestFilesRecursive(dir); +} + +function toTestDef(testDef: any, targetUri: string, defaultConfig: any): TestCaseInvocation { + const steps = testDef.steps ?? []; + const route = testDef.testConfig?.route ?? defaultConfig?.route ?? ""; + const browsers: Browser[] = testDef.testConfig?.browsers ?? + defaultConfig?.browsers ?? [Browser.CHROME]; + return { + testCase: { + startUri: targetUri + route, + displayName: testDef.testName, + instructions: { steps }, + }, + testExecution: browsers.map((browser) => ({ config: { browser } })), + }; +} diff --git a/src/apptesting/types.ts b/src/apptesting/types.ts new file mode 100644 index 00000000000..fc1fefca1df --- /dev/null +++ b/src/apptesting/types.ts @@ -0,0 +1,71 @@ +export interface TestStep { + goal: string; + successCriteria?: string; + hint?: string; +} + +export enum Browser { + BROWSER_UNSPECIFIED = "BROWSER_UNSPECIFIED", + CHROME = "CHROME", +} + +export interface InvokedTestCases { + testInvocation: TestInvocation; + testCaseInvocations: TestCaseInvocation[]; +} + +export interface ExecutionMetadata { + runningExecutions?: number; + succeededExecutions?: number; + failedExecutions?: number; + totalExecutions?: number; + cancelledExecutions?: number; +} + +export interface TestInvocation extends ExecutionMetadata { + name?: string; + createTime?: string; +} + +export interface TestCaseInvocation { + name?: string; + testCase: TestCase; + testExecution: TestExecution[]; +} + +export interface TestExecution { + config: ExecutionConfig; + result?: TestExecutionResult; +} + +export interface ExecutionConfig { + browser: Browser; +} + +export enum CompletionReason { + COMPLETION_REASON_UNSPECIFIED, + MAX_STEPS_REACHED, + GOAL_FAILED, + NO_ACTIONS_REQUIRED, + GOAL_INCONCLUSIVE, + TEST_CANCELLED, + GOAL_SUCCEEDED, +} + +export interface TestExecutionResult { + completionReason: CompletionReason; +} + +export interface TestCase { + startUri: string; + displayName: string; + instructions: Instructions; +} + +export interface Instructions { + steps: TestStep[]; +} + +export interface InvokeTestCasesRequest { + resource: InvokedTestCases; +} 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..cbcadd992b3 --- /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, getError } 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: unknown) { + if (err instanceof FirebaseError) { + throw err; + } + throw new FirebaseError("Failed to create archive.", { original: getError(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..e65d18a12d5 --- /dev/null +++ b/src/auth.spec.ts @@ -0,0 +1,178 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { + assertAccount, + 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); + }); + }); + + describe("assertAccount", () => { + const defaultAccount: Account = { + user: { + email: "test@test.com", + }, + tokens: { + access_token: "abc1234", + }, + }; + + beforeEach(() => { + configstore.set("user", defaultAccount.user); + configstore.set("tokens", defaultAccount.tokens); + }); + + it("should not throw an error if the account exists", () => { + expect(() => assertAccount("test@test.com")).to.not.throw(); + }); + + it("should throw an error if the account does not exist", () => { + expect(() => assertAccount("nonexistent@test.com")).to.throw( + "Account nonexistent@test.com does not exist", + ); + }); + }); +}); diff --git a/src/auth.ts b/src/auth.ts index ed1b1c7bc95..b099c848f94 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,80 +1,46 @@ -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"; +import { FirebaseError, getErrMsg } from "./error"; import * as utils from "./utils"; import { logger } from "./logger"; -import { prompt } from "./prompt"; +import { input } 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"; +import { refreshAuth } from "./requireAuth"; + +portfinder.setBasePort(9005); /** * Get the global default account. Before multi-auth was implemented @@ -139,6 +105,23 @@ export function getAllAccounts(): Account[] { return res; } +/** + * Throw an error if the provided email is not a signed-in user. + */ +export function assertAccount(email: string, options?: { mcp?: boolean }) { + const allAccounts = getAllAccounts(); + const accountExists = allAccounts.some((a) => a.user.email === email); + if (!accountExists) { + throw new FirebaseError( + `Account ${email} does not exist, ${ + options?.mcp + ? `use the 'firebase_get_environment' tool to see available accounts or instruct the user to use the 'firebase login:add' terminal command to add a new account.` + : `run "${clc.bold("firebase login:list")} to see valid accounts` + }`, + ); + } +} + /** * Set the globally active account. Modifies the options object * and sets global refresh token state. @@ -159,7 +142,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 +170,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 +205,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; @@ -247,7 +227,19 @@ export function setProjectAccount(projectDir: string, email: string) { /** * Set the global default account. */ -export function setGlobalDefaultAccount(account: Account) { +export function setGlobalDefaultAccount(accountOrEmail: string | Account) { + let account: Account; + if (typeof accountOrEmail === "string") { + const accountFromEmail = getAllAccounts().find((acc) => acc.user.email === accountOrEmail)!; + if (!accountFromEmail) + throw new FirebaseError( + `Account '${accountOrEmail}' is not a signed-in user on this device.`, + ); + account = accountFromEmail; + } else { + account = accountOrEmail; + } + configstore.set("user", account.user); configstore.set("tokens", account.tokens); @@ -267,14 +259,14 @@ function open(url: string): void { // Always create a new error so that the stack is useful function invalidCredentialError(): FirebaseError { - return new FirebaseError( + const message = "Authentication Error: Your credentials are no longer valid. Please run " + - clc.bold("firebase login --reauth") + - "\n\n" + - "For CI servers and headless environments, generate a new token with " + - clc.bold("firebase login:ci"), - { exit: 1 } - ); + clc.bold("firebase login --reauth") + + "\n\n" + + "For CI servers and headless environments, generate a new token with " + + clc.bold("firebase login:ci"); + logger.error(message); + return new FirebaseError(message, { exit: 1 }); } const FIFTEEN_MINUTES_IN_MS = 15 * 60 * 1000; @@ -311,10 +303,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 +316,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: unknown) { if (err instanceof Error) { logger.debug("Token Fetch Error:", err.stack || ""); } else { @@ -349,15 +355,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 +372,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,77 +384,173 @@ 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, "_"); +} - logger.info(); - logger.info("Visit this URL on any device to log in:"); - logger.info(clc.bold.underline(authUrl)); - logger.info(); +interface PrototyperRes { + uri: string; + sessionId: string; + authorize: (authorizationCode: string) => Promise; +} - open(authUrl); +export async function loginPrototyper(): Promise { + const authProxyClient = new apiv2.Client({ + urlPrefix: authProxyOrigin(), + auth: false, + }); - const answers: { code: string } = await prompt({}, [ - { - type: "input", - name: "code", - message: "Paste authorization code here:", - }, - ]); - const tokens = await getTokensFromAuthorizationCode(answers.code, callbackUrl); - // getTokensFromAuthorizationCode doesn't handle the --token case, so we know - // that we'll have a valid id_token. + 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}&studio_prototyper=true}`; return { - user: jwt.decode(tokens.id_token!) as User, - tokens: tokens, - scopes: SCOPES, + uri: loginUrl, + sessionId: sessionId.substring(0, 5).toUpperCase(), + authorize: async (code: string) => { + const tokens = await getTokensFromAuthorizationCode( + code, + `${authProxyOrigin()}/complete`, + codeVerifier, + ); + + const creds = { + user: jwt.decode(tokens.id_token!, { json: true }) as any as User, + tokens: tokens, + scopes: SCOPES, + }; + recordCredentials(creds); + return creds; + }, }; } +// recordCredentials saves credentials to configstore to be used in future command runs. +export function recordCredentials(creds: UserCredentials) { + configstore.set("user", creds.user); + configstore.set("tokens", creds.tokens); + // store login scopes in case mandatory scopes grow over time + configstore.set("loginScopes", creds.scopes); + // remove old session token, if it exists + configstore.delete("session"); +} + +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("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(); + + const code = await input({ message: "Enter authorization code:" }); + + 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!, }; @@ -457,32 +559,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; @@ -490,10 +594,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: unknown) { + const html = await readTemplate("loginFailure.html"); + respondHtml(req, res, 400, html); reject(err); } server.close(); @@ -503,7 +608,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..."); @@ -518,15 +623,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 { @@ -538,7 +642,20 @@ export function findAccountByEmail(email: string): Account | undefined { return getAllAccounts().find((a) => a.user.email === email); } -function haveValidTokens(refreshToken: string, authScopes: string[]) { +export function loggedIn() { + return !!lastAccessToken; +} + +export function isExpired(tokens: Tokens | undefined): boolean { + const hasExpiration = (p: any): p is TokensWithExpiration => !!p.expires_at; + if (hasExpiration(tokens)) { + return !(tokens && tokens.expires_at && tokens.expires_at > Date.now()); + } else { + return !tokens; + } +} + +export function haveValidTokens(refreshToken: string, authScopes: string[]) { if (!lastAccessToken?.access_token) { const tokens = configstore.get("tokens"); if (refreshToken === tokens?.refresh_token) { @@ -552,9 +669,16 @@ function haveValidTokens(refreshToken: string, authScopes: string[]) { const hasSameScopes = oldScopesJSON === newScopesJSON; // To avoid token expiration in the middle of a long process we only hand out // tokens if they have a _long_ time before the server rejects them. - const isExpired = (lastAccessToken?.expires_at || 0) < Date.now() + FIFTEEN_MINUTES_IN_MS; - - return hasTokens && hasSameScopes && !isExpired; + const expired = (lastAccessToken?.expires_at || 0) < Date.now() + FIFTEEN_MINUTES_IN_MS; + const valid = hasTokens && hasSameScopes && !expired; + if (hasTokens) { + logger.debug( + `Checked if tokens are valid: ${valid}, expires at: ${lastAccessToken?.expires_at}`, + ); + } else { + logger.debug("No OAuth tokens found"); + } + return valid; } function deleteAccount(account: Account) { @@ -613,37 +737,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); @@ -653,7 +795,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 " + @@ -661,7 +803,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 }, ); } @@ -669,12 +811,20 @@ 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); + if (refreshToken) { + return refreshTokens(refreshToken, authScopes); + } else { + try { + return refreshAuth(); + } catch (err: unknown) { + logger.debug(`Unable to refresh token: ${getErrMsg(err)}`); + } + throw new FirebaseError("Unable to getAccessToken"); + } } export async function logout(refreshToken: string) { @@ -683,13 +833,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, @@ -697,3 +843,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/cli.ts b/src/bin/cli.ts new file mode 100644 index 00000000000..32f7f324584 --- /dev/null +++ b/src/bin/cli.ts @@ -0,0 +1,118 @@ +import * as updateNotifierPkg from "update-notifier-cjs"; +import * as clc from "colorette"; +import { markedTerminal } from "marked-terminal"; +import { marked } from "marked"; +marked.use(markedTerminal() as any); + +import { CommanderStatic } from "commander"; +import * as fs from "node:fs"; + +import { configstore } from "../configstore"; +import { errorOut } from "../errorOut"; +import { handlePreviewToggles } from "../handlePreviewToggles"; +import { logger, useFileLogger } from "../logger"; +import * as client from ".."; +import * as fsutils from "../fsutils"; +import * as utils from "../utils"; + +import { enableExperimentsFromCliEnvVariable } from "../experiments"; +import { fetchMOTD } from "../fetchMOTD"; + +export function cli(pkg: any) { + const updateNotifier = updateNotifierPkg({ pkg }); + + const args = process.argv.slice(2); + let cmd: CommanderStatic; + + if (!process.env.DEBUG && args.includes("--debug")) { + process.env.DEBUG = "true"; + } + + process.env.IS_FIREBASE_CLI = "true"; + + const logFilename = useFileLogger(); + + 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(); + + 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 installMethod = !process.env.FIREPIT_VERSION ? "npm" : "automatic script"; + const updateCommand = !process.env.FIREPIT_VERSION + ? "npm install -g firebase-tools" + : "curl -sL https://firebase.tools | upgrade=true bash"; + + const updateMessage = + `Update available ${clc.gray("{currentVersion}")} → ${clc.green("{latestVersion}")}\n` + + `To update to the latest version using ${installMethod}, run\n${clc.cyan(updateCommand)}\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)) { + // determine if there are any arguments. if not, display help + if (!args.length) { + client.cli.help(); + } else { + cmd = client.cli.parse(process.argv); + } + } +} 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..9e80b9748ac --- /dev/null +++ b/src/bin/firebase.ts @@ -0,0 +1,21 @@ +#!/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); +} + +// we short-circuit the normal process for MCP +if (process.argv[2] === "mcp" || process.argv[2] === "experimental:mcp") { + const { mcp } = require("./mcp"); + mcp(); +} else { + const { cli } = require("./cli"); + cli(pkg); +} diff --git a/src/bin/mcp.ts b/src/bin/mcp.ts new file mode 100644 index 00000000000..e0905b5cf84 --- /dev/null +++ b/src/bin/mcp.ts @@ -0,0 +1,63 @@ +#!/usr/bin/env node + +import { useFileLogger } from "../logger"; +import { FirebaseMcpServer } from "../mcp/index"; +import { parseArgs } from "util"; +import { SERVER_FEATURES, ServerFeature } from "../mcp/types"; +import { markdownDocsOfTools } from "../mcp/tools/index.js"; +import { markdownDocsOfPrompts } from "../mcp/prompts/index.js"; +import { markdownDocsOfResources } from "../mcp/resources/index.js"; +import { resolve } from "path"; + +const STARTUP_MESSAGE = ` +This is a running process of the Firebase MCP server. This command should only be executed by an MCP client. An example MCP client configuration might be: + +{ + "mcpServers": { + "firebase": { + "command": "firebase", + "args": ["mcp", "--dir", "/path/to/firebase/project"] + } + } +} +`; + +export async function mcp(): Promise { + const { values } = parseArgs({ + options: { + only: { type: "string", default: "" }, + dir: { type: "string" }, + "generate-tool-list": { type: "boolean", default: false }, + "generate-prompt-list": { type: "boolean", default: false }, + "generate-resource-list": { type: "boolean", default: false }, + }, + allowPositionals: true, + }); + + let earlyExit = false; + if (values["generate-tool-list"]) { + console.log(markdownDocsOfTools()); + earlyExit = true; + } + if (values["generate-prompt-list"]) { + console.log(markdownDocsOfPrompts()); + earlyExit = true; + } + if (values["generate-resource-list"]) { + console.log(markdownDocsOfResources()); + earlyExit = true; + } + if (earlyExit) return; + + process.env.IS_FIREBASE_MCP = "true"; + useFileLogger(); + const activeFeatures = (values.only || "") + .split(",") + .filter((f) => SERVER_FEATURES.includes(f as ServerFeature)) as ServerFeature[]; + const server = new FirebaseMcpServer({ + activeFeatures, + projectRoot: values.dir ? resolve(values.dir) : undefined, + }); + await server.start(); + if (process.stdin.isTTY) process.stderr.write(STARTUP_MESSAGE); +} diff --git a/src/checkFirebaseSDKVersion.ts b/src/checkFirebaseSDKVersion.ts deleted file mode 100644 index 64460e9e407..00000000000 --- a/src/checkFirebaseSDKVersion.ts +++ /dev/null @@ -1,104 +0,0 @@ -import * as _ from "lodash"; -import * as clc from "cli-color"; -import * as path from "path"; -import * as semver from "semver"; -import * as spawn from "cross-spawn"; - -import * as utils from "./utils"; -import { logger } from "./logger"; - -interface NpmListResult { - name: string; - dependencies: { - "firebase-functions": { - version: string; - from: string; - resolved: string; - }; - }; -} - -interface NpmShowResult { - "dist-tags": { - latest: string; - }; -} - -/** - * Returns the version of firebase-functions SDK specified by package.json and package-lock.json. - * @param sourceDir Source directory of functions code - * @return version string (e.g. "3.1.2"), or void if firebase-functions is not in package.json - * or if we had trouble getting the version. - */ -export function getFunctionsSDKVersion(sourceDir: string): string | void { - try { - const child = spawn.sync("npm", ["list", "firebase-functions", "--json=true"], { - cwd: sourceDir, - encoding: "utf8", - }); - if (child.error) { - logger.debug("getFunctionsSDKVersion encountered error:", child.error.stack); - return; - } - const output: NpmListResult = JSON.parse(child.stdout); - return _.get(output, ["dependencies", "firebase-functions", "version"]); - } catch (e) { - logger.debug("getFunctionsSDKVersion encountered error:", e); - return; - } -} - -/** - * Checks if firebase-functions SDK is not the latest version in NPM, and prints update notice if it is outdated. - * If it is unable to do the check, it does nothing. - * @param options Options object from "firebase deploy" command. - */ -export function checkFunctionsSDKVersion(options: any): void { - if (!options.config.has("functions")) { - return; - } - - const sourceDir = path.join(options.config.projectDir, options.config.get("functions.source")); - const currentVersion = getFunctionsSDKVersion(sourceDir); - if (!currentVersion) { - logger.debug("getFunctionsSDKVersion was unable to retrieve 'firebase-functions' version"); - return; - } - try { - const child = spawn.sync("npm", ["show", "firebase-functions", "--json=true"], { - encoding: "utf8", - }); - if (child.error) { - logger.debug( - "checkFunctionsSDKVersion was unable to fetch information from NPM", - child.error.stack - ); - return; - } - const output: NpmShowResult = JSON.parse(child.stdout); - if (_.isEmpty(output)) { - return; - } - const latest = _.get(output, ["dist-tags", "latest"]); - - if (semver.lt(currentVersion, latest)) { - utils.logWarning( - clc.bold.yellow("functions: ") + - "package.json indicates an outdated version of firebase-functions.\nPlease upgrade using " + - clc.bold("npm install --save firebase-functions@latest") + - " in your functions directory." - ); - if (semver.satisfies(currentVersion, "0.x") && semver.satisfies(latest, "1.x")) { - utils.logWarning( - clc.bold.yellow("functions: ") + - "Please note that there will be breaking changes when you upgrade.\n Go to " + - clc.bold("https://firebase.google.com/docs/functions/beta-v1-diff") + - " to learn more." - ); - } - } - } catch (e) { - logger.debug("checkFunctionsSDKVersion encountered error:", e); - return; - } -} diff --git a/src/checkMinRequiredVersion.spec.ts b/src/checkMinRequiredVersion.spec.ts new file mode 100644 index 00000000000..2c4da43929e --- /dev/null +++ b/src/checkMinRequiredVersion.spec.ts @@ -0,0 +1,34 @@ +import { expect } from "chai"; +import { configstore } from "./configstore"; +import * as sinon from "sinon"; + +import { checkMinRequiredVersion } from "./checkMinRequiredVersion"; +import Sinon from "sinon"; + +describe("checkMinRequiredVersion", () => { + let configstoreStub: Sinon.SinonStub; + + beforeEach(() => { + configstoreStub = sinon.stub(configstore, "get"); + }); + + afterEach(() => { + configstoreStub.restore(); + }); + + it("should error if installed version is below the min required version", () => { + configstoreStub.withArgs("motd.key").returns("1000.1000.1000"); + + expect(() => { + checkMinRequiredVersion({}, "key"); + }).to.throw(); + }); + + it("should not error if installed version is above the min required version", () => { + configstoreStub.withArgs("motd.key").returns("0.0.0"); + + expect(() => { + checkMinRequiredVersion({}, "key"); + }).not.to.throw(); + }); +}); diff --git a/src/checkMinRequiredVersion.ts b/src/checkMinRequiredVersion.ts index 402065e555d..943c8e09a8e 100644 --- a/src/checkMinRequiredVersion.ts +++ b/src/checkMinRequiredVersion.ts @@ -8,13 +8,14 @@ const pkg = require("../package.json"); // eslint-disable-line @typescript-eslin /** * Checks if the CLI is on a recent enough version to use a command. * Errors if a min version is found and the CLI is below the minimum required version. + * @param options * @param key the motd key to that contains semver for the min version for a command. */ -export function checkMinRequiredVersion(key: string) { +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..46ee10a9ebf --- /dev/null +++ b/src/checkValidTargetFilters.ts @@ -0,0 +1,73 @@ +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", + "apphosting", +]); + +/** + * 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/command.spec.ts b/src/command.spec.ts new file mode 100644 index 00000000000..7fae95b9772 --- /dev/null +++ b/src/command.spec.ts @@ -0,0 +1,200 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as rc from "./rc"; +import * as nock from "nock"; + +import { Command, validateProjectId } from "./command"; +import { FirebaseError } from "./error"; + +describe("Command", () => { + let command: Command; + + beforeEach(() => { + command = new Command("example"); + }); + + it("should allow all basic behavior", () => { + expect(() => { + command.description("description!"); + command.option("-x, --foobar", "description", "value"); + command.withForce(); + command.before( + (arr: string[]) => { + return arr; + }, + ["foo", "bar"], + ); + command.alias("example2"); + command.help("here's how!"); + command.action(() => { + // do nothing + }); + }).not.to.throw(); + }); + + describe("runner", () => { + let rcStub: sinon.SinonStub; + beforeEach(() => { + rcStub = sinon + .stub(rc, "loadRC") + .returns(new rc.RC(undefined, { projects: { default: "default-project" } })); + }); + + afterEach(() => { + rcStub.restore(); + nock.cleanAll(); + }); + + it("should work when no arguments are passed and options", async () => { + const run = command + .action((options) => { + options.foo = "bar"; + return options; + }) + .runner(); + + const result = run({ foo: "baz" }); + await expect(result).to.eventually.have.property("foo", "bar"); + }); + + it("should execute befores before the action", async () => { + const run = command + .before((options) => { + options.foo = true; + }) + .action((options) => { + if (options.foo) { + options.bar = "baz"; + } + return options; + }) + .runner(); + + const result = run({}); + await expect(result).to.eventually.have.property("bar"); + }); + + it("should terminate execution if a before errors", async () => { + const run = command + .before(() => { + throw new Error("foo"); + }) + .action(() => { + throw new Error("THIS IS NOT FOO"); + }) + .runner(); + + const result = run(); + return expect(result).to.be.rejectedWith("foo"); + }); + + it("should reject the promise if an error is thrown", async () => { + const run = command + .action(() => { + throw new Error("foo"); + }) + .runner(); + + const result = run(); + await expect(result).to.be.rejectedWith("foo"); + }); + + it("should resolve a numeric --project flag into a project id", async () => { + nock("https://cloudresourcemanager.googleapis.com").get("/v1/projects/12345678").reply(200, { + projectNumber: "12345678", + projectId: "resolved-project", + }); + nock("https://serviceusage.googleapis.com") + .get("/v1/projects/12345678/services/cloudresourcemanager.googleapis.com") + .reply(200, { + state: "ENABLED", + }); + const run = command + .action((options) => { + return { + project: options.project, + projectNumber: options.projectNumber, + projectId: options.projectId, + }; + }) + .runner(); + + const result = await run({ project: "12345678", token: "thisisatoken" }); + expect(result).to.deep.eq({ + projectId: "resolved-project", + projectNumber: "12345678", + project: "12345678", + }); + }); + + it("should resolve a non-numeric --project flag into a project id", async () => { + const run = command + .action((options) => { + return { + project: options.project, + projectNumber: options.projectNumber, + projectId: options.projectId, + }; + }) + .runner(); + + const result = await run({ project: "resolved-project" }); + expect(result).to.deep.eq({ + projectId: "resolved-project", + projectNumber: undefined, + project: "resolved-project", + }); + }); + + it("should use the 'default' alias if no project is passed", async () => { + const run = command + .action((options) => { + return { + project: options.project, + projectNumber: options.projectNumber, + projectId: options.projectId, + }; + }) + .runner(); + + const result = await run({}); + expect(result).to.deep.eq({ + projectId: "default-project", + projectNumber: undefined, + project: "default-project", + }); + }); + }); +}); + +describe("validateProjectId", () => { + it("should not throw for valid project ids", () => { + expect(() => validateProjectId("example")).not.to.throw(); + expect(() => validateProjectId("my-project")).not.to.throw(); + expect(() => validateProjectId("myproject4fun")).not.to.throw(); + }); + + it("should not throw for legacy project ids", () => { + // The project IDs below are not technically valid, but some legacy projects + // may have IDs like that. We should not block these. + // https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects#resource:-project + expect(() => validateProjectId("example-")).not.to.throw(); + expect(() => validateProjectId("0123456")).not.to.throw(); + expect(() => validateProjectId("google.com:some-project")).not.to.throw(); + }); + + it("should block invalid project ids", () => { + expect(() => validateProjectId("EXAMPLE")).to.throw(FirebaseError, /Invalid project id/); + expect(() => validateProjectId("!")).to.throw(FirebaseError, /Invalid project id/); + expect(() => validateProjectId("with space")).to.throw(FirebaseError, /Invalid project id/); + expect(() => validateProjectId(" leadingspace")).to.throw(FirebaseError, /Invalid project id/); + expect(() => validateProjectId("trailingspace ")).to.throw(FirebaseError, /Invalid project id/); + expect(() => validateProjectId("has.dot")).to.throw(FirebaseError, /Invalid project id/); + }); + + it("should error with additional note for uppercase project ids", () => { + expect(() => validateProjectId("EXAMPLE")).to.throw(FirebaseError, /lowercase/); + expect(() => validateProjectId("Example")).to.throw(FirebaseError, /lowercase/); + expect(() => validateProjectId("Example-Project")).to.throw(FirebaseError, /lowercase/); + }); +}); diff --git a/src/command.ts b/src/command.ts index cf4b428ec0d..36d6ab502f6 100644 --- a/src/command.ts +++ b/src/command.ts @@ -1,16 +1,22 @@ -import { bold } from "cli-color"; +import * as clc from "colorette"; +import * as path from "node:path"; import { CommanderStatic } from "commander"; -import { first, last, get, size, head, keys, values } from "lodash"; +import { first, last, size, head, keys, values } from "lodash"; import { FirebaseError } from "./error"; -import { getInheritedOption, setupLoggers } from "./utils"; -import { load } from "./rc"; -import { load as _load } from "./config"; +import { getInheritedOption, 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 { getProject } from "./management/projects"; +import { reconcileStudioFirebaseProject } from "./management/studio"; +import { requireAuth } from "./requireAuth"; +import { Options } from "./options"; +import { useConsoleLoggers } from "./logger"; +import { isFirebaseStudio } from "./env"; // eslint-disable-next-line @typescript-eslint/no-explicit-any type ActionFunction = (...args: any[]) => any; @@ -35,6 +41,7 @@ export class Command { private descriptionText = ""; // eslint-disable-next-line @typescript-eslint/no-explicit-any private options: any[][] = []; + private aliases: string[] = []; private actionFn: ActionFunction = (): void => { // noop by default, unless overwritten by `.action(fn)`. }; @@ -60,6 +67,16 @@ export class Command { return this; } + /** + * Sets an alias for a command. + * @param aliases an alternativre name for the command. Users will be able to call the command via this name. + * @return the command, for chaining. + */ + alias(alias: string): Command { + this.aliases.push(alias); + return this; + } + /** * Sets any options for the command. * @@ -75,6 +92,17 @@ export class Command { return this; } + /** + * Sets up --force flag for the command. + * + * @param message overrides the description for --force for this command + * @returns the command, for chaining + */ + withForce(message?: string): Command { + this.options.push(["-f, --force", message || "automatically accept all interactive prompts"]); + return this; + } + /** * Attaches a function to run before the command's action function. * @param fn the function to run. @@ -113,7 +141,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). @@ -125,6 +153,9 @@ export class Command { if (this.descriptionText) { cmd.description(this.descriptionText); } + if (this.aliases) { + cmd.aliases(this.aliases); + } this.options.forEach((args) => { const flags = args.shift(); cmd.option(flags, ...args); @@ -132,6 +163,7 @@ export class Command { if (this.helpText) { cmd.on("--help", () => { + console.log(); // Seperates the help text from global options. console.log(this.helpText); }); } @@ -144,7 +176,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 @@ -160,47 +192,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); }); }); @@ -211,7 +295,7 @@ export class Command { * @param options the command options object. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - private prepare(options: any): void { + public async prepare(options: any): Promise { options = options || {}; options.project = getInheritedOption(options, "project"); @@ -230,8 +314,8 @@ export class Command { if (getInheritedOption(options, "json")) { options.nonInteractive = true; - } else { - setupLoggers(); + } else if (!options.isMCP) { + useConsoleLoggers(); } if (getInheritedOption(options, "config")) { @@ -239,26 +323,29 @@ export class Command { } try { - options.config = _load(options); - } catch (e) { + options.config = Config.load(options); + } catch (e: any) { options.configError = e; } - options.projectRoot = detectProjectRoot(options); - this.applyRC(options); - if (options.project) { - validateProjectId(options.project); - } - const account = getInheritedOption(options, "account"); options.account = account; + // selectAccount needs the projectRoot to be set. + options.projectRoot = detectProjectRoot(options); + const projectRoot = options.projectRoot; const activeAccount = selectAccount(account, projectRoot); if (activeAccount) { setActiveAccount(options, activeAccount); } + + await this.applyRC(options); + if (options.project) { + await this.resolveProjectIdentifiers(options); + validateProjectId(options.projectId); + } } /** @@ -266,25 +353,71 @@ export class Command { * @param options the command options object. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - private applyRC(options: any): void { - const rc = load(options); + private async applyRC(options: Options) { + const rc = loadRC(options); options.rc = rc; + let activeProject = this.configstoreProject(options.projectRoot || process.cwd()); + + // Only fetch the Studio Workspace project if we're running in Firebase + // Studio. If the user passes the project via --project, it should take + // priority. + // If this is the firebase use command, don't worry about reconciling - the user is changing it anyway + const isUseCommand = process.argv.includes("use"); + if (isFirebaseStudio() && !options.project && !isUseCommand) { + activeProject = await reconcileStudioFirebaseProject(options, activeProject); + } - options.project = - options.project || (configstore.get("activeProjects") || {})[options.projectRoot]; + options.project = options.project ?? activeProject; // support deprecated "firebase" key in firebase.json if (options.config && !options.project) { options.project = options.config.defaults.project; } const aliases = rc.projects; - const rcProject = get(aliases, options.project); + const rcProject = options.project ? aliases[options.project] : undefined; if (rcProject) { + // Look up aliases options.projectAlias = options.project; options.project = rcProject; } else if (!options.project && size(aliases) === 1) { + // If there's only a single alias, use that. + // This seems to be how we originally implemented default project - keeping this behavior to avoid breaking any unusual set ups. options.projectAlias = head(keys(aliases)); options.project = head(values(aliases)); + } else if (!options.project && aliases["default"]) { + // If there's an alias named 'default', default to that. + options.projectAlias = "default"; + options.project = aliases["default"]; + } + } + + private configstoreProject(dir: string) { + const projectMap = configstore.get("activeProjects") ?? {}; + let currentDir = path.resolve(dir); + while (true) { + if (projectMap[currentDir]) { + return projectMap[currentDir]; + } + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + return null; + } + currentDir = parentDir; + } + } + + private async resolveProjectIdentifiers(options: { + project?: string; + projectId?: string; + projectNumber?: string; + }): Promise { + if (options.project?.match(/^\d+$/)) { + await requireAuth(options); + const { projectId, projectNumber } = await getProject(options.project); + options.projectId = projectId; + options.projectNumber = projectNumber; + } else { + options.projectId = options.project; } } @@ -294,7 +427,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 @@ -310,7 +443,7 @@ export class Command { } const options = last(args); - this.prepare(options); + await this.prepare(options); for (const before of this.befores) { await before.fn(options, ...before.args); @@ -335,7 +468,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 6a2917525c4..61fb435fb2c 100644 --- a/src/commands/appdistribution-distribute.ts +++ b/src/commands/appdistribution-distribute.ts @@ -3,28 +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 { - AabState, - AppDistributionApp, - AppDistributionClient, - AppView, - UploadStatus, -} from "../appdistribution/client"; -import { FirebaseError } from "../error"; + AabInfo, + IntegrationState, + UploadReleaseResult, + TestDevice, + ReleaseTest, +} from "../appdistribution/types"; +import { FirebaseError, getErrMsg, getErrStatus } from "../error"; import { Distribution, DistributionFileType } from "../appdistribution/distribution"; +import { + ensureFileExists, + getAppName, + getLoginCredential, + parseTestDevices, + parseIntoStringArray, +} from "../appdistribution/options-parser-util"; -function ensureFileExists(file: string, message = ""): void { - if (!fs.existsSync(file)) { - throw new FirebaseError(`File ${file} does not exist: ${message}`); - } -} - -function getAppId(appId: string): string { - if (!appId) { - throw new FirebaseError("set the --app option to a valid Firebase app id and try again"); - } - return appId; -} +const TEST_MAX_POLLING_RETRIES = 40; +const TEST_POLLING_INTERVAL_MILLIS = 30_000; function getReleaseNotes(releaseNotes: string, releaseNotesFile: string): string { if (releaseNotes) { @@ -37,145 +35,275 @@ function getReleaseNotes(releaseNotes: string, releaseNotesFile: string): string return ""; } -function getTestersOrGroups(value: string, file: string): string[] { - if (!value && file) { - ensureFileExists(file); - value = fs.readFileSync(file, "utf8"); - } - - if (value) { - return value - .split(/,|\n/) - .map((entry) => entry.trim()) - .filter((entry) => !!entry); - } - - return []; -} - -module.exports = new Command("appdistribution:distribute ") - .description("upload a distribution") - .option("--app ", "the app id of your Firebase app") - .option("--release-notes ", "release notes to include with this distribution") - .option( - "--release-notes-file ", - "path to file with release notes to include with this distribution" +export const command = new Command("appdistribution:distribute ") + .description( + "upload a release binary and optionally distribute it to testers and run automated tests", ) - .option("--testers ", "a comma separated list of tester emails to distribute to") + .option("--app ", "the app id of your Firebase app") + .option("--release-notes ", "release notes to include") + .option("--release-notes-file ", "path to file with release notes") + .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- or newline-separated list of tester emails to distribute to", ) - .option("--groups ", "a comma separated list of group aliases 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- or newline-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.", + ) + .option("--test-case-ids ", "a comma-separated list of test case IDs.") + .option( + "--test-case-ids-file ", + "path to file with a comma- or newline-separated list of test case IDs.", ) .before(requireAuth) .action(async (file: string, options: any) => { - const appId = getAppId(options.app); + const appName = getAppName(options); const distribution = new Distribution(file); const releaseNotes = getReleaseNotes(options.releaseNotes, options.releaseNotesFile); - const testers = getTestersOrGroups(options.testers, options.testersFile); - const groups = getTestersOrGroups(options.groups, options.groupsFile); - const requests = new AppDistributionClient(appId); - let app: AppDistributionApp; - - try { - const appView = - distribution.distributionFileType() === DistributionFileType.AAB - ? AppView.FULL - : AppView.BASIC; - app = await requests.getApp(appView); - } catch (err) { - if (err.status === 404) { - throw new FirebaseError( - `App Distribution could not find your app ${appId}. ` + - `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 } - ); - } - throw new FirebaseError(`failed to fetch app information. ${err.message}`, { exit: 1 }); - } - - if (!app.contactEmail) { + const testers = parseIntoStringArray(options.testers, options.testersFile); + const groups = parseIntoStringArray(options.groups, options.groupsFile); + const testCases = parseIntoStringArray(options.testCaseIds, options.testCaseIdsFile); + const testDevices = parseTestDevices(options.testDevices, options.testDevicesFile); + if (testCases.length && (options.testUsernameResource || options.testPasswordResource)) { throw new FirebaseError( - `We could not find a contact email for app ${appId}. Please visit App Distribution within ` + - "the Firebase Console to set one up.", - { exit: 1 } + "Password and username resource names are not supported for the testing agent.", ); } + 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 && - app.aabState !== AabState.ACTIVE && - app.aabState !== AabState.AAB_STATE_UNAVAILABLE - ) { - switch (app.aabState) { - case AabState.PLAY_ACCOUNT_NOT_LINKED: { - throw new FirebaseError("This project is not linked to a Google Play account."); - } - case AabState.APP_NOT_PUBLISHED: { - throw new FirebaseError('"This app is not published in the Google Play console.'); - } - case AabState.NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT: { - throw new FirebaseError("App with matching package name does not exist in Google Play."); - } - case AabState.PLAY_IAS_TERMS_NOT_ACCEPTED: { + if (distribution.distributionFileType() === DistributionFileType.AAB) { + try { + aabInfo = await requests.getAabInfo(appName); + } catch (err: unknown) { + if (getErrStatus(err) === 404) { throw new FirebaseError( - "You must accept the Play Internal App Sharing (IAS) terms to upload AABs." + `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 }, ); } - default: { - throw new FirebaseError("App Distribution failed to process the AAB: " + app.aabState); + throw new FirebaseError(`failed to determine AAB info. ${getErrMsg(err)}`, { exit: 1 }); + } + + if ( + aabInfo.integrationState !== IntegrationState.INTEGRATED && + aabInfo.integrationState !== IntegrationState.AAB_STATE_UNAVAILABLE + ) { + switch (aabInfo.integrationState) { + case IntegrationState.PLAY_ACCOUNT_NOT_LINKED: { + throw new FirebaseError("This project is not linked to a Google Play account."); + } + case IntegrationState.APP_NOT_PUBLISHED: { + throw new FirebaseError('"This app is not published in the Google Play console.'); + } + 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.", + ); + } + case IntegrationState.PLAY_IAS_TERMS_NOT_ACCEPTED: { + throw new FirebaseError( + "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, + ); + } } } } - let binaryName = await distribution.binaryName(app); - - // Upload the distribution if it hasn't been uploaded before - let releaseId: string; - const uploadStatus = await requests.getUploadStatus(binaryName); + utils.logBullet("uploading binary..."); + let releaseName; + try { + const operationName = await requests.uploadRelease(appName, distribution); - if (uploadStatus.status === UploadStatus.SUCCESS) { - utils.logWarning("this distribution has been uploaded before, skipping upload"); - releaseId = uploadStatus.release.id; - } else { - // If there's an error, we know that the distribution hasn't been uploaded before - utils.logBullet("uploading distribution..."); + // The upload process is asynchronous, so poll to figure out when the upload has finished successfully + const uploadResponse = await requests.pollUploadStatus(operationName); - try { - binaryName = await requests.uploadDistribution(distribution); - - // The upload process is asynchronous, so poll to figure out when the upload has finished successfully - releaseId = await requests.pollUploadStatus(binaryName); - utils.logSuccess("uploaded distribution successfully!"); - } catch (err) { - throw new FirebaseError(`failed to upload distribution. ${err.message}`, { exit: 1 }); + const release = uploadResponse.release; + switch (uploadResponse.result) { + case UploadReleaseResult.RELEASE_CREATED: + utils.logSuccess( + `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!`, + ); + break; + case UploadReleaseResult.RELEASE_UNMODIFIED: + utils.logSuccess( + `re-uploaded already existing release ${release.displayVersion} (${release.buildVersion}) successfully!`, + ); + break; + default: + utils.logSuccess( + `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: unknown) { + if (getErrStatus(err) === 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 }, + ); } + throw new FirebaseError(`Failed to upload release. ${getErrMsg(err)}`, { exit: 1 }); } // If this is an app bundle and the certificate was originally blank fetch the updated // certificate and print - if (distribution.distributionFileType() === DistributionFileType.AAB && !app.aabCertificate) { - const updatedApp = await requests.getApp(); - if (updatedApp.aabCertificate) { + if (aabInfo && !aabInfo.testCertificate) { + aabInfo = await requests.getAabInfo(appName); + if (aabInfo.testCertificate) { utils.logBullet( "After you upload an AAB for the first time, App Distribution " + "generates a new test certificate. All AAB uploads are re-signed with this test " + "certificate. Use the certificate fingerprints below to register your app " + "signing key with API providers, such as Google Sign-In and Google Maps.\n" + - `MD-1 certificate fingerprint: ${updatedApp.aabCertificate.certificateHashMd5}\n` + - `SHA-1 certificate fingerprint: ${updatedApp.aabCertificate.certificateHashSha1}\n` + - `SHA-256 certificate fingerprint: ${updatedApp.aabCertificate.certificateHashSha256}` + `MD-1 certificate fingerprint: ${aabInfo.testCertificate.hashMd5}\n` + + `SHA-1 certificate fingerprint: ${aabInfo.testCertificate.hashSha1}\n` + + `SHA-256 certificate fingerprint: ${aabInfo.testCertificate.hashSha256}`, ); } } - // Add release notes and testers/groups - await requests.addReleaseNotes(releaseId, releaseNotes); - await requests.enableAccess(releaseId, testers, groups); + // 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 test (note: this feature is in beta)"); + const releaseTestPromises: Promise[] = []; + if (!testCases.length) { + // fallback to basic automated test + releaseTestPromises.push( + requests.createReleaseTest(releaseName, testDevices, loginCredential), + ); + } else { + for (const testCaseId of testCases) { + releaseTestPromises.push( + requests.createReleaseTest( + releaseName, + testDevices, + loginCredential, + `${appName}/testCases/${testCaseId}`, + ), + ); + } + } + const releaseTests = await Promise.all(releaseTestPromises); + utils.logSuccess(`${releaseTests.length} release test(s) started successfully`); + if (!options.testNonBlocking) { + await awaitTestResults(releaseTests, requests); + } + } + }); + +async function awaitTestResults( + releaseTests: ReleaseTest[], + requests: AppDistributionClient, +): Promise { + const releaseTestNames = new Set(releaseTests.map((rt) => rt.name!)); + for (let i = 0; i < TEST_MAX_POLLING_RETRIES; i++) { + utils.logBullet(`${releaseTestNames.size} automated test results are pending...`); + await delay(TEST_POLLING_INTERVAL_MILLIS); + for (const releaseTestName of releaseTestNames) { + const releaseTest = await requests.getReleaseTest(releaseTestName); + if (releaseTest.deviceExecutions.every((e) => e.state === "PASSED")) { + releaseTestNames.delete(releaseTestName); + if (releaseTestNames.size === 0) { + utils.logSuccess("Automated test(s) passed!"); + return; + } else { + continue; + } + } + 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 run your test(s), 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-groups-create.ts b/src/commands/appdistribution-groups-create.ts new file mode 100644 index 00000000000..4a5efb0d0f8 --- /dev/null +++ b/src/commands/appdistribution-groups-create.ts @@ -0,0 +1,18 @@ +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:groups:create [alias]") + .description("create an App Distribution group") + .alias("appdistribution:group:create") + .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-groups-delete.ts b/src/commands/appdistribution-groups-delete.ts new file mode 100644 index 00000000000..9ee71bad793 --- /dev/null +++ b/src/commands/appdistribution-groups-delete.ts @@ -0,0 +1,22 @@ +import { Command } from "../command"; +import * as utils from "../utils"; +import { requireAuth } from "../requireAuth"; +import { FirebaseError, getErrMsg } from "../error"; +import { AppDistributionClient } from "../appdistribution/client"; +import { getProjectName } from "../appdistribution/options-parser-util"; + +export const command = new Command("appdistribution:groups:delete ") + .description("delete an App Distribution group") + .alias("appdistribution:group:delete") + .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: unknown) { + throw new FirebaseError(`Failed to delete group ${getErrMsg(err)}`); + } + utils.logSuccess(`Group ${alias} has successfully been deleted`); + }); diff --git a/src/commands/appdistribution-groups-list.ts b/src/commands/appdistribution-groups-list.ts new file mode 100644 index 00000000000..5b7fa7ae6b2 --- /dev/null +++ b/src/commands/appdistribution-groups-list.ts @@ -0,0 +1,61 @@ +import * as ora from "ora"; +import { AppDistributionClient } from "../appdistribution/client"; +import { getProjectName } from "../appdistribution/options-parser-util"; +import { Group, ListGroupsResponse } from "../appdistribution/types"; +import { Command } from "../command"; +import { FirebaseError } from "../error"; +import { logger } from "../logger"; +import { Options } from "../options"; +import { requireAuth } from "../requireAuth"; +import * as utils from "../utils"; +import * as Table from "cli-table3"; + +export const command = new Command("appdistribution:groups:list") + .description("list App Distribution groups") + .alias("appdistribution:group:list") + .before(requireAuth) + .action(async (options?: Options): Promise => { + const projectName = await getProjectName(options); + const appDistroClient = new AppDistributionClient(); + let groupsResponse: ListGroupsResponse; + const spinner = ora("Preparing the list of your App Distribution Groups").start(); + try { + groupsResponse = await appDistroClient.listGroups(projectName); + } catch (err: any) { + spinner.fail(); + throw new FirebaseError("Failed to list groups.", { + exit: 1, + original: err, + }); + } + spinner.succeed(); + const groups = groupsResponse.groups ?? []; + printGroupsTable(groups); + utils.logSuccess(`Groups listed successfully`); + return groupsResponse; + }); + +/** + * Prints a table given a list of groups + */ +function printGroupsTable(groups: Group[]): void { + const tableHead = ["Group", "Display Name", "Tester Count", "Release Count", "Invite Link Count"]; + + const table = new Table({ + head: tableHead, + style: { head: ["green"] }, + }); + + for (const group of groups) { + const name = group.name.split("/").pop(); + table.push([ + name, + group.displayName, + group.testerCount || 0, + group.releaseCount || 0, + group.inviteLinkCount || 0, + ]); + } + + logger.info(table.toString()); +} diff --git a/src/commands/appdistribution-testers-add.ts b/src/commands/appdistribution-testers-add.ts new file mode 100644 index 00000000000..27d85a68a64 --- /dev/null +++ b/src/commands/appdistribution-testers-add.ts @@ -0,0 +1,28 @@ +import { Command } from "../command"; +import * as utils from "../utils"; +import { requireAuth } from "../requireAuth"; +import { AppDistributionClient } from "../appdistribution/client"; +import { getEmails, getProjectName } from "../appdistribution/options-parser-util"; + +export const command = new Command("appdistribution:testers:add [emails...]") + .description("add testers to project (and App Distribution group, if specified via flag)") + .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); + const appDistroClient = new AppDistributionClient(); + 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-list.ts b/src/commands/appdistribution-testers-list.ts new file mode 100644 index 00000000000..5481f175d53 --- /dev/null +++ b/src/commands/appdistribution-testers-list.ts @@ -0,0 +1,58 @@ +import * as ora from "ora"; +import { AppDistributionClient } from "../appdistribution/client"; +import { getProjectName } from "../appdistribution/options-parser-util"; +import { ListTestersResponse, Tester } from "../appdistribution/types"; +import { Command } from "../command"; +import { FirebaseError } from "../error"; +import { logger } from "../logger"; +import { Options } from "../options"; +import { requireAuth } from "../requireAuth"; +import * as utils from "../utils"; +import * as Table from "cli-table3"; + +export const command = new Command("appdistribution:testers:list [group]") + .description("list testers in project") + .before(requireAuth) + .action(async (group: string | undefined, options: Options): Promise => { + const projectName = await getProjectName(options); + const appDistroClient = new AppDistributionClient(); + let testersResponse: ListTestersResponse; + const spinner = ora("Preparing the list of your App Distribution testers").start(); + try { + testersResponse = await appDistroClient.listTesters(projectName, group); + } catch (err: any) { + spinner.fail(); + throw new FirebaseError("Failed to list testers.", { + exit: 1, + original: err, + }); + } + spinner.succeed(); + const testers = testersResponse.testers ?? []; + printTestersTable(testers); + utils.logSuccess(`Testers listed successfully`); + return testersResponse; + }); + +/** + * Prints a table given a list of testers + */ +function printTestersTable(testers: Tester[]): void { + const tableHead = ["Name", "Display Name", "Last Activity Time", "Groups"]; + + const table = new Table({ + head: tableHead, + style: { head: ["green"] }, + }); + + for (const tester of testers) { + const name = tester.name.split("/").pop(); + const groups = (tester.groups ?? []) + .map((grp) => grp.split("/").pop()) + .sort() + .join(";"); + table.push([name, tester.displayName ?? "", tester.lastActivityTime.toString(), groups]); + } + + logger.info(table.toString()); +} diff --git a/src/commands/appdistribution-testers-remove.ts b/src/commands/appdistribution-testers-remove.ts new file mode 100644 index 00000000000..2070454df02 --- /dev/null +++ b/src/commands/appdistribution-testers-remove.ts @@ -0,0 +1,43 @@ +import { Command } from "../command"; +import * as utils from "../utils"; +import { requireAuth } from "../requireAuth"; +import { FirebaseError, getErrMsg } from "../error"; +import { AppDistributionClient } from "../appdistribution/client"; +import { getEmails, getProjectName } from "../appdistribution/options-parser-util"; +import { logger } from "../logger"; + +export const command = new Command("appdistribution:testers:remove [emails...]") + .description("remove testers from a project (or App Distribution group, if specified via flag)") + .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); + 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: unknown) { + throw new FirebaseError(`Failed to remove testers ${getErrMsg(err)}`); + } + + 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`); + } + }); diff --git a/src/commands/apphosting-backends-create.ts b/src/commands/apphosting-backends-create.ts new file mode 100644 index 00000000000..b09a02d585c --- /dev/null +++ b/src/commands/apphosting-backends-create.ts @@ -0,0 +1,49 @@ +import { Command } from "../command"; +import { FirebaseError } from "../error"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { requireAuth } from "../requireAuth"; +import { doSetup } from "../apphosting/backend"; +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( + "--backend ", + "specify the name of the new backend. Required with --non-interactive.", + ) + .option( + "-s, --service-account ", + "specify the service account used to run the server", + "", + ) + .option( + "--primary-region ", + "specify the primary region for the backend. Required with --non-interactive.", + ) + .option("--root-dir ", "specify the root directory for the backend.") + .before(requireAuth) + .before(ensureApiEnabled) + .before(requireTosAcceptance(APPHOSTING_TOS_ID)) + .action(async (options: Options) => { + const projectId = needProjectId(options); + if (options.nonInteractive && (options.backend == null || options.primaryRegion == null)) { + throw new FirebaseError(`--non-interactive option requires --backend and --primary-region`); + } + + await doSetup( + projectId, + options.nonInteractive, + options.app as string | undefined, + options.backend as string | undefined, + options.serviceAccount as string | undefined, + options.primaryRegion as string | undefined, + options.rootDir as string | undefined, + ); + }); diff --git a/src/commands/apphosting-backends-delete.ts b/src/commands/apphosting-backends-delete.ts new file mode 100644 index 00000000000..fe1dd45817f --- /dev/null +++ b/src/commands/apphosting-backends-delete.ts @@ -0,0 +1,54 @@ +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { requireAuth } from "../requireAuth"; +import { FirebaseError, getError } from "../error"; +import { confirm } from "../prompt"; +import * as utils from "../utils"; +import * as apphosting from "../gcp/apphosting"; +import { printBackendsTable } from "./apphosting-backends-list"; +import { deleteBackendAndPoll, chooseBackends } from "../apphosting/backend"; +import * as ora from "ora"; + +export const command = new Command("apphosting:backends:delete ") + .description("delete a Firebase App Hosting backend") + .withForce() + .before(requireAuth) + .before(apphosting.ensureApiEnabled) + .action(async (backendId: string, options: Options) => { + const projectId = needProjectId(options); + + const backends = await chooseBackends( + projectId, + backendId, + "Please select the backends you'd like to delete:", + options.force, + ); + + utils.logWarning("You are about to permanently delete these backend(s):"); + printBackendsTable(backends); + + const confirmDeletion = await confirm({ + message: "Are you sure?", + default: false, + force: options.force, + nonInteractive: options.nonInteractive, + }); + if (!confirmDeletion) { + return; + } + + for (const b of backends) { + const { location, id } = apphosting.parseBackendName(b.name); + const spinner = ora(`Deleting backend ${id}(${location})...`).start(); + try { + await deleteBackendAndPoll(projectId, location, id); + spinner.succeed(`Successfully deleted the backend: ${id}(${location})`); + } catch (err: unknown) { + spinner.stop(); + throw new FirebaseError(`Failed to delete backend: ${id}(${location}). Please retry.`, { + original: getError(err), + }); + } + } + }); diff --git a/src/commands/apphosting-backends-get.ts b/src/commands/apphosting-backends-get.ts new file mode 100644 index 00000000000..9040d429724 --- /dev/null +++ b/src/commands/apphosting-backends-get.ts @@ -0,0 +1,42 @@ +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { requireAuth } from "../requireAuth"; +import { FirebaseError, getError } 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") + .before(requireAuth) + .before(apphosting.ensureApiEnabled) + .action(async (backend: string, options: Options) => { + const projectId = needProjectId(options); + + let backendsList: apphosting.Backend[] = []; + try { + const resp = await apphosting.listBackends(projectId, "-"); + const allBackends = resp.backends || []; + backendsList = allBackends.filter((bkd) => bkd.name.split("/").pop() === backend); + } catch (err: unknown) { + throw new FirebaseError( + `Failed to get backend: ${backend}. Please check the parameters you have provided.`, + { original: getError(err) }, + ); + } + if (backendsList.length === 0) { + logWarning(`Backend "${backend}" not found`); + return; + } + if (backendsList.length > 1) { + const regions = backendsList.map((b) => apphosting.parseBackendName(b.name).location); + logWarning( + `Detected multiple backends with the same ${backend} ID in regions: ${regions.join(", ")}}. This is not allowed until we can support more locations.\n` + + `Please delete and recreate any backends that share an ID with another backend. ` + + `Use apphosting:backends:list to see all backends.\n Returning the following backend:`, + ); + } + printBackendsTable(backendsList.slice(0, 1)); + 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..5c750689f6d --- /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 { requireAuth } from "../requireAuth"; +import { Options } from "../options"; +import * as apphosting from "../gcp/apphosting"; +import * as Table from "cli-table3"; + +const TABLE_HEAD = ["Backend", "Repository", "URL", "Primary Region", "Updated Date"]; + +export const command = new Command("apphosting:backends:list") + .description("list Firebase App Hosting backends") + .before(requireAuth) + .before(apphosting.ensureApiEnabled) + .action(async (options: Options) => { + const projectId = needProjectId(options); + 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..48b6745b915 --- /dev/null +++ b/src/commands/apphosting-builds-create.ts @@ -0,0 +1,39 @@ +import * as apphosting from "../gcp/apphosting"; +import { logger } from "../logger"; +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { requireAuth } from "../requireAuth"; +import { logWarning } from "../utils"; + +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") + .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(requireAuth) + .before(apphosting.ensureApiEnabled) + .action(async (backendId: string, options: Options) => { + const projectId = needProjectId(options); + if (options.location !== undefined) { + logWarning("--location is being removed in the next major release."); + } + const location = (options.location as string) ?? "us-central1"; + 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..60fde0c335d --- /dev/null +++ b/src/commands/apphosting-builds-get.ts @@ -0,0 +1,24 @@ +import * as apphosting from "../gcp/apphosting"; +import { logger } from "../logger"; +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { requireAuth } from "../requireAuth"; +import { logWarning } from "../utils"; + +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") + .before(requireAuth) + .before(apphosting.ensureApiEnabled) + .action(async (backendId: string, buildId: string, options: Options) => { + if (options.location !== undefined) { + logWarning("--location is being removed in the next major release."); + } + options.location = options.location ?? "us-central"; + 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-repos-create.ts b/src/commands/apphosting-repos-create.ts new file mode 100644 index 00000000000..e361a35b8bc --- /dev/null +++ b/src/commands/apphosting-repos-create.ts @@ -0,0 +1,23 @@ +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import requireInteractive from "../requireInteractive"; +import { createGitRepoLink } from "../apphosting/backend"; +import { ensureApiEnabled } from "../gcp/apphosting"; +import { APPHOSTING_TOS_ID } from "../gcp/firedata"; +import { requireTosAcceptance } from "../requireTosAcceptance"; + +export const command = new Command("apphosting:repos:create") + .description("create a Firebase App Hosting Developer Connect Git Repository Link") + .option("-l, --location ", "specify the location of the backend", "") + .option("-g, --gitconnection ", "id of the connection", "") + .before(ensureApiEnabled) + .before(requireInteractive) + .before(requireTosAcceptance(APPHOSTING_TOS_ID)) + .action(async (options: Options) => { + const projectId = needProjectId(options); + const location = options.location; + const connection = options.gitconnection; + + await createGitRepoLink(projectId, location as string | null, connection as string | undefined); + }); diff --git a/src/commands/apphosting-rollouts-create.ts b/src/commands/apphosting-rollouts-create.ts new file mode 100644 index 00000000000..5616e847ebc --- /dev/null +++ b/src/commands/apphosting-rollouts-create.ts @@ -0,0 +1,31 @@ +import * as apphosting from "../gcp/apphosting"; +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { requireAuth } from "../requireAuth"; +import { FirebaseError } from "../error"; +import { createRollout } from "../apphosting/rollout"; + +export const command = new Command("apphosting:rollouts:create ") + .description("create a rollout using a build for an App Hosting backend") + .option( + "-b, --git-branch ", + "repository branch to deploy (mutually exclusive with -g)", + ) + .option("-g, --git-commit ", "git commit to deploy (mutually exclusive with -b)") + .withForce("Skip confirmation before creating rollout") + .before(requireAuth) + .before(apphosting.ensureApiEnabled) + .action(async (backendId: string, options: Options) => { + const projectId = needProjectId(options); + + const branch = options.gitBranch as string | undefined; + const commit = options.gitCommit as string | undefined; + if (branch && commit) { + throw new FirebaseError( + "Cannot specify both a branch and commit to deploy. Please specify either --git-branch or --git-commit.", + ); + } + + await createRollout(backendId, projectId, branch, commit, options.force); + }); diff --git a/src/commands/apphosting-rollouts-list.ts b/src/commands/apphosting-rollouts-list.ts new file mode 100644 index 00000000000..4ee5b7a075d --- /dev/null +++ b/src/commands/apphosting-rollouts-list.ts @@ -0,0 +1,31 @@ +import * as apphosting from "../gcp/apphosting"; +import { logger } from "../logger"; +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { requireAuth } from "../requireAuth"; +import { logWarning } from "../utils"; + +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(requireAuth) + .before(apphosting.ensureApiEnabled) + .action(async (backendId: string, options: Options) => { + if (options.location !== undefined) { + logWarning("--location is being removed in the next major release."); + } + 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..ffbc4024234 --- /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..5f6b6076359 --- /dev/null +++ b/src/commands/apphosting-secrets-describe.ts @@ -0,0 +1,29 @@ +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"; +import * as Table from "cli-table3"; + +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..cf7b500074a --- /dev/null +++ b/src/commands/apphosting-secrets-grantaccess.ts @@ -0,0 +1,83 @@ +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/backend"; + +export const command = new Command("apphosting:secrets:grantaccess ") + .description( + "Grant service accounts, users, or groups permissions to the provided secret(s). Can pass one or more secrets, separated by a comma", + ) + .option("-l, --location ", "backend location", "-") + .option("-b, --backend ", "backend name") + .option("-e, --emails ", "comma delimited list of user or group emails") + .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 (secretNames: string, options: Options) => { + const projectId = needProjectId(options); + const projectNumber = await needProjectNumber(options); + + if (!options.backend && !options.emails) { + throw new FirebaseError( + "Missing required flag --backend or --emails. See firebase apphosting:secrets:grantaccess --help for more info", + ); + } + if (options.backend && options.emails) { + throw new FirebaseError( + "Cannot specify both --backend and --emails. See firebase apphosting:secrets:grantaccess --help for more info", + ); + } + + const secretList = secretNames.split(","); + for (const secretName of secretList) { + const exists = await secretManager.secretExists(projectId, secretName); + if (!exists) { + throw new FirebaseError(`Cannot find secret ${secretName}`); + } + } + + if (options.emails) { + return await secrets.grantEmailsSecretAccess( + projectId, + secretList, + String(options.emails).split(","), + ); + } + + 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( + await secrets.serviceAccountsForBackend(projectNumber, backend), + ); + + await Promise.allSettled( + secretList.map((secretName) => + 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..cff79788294 --- /dev/null +++ b/src/commands/apphosting-secrets-set.ts @@ -0,0 +1,110 @@ +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"; +import * as prompt from "../prompt"; + +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 type = await prompt.select({ + message: "Is this secret for production or only local testing?", + choices: [ + { name: "Production", value: "production" }, + { name: "Local testing only", value: "local" }, + ], + }); + + if (type === "local") { + const emailList = await prompt.input({ + message: + "Please enter a comma separated list of user or groups who should have access to this secret:", + }); + if (emailList.length) { + await secrets.grantEmailsSecretAccess(projectId, [secretName], emailList.split(",")); + } else { + utils.logBullet( + "To grant access in the future run " + + clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --emails [email list]`), + ); + } + await config.maybeAddSecretToYaml(secretName, config.APPHOSTING_EMULATORS_YAML_FILE); + 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, config.APPHOSTING_BASE_YAML_FILE); + utils.logBullet( + "To grant additional users access to this secret run " + + clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --email [email list]`) + + ".\nTo grant additional backends access to this secret run " + + clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --backend [backend ID]`), + ); + }); diff --git a/src/commands/apps-android-sha-create.ts b/src/commands/apps-android-sha-create.ts index 4697615b730..7a7609ea9ee 100644 --- a/src/commands/apps-android-sha-create.ts +++ b/src/commands/apps-android-sha-create.ts @@ -1,7 +1,7 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import { Command } from "../command"; -import * as getProjectId from "../getProjectId"; +import { needProjectId } from "../projectUtils"; import { AppAndroidShaData, createAppAndroidSha, ShaCertificateType } from "../management/apps"; import { requireAuth } from "../requireAuth"; import { promiseWithSpinner } from "../utils"; @@ -9,17 +9,17 @@ 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 ") - .description("add a SHA certificate hash for a given app id.") +export const command = new Command("apps:android:sha:create ") + .description("add a SHA certificate hash for a given app id") .before(requireAuth) .action( async (appId: string = "", shaHash: string = "", options: any): Promise => { - const projectId = getProjectId(options); + const projectId = needProjectId(options); const shaCertificate = await promiseWithSpinner( async () => @@ -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 f6128ec8dc5..02cac18d3b1 100644 --- a/src/commands/apps-android-sha-delete.ts +++ b/src/commands/apps-android-sha-delete.ts @@ -1,23 +1,21 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import { Command } from "../command"; -import * as getProjectId from "../getProjectId"; +import { needProjectId } from "../projectUtils"; import { deleteAppAndroidSha } from "../management/apps"; import { requireAuth } from "../requireAuth"; import { promiseWithSpinner } from "../utils"; -module.exports = new Command("apps:android:sha:delete ") - .description("delete a SHA certificate hash for a given app id.") +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 = getProjectId(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 03a73cbd62a..65e0f260f1c 100644 --- a/src/commands/apps-android-sha-list.ts +++ b/src/commands/apps-android-sha-list.ts @@ -1,7 +1,6 @@ -import Table = require("cli-table"); - +import * as Table from "cli-table3"; import { Command } from "../command"; -import * as getProjectId from "../getProjectId"; +import { needProjectId } from "../projectUtils"; import { listAppAndroidSha, AppAndroidShaData } from "../management/apps"; import { requireAuth } from "../requireAuth"; import { logger } from "../logger"; @@ -36,20 +35,18 @@ function logCertificatesCount(count: number = 0): void { logger.info(`${count} SHA hash(es) total.`); } -module.exports = new Command("apps:android:sha:list ") - .description("list the SHA certificate hashes for a given app id. ") +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 = getProjectId(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 653ec59f028..452ec2cf093 100644 --- a/src/commands/apps-create.ts +++ b/src/commands/apps-create.ts @@ -1,53 +1,26 @@ -import * as clc from "cli-color"; -import * as ora from "ora"; +import * as clc from "colorette"; import { Command } from "../command"; -import * as getProjectId from "../getProjectId"; +import { needProjectId } from "../projectUtils"; import { FirebaseError } from "../error"; import { AndroidAppMetadata, AppMetadata, AppPlatform, - createAndroidApp, - createIosApp, - createWebApp, getAppPlatform, IosAppMetadata, + sdkInit, + SdkInitOptions, WebAppMetadata, } from "../management/apps"; -import { prompt, promptOnce, Question } from "../prompt"; import { requireAuth } from "../requireAuth"; import { logger } from "../logger"; - -const DISPLAY_NAME_QUESTION: Question = { - type: "input", - name: "displayName", - default: "", - message: "What would you like to call your app?", -}; - -interface CreateFirebaseAppOptions { - project: string; - nonInteractive: boolean; - displayName?: string; -} - -interface CreateIosAppOptions extends CreateFirebaseAppOptions { - bundleId?: string; - appStoreId?: string; -} - -interface CreateAndroidAppOptions extends CreateFirebaseAppOptions { - packageName: string; -} - -interface CreateWebAppOptions extends CreateFirebaseAppOptions { - displayName: string; -} +import { Options } from "../options"; +import { select } from "../prompt"; function logPostAppCreationInformation( appMetadata: IosAppMetadata | AndroidAppMetadata | WebAppMetadata, - appPlatform: AppPlatform + appPlatform: AppPlatform, ): void { logger.info(""); logger.info(`🎉🎉🎉 Your Firebase ${appPlatform} App is ready! 🎉🎉🎉`); @@ -72,96 +45,15 @@ function logPostAppCreationInformation( logger.info(` firebase apps:sdkconfig ${appPlatform} ${appMetadata.appId}`); } -async function initiateIosAppCreation(options: CreateIosAppOptions): Promise { - if (!options.nonInteractive) { - await prompt(options, [ - DISPLAY_NAME_QUESTION, - { - type: "input", - default: "", - name: "bundleId", - message: "Please specify your iOS app bundle ID:", - }, - { - type: "input", - default: "", - name: "appStoreId", - message: "Please specify your iOS app App Store ID:", - }, - ]); - } - if (!options.bundleId) { - throw new FirebaseError("Bundle ID for iOS app cannot be empty"); - } - - const spinner = ora("Creating your iOS app").start(); - try { - const appData = await createIosApp(options.project, { - displayName: options.displayName, - bundleId: options.bundleId, - appStoreId: options.appStoreId, - }); - spinner.succeed(); - return appData; - } catch (err) { - spinner.fail(); - throw err; - } -} - -async function initiateAndroidAppCreation( - options: CreateAndroidAppOptions -): Promise { - if (!options.nonInteractive) { - await prompt(options, [ - DISPLAY_NAME_QUESTION, - { - type: "input", - default: "", - name: "packageName", - message: "Please specify your Android app package name:", - }, - ]); - } - if (!options.packageName) { - throw new FirebaseError("Package name for Android app cannot be empty"); - } - - const spinner = ora("Creating your Android app").start(); - try { - const appData = await createAndroidApp(options.project, { - displayName: options.displayName, - packageName: options.packageName, - }); - spinner.succeed(); - return appData; - } catch (err) { - spinner.fail(); - throw err; - } -} - -async function initiateWebAppCreation(options: CreateWebAppOptions): Promise { - if (!options.nonInteractive) { - await prompt(options, [DISPLAY_NAME_QUESTION]); - } - if (!options.displayName) { - throw new FirebaseError("Display name for Web app cannot be empty"); - } - const spinner = ora("Creating your Web app").start(); - try { - const appData = await createWebApp(options.project, { displayName: options.displayName }); - spinner.succeed(); - return appData; - } catch (err) { - spinner.fail(); - throw err; - } +interface AppsCreateOptions extends Options { + packageName: string; + bundleId: string; + appStoreId: string; } -module.exports = new Command("apps:create [platform] [displayName]") +export const command = new Command("apps:create [platform] [displayName]") .description( - "create a new Firebase app. [platform] can be IOS, ANDROID or WEB (case insensitive)." + "create a new Firebase app. [platform] can be IOS, ANDROID or WEB (case insensitive)", ) .option("-a, --package-name ", "required package name for the Android app") .option("-b, --bundle-id ", "required bundle id for the iOS app") @@ -169,15 +61,14 @@ module.exports = new Command("apps:create [platform] [displayName]") .before(requireAuth) .action( async ( - platform: string = "", + platform = "", displayName: string | undefined, - options: any + options: AppsCreateOptions, ): Promise => { - const projectId = getProjectId(options); + const projectId = needProjectId(options); if (!options.nonInteractive && !platform) { - platform = await promptOnce({ - type: "list", + platform = await select({ message: "Please choose the platform of the app:", choices: [ { name: "iOS", value: AppPlatform.IOS }, @@ -194,22 +85,8 @@ module.exports = new Command("apps:create [platform] [displayName]") logger.info(`Create your ${appPlatform} app in project ${clc.bold(projectId)}:`); options.displayName = displayName; // add displayName into options to pass into prompt function - let appData; - switch (appPlatform) { - case AppPlatform.IOS: - appData = await initiateIosAppCreation(options); - break; - case AppPlatform.ANDROID: - appData = await initiateAndroidAppCreation(options); - break; - case AppPlatform.WEB: - appData = await initiateWebAppCreation(options); - break; - default: - throw new FirebaseError("Unexpected error. This should not happen"); - } - + const appData = await sdkInit(appPlatform, options as SdkInitOptions); logPostAppCreationInformation(appData, appPlatform); return appData; - } + }, ); diff --git a/src/commands/apps-init.ts b/src/commands/apps-init.ts new file mode 100644 index 00000000000..c147b926404 --- /dev/null +++ b/src/commands/apps-init.ts @@ -0,0 +1,122 @@ +import * as fs from "fs-extra"; +import * as path from "path"; + +import { Command } from "../command"; +import { + AppConfig, + AppPlatform, + getAppConfigFile, + getAppPlatform, + getPlatform, + getSdkConfig, + getSdkOutputPath, + sdkInit, + writeConfigToFile, +} from "../management/apps"; +import { requireAuth } from "../requireAuth"; +import { logger } from "../logger"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { Platform } from "../appUtils"; +import { assertEnabled } from "../experiments"; + +export interface AppsInitOptions extends Options { + out?: string | boolean; +} + +function logUse(platform: AppPlatform, filePath: string) { + switch (platform) { + case AppPlatform.WEB: + logger.info(` +How to use your JS SDK Config: +ES Module: +import { initializeApp } from 'firebase/app'; +import json from './${filePath || "firebase-sdk-config.json"}'; +initializeApp(json); // or copy and paste the config directly from the json file here +// CommonJS Module: +const { initializeApp } = require('firebase/app'); +const json = require('./firebase-js-config.json'); +initializeApp(json); // or copy and paste the config directly from the json file here`); + break; + case AppPlatform.ANDROID: + logger.info( + `Visit https://firebase.google.com/docs/android/setup#add-config-file +for information on editing your gradle file and adding Firebase SDKs to your app. + +If you're using Unity or C++, visit https://firebase.google.com/docs/cpp/setup?platform=android#add-config-file +for information about adding your config file to your project.`, + ); + break; + case AppPlatform.IOS: + logger.info( + `Visit https://firebase.google.com/docs/ios/setup#add-config-file +for information on adding the config file to your targets and adding Firebase SDKs to your app. + +If you're using Unity or C++, visit https://firebase.google.com/docs/cpp/setup?platform=ios#add-config-file +for information about adding your config file to your project.`, + ); + break; + } +} + +function toAppPlatform(str: string) { + switch (str.toUpperCase()) { + case Platform.ANDROID: + return Platform.ANDROID as unknown as AppPlatform.ANDROID; + case Platform.IOS: + return Platform.IOS as unknown as AppPlatform.IOS; + case Platform.WEB: + return Platform.WEB as unknown as AppPlatform.WEB; + } + throw new Error(`Platform ${str} is not compatible with apps:configure`); +} + +export const command = new Command("apps:init [platform] [appId]") + .description("automatically download and create config of a Firebase app") + .before(requireAuth) + .option("-o, --out [file]", "(optional) write config output to a file") + .action(async (platform = "", appId = "", options: AppsInitOptions): Promise => { + assertEnabled("appsinit", "autoconfigure an app"); + if (typeof options.out === "boolean") { + throw new Error("Please specify a file path to output to."); + } + const config = options.config; + const appDir = process.cwd(); + // auto-detect the platform + const detectedPlatform = platform ? toAppPlatform(platform) : await getPlatform(appDir, config); + + let sdkConfig: AppConfig | undefined; + while (sdkConfig === undefined) { + try { + sdkConfig = await getSdkConfig(options, getAppPlatform(detectedPlatform), appId); + } catch (e) { + if ((e as Error).message.includes("associated with this Firebase project")) { + const projectId = needProjectId(options); + await sdkInit(detectedPlatform, { ...options, project: projectId }); + } else { + throw e; + } + } + } + + let outputPath = options.out; + + const fileInfo = getAppConfigFile(sdkConfig, detectedPlatform); + let relativePath = ""; + outputPath = outputPath || (await getSdkOutputPath(appDir, detectedPlatform, options)); + const outputDir = path.dirname(outputPath); + fs.mkdirpSync(outputDir); + relativePath = path.relative(appDir, outputPath); + const written = await writeConfigToFile( + outputPath, + options.nonInteractive, + fileInfo.fileContents, + ); + + if (written) { + logger.info(`App configuration is written in ${relativePath}`); + } + logUse(detectedPlatform, relativePath); + + return sdkConfig; + }); diff --git a/src/commands/apps-list.ts b/src/commands/apps-list.ts index 69d9dcf9e39..c3f3fa4fd4a 100644 --- a/src/commands/apps-list.ts +++ b/src/commands/apps-list.ts @@ -1,9 +1,9 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as ora from "ora"; -import Table = require("cli-table"); +import * as Table from "cli-table3"; import { Command } from "../command"; -import * as getProjectId from "../getProjectId"; +import { needProjectId } from "../projectUtils"; import { AppMetadata, AppPlatform, getAppPlatform, listFirebaseApps } from "../management/apps"; import { requireAuth } from "../requireAuth"; import { logger } from "../logger"; @@ -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 = getProjectId(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: unknown) { + 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 18bdd961f0b..3d5ea2c2f4d 100644 --- a/src/commands/apps-sdkconfig.ts +++ b/src/commands/apps-sdkconfig.ts @@ -3,6 +3,7 @@ import * as fs from "fs-extra"; import { Command } from "../command"; import { + AppConfig, AppConfigurationData, AppMetadata, AppPlatform, @@ -11,23 +12,30 @@ import { getAppPlatform, listFirebaseApps, } from "../management/apps"; -import * as getProjectId from "../getProjectId"; +import { needProjectId } from "../projectUtils"; import { getOrPromptProject } from "../management/projects"; import { FirebaseError } from "../error"; import { requireAuth } from "../requireAuth"; import { logger } from "../logger"; -import { promptOnce } from "../prompt"; +import { Options } from "../options"; +import { select, confirm } from "../prompt"; -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", ); } +} +export interface AppsSdkConfigOptions extends Options { + out?: string | boolean; +} +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) => { @@ -39,9 +47,7 @@ async function selectAppInteractively( }; }); - return await promptOnce({ - type: "list", - name: "id", + return await select({ message: `Select the ${appPlatform === AppPlatform.ANY ? "" : appPlatform + " "}` + "app to get the configuration data:", @@ -49,20 +55,23 @@ 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 => { + async ( + platform = "", + appId = "", + options: AppsSdkConfigOptions, + ): Promise => { let appPlatform = getAppPlatform(platform); if (!appId) { - let projectId = getProjectId(options); + let projectId = needProjectId(options); if (options.nonInteractive && !projectId) { throw new FirebaseError("Must supply app and project ids in non-interactive mode."); } else if (!projectId) { @@ -71,63 +80,66 @@ module.exports = new Command("apps:sdkconfig [platform] [appId]") } 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.` + `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; } } - let configData; + let configData: AppConfig; const spinner = ora( - `Downloading configuration data of your Firebase ${appPlatform} app` + `Downloading configuration data of your Firebase ${appPlatform} app`, ).start(); try { configData = await getAppConfig(appId, appPlatform); - } catch (err) { + } catch (err: unknown) { spinner.fail(); throw err; } spinner.succeed(); const fileInfo = getAppConfigFile(configData, appPlatform); - if (appPlatform == AppPlatform.WEB) { + if (appPlatform === AppPlatform.WEB) { fileInfo.sdkConfig = configData; } + // Note: options.out can only be true (when -o with no args is passed in) + // or a string, or undefined. if (options.out === undefined) { logger.info(fileInfo.fileContents); return fileInfo; } const shouldUseDefaultFilename = options.out === true || options.out === ""; - const filename = shouldUseDefaultFilename ? configData.fileName : options.out; + const filename = shouldUseDefaultFilename ? fileInfo.fileName : (options.out as string); 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 overwrite = await confirm(`${filename} already exists. Do you want to overwrite?`); if (!overwrite) { - return configData; + return fileInfo; } } fs.writeFileSync(filename, fileInfo.fileContents); logger.info(`App configuration is written in ${filename}`); - return configData; - } + return fileInfo; + }, ); diff --git a/src/commands/apptesting-execute.ts b/src/commands/apptesting-execute.ts new file mode 100644 index 00000000000..4d763b286a8 --- /dev/null +++ b/src/commands/apptesting-execute.ts @@ -0,0 +1,145 @@ +import { requireAuth } from "../requireAuth"; +import { Command } from "../command"; +import { requireConfig } from "../requireConfig"; +import { logger } from "../logger"; +import * as clc from "colorette"; +import { parseTestFiles } from "../apptesting/parseTestFiles"; +import * as ora from "ora"; +import { invokeTests, pollInvocationStatus } from "../apptesting/invokeTests"; +import { ExecutionMetadata } from "../apptesting/types"; +import { FirebaseError } from "../error"; +import { marked } from "marked"; +import { needProjectId } from "../projectUtils"; +import { consoleUrl } from "../utils"; +import { AppPlatform, listFirebaseApps, checkForApps } from "../management/apps"; + +export const command = new Command("apptesting:execute ") + .description("Run automated tests written in natural language driven by AI") + .option( + "--app ", + "The app id of your Firebase web app. Optional if the project contains exactly one web app.", + ) + .option( + "--test-file-pattern ", + "Test file pattern. Only tests contained in files that match this pattern will be executed.", + ) + .option( + "--test-name-pattern ", + "Test name pattern. Only tests with names that match this pattern will be executed.", + ) + .option("--tests-non-blocking", "Request test execution without waiting for them to complete.") + .before(requireAuth) + .before(requireConfig) + .action(async (target: string, options: any) => { + const projectId = needProjectId(options); + const apps = await listFirebaseApps(projectId, AppPlatform.WEB); + // Fail out early if there's no apps. + checkForApps(apps, AppPlatform.WEB); + + let app = apps.find((a) => a.appId === options.app); + if (!app) { + if (options.app) { + // An app ID was provided, but it's invalid. + throw new FirebaseError( + `App with ID '${options.app}' was not found in project ${projectId}. You can list available apps with 'firebase apps:list'.`, + ); + } + // if there's only one app, we don't need to prompt interactively + if (apps.length === 1) { + // If there's only one, use it. + app = apps[0]; + } else { + // If there's > 1, fail + throw new FirebaseError( + `Project ${projectId} has multiple apps, must specify a web app id with '--app', you can list available apps with 'firebase apps:list'.`, + ); + } + } + + const testDir = options.config.src.apptesting?.testDir || "tests"; + const tests = await parseTestFiles( + testDir, + target, + options.testFilePattern, + options.testNamePattern, + ); + + if (!tests.length) { + throw new FirebaseError("No tests found"); + } + + const invokeSpinner = ora("Requesting test execution"); + invokeSpinner.start(); + + let invocationOperation; + try { + invocationOperation = await invokeTests(app.appId, target, tests); + invokeSpinner.text = "Test execution requested"; + invokeSpinner.succeed(); + } catch (ex) { + invokeSpinner.fail("Failed to request test execution"); + throw ex; + } + + logger.info(clc.bold(`\n${clc.white("===")} Running ${pluralizeTests(tests.length)}`)); + + const invocationId = invocationOperation.name?.split("/").pop(); + + // The console expects legacy namespace style IDs. + // This is temporary until console supports appId URLs. + const appWebId = (app as any).webId; + const url = consoleUrl( + projectId, + `/apptesting/app/web:${appWebId}/invocations/${invocationId}`, + ); + logger.info(await marked(`**Invocation ID:** ${invocationId}`)); + logger.info(await marked(`View progress and results in the [Firebase Console](${url})`)); + + if (options.testsNonBlocking) { + logger.info("Not waiting for results"); + return; + } + + if (!invocationOperation.metadata) { + throw new FirebaseError("Invocation details unavailable"); + } + + const executionSpinner = ora(getOutput(invocationOperation.metadata)); + executionSpinner.start(); + const invocationOp = await pollInvocationStatus(invocationOperation.name, (operation) => { + if (!operation.done) { + executionSpinner.text = getOutput(operation.metadata as ExecutionMetadata); + } + }); + const response = invocationOp.resource.testInvocation; + executionSpinner.text = `Testing complete\n${getOutput(response)}`; + if (response.failedExecutions || response.cancelledExecutions) { + executionSpinner.fail(); + throw new FirebaseError("Testing complete with errors"); + } else { + executionSpinner.succeed(); + } + }); + +function pluralizeTests(numTests: number) { + return `${numTests} test${numTests === 1 ? "" : "s"}`; +} + +function getOutput(invocation: ExecutionMetadata) { + const output = []; + if (invocation.runningExecutions) { + output.push( + `${pluralizeTests(invocation.runningExecutions)} running (this may take a while)...`, + ); + } + if (invocation.succeededExecutions) { + output.push(`✔ ${pluralizeTests(invocation.succeededExecutions)} passed`); + } + if (invocation.failedExecutions) { + output.push(`✖ ${pluralizeTests(invocation.failedExecutions)} failed`); + } + if (invocation.cancelledExecutions) { + output.push(`⊝ ${pluralizeTests(invocation.cancelledExecutions)} cancelled`); + } + return output.length ? output.join("\n") : "Tests are starting"; +} diff --git a/src/commands/auth-export.js b/src/commands/auth-export.js deleted file mode 100644 index bec5f640b80..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 getProjectId = require("../getProjectId"); -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 = getProjectId(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..bb45e32fc64 --- /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 bb5ef9dbede..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 getProjectId = require("../getProjectId"); -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 = getProjectId(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..cbefe8191e2 --- /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 new file mode 100644 index 00000000000..e2123a393d9 --- /dev/null +++ b/src/commands/crashlytics-symbols-upload.ts @@ -0,0 +1,114 @@ +import * as os from "os"; +import * as path from "path"; +import * as uuid from "uuid"; + +import { Command } from "../command"; +import { FirebaseError } from "../error"; +import * as utils from "../utils"; + +import { fetchBuildtoolsJar, runBuildtoolsCommand } from "../crashlytics/buildToolsJarHelper"; +import { Options } from "../options"; + +enum SymbolGenerator { + breakpad = "breakpad", + csym = "csym", +} + +interface CommandOptions extends Options { + app?: string; + generator?: SymbolGenerator; + dryRun?: boolean; +} + +interface JarOptions { + app: string; + generator: SymbolGenerator; + cachePath: string; + symbolFile: string; + generate: boolean; +} + +const SYMBOL_CACHE_ROOT_DIR = process.env.FIREBASE_CRASHLYTICS_CACHE_PATH || os.tmpdir(); + +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, default is breakpad") + .option("--dry-run", "generate symbols without uploading them") + .action(async (symbolFiles: string[], options: CommandOptions) => { + const app = getGoogleAppID(options); + const generator = getSymbolGenerator(options); + const dryRun = !!options.dryRun; + const debug = !!options.debug; + + const jarFile = await fetchBuildtoolsJar(); + + const jarOptions: JarOptions = { + app, + generator, + cachePath: path.join( + SYMBOL_CACHE_ROOT_DIR, + `crashlytics-${uuid.v4()}`, + "nativeSymbols", + // Windows does not allow ":" in their directory names + app.replace(/:/g, "-"), + generator, + ), + symbolFile: "", + generate: true, + }; + + for (const symbolFile of symbolFiles) { + utils.logBullet(`Generating symbols for ${symbolFile}`); + const generateArgs = buildArgs({ ...jarOptions, symbolFile }); + runBuildtoolsCommand(jarFile, generateArgs, debug); + utils.logBullet(`Generated symbols for ${symbolFile}`); + utils.logBullet(`Output Path: ${jarOptions.cachePath}`); + } + + if (dryRun) { + utils.logBullet("Skipping upload because --dry-run was passed"); + return; + } + + utils.logBullet(`Uploading all generated symbols...`); + const uploadArgs = buildArgs({ ...jarOptions, generate: false }); + runBuildtoolsCommand(jarFile, uploadArgs, debug); + utils.logBullet("Successfully uploaded all symbols"); + }); + +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 getSymbolGenerator(options: CommandOptions): SymbolGenerator { + // Default to using BreakPad symbols + if (!options.generator) { + return SymbolGenerator.breakpad; + } + if (!Object.values(SymbolGenerator).includes(options.generator)) { + throw new FirebaseError('--symbol-generator should be set to either "breakpad" or "csym"'); + } + return options.generator; +} + +function buildArgs(options: JarOptions): string[] { + const baseArgs = [ + "-symbolGenerator", + options.generator, + "-symbolFileCacheDir", + options.cachePath, + "-verbose", + ]; + + if (options.generate) { + return baseArgs.concat(["-generateNativeSymbols", "-unstrippedLibrary", options.symbolFile]); + } + + 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..3e75cc67466 --- /dev/null +++ b/src/commands/database-import.ts @@ -0,0 +1,116 @@ +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, getErrMsg } from "../error"; +import { logger } from "../logger"; +import { needProjectId } from "../projectUtils"; +import { Options } from "../options"; +import { printNoticeIfEmulated } from "../emulator/commandUtils"; +import { confirm } 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 areYouSure = await confirm({ + message: "You are about to import data to " + clc.cyan(dbPath) + ". Are you sure?", + force: options.force, + }); + if (!areYouSure) { + 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: unknown) { + if (err instanceof FirebaseError) { + throw err; + } + logger.debug(getErrMsg(err)); + throw new FirebaseError(`Unexpected error while importing data: ${getErrMsg(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 bfb99922aed..19bd99a8c41 100644 --- a/src/commands/database-instances-create.ts +++ b/src/commands/database-instances-create.ts @@ -9,23 +9,23 @@ import { DatabaseLocation, parseDatabaseLocation, } from "../management/database"; -import getProjectId = require("../getProjectId"); +import { needProjectId } from "../projectUtils"; import { getDefaultDatabaseInstance } from "../getDefaultDatabaseInstance"; import { FirebaseError } from "../error"; import { MISSING_DEFAULT_INSTANCE_ERROR_MESSAGE } from "../requireDatabaseInstance"; -export default new Command("database:instances:create ") - .description("create a realtime database instance") +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) // eslint-disable-next-line @typescript-eslint/no-explicit-any .action(async (instanceName: string, options: any) => { - const projectId = getProjectId(options); - const defaultDatabaseInstance = await getDefaultDatabaseInstance({ project: projectId }); + const projectId = needProjectId(options); + const defaultDatabaseInstance = await getDefaultDatabaseInstance(projectId); if (defaultDatabaseInstance === "") { throw new FirebaseError(MISSING_DEFAULT_INSTANCE_ERROR_MESSAGE); } @@ -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 9354a12359b..627b0dc82a7 100644 --- a/src/commands/database-instances-list.ts +++ b/src/commands/database-instances-list.ts @@ -1,16 +1,14 @@ +import * as Table from "cli-table3"; 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 { getProjectNumber } from "../getProjectNumber"; -import firedata = require("../gcp/firedata"); import { Emulators } from "../emulator/types"; import { warnEmulatorNotSupported } from "../emulator/commandUtils"; -import { previews } from "../previews"; -import getProjectId = require("../getProjectId"); +import * as experiments from "../experiments"; +import { needProjectId } from "../projectUtils"; import { listDatabaseInstances, DatabaseInstance, @@ -18,72 +16,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 = getProjectId(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 getProjectNumber(options); + const projectId = needProjectId(options); + let instances: DatabaseInstance[] = []; try { - instances = await firedata.listDatabaseInstances(projectNumber); - } catch (err) { + instances = await listDatabaseInstances(projectId, location); + } catch (err: unknown) { 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..f6be4bd209e 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"; @@ -9,25 +7,23 @@ import { profiler } from "../profiler"; import { Emulators } from "../emulator/types"; import { warnEmulatorNotSupported } from "../emulator/commandUtils"; -const description = "profile the Realtime Database and generate a usage report"; - -module.exports = new Command("database:profile") - .description(description) +export const command = new Command("database:profile") + .description("profile the Realtime Database and generate a usage report") .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 +37,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..7a9ea92c606 100644 --- a/src/commands/database-push.ts +++ b/src/commands/database-push.ts @@ -1,11 +1,10 @@ -import * as _ from "lodash"; -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as fs from "fs"; import { Client } from "../apiv2"; import { Command } from "../command"; import { Emulators } from "../emulator/types"; -import { FirebaseError } from "../error"; +import { FirebaseError, getErrMsg } from "../error"; import { populateInstanceDetails } from "../management/database"; import { printNoticeIfEmulated } from "../emulator/commandUtils"; import { realtimeOriginOrEmulatorOrCustomUrl } from "../database/api"; @@ -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,16 @@ export default new Command("database:push [infile]") method: "POST", path: u.pathname, body: inStream, + queryParams: u.searchParams, + }); + } catch (err: unknown) { + logger.debug(getErrMsg(err)); + throw new FirebaseError(`Unexpected error while pushing data: ${getErrMsg(err)}`, { + exit: 2, }); - } catch (err) { - logger.debug(err); - throw new FirebaseError(`Unexpected error while pushing data: ${err}`, { exit: 2 }); } - if (!_.endsWith(path, "/")) { + if (!path.endsWith("/")) { path += "/"; } @@ -61,7 +67,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 47392f007b9..3b70039d422 100644 --- a/src/commands/database-remove.ts +++ b/src/commands/database-remove.ts @@ -7,42 +7,36 @@ import { warnEmulatorNotSupported } from "../emulator/commandUtils"; import { populateInstanceDetails } from "../management/database"; import { realtimeOriginOrEmulatorOrCustomUrl } from "../database/api"; import * as utils from "../utils"; -import { prompt } from "../prompt"; -import * as clc from "cli-color"; -import * as _ from "lodash"; +import { confirm } from "../prompt"; +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("-y, --confirm", "pass this option to bypass confirmation prompt") + .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((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); const databaseUrl = utils.getDatabaseUrl(origin, options.instance, path); - return prompt(options, [ - { - type: "confirm", - name: "confirm", - default: false, - message: "You are about to remove all data at " + clc.cyan(databaseUrl) + ". Are you sure?", - }, - ]).then(() => { - if (!options.confirm) { - return utils.reject("Command aborted.", { exit: 1 }); - } - - const removeOps = new DatabaseRemove(options.instance, path, origin); - return removeOps.execute().then(() => { - utils.logSuccess("Data removed successfully"); - }); + const areYouSure = await confirm({ + message: "You are about to remove all data at " + clc.cyan(databaseUrl) + ". Are you sure?", + force: options.force, }); + if (!areYouSure) { + return utils.reject("Command aborted.", { exit: 1 }); + } + + 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 2172bccb15b..e5fbe1d39c6 100644 --- a/src/commands/database-set.ts +++ b/src/commands/database-set.ts @@ -1,14 +1,13 @@ -import * as _ from "lodash"; -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as fs from "fs"; import { Client } from "../apiv2"; import { Command } from "../command"; import { Emulators } from "../emulator/types"; -import { FirebaseError } from "../error"; +import { FirebaseError, getErrMsg } from "../error"; import { populateInstanceDetails } from "../management/database"; import { printNoticeIfEmulated } from "../emulator/commandUtils"; -import { promptOnce } from "../prompt"; +import { confirm } from "../prompt"; import { realtimeOriginOrEmulatorOrCustomUrl } from "../database/api"; import { requirePermissions } from "../requirePermissions"; import { URL } from "url"; @@ -16,36 +15,38 @@ 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("-y, --confirm", "pass this option to bypass confirmation prompt") + .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"); + } - if (!options.confirm) { - const confirm = await promptOnce({ - type: "confirm", - name: "confirm", - default: false, - message: "You are about to overwrite all data at " + clc.cyan(dbPath) + ". Are you sure?", - }); - if (!confirm) { - throw new FirebaseError("Command aborted."); - } + const confirmed = await confirm({ + message: "You are about to overwrite all data at " + clc.cyan(dbPath) + ". Are you sure?", + default: false, + force: options.force, + nonInteractive: options.nonInteractive, + }); + if (!confirmed) { + throw new FirebaseError("Command aborted."); } const inStream = @@ -61,16 +62,19 @@ export default new Command("database:set [infile]") method: "PUT", path: dbJsonURL.pathname, body: inStream, + queryParams: dbJsonURL.searchParams, + }); + } catch (err: unknown) { + logger.debug(getErrMsg(err)); + throw new FirebaseError(`Unexpected error while setting data: ${getErrMsg(err)}`, { + exit: 2, }); - } catch (err) { - logger.debug(err); - throw new FirebaseError(`Unexpected error while setting data: ${err}`, { exit: 2 }); } utils.logSuccess("Data persisted successfully"); 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..f6109d3c503 100644 --- a/src/commands/database-settings-get.ts +++ b/src/commands/database-settings-get.ts @@ -4,7 +4,7 @@ import { Client } from "../apiv2"; import { Command } from "../command"; import { DATABASE_SETTINGS, HELP_TEXT, INVALID_PATH_ERROR } from "../database/settings"; import { Emulators } from "../emulator/types"; -import { FirebaseError } from "../error"; +import { FirebaseError, getError } from "../error"; import { populateInstanceDetails } from "../management/database"; import { realtimeOriginOrCustomUrl } from "../database/api"; import { requirePermissions } from "../requirePermissions"; @@ -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,17 +33,17 @@ 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: unknown) { throw new FirebaseError(`Unexpected error fetching configs at ${path}`, { exit: 2, - original: err, + original: getError(err), }); } // strictTriggerValidation returns an object, not a single string. @@ -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..1c7092a8947 100644 --- a/src/commands/database-settings-set.ts +++ b/src/commands/database-settings-set.ts @@ -4,7 +4,7 @@ import { Client } from "../apiv2"; import { Command } from "../command"; import { DATABASE_SETTINGS, HELP_TEXT, INVALID_PATH_ERROR } from "../database/settings"; import { Emulators } from "../emulator/types"; -import { FirebaseError } from "../error"; +import { FirebaseError, getError } from "../error"; import { populateInstanceDetails } from "../management/database"; import { realtimeOriginOrCustomUrl } from "../database/api"; import { requirePermissions } from "../requirePermissions"; @@ -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 ") - .description("set the realtime database setting at path.") +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,20 +38,20 @@ 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: unknown) { throw new FirebaseError(`Unexpected error fetching configs at ${path}`, { exit: 2, - original: err, + original: getError(err), }); } 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 4f79804f037..e1956057732 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"; @@ -8,21 +8,22 @@ import { Emulators } from "../emulator/types"; import { FirebaseError } from "../error"; import { populateInstanceDetails } from "../management/database"; import { printNoticeIfEmulated } from "../emulator/commandUtils"; -import { promptOnce } from "../prompt"; +import { confirm } from "../prompt"; import { realtimeOriginOrEmulatorOrCustomUrl } from "../database/api"; import { requirePermissions } from "../requirePermissions"; import { logger } from "../logger"; import { requireDatabaseInstance } from "../requireDatabaseInstance"; import * as utils from "../utils"; -export default new Command("database:update [infile]") - .description("update some of the keys for the defined path in your Firebase") +export const command = new Command("database:update [infile]") + .description("update some of the keys for the defined path in your Realtime Database") .option("-d, --data ", "specify escaped JSON directly") - .option("-y, --confirm", "pass this option to bypass confirmation prompt") + .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) @@ -33,16 +34,14 @@ export default new Command("database:update [infile]") } const origin = realtimeOriginOrEmulatorOrCustomUrl(options.instanceDetails.databaseUrl); const url = utils.getDatabaseUrl(origin, options.instance, path); - if (!options.confirm) { - const confirmed = await promptOnce({ - type: "confirm", - name: "confirm", - default: false, - message: `You are about to modify data at ${clc.cyan(url)}. Are you sure?`, - }); - if (!confirmed) { - throw new FirebaseError("Command aborted."); - } + const confirmed = await confirm({ + message: `You are about to modify data at ${clc.cyan(url)}. Are you sure?`, + default: false, + force: options.force, + nonInteractive: options.nonInteractive, + }); + if (!confirmed) { + throw new FirebaseError("Command aborted."); } const inStream = @@ -50,6 +49,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(); @@ -61,8 +63,9 @@ export default new Command("database:update [infile]") method: "PATCH", path: jsonUrl.pathname, body: inStream, + queryParams: jsonUrl.searchParams, }); - } catch (err) { + } catch (err: unknown) { throw new FirebaseError("Unexpected error while setting data"); } @@ -70,6 +73,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..741b74370b4 --- /dev/null +++ b/src/commands/dataconnect-sdk-generate.ts @@ -0,0 +1,62 @@ +import * as clc from "colorette"; + +import { Command } from "../command"; +import { Options } from "../options"; +import { DataConnectEmulator } from "../emulator/dataconnectEmulator"; +import { needProjectId } from "../projectUtils"; +import { loadAll } from "../dataconnect/load"; +import { logger } from "../logger"; +import { getProjectDefaultAccount } from "../auth"; +import { logLabeledSuccess } from "../utils"; +import { ServiceInfo } from "../dataconnect/types"; + +type GenerateOptions = Options & { watch?: boolean }; + +export const command = new Command("dataconnect:sdk:generate") + .description("generate typed SDKs for your Data Connect connectors") + .option( + "--watch", + "watch for changes to your connector GQL files and regenerate your SDKs when updates occur", + ) + .action(async (options: GenerateOptions) => { + const projectId = needProjectId(options); + + const serviceInfos = await loadAll(projectId, options.config); + const serviceInfosWithSDKs = serviceInfos.filter((serviceInfo) => + serviceInfo.connectorInfo.some((c) => { + return ( + c.connectorYaml.generate?.javascriptSdk || + c.connectorYaml.generate?.kotlinSdk || + c.connectorYaml.generate?.swiftSdk || + c.connectorYaml.generate?.dartSdk + ); + }), + ); + if (!serviceInfosWithSDKs.length) { + 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/web-sdk for more details of how to configure generated SDKs.`, + ); + return; + } + async function generateSDK(serviceInfo: ServiceInfo): Promise { + return DataConnectEmulator.generate({ + configDir: serviceInfo.sourceDirectory, + watch: options.watch, + account: getProjectDefaultAccount(options.projectRoot), + }); + } + if (options.watch) { + await Promise.race(serviceInfosWithSDKs.map(generateSDK)); + } else { + for (const s of serviceInfosWithSDKs) { + await generateSDK(s); + } + const services = serviceInfosWithSDKs.map((s) => s.dataConnectYaml.serviceId).join(", "); + logLabeledSuccess( + "dataconnect", + `Successfully Generated SDKs for services: ${clc.bold(services)}`, + ); + } + }); diff --git a/src/commands/dataconnect-services-list.ts b/src/commands/dataconnect-services-list.ts new file mode 100644 index 00000000000..8a55ad9554b --- /dev/null +++ b/src/commands/dataconnect-services-list.ts @@ -0,0 +1,74 @@ +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"; +import * as Table from "cli-table3"; + +export const command = new Command("dataconnect:services:list") + .description("list all deployed Data Connect services") + .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: "", + datasources: [{}], + source: { files: [] }, + }; + const connectors = await client.listConnectors(service.name); + const serviceName = names.parseServiceName(service.name); + const postgresDatasource = schema?.datasources.find((d) => d.postgresql); + const instanceName = postgresDatasource?.postgresql?.cloudSql?.instance ?? ""; + const instanceId = instanceName.split("/").pop(); + const dbId = postgresDatasource?.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..73035ad52e8 --- /dev/null +++ b/src/commands/dataconnect-sql-diff.ts @@ -0,0 +1,31 @@ +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/load"; +import { diffSchema } from "../dataconnect/schemaMigration"; +import { requireAuth } from "../requireAuth"; + +export const command = new Command("dataconnect:sql:diff [serviceId]") + .description( + "display the differences between a local Data Connect 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( + options, + serviceInfo.schema, + serviceInfo.dataConnectYaml.schema.datasource.postgresql?.schemaValidation, + ); + return { projectId, serviceId, diffs }; + }); diff --git a/src/commands/dataconnect-sql-grant.ts b/src/commands/dataconnect-sql-grant.ts new file mode 100644 index 00000000000..30a25da9e9e --- /dev/null +++ b/src/commands/dataconnect-sql-grant.ts @@ -0,0 +1,56 @@ +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/load"; +import { grantRoleToUserInSchema } from "../dataconnect/schemaMigration"; +import { requireAuth } from "../requireAuth"; +import { FirebaseError } from "../error"; +import { fdcSqlRoleMap } from "../gcp/cloudsql/permissionsSetup"; +import { iamUserIsCSQLAdmin } from "../gcp/cloudsql/cloudsqladmin"; + +const allowedRoles = Object.keys(fdcSqlRoleMap); + +export const command = new Command("dataconnect:sql:grant [serviceId]") + .description("grants the SQL role to the provided user or service account ") + .option("-R, --role ", "The SQL role to grant. One of: owner, writer, or reader.") + .option( + "-E, --email ", + "The email of the user or service account we would like to grant the role to.", + ) + .before(requirePermissions, ["firebasedataconnect.services.list"]) + .before(requireAuth) + .action(async (serviceId: string, options: Options) => { + const role = options.role as string; + const email = options.email as string; + if (!role) { + throw new FirebaseError( + "-R, --role is required. Run the command with -h for more info.", + ); + } + if (!email) { + throw new FirebaseError( + "-E, --email is required. Run the command with -h for more info.", + ); + } + + if (!allowedRoles.includes(role.toLowerCase())) { + throw new FirebaseError(`Role should be one of ${allowedRoles.join(" | ")}.`); + } + + // Make sure current user can perform this action. + const userIsCSQLAdmin = await iamUserIsCSQLAdmin(options); + if (!userIsCSQLAdmin) { + throw new FirebaseError( + `Only users with 'roles/cloudsql.admin' can grant SQL roles. If you do not have this role, ask your database administrator to run this command or manually grant ${role} to ${email}`, + ); + } + + const projectId = needProjectId(options); + await ensureApis(projectId); + const serviceInfo = await pickService(projectId, options.config, serviceId); + + await grantRoleToUserInSchema(options, serviceInfo.schema); + return { projectId, serviceId }; + }); diff --git a/src/commands/dataconnect-sql-migrate.ts b/src/commands/dataconnect-sql-migrate.ts new file mode 100644 index 00000000000..bcc72943327 --- /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/load"; +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("migrate your CloudSQL database's schema to match your local Data Connect schema") + .before(requirePermissions, [ + "firebasedataconnect.services.list", + "firebasedataconnect.schemas.list", + "firebasedataconnect.schemas.update", + "cloudsql.instances.connect", + ]) + .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, + schemaValidation: serviceInfo.dataConnectYaml.schema.datasource.postgresql?.schemaValidation, + }); + 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/dataconnect-sql-setup.ts b/src/commands/dataconnect-sql-setup.ts new file mode 100644 index 00000000000..164a1d55d53 --- /dev/null +++ b/src/commands/dataconnect-sql-setup.ts @@ -0,0 +1,48 @@ +import { Command } from "../command"; +import { Options } from "../options"; +import { needProjectId } from "../projectUtils"; +import { pickService } from "../dataconnect/load"; +import { FirebaseError } from "../error"; +import { requireAuth } from "../requireAuth"; +import { requirePermissions } from "../requirePermissions"; +import { ensureApis } from "../dataconnect/ensureApis"; +import { setupSQLPermissions, getSchemaMetadata } from "../gcp/cloudsql/permissionsSetup"; +import { DEFAULT_SCHEMA } from "../gcp/cloudsql/permissions"; +import { getIdentifiers, ensureServiceIsConnectedToCloudSql } from "../dataconnect/schemaMigration"; +import { setupIAMUsers } from "../gcp/cloudsql/connect"; + +export const command = new Command("dataconnect:sql:setup [serviceId]") + .description("set up your CloudSQL database") + .before(requirePermissions, [ + "firebasedataconnect.services.list", + "firebasedataconnect.schemas.list", + "firebasedataconnect.schemas.update", + "cloudsql.instances.connect", + ]) + .before(requireAuth) + .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 { serviceName, instanceName, databaseId } = getIdentifiers(serviceInfo.schema); + await ensureServiceIsConnectedToCloudSql( + serviceName, + instanceName, + databaseId, + /* linkIfNotConnected=*/ true, + ); + + // Setup the IAM user for the current identity. + await setupIAMUsers(instanceId, options); + + const schemaInfo = await getSchemaMetadata(instanceId, databaseId, DEFAULT_SCHEMA, options); + await setupSQLPermissions(instanceId, databaseId, schemaInfo, options); + }); diff --git a/src/commands/dataconnect-sql-shell.ts b/src/commands/dataconnect-sql-shell.ts new file mode 100644 index 00000000000..6023fb44905 --- /dev/null +++ b/src/commands/dataconnect-sql-shell.ts @@ -0,0 +1,138 @@ +import * as pg from "pg"; +import * as clc from "colorette"; +import { Connector, IpAddressTypes, AuthTypes } from "@google-cloud/cloud-sql-connector"; + +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/load"; +import { getIdentifiers } from "../dataconnect/schemaMigration"; +import { requireAuth } from "../requireAuth"; +import { getIAMUser } from "../gcp/cloudsql/connect"; +import * as cloudSqlAdminClient from "../gcp/cloudsql/cloudsqladmin"; +import { input } from "../prompt"; +import { logger } from "../logger"; +import { FirebaseError } from "../error"; +import { FBToolsAuthClient } from "../gcp/cloudsql/fbToolsAuthClient"; +import { confirmDangerousQuery, interactiveExecuteQuery } from "../gcp/cloudsql/interactive"; + +// Not a comprehensive list, used for keyword coloring. +const sqlKeywords = [ + "SELECT", + "FROM", + "WHERE", + "INSERT", + "UPDATE", + "DELETE", + "JOIN", + "GROUP", + "ORDER", + "LIMIT", + "GRANT", + "CREATE", + "DROP", +]; + +async function promptForQuery(): Promise { + let query = ""; + const line = ""; + + do { + let line = await input({ + message: query ? "> " : "Enter your SQL query (or '.exit'):", + transformer: (input: string) => { + // Highlight SQL keywords + return input + .split(" ") + .map((word) => (sqlKeywords.includes(word.toUpperCase()) ? clc.cyan(word) : word)) + .join(" "); + }, + nonInteractive: false, + }); + line = line.trimEnd(); + + if (line.toLowerCase() === ".exit") { + return ".exit"; + } + + query += (query ? "\n" : "") + line; + } while (line !== "" && !query.endsWith(";")); + return query; +} + +async function mainShellLoop(conn: pg.PoolClient) { + while (true) { + const query = await promptForQuery(); + if (query.toLowerCase() === ".exit") { + break; + } + + if (query === "") { + continue; + } + + if (await confirmDangerousQuery(query)) { + await interactiveExecuteQuery(query, conn); + } else { + logger.info(clc.yellow("Query cancelled.")); + } + } +} + +export const command = new Command("dataconnect:sql:shell [serviceId]") + .description( + "start a shell connected directly to your Data Connect service's linked CloudSQL instance", + ) + .before(requirePermissions, ["firebasedataconnect.services.list", "cloudsql.instances.connect"]) + .before(requireAuth) + .action(async (serviceId: string, options: Options) => { + const projectId = needProjectId(options); + await ensureApis(projectId); + const serviceInfo = await pickService(projectId, options.config, serviceId); + const { instanceId, databaseId } = getIdentifiers(serviceInfo.schema); + const { user: username } = await getIAMUser(options); + const instance = await cloudSqlAdminClient.getInstance(projectId, instanceId); + + // Setup the connection + const connectionName = instance.connectionName; + if (!connectionName) { + throw new FirebaseError( + `Could not get instance connection string for ${options.instanceId}:${options.databaseId}`, + ); + } + const connector: Connector = new Connector({ + auth: new FBToolsAuthClient(), + }); + const clientOpts = await connector.getOptions({ + instanceConnectionName: connectionName, + ipType: IpAddressTypes.PUBLIC, + authType: AuthTypes.IAM, + }); + const pool: pg.Pool = new pg.Pool({ + ...clientOpts, + user: username, + database: databaseId, + }); + const conn: pg.PoolClient = await pool.connect(); + + logger.info(`Logged in as ${username}`); + logger.info(clc.cyan("Welcome to Data Connect Cloud SQL Shell")); + logger.info( + clc.gray( + "Type your your SQL query or '.exit' to quit, queries should end with ';' or add empty line to execute.", + ), + ); + + // Start accepting queries + await mainShellLoop(conn); + + // Cleanup after exit + logger.info(clc.yellow("Exiting shell...")); + conn.release(); + await pool.end(); + connector.close(); + + return { projectId, serviceId }; + }); diff --git a/src/commands/deploy.js b/src/commands/deploy.js deleted file mode 100644 index 6a96c62d12e..00000000000 --- a/src/commands/deploy.js +++ /dev/null @@ -1,85 +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 checkFunctionsSDKVersion = require("../checkFirebaseSDKVersion").checkFunctionsSDKVersion; -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"]; -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") - .option("-p, --public ", "override the Hosting public directory specified in firebase.json") - .option("-m, --message ", "an optional message describing this deploy") - .option( - "-f, --force", - "delete Cloud Functions missing from the current working directory without confirmation" - ) - .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) - .before(checkFunctionsSDKVersion) - .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..5b3e036138b --- /dev/null +++ b/src/commands/deploy.ts @@ -0,0 +1,151 @@ +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", + "apphosting", +]; +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"). ' + + "When filtering based on codebases, use colons to specify codebase names " + + '(e.g. "--only functions:codebase1:func1,functions:codebase2:group1.subgroup1"). ' + + "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")') + .option( + "--dry-run", + "perform a dry run of your deployment. Validates your changes and builds your code without deploying any changes to your project. " + + "In order to provide better validation, this may still enable APIs on the target project", + ) + .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) { + const isPermissionError = + err instanceof FirebaseError && + err.original instanceof FirebaseError && + err.original.status === 403; + if (isPermissionError) { + throw err; + } else 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..dadd455bd3a 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/emulator/auth/idp.spec.ts b/src/emulator/auth/idp.spec.ts new file mode 100644 index 00000000000..b02b270160a --- /dev/null +++ b/src/emulator/auth/idp.spec.ts @@ -0,0 +1,1455 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; +import { FirebaseJwtPayload } from "./operations"; +import { PROVIDER_PASSWORD, SIGNIN_METHOD_EMAIL_LINK } from "./state"; +import { describeAuthEmulator, PROJECT_ID } from "./testing/setup"; +import { + expectStatusCode, + getAccountInfoByIdToken, + registerUser, + signInWithFakeClaims, + registerAnonUser, + signInWithPhoneNumber, + updateAccountByLocalId, + getSigninMethods, + signInWithEmailLink, + updateProjectConfig, + fakeClaims, + TEST_PHONE_NUMBER, + FAKE_GOOGLE_ACCOUNT, + REAL_GOOGLE_ACCOUNT, + enrollPhoneMfa, + getAccountInfoByLocalId, + registerTenant, + 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. + +describeAuthEmulator("sign-in with credential", ({ authApi, getClock }) => { + it("should create new account with IDP from unsigned ID token", async () => { + 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); + 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}`, + ); + expect(res.body.oauthIdToken).to.equal(FAKE_GOOGLE_ACCOUNT.idToken); + expect(res.body.providerId).to.equal("google.com"); + expect(res.body).to.have.property("refreshToken").that.is.a("string"); + + // The ID Token used above does NOT contain name or photo, so the + // account created won't have those attributes either. + expect(res.body).not.to.have.property("displayName"); + expect(res.body).not.to.have.property("photoUrl"); + + const raw = JSON.parse(res.body.rawUserInfo); + expect(raw.id).to.equal(FAKE_GOOGLE_ACCOUNT.rawId); + expect(raw.email).to.equal(FAKE_GOOGLE_ACCOUNT.email); + expect(raw.verified_email).to.equal(true); + expect(raw.locale).to.equal("en"); + // name, given_name, family_name, and picture are not populated since + // they are not in the ID Token used above. + expect(raw.granted_scopes.split(" ")).to.have.members([ + "openid", + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/userinfo.email", + ]); + + 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).not.to.have.property("provider_id"); + expect(decoded!.payload.firebase) + .to.have.property("identities") + .eql({ + "google.com": [FAKE_GOOGLE_ACCOUNT.rawId], + email: [FAKE_GOOGLE_ACCOUNT.email], + }); + expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("google.com"); + }); + }); + + it("should create new account with IDP from production ID token", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `providerId=google.com&id_token=${REAL_GOOGLE_ACCOUNT.idToken}`, + requestUri: "http://localhost", + returnIdpCredential: true, + returnSecureToken: true, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.isNewUser).to.equal(true); + 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}`, + ); + expect(res.body.oauthIdToken).to.equal(REAL_GOOGLE_ACCOUNT.idToken); + expect(res.body.providerId).to.equal("google.com"); + expect(res.body).to.have.property("refreshToken").that.is.a("string"); + + // The ID Token used above does NOT contain name or photo, so the + // account created won't have those attributes either. + expect(res.body).not.to.have.property("displayName"); + expect(res.body).not.to.have.property("photoUrl"); + + const raw = JSON.parse(res.body.rawUserInfo); + expect(raw.id).to.equal(REAL_GOOGLE_ACCOUNT.rawId); + expect(raw.email).to.equal(REAL_GOOGLE_ACCOUNT.email); + expect(raw.verified_email).to.equal(true); + expect(raw.locale).to.equal("en"); + // name, given_name, family_name, and picture are not populated since + // they are not in the ID Token used above. + expect(raw.granted_scopes.split(" ")).to.have.members([ + "openid", + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/userinfo.email", + ]); + + 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).not.to.have.property("provider_id"); + expect(decoded!.payload.firebase) + .to.have.property("identities") + .eql({ + "google.com": [REAL_GOOGLE_ACCOUNT.rawId], + email: [REAL_GOOGLE_ACCOUNT.email], + }); + expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("google.com"); + }); + }); + + it("should create new account with IDP from unencoded JSON claims", async () => { + const claims = fakeClaims({ + sub: "123456789012345678901", + name: "Ada Lovelace", + given_name: "Ada", + family_name: "Lovelace", + picture: "http://localhost/fake-picture-url.png", + }); + const fakeIdToken = JSON.stringify(claims); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `providerId=google.com&id_token=${encodeURIComponent(fakeIdToken)}`, + requestUri: "http://localhost", + returnIdpCredential: true, + returnSecureToken: true, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.isNewUser).to.equal(true); + expect(res.body.federatedId).to.equal(`https://accounts.google.com/${claims.sub}`); + expect(res.body.oauthIdToken).to.equal(fakeIdToken); + expect(res.body.providerId).to.equal("google.com"); + expect(res.body.displayName).to.equal(claims.name); + expect(res.body.fullName).to.equal(claims.name); + expect(res.body.firstName).to.equal(claims.given_name); + expect(res.body.lastName).to.equal(claims.family_name); + expect(res.body.photoUrl).to.equal(claims.picture); + expect(res.body).to.have.property("refreshToken").that.is.a("string"); + + const raw = JSON.parse(res.body.rawUserInfo); + expect(raw.id).to.equal(claims.sub); + expect(raw.name).to.equal(claims.name); + expect(raw.given_name).to.equal(claims.given_name); + 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", + ); + + 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).not.to.have.property("provider_id"); + expect(decoded!.payload.firebase) + .to.have.property("identities") + .eql({ + "google.com": [claims.sub], + }); + expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("google.com"); + }); + }); + + it("should accept params (e.g. providerId, id_token) in requestUri", async () => { + const claims = fakeClaims({ + sub: "123456789012345678901", + }); + const fakeIdToken = JSON.stringify(claims); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + // No postBody, all params in requestUri below. + requestUri: `http://localhost?providerId=google.com&id_token=${encodeURIComponent( + fakeIdToken, + )}`, + returnIdpCredential: true, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.providerId).to.equal("google.com"); + }); + }); + + it("should copy attributes to user on IDP sign-up", async () => { + const claims = fakeClaims({ + sub: "123456789012345678901", + screen_name: "turingcomplete", + name: "Alan Turing", + picture: "http://localhost/turing.png", + }); + const { idToken } = await signInWithFakeClaims(authApi(), "google.com", claims); + + const user = await getAccountInfoByIdToken(authApi(), idToken); + expect(user.photoUrl).equal(claims.picture); + expect(user.displayName).equal(claims.name); + expect(user.screenName).equal(claims.screen_name); + }); + + it("should allow duplicate emails if set in project config", async () => { + await updateProjectConfig(authApi(), { signIn: { allowDuplicateEmails: true } }); + + const email = "alice@example.com"; + + // Given there exists an account with email already: + const user1 = await registerUser(authApi(), { email, password: "notasecret" }); + + // When trying to sign-in with IDP that claims the same email: + const user2 = await signInWithFakeClaims(authApi(), "google.com", { + sub: "123456789012345678901", + email, + }); + + // It should create a new account with different local ID: + expect(user2.localId).not.to.equal(user1.localId); + }); + + it("should sign-up new users without copying email when allowing duplicate emails", async () => { + await updateProjectConfig(authApi(), { signIn: { allowDuplicateEmails: true } }); + + const email = "alice@example.com"; + + const user1 = await signInWithFakeClaims(authApi(), "google.com", { + sub: "123456789012345678901", + email, + }); + + const info = await getAccountInfoByIdToken(authApi(), user1.idToken); + expect(info.email).to.be.undefined; + }); + + it("should allow multiple providers with same email when allowing duplicate emails", async () => { + await updateProjectConfig(authApi(), { signIn: { allowDuplicateEmails: true } }); + + const email = "alice@example.com"; + + const user1 = await signInWithFakeClaims(authApi(), "google.com", { + sub: "123456789012345678901", + email, + }); + const user2 = await signInWithFakeClaims(authApi(), "facebook.com", { + sub: "123456789012345678901", + email, + }); + + expect(user2.localId).not.to.equal(user1.localId); + }); + + it("should sign in existing account if (providerId, sub) is the same", async () => { + const user1 = await signInWithFakeClaims(authApi(), "google.com", { + sub: "123456789012345678901", + }); + + // Same sub, same user. + const user2 = await signInWithFakeClaims(authApi(), "google.com", { + sub: "123456789012345678901", + }); + expect(user2.localId).to.equal(user1.localId); + + // Different sub, different user. + const user3 = await signInWithFakeClaims(authApi(), "google.com", { + sub: "000000000000000000000", + }); + expect(user3.localId).not.to.equal(user1.localId); + + // Different providerId, different user. + const user4 = await signInWithFakeClaims(authApi(), "apple.com", { + sub: "123456789012345678901", + }); + expect(user4.localId).not.to.equal(user1.localId); + }); + + it("should error if user is disabled", async () => { + const user = await signInWithFakeClaims(authApi(), "google.com", { + sub: "123456789012345678901", + }); + await updateAccountByLocalId(authApi(), user.localId, { disableUser: true }); + + const claims = fakeClaims({ + sub: "123456789012345678901", + name: "Foo", + }); + const fakeIdToken = JSON.stringify(claims); + 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(400, res); + expect(res.body.error.message).to.equal("USER_DISABLED"); + }); + }); + + it("should add IDP as a sign-in method for email if available", async () => { + const email = "foo@example.com"; + const sub = "123456789012345678901"; + await signInWithFakeClaims(authApi(), "google.com", { + sub, + email, + }); + expect(await getSigninMethods(authApi(), email)).to.eql(["google.com"]); + + const newEmail = "bar@example.com"; + const { idToken } = await signInWithFakeClaims(authApi(), "google.com", { + sub, + email: newEmail, + }); + + expect(await getSigninMethods(authApi(), newEmail)).to.eql(["google.com"]); + + // The account-level email is still the old email. + const info = await getAccountInfoByIdToken(authApi(), idToken); + expect(info.email).to.equal(email); + }); + + it("should unlink password and overwite profile attributes if user had unverified email", async () => { + // Given a user with unverified email, linked with password: + const { localId, email } = await registerUser(authApi(), { + email: "foo@example.com", + password: "notasecret", + displayName: "Foo", + }); + + // When signing in with IDP and IDP verifies email: + const providerId = "google.com"; + const photoUrl = "http://localhost/photo-from-idp.png"; + const idpSignIn = await signInWithFakeClaims(authApi(), providerId, { + sub: "123456789012345678901", + email, + email_verified: true, + picture: photoUrl, + }); + + // It should sign-in into the same account, but the account's password + // should be unlinked. + expect(idpSignIn.localId).to.equal(localId); + const signInMethods = await getSigninMethods(authApi(), email); + expect(signInMethods).to.eql([providerId]); + expect(signInMethods).not.to.contain([PROVIDER_PASSWORD]); + + const info = await getAccountInfoByIdToken(authApi(), idpSignIn.idToken); + expect(info.emailVerified).to.be.true; // Verified by IDP. + + // Profile attributes should be overwritten (if provided by IDP) or cleared. + expect(info.photoUrl).to.equal(photoUrl); + expect(info.displayName).to.be.undefined; // Not provided by IDP. + }); + + it("should not unlink password if email was already verified", async () => { + // Given a user with verified email, linked with password: + const user = { + email: "foo@example.com", + password: "notasecret", + displayName: "Foo", + }; + const { localId, email } = await registerUser(authApi(), user); + await signInWithEmailLink(authApi(), email); // Verify email via email link sign-in. + + // When signing in with IDP and IDP verifies email: + const providerId = "google.com"; + const photoUrl = "http://localhost/photo-from-idp.png"; + const idpSignIn = await signInWithFakeClaims(authApi(), providerId, { + sub: "123456789012345678901", + email, + email_verified: true, + picture: photoUrl, + }); + + // It should sign-in into the same account and keep all providers and info. + expect(idpSignIn.localId).to.equal(localId); + const signInMethods = await getSigninMethods(authApi(), email); + expect(signInMethods).to.have.members([ + providerId, + PROVIDER_PASSWORD, + SIGNIN_METHOD_EMAIL_LINK, + ]); + + const info = await getAccountInfoByIdToken(authApi(), idpSignIn.idToken); + expect(info.emailVerified).to.be.true; // Verified by IDP. + + // Profile attributes should be overwritten (if provided by IDP) or cleared. + expect(info.photoUrl).to.equal(photoUrl); + expect(info.displayName).to.be.undefined; // Not provided by IDP. + }); + + it("should return needConfirmation if both account and IDP has unverified email", async () => { + // Given a user with unverified email: + const email = "bar@example.com"; + const providerId1 = "facebook.com"; + const originalDisplayName = "Bar"; + const { localId, idToken } = await signInWithFakeClaims(authApi(), providerId1, { + sub: "123456789012345678901", + email, + email_verified: false, + name: originalDisplayName, + }); + + // When signing in with IDP and IDP does not verify email: + const providerId2 = "google.com"; + const fakeIdToken = JSON.stringify( + fakeClaims({ + sub: "123456789012345678901", + email, + email_verified: false, + name: "Foo", + picture: "http://localhost/photo-from-idp.png", + }), + ); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + requestUri: `http://localhost?providerId=${providerId2}&id_token=${encodeURIComponent( + fakeIdToken, + )}`, + returnIdpCredential: true, + }) + .then((res) => { + // It should fail to sign in with needConfirmation. + expectStatusCode(200, res); + expect(res.body.needConfirmation).to.equal(true); + expect(res.body.localId).to.equal(localId); + expect(res.body).not.to.have.property("idToken"); + expect(res.body.verifiedProvider).to.eql([providerId1]); + }); + + const signInMethods = await getSigninMethods(authApi(), email); + expect(signInMethods).to.have.members([providerId1]); + expect(signInMethods).not.to.include([providerId2]); + + // Account should not be updated. + const info = await getAccountInfoByIdToken(authApi(), idToken); + expect(info.emailVerified).to.be.false; + expect(info.displayName).to.equal(originalDisplayName); + expect(info.photoUrl).to.be.undefined; + }); + + it("should error when requestUri is missing or invalid", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `id_token=${FAKE_GOOGLE_ACCOUNT.idToken}`, + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("MISSING_REQUEST_URI"); + }); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `id_token=${FAKE_GOOGLE_ACCOUNT.idToken}`, + requestUri: "notAnAbsoluteUriAndThusInvalid", + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("INVALID_REQUEST_URI"); + }); + }); + + it("should error when missing providerId is missing", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `id_token=${FAKE_GOOGLE_ACCOUNT.idToken}`, + requestUri: "http://localhost", + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.contain( + "INVALID_CREDENTIAL_OR_PROVIDER_ID : Invalid IdP response/credential:", + ); + }); + }); + + it("should error when sub is missing or not a string", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + // No sub in token. + postBody: `providerId=google.com&id_token=${JSON.stringify({})}`, + requestUri: "http://localhost", + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.contain("INVALID_IDP_RESPONSE"); + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + // sub is not a string + postBody: `providerId=google.com&id_token=${JSON.stringify({ sub: 12345 })}`, + requestUri: "http://localhost", + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.contain("INVALID_IDP_RESPONSE"); + }); + }); + + it("should link IDP to existing account by idToken", 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 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); + expect(res.body.localId).to.equal(user.localId); + 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).not.to.have.property("provider_id"); + expect(decoded!.payload.firebase) + .to.have.property("identities") + .eql({ + "google.com": [claims.sub], + email: [user.email], + }); + expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("google.com"); + }); + + const signInMethods = await getSigninMethods(authApi(), user.email); + expect(signInMethods).to.have.members(["google.com", PROVIDER_PASSWORD]); + }); + + it("should copy IDP email to user-level email if not present", async () => { + const user = await signInWithPhoneNumber(authApi(), TEST_PHONE_NUMBER); + const claims = fakeClaims({ + sub: "123456789012345678901", + name: "Foo", + email: "example@google.com", + }); + const fakeIdToken = JSON.stringify(claims); + const idToken = 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); + expect(res.body.localId).to.equal(user.localId); + 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).not.to.have.property("provider_id"); + expect(decoded!.payload.firebase) + .to.have.property("identities") + .eql({ + "google.com": [claims.sub], + email: [claims.email], + phone: [TEST_PHONE_NUMBER], + }); + expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("google.com"); + + return res.body.idToken as string; + }); + + const info = await getAccountInfoByIdToken(authApi(), idToken); + expect(info.email).to.be.equal(claims.email); + expect(!!info.emailVerified).to.be.equal(!!claims.email_verified); + }); + + it("should error if user to be linked is disabled", async () => { + const user = await registerUser(authApi(), { + email: "foo@example.com", + password: "notasecret", + }); + await updateAccountByLocalId(authApi(), user.localId, { disableUser: true }); + + const claims = fakeClaims({ + sub: "123456789012345678901", + name: "Foo", + }); + const fakeIdToken = JSON.stringify(claims); + 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(400, res); + expect(res.body.error.message).to.equal("USER_DISABLED"); + }); + }); + + it("should return pending credential for MFA-enabled user", async () => { + const claims = fakeClaims({ + sub: "123456789012345678901", + name: "Foo", + email: "foo@example.com", + email_verified: true, + }); + const { idToken, localId } = await signInWithFakeClaims(authApi(), "google.com", claims); + await enrollPhoneMfa(authApi(), idToken, TEST_PHONE_NUMBER); + const beforeSignIn = await getAccountInfoByLocalId(authApi(), localId); + + getClock().tick(3333); + + const fakeIdToken = JSON.stringify(claims); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `providerId=google.com&id_token=${encodeURIComponent(fakeIdToken)}`, + requestUri: "http://localhost", + returnIdpCredential: true, + }) + .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; + expect(mfaPendingCredential).to.be.a("string"); + expect(res.body.mfaInfo).to.be.an("array").with.lengthOf(1); + }); + + // 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); + }); + + it("should link IDP for existing MFA-enabled user", async () => { + const email = "alice@example.com"; + const { idToken, localId } = await signInWithEmailLink(authApi(), email); + await enrollPhoneMfa(authApi(), idToken, TEST_PHONE_NUMBER); + + const claims = fakeClaims({ + sub: "123456789012345678901", + name: "Foo", + email, + email_verified: true, + }); + + const fakeIdToken = JSON.stringify(claims); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `providerId=google.com&id_token=${encodeURIComponent(fakeIdToken)}`, + requestUri: "http://localhost", + returnIdpCredential: true, + }) + .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; + expect(mfaPendingCredential).to.be.a("string"); + expect(res.body.mfaInfo).to.be.an("array").with.lengthOf(1); + }); + + // IDP sign-in should be linked even before second factor is verified. + expect(await getSigninMethods(authApi(), email)).to.have.members(["google.com", "emailLink"]); + + // Similarly, user information from IDP should be added to account. + const info = await getAccountInfoByLocalId(authApi(), localId); + expect(info.displayName).to.equal(claims.name); + }); + + it("should return error if IDP account is already linked to the same user", async () => { + // Given a user with already linked with IDP account: + const providerId = "google.com"; + const claims = { + sub: "123456789012345678901", + email: "alice@example.com", + email_verified: false, + }; + const { idToken } = await signInWithFakeClaims(authApi(), providerId, claims); + + // When trying to link the same IDP account on the same user: + const fakeIdToken = JSON.stringify(claims); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + idToken, + postBody: `providerId=google.com&id_token=${encodeURIComponent(fakeIdToken)}`, + requestUri: "http://localhost", + returnIdpCredential: true, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.localId).to.be.undefined; + expect(res.body).not.to.have.property("refreshToken"); + + expect(res.body.errorMessage).to.equal("FEDERATED_USER_ID_ALREADY_LINKED"); + expect(res.body.oauthIdToken).to.equal(fakeIdToken); + }); + }); + + it("should return error if IDP account is already linked to another user", async () => { + // Given a user with already linked with IDP account: + const providerId = "google.com"; + const claims = { + sub: "123456789012345678901", + email: "alice@example.com", + email_verified: false, + }; + await signInWithFakeClaims(authApi(), providerId, claims); + + const user = await registerUser(authApi(), { + email: "foo@example.com", + password: "notasecret", + }); + // When trying to link the same IDP account on a different user: + const fakeIdToken = JSON.stringify(claims); + 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", + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("FEDERATED_USER_ID_ALREADY_LINKED"); + }); + + // Sign-in methods for either user should not be changed since linking failed. + const signInMethods1 = await getSigninMethods(authApi(), user.email); + expect(signInMethods1).to.have.members([PROVIDER_PASSWORD]); + const signInMethods2 = await getSigninMethods(authApi(), claims.email); + expect(signInMethods2).to.have.members([providerId]); + }); + + it("should return error if IDP account email already exists if NOT allowDuplicateEmail", async () => { + // Given an existing account with the email: + const email = "alice@example.com"; + await registerUser(authApi(), { email, password: "notasecret" }); + + // When trying to link an IDP account on a different user with the same email: + const { idToken } = await registerAnonUser(authApi()); + const fakeIdToken = JSON.stringify( + fakeClaims({ + sub: "12345", + email, + }), + ); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + idToken, + postBody: `providerId=google.com&id_token=${encodeURIComponent(fakeIdToken)}`, + requestUri: "http://localhost", + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("EMAIL_EXISTS"); + }); + }); + + it("should allow linking IDP account with same email to same user", async () => { + // Given an existing account with the email: + const email = "alice@example.com"; + const { idToken, localId } = await registerUser(authApi(), { email, password: "notasecret" }); + + // When trying to link an IDP account on user with the same email: + const fakeIdToken = JSON.stringify( + fakeClaims({ + sub: "12345", + email, + email_verified: true, + }), + ); + const newIdToken = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + idToken, + postBody: `providerId=google.com&id_token=${encodeURIComponent(fakeIdToken)}`, + requestUri: "http://localhost", + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.localId).to.equal(localId); + + return res.body.idToken as string; + }); + + // Account email is now verified. + const info = await getAccountInfoByIdToken(authApi(), newIdToken); + expect(info.emailVerified).to.be.true; + }); + + it("should allow linking IDP account with same email if allowDuplicateEmail", async () => { + // Given an existing account with the email: + const email = "alice@example.com"; + await registerUser(authApi(), { email, password: "notasecret" }); + + await updateProjectConfig(authApi(), { signIn: { allowDuplicateEmails: true } }); + + // When trying to link an IDP account on a different user with the same email: + const { idToken, localId } = await registerAnonUser(authApi()); + const fakeIdToken = JSON.stringify( + fakeClaims({ + sub: "12345", + email, + }), + ); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + idToken, + postBody: `providerId=google.com&id_token=${encodeURIComponent(fakeIdToken)}`, + requestUri: "http://localhost", + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.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:signInWithIdp") + .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 create a new account with tenantId", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: false }); + + const localId = 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, + tenantId: tenant.tenantId, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.tenantId).to.eql(tenant.tenantId); + return res.body.localId; + }); + + const user = await getAccountInfoByLocalId(authApi(), localId, tenant.tenantId); + expect(user.tenantId).to.eql(tenant.tenantId); + }); + + it("should return pending credential for MFA-enabled user and enabled on tenant project", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { + disableAuth: false, + mfaConfig: { + state: "ENABLED", + enabledProviders: ["PHONE_SMS"], + }, + }); + const claims = fakeClaims({ + sub: "123456789012345678901", + name: "Foo", + email: "foo@example.com", + email_verified: true, + }); + const { idToken, localId } = await signInWithFakeClaims( + authApi(), + "google.com", + claims, + tenant.tenantId, + ); + await enrollPhoneMfa(authApi(), idToken, TEST_PHONE_NUMBER, tenant.tenantId); + const beforeSignIn = await getAccountInfoByLocalId(authApi(), localId, tenant.tenantId); + + getClock().tick(3333); + + const fakeIdToken = JSON.stringify(claims); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `providerId=google.com&id_token=${encodeURIComponent(fakeIdToken)}`, + requestUri: "http://localhost", + returnIdpCredential: true, + tenantId: tenant.tenantId, + }) + .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; + expect(mfaPendingCredential).to.be.a("string"); + expect(res.body.mfaInfo).to.be.an("array").with.lengthOf(1); + }); + + // Login / refresh timestamps should not change until MFA was successful. + const afterFirstFactor = await getAccountInfoByLocalId(authApi(), localId, tenant.tenantId); + expect(afterFirstFactor.lastLoginAt).to.equal(beforeSignIn.lastLoginAt); + expect(afterFirstFactor.lastRefreshAt).to.equal(beforeSignIn.lastRefreshAt); + }); + + it("should error if SAMLResponse is missing assertion", async () => { + const samlResponse = {}; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `providerId=saml.saml&id_token=${ + FAKE_GOOGLE_ACCOUNT.idToken + }&SAMLResponse=${JSON.stringify(samlResponse)}`, + requestUri: "http://localhost", + returnIdpCredential: true, + returnSecureToken: true, + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.include("INVALID_IDP_RESPONSE"); + }); + }); + + it("should error if SAMLResponse is missing assertion.subject", async () => { + const samlResponse = { assertion: {} }; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `providerId=saml.saml&id_token=${ + FAKE_GOOGLE_ACCOUNT.idToken + }&SAMLResponse=${JSON.stringify(samlResponse)}`, + requestUri: "http://localhost", + returnIdpCredential: true, + returnSecureToken: true, + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.include("INVALID_IDP_RESPONSE"); + }); + }); + + it("should error if SAMLResponse is missing assertion.subject.nameId", async () => { + const samlResponse = { assertion: { subject: {} } }; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `providerId=saml.saml&id_token=${ + FAKE_GOOGLE_ACCOUNT.idToken + }&SAMLResponse=${JSON.stringify(samlResponse)}`, + requestUri: "http://localhost", + returnIdpCredential: true, + returnSecureToken: true, + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.include("INVALID_IDP_RESPONSE"); + }); + }); + + it("should create an account for generic SAML providers", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `providerId=saml.saml&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); + expect(res.body.email).to.equal(FAKE_GOOGLE_ACCOUNT.email); + expect(res.body.emailVerified).to.equal(true); + expect(res.body.federatedId).to.equal(FAKE_GOOGLE_ACCOUNT.rawId); + expect(res.body.oauthIdToken).to.equal(FAKE_GOOGLE_ACCOUNT.idToken); + expect(res.body.providerId).to.equal("saml.saml"); + expect(res.body).to.have.property("refreshToken").that.is.a("string"); + + // The ID Token used above does NOT contain name or photo, so the + // account created won't have those attributes either. + expect(res.body).not.to.have.property("displayName"); + expect(res.body).not.to.have.property("photoUrl"); + + 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).not.to.have.property("provider_id"); + expect(decoded!.payload.firebase) + .to.have.property("identities") + .eql({ + "saml.saml": [FAKE_GOOGLE_ACCOUNT.rawId], + email: [FAKE_GOOGLE_ACCOUNT.email], + }); + expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("saml.saml"); + }); + }); + + it("should include fields in SAMLResponse for SAML providers", async () => { + const otherEmail = "otherEmail@gmail.com"; + const attributeStatements = { + name: "Jane Doe", + mail: "otherOtherEmail@gmail.com", + }; + const samlResponse = { assertion: { subject: { nameId: otherEmail }, attributeStatements } }; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `providerId=saml.saml&id_token=${ + FAKE_GOOGLE_ACCOUNT.idToken + }&SAMLResponse=${JSON.stringify(samlResponse)}`, + requestUri: "http://localhost", + returnIdpCredential: true, + returnSecureToken: true, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.email).to.equal(otherEmail); + + const rawUserInfo = JSON.parse(res.body.rawUserInfo); + expect(rawUserInfo).to.eql(attributeStatements); + + const idToken = res.body.idToken; + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + expect(decoded!.payload.firebase) + .to.have.property("sign_in_attributes") + .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..cb05b6ae3d9 --- /dev/null +++ b/src/emulator/auth/misc.spec.ts @@ -0,0 +1,712 @@ +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 email when privileged", async () => { + const { email } = await registerUser(authApi(), { + email: "bob@example.com", + password: "password", + }); + + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:lookup`) + .set("Authorization", "Bearer owner") + .send({ email: [email] }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.users).to.have.length(1); + expect(res.body.users[0].email).to.equal(email); + }); + }); + + it("should return user by email even when uppercased", async () => { + const { email } = await registerUser(authApi(), { + email: "foo@example.com", + password: "password", + }); + const caplitalizedEmail = email.toUpperCase(); + + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:lookup`) + .set("Authorization", "Bearer owner") + .send({ email: [caplitalizedEmail] }) + .then((res) => { + console.log(res.body.users); + expectStatusCode(200, res); + expect(res.body.users).to.have.length(1); + expect(res.body.users[0].email).to.equal(email); + }); + }); + + it("should return empty result when email is not found", async () => { + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:lookup`) + .set("Authorization", "Bearer owner") + .send({ email: ["non-existent-email@example.com"] }) + .then((res) => { + console.log(res.body.users); + expectStatusCode(200, res); + expect(res.body).not.to.have.property("users"); + }); + }); + + 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/emulator/auth/oob.spec.ts b/src/emulator/auth/oob.spec.ts new file mode 100644 index 00000000000..b85ee656fa6 --- /dev/null +++ b/src/emulator/auth/oob.spec.ts @@ -0,0 +1,631 @@ +import { expect } from "chai"; +import { describeAuthEmulator, PROJECT_ID } from "./testing/setup"; +import { + expectStatusCode, + registerUser, + registerAnonUser, + updateAccountByLocalId, + expectIdTokenExpired, + inspectOobs, + registerTenant, + updateConfig, +} from "./testing/helpers"; + +describeAuthEmulator("accounts:sendOobCode", ({ authApi, getClock }) => { + it("should generate OOB code for verify email", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + const { idToken, localId } = await registerUser(authApi(), user); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .query({ key: "fake-api-key" }) + .send({ idToken, requestType: "VERIFY_EMAIL" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.email).to.equal(user.email); + + // These fields should not be set since returnOobLink is not set. + expect(res.body).not.to.have.property("oobCode"); + expect(res.body).not.to.have.property("oobLink"); + }); + + const oobs = await inspectOobs(authApi()); + expect(oobs).to.have.length(1); + expect(oobs[0].email).to.equal(user.email); + expect(oobs[0].requestType).to.equal("VERIFY_EMAIL"); + + // The returned oobCode can be redeemed to verify the email. + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:update") + .query({ key: "fake-api-key" }) + // OOB code is enough, no idToken needed. + .send({ oobCode: oobs[0].oobCode }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.localId).to.equal(localId); + expect(res.body.email).to.equal(user.email); + expect(res.body.emailVerified).to.equal(true); + }); + + // oobCode is removed after redeemed. + const oobs2 = await inspectOobs(authApi()); + expect(oobs2).to.have.length(0); + }); + + it("should return OOB code directly for requests with OAuth 2", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + await registerUser(authApi(), user); + 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.email).to.equal(user.email); + expect(res.body.oobCode).to.be.a("string"); + expect(res.body.oobLink).to.be.a("string"); + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .set("Authorization", "Bearer owner") + .send({ email: user.email, requestType: "VERIFY_EMAIL", returnOobLink: true }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.email).to.equal(user.email); + expect(res.body.oobCode).to.be.a("string"); + expect(res.body.oobLink).to.be.a("string"); + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .set("Authorization", "Bearer owner") + .send({ + email: user.email, + newEmail: "bob@example.com", + requestType: "VERIFY_AND_CHANGE_EMAIL", + returnOobLink: true, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.email).to.equal(user.email); + expect(res.body.oobCode).to.be.a("string"); + expect(res.body.oobLink).to.be.a("string"); + }); + }); + + it("should return OOB code by idToken for OAuth 2 requests as well", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + const { idToken } = await registerUser(authApi(), user); + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .set("Authorization", "Bearer owner") + .send({ idToken, requestType: "VERIFY_EMAIL", returnOobLink: true }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.email).to.equal(user.email); + expect(res.body.oobCode).to.be.a("string"); + expect(res.body.oobLink).to.be.a("string"); + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .set("Authorization", "Bearer owner") + .send({ + email: user.email, + newEmail: "bob@example.com", + idToken, + requestType: "VERIFY_AND_CHANGE_EMAIL", + returnOobLink: true, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.email).to.equal(user.email); + expect(res.body.oobCode).to.be.a("string"); + expect(res.body.oobLink).to.be.a("string"); + }); + }); + + it("should error when trying to verify email without idToken or email", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + await registerUser(authApi(), user); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .query({ key: "fake-api-key" }) + .send({ idToken: "hoge", requestType: "VERIFY_EMAIL" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equal("INVALID_ID_TOKEN"); + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .set("Authorization", "Bearer owner") + // This causes a different error message to be returned, see below. + .send({ returnOobLink: true, requestType: "VERIFY_EMAIL" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equal("MISSING_EMAIL"); + }); + + const oobs = await inspectOobs(authApi()); + expect(oobs).to.have.length(0); + }); + + it("should error when trying to verify email without idToken if not returnOobLink", async () => { + const user = await registerUser(authApi(), { + email: "alice@example.com", + password: "notasecret", + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .query({ key: "fake-api-key" }) + // email here is ignored because returnOobLink is not set. + .send({ email: user.email, requestType: "VERIFY_EMAIL" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equal("INVALID_ID_TOKEN"); + }); + + const oobs = await inspectOobs(authApi()); + expect(oobs).to.have.length(0); + }); + + it("should error when trying to verify email not associated with any user", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .set("Authorization", "Bearer owner") + .send({ email: "nosuchuser@example.com", returnOobLink: true, requestType: "VERIFY_EMAIL" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equal("USER_NOT_FOUND"); + }); + + const oobs = await inspectOobs(authApi()); + expect(oobs).to.have.length(0); + }); + + it("should error when verifying email for accounts without email", async () => { + const { idToken } = await registerAnonUser(authApi()); + + 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").equal("MISSING_EMAIL"); + }); + + const oobs = await inspectOobs(authApi()); + expect(oobs).to.have.length(0); + }); + + it("should error if user is disabled", async () => { + const { localId, idToken, email } = await registerUser(authApi(), { + email: "foo@example.com", + password: "foobar", + }); + await updateAccountByLocalId(authApi(), localId, { disableUser: true }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .query({ key: "fake-api-key" }) + .send({ email, idToken, requestType: "VERIFY_EMAIL" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("USER_DISABLED"); + }); + }); + + it("should error when continueUrl is invalid", async () => { + const { idToken } = await registerUser(authApi(), { + email: "alice@example.com", + password: "notasecret", + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .query({ key: "fake-api-key" }) + .send({ + idToken, + requestType: "VERIFY_EMAIL", + continueUrl: "noSchemeOrHost", + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").contain("INVALID_CONTINUE_URI"); + }); + + const oobs = await inspectOobs(authApi()); + expect(oobs).to.have.length(0); + }); + + 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:sendOobCode") + .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 for email sign in if not enabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { + disableAuth: false, + enableEmailLinkSignin: false, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId, email: "bob@example.com", requestType: "EMAIL_SIGNIN" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("OPERATION_NOT_ALLOWED"); + }); + }); + + it("should generate OOB code for reset password", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + const { idToken } = await registerUser(authApi(), user); + + getClock().tick(2000); // Wait for idToken to be issued in the past. + + 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); + + // These fields should not be set since returnOobLink is not set. + expect(res.body).not.to.have.property("oobCode"); + expect(res.body).not.to.have.property("oobLink"); + }); + + const oobs = await inspectOobs(authApi()); + expect(oobs).to.have.length(1); + expect(oobs[0].email).to.equal(user.email); + expect(oobs[0].requestType).to.equal("PASSWORD_RESET"); + + // The returned oobCode can be redeemed to reset the password. + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:resetPassword") + .query({ key: "fake-api-key" }) + .send({ oobCode: oobs[0].oobCode, newPassword: "notasecret2" }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.requestType).to.equal("PASSWORD_RESET"); + expect(res.body.email).to.equal(user.email); + }); + + // All old idTokens are invalidated. + await expectIdTokenExpired(authApi(), idToken); + }); + + it("should return purpose of oobCodes via resetPassword endpoint", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + const { idToken } = await registerUser(authApi(), user); + const newEmail = "bob@example.com"; + + 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)); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .query({ key: "fake-api-key" }) + .send({ requestType: "VERIFY_EMAIL", idToken }) + .then((res) => expectStatusCode(200, res)); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .query({ key: "fake-api-key" }) + .send({ email: newEmail, requestType: "EMAIL_SIGNIN" }) + .then((res) => expectStatusCode(200, res)); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .query({ key: "fake-api-key" }) + .send({ + email: user.email, + newEmail, + requestType: "VERIFY_AND_CHANGE_EMAIL", + idToken, + }) + .then((res) => expectStatusCode(200, res)); + + const oobs = await inspectOobs(authApi()); + expect(oobs).to.have.length(4); + + for (const oob of oobs) { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:resetPassword") + .query({ key: "fake-api-key" }) + // If newPassword is not set, this API will just return the purpose + // (requestType) of the code without consuming it. + .send({ oobCode: oob.oobCode }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.requestType).to.equal(oob.requestType); + if (oob.requestType === "EMAIL_SIGNIN") { + // Do not reveal the email when inspecting an email sign-in oobCode. + // Instead, the client must provide email (e.g. by asking the user) + // when they call the emailLinkSignIn endpoint. + // See: https://firebase.google.com/docs/auth/web/email-link-auth#security_concerns + expect(res.body).not.to.have.property("email"); + } else { + expect(res.body.email).to.equal(oob.email); + } + if (oob.requestType === "VERIFY_AND_CHANGE_EMAIL") { + expect(res.body.newEmail).to.equal(newEmail); + } + }); + } + + // OOB codes are not consumed by the lookup above. + const oobs2 = await inspectOobs(authApi()); + expect(oobs2).to.have.length(4); + }); + + it("should error on resetPassword if auth is disabled", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { disableAuth: true }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:resetPassword") + .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 resetPassword 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:resetPassword") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(400, res); + 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); + }); + }); + + it("should generate OOB code for verify and change email", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + const { idToken, localId } = await registerUser(authApi(), user); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .query({ key: "fake-api-key" }) + .send({ + email: user.email, + newEmail: "bob@example.com", + idToken, + requestType: "VERIFY_AND_CHANGE_EMAIL", + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body) + .to.have.property("kind") + .equals("identitytoolkit#GetOobConfirmationCodeResponse"); + expect(res.body.email).to.equal(user.email); + + // These fields should not be set since returnOobLink is not set. + expect(res.body).not.to.have.property("oobCode"); + expect(res.body).not.to.have.property("oobLink"); + }); + + const oobs = await inspectOobs(authApi()); + expect(oobs).to.have.length(1); + expect(oobs[0].email).to.equal(user.email); + expect(oobs[0].requestType).to.equal("VERIFY_AND_CHANGE_EMAIL"); + + // The returned oobCode can be redeemed to verify and change the email. + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:update") + .query({ key: "fake-api-key" }) + // OOB code is enough, no idToken needed. + .send({ oobCode: oobs[0].oobCode }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.localId).to.equal(localId); + expect(res.body.email).to.equal("bob@example.com"); + expect(res.body.emailVerified).to.equal(true); + }); + + // oobCode is removed after redeemed. + const oobs2 = await inspectOobs(authApi()); + expect(oobs2).to.have.length(0); + }); + + it("should error when trying to verify and change email without idToken or email or newEmail", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + await registerUser(authApi(), user); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .query({ key: "fake-api-key" }) + .send({ newEmail: "bob@example.com", requestType: "VERIFY_AND_CHANGE_EMAIL" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equal("MISSING_ID_TOKEN"); + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .query({ key: "fake-api-key" }) + .send({ email: user.email, requestType: "VERIFY_AND_CHANGE_EMAIL" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equal("MISSING_NEW_EMAIL"); + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .set("Authorization", "Bearer owner") + .send({ + newEmail: "bob@example.com", + returnOobLink: true, + requestType: "VERIFY_AND_CHANGE_EMAIL", + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equal("MISSING_EMAIL"); + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .set("Authorization", "Bearer owner") + .send({ + email: user.email, + returnOobLink: true, + requestType: "VERIFY_AND_CHANGE_EMAIL", + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equal("MISSING_NEW_EMAIL"); + }); + + const oobs = await inspectOobs(authApi()); + expect(oobs).to.have.length(0); + }); + + it("should error when trying to verify and change email without idToken if not returnOobLink", async () => { + const user = await registerUser(authApi(), { + email: "alice@example.com", + password: "notasecret", + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .query({ key: "fake-api-key" }) + .send({ + email: user.email, + newEmail: "bob@example.com", + requestType: "VERIFY_AND_CHANGE_EMAIL", + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equal("MISSING_ID_TOKEN"); + }); + + const oobs = await inspectOobs(authApi()); + expect(oobs).to.have.length(0); + }); + + it("should error when trying to verify and change email not associated with any user", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .set("Authorization", "Bearer owner") + .send({ + email: "nosuchuser@example.com", + newEmail: "bob@example.com", + returnOobLink: true, + requestType: "VERIFY_AND_CHANGE_EMAIL", + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equal("USER_NOT_FOUND"); + }); + + const oobs = await inspectOobs(authApi()); + expect(oobs).to.have.length(0); + }); + + it("should error if newEmail is already associated to another user", async () => { + const user = { + email: "alice@example.com", + password: "notasecret", + }; + const { idToken } = await registerUser(authApi(), user); + const anotherUser = await registerUser(authApi(), { + email: "bob@example.com", + password: "notasecret", + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .query({ key: "fake-api-key" }) + .send({ + idToken, + email: user.email, + newEmail: anotherUser.email, + requestType: "VERIFY_AND_CHANGE_EMAIL", + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equal("EMAIL_EXISTS"); + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .set("Authorization", "Bearer owner") + .send({ + email: user.email, + newEmail: anotherUser.email, + returnOobLink: true, + requestType: "VERIFY_AND_CHANGE_EMAIL", + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equal("EMAIL_EXISTS"); + }); + + const oobs = await inspectOobs(authApi()); + expect(oobs).to.have.length(0); + }); +}); diff --git a/src/emulator/auth/operations.ts b/src/emulator/auth/operations.ts index d018dacb433..d49524af711 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"; @@ -27,8 +30,14 @@ import { SIGNIN_METHOD_EMAIL_LINK, PROVIDER_CUSTOM, OobRecord, + PROVIDER_GAME_CENTER, + SecondFactorRecord, + AgentProjectState, + TenantProjectState, + MfaConfig, + BlockingFunctionEvents, } from "./state"; -import { MfaEnrollments, CreateMfaEnrollmentsRequest, Schemas, MfaEnrollment } from "./types"; +import { MfaEnrollments, Schemas } from "./types"; /** * Create a map from IDs to operations handlers suitable for exegesis. @@ -54,10 +63,21 @@ export const authOperations: AuthOps = { signInWithPhoneNumber, signUp, update: setAccountInfo, + mfaEnrollment: { + finalize: mfaEnrollmentFinalize, + start: mfaEnrollmentStart, + withdraw: mfaEnrollmentWithdraw, + }, + mfaSignIn: { + start: mfaSignInStart, + finalize: mfaSignInFinalize, + }, }, projects: { createSessionCookie, queryAccounts, + getConfig, + updateConfig, accounts: { _: signUp, delete: deleteAccount, @@ -69,6 +89,25 @@ export const authOperations: AuthOps = { batchDelete, batchGet, }, + tenants: { + create: createTenant, + delete: deleteTenant, + get: getTenant, + list: listTenants, + patch: updateTenant, + createSessionCookie, + accounts: { + _: signUp, + batchCreate, + batchDelete, + batchGet, + delete: deleteAccount, + lookup, + query: queryAccounts, + sendOobCode, + update: setAccountInfo, + }, + }, }, }, securetoken: { @@ -101,14 +140,23 @@ const PASSWORD_MIN_LENGTH = 6; export const CUSTOM_TOKEN_AUDIENCE = "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"; -function signUp( +const MFA_INELIGIBLE_PROVIDER = new Set([ + PROVIDER_ANONYMOUS, + PROVIDER_PHONE, + PROVIDER_CUSTOM, + PROVIDER_GAME_CENTER, +]); + +async function signUp( state: ProjectState, reqBody: Schemas["GoogleCloudIdentitytoolkitV1SignUpRequest"], - ctx: ExegesisContext -): Schemas["GoogleCloudIdentitytoolkitV1SignUpResponse"] { + ctx: ExegesisContext, +): Promise { + assert(!state.disableAuth, "PROJECT_DISABLED"); 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) { @@ -142,13 +190,17 @@ function signUp( assert(reqBody.email, "MISSING_EMAIL"); assert(reqBody.password, "MISSING_PASSWORD"); provider = PROVIDER_PASSWORD; + assert(state.allowPasswordSignup, "OPERATION_NOT_ALLOWED"); } else { // Most attributes are ignored when creating anon user without privilege. provider = PROVIDER_ANONYMOUS; + assert(state.enableAnonymousUser, "ADMIN_ONLY_OPERATION"); } } - 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"); @@ -157,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); @@ -169,17 +221,47 @@ function signUp( generateEnrollmentIds: true, }); } + if (state instanceof TenantProjectState) { + updates.tenantId = state.tenantId; + } let user: UserInfo | undefined; if (reqBody.idToken) { ({ 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); @@ -190,15 +272,16 @@ 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(); const users: UserInfo[] = []; function tryAddUser(maybeUser: UserInfo | undefined): void { @@ -217,7 +300,8 @@ function lookup( tryAddUser(state.getUserByLocalId(localId)); } for (const email of reqBody.email ?? []) { - tryAddUser(state.getUserByEmail(email)); + const canonicalizedEmail = canonicalizeEmailAddress(email); + tryAddUser(state.getUserByEmail(canonicalizedEmail)); } for (const phoneNumber of reqBody.phoneNumber ?? []) { tryAddUser(state.getUserByPhoneNumber(phoneNumber)); @@ -244,8 +328,9 @@ function lookup( function batchCreate( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV1UploadAccountRequest"] + reqBody: Schemas["GoogleCloudIdentitytoolkitV1UploadAccountRequest"], ): Schemas["GoogleCloudIdentitytoolkitV1UploadAccountResponse"] { + assert(!state.disableAuth, "PROJECT_DISABLED"); assert(reqBody.users?.length, "MISSING_USER_ACCOUNT"); if (reqBody.sanityCheck) { @@ -266,7 +351,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); } @@ -294,6 +379,15 @@ function batchCreate( photoUrl: userInfo.photoUrl, lastLoginAt: userInfo.lastLoginAt, }; + if (userInfo.tenantId) { + assert( + state instanceof TenantProjectState && state.tenantId === userInfo.tenantId, + "Tenant id in userInfo does not match the tenant id in request.", + ); + } + if (state instanceof TenantProjectState) { + fields.tenantId = state.tenantId; + } // password if (userInfo.passwordHash) { @@ -329,14 +423,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 }); } @@ -347,7 +441,6 @@ function batchCreate( assert(isValidPhoneNumber(userInfo.phoneNumber), "phone number format is invalid"); fields.phoneNumber = userInfo.phoneNumber; } - // TODO: Support MFA. fields.validSince = toUnixTimestamp(uploadTime).toString(); fields.createdAt = uploadTime.getTime().toString(); @@ -366,21 +459,44 @@ 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); } fields.emailVerified = !!userInfo.emailVerified; fields.disabled = !!userInfo.disabled; + // MFA + 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."); + const existingIds = new Set(); + for (const enrollment of userInfo.mfaInfo) { + if (enrollment.mfaEnrollmentId) { + assert(!existingIds.has(enrollment.mfaEnrollmentId), "Enrollment id already exists."); + existingIds.add(enrollment.mfaEnrollmentId); + } + } + + for (const enrollment of userInfo.mfaInfo) { + enrollment.mfaEnrollmentId = enrollment.mfaEnrollmentId || newRandomId(28, existingIds); + enrollment.enrolledAt = enrollment.enrolledAt || new Date().toISOString(); + assert(enrollment.phoneInfo, "Second factor not supported."); + assert(isValidPhoneNumber(enrollment.phoneInfo), "Phone number format is invalid"); + enrollment.unobfuscatedPhoneInfo = enrollment.phoneInfo; + fields.mfaInfo.push(enrollment); + } + } + 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; @@ -408,7 +524,7 @@ function batchCreate( function batchDelete( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV1BatchDeleteAccountsRequest"] + reqBody: Schemas["GoogleCloudIdentitytoolkitV1BatchDeleteAccountsRequest"], ): Schemas["GoogleCloudIdentitytoolkitV1BatchDeleteAccountsResponse"] { const errors: Required< Schemas["GoogleCloudIdentitytoolkitV1BatchDeleteAccountsResponse"]["errors"] @@ -438,19 +554,20 @@ function batchDelete( function batchGet( state: ProjectState, reqBody: unknown, - ctx: ExegesisContext + ctx: ExegesisContext, ): Schemas["GoogleCloudIdentitytoolkitV1DownloadAccountResponse"] { - const limit = Math.min(Math.floor(ctx.params.query.maxResults) || 20, 1000); + 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; - // As a non-standard behavior, passing in limit=-1 will return all users. - if (limit >= 0 && users.length >= limit) { - users.length = limit; + // As a non-standard behavior, passing in maxResults=-1 will return all users. + if (maxResults >= 0 && users.length >= maxResults) { + users.length = maxResults; if (users.length) { newPageToken = users[users.length - 1].localId; } @@ -465,8 +582,9 @@ function batchGet( function createAuthUri( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV1CreateAuthUriRequest"] + reqBody: Schemas["GoogleCloudIdentitytoolkitV1CreateAuthUriRequest"], ): Schemas["GoogleCloudIdentitytoolkitV1CreateAuthUriResponse"] { + assert(!state.disableAuth, "PROJECT_DISABLED"); const sessionId = reqBody.sessionId || randomId(27); if (reqBody.providerId) { throw new NotImplementedError("Sign-in with IDP is not yet supported."); @@ -521,13 +639,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 */ @@ -536,14 +661,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(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()); @@ -555,12 +679,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 }; @@ -569,8 +693,9 @@ function createSessionCookie( function deleteAccount( state: ProjectState, reqBody: Schemas["GoogleCloudIdentitytoolkitV1DeleteAccountRequest"], - ctx: ExegesisContext + ctx: ExegesisContext, ): Schemas["GoogleCloudIdentitytoolkitV1DeleteAccountResponse"] { + assert(!state.disableAuth, "PROJECT_DISABLED"); let user: UserInfo; if (ctx.security?.Oauth2) { assert(reqBody.localId, "MISSING_LOCAL_ID"); @@ -590,18 +715,25 @@ function deleteAccount( } function getProjects( - state: ProjectState + state: ProjectState, ): Schemas["GoogleCloudIdentitytoolkitV1GetProjectConfigResponse"] { + assert(!state.disableAuth, "PROJECT_DISABLED"); + assert(state instanceof AgentProjectState, "UNSUPPORTED_TENANT_OPERATION"); return { projectId: state.projectNumber, authorizedDomains: [ + // This list is just a placeholder -- the JS SDK will NOT validate the + // domain at all when connecting to the emulator. Google-internal context: + // http://go/firebase-auth-emulator-dd#heading=h.3r9cilur7s46 "localhost", - // TODO: Shall we allow more domains? ], }; } -function getRecaptchaParams(): Schemas["GoogleCloudIdentitytoolkitV1GetRecaptchaParamResponse"] { +function getRecaptchaParams( + state: ProjectState, +): Schemas["GoogleCloudIdentitytoolkitV1GetRecaptchaParamResponse"] { + assert(!state.disableAuth, "PROJECT_DISABLED"); return { kind: "identitytoolkit#GetRecaptchaParamResponse", @@ -618,8 +750,9 @@ function getRecaptchaParams(): Schemas["GoogleCloudIdentitytoolkitV1GetRecaptcha function queryAccounts( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV1QueryUserInfoRequest"] + reqBody: Schemas["GoogleCloudIdentitytoolkitV1QueryUserInfoRequest"], ): Schemas["GoogleCloudIdentitytoolkitV1QueryUserInfoResponse"] { + assert(!state.disableAuth, "PROJECT_DISABLED"); if (reqBody.expression?.length) { throw new NotImplementedError("expression is not implemented."); } @@ -676,8 +809,10 @@ function queryAccounts( */ export function resetPassword( state: ProjectState, - reqBody: Schemas["GoogleCloudIdentitytoolkitV1ResetPasswordRequest"] + reqBody: Schemas["GoogleCloudIdentitytoolkitV1ResetPasswordRequest"], ): Schemas["GoogleCloudIdentitytoolkitV1ResetPasswordResponse"] { + assert(!state.disableAuth, "PROJECT_DISABLED"); + assert(state.allowPasswordSignup, "PASSWORD_LOGIN_DISABLED"); assert(reqBody.oobCode, "MISSING_OOB_CODE"); const oob = state.validateOobCode(reqBody.oobCode); assert(oob, "INVALID_OOB_CODE"); @@ -686,15 +821,15 @@ 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); - const user = state.getUserByEmail(oob.email); + let user = state.getUserByEmail(oob.email); assert(user, "INVALID_OOB_CODE"); const salt = "fakeSalt" + randomId(20); const passwordHash = hashPassword(reqBody.newPassword, salt); - state.updateUserByLocalId( + user = state.updateUserByLocalId( user.localId, { emailVerified: true, @@ -703,7 +838,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) }, ); } @@ -715,17 +850,19 @@ export function resetPassword( // when they call the emailLinkSignIn endpoint. // See: https://firebase.google.com/docs/auth/web/email-link-auth#security_concerns email: oob.requestType === "EMAIL_SIGNIN" ? undefined : oob.email, + newEmail: oob.newEmail, }; } function sendOobCode( state: ProjectState, reqBody: Schemas["GoogleCloudIdentitytoolkitV1GetOobCodeRequest"], - ctx: ExegesisContext + ctx: ExegesisContext, ): Schemas["GoogleCloudIdentitytoolkitV1GetOobCodeResponse"] { + assert(!state.disableAuth, "PROJECT_DISABLED"); assert( reqBody.requestType && reqBody.requestType !== "OOB_REQ_TYPE_UNSPECIFIED", - "MISSING_REQ_TYPE" + "MISSING_REQ_TYPE", ); if (reqBody.returnOobLink) { assert(ctx.security?.Oauth2, "INSUFFICIENT_PERMISSION"); @@ -733,15 +870,17 @@ 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))", ); } let email: string; + let newEmail: string | undefined; let mode: string; switch (reqBody.requestType) { case "EMAIL_SIGNIN": + assert(state.enableEmailLinkSignin, "OPERATION_NOT_ALLOWED"); mode = "signIn"; assert(reqBody.email, "MISSING_EMAIL"); email = canonicalizeEmailAddress(reqBody.email); @@ -750,7 +889,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"; @@ -769,7 +915,23 @@ function sendOobCode( email = user.email; } break; - + case "VERIFY_AND_CHANGE_EMAIL": + mode = "verifyAndChangeEmail"; + assert(reqBody.newEmail, "MISSING_NEW_EMAIL"); + newEmail = canonicalizeEmailAddress(reqBody.newEmail); + if (reqBody.returnOobLink && !reqBody.idToken) { + assert(reqBody.email, "MISSING_EMAIL"); + email = canonicalizeEmailAddress(reqBody.email); + const maybeUser = state.getUserByEmail(email); + assert(maybeUser, "USER_NOT_FOUND"); + } else { + assert(reqBody.idToken, "MISSING_ID_TOKEN"); + const user = parseIdToken(state, reqBody.idToken).user; + assert(user.email, "MISSING_EMAIL"); + email = user.email; + } + assert(!state.getUserByEmail(newEmail), "EMAIL_EXISTS"); + break; default: throw new NotImplementedError(reqBody.requestType); } @@ -777,7 +939,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.", ); } @@ -786,6 +948,7 @@ function sendOobCode( requestType: reqBody.requestType, mode, continueUrl: reqBody.continueUrl, + newEmail, }); if (reqBody.returnOobLink) { @@ -807,15 +970,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"); // 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.", ); const { sessionInfo, phoneNumber, code } = state.createVerificationCode(reqBody.phoneNumber); @@ -824,7 +995,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 { @@ -835,8 +1006,9 @@ function sendVerificationCode( function setAccountInfo( state: ProjectState, reqBody: Schemas["GoogleCloudIdentitytoolkitV1SetAccountInfoRequest"], - ctx: ExegesisContext + ctx: ExegesisContext, ): Schemas["GoogleCloudIdentitytoolkitV1SetAccountInfoResponse"] { + assert(!state.disableAuth, "PROJECT_DISABLED"); const url = authEmulatorUrl(ctx.req as express.Request); return setAccountInfoImpl(state, reqBody, { privileged: !!ctx.security?.Oauth2, @@ -856,16 +1028,10 @@ 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)[] = [ - "provider", - "upgradeToFederatedLogin", - "captchaChallenge", - "captchaResponse", - "linkProviderUserInfo", - ]; + const unimplementedFields: (keyof typeof reqBody)[] = ["provider", "upgradeToFederatedLogin"]; for (const field of unimplementedFields) { if (field in reqBody) { throw new NotImplementedError(`${field} is not implemented yet.`); @@ -875,7 +1041,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 { @@ -897,6 +1063,7 @@ export function setAccountInfoImpl( let user: UserInfo; let signInProvider: string | undefined; let isEmailUpdate: boolean = false; + let newEmail: string | undefined; if (reqBody.oobCode) { const oob = state.validateOobCode(reqBody.oobCode); @@ -914,6 +1081,19 @@ export function setAccountInfoImpl( } break; } + case "VERIFY_AND_CHANGE_EMAIL": + state.deleteOobCode(reqBody.oobCode); + const maybeUser = state.getUserByEmail(oob.email); + assert(maybeUser, "INVALID_OOB_CODE"); + assert(oob.newEmail, "INVALID_OOB_CODE"); + assert(!state.getUserByEmail(oob.newEmail), "EMAIL_EXISTS"); + user = maybeUser; + if (oob.newEmail !== user.email) { + updates.email = oob.newEmail; + updates.emailVerified = true; + newEmail = oob.newEmail; + } + break; case "RECOVER_EMAIL": { state.deleteOobCode(reqBody.oobCode); const maybeUser = state.getUserByInitialEmail(oob.email); @@ -945,7 +1125,7 @@ export function setAccountInfoImpl( if (reqBody.email) { assert(isValidEmailAddress(reqBody.email), "INVALID_EMAIL"); - const newEmail = canonicalizeEmailAddress(reqBody.email); + newEmail = canonicalizeEmailAddress(reqBody.email); if (newEmail !== user.email) { assert(!state.getUserByEmail(newEmail), "EMAIL_EXISTS"); updates.email = newEmail; @@ -964,7 +1144,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); @@ -1006,7 +1186,7 @@ export function setAccountInfoImpl( "customAttributes", "createdAt", "lastLoginAt", - "validSince" + "validSince", ); } for (const field of fieldsToCopy) { @@ -1049,8 +1229,16 @@ export function setAccountInfoImpl( } } + if (reqBody.linkProviderUserInfo) { + assert(reqBody.linkProviderUserInfo.providerId, "MISSING_PROVIDER_ID"); + assert(reqBody.linkProviderUserInfo.rawId, "MISSING_RAW_ID"); + } + user = state.updateUserByLocalId(user.localId, updates, { deleteProviders: reqBody.deleteProvider, + upsertProviders: reqBody.linkProviderUserInfo + ? [reqBody.linkProviderUserInfo as ProviderUserInfo] + : undefined, }); // Only initiate the recover email OOB flow for non-anonymous users @@ -1070,13 +1258,14 @@ export function setAccountInfoImpl( email: user.email, displayName: user.displayName, photoUrl: user.photoUrl, + newEmail, passwordHash: user.passwordHash, ...(updates.validSince && signInProvider ? issueTokens(state, user, signInProvider) : {}), }); } -function sendOobForEmailReset(state: ProjectState, initialEmail: string, url: URL) { +function sendOobForEmailReset(state: ProjectState, initialEmail: string, url: URL): void { const oobRecord = createOobRecord(state, initialEmail, url, { requestType: "RECOVER_EMAIL", mode: "recoverEmail", @@ -1094,9 +1283,10 @@ function createOobRecord( requestType: OobRequestType; mode: string; continueUrl?: string; - } + newEmail?: string; + }, ): OobRecord { - const oobRecord = state.createOob(email, params.requestType, (oobCode) => { + const oobRecord = state.createOob(email, params.newEmail, params.requestType, (oobCode) => { url.pathname = "/emulator/action"; url.searchParams.set("mode", params.mode); url.searchParams.set("lang", "en"); @@ -1111,6 +1301,10 @@ function createOobRecord( url.searchParams.set("continueUrl", params.continueUrl); } + if (state instanceof TenantProjectState) { + url.searchParams.set("tenantId", state.tenantId); + } + return url.toString(); }); @@ -1134,6 +1328,9 @@ function logOobMessage(oobRecord: OobRecord) { case "VERIFY_EMAIL": maybeMessage = `To verify the email address ${email}, follow this link: ${oobLink}`; break; + case "VERIFY_AND_CHANGE_EMAIL": + maybeMessage = `To verify and change the email address from ${email} to ${oobRecord.newEmail}, follow this link: ${oobLink}`; + break; case "RECOVER_EMAIL": maybeMessage = `To reset your email address to ${email}, follow this link: ${oobLink}`; break; @@ -1146,12 +1343,18 @@ 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; user_id?: unknown; claims?: unknown }; + let payload: { + aud?: unknown; + uid?: unknown; + user_id?: unknown; + claims?: unknown; + tenant_id?: unknown; + }; if (reqBody.token.startsWith("{")) { // In the emulator only, we allow plain JSON strings as custom tokens, to // simplify testing. This won't work in production. @@ -1159,15 +1362,18 @@ 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; + if (state instanceof TenantProjectState) { + assert(decoded?.payload.tenant_id === state.tenantId, "TENANT_ID_MISMATCH"); + } assert(decoded, "INVALID_CUSTOM_TOKEN : Invalid assertion format"); if (decoded.header.alg !== "none") { // We may have received a real token, signed using a service account private @@ -1176,13 +1382,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. @@ -1193,45 +1399,46 @@ function signInWithCustomToken( const localId = coercePrimitiveToString(payload.uid) ?? coercePrimitiveToString(payload.user_id); assert(localId, "MISSING_IDENTIFIER"); - let claims: Record = {}; + let extraClaims: Record = {}; if ("claims" in payload) { validateCustomClaims(payload.claims); - claims = payload.claims; + extraClaims = payload.claims; } let user = state.getUserByLocalId(localId); 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, }; if (user) { 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}`); } } - if (user.mfaInfo) { - throw new NotImplementedError("MFA Login not yet implemented."); - } - return { kind: "identitytoolkit#VerifyCustomTokenResponse", isNewUser, - ...issueTokens(state, user, PROVIDER_CUSTOM, claims), + ...issueTokens(state, user, PROVIDER_CUSTOM, { extraClaims }), }; } -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"); const userFromIdToken = reqBody.idToken ? parseIdToken(state, reqBody.idToken).user : undefined; assert(reqBody.email, "MISSING_EMAIL"); const email = canonicalizeEmailAddress(reqBody.email); @@ -1240,52 +1447,97 @@ 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 = { + 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, - lastLoginAt: Date.now().toString(), }; - let user = state.getUserByEmail(email); - const isNewUser = !user && !userFromIdToken; + if (state instanceof TenantProjectState) { + updates.tenantId = state.tenantId; + } + + 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"); - user = state.updateUserByLocalId(user.localId, updates); - } + if (userFromIdToken && userFromEmail) { + assert(userFromIdToken.localId === userFromEmail.localId, "EMAIL_EXISTS"); + } - if (user.mfaInfo) { - throw new NotImplementedError("MFA Login not yet implemented."); + 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); } - const tokens = issueTokens(state, user, PROVIDER_PASSWORD); - return { + const response = { kind: "identitytoolkit#EmailLinkSigninResponse", email, localId: user.localId, isNewUser, - ...tokens, }; + + // 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, { 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"); + if (reqBody.returnRefreshToken) { throw new NotImplementedError("returnRefreshToken is not implemented yet."); } @@ -1297,7 +1549,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; @@ -1307,26 +1559,46 @@ 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).", ); } } - let { response, rawId } = fakeFetchUserInfoFromIdp(providerId, claims); + // Generic SAML flow + let samlResponse: SamlResponse | undefined; + let signInAttributes = undefined; + if (normalizedUri.searchParams.get("SAMLResponse")) { + // Auth emulator purposefully does not parse SAML and expects SAML-related + // fields to be JSON objects. + samlResponse = JSON.parse(normalizedUri.searchParams.get("SAMLResponse")!) as SamlResponse; + signInAttributes = samlResponse.assertion?.attributeStatements; + + assert(samlResponse.assertion, "INVALID_IDP_RESPONSE ((Missing assertion in SAMLResponse.))"); + assert( + samlResponse.assertion.subject, + "INVALID_IDP_RESPONSE ((Missing assertion.subject in SAMLResponse.))", + ); + assert( + samlResponse.assertion.subject.nameId, + "INVALID_IDP_RESPONSE ((Missing assertion.subject.nameId in SAMLResponse.))", + ); + } + + let { response, rawId } = fakeFetchUserInfoFromIdp(providerId, claims, samlResponse); // Always return an access token, so that clients depending on it sorta work. // e.g. JS SDK creates credentials from accessTokens for most providers: @@ -1350,15 +1622,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; @@ -1384,85 +1656,182 @@ 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, - lastLoginAt: Date.now().toString(), - }, - { - upsertProviders: [providerUserInfo], - } - ); - } - if (user.mfaInfo) { - throw new NotImplementedError("MFA Login not yet implemented."); + 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) { response.emailVerified = user.emailVerified; } - Object.assign(response, issueTokens(state, user, providerId)); - return response; + + if (state instanceof TenantProjectState) { + response.tenantId = state.tenantId; + } + + if (isMfaEnabled(state, user)) { + return { ...response, ...mfaPending(state, user, providerId) }; + } else { + user = state.updateUserByLocalId(user.localId, { lastLoginAt: Date.now().toString() }); + // 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"] { - assert(reqBody.email, "MISSING_EMAIL"); + reqBody: Schemas["GoogleCloudIdentitytoolkitV1SignInWithPasswordRequest"], +): Promise { + assert(!state.disableAuth, "PROJECT_DISABLED"); + assert(state.allowPasswordSignup, "PASSWORD_LOGIN_DISABLED"); + 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); - const 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"); + let user = state.getUserByEmail(email); - if (user.mfaInfo) { - throw new NotImplementedError("MFA Login not yet implemented."); + 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 tokens = issueTokens(state, user, PROVIDER_PASSWORD); - - return { + const response = { kind: "identitytoolkit#VerifyPasswordResponse", registered: true, localId: user.localId, email, - - displayName: user.displayName, - profilePicture: user.photoUrl, - - ...tokens, }; + + if (isMfaEnabled(state, user)) { + return { ...response, ...mfaPending(state, user, PROVIDER_PASSWORD) }; + } else { + 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"); let phoneNumber: string; if (reqBody.temporaryProof) { assert(reqBody.phoneNumber, "MISSING_PHONE_NUMBER"); @@ -1476,42 +1845,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) { - user = state.updateUserByLocalId(userFromIdToken.localId, updates); - } else { - isNewUser = true; - 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: "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" }, + ); + 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); } - if (user.mfaInfo) { - throw new NotImplementedError("MFA Login not yet implemented."); - } + // 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); + const tokens = issueTokens(state, user, PROVIDER_PHONE, { + extraClaims, + }); return { isNewUser, @@ -1524,7 +1934,7 @@ 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. @@ -1533,16 +1943,12 @@ function grantToken( 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, - refreshTokenRecord.extraClaims - ); + 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, @@ -1553,7 +1959,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 */ }; } @@ -1567,17 +1972,29 @@ function getEmulatorProjectConfig(state: ProjectState): Schemas["EmulatorV1Proje signIn: { allowDuplicateEmails: !state.oneAccountPerEmail, }, + 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) { - state.oneAccountPerEmail = !allowDuplicateEmails; + // 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); } @@ -1588,77 +2005,383 @@ function listOobCodesInProject(state: ProjectState): Schemas["EmulatorV1Projects } function listVerificationCodesInProject( - state: ProjectState + state: ProjectState, ): Schemas["EmulatorV1ProjectsVerificationCodes"] { return { verificationCodes: [...state.listVerificationCodes()], }; } -export type AuthOperation = ( +function mfaEnrollmentStart( state: ProjectState, - reqBody: object, - ctx: ExegesisContext -) => Promise | object; + 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.", + ); + assert(reqBody.idToken, "MISSING_ID_TOKEN"); -export type AuthOps = { - [key: string]: AuthOps | AuthOperation; -}; + 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.", + ); + assert( + user.emailVerified, + "UNVERIFIED_EMAIL : Need to verify email first before enrolling second factors.", + ); -function coercePrimitiveToString(value: unknown): string | undefined { - switch (typeof value) { - case "string": - return value; - case "number": - case "boolean": - return value.toString(); - default: - return undefined; - } -} + assert(reqBody.phoneEnrollmentInfo, "INVALID_ARGUMENT : ((Missing phoneEnrollmentInfo.))"); + // recaptchaToken, safetyNetToken, iosReceipt, and iosSecret are intentionally + // ignored because the emulator doesn't implement anti-abuse features. + // autoRetrievalInfo is ignored because SMS will not actually be sent. -function redactPasswordHash(user: T): T { - // In production, salt will be removed and passwordHash will be set to - // "UkVEQUNURUQ=" (i.e. "REDACTED" in base64), unless exporting users. - // The emulator does NOT do that, allowing easier inspection (e.g. in tests). - // Developers should not put real secrets in the Auth Emulator anyway. - return user; -} + const phoneNumber = reqBody.phoneEnrollmentInfo.phoneNumber; -function hashPassword(password: string, salt: string): string { - // We don't actually hash passwords because this is an emulator. - // Secrets should not be entered at all here and let's not give - // people a fake sense of security. - return `fakeHash:salt=${salt}:password=${password}`; -} + // Production Firebase Auth service also throws INVALID_PHONE_NUMBER instead + // of MISSING_XXXX when phoneNumber is missing. Matching the behavior here. + 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.", + ); -function issueTokens( - state: ProjectState, - user: UserInfo, - signInProvider: string, - extraClaims: Record = {} -): { idToken: string; refreshToken: string; expiresIn: string } { - state.updateUserByLocalId(user.localId, { lastRefreshAt: new Date().toISOString() }); + const { sessionInfo, code } = state.createVerificationCode(phoneNumber); + + // Print out a developer-friendly log containing the link, in lieu of sending + // a real text message out to the phone number. + EmulatorLogger.forEmulator(Emulators.AUTH).log( + "BULLET", + `To enroll MFA with ${phoneNumber}, use the code ${code}.`, + ); - const expiresInSeconds = 60 * 60; - const idToken = generateJwt(state.projectId, user, signInProvider, expiresInSeconds, extraClaims); - const refreshToken = state.createRefreshTokenFor(user, signInProvider, extraClaims); return { - idToken, - refreshToken, + phoneSessionInfo: { + sessionInfo, + }, + }; +} + +function mfaEnrollmentFinalize( + state: ProjectState, + 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.", + ); + 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.", + ); + assert(reqBody.phoneVerificationInfo, "INVALID_ARGUMENT : ((Missing phoneVerificationInfo.))"); + + if (reqBody.phoneVerificationInfo.androidVerificationProof) { + throw new NotImplementedError("androidVerificationProof is unsupported!"); + } + const { code, sessionInfo } = reqBody.phoneVerificationInfo; + + assert(code, "MISSING_CODE"); + assert(sessionInfo, "MISSING_SESSION_INFO"); + + 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.", + ); + + const existingFactors = user.mfaInfo || []; + const existingIds = new Set(); + for (const { mfaEnrollmentId } of existingFactors) { + if (mfaEnrollmentId) { + existingIds.add(mfaEnrollmentId); + } + } + const enrollment = { + displayName: reqBody.displayName, + enrolledAt: new Date().toISOString(), + mfaEnrollmentId: newRandomId(28, existingIds), + phoneInfo: phoneNumber, + unobfuscatedPhoneInfo: phoneNumber, + }; + user = state.updateUserByLocalId(user.localId, { + mfaInfo: [...existingFactors, enrollment], + }); + + // TODO: Generate OOB code for reverting enrollment. + + const { idToken, refreshToken } = issueTokens(state, user, signInProvider, { + secondFactor: { identifier: enrollment.mfaEnrollmentId, provider: PROVIDER_PHONE }, + }); + + return { + idToken, + refreshToken, + }; +} + +function mfaEnrollmentWithdraw( + state: ProjectState, + reqBody: Schemas["GoogleCloudIdentitytoolkitV2WithdrawMfaRequest"], +): Schemas["GoogleCloudIdentitytoolkitV2WithdrawMfaResponse"] { + assert(!state.disableAuth, "PROJECT_DISABLED"); + assert(reqBody.idToken, "MISSING_ID_TOKEN"); + + let { user, signInProvider } = parseIdToken(state, reqBody.idToken); + assert(user.mfaInfo, "MFA_ENROLLMENT_NOT_FOUND"); + + const updatedList = user.mfaInfo.filter( + (enrollment) => enrollment.mfaEnrollmentId !== reqBody.mfaEnrollmentId, + ); + assert(updatedList.length < user.mfaInfo.length, "MFA_ENROLLMENT_NOT_FOUND"); + + user = state.updateUserByLocalId(user.localId, { mfaInfo: updatedList }); + + return { + ...issueTokens(state, user, signInProvider), + }; +} + +function mfaSignInStart( + state: ProjectState, + 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.", + ); + assert( + reqBody.mfaPendingCredential, + "MISSING_MFA_PENDING_CREDENTIAL : Request does not have MFA pending credential.", + ); + assert( + reqBody.mfaEnrollmentId, + "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 + // in the emulator. e.g. recaptchaToken/safetyNetToken doesn't make sense; + const { user } = parsePendingCredential(state, reqBody.mfaPendingCredential); + + const enrollment = user.mfaInfo?.find( + (factor) => factor.mfaEnrollmentId === reqBody.mfaEnrollmentId, + ); + assert(enrollment, "MFA_ENROLLMENT_NOT_FOUND"); + const phoneNumber = enrollment.unobfuscatedPhoneInfo; + assert(phoneNumber, "INVALID_ARGUMENT : MFA provider not supported!"); + + const { sessionInfo, code } = state.createVerificationCode(phoneNumber); + + // Print out a developer-friendly log containing the link, in lieu of sending + // 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}.`, + ); + + return { + phoneResponseInfo: { + sessionInfo, + }, + }; +} + +async function mfaSignInFinalize( + state: ProjectState, + 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.", + ); + // Inconsistent with mfaSignInStart (where MISSING_MFA_PENDING_CREDENTIAL is + // returned), but matches production behavior. + assert(reqBody.mfaPendingCredential, "MISSING_CREDENTIAL : Please set MFA Pending Credential."); + assert(reqBody.phoneVerificationInfo, "INVALID_ARGUMENT : MFA provider not supported!"); + + if (reqBody.phoneVerificationInfo.androidVerificationProof) { + throw new NotImplementedError("androidVerificationProof is unsupported!"); + } + const { code, sessionInfo } = reqBody.phoneVerificationInfo; + assert(code, "MISSING_CODE"); + assert(sessionInfo, "MISSING_SESSION_INFO"); + + const phoneNumber = verifyPhoneNumber(state, sessionInfo, code); + + let { user, signInProvider } = parsePendingCredential(state, reqBody.mfaPendingCredential); + const enrollment = user.mfaInfo?.find((enrollment) => { + // All but firebase-ios-sdk finalize with unobfuscated phone number. + if (enrollment.unobfuscatedPhoneInfo === phoneNumber) { + return true; + } + + // But firebase-ios-sdk finalizes with an obfuscated number. This works against + // cloud auth, so emulator should attempt to find enrollment obfuscated as well. + if ( + !!enrollment.unobfuscatedPhoneInfo && + obfuscatePhoneNumber(enrollment.unobfuscatedPhoneInfo) === phoneNumber + ) { + return true; + } + + return false; + }); + + 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 { + idToken, + refreshToken, + }; +} + +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, +) => Promise | object; + +export type AuthOps = { + [key: string]: AuthOps | AuthOperation; +}; + +function coercePrimitiveToString(value: unknown): string | undefined { + switch (typeof value) { + case "string": + return value; + case "number": + case "boolean": + return value.toString(); + default: + return undefined; + } +} + +function redactPasswordHash(user: T): T { + // In production, salt will be removed and passwordHash will be set to + // "UkVEQUNURUQ=" (i.e. "REDACTED" in base64), unless exporting users. + // The emulator does NOT do that, allowing easier inspection (e.g. in tests). + // Developers should not put real secrets in the Auth Emulator anyway. + return user; +} + +function hashPassword(password: string, salt: string): string { + // We don't actually hash passwords because this is an emulator. + // Secrets should not be entered at all here and let's not give + // people a fake sense of security. + return `fakeHash:salt=${salt}:password=${password}`; +} + +function issueTokens( + state: ProjectState, + user: UserInfo, + signInProvider: string, + { + extraClaims, + secondFactor, + signInAttributes, + }: { + extraClaims?: Record; + secondFactor?: SecondFactorRecord; + signInAttributes?: unknown; + } = {}, +): { idToken: string; refreshToken?: string; expiresIn: string } { + user = state.updateUserByLocalId(user.localId, { lastRefreshAt: new Date().toISOString() }); + + const tenantId = state instanceof TenantProjectState ? state.tenantId : undefined; + + const expiresInSeconds = 60 * 60; + const idToken = generateJwt(user, { + projectId: state.projectId, + signInProvider, + expiresInSeconds, + extraClaims, + secondFactor, + tenantId, + signInAttributes, + }); + const refreshToken = state.createRefreshTokenFor(user, signInProvider, { + extraClaims, + secondFactor, + }); + return { + idToken, + refreshToken, expiresIn: expiresInSeconds.toString(), // String typed in API spec. }; } function parseIdToken( state: ProjectState, - idToken: string + idToken: string, ): { user: UserInfo; payload: FirebaseJwtPayload; signInProvider: string; } { - const decoded = decodeJwt(idToken, { complete: true }) as { + const decoded = decodeJwt(idToken, { complete: true }) as unknown as { header: JwtHeader; payload: FirebaseJwtPayload; } | null; @@ -1671,12 +2394,20 @@ 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", ); } - // TODO: Check JWT expiration here. + if (decoded.payload.firebase.tenant) { + assert( + state instanceof TenantProjectState, + "((Parsed token that belongs to tenant in a non-tenant project.))", + ); + assert(decoded.payload.firebase.tenant === state.tenantId, "TENANT_ID_MISMATCH"); + } const user = state.getUserByLocalId(decoded.payload.user_id); assert(user, "USER_NOT_FOUND"); + // To make interactive debugging easier, idTokens in the emulator never expire + // due to the passage of time (exp unchecked) but they may still be _revoked_: assert(!user.validSince || decoded.payload.iat >= Number(user.validSince), "TOKEN_EXPIRED"); assert(!user.disabled, "USER_DISABLED"); @@ -1685,11 +2416,24 @@ function parseIdToken( } function generateJwt( - projectId: string, user: UserInfo, - signInProvider: string, - expiresInSeconds: number, - extraClaims: Record = {} + { + projectId, + signInProvider, + expiresInSeconds, + extraClaims = {}, + secondFactor, + tenantId, + signInAttributes, + }: { + projectId: string; + signInProvider: string; + expiresInSeconds: number; + extraClaims?: Record; + secondFactor?: SecondFactorRecord; + tenantId?: string; + signInAttributes?: unknown; + }, ): string { const identities: Record = {}; if (user.email) { @@ -1709,9 +2453,8 @@ function generateJwt( } } - const customAttributes = JSON.parse(user.customAttributes || "{}"); - /* eslint-disable camelcase */ - const customPayloadFields: FirebaseJwtPayload = { + const customAttributes = JSON.parse(user.customAttributes || "{}") as Record; + const customPayloadFields: Partial = { // Non-reserved fields (set before custom attributes): name: user.displayName, picture: user.photoUrl, @@ -1726,30 +2469,62 @@ function generateJwt( // This field is only set for anonymous sign-in but not for any other // provider (such as email or Google) in production. Let's match that. provider_id: signInProvider === "anonymous" ? signInProvider : undefined, - auth_time: toUnixTimestamp(new Date()), + auth_time: toUnixTimestamp(getAuthTime(user)), user_id: user.localId, firebase: { identities, sign_in_provider: signInProvider, + second_factor_identifier: secondFactor?.identifier, + sign_in_second_factor: secondFactor?.provider, + 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; } +function getAuthTime(user: UserInfo): Date { + if (user.lastLoginAt != null) { + const millisSinceEpoch = parseInt(user.lastLoginAt, 10); + const authTime = new Date(millisSinceEpoch); + if (isNaN(authTime.getTime())) { + throw new Error(`Internal assertion error: invalid user.lastLoginAt = ${user.lastLoginAt}`); + } + return authTime; + } else if (user.lastRefreshAt != null) { + 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}`, + ); + } + return authTime; + } else { + throw new Error(`Internal assertion error: Missing user.lastLoginAt and user.lastRefreshAt`); + } +} + function verifyPhoneNumber(state: ProjectState, sessionInfo: string, code: string): string { const verification = state.getVerificationCodeBySessionInfo(sessionInfo); assert(verification, "INVALID_SESSION_INFO"); @@ -1814,14 +2589,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(); @@ -1829,7 +2604,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 @@ -1837,7 +2612,11 @@ function getMfaEnrollmentsFromRequest( : enrollment.mfaEnrollmentId; assert(mfaEnrollmentId, "INVALID_MFA_ENROLLMENT_ID : mfaEnrollmentId must be defined."); assert(!enrollmentIds.has(mfaEnrollmentId), "DUPLICATE_MFA_ENROLLMENT_ID"); - enrollments.push({ ...enrollment, mfaEnrollmentId }); + enrollments.push({ + ...enrollment, + mfaEnrollmentId, + unobfuscatedPhoneInfo: enrollment.phoneInfo, + }); phoneNumbers.add(enrollment.phoneInfo); enrollmentIds.add(mfaEnrollmentId); } @@ -1880,11 +2659,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; } @@ -1893,18 +2672,19 @@ 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; } function fakeFetchUserInfoFromIdp( providerId: string, - claims: IdpJwtPayload + claims: IdpJwtPayload, + samlResponse?: SamlResponse, ): { response: SignInWithIdpResponse; rawId: string; @@ -1930,19 +2710,18 @@ function fakeFetchUserInfoFromIdp( photoUrl, }; - let federatedId: string; - /* eslint-disable camelcase */ + let federatedId = rawId; 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, @@ -1954,8 +2733,14 @@ function fakeFetchUserInfoFromIdp( }); break; } + case providerId.match(/^saml\./)?.input: + const nameId = samlResponse?.assertion?.subject?.nameId; + response.email = nameId && isValidEmailAddress(nameId) ? nameId : response.email; + response.emailVerified = true; + response.rawUserInfo = JSON.stringify(samlResponse?.assertion?.attributeStatements); + break; + case providerId.match(/^oidc\./)?.input: default: - federatedId = rawId; response.rawUserInfo = JSON.stringify(claims); break; } @@ -1973,7 +2758,7 @@ interface AccountUpdates { function handleLinkIdp( state: ProjectState, response: SignInWithIdpResponse, - userFromIdToken: UserInfo + userFromIdToken: UserInfo, ): { response: SignInWithIdpResponse; accountUpdates: AccountUpdates; @@ -1982,7 +2767,7 @@ function handleLinkIdp( const userMatchingEmail = state.getUserByEmail(response.email); assert( !userMatchingEmail || userMatchingEmail.localId === userFromIdToken.localId, - "EMAIL_EXISTS" + "EMAIL_EXISTS", ); } response.localId = userFromIdToken.localId; @@ -2003,7 +2788,7 @@ function handleLinkIdp( function handleIdpSigninEmailNotRequired( response: SignInWithIdpResponse, - userMatchingProvider: UserInfo | undefined + userMatchingProvider: UserInfo | undefined, ): { response: SignInWithIdpResponse; accountUpdates: AccountUpdates; @@ -2023,7 +2808,7 @@ function handleIdpSigninEmailRequired( response: SignInWithIdpResponse, rawId: string, userMatchingProvider: UserInfo | undefined, - userMatchingEmail: UserInfo | undefined + userMatchingEmail: UserInfo | undefined, ): { response: SignInWithIdpResponse; accountUpdates: AccountUpdates; @@ -2038,7 +2823,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, @@ -2058,7 +2843,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, ); } @@ -2086,7 +2871,7 @@ function handleIdpSigninEmailRequired( function handleIdpSignUp( response: SignInWithIdpResponse, - options: { emailRequired: boolean } + options: { emailRequired: boolean }, ): { response: SignInWithIdpResponse; accountUpdates: AccountUpdates; @@ -2115,7 +2900,476 @@ function handleIdpSignUp( }; } -/* eslint-disable camelcase */ +type MfaEnrollment = Schemas["GoogleCloudIdentitytoolkitV1MfaEnrollment"]; + +interface MfaPendingCredential { + _AuthEmulatorMfaPendingCredential: string; + localId: string; + signInProvider: string; + projectId: string; + tenantId?: string; + // MfaPendingCredential in emulator never expire to make interactive debugging + // a bit easier. Therefore, there's no need to record createdAt timestamps. +} + +function mfaPending( + state: ProjectState, + user: UserInfo, + signInProvider: string, +): { mfaPendingCredential: string; mfaInfo: MfaEnrollment[] } { + if (!user.mfaInfo) { + throw new Error("Internal assertion error: mfaPending called on user without MFA."); + } + const pendingCredentialPayload: MfaPendingCredential = { + _AuthEmulatorMfaPendingCredential: "DO NOT MODIFY", + localId: user.localId, + signInProvider, + projectId: state.projectId, + }; + if (state instanceof TenantProjectState) { + pendingCredentialPayload.tenantId = state.tenantId; + } + + // Encode pendingCredentialPayload using base64. We don't encrypt or sign the + // data in the Auth Emulator but just trust developers not to modify it. + const mfaPendingCredential = Buffer.from( + JSON.stringify(pendingCredentialPayload), + "utf8", + ).toString("base64"); + + return { mfaPendingCredential, mfaInfo: user.mfaInfo.map(redactMfaInfo) }; +} + +function redactMfaInfo(mfaInfo: MfaEnrollment): MfaEnrollment { + return { + displayName: mfaInfo.displayName, + enrolledAt: mfaInfo.enrolledAt, + mfaEnrollmentId: mfaInfo.mfaEnrollmentId, + phoneInfo: mfaInfo.unobfuscatedPhoneInfo + ? obfuscatePhoneNumber(mfaInfo.unobfuscatedPhoneInfo) + : undefined, + }; +} + +// Create an obfuscated version of a phone number, where all but the last +// four digits are replaced by the character "*". +function obfuscatePhoneNumber(phoneNumber: string): string { + const split = phoneNumber.split(""); + let digitsEncountered = 0; + for (let i = split.length - 1; i >= 0; i--) { + if (/[0-9]/.test(split[i])) { + digitsEncountered++; + if (digitsEncountered > 4) { + split[i] = "*"; + } + } + } + return split.join(""); +} + +function parsePendingCredential( + state: ProjectState, + pendingCredential: string, +): { + user: UserInfo; + signInProvider: string; +} { + let pendingCredentialPayload: MfaPendingCredential; + try { + const json = Buffer.from(pendingCredential, "base64").toString("utf8"); + pendingCredentialPayload = JSON.parse(json) as MfaPendingCredential; + } catch { + assert(false, "((Invalid phoneVerificationInfo.mfaPendingCredential.))"); + } + assert( + pendingCredentialPayload._AuthEmulatorMfaPendingCredential, + "((Invalid phoneVerificationInfo.mfaPendingCredential.))", + ); + assert( + pendingCredentialPayload.projectId === state.projectId, + "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.", + ); + } + + const { localId, signInProvider } = pendingCredentialPayload; + const user = state.getUserByLocalId(localId); + assert(user, "((User in pendingCredentialPayload does not exist.))"); + + return { user, signInProvider }; +} + +function createTenant( + state: ProjectState, + reqBody: Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"], +): Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"] { + if (!(state instanceof AgentProjectState)) { + throw new InternalError("INTERNAL_ERROR : Can only create tenant in agent project", "INTERNAL"); + } + + const mfaConfig = reqBody.mfaConfig ?? {}; + if (!("state" in mfaConfig)) { + mfaConfig.state = "DISABLED"; + } + if (!("enabledProviders" in mfaConfig)) { + mfaConfig.enabledProviders = []; + } + + // Default to production settings if unset + const tenant = { + displayName: reqBody.displayName, + allowPasswordSignup: reqBody.allowPasswordSignup ?? false, + enableEmailLinkSignin: reqBody.enableEmailLinkSignin ?? false, + enableAnonymousUser: reqBody.enableAnonymousUser ?? false, + disableAuth: reqBody.disableAuth ?? false, + mfaConfig: mfaConfig as MfaConfig, + tenantId: "", // Placeholder until one is generated + }; + + return state.createTenant(tenant); +} + +function listTenants( + state: ProjectState, + reqBody: unknown, + 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); + const tenants = state.listTenants(ctx.params.query.pageToken); + + // As a non-standard behavior, passing in negative pageSize will + // return all users starting from the pageToken. + let nextPageToken: string | undefined = undefined; + if (pageSize > 0 && tenants.length >= pageSize) { + tenants.length = pageSize; + nextPageToken = tenants[tenants.length - 1].tenantId; + } + + return { + nextPageToken, + tenants, + }; +} + +function deleteTenant(state: ProjectState): Schemas["GoogleProtobufEmpty"] { + assert(state instanceof TenantProjectState, "((Can only delete tenant on tenant projects.))"); + state.delete(); + return {}; +} + +function getTenant(state: ProjectState): Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"] { + assert(state instanceof TenantProjectState, "((Can only get tenant on tenant projects.))"); + return state.tenantConfig; +} + +function updateTenant( + state: ProjectState, + reqBody: Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"], + 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; + }; + attributeStatements?: unknown; +} + +export interface SamlResponse { + assertion?: SamlAssertion; +} + export interface FirebaseJwtPayload { // Standard fields: iat: number; // issuedAt (in seconds since epoch) @@ -2125,8 +3379,14 @@ export interface FirebaseJwtPayload { // ...and other fields that we don't care for now. // Firebase-specific fields: + + // the last login time (in seconds since epoch), may be different from iat + auth_time: number; email?: string; + email_verified?: boolean; phone_number?: string; + name?: string; + picture?: string; user_id: string; provider_id?: string; firebase: { @@ -2135,6 +3395,10 @@ export interface FirebaseJwtPayload { phone?: string[]; }; sign_in_provider: string; + sign_in_second_factor?: string; + second_factor_identifier?: string; + tenant?: string; + sign_in_attributes?: unknown; }; // ...and other fields that we don't care for now. } @@ -2156,7 +3420,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 @@ -2234,4 +3498,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..94a27e276ab --- /dev/null +++ b/src/emulator/auth/password.spec.ts @@ -0,0 +1,400 @@ +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); + + const res = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: user.email, password: user.password }); + + 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); + + const res = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: user.email, password: user.password }); + 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); + + const res = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: "AlIcE@exAMPle.COM", password: user.password }); + expectStatusCode(200, res); + expect(res.body.localId).equals(localId); + }); + + it("should error if email or password is missing", async () => { + const noEmailRes = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ /* no email */ password: "notasecret" }); + expectStatusCode(400, noEmailRes); + expect(noEmailRes.body.error.message).equals("MISSING_EMAIL"); + + const noPasswordRes = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: "nosuchuser@example.com" /* no password */ }); + expectStatusCode(400, noPasswordRes); + expect(noPasswordRes.body.error.message).equals("MISSING_PASSWORD"); + }); + + it("should error if email is invalid", async () => { + const invalidEmailRes = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: "ill-formatted-email", password: "notasecret" }); + expectStatusCode(400, invalidEmailRes); + expect(invalidEmailRes.body.error.message).equals("INVALID_EMAIL"); + + const emptyEmailRes = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: "", password: "notasecret" }); + expectStatusCode(400, emptyEmailRes); + expect(emptyEmailRes.body.error.message).equals("INVALID_EMAIL"); + }); + + it("should error if email is not found", async () => { + const res = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: "nosuchuser@example.com", password: "notasecret" }); + 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", + ); + + const res = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: "nosuchuser@example.com", password: "notasecret" }); + 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); + const res = 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" }); + 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); + const res = 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" }); + 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 }); + + const res = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: user.email, password: "notasecret" }); + 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); + + const res = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: user.email, password: user.password }); + 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 }); + + const res = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }); + 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, + }); + + const res = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId }); + 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); + + const res = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ tenantId: tenant.tenantId, email: user.email, password: user.password }); + 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" }, + }, + }); + + const res = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: user.email, password: user.password }); + + 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, + }, + }); + + const res = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: user.email, password: user.password }); + 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, + }, + }); + + const res = await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .query({ key: "fake-api-key" }) + .send({ email: user.email, password: user.password }); + 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..48050035367 --- /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"); + }); + }); + }); +}).timeout(5000); diff --git a/src/emulator/auth/rest.spec.ts b/src/emulator/auth/rest.spec.ts new file mode 100644 index 00000000000..5ad1e3b9a07 --- /dev/null +++ b/src/emulator/auth/rest.spec.ts @@ -0,0 +1,257 @@ +import { expect } from "chai"; +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 () => { + await authApi() + .get("/") + .then((res) => { + expectStatusCode(200, res); + expect(res.body.authEmulator).to.be.an("object"); + }); + }); + + it("should allow cross-origin requests", async () => { + await 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); + + // Some clients (including older browsers and jsdom) won't accept '*' as a + // wildcard, so we need to reflect Origin and Access-Control-Request-Headers. + // https://github.com/firebase/firebase-tools/issues/3200 + expect(res.header["access-control-allow-origin"]).to.eql("example.com"); + expect((res.header["access-control-allow-headers"] as string).split(",")).to.have.members([ + "Authorization", + "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"); + }); + }); + + it("should handle integer values for enums", async () => { + // Proto integer value for "EMAIL_SIGNIN". Android client SDK sends this. + const requestType = 6; + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .set("Authorization", "Bearer owner") + .send({ email: "bob@example.com", requestType, returnOobLink: true }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.oobLink).to.include("mode=signIn"); + }); + }); + + it("should handle integer values for enums (legacy API path)", async () => { + // Proto integer value for "EMAIL_SIGNIN". Android client SDK sends this. + const requestType = 6; + await authApi() + .post("/www.googleapis.com/identitytoolkit/v3/relyingparty/getOobConfirmationCode") + .set("Authorization", "Bearer owner") + .send({ email: "bob@example.com", requestType, returnOobLink: true }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.oobLink).to.include("mode=signIn"); + }); + }); + + it("should convert numbers to strings for type:string fields", async () => { + // validSince should be an int64-formatted string, but Node.js Admin SDK + // sends it as a plain number (without quotes). + const validSince = 1611780718; + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:update") + .set("Authorization", "Bearer owner") + .send({ localId: "nosuch", validSince }) + .then((res) => { + expectStatusCode(400, res); + // It should pass JSON schema validation and get into handler logic. + expect(res.body.error.message).to.equal("USER_NOT_FOUND"); + }); + }); +}); + +describeAuthEmulator("authentication", ({ authApi }) => { + it("should throw 403 if API key is not provided", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .query({ + /* no API "key" */ + }) + .send({ returnSecureToken: true }) + .then((res) => { + expectStatusCode(403, res); + expect(res.body.error).to.have.property("status").equal("PERMISSION_DENIED"); + }); + }); + + 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") + // This has no effect on the request handling, since it is not Bearer. + .set("Authorization", "Basic YWxhZGRpbjpvcGVuc2VzYW1l") + .query({ + /* no API "key" */ + }) + .send({ returnSecureToken: true }) + .then((res) => { + expectStatusCode(403, res); + expect(res.body.error).to.have.property("status").equal("PERMISSION_DENIED"); + }); + }); + + it("should treat Bearer owner as authenticated to project", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + // This authenticates as valid OAuth 2 credentials, no API key needed. + .set("Authorization", "Bearer owner") + .send({ + // This field requires OAuth 2 and should work correctly. + targetProjectId: "example2", + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).not.to.have.property("error"); + }); + }); + + it("should ignore casing of Bearer / owner in Authorization header", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + // This authenticates as valid OAuth 2 credentials, no API key needed. + .set("Authorization", "bEArEr OWNER") + .send({ + // This field requires OAuth 2 and should work correctly. + targetProjectId: "example2", + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).not.to.have.property("error"); + }); + }); + + it("should treat production service account as authenticated to project", async () => { + await 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", + // 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", + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).not.to.have.property("error"); + }); + }); + + it("should deny requests with targetProjectId but without OAuth 2", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .query({ key: "fake-api-key" }) + .send({ + // Specifying this field requires OAuth 2. API key is not sufficient. + targetProjectId: "example2", + }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error) + .to.have.property("message") + .equals( + "INSUFFICIENT_PERMISSION : Only authenticated requests can specify target_project_id.", + ); + }); + }); + + 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", + ) + .set("Authorization", "Bearer owner") + .send({ localId: "local-id", tenantId: "mismatching-tenant-id" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("TENANT_ID_MISMATCH"); + }); + }); + + 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 } = await registerUser(authApi(), { + email: "alice@example.com", + password: "notasecret", + tenantId: tenant.tenantId, + }); + + await authApi() + .post( + `/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/tenants/not-matching-tenant-id/accounts:lookup`, + ) + .send({ idToken }) + .set("Authorization", "Bearer owner") + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("TENANT_ID_MISMATCH"); + }); + }); + + 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 } = await registerUser(authApi(), { + email: "alice@example.com", + password: "notasecret", + tenantId: tenant.tenantId, + }); + + await authApi() + .post(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:lookup`) + .send({ idToken, tenantId: "not-matching-tenant-id" }) + .set("Authorization", "Bearer owner") + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("TENANT_ID_MISMATCH"); + }); + }); +}); diff --git a/src/emulator/auth/schema.ts b/src/emulator/auth/schema.ts index dd67856b33c..489a3e5f141 100644 --- a/src/emulator/auth/schema.ts +++ b/src/emulator/auth/schema.ts @@ -3,383 +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 information required to auto-retrieve an SMS. - */ - GoogleCloudIdentitytoolkitV1AutoRetrievalInfo: { + /** @description The parameters for Argon2 hashing algorithm. */ + GoogleCloudIdentitytoolkitV1Argon2Parameters: { /** - * The Android app's signature hash for Google Play Service's SMS Retriever API. + * 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. */ - appSignatureHash?: string; - }; - /** - * Request message for BatchDeleteAccounts. - */ - GoogleCloudIdentitytoolkitV1BatchDeleteAccountsRequest: { + associatedData?: string; /** - * 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`. + * Format: int32 + * @description Required. The desired hash length in bytes. Minimum is 4 and maximum is 1024. */ - force?: boolean; + hashLengthBytes?: number; + /** @description Required. Must not be HASH_TYPE_UNSPECIFIED. */ + hashType?: "HASH_TYPE_UNSPECIFIED" | "ARGON2_D" | "ARGON2_ID" | "ARGON2_I"; /** - * Required. List of user IDs to be deleted. + * Format: int32 + * @description Required. The number of iterations to perform. Minimum is 1, maximum is 16. */ - localIds?: string[]; + iterations?: number; /** - * 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. + * Format: int32 + * @description Required. The memory cost in kibibytes. Maximum is 32768. */ + memoryCostKib?: number; + /** + * Format: int32 + * @description Required. The degree of parallelism, also called threads or lanes. Minimum is 1, maximum is 16. + */ + parallelism?: number; + /** @description The version of the Argon2 algorithm. This defaults to VERSION_13 if not specified. */ + version?: "VERSION_UNSPECIFIED" | "VERSION_10" | "VERSION_13"; + }; + /** @description The information required to auto-retrieve an SMS. */ + GoogleCloudIdentitytoolkitV1AutoRetrievalInfo: { + /** @description The Android app's signature hash for Google Play Service's SMS Retriever API. */ + appSignatureHash?: string; + }; + /** @description Request message for BatchDeleteAccounts. */ + GoogleCloudIdentitytoolkitV1BatchDeleteAccountsRequest: { + /** @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; + /** @description Required. List of user IDs to be deleted. */ + localIds?: string[]; + /** @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" @@ -390,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" @@ -519,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" @@ -722,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; @@ -787,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" @@ -856,1036 +2514,893 @@ 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 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 - */ + /** @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; }; - /** - * Additional config for SignInWithApple. - */ + /** @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: { + /** @description Whether anonymous user auth is enabled for the project or not. */ + enabled?: boolean; + }; + /** @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"]; }; - /** - * Additional config for Apple for code flow. - */ + /** @description Configuration related to Blocking Functions. */ + GoogleCloudIdentitytoolkitAdminV2BlockingFunctionsConfig: { + forwardInboundCredentials?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2ForwardInboundCredentials"]; + /** @description Map of Trigger to event type. Key should be one of the supported event types: "beforeCreate", "beforeSignIn" */ + triggers?: { + [key: string]: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Trigger"]; + }; + }; + /** @description Options related to how clients making requests on behalf of a project should be configured. */ + GoogleCloudIdentitytoolkitAdminV2ClientConfig: { + /** @description Output only. API key that can be used when making requests for this project. */ + apiKey?: string; + /** @description Output only. Firebase subdomain. */ + firebaseSubdomain?: string; + permissions?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2Permissions"]; + }; + /** @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; }; - /** - * Standard Identity Toolkit-trusted IDPs. - */ + /** @description Represents an Identity Toolkit project. */ + GoogleCloudIdentitytoolkitAdminV2Config: { + /** @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"]; + /** @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"]; + smsRegionConfig?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SmsRegionConfig"]; + /** @description Output only. The subtype of this config. */ + subtype?: "SUBTYPE_UNSPECIFIED" | "IDENTITY_PLATFORM" | "FIREBASE_AUTH"; + }; + /** @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; + /** + * Format: int32 + * @description Maximum password length. No default max length + */ + maxPasswordLength?: number; + /** + * Format: int32 + * @description Minimum password length. Range from 6 to 30 + */ + minPasswordLength?: number; + }; + /** @description Standard Identity Toolkit-trusted IDPs. */ GoogleCloudIdentitytoolkitAdminV2DefaultSupportedIdp: { - /** - * Description of the Idp - */ + /** @description Description of the Idp */ description?: string; - /** - * Id the of Idp - */ + /** @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; }; - /** - * History information of the hash algorithm and key. Different accounts' passwords may be generated by different version. - */ + /** @description Information of custom domain DNS verification. By default, default_domain will be used. A custom domain can be configured using VerifyCustomDomain. */ + GoogleCloudIdentitytoolkitAdminV2DnsInfo: { + /** @description Output only. The applied verified custom domain. */ + customDomain?: string; + /** @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" + | "IN_PROGRESS" + | "FAILED" + | "SUCCEEDED"; + /** + * Format: google-datetime + * @description Output only. The timestamp of initial request for the current domain verification. + */ + domainVerificationRequestTime?: string; + /** @description Output only. The custom domain that's to be verified. */ + pendingCustomDomain?: string; + /** @description Whether to use custom domain. */ + useCustomDomain?: boolean; + }; + /** @description Configuration options related to authenticating a user by their email address. */ + GoogleCloudIdentitytoolkitAdminV2Email: { + /** @description Whether email auth is enabled for the project or not. */ + enabled?: boolean; + /** @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; + }; + /** @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: { + /** @description Email body */ + body?: string; + /** @description Email body format */ + bodyFormat?: "BODY_FORMAT_UNSPECIFIED" | "PLAIN_TEXT" | "HTML"; + /** @description Output only. Whether the body or subject of the email is customized. */ + customized?: boolean; + /** @description Reply-to address */ + replyTo?: string; + /** @description Sender display name */ + senderDisplayName?: string; + /** @description Local part of From address */ + senderLocalPart?: string; + /** @description Subject of the email */ + subject?: string; + }; + /** @description Indicates which credentials to pass to the registered Blocking Functions. */ + GoogleCloudIdentitytoolkitAdminV2ForwardInboundCredentials: { + /** @description Whether to pass the user's OAuth identity provider's access token. */ + accessToken?: boolean; + /** @description Whether to pass the user's OIDC identity provider's ID token. */ + idToken?: boolean; + /** @description Whether to pass the user's OAuth identity provider's refresh token. */ + refreshToken?: boolean; + }; + /** @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" @@ -1902,665 +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"]; }; - /** - * Response for DefaultSupportedIdpConfigs - */ + /** @description Settings that the tenants will inherit from project level. */ + GoogleCloudIdentitytoolkitAdminV2Inheritance: { + /** @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; + }; + /** @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"][]; }; - /** - * Options related to MultiFactor Authentication for the project. - */ + /** @description Configuration related to monitoring project activity. */ + GoogleCloudIdentitytoolkitAdminV2MonitoringConfig: { + requestLogging?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2RequestLogging"]; + }; + /** @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 options for authenticating with an OAuth IDP. - */ + /** @description Configuration related to multi-tenant functionality. */ + GoogleCloudIdentitytoolkitAdminV2MultiTenantConfig: { + /** @description Whether this project can have tenants or not. */ + allowTenants?: boolean; + /** @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; + }; + /** @description Configuration related to sending notifications to users. */ + GoogleCloudIdentitytoolkitAdminV2NotificationConfig: { + /** @description Default locale used for email and SMS in IETF BCP 47 format. */ + defaultLocale?: string; + sendEmail?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SendEmail"]; + sendSms?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SendSms"]; + }; + /** @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 multiple response type to request for in the OAuth authorization flow. This can possibly be a combination of set bits (e.g. {id_token, token}). - */ + /** @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; /** - * If true, access token is returned from IdP's authorization endpoint. + * @deprecated + * @description Do not use. The `token` response type is not supported at the moment. */ token?: boolean; }; - /** - * The SP's certificate data for IDP to verify the SAMLRequest generated by the SP. - */ - GoogleCloudIdentitytoolkitAdminV2SpCertificate: { - /** - * Timestamp of the cert expiration instance. - */ - expiresAt?: string; + /** @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; + /** + * Format: google-datetime + * @description Output only. The last time the password policy on the project was updated. + */ + 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"]; /** - * Self-signed public certificate. + * Format: int32 + * @description Output only. schema version number for the password policy */ - x509Certificate?: string; + schemaVersion?: number; }; - /** - * 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: { + /** @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; + }; + /** @description Configuration options related to authenticated a user by their phone number. */ + GoogleCloudIdentitytoolkitAdminV2PhoneNumber: { + /** @description Whether phone number auth is enabled for the project or not. */ + enabled?: boolean; + /** @description A map of that can be used for phone auth testing. */ + testPhoneNumbers?: { [key: string]: string }; + }; + /** @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"]; + }; + /** @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"; /** - * Callback URI where responses from IDP are handled. + * 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: { + /** @description Whether logging is enabled for this project or not. */ + enabled?: boolean; + }; + /** @description Options for email sending. */ + GoogleCloudIdentitytoolkitAdminV2SendEmail: { + /** @description action url in email template. */ callbackUri?: string; + changeEmailTemplate?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2EmailTemplate"]; + dnsInfo?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2DnsInfo"]; + legacyResetPasswordTemplate?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2EmailTemplate"]; + /** @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"]; + }; + /** @description Options for SMS sending. */ + GoogleCloudIdentitytoolkitAdminV2SendSms: { + smsTemplate?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SmsTemplate"]; + /** @description Whether to use the accept_language header for SMS. */ + useDeviceLocale?: boolean; + }; + /** @description Configuration related to local sign in methods. */ + GoogleCloudIdentitytoolkitAdminV2SignInConfig: { + /** @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"]; + }; + /** @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: { + /** @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; + }; + /** @description Configuration for SMTP relay */ + GoogleCloudIdentitytoolkitAdminV2Smtp: { + /** @description SMTP relay host */ + host?: string; + /** @description SMTP relay password */ + password?: string; /** - * Output only. Public certificates generated by the server to verify the signature in SAMLRequest in the SP-initiated flow. + * Format: int32 + * @description SMTP relay port */ - spCertificates?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SpCertificate"][]; + port?: number; + /** @description SMTP security mode. */ + securityMode?: "SECURITY_MODE_UNSPECIFIED" | "SSL" | "START_TLS"; + /** @description Sender email for the SMTP relay */ + senderEmail?: string; + /** @description SMTP relay username */ + username?: string; + }; + /** @description The SP's certificate data for IDP to verify the SAMLRequest generated by the SP. */ + GoogleCloudIdentitytoolkitAdminV2SpCertificate: { /** - * Unique identifier for all SAML entities. + * Format: google-datetime + * @description Timestamp of the cert expiration instance. */ + expiresAt?: string; + /** @description Self-signed public certificate. */ + x509Certificate?: string; + }; + /** @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: { + /** @description Callback URI where responses from IDP are handled. */ + callbackUri?: string; + /** @description Output only. Public certificates generated by the server to verify the signature in SAMLRequest in the SP-initiated flow. */ + spCertificates?: components["schemas"]["GoogleCloudIdentitytoolkitAdminV2SpCertificate"][]; + /** @description Unique identifier for all SAML entities. */ spEntityId?: string; }; - /** - * A Tenant contains configuration for the tenant in a multi-tenant project. - */ - GoogleCloudIdentitytoolkitAdminV2Tenant: { + /** @description Temporary quota increase / decrease */ + GoogleCloudIdentitytoolkitAdminV2TemporaryQuota: { /** - * Whether to allow email/password user authentication. + * Format: int64 + * @description Corresponds to the 'refill_token_count' field in QuotaServer config */ - allowPasswordSignup?: boolean; + quota?: string; /** - * 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. + * Format: google-duration + * @description How long this quota will be active for */ - disableAuth?: boolean; + quotaDuration?: string; /** - * Display name of the tenant. + * Format: google-datetime + * @description When this quota will take affect */ + startTime?: string; + }; + /** @description A Tenant contains configuration for the tenant in a multi-tenant project. */ + GoogleCloudIdentitytoolkitAdminV2Tenant: { + /** @description Whether to allow email/password user authentication. */ + allowPasswordSignup?: boolean; + /** @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; + /** @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; + 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 }; + }; + /** @description TotpMFAProviderConfig represents the TOTP based MFA provider. */ + GoogleCloudIdentitytoolkitAdminV2TotpMfaProviderConfig: { /** - * 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). + * Format: int32 + * @description The allowed number of adjacent intervals that will be used for verification to avoid clock skew. */ - testPhoneNumbers?: { [key: string]: string }; + adjacentIntervals?: number; }; - /** - * The information required to auto-retrieve an SMS. - */ - GoogleCloudIdentitytoolkitV2AutoRetrievalInfo: { + /** @description Synchronous Cloud Function with HTTP Trigger */ + GoogleCloudIdentitytoolkitAdminV2Trigger: { + /** @description HTTP URI trigger for the Cloud Function. */ + functionUri?: string; /** - * The Android app's signature hash for Google Play Service's SMS Retriever API. + * Format: google-datetime + * @description When the trigger was changed. */ + updateTime?: string; + }; + /** @description The information required to auto-retrieve an SMS. */ + GoogleCloudIdentitytoolkitV2AutoRetrievalInfo: { + /** @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. - */ + /** @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; + /** + * Format: int32 + * @description Maximum password length. No default max length + */ + maxPasswordLength?: number; + /** + * Format: int32 + * @description Minimum password length. Range from 6 to 30 + */ + minPasswordLength?: number; + }; + /** @description Finishes enrolling a second factor for the user. */ GoogleCloudIdentitytoolkitV2FinalizeMfaEnrollmentRequest: { - /** - * Display name which is entered by users to distinguish between different second factors with same type or different type. - */ + /** @description Display name which is entered by users to distinguish between different second factors with same type or different type. */ displayName?: string; - /** - * Required. ID token. - */ + /** @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` with a `role`. - */ + /** @description Associates `members`, or principals, with a `role`. */ GoogleIamV1Binding: { condition?: components["schemas"]["GoogleTypeExpr"]; - /** - * Specifies the identities 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 `members`. 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` to a single `role`. Members 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` to 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 member. - */ + /** @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. - */ - EmulatorV1ProjectsConfig: { signIn?: { allowDuplicateEmails?: boolean } }; - /** - * Details of all pending confirmation codes. - */ + /** @description Emulator-specific configuration. */ + EmulatorV1ProjectsConfig: { + signIn?: { + allowDuplicateEmails?: boolean; + }; + emailPrivacyConfig?: { + enableImprovedEmailPrivacy?: boolean, + }, + }; + /** @description Details of all pending confirmation codes. */ EmulatorV1ProjectsOobCodes: { - oobCodes?: { email?: string; oobCode?: string; oobLink?: string; requestType?: string }[]; + oobCodes?: { + email?: string; + oobCode?: string; + oobLink?: string; + requestType?: string; + }[]; }; - /** - * Details of all pending verification codes. - */ + /** @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 4ab40fea980..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 } from "./operations"; -import { ProjectState } from "./state"; +import { authOperations, AuthOps, AuthOperation, FirebaseJwtPayload } from "./operations"; +import { AgentProjectState, decodeRefreshToken, ProjectState } from "./state"; import apiSpecUntyped from "./apiSpec"; import { PromiseController, @@ -31,8 +32,9 @@ 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; const API_SPEC_PATH = "/emulator/openapi.json"; @@ -115,14 +117,35 @@ 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 })); + // Workaround for clients (e.g. Node.js Admin SDK) that send request bodies + // with HTTP DELETE requests. Such requests are tolerated by production, but + // exegesis will reject them without the following hack. + app.delete("*", (req, _, next) => { + delete req.headers["content-type"]; + next(); + }); + app.get("/", (req, res) => { return res.json({ authEmulator: { @@ -138,17 +161,30 @@ export async function createApp( }); registerLegacyRoutes(app); - registerHandlers(app, (apiKey) => getProjectStateById(getProjectIdByApiKey(apiKey))); + registerHandlers(app, (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; @@ -161,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") { @@ -173,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 }; } @@ -187,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) { @@ -226,16 +263,17 @@ export async function createApp( // Let errors propagate to our universal error handler below. throw err; }, + defaultMaxBodySize: 1024 * 1024 * 1024, // 1GB instead of the default 10k. validateDefaultResponses: true, 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: { @@ -247,6 +285,10 @@ export async function createApp( // TODO return true; }, + "google-duration"() { + // TODO + return true; + }, uint64() { // TODO return true; @@ -255,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: [ { @@ -269,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( @@ -282,7 +330,7 @@ export async function createApp( location: "Authorization", locationType: "header", }, - ] + ], ); } } @@ -326,13 +374,33 @@ export async function createApp( return defaultProjectId; } - function getProjectStateById(projectId: string): ProjectState { - let state = projectStateForId.get(projectId); - if (!state) { - state = new ProjectState(projectId); - projectStateForId.set(projectId, state); + 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); + } } - return state; + if (!agentState) { + agentState = new AgentProjectState(projectId); + projectStateForId.set(projectId, agentState); + } + if (!tenantId) { + return agentState; + } + + return agentState.getTenantProject(tenantId); } } @@ -406,7 +474,7 @@ function registerLegacyRoutes(app: express.Express): void { function toExegesisController( ops: AuthOps, - getProjectStateById: (projectId: string) => ProjectState + getProjectStateById: (projectId: string, tenantId?: string) => ProjectState, ): Record { const result: Record = {}; processNested(ops, ""); @@ -450,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 { @@ -460,26 +528,58 @@ function toExegesisController( // See: https://cloud.google.com/identity-platform/docs/reference/rest/v1/accounts/signUp targetProjectId = ctx.user; } - if (ctx.params.path.tenantId || ctx.requestBody?.tenantId) { - throw new NotImplementedError("Multi-tenancy is unimplemented."); + + let targetTenantId: string | undefined = undefined; + if (ctx.params.path.tenantId && ctx.requestBody?.tenantId) { + assert(ctx.params.path.tenantId === ctx.requestBody.tenantId, "TENANT_ID_MISMATCH"); } - return operation(getProjectStateById(targetProjectId), ctx.requestBody, ctx); + targetTenantId = ctx.params.path.tenantId || ctx.requestBody?.tenantId; + + // 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 any as { + header: JwtHeader; + payload: FirebaseJwtPayload; + } | null; + if (decoded?.payload.firebase.tenant && targetTenantId) { + assert(decoded?.payload.firebase.tenant === targetTenantId, "TENANT_ID_MISMATCH"); + } + 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); }; } } 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; } @@ -489,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 83% rename from src/test/emulators/auth/setAccountInfo.spec.ts rename to src/emulator/auth/setAccountInfo.spec.ts index d6fa3012bb9..6d2fe4481bc 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 } 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,7 +20,8 @@ import { TEST_PHONE_NUMBER_2, TEST_PHONE_NUMBER_3, TEST_INVALID_PHONE_NUMBER, -} from "./helpers"; + registerTenant, +} from "./testing/helpers"; describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { it("should allow updating and deleting displayName and photoUrl", async () => { @@ -66,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; @@ -101,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; @@ -137,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; @@ -166,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; @@ -360,6 +361,79 @@ describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { }); }); + it("should update email when OOB flow of VERIFY_AND_CHANGE_EMAIL is initiated", async () => { + const oldEmail = "alice@example.com"; + const password = "notasecret"; + const newEmail = "bob@example.com"; + const { idToken } = await registerUser(authApi(), { email: oldEmail, password }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .query({ key: "fake-api-key" }) + .send({ idToken, newEmail, requestType: "VERIFY_AND_CHANGE_EMAIL" }) + .then((res) => { + expectStatusCode(200, res); + }); + + const oobs = await inspectOobs(authApi()); + expect(oobs).to.have.length(1); + expect(oobs[0].email).to.equal(oldEmail); + expect(oobs[0].newEmail).to.equal(newEmail); + expect(oobs[0].requestType).to.equal("VERIFY_AND_CHANGE_EMAIL"); + + // The returned oobCode can be redeemed to verify and change the email. + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:update") + .query({ key: "fake-api-key" }) + // OOB code is enough, no idToken needed. + .send({ oobCode: oobs[0].oobCode }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.email).to.equal(newEmail); + expect(res.body.newEmail).to.equal(newEmail); + // Email is verified since this flow can only be initiated through a link sent to the user's email. + expect(res.body.emailVerified).to.equal(true); + }); + + // oobCode is removed after redeemed. + const oobs2 = await inspectOobs(authApi()); + expect(oobs2).to.have.length(0); + }); + + it("should disallow changing an email if another user exists with the same email", async () => { + const user = { email: "alice@example.com", password: "notasecret" }; + const anotherUser = { email: "bob@example.com", password: "notasecreteither" }; + + const { idToken } = await registerUser(authApi(), user); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .query({ key: "fake-api-key" }) + .send({ idToken, newEmail: anotherUser.email, requestType: "VERIFY_AND_CHANGE_EMAIL" }) + .then((res) => { + expectStatusCode(200, res); + }); + + const oobs = await inspectOobs(authApi()); + expect(oobs).to.have.length(1); + expect(oobs[0].requestType).to.equal("VERIFY_AND_CHANGE_EMAIL"); + + await registerUser(authApi(), anotherUser); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:update") + .send({ oobCode: oobs[0].oobCode }) + .query({ key: "fake-api-key" }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("EMAIL_EXISTS"); + }); + + // oobCode is removed after redeemed. + const oobs2 = await inspectOobs(authApi()); + expect(oobs2).to.have.length(0); + }); + it("should update phoneNumber if specified", async () => { const phoneNumber = TEST_PHONE_NUMBER; const { localId, idToken } = await signInWithPhoneNumber(authApi(), phoneNumber); @@ -549,7 +623,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", @@ -642,7 +716,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.", ); }); }); @@ -916,7 +990,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", ); }); }); @@ -986,7 +1060,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(); @@ -997,7 +1071,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); }); @@ -1011,7 +1085,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( @@ -1020,7 +1094,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 () => { @@ -1074,7 +1148,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; @@ -1136,4 +1210,144 @@ describeAuthEmulator("accounts:update", ({ authApi, getClock }) => { expect(res.body.error.message).to.equal("CLAIMS_TOO_LARGE"); }); }); + + 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:update") + .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 set tenantId in oobLink", async () => { + const tenant = await registerTenant(authApi(), PROJECT_ID, { + disableAuth: false, + mfaConfig: { state: "ENABLED" as const }, + allowPasswordSignup: true, + }); + const oldEmail = "alice@example.com"; + const password = "notasecret"; + const newEmail = "bob@example.com"; + const { idToken } = await registerUser(authApi(), { + email: oldEmail, + password, + tenantId: tenant.tenantId, + }); + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:update") + .query({ key: "fake-api-key" }) + .send({ idToken, email: newEmail, tenantId: tenant.tenantId }) + .then((res) => { + expectStatusCode(200, res); + }); + + // An oob is sent to the oldEmail + const oobs = await inspectOobs(authApi(), tenant.tenantId); + expect(oobs).to.have.length(1); + expect(oobs[0].email).to.equal(oldEmail); + expect(oobs[0].requestType).to.equal("RECOVER_EMAIL"); + expect(oobs[0].oobLink).to.include(tenant.tenantId); + }); + + it("should link provider account with existing user account", async () => { + const { idToken } = await registerUser(authApi(), { + email: "test@example.com", + password: "password", + }); + + const providerId = "google.com"; + const rawId = "google_user_id"; + const providerUserInfo = { + providerId, + rawId, + email: "linked@example.com", + displayName: "Linked User", + photoUrl: "https://example.com/photo.jpg", + }; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:update") + .query({ key: "fake-api-key" }) + .send({ idToken, linkProviderUserInfo: providerUserInfo }) + .then((res) => { + expectStatusCode(200, res); + const providers = res.body.providerUserInfo; + expect(providers).to.have.length(2); // Original email/password + linked provider + + const linkedProvider = providers.find((p: ProviderUserInfo) => p.providerId === providerId); + expect(linkedProvider).to.deep.equal(providerUserInfo); + }); + + const accountInfo = await getAccountInfoByIdToken(authApi(), idToken); + expect(accountInfo.providerUserInfo).to.have.length(2); + const linkedProviderInfo = accountInfo.providerUserInfo?.find( + (p: ProviderUserInfo) => p.providerId === providerId, + ); + expect(linkedProviderInfo).to.deep.equal(providerUserInfo); + }); + + it("should error if linkProviderUserInfo is missing required fields", async () => { + const { idToken } = await registerUser(authApi(), { + email: "test@example.com", + password: "password", + }); + + const incompleteProviderUserInfo1 = { + providerId: "google.com", + email: "linked@example.com", + }; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:update") + .query({ key: "fake-api-key" }) + .send({ idToken, linkProviderUserInfo: incompleteProviderUserInfo1 }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.contain("MISSING_RAW_ID"); + }); + + const incompleteProviderUserInfo2 = { + rawId: "google_user_id", + email: "linked@example.com", + }; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:update") + .query({ key: "fake-api-key" }) + .send({ idToken, linkProviderUserInfo: incompleteProviderUserInfo2 }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.contain("MISSING_PROVIDER_ID"); + }); + }); + + it("should error if user is disabled when linking a provider", async () => { + const { localId, idToken } = await registerUser(authApi(), { + email: "test@example.com", + password: "password", + }); + + await updateAccountByLocalId(authApi(), localId, { disableUser: true }); + + const providerUserInfo = { + providerId: "google.com", + rawId: "google_user_id", + email: "linked@example.com", + }; + + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:update") + .query({ key: "fake-api-key" }) + .send({ idToken, linkProviderUserInfo: providerUserInfo }) + .then((res) => { + expectStatusCode(400, res); + expect(res.body.error.message).to.equal("USER_DISABLED"); + }); + }); }); 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 b8aebd3b556..53d539e779a 100644 --- a/src/emulator/auth/state.ts +++ b/src/emulator/auth/state.ts @@ -4,37 +4,34 @@ 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"; export const PROVIDER_PHONE = "phone"; export const PROVIDER_ANONYMOUS = "anonymous"; export const PROVIDER_CUSTOM = "custom"; +export const PROVIDER_GAME_CENTER = "gc.apple.com"; // Not yet implemented export const SIGNIN_METHOD_EMAIL_LINK = "emailLink"; -export class ProjectState { +export abstract class ProjectState { private users: Map = new Map(); private localIdForEmail: Map = new Map(); private localIdForInitialEmail: Map = new Map(); 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(); - public oneAccountPerEmail = true; - private authCloudFunction: AuthCloudFunction; + private pendingLocalIds: Set = new Set(); - constructor(public readonly projectId: string) { - this.authCloudFunction = new AuthCloudFunction(projectId); - } + constructor(public readonly projectId: string) {} get projectNumber(): string { // TODO: Shall we generate something different for each project? @@ -42,14 +39,38 @@ export class ProjectState { return "12345"; } - createUser(props: Omit): UserInfo { + abstract get oneAccountPerEmail(): boolean; + + abstract get enableImprovedEmailPrivacy(): boolean; + + abstract get authCloudFunction(): AuthCloudFunction; + + abstract get allowPasswordSignup(): boolean; + + abstract get disableAuth(): boolean; + + abstract get mfaConfig(): Schemas["GoogleCloudIdentitytoolkitAdminV2MultiFactorAuthConfig"]; + + abstract get enableAnonymousUser(): boolean; + + abstract get enableEmailLinkSignin(): boolean; + + 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. @@ -58,17 +79,15 @@ export 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, @@ -85,7 +104,7 @@ export class ProjectState { */ overwriteUserWithLocalId( localId: string, - props: Omit + props: Omit, ): UserInfo { const userInfoBefore = this.users.get(localId); if (userInfoBefore) { @@ -108,15 +127,6 @@ export 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); } @@ -126,7 +136,7 @@ export class ProjectState { options: { upsertProviders?: ProviderUserInfo[]; deleteProviders?: string[]; - } = {} + } = {}, ): UserInfo { const upsertProviders = options.upsertProviders ?? []; const deleteProviders = options.deleteProviders ?? []; @@ -208,16 +218,16 @@ export 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); @@ -228,7 +238,7 @@ export class ProjectState { private updateUserProviderInfo( user: UserInfo, upsertProviders: ProviderUserInfo[], - deleteProviders: string[] + deleteProviders: string[], ): UserInfo { const oldProviderEmails = getProviderEmailsForUser(user); @@ -256,7 +266,7 @@ export 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); @@ -360,7 +370,7 @@ export 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); @@ -375,44 +385,62 @@ export class ProjectState { createRefreshTokenFor( userInfo: UserInfo, provider: string, - extraClaims: Record = {} + { + extraClaims = {}, + secondFactor, + }: { + extraClaims?: Record; + secondFactor?: SecondFactorRecord; + } = {}, ): string { const localId = userInfo.localId; - const refreshToken = randomBase64UrlStr(204); - this.refreshTokens.set(refreshToken, { localId, provider, extraClaims }); - let refreshTokens = this.refreshTokensForLocalId.get(localId); - if (!refreshTokens) { - refreshTokens = new Set(); - this.refreshTokensForLocalId.set(localId, refreshTokens); - } - refreshTokens.add(refreshToken); + const refreshTokenRecord = { + _AuthEmulatorRefreshToken: "DO NOT MODIFY", + localId, + provider, + extraClaims, + projectId: this.projectId, + secondFactor, + tenantId: userInfo.tenantId, + }; + const refreshToken = encodeRefreshToken(refreshTokenRecord); return refreshToken; } - validateRefreshToken( - refreshToken: string - ): { user: UserInfo; provider: string; extraClaims: Record } | 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, }; } createOob( email: string, + newEmail: string | undefined, requestType: OobRequestType, - generateLink: (oobCode: string) => string + generateLink: (oobCode: string) => string, ): OobRecord { const oobCode = randomBase64UrlStr(54); const oobLink = generateLink(oobCode); const oob: OobRecord = { email, + newEmail, requestType, oobCode, oobLink, @@ -462,8 +490,6 @@ export 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 @@ -483,7 +509,7 @@ export class ProjectState { order: "ASC" | "DESC"; sortByField: "localId"; startToken?: string; - } + }, ): UserInfo[] { const users = []; for (const user of this.users.values()) { @@ -517,13 +543,12 @@ export class ProjectState { validateTemporaryProof( temporaryProof: string, - phoneNumber: string + phoneNumber: string, ): TemporaryProofRecord | undefined { const record = this.temporaryProofs.get(temporaryProof); if (!record || record.phoneNumber !== phoneNumber) { return undefined; } - // TODO: Find some way to enforce record.temporaryProofExpiresIn. return record; } @@ -550,6 +575,274 @@ export class ProjectState { } } } + +export class AgentProjectState extends ProjectState { + 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); + } + + get authCloudFunction() { + return this._authCloudFunction; + } + + get oneAccountPerEmail() { + return !this._config.signIn.allowDuplicateEmails; + } + + set oneAccountPerEmail(oneAccountPerEmail: boolean) { + this._config.signIn.allowDuplicateEmails = !oneAccountPerEmail; + } + + get enableImprovedEmailPrivacy() { + return !!this._config.emailPrivacyConfig.enableImprovedEmailPrivacy; + } + + set enableImprovedEmailPrivacy(improveEmailPrivacy: boolean) { + this._config.emailPrivacyConfig.enableImprovedEmailPrivacy = improveEmailPrivacy; + } + + get allowPasswordSignup() { + return true; + } + + get disableAuth() { + return false; + } + + get mfaConfig() { + return { state: "ENABLED" as const, enabledProviders: ["PHONE_SMS" as const] }; + } + + get enableAnonymousUser() { + return true; + } + + get enableEmailLinkSignin() { + 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 + // configurations to enabled. This is for convenience and differs from + // production in which configurations, are default disabled. Tests that + // need to reflect production defaults should first explicitly call + // `createTenant()` with a `Tenant` object. + this.createTenantWithTenantId(tenantId, { + tenantId, + allowPasswordSignup: true, + disableAuth: false, + mfaConfig: { + state: "ENABLED", + enabledProviders: ["PHONE_SMS"], + }, + enableAnonymousUser: true, + enableEmailLinkSignin: true, + }); + } + return this.tenantProjectForTenantId.get(tenantId)!; + } + + listTenants(startToken?: string): Tenant[] { + const tenantProjects = []; + for (const tenantProject of this.tenantProjectForTenantId.values()) { + if (!startToken || tenantProject.tenantId > startToken) { + tenantProjects.push(tenantProject); + } + } + // Sort in ascending order by tenantId + tenantProjects.sort((a, b) => { + if (a.tenantId < b.tenantId) { + return -1; + } else if (a.tenantId > b.tenantId) { + return 1; + } + return 0; + }); + return tenantProjects.map((tenantProject) => tenantProject.tenantConfig); + } + + createTenant(tenant: Tenant): Tenant { + for (let i = 0; i < 10; i++) { + const tenantId = randomId(28); + const createdTenant = this.createTenantWithTenantId(tenantId, tenant); + if (createdTenant) { + return createdTenant; + } + } + throw new Error("Could not generate a random unique tenantId after 10 tries"); + } + + private createTenantWithTenantId(tenantId: string, tenant: Tenant): Tenant | undefined { + if (this.tenantProjectForTenantId.has(tenantId)) { + return undefined; + } + tenant.name = `projects/${this.projectId}/tenants/${tenantId}`; + tenant.tenantId = tenantId; + this.tenantProjectForTenantId.set( + tenantId, + new TenantProjectState(this.projectId, tenantId, tenant, this), + ); + return tenant; + } + + deleteTenant(tenantId: string): void { + this.tenantProjectForTenantId.delete(tenantId); + } +} + +export class TenantProjectState extends ProjectState { + constructor( + projectId: string, + readonly tenantId: string, + private _tenantConfig: Tenant, + private readonly parentProject: AgentProjectState, + ) { + super(projectId); + } + + get oneAccountPerEmail() { + return this.parentProject.oneAccountPerEmail; + } + + get enableImprovedEmailPrivacy() { + return this.parentProject.enableImprovedEmailPrivacy; + } + + get authCloudFunction() { + return this.parentProject.authCloudFunction; + } + + get tenantConfig() { + return this._tenantConfig; + } + + get allowPasswordSignup() { + return this._tenantConfig.allowPasswordSignup; + } + + get disableAuth() { + return this._tenantConfig.disableAuth; + } + + get mfaConfig() { + return this._tenantConfig.mfaConfig; + } + + get enableAnonymousUser() { + return this._tenantConfig.enableAnonymousUser; + } + + get enableEmailLinkSignin() { + 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, + ): Tenant { + // Empty masks indicate a full update + if (!updateMask) { + const mfaConfig = update.mfaConfig ?? {}; + if (!("state" in mfaConfig)) { + mfaConfig.state = "DISABLED"; + } + if (!("enabledProviders" in mfaConfig)) { + mfaConfig.enabledProviders = []; + } + + // Default to production defaults if unset + this._tenantConfig = { + tenantId: this.tenantId, + name: this.tenantConfig.name, + allowPasswordSignup: update.allowPasswordSignup ?? false, + disableAuth: update.disableAuth ?? false, + mfaConfig: mfaConfig as MfaConfig, + enableAnonymousUser: update.enableAnonymousUser ?? false, + enableEmailLinkSignin: update.enableEmailLinkSignin ?? false, + displayName: update.displayName, + }; + return this.tenantConfig; + } + + return applyMask(updateMask, this.tenantConfig, update); + } +} + export type ProviderUserInfo = MakeRequired< Schemas["GoogleCloudIdentitytoolkitV1ProviderUserInfo"], "rawId" | "providerId" @@ -561,11 +854,51 @@ export type UserInfo = Omit< localId: string; providerUserInfo?: ProviderUserInfo[]; }; +export type MfaConfig = MakeRequired< + Schemas["GoogleCloudIdentitytoolkitAdminV2MultiFactorAuthConfig"], + "enabledProviders" | "state" +>; +export type Tenant = Omit< + MakeRequired< + Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"], + "allowPasswordSignup" | "disableAuth" | "enableAnonymousUser" | "enableEmailLinkSignin" + >, + "testPhoneNumbers" | "mfaConfig" +> & { tenantId: string; mfaConfig: MfaConfig }; + +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; +}; -interface RefreshTokenRecord { +export interface RefreshTokenRecord { + _AuthEmulatorRefreshToken: string; localId: string; provider: string; extraClaims: Record; + projectId: string; + secondFactor?: SecondFactorRecord; + tenantId?: string; +} + +export interface SecondFactorRecord { + identifier: string; + provider: string; } export type OobRequestType = NonNullable< @@ -574,6 +907,7 @@ export type OobRequestType = NonNullable< export interface OobRecord { email: string; + newEmail?: string; oobLink: string; oobCode: string; requestType: OobRequestType; @@ -585,10 +919,33 @@ export interface PhoneVerificationRecord { sessionInfo: string; } +export enum BlockingFunctionEvents { + BEFORE_CREATE = "beforeCreate", + BEFORE_SIGN_IN = "beforeSignIn", +} + interface TemporaryProofRecord { phoneNumber: string; temporaryProof: string; temporaryProofExpiresIn: string; + // Temporary proofs in emulator never expire to make interactive debugging + // 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 { @@ -600,3 +957,58 @@ function getProviderEmailsForUser(user: UserInfo): Set { }); return emails; } + +/** + * 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/emulator/auth/tenant.spec.ts b/src/emulator/auth/tenant.spec.ts new file mode 100644 index 00000000000..8fc5697ff16 --- /dev/null +++ b/src/emulator/auth/tenant.spec.ts @@ -0,0 +1,378 @@ +import { expect } from "chai"; +import { Tenant } from "./state"; +import { expectStatusCode, registerTenant } from "./testing/helpers"; +import { describeAuthEmulator } from "./testing/setup"; + +describeAuthEmulator("tenant management", ({ authApi }) => { + describe("createTenant", () => { + it("should create tenants", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v2/projects/project-id/tenants") + .set("Authorization", "Bearer owner") + .send({ + allowPasswordSignup: true, + disableAuth: false, + displayName: "display", + enableAnonymousUser: true, + enableEmailLinkSignin: true, + mfaConfig: { + enabledProviders: ["PROVIDER_UNSPECIFIED", "PHONE_SMS"], + state: "ENABLED", + }, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.allowPasswordSignup).to.be.true; + expect(res.body.disableAuth).to.be.false; + expect(res.body.displayName).to.eql("display"); + expect(res.body.enableAnonymousUser).to.be.true; + expect(res.body.enableEmailLinkSignin).to.be.true; + expect(res.body.mfaConfig).to.eql({ + enabledProviders: ["PROVIDER_UNSPECIFIED", "PHONE_SMS"], + state: "ENABLED", + }); + + // Should have a non-empty tenantId and matching resource name + expect(res.body).to.have.property("tenantId"); + expect(res.body.tenantId).to.not.eql(""); + expect(res.body).to.have.property("name"); + expect(res.body.name).to.eql(`projects/project-id/tenants/${res.body.tenantId}`); + }); + }); + + it("should create a tenant with default disabled settings", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v2/projects/project-id/tenants") + .set("Authorization", "Bearer owner") + .send({}) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.allowPasswordSignup).to.be.false; + expect(res.body.disableAuth).to.be.false; + expect(res.body.enableAnonymousUser).to.be.false; + expect(res.body.enableEmailLinkSignin).to.be.false; + expect(res.body.mfaConfig).to.eql({ + state: "DISABLED", + enabledProviders: [], + }); + }); + }); + }); + + describe("getTenants", () => { + it("should get tenants", async () => { + const projectId = "project-id"; + const tenant = await registerTenant(authApi(), projectId, {}); + + await authApi() + .get(`/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}`) + .set("Authorization", "Bearer owner") + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.eql(tenant); + }); + }); + + it("should create tenants with default enabled settings if they do not exist", async () => { + // No projects exist initially + const projectId = "project-id"; + await authApi() + .get(`/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants`) + .set("Authorization", "Bearer owner") + .then((res) => { + expectStatusCode(200, res); + expect(res.body.tenants).to.have.length(0); + }); + + // Get should implicitly create a tenant that does not exist + const tenantId = "tenant-id"; + const createdTenant: Tenant = { + tenantId, + name: `projects/${projectId}/tenants/${tenantId}`, + allowPasswordSignup: true, + disableAuth: false, + enableAnonymousUser: true, + enableEmailLinkSignin: true, + mfaConfig: { + enabledProviders: ["PHONE_SMS"], + state: "ENABLED", + }, + }; + await authApi() + .get(`/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenantId}`) + .set("Authorization", "Bearer owner") + .then((res) => { + expectStatusCode(200, res); + expect(res.body).to.eql(createdTenant); + }); + + // The newly created tenant should be returned + await authApi() + .get(`/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants`) + .set("Authorization", "Bearer owner") + .then((res) => { + expectStatusCode(200, res); + expect(res.body.tenants).to.have.length(1); + expect(res.body.tenants[0].tenantId).to.eql(tenantId); + }); + }); + }); + + describe("deleteTenants", () => { + it("should delete tenants", async () => { + const projectId = "project-id"; + const tenant = await registerTenant(authApi(), projectId, {}); + + await authApi() + .delete( + `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}`, + ) + .set("Authorization", "Bearer owner") + .then((res) => { + expectStatusCode(200, res); + }); + }); + + it("should delete tenants if request body is passed", async () => { + const projectId = "project-id"; + const tenant = await registerTenant(authApi(), projectId, {}); + + await authApi() + .delete( + `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}`, + ) + .set("Authorization", "Bearer owner") + // Sets content-type and sends "{}" in request payload. This is very + // uncommon for HTTP DELETE requests, but clients like the Node.js Admin + // SDK do it anyway. We want the emulator to tolerate this. + .send({}) + .then((res) => { + expectStatusCode(200, res); + }); + }); + }); + + describe("listTenants", () => { + it("should list tenants", async () => { + const projectId = "project-id"; + const tenant1 = await registerTenant(authApi(), projectId, {}); + const tenant2 = await registerTenant(authApi(), projectId, {}); + + await authApi() + .get(`/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants`) + .set("Authorization", "Bearer owner") + .then((res) => { + expectStatusCode(200, res); + expect(res.body.tenants).to.have.length(2); + expect(res.body.tenants.map((tenant: Tenant) => tenant.tenantId)).to.have.members([ + tenant1.tenantId, + tenant2.tenantId, + ]); + expect(res.body).not.to.have.property("nextPageToken"); + }); + }); + + it("should allow specifying pageSize and pageToken", async () => { + const projectId = "project-id"; + const tenant1 = await registerTenant(authApi(), projectId, {}); + const tenant2 = await registerTenant(authApi(), projectId, {}); + const tenantIds = [tenant1.tenantId, tenant2.tenantId].sort(); + + const nextPageToken = await authApi() + .get(`/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants`) + .set("Authorization", "Bearer owner") + .query({ pageSize: 1 }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.tenants).to.have.length(1); + expect(res.body.tenants[0].tenantId).to.eql(tenantIds[0]); + expect(res.body).to.have.property("nextPageToken").which.is.a("string"); + return res.body.nextPageToken; + }); + + await authApi() + .get(`/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants`) + .set("Authorization", "Bearer owner") + .query({ pageToken: nextPageToken }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.tenants).to.have.length(1); + expect(res.body.tenants[0].tenantId).to.eql(tenantIds[1]); + expect(res.body).not.to.have.property("nextPageToken"); + }); + }); + + it("should always return a page token even if page is full", async () => { + const projectId = "project-id"; + const tenant1 = await registerTenant(authApi(), projectId, {}); + const tenant2 = await registerTenant(authApi(), projectId, {}); + const tenantIds = [tenant1.tenantId, tenant2.tenantId].sort(); + + const nextPageToken = await authApi() + .get(`/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants`) + .set("Authorization", "Bearer owner") + .query({ pageSize: 2 }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.tenants).to.have.length(2); + expect(res.body.tenants[0].tenantId).to.eql(tenantIds[0]); + expect(res.body.tenants[1].tenantId).to.eql(tenantIds[1]); + expect(res.body).to.have.property("nextPageToken").which.is.a("string"); + return res.body.nextPageToken; + }); + + await authApi() + .get(`/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants`) + .set("Authorization", "Bearer owner") + .query({ pageToken: nextPageToken }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.tenants || []).to.have.length(0); + expect(res.body).not.to.have.property("nextPageToken"); + }); + }); + }); + + describe("updateTenants", () => { + it("updates tenant config", async () => { + const projectId = "project-id"; + const tenant = await registerTenant(authApi(), projectId, {}); + const updateMask = + "allowPasswordSignup,disableAuth,displayName,enableAnonymousUser,enableEmailLinkSignin,mfaConfig"; + + await authApi() + .patch( + `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}`, + ) + .set("Authorization", "Bearer owner") + .query({ updateMask }) + .send({ + allowPasswordSignup: true, + disableAuth: false, + displayName: "display", + enableAnonymousUser: true, + enableEmailLinkSignin: true, + mfaConfig: { + enabledProviders: ["PROVIDER_UNSPECIFIED", "PHONE_SMS"], + state: "ENABLED", + }, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.allowPasswordSignup).to.be.true; + expect(res.body.disableAuth).to.be.false; + expect(res.body.displayName).to.eql("display"); + expect(res.body.enableAnonymousUser).to.be.true; + expect(res.body.enableEmailLinkSignin).to.be.true; + expect(res.body.mfaConfig).to.eql({ + enabledProviders: ["PROVIDER_UNSPECIFIED", "PHONE_SMS"], + state: "ENABLED", + }); + }); + }); + + it("does not update if the field does not exist on the update tenant", async () => { + const projectId = "project-id"; + const tenant = await registerTenant(authApi(), projectId, {}); + const updateMask = "displayName"; + + await authApi() + .patch( + `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}`, + ) + .set("Authorization", "Bearer owner") + .query({ updateMask }) + .send({}) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).not.to.have.property("displayName"); + }); + }); + + it("does not update if indexing a primitive field or array on the update tenant", async () => { + const projectId = "project-id"; + const tenant = await registerTenant(authApi(), projectId, { + displayName: "display", + mfaConfig: { + enabledProviders: ["PROVIDER_UNSPECIFIED", "PHONE_SMS"], + }, + }); + const updateMask = "displayName.0,mfaConfig.enabledProviders.nonexistentField"; + + await authApi() + .patch( + `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}`, + ) + .set("Authorization", "Bearer owner") + .query({ updateMask }) + .send({ + displayName: "unused", + mfaConfig: { + enabledProviders: ["PROVIDER_UNSPECIFIED"], + }, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.displayName).to.eql("display"); + expect(res.body.mfaConfig.enabledProviders).to.eql(["PROVIDER_UNSPECIFIED", "PHONE_SMS"]); + }); + }); + + it("performs a full update if the update mask is empty", async () => { + const projectId = "project-id"; + const tenant = await registerTenant(authApi(), projectId, {}); + + await authApi() + .patch( + `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}`, + ) + .set("Authorization", "Bearer owner") + .send({ + allowPasswordSignup: true, + disableAuth: false, + displayName: "display", + enableAnonymousUser: true, + enableEmailLinkSignin: true, + mfaConfig: { + enabledProviders: ["PROVIDER_UNSPECIFIED", "PHONE_SMS"], + state: "ENABLED", + }, + }) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.allowPasswordSignup).to.be.true; + expect(res.body.disableAuth).to.be.false; + expect(res.body.displayName).to.eql("display"); + expect(res.body.enableAnonymousUser).to.be.true; + expect(res.body.enableEmailLinkSignin).to.be.true; + expect(res.body.mfaConfig).to.eql({ + enabledProviders: ["PROVIDER_UNSPECIFIED", "PHONE_SMS"], + state: "ENABLED", + }); + }); + }); + + it("performs a full update with production defaults if the update mask is empty", async () => { + const projectId = "project-id"; + const tenant = await registerTenant(authApi(), projectId, {}); + + await authApi() + .patch( + `/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants/${tenant.tenantId}`, + ) + .set("Authorization", "Bearer owner") + .send({}) + .then((res) => { + expectStatusCode(200, res); + expect(res.body.allowPasswordSignup).to.be.false; + expect(res.body.disableAuth).to.be.false; + expect(res.body.enableAnonymousUser).to.be.false; + expect(res.body.enableEmailLinkSignin).to.be.false; + expect(res.body.mfaConfig).to.eql({ + enabledProviders: [], + state: "DISABLED", + }); + }); + }); + }); +}); diff --git a/src/emulator/auth/testing/helpers.ts b/src/emulator/auth/testing/helpers.ts new file mode 100644 index 00000000000..7d5821bdd56 --- /dev/null +++ b/src/emulator/auth/testing/helpers.ts @@ -0,0 +1,423 @@ +import { STATUS_CODES } from "http"; +import { inspect } from "util"; +import * as supertest from "supertest"; +import { expect, AssertionError } from "chai"; +import { IdpJwtPayload } from "../operations"; +import { OobRecord, PhoneVerificationRecord, Tenant, UserInfo } from "../state"; +import { TestAgent, PROJECT_ID } from "./setup"; +import { MfaEnrollment, MfaEnrollments, Schemas } from "../types"; + +export { PROJECT_ID }; +export const TEST_PHONE_NUMBER = "+15555550100"; +export const TEST_PHONE_NUMBER_OBFUSCATED = "+*******0100"; +export const TEST_PHONE_NUMBER_2 = "+15555550101"; +export const TEST_PHONE_NUMBER_3 = "+15555550102"; +export const TEST_MFA_INFO = { + displayName: "Cell Phone", + 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", + emailVerified: true, + rawId: "123456789012345678901", + // An unsigned token, with payload format similar to a real one. + idToken: + "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiMjI4NzQ2ODI4NDQtYjBzOHM3NWIzaWVkYjJtZDRobHMydm9xNnNsbGJzbTMuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIyMjg3NDY4Mjg0NC1iMHM4czc1YjNpZWRiMm1kNGhsczJ2b3E2c2xsYnNtMy5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjEyMzQ1Njc4OTAxMjM0NTY3ODkwMSIsImVtYWlsIjoiZXhhbXBsZUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJpYXQiOjE1OTc4ODI2ODEsImV4cCI6MTU5Nzg4NjI4MX0.", + // Same as above, except with no email included. + idTokenNoEmail: + "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiMjI4NzQ2ODI4NDQtYjBzOHM3NWIzaWVkYjJtZDRobHMydm9xNnNsbGJzbTMuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIyMjg3NDY4Mjg0NC1iMHM4czc1YjNpZWRiMm1kNGhsczJ2b3E2c2xsYnNtMy5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjEyMzQ1Njc4OTAxMjM0NTY3ODkwMSIsImF0X2hhc2giOiIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwiaWF0IjoxNTk3ODgyNjgxLCJleHAiOjE1OTc4ODYyODF9.", +}; + +// This is a real Google test account (go/rhea), owned and managed by a Googler. +// However, nobody needs to actually sign-in using this account -- no tests +// below requires actual Google sign-in, and the Auth Emulator doesn't validate. +// If for some reason the account or idToken below doesn't fit our testing need +// anymore, create a new test account and token. Don't ping anyone for password. +export const REAL_GOOGLE_ACCOUNT = { + displayName: "Oberyn Baelish", + email: "oberynbaelish.331826@gmail.com", + emailVerified: true, + rawId: "115113236566683398301", + photoUrl: + "https://lh3.googleusercontent.com/-KNaMyFnKZ9o/AAAAAAAAAAI/AAAAAAAAAAA/AMZuucnZC9bn4HcT-8bQka3uG3lUYd4lSA/photo.jpg", + // ID Tokens below are also real, but their signatures has been zero'd out and + // have expired long ago, so they are safe to use as examples in tests below. + idToken: + "eyJhbGciOiJSUzI1NiIsImtpZCI6IjZiYzYzZTlmMThkNTYxYjM0ZjU2NjhmODhhZTI3ZDQ4ODc2ZDgwNzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiMjI4NzQ2ODI4NDQtYjBzOHM3NWIzaWVkYjJtZDRobHMydm9xNnNsbGJzbTMuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIyMjg3NDY4Mjg0NC1iMHM4czc1YjNpZWRiMm1kNGhsczJ2b3E2c2xsYnNtMy5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjExNTExMzIzNjU2NjY4MzM5ODMwMSIsImVtYWlsIjoib2JlcnluYmFlbGlzaC4zMzE4MjZAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiJXNTlTOEs4Y3g0Y3hYYmh0YmFXYndBIiwiaWF0IjoxNTk3ODgyNjgxLCJleHAiOjE1OTc4ODYyODF9.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + idTokenNoEmail: + "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 + * @param res the supertest Response + */ +export function expectStatusCode(expected: number, res: supertest.Response): void { + if (res.status !== expected) { + const body = inspect(res.body); + throw new AssertionError( + `expected ${expected} "${STATUS_CODES[expected]}", got ${res.status} "${ + STATUS_CODES[res.status] + }", with response body:\n${body}`, + ); + } +} + +/** + * Create a fake claims object with some default field values plus custom ones. + * @param input custom field values + * @return a complete claims plain JS object + */ +export function fakeClaims(input: Partial & { sub: string }): IdpJwtPayload { + return Object.assign( + { + iss: "example.com", + aud: "example.com", + exp: 1597974008, + iat: 1597970408, + }, + input, + ); +} + +/* eslint-disable jsdoc/require-jsdoc */ +// Most functions below are self-documenting test helpers. + +export async function registerUser( + testAgent: TestAgent, + user: { + email: string; + password: string; + displayName?: string; + mfaInfo?: MfaEnrollments; + tenantId?: string; + }, +): Promise<{ idToken: string; localId: string; refreshToken: string; email: string }> { + const res = await testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send(user) + .query({ key: "fake-api-key" }); + expectStatusCode(200, res); + return { + idToken: res.body.idToken, + localId: res.body.localId, + refreshToken: res.body.refreshToken, + email: res.body.email, + }; +} + +export async function registerAnonUser( + testAgent: TestAgent, +): Promise<{ idToken: string; localId: string; refreshToken: string }> { + const res = await testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .send({ returnSecureToken: true }) + .query({ key: "fake-api-key" }); + expectStatusCode(200, res); + return { + idToken: res.body.idToken, + localId: res.body.localId, + refreshToken: res.body.refreshToken, + }; +} + +export async function signInWithEmailLink( + testAgent: TestAgent, + email: string, + idTokenToLink?: string, +): Promise<{ idToken: string; localId: string; refreshToken: string; email: string }> { + const { oobCode } = await createEmailSignInOob(testAgent, email); + + const res = await testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") + .query({ key: "fake-api-key" }) + .send({ email, oobCode, idToken: idTokenToLink }); + return { + idToken: res.body.idToken, + localId: res.body.localId, + refreshToken: res.body.refreshToken, + email, + }; +} + +export async function signInWithPassword( + testAgent: TestAgent, + email: string, + password: string, + extractMfaPending: boolean = false, +): Promise<{ + idToken?: string; + localId?: string; + refreshToken?: string; + email?: string; + mfaPendingCredential?: string; + mfaEnrollmentId?: string; +}> { + if (extractMfaPending) { + const res = await testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .send({ email, password }) + .query({ key: "fake-api-key" }); + + expectStatusCode(200, res); + const mfaPendingCredential = res.body.mfaPendingCredential as string; + const mfaInfo = res.body.mfaInfo as MfaEnrollment[]; + return { mfaPendingCredential, mfaEnrollmentId: mfaInfo[0].mfaEnrollmentId }; + } + const res = await testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") + .send({ email, password }) + .query({ key: "fake-api-key" }); + expectStatusCode(200, res); + return { + idToken: res.body.idToken, + localId: res.body.localId, + refreshToken: res.body.refreshToken, + email: res.body.email, + }; +} + +export async function signInWithPhoneNumber( + testAgent: TestAgent, + phoneNumber: string, +): Promise<{ idToken: string; localId: string; refreshToken: string }> { + const verificationRes = await testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:sendVerificationCode") + .query({ key: "fake-api-key" }) + .send({ phoneNumber, recaptchaToken: "ignored" }); + expectStatusCode(200, verificationRes); + const sessionInfo = verificationRes.body.sessionInfo; + + const codes = await inspectVerificationCodes(testAgent); + + const signInRes = await testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") + .query({ key: "fake-api-key" }) + .send({ sessionInfo, code: codes[0].code }); + expectStatusCode(200, signInRes); + return { + idToken: signInRes.body.idToken, + localId: signInRes.body.localId, + refreshToken: signInRes.body.refreshToken, + }; +} + +export async function signInWithFakeClaims( + testAgent: TestAgent, + providerId: string, + claims: Partial & { sub: string }, + tenantId?: string, +): Promise<{ idToken: string; localId: string; refreshToken: string; email?: string }> { + const fakeIdToken = JSON.stringify(fakeClaims(claims)); + const res = await testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") + .query({ key: "fake-api-key" }) + .send({ + postBody: `providerId=${encodeURIComponent(providerId)}&id_token=${encodeURIComponent( + fakeIdToken, + )}`, + requestUri: "http://localhost", + returnIdpCredential: true, + returnSecureToken: true, + tenantId, + }); + expectStatusCode(200, res); + return { + idToken: res.body.idToken, + localId: res.body.localId, + refreshToken: res.body.refreshToken, + email: res.body.email, + }; +} + +export async function expectUserNotExistsForIdToken( + testAgent: TestAgent, + idToken: string, + tenantId?: string, +): Promise { + const res = await testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:lookup") + .send({ idToken, tenantId }) + .query({ key: "fake-api-key" }); + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("USER_NOT_FOUND"); +} + +export async function expectIdTokenExpired(testAgent: TestAgent, idToken: string): Promise { + const res = await testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:lookup") + .send({ idToken }) + .query({ key: "fake-api-key" }); + expectStatusCode(400, res); + expect(res.body.error).to.have.property("message").equals("TOKEN_EXPIRED"); +} + +export async function getAccountInfoByIdToken( + testAgent: TestAgent, + idToken: string, + tenantId?: string, +): Promise { + const res = await testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:lookup") + .send({ idToken, tenantId }) + .query({ key: "fake-api-key" }); + expectStatusCode(200, res); + expect(res.body.users || []).to.have.length(1); + return res.body.users[0]; +} + +export async function getAccountInfoByLocalId( + testAgent: TestAgent, + localId: string, + tenantId?: string, +): Promise { + const res = await testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:lookup") + .send({ localId: [localId], tenantId }) + .set("Authorization", "Bearer owner"); + expectStatusCode(200, res); + expect(res.body.users || []).to.have.length(1); + return res.body.users[0]; +} + +export async function inspectOobs(testAgent: TestAgent, tenantId?: string): Promise { + const path = tenantId + ? `/emulator/v1/projects/${PROJECT_ID}/tenants/${tenantId}/oobCodes` + : `/emulator/v1/projects/${PROJECT_ID}/oobCodes`; + const res = await testAgent.get(path); + expectStatusCode(200, res); + return res.body.oobCodes; +} + +export async function inspectVerificationCodes( + testAgent: TestAgent, + tenantId?: string, +): Promise { + const path = tenantId + ? `/emulator/v1/projects/${PROJECT_ID}/tenants/${tenantId}/verificationCodes` + : `/emulator/v1/projects/${PROJECT_ID}/verificationCodes`; + const res = await testAgent.get(path); + expectStatusCode(200, res); + return res.body.verificationCodes; +} + +export async function createEmailSignInOob( + testAgent: TestAgent, + email: string, + tenantId?: string, +): Promise<{ oobCode: string; oobLink: string }> { + const res = await testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") + .send({ email, requestType: "EMAIL_SIGNIN", returnOobLink: true, tenantId }) + .set("Authorization", "Bearer owner"); + expectStatusCode(200, res); + return { + oobCode: res.body.oobCode, + oobLink: res.body.oobLink, + }; +} + +export async function getSigninMethods(testAgent: TestAgent, email: string): Promise { + const res = await testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri") + .send({ continueUri: "http://example.com/", identifier: email }) + .query({ key: "fake-api-key" }); + expectStatusCode(200, res); + return res.body.signinMethods; +} + +export async function updateProjectConfig(testAgent: TestAgent, config: {}): Promise { + const res = await testAgent + .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) + .set("Authorization", "Bearer owner") + .send(config); + expectStatusCode(200, res); +} + +export async function updateAccountByLocalId( + testAgent: TestAgent, + localId: string, + fields: {}, +): Promise { + const res = await testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:update") + .set("Authorization", "Bearer owner") + .send({ localId, ...fields }); + expectStatusCode(200, res); +} + +export async function enrollPhoneMfa( + testAgent: TestAgent, + idToken: string, + phoneNumber: string, + tenantId?: string, +): Promise<{ idToken: string; refreshToken: string }> { + const mfaStartRes = await testAgent + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:start") + .query({ key: "fake-api-key" }) + .send({ idToken, phoneEnrollmentInfo: { phoneNumber }, tenantId }); + expectStatusCode(200, mfaStartRes); + expect(mfaStartRes.body.phoneSessionInfo.sessionInfo).to.be.a("string"); + const sessionInfo = mfaStartRes.body.phoneSessionInfo.sessionInfo as string; + + const code = (await inspectVerificationCodes(testAgent, tenantId))[0].code; + + const mfaFinalRes = await testAgent + .post("/identitytoolkit.googleapis.com/v2/accounts/mfaEnrollment:finalize") + .query({ key: "fake-api-key" }) + .send({ idToken, phoneVerificationInfo: { code, sessionInfo }, tenantId }); + expectStatusCode(200, mfaFinalRes); + expect(mfaFinalRes.body.idToken).to.be.a("string"); + expect(mfaFinalRes.body.refreshToken).to.be.a("string"); + return { idToken: mfaFinalRes.body.idToken, refreshToken: mfaFinalRes.body.refreshToken }; +} + +export async function deleteAccount(testAgent: TestAgent, reqBody: {}): Promise { + const res = await testAgent + .post("/identitytoolkit.googleapis.com/v1/accounts:delete") + .send(reqBody) + .query({ key: "fake-api-key" }); + expectStatusCode(200, res); + expect(res.body).not.to.have.property("error"); + return res.body.kind; +} + +export async function registerTenant( + testAgent: TestAgent, + projectId: string, + tenant?: Schemas["GoogleCloudIdentitytoolkitAdminV2Tenant"], +): Promise { + const res = await testAgent + .post(`/identitytoolkit.googleapis.com/v2/projects/${projectId}/tenants`) + .query({ key: "fake-api-key" }) + .set("Authorization", "Bearer owner") + .send(tenant); + expectStatusCode(200, res); + return res.body; +} + +export async function updateConfig( + testAgent: TestAgent, + projectId: string, + config: Schemas["GoogleCloudIdentitytoolkitAdminV2Config"], + updateMask?: string, +): Promise { + const res = await testAgent + .patch(`/identitytoolkit.googleapis.com/v2/projects/${projectId}/config`) + .set("Authorization", "Bearer owner") + .query({ updateMask }) + .send(config); + 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..a34dddfe3a8 --- /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) { + this.timeout(20000); + let authApp: Express.Application; + beforeEach("setup or reuse auth server", async () => { + 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/types.ts b/src/emulator/auth/types.ts index faaccd3ebd5..8d8513daed8 100644 --- a/src/emulator/auth/types.ts +++ b/src/emulator/auth/types.ts @@ -2,4 +2,3 @@ import * as schema from "./schema"; export type Schemas = schema.components["schemas"]; export type MfaEnrollment = Schemas["GoogleCloudIdentitytoolkitV1MfaEnrollment"]; export type MfaEnrollments = MfaEnrollment[]; -export type CreateMfaEnrollmentsRequest = Schemas["GoogleCloudIdentitytoolkitV1MfaFactor"][]; 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/auth/widget_ui.ts b/src/emulator/auth/widget_ui.ts index a5b8696613c..502fadc5804 100644 --- a/src/emulator/auth/widget_ui.ts +++ b/src/emulator/auth/widget_ui.ts @@ -21,6 +21,7 @@ assert(!internalError, internalError); var apiKey = query.get('apiKey'); var appName = query.get('appName'); +var tenantId = query.get('tid'); var authType = query.get('authType'); var providerId = query.get('providerId'); var redirectUrl = query.get('redirectUrl'); @@ -33,6 +34,7 @@ var firebaseAppId = query.get('appId'); var apn = query.get('apn'); var ibi = query.get('ibi'); var appIdentifier = apn || ibi; +var isSamlProvider = !!providerId.match(/^saml\./); assert( appName || clientId || firebaseAppId || appIdentifier, 'Missing one of appName / clientId / appId / apn / ibi query params.' @@ -157,28 +159,41 @@ var reuseAccountEls = document.querySelectorAll('.js-reuse-account'); if (reuseAccountEls.length) { [].forEach.call(reuseAccountEls, function (el) { var urlEncodedIdToken = el.dataset.idToken; + const decoded = JSON.parse(decodeURIComponent(urlEncodedIdToken)); el.addEventListener('click', function (e) { e.preventDefault(); - finishWithUser(urlEncodedIdToken); + finishWithUser(urlEncodedIdToken, decoded.email); }); }); } else { document.querySelector('.js-accounts-help-text').textContent = "No " + formattedProviderId + " accounts exist in the Auth Emulator."; } -function finishWithUser(urlEncodedIdToken) { +function finishWithUser(urlEncodedIdToken, email) { // Use widget URL, but replace all query parameters (no apiKey etc.). var url = window.location.href.split('?')[0]; // Avoid URLSearchParams for browser compatibility. url += '?providerId=' + encodeURIComponent(providerId); url += '&id_token=' + urlEncodedIdToken; + + // Save reasonable defaults for SAML providers + if (isSamlProvider) { + url += '&SAMLResponse=' + encodeURIComponent(JSON.stringify({ + assertion: { + subject: { + nameId: email, + }, + }, + })); + } + saveAuthEvent({ type: authType, eventId: eventId, urlResponse: url, sessionId: "ValueNotUsedByAuthEmulator", postBody: "", - tenantId: null, + tenantId: tenantId, error: null, }); } @@ -220,7 +235,7 @@ document.getElementById('main-form').addEventListener('submit', function(e) { if (screenName) claims.screenName = screenName; if (photoUrl) claims.photoUrl = photoUrl; - finishWithUser(createFakeClaims(claims)); + finishWithUser(createFakeClaims(claims), claims.email); } }); @@ -491,7 +506,7 @@ export const WIDGET_UI = ` Auth Emulator IDP Login Widget - + @@ -604,6 +619,6 @@ export const WIDGET_UI = ` - + `; 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 18f7e68fd8b..43cd9a72b05 100644 --- a/src/emulator/commandUtils.ts +++ b/src/emulator/commandUtils.ts @@ -1,25 +1,26 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as childProcess from "child_process"; import * as controller from "../emulator/controller"; -import * as Config from "../config"; +import { Config } from "../config"; import * as utils from "../utils"; 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 * as getProjectId from "../getProjectId"; -import { prompt } from "../prompt"; -import { onExit } from "./controller"; +import { getProjectId } from "../projectUtils"; +import { confirm } from "../prompt"; import * as fsutils from "../fsutils"; import Signals = NodeJS.Signals; import SignalsListener = NodeJS.SignalsListener; -import Table = require("cli-table"); +import * as Table from "cli-table3"; +import { emulatorSession } from "../track"; +import { setEnvVarsForEmulators } from "./env"; +import { sendVSCodeMessage, VSCODE_MESSAGE } from "../dataconnect/webhook"; export const FLAG_ONLY = "--only "; export const DESC_ONLY = @@ -45,6 +46,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 +64,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,16 +98,21 @@ 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.`, ); } } -export function warnEmulatorNotSupported( +/** + * 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 async function warnEmulatorNotSupported( options: any, - emulator: Emulators.DATABASE | Emulators.FIRESTORE -): void | Promise { + emulator: Emulators.DATABASE | Emulators.FIRESTORE, +): Promise { if (emulator !== Emulators.DATABASE && emulator !== Emulators.FIRESTORE) { return; } @@ -103,28 +127,28 @@ 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 = { - confirm: undefined, - }; - return prompt(opts, [ - { - type: "confirm", - name: "confirm", - default: false, - message: "Do you want to continue?", - }, - ]).then(() => { - if (!opts.confirm) { - return utils.reject("Command aborted.", { exit: 1 }); - } - }); + if (!(await confirm("Do you want to continue?"))) { + throw new FirebaseError("Command aborted.", { exit: 1 }); + } + } +} + +export async function errorMissingProject(options: any): Promise { + if (!options.project) { + throw new FirebaseError( + "Project is not defined. Either use `--project` or use `firebase use` to set your active project.", + ); } } +/** + * 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, @@ -140,16 +164,23 @@ export async function beforeEmulatorCommand(options: any): Promise { !controller.shouldStart(optionsWithConfig, Emulators.FUNCTIONS) && !controller.shouldStart(optionsWithConfig, Emulators.HOSTING); - try { - await requireAuth(options); - } catch (e) { - 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.` - ); + // We generally should not check for auth if you are using a demo project since prod calls to a fake project will fail. + // However, extensions makes 'publishers/*' calls that require auth, so we'll requireAuth if you are using extensions. + if ( + !Constants.isDemoProject(options.project) || + controller.shouldStart(optionsWithConfig, Emulators.EXTENSIONS) + ) { + try { + await requireAuth(options); + } 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.`, + ); + } } if (canStartWithoutConfig && !options.config) { @@ -160,22 +191,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. @@ -184,7 +226,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 @@ -206,15 +248,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; @@ -238,19 +284,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( @@ -259,7 +305,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[] = []; @@ -275,7 +321,7 @@ function processKillSignal( pids.push(emulatorInfo.pid as number); emulatorsTable.push([ Constants.description(emulatorInfo.name), - EmulatorRegistry.getInfoHostString(emulatorInfo), + getListenOverview(emulatorInfo.name) ?? "unknown", emulatorInfo.pid, ]); } @@ -287,66 +333,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 authInstance = EmulatorRegistry.get(Emulators.AUTH); - if (authInstance) { - const info = authInstance.getInfo(); - const address = EmulatorRegistry.getInfoHostString(info); - env[Constants.FIREBASE_AUTH_EMULATOR_HOST] = 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 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, @@ -390,32 +423,171 @@ 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 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) { - shutdownWhenKilled(options); - const projectId = getProjectId(options, true); +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: string[] = []; try { const showUI = !!options.ui; - await controller.startAll(options, showUI); + ({ deprecationNotices } = await controller.startAll(options, showUI, true)); + await sendVSCodeMessage({ message: VSCODE_MESSAGE.EMULATORS_STARTED }); exitCode = await runScript(script, extraEnv); - await onExit(options); + await controller.onExit(options); + } catch (err: unknown) { + await sendVSCodeMessage({ message: VSCODE_MESSAGE.EMULATORS_START_ERRORED }); + throw err; } 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 = 21; +export const JAVA_DEPRECATION_WARNING = + "firebase-tools will drop support for Java version < 21 soon in firebase-tools@15. " + + "Please install a JDK at version 21 or above to get a compatible runtime."; diff --git a/src/emulator/constants.ts b/src/emulator/constants.ts index 78ce5c7c916..8d3a1c875ca 100644 --- a/src/emulator/constants.ts +++ b/src/emulator/constants.ts @@ -1,31 +1,39 @@ -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 + apphosting: 5002, firestore: 8080, pubsub: 8085, database: 9000, auth: 9099, storage: 9199, + eventarc: 9299, + dataconnect: 9399, + tasks: 9499, }; export const FIND_AVAILBLE_PORT_BY_DEFAULT: Record = { ui: true, hub: true, logging: true, - hosting: false, + hosting: true, + apphosting: true, functions: false, firestore: false, database: false, pubsub: false, auth: false, storage: false, + extensions: false, + eventarc: true, + dataconnect: false, + tasks: true, }; export const EMULATOR_DESCRIPTION: Record = { @@ -33,34 +41,78 @@ export const EMULATOR_DESCRIPTION: Record = { hub: "emulator hub", logging: "Logging Emulator", hosting: "Hosting Emulator", + apphosting: "App Hosting Emulator", functions: "Functions Emulator", firestore: "Firestore Emulator", database: "Database Emulator", pubsub: "Pub/Sub Emulator", auth: "Authentication Emulator", storage: "Storage Emulator", + extensions: "Extensions Emulator", + eventarc: "Eventarc Emulator", + dataconnect: "Data Connect Emulator", + tasks: "Cloud Tasks 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"; + // Environment variable to discover the Data Connect emulator. + static FIREBASE_DATACONNECT_EMULATOR_HOST = "FIREBASE_DATA_CONNECT_EMULATOR_HOST"; + + // Alternative (deprecated) env var for Data Connect Emulator. + static FIREBASE_DATACONNECT_ENV_ALT = "DATA_CONNECT_EMULATOR_HOST"; + // Environment variable to override SDK/CLI to point at the Firebase Auth emulator. static FIREBASE_AUTH_EMULATOR_HOST = "FIREBASE_AUTH_EMULATOR_HOST"; + // Environment variable to override SDK/CLI to point at the Firebase Storage emulator. + static FIREBASE_STORAGE_EMULATOR_HOST = "FIREBASE_STORAGE_EMULATOR_HOST"; + + // Environment variable to override SDK/CLI to point at the Firebase Storage emulator + // for firebase-admin <= 9.6.0. Unlike the FIREBASE_STORAGE_EMULATOR_HOST variable + // 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 tasks emulator. + static CLOUD_TASKS_EMULATOR_HOST = "CLOUD_TASKS_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"; + static SERVICE_CLOUD_TASKS = "cloudtasks.googleapis.com"; + static SERVICE_FIREALERTS = "firebasealerts.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"; @@ -90,12 +142,16 @@ export class Constants { return "storage"; case this.SERVICE_TEST_LAB: return "test lab"; + case this.SERVICE_EVENTARC: + return "eventarc"; + case this.SERVICE_CLOUD_TASKS: + return "tasks"; default: return service; } } - static getDefaultHost(emulator: Emulators): string { + static getDefaultHost(): string { return DEFAULT_HOST; } @@ -103,25 +159,11 @@ export class Constants { return DEFAULT_PORTS[emulator]; } - static getHostKey(emulator: Emulators): string { - return `emulators.${emulator.toString()}.host`; - } - - static getPortKey(emulator: Emulators): string { - return `emulators.${emulator.toString()}.port`; - } - static description(name: Emulators): string { 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..a8bb3529234 --- /dev/null +++ b/src/emulator/controller.spec.ts @@ -0,0 +1,107 @@ +import { Emulators } from "./types"; +import { EmulatorRegistry } from "./registry"; +import { expect } from "chai"; +import { FakeEmulator } from "./testing/fakeEmulator"; +import { shouldStart } from "./controller"; +import { Options } from "../options"; + +function createMockOptions( + only: string | undefined, + configValues: { [key: string]: any }, +): Options { + const config = { + get: (key: string) => configValues[key], + has: (key: string) => !!configValues[key], + src: { + emulators: configValues, + functions: configValues.functions, + }, + }; + return { + only, + config, + project: "test-project", + } as any; +} + +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); + }); + + describe("shouldStart", () => { + it("should start the hub if a project is specified", () => { + const options = { project: "test-project" } as Options; + expect(shouldStart(options, Emulators.HUB)).to.be.true; + }); + + it("should start the hub even if no project is specified", () => { + const options = {} as Options; + expect(shouldStart(options, Emulators.HUB)).to.be.true; + }); + + it("should start the UI if options.ui is true", () => { + const options = createMockOptions(undefined, {}); + options.ui = true; + expect(shouldStart(options, Emulators.UI)).to.be.true; + }); + + it("should start the UI if a project is specified and a UI-supported emulator is running", () => { + const options = createMockOptions("firestore", { firestore: {} }); + expect(shouldStart(options, Emulators.UI)).to.be.true; + }); + + it("should start the UI even if no project is specified", () => { + const options = createMockOptions("firestore", { firestore: {} }); + delete options.project; + expect(shouldStart(options, Emulators.UI)).to.be.true; + }); + + it("should not start the UI if no UI-supported emulator is running", () => { + const options = createMockOptions(undefined, {}); + expect(shouldStart(options, Emulators.UI)).to.be.false; + }); + + it("should start an emulator if it's in the only string", () => { + const options = createMockOptions("functions,hosting", { + functions: { source: "functions" }, + hosting: {}, + }); + expect(shouldStart(options, Emulators.FUNCTIONS)).to.be.true; + }); + + it("should not start an emulator if it's not in the only string", () => { + const options = createMockOptions("functions", { + functions: { source: "functions" }, + hosting: {}, + }); + expect(shouldStart(options, Emulators.HOSTING)).to.be.false; + }); + + it("should not start an emulator if it's in the only string but has no config", () => { + const options = createMockOptions("hosting,functions", { + hosting: {}, + }); + expect(shouldStart(options, Emulators.FUNCTIONS)).to.be.false; + }); + + it("should not start functions emulator if source directory is not configured", () => { + const options = createMockOptions("functions", { + functions: {}, // Config is present, but no source + }); + expect(shouldStart(options, Emulators.FUNCTIONS)).to.be.false; + }); + }); +}).timeout(2000); diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts old mode 100644 new mode 100755 index 091a68c140e..88004d71783 --- a/src/emulator/controller.ts +++ b/src/emulator/controller.ts @@ -1,154 +1,92 @@ -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 * as proto from "../gcp/proto"; -import * as 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 * as getProjectId from "../getProjectId"; -import { PubsubEmulator } from "./pubsubEmulator"; +import { getProjectId, 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 * as rimraf from "rimraf"; -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"; - -async function getAndCheckAddress(emulator: Emulators, options: any): Promise
    { - let host = Constants.normalizeHost( - options.config.get(Constants.getHostKey(emulator), 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.get(Constants.getPortKey(emulator), undefined); - 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("emulators:start", name); - - await EmulatorRegistry.start(instance); -} +import { Options } from "../options"; +import { ParsedTriggerDefinition } from "./functionsEmulatorShared"; +import { ExtensionsEmulator } from "./extensionsEmulator"; +import { normalizeAndValidate, requireLocal } 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, DataConnectEmulatorArgs } from "./dataconnectEmulator"; +import { FirestoreEmulator, FirestoreEmulatorArgs } from "./firestoreEmulator"; +import { HostingEmulator } from "./hostingEmulator"; +import { PubsubEmulator } from "./pubsubEmulator"; +import { StorageEmulator } from "./storage"; +import { readFirebaseJson } from "../dataconnect/load"; +import { TasksEmulator } from "./tasksEmulator"; +import { AppHostingEmulator } from "./apphosting"; +import { sendVSCodeMessage, VSCODE_MESSAGE } from "../dataconnect/webhook"; +import { dataConnectLocalConnString } from "../api"; +import { AppHostingSingle } from "../firebaseConfig"; +import { resolveProjectPath } from "../projectPath"; + +const START_LOGGING_EMULATOR = utils.envOverride( + "START_LOGGING_EMULATOR", + "false", + (val) => val === "true", +); /** * Exports emulator data on clean exit (SIGINT or process end) * @param options */ -export async function exportOnExit(options: any) { - const exportOnExitDir = options.exportOnExit; +export async function exportOnExit(options: Options): Promise { + // Note: options.exportOnExit is coerced to a string before this point in commandUtils.ts#setExportOnExitOptions + const exportOnExitDir = options.exportOnExit as string; if (exportOnExitDir) { 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) { - utils.logWarning(e); + await exportEmulatorData(exportOnExitDir, options, /* initiatedBy= */ "exit"); + } catch (e: unknown) { + utils.logWarning(`${e}`); utils.logWarning(`Automatic export to "${exportOnExitDir}" failed, going to exit now...`); } } @@ -170,26 +108,34 @@ export async function cleanShutdown(): Promise { EmulatorLogger.forEmulator(Emulators.HUB).logLabeled( "BULLET", "emulators", - "Shutting down emulators." + "Shutting down emulators.", ); await EmulatorRegistry.stopAll(); + await sendVSCodeMessage({ message: VSCODE_MESSAGE.EMULATORS_SHUTDOWN }); } /** * 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}`); }); + // Extensions may not be initialized but we can have SDK defined extensions + if (targets.includes(Emulators.FUNCTIONS) && !targets.includes(Emulators.EXTENSIONS)) { + targets.push(Emulators.EXTENSIONS); + } + const onlyOptions: string = options.only; if (onlyOptions) { 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; @@ -200,10 +146,10 @@ export function filterEmulatorTargets(options: any): Emulators[] { * @param options * @param name */ -export function shouldStart(options: any, name: Emulators): boolean { +export function shouldStart(options: Options, name: Emulators): boolean { if (name === Emulators.HUB) { - // The hub only starts if we know the project ID. - return !!options.project; + // The emulator hub always starts. + return true; } const targets = filterEmulatorTargets(options); const emulatorInTargets = targets.includes(name); @@ -213,32 +159,29 @@ export function shouldStart(options: any, name: Emulators): boolean { return true; } - if (options.config.get("emulators.ui.enabled") === false) { + if (options.config.src.emulators?.ui?.enabled === false) { // Allow disabling UI via `{emulators: {"ui": {"enabled": false}}}`. // Emulator UI is by default enabled if that option is not specified. return false; } // Emulator UI only starts if we know the project ID AND at least one // emulator supported by Emulator UI is launching. - return ( - !!options.project && targets.some((target) => EMULATORS_SUPPORTED_BY_UI.includes(target)) - ); + return targets.some((target) => EMULATORS_SUPPORTED_BY_UI.includes(target)); } - // Don't start the functions emulator if we can't find the source directory - if ( - name === Emulators.FUNCTIONS && - emulatorInTargets && - !options.config.get("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; + // Don't start the functions emulator if we can't validate the functions config + if (name === Emulators.FUNCTIONS && emulatorInTargets) { + try { + normalizeAndValidate(options.config.src.functions); + return true; + } catch (err: any) { + EmulatorLogger.forEmulator(Emulators.FUNCTIONS).logLabeled( + "ERROR", + "functions", + `Failed to start Functions emulator: ${err.message}`, + ); + return false; + } } if (name === Emulators.HOSTING && emulatorInTargets && !options.config.get("hosting")) { @@ -246,8 +189,8 @@ export function shouldStart(options: any, 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; } @@ -256,6 +199,11 @@ export function shouldStart(options: any, 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; @@ -284,7 +232,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; @@ -304,14 +252,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: any, 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": { @@ -326,26 +286,45 @@ export async function startAll(options: any, showUI: boolean = true): Promise { 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)) { @@ -353,25 +332,139 @@ export async function startAll(options: any, showUI: boolean = true): Promise; + 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); + listenConfig[Emulators.TASKS] = getListenConfig(options, Emulators.TASKS); + } + for (const emulator of ALL_EMULATORS) { + if ( + emulator === Emulators.FUNCTIONS || + emulator === Emulators.EVENTARC || + emulator === Emulators.TASKS || + // 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, + }; + } + if (emulator === Emulators.DATACONNECT && !dataConnectLocalConnString()) { + const pglitePortConfig = options.config.src.emulators?.dataconnect?.postgresPort; + listenConfig["dataconnect.postgres"] = { + host: config.host, + port: pglitePortConfig || 5432, + portFixed: !!pglitePortConfig, + }; + } + } + } + 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 (shouldStart(options, Emulators.HUB)) { - const hubAddr = await getAndCheckAddress(Emulators.HUB, options); - const hub = new EmulatorHub({ projectId, ...hubAddr }); + 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. await startEmulator(hub); } @@ -380,102 +473,228 @@ export async function startAll(options: any, showUI: boolean = true): Promise 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, ); + } - let inspectFunctions: number | undefined; - if (options.inspectFunctions) { - inspectFunctions = commandUtils.parseInspectionPort(options); + 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 localCfg = requireLocal( + cfg, + "Remote sources are not supported in the Functions emulator.", + ); + const functionsDir = path.join(projectDir, localCfg.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}`, + ); + } + const backend: EmulatableBackend = { + functionsDir, + runtime, + codebase: localCfg.codebase, + prefix: localCfg.prefix, + 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, + ignore: localCfg.ignore, + }; + proto.convertIfPresent(backend, localCfg, "configDir", (cd) => path.join(projectDir, cd)); + emulatableBackends.push(backend); + } + } + if (extensionEmulator) { + await startEmulator(extensionEmulator); + } + + const account = getProjectDefaultAccount(options.projectRoot); + + if (emulatableBackends.length) { + if (!listenForEmulator.functions || !listenForEmulator.eventarc || !listenForEmulator.tasks) { + // 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), + tasks: listenForEmulator.eventarc ?? getListenConfig(options, Emulators.TASKS), + }); + 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 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) { + 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); + // 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, - }, - predefinedTriggers: options.extensionTriggers, - nodeMajorVersion: parseRuntimeVersion( - options.extensionNodeVersion || options.config.get("functions.runtime") - ), + verbosity: options.logVerbosity, + projectAlias: options.projectAlias, + extensionsEmulator: extensionEmulator, }); await startEmulator(functionsEmulator); + + const eventarcAddr = legacyGetFirstAddr(Emulators.EVENTARC); + const eventarcEmulator = new EventarcEmulator({ + host: eventarcAddr.host, + port: eventarcAddr.port, + }); + await startEmulator(eventarcEmulator); + + const tasksAddr = legacyGetFirstAddr(Emulators.TASKS); + const tasksEmulator = new TasksEmulator({ + host: tasksAddr.host, + port: tasksAddr.port, + }); + + await startEmulator(tasksEmulator); } - 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, }; if (exportMetadata.firestore) { + utils.assertIsString(options.import); 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 as Config; - const rulesLocalPath = config.get("firestore.rules"); - let rulesFileFound = false; + const config = options.config; + // 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); @@ -485,14 +704,14 @@ export async function startAll(options: any, showUI: boolean = true): Promise 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"); @@ -577,39 +815,29 @@ export async function startAll(options: any, showUI: boolean = true): Promise 1) { + logger.warn( + `TODO: Add support for multiple services in the Data Connect emulator. Currently emulating first service ${config[0].source}`, + ); + } + + const args: DataConnectEmulatorArgs = { + listen: listenForEmulator.dataconnect, + projectId, + auto_download: true, + configDir: config[0].source, + config: options.config, + autoconnectToPostgres: true, + postgresListen: listenForEmulator["dataconnect.postgres"], + enable_output_generated_sdk: true, // TODO: source from arguments + enable_output_schema_extensions: true, + debug: options.debug, + account, + }; - 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 (exportMetadata.dataconnect) { + utils.assertIsString(options.import); + const importDirAbsPath = path.resolve(options.import); + const exportMetadataFilePath = path.resolve( + importDirAbsPath, + exportMetadata.dataconnect.path, ); + const dataDirectory = options.config.get("emulators.dataconnect.dataDir"); + if (exportMetadataFilePath && dataDirectory) { + EmulatorLogger.forEmulator(Emulators.DATACONNECT).logLabeled( + "WARN", + "dataconnect", + "'firebase.json#emulators.dataconnect.dataDir' is set and `--import` flag was passed. " + + "This will overwrite any data saved from previous runs.", + ); + if ( + !options.nonInteractive && + !(await confirm({ + message: `Do you wish to continue and overwrite data in ${dataDirectory}?`, + default: false, + })) + ) { + await cleanShutdown(); + throw new FirebaseError("Command aborted"); + } + } + + EmulatorLogger.forEmulator(Emulators.DATACONNECT).logLabeled( + "BULLET", + "dataconnect", + `Importing data from ${exportMetadataFilePath}`, + ); + args.importPath = exportMetadataFilePath; + void trackEmulator("emulator_import", { + initiated_by: "start", + emulator_name: Emulators.DATACONNECT, + }); } + const dataConnectEmulator = new DataConnectEmulator(args); + await startEmulator(dataConnectEmulator); + } + + if (listenForEmulator.storage) { + const storageAddr = legacyGetFirstAddr(Emulators.STORAGE); + const storageEmulator = new StorageEmulator({ host: storageAddr.host, port: storageAddr.port, projectId, - rules: options.config.path(storageConfig.rules), + rules: getStorageRulesConfig(projectId, options), }); await startEmulator(storageEmulator); + + if (exportMetadata.storage) { + utils.assertIsString(options.import); + const importDirAbsPath = path.resolve(options.import); + const storageExportDir = path.resolve(importDirAbsPath, exportMetadata.storage.path); + 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, @@ -651,39 +946,133 @@ export async function startAll(options: any, showUI: boolean = true): Promise resolveProjectPath({}, path.join(".", config.rootDir ?? "/")) === backendRoot, + ); + if (matchingAppHostingConfig.length === 1) { + apphostingConfig = matchingAppHostingConfig[0]; + } + } else { + apphostingConfig = options.config.src.apphosting; + } + + const apphostingAddr = legacyGetFirstAddr(Emulators.APPHOSTING); + if (apphostingEmulatorConfig?.startCommandOverride) { + const apphostingLogger = EmulatorLogger.forEmulator(Emulators.APPHOSTING); + apphostingLogger.logLabeled( + "WARN", + Emulators.APPHOSTING, + "The `firebase.json#emulators.apphosting.startCommandOverride` config is deprecated, please use `firebase.json#emulators.apphosting.startCommand` to set a custom start command instead", + ); + } + const apphostingEmulator = new AppHostingEmulator({ + projectId: options.project, + backendId: apphostingConfig?.backendId, + host: apphostingAddr.host, + port: apphostingAddr.port, + startCommand: + apphostingEmulatorConfig?.startCommand || apphostingEmulatorConfig?.startCommandOverride, + rootDirectory, + options, + }); + + await startEmulator(apphostingEmulator); } - 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 because none of the running " + + "emulators have a UI component.", + ); + } + + if (listenForEmulator.ui) { const ui = new EmulatorUI({ 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, + }; } /** @@ -691,57 +1080,51 @@ export async function startAll(options: any, showUI: boolean = true): Promise { + 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/dataconnect/pgliteServer.ts b/src/emulator/dataconnect/pgliteServer.ts new file mode 100644 index 00000000000..381a91143a4 --- /dev/null +++ b/src/emulator/dataconnect/pgliteServer.ts @@ -0,0 +1,331 @@ +// https://github.com/supabase-community/pg-gateway + +import { DebugLevel, PGlite, PGliteOptions } from "@electric-sql/pglite"; +// Unfortunately, we need to dynamically import the Postgres extensions. +// They are only available as ESM, and if we import them normally, +// our tsconfig will convert them to requires, which will cause errors +// during module resolution. +const { dynamicImport } = require(true && "../../dynamicImport"); +import * as net from "node:net"; +import { Readable, Writable } from "node:stream"; +import * as fs from "fs"; +import * as path from "node:path"; + +import { + getMessages, + PostgresConnection, + type PostgresConnectionOptions, + FrontendMessageCode, + BackendMessageCode, +} from "pg-gateway"; +import { logger } from "../../logger"; +import { hasMessage, FirebaseError } from "../../error"; +import { moveAll } from "../../fsutils"; +import { StringDecoder } from "node:string_decoder"; + +export const TRUNCATE_TABLES_SQL = ` +DO $do$ +DECLARE _clear text; +BEGIN + SELECT 'TRUNCATE TABLE ' || string_agg(oid::regclass::text, ', ') || ' CASCADE' + FROM pg_class + WHERE relkind = 'r' + AND relnamespace = 'public'::regnamespace + INTO _clear; + EXECUTE COALESCE(_clear, 'select now()'); +END +$do$;`; + +const decoder = new StringDecoder(); + +export class PostgresServer { + private baseDataDirectory?: string; + private importPath?: string; + private debug: DebugLevel; + + public db: PGlite | undefined = undefined; + private server: net.Server | undefined = undefined; + + public async createPGServer(host: string = "127.0.0.1", port: number): Promise { + const getDb = this.getDb.bind(this); + + const server = net.createServer(async (socket) => { + const connection = await fromNodeSocket(socket, { + serverVersion: "17.4 (PGlite 0.3.3)", + auth: { method: "trust" }, + + async *onMessage(data: Uint8Array, { isAuthenticated }: { isAuthenticated: boolean }) { + // Only forward messages to PGlite after authentication + if (!isAuthenticated) { + return; + } + const db = await getDb(); + if (data[0] === FrontendMessageCode.Terminate) { + // When the frontend terminates a connection, throw out all prepared statements + // because the next client won't know about them (and may create overlapping statements) + await db.query("DEALLOCATE ALL"); + } + const response = await db.execProtocolRaw(data); + for await (const message of extendedQueryPatch.filterResponse(data, response)) { + yield message; + } + + // Extended query patch removes the extra Ready for Query messages that + // pglite wrongly sends. + }, + }); + + const extendedQueryPatch: PGliteExtendedQueryPatch = new PGliteExtendedQueryPatch(connection); + socket.on("end", () => { + logger.debug("Postgres client disconnected"); + }); + socket.on("error", (err) => { + server.emit("error", err); + }); + }); + this.server = server; + + const listeningPromise = new Promise((resolve) => { + server.listen(port, host, () => { + resolve(); + }); + }); + await listeningPromise; + return server; + } + + async getDb(): Promise { + if (!this.db) { + this.db = await this.forceCreateDB(); + } + return this.db; + } + + private async getExtensions() { + const vector = (await dynamicImport("@electric-sql/pglite/vector")).vector; + const uuidOssp = (await dynamicImport("@electric-sql/pglite/contrib/uuid_ossp")).uuid_ossp; + return { vector, uuidOssp }; + } + + public async clearDb(): Promise { + const db = await this.getDb(); + await db.query(TRUNCATE_TABLES_SQL); + } + + public async exportData(exportPath: string): Promise { + const db = await this.getDb(); + const dump = await db.dumpDataDir(); + const arrayBuff = await dump.arrayBuffer(); + fs.writeFileSync(exportPath, new Uint8Array(arrayBuff)); + } + + private async migrateDb(pgliteArgs: PGliteOptions): Promise { + if (!this.baseDataDirectory) { + throw new FirebaseError("Cannot migrate database without a data directory."); + } + + // 1. Import old PGlite and pgDump + const { PGlite: PGlite02 } = await dynamicImport("pglite-2"); + const pgDump = (await dynamicImport("@electric-sql/pglite-tools/pg_dump")).pgDump; + + // 2. Open old DB with old PGlite + + logger.info("Opening database with Postgres 16..."); + const extensions = await this.getExtensions(); + const dataDir = this.baseDataDirectory; + const oldDb = new PGlite02({ ...pgliteArgs, dataDir }); + await oldDb.waitReady; + + const oldVersion = await (oldDb as PGlite).query<{ version: string }>("SELECT version();"); + logger.debug(`Old database version: ${oldVersion.rows[0].version}`); + if (!oldVersion.rows[0].version.includes("PostgreSQL 16")) { + await oldDb.close(); + throw new FirebaseError("Migration started, but DB version is not PostgreSQL 16."); + } + + // 3. Dump data + logger.info("Dumping data from old database..."); + const dumpDir = await oldDb.dumpDataDir("none"); + const tempOldDb = await PGlite02.create({ + loadDataDir: dumpDir, + extensions, + }); + + const dumpResult = await pgDump({ pg: tempOldDb, args: ["--verbose", "--verbose"] }); + await tempOldDb.close(); + await oldDb.close(); + + // 4. Move old dataDir to pg16 directory + logger.info(`Moving old database directory to ${this.baseDataDirectory}/pg16...`); + const pg16Dir = this.getVersionedDataDir(16)!; + moveAll(this.baseDataDirectory, pg16Dir); + logger.info( + "If you need to use an older version of the Firebase CLI, you can restore from that directory.", + ); + + // 5. Create new DB with new PGlite + logger.info("Creating new database with Postgres 17..."); + const pg17Dir = this.getVersionedDataDir(17)!; + const newDb = new PGlite({ ...pgliteArgs, dataDir: pg17Dir }); + await newDb.waitReady; + + // 6. Import data + logger.info("Importing data into new database..."); + const dumpText = await dumpResult.text(); + await newDb.exec(dumpText); + await newDb.exec("SET SEARCH_PATH = public;"); + + logger.info("Postgres database migration successful."); + return newDb; + } + + // When we upgrade Postgres versions, we need to migrate old data. To make this simpler, + // we started using versioned subdirectories of the dataDir. + // Note that we did not do this originally, so PG16 data is often found in the baseDataDir + private getVersionedDataDir(version: number): string | undefined { + if (!this.baseDataDirectory) { + return; + } + return path.join(this.baseDataDirectory, `pg${version}`); + } + + async forceCreateDB(): Promise { + const baseArgs: PGliteOptions = { + debug: this.debug, + extensions: await this.getExtensions(), + }; + + const pg17Dir = this.getVersionedDataDir(17); + // First, ensure that the data directory exists - PGLite tries to do this but doesn't do so recursively + if (pg17Dir && !fs.existsSync(pg17Dir)) { + fs.mkdirSync(pg17Dir, { recursive: true }); + } + + if (this.importPath) { + logger.debug(`Importing from ${this.importPath}`); + const rf = fs.readFileSync(this.importPath) as unknown as BlobPart; + const file = new File([rf], this.importPath); + baseArgs.loadDataDir = file; + } + + // Detect and handle migration from older versions. Originally, we did not do versioned subdirectories, + // so we just check the base directory here + if (this.baseDataDirectory && fs.existsSync(this.baseDataDirectory)) { + const versionFilePath = path.join(this.baseDataDirectory, "PG_VERSION"); + if (fs.existsSync(versionFilePath)) { + const version = fs.readFileSync(versionFilePath, "utf-8").trim(); + logger.debug(`Found Postgres version file with version: ${version}`); + if (version === "16") { + logger.info( + "Detected a Postgres 16 data directory from an older version of firebase-tools. Migrating to Postgres 17...", + ); + return this.migrateDb(baseArgs); + } + } + } + + try { + const db = new PGlite({ ...baseArgs, dataDir: pg17Dir }); + await db.waitReady; + return db; + } catch (err: unknown) { + if (pg17Dir && hasMessage(err) && /Database already exists/.test(err.message)) { + // Clear out the current pglite data + fs.rmSync(pg17Dir, { force: true, recursive: true }); + const db = new PGlite({ ...baseArgs, dataDir: pg17Dir }); + await db.waitReady; + return db; + } + logger.warn(`Error from pglite: ${err}`); + throw new FirebaseError("Unexpected error starting up Postgres."); + } + } + + public async stop(): Promise { + if (this.db) { + await this.db.close(); + } + if (this.server) { + this.server.close(); + } + return; + } + + constructor(args: { dataDirectory?: string; importPath?: string; debug?: boolean }) { + this.baseDataDirectory = args.dataDirectory; + this.importPath = args.importPath; + this.debug = args.debug ? 1 : 0; + } +} + +/** + * Creates a `PostgresConnection` from a Node.js TCP/Unix `Socket`. + * + * `PostgresConnection` operates on web streams, so this helper + * converts a `Socket` to/from the respective web streams. + * + * Also implements `upgradeTls()`, which makes Postgres `SSLRequest` + * upgrades available in Node.js environments. + */ +export async function fromNodeSocket(socket: net.Socket, options?: PostgresConnectionOptions) { + const rs = Readable.toWeb(socket) as unknown as ReadableStream; + const ws = Writable.toWeb(socket); + const opts = options + ? { + ...options, + } + : undefined; + + return new PostgresConnection({ readable: rs, writable: ws }, opts); +} + +export class PGliteExtendedQueryPatch { + isExtendedQuery = false; + eqpErrored = false; + pgliteDebugLog: fs.WriteStream; + + constructor(public connection: PostgresConnection) { + this.pgliteDebugLog = fs.createWriteStream("pglite-debug.log"); + } + + async *filterResponse(message: Uint8Array, response: Uint8Array) { + // 'Parse' indicates the start of an extended query + const pipelineStartMessages: number[] = [ + FrontendMessageCode.Parse, + FrontendMessageCode.Bind, + FrontendMessageCode.Close, + ]; + const decoded = decoder.write(message as any as Buffer); + + this.pgliteDebugLog.write("Front: " + decoded); + + if (pipelineStartMessages.includes(message[0])) { + this.isExtendedQuery = true; + } + + // 'Sync' indicates the end of an extended query + if (message[0] === FrontendMessageCode.Sync) { + this.isExtendedQuery = false; + this.eqpErrored = false; + } + + // A PGlite response can contain multiple messages + for await (const bm of getMessages(response)) { + // After an ErrorMessage in extended query protocol, we should throw away messages until the next Sync + // (per https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY:~:text=When%20an%20error,for%20each%20Sync.)) + if (this.eqpErrored) { + continue; + } + if (this.isExtendedQuery && bm[0] === BackendMessageCode.ErrorMessage) { + this.eqpErrored = true; + } + // Filter out incorrect `ReadyForQuery` messages during the extended query protocol + if (this.isExtendedQuery && bm[0] === BackendMessageCode.ReadyForQuery) { + this.pgliteDebugLog.write("Filtered: " + decoder.write(bm as any as Buffer)); + continue; + } + this.pgliteDebugLog.write("Sent: " + decoder.write(bm as any as Buffer)); + yield bm; + } + } +} diff --git a/src/emulator/dataconnectEmulator.ts b/src/emulator/dataconnectEmulator.ts new file mode 100644 index 00000000000..554c8405ce5 --- /dev/null +++ b/src/emulator/dataconnectEmulator.ts @@ -0,0 +1,436 @@ +import * as childProcess from "child_process"; +import * as pg from "pg"; +import { EventEmitter } from "events"; +import * as clc from "colorette"; +import * as path from "path"; + +import { dataConnectLocalConnString, vertexAIOrigin } from "../api"; +import { Constants } from "./constants"; +import { + getPID, + start, + stop, + downloadIfNecessary, + isIncomaptibleArchError, + getDownloadDetails, +} from "./downloadableEmulators"; +import { EmulatorInfo, EmulatorInstance, Emulators, ListenSpec } from "./types"; +import { FirebaseError } from "../error"; +import { EmulatorLogger } from "./emulatorLogger"; +import { BuildResult, requiresVector } from "../dataconnect/types"; +import { listenSpecsToString } from "./portUtils"; +import { Client, ClientResponse } from "../apiv2"; +import { EmulatorRegistry } from "./registry"; +import { load } from "../dataconnect/load"; +import { Config } from "../config"; +import { PostgresServer, TRUNCATE_TABLES_SQL } from "./dataconnect/pgliteServer"; +import { cleanShutdown } from "./controller"; +import { connectableHostname } from "../utils"; +import { Account } from "../types/auth"; +import { ensure } from "../ensureApiEnabled"; +import { getCredentialPathAsync } from "../defaultCredentials"; + +export interface DataConnectEmulatorArgs { + projectId: string; + listen: ListenSpec[]; + configDir: string; + auto_download?: boolean; + config: Config; + autoconnectToPostgres: boolean; + postgresListen?: ListenSpec[]; + enable_output_schema_extensions: boolean; + enable_output_generated_sdk: boolean; + importPath?: string; + debug?: boolean; + extraEnv?: Record; + account?: Account; +} + +export interface DataConnectGenerateArgs { + configDir: string; + watch?: boolean; + account?: Account; +} + +export interface DataConnectBuildArgs { + configDir: string; + projectId?: string; + account?: Account; +} + +// 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; + private postgresServer: PostgresServer | undefined; + + constructor(private args: DataConnectEmulatorArgs) { + this.emulatorClient = new DataConnectEmulatorClient(); + } + private logger = EmulatorLogger.forEmulator(Emulators.DATACONNECT); + + async start(): Promise { + let resolvedConfigDir; + try { + resolvedConfigDir = this.args.config.path(this.args.configDir); + const info = await DataConnectEmulator.build({ + configDir: resolvedConfigDir, + account: this.args.account, + }); + if (requiresVector(info.metadata)) { + if (Constants.isDemoProject(this.args.projectId)) { + this.logger.logLabeled( + "WARN", + "dataconnect", + "Detected a 'demo-' project, but vector embeddings require a real project. Operations that use vector_embed will fail.", + ); + } else { + await ensure(this.args.projectId, vertexAIOrigin(), "dataconnect", /* silent=*/ true); + this.logger.logLabeled( + "WARN", + "dataconnect", + "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 env = await DataConnectEmulator.getEnv(this.args.account, this.args.extraEnv); + await start( + Emulators.DATACONNECT, + { + auto_download: this.args.auto_download, + listen: listenSpecsToString(this.args.listen), + config_dir: resolvedConfigDir, + enable_output_schema_extensions: this.args.enable_output_schema_extensions, + enable_output_generated_sdk: this.args.enable_output_generated_sdk, + }, + env, + ); + + this.usingExistingEmulator = false; + if (this.args.autoconnectToPostgres) { + const info = await load(this.args.projectId, this.args.config, this.args.configDir); + const dbId = info.dataConnectYaml.schema.datasource.postgresql?.database || "postgres"; + const serviceId = info.dataConnectYaml.serviceId; + const pgPort = this.args.postgresListen?.[0].port; + const pgHost = this.args.postgresListen?.[0].address; + let connStr = dataConnectLocalConnString(); + if (connStr) { + this.logger.logLabeled( + "INFO", + "dataconnect", + `FIREBASE_DATACONNECT_POSTGRESQL_STRING is set to ${clc.bold(connStr)} - using that instead of starting a new database`, + ); + } else if (pgHost && pgPort) { + let dataDirectory = this.args.config.get("emulators.dataconnect.dataDir"); + if (dataDirectory) { + dataDirectory = this.args.config.path(dataDirectory); + } + const postgresDumpPath = this.args.importPath + ? path.join(this.args.importPath, "postgres.tar.gz") + : undefined; + this.postgresServer = new PostgresServer({ + dataDirectory, + importPath: postgresDumpPath, + debug: this.args.debug, + }); + const server = await this.postgresServer.createPGServer(pgHost, pgPort); + const connectableHost = connectableHostname(pgHost); + connStr = `postgres://${connectableHost}:${pgPort}/${dbId}?sslmode=disable`; + server.on("error", (err: any) => { + if (err instanceof FirebaseError) { + this.logger.logLabeled("ERROR", "Data Connect", `${err}`); + } else { + this.logger.logLabeled( + "ERROR", + "dataconnect", + `Postgres threw an unexpected error, shutting down the Data Connect emulator: ${err}`, + ); + } + void cleanShutdown(); + }); + this.logger.logLabeled( + "INFO", + "dataconnect", + `Started up Postgres server, listening on ${JSON.stringify(server.address())}`, + ); + } + await this.connectToPostgres(new URL(connStr), dbId, serviceId); + } + 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", + "dataconnect", + "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", + "dataconnect", + "Skipping cleanup of Data Connect emulator, as it was not started by this process.", + ); + return; + } + if (this.postgresServer) { + await this.postgresServer.stop(); + } + 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; + } + + getVersion(): string { + return getDownloadDetails(Emulators.DATACONNECT).version; + } + + async clearData(): Promise { + if (this.postgresServer) { + await this.postgresServer.clearDb(); + } else { + const conn = new pg.Client(dataConnectLocalConnString()); + await conn.query(TRUNCATE_TABLES_SQL); + await conn.end(); + } + } + + async exportData(exportPath: string): Promise { + if (this.postgresServer) { + await this.postgresServer.exportData( + path.join(this.args.config.path(exportPath), "postgres.tar.gz"), + ); + } else { + throw new FirebaseError( + "The Data Connect emulator is currently connected to a separate Postgres instance. Export is not supported.", + ); + } + } + + static async generate(args: DataConnectGenerateArgs): Promise { + const commandInfo = await downloadIfNecessary(Emulators.DATACONNECT); + const cmd = ["--logtostderr", "-v=2", "sdk", "generate", `--config_dir=${args.configDir}`]; + if (args.watch) { + cmd.push("--watch"); + } + const env = await DataConnectEmulator.getEnv(args.account); + return new Promise((resolve, reject) => { + try { + const proc = childProcess.spawn(commandInfo.binary, cmd, { stdio: "inherit", env }); + proc.on("close", (code) => { + if (code === 0) { + // Command executed successfully + resolve(); + } else { + // Command failed + reject(new Error(`Command failed with exit code ${code}`)); + } + }); + + proc.on("error", (err) => { + // Handle errors like command not found + reject(err); + }); + } catch (e: any) { + if (isIncomaptibleArchError(e)) { + reject( + new FirebaseError( + `Unknown system error when running the Data Connect toolkit. ` + + `You may be able to fix this by installing Rosetta: ` + + `softwareupdate --install-rosetta`, + ), + ); + } else { + reject(e); + } + } + }); + } + + static async build(args: DataConnectBuildArgs): Promise { + const commandInfo = await downloadIfNecessary(Emulators.DATACONNECT); + const cmd = ["--logtostderr", "-v=2", "build", `--config_dir=${args.configDir}`]; + if (args.projectId) { + cmd.push(`--project_id=${args.projectId}`); + } + const env = await DataConnectEmulator.getEnv(args.account); + const res = childProcess.spawnSync(commandInfo.binary, cmd, { encoding: "utf-8", env }); + if (isIncomaptibleArchError(res.error)) { + throw new FirebaseError( + `Unkown system error when running the Data Connect toolkit. ` + + `You may be able to fix this by installing Rosetta: ` + + `softwareupdate --install-rosetta`, + ); + } + 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}`); + } + } + + public async connectToPostgres( + connectionString: URL, + database?: string, + serviceId?: string, + ): Promise { + if (!connectionString) { + const msg = `No Postgres connection found. The Data Connect emulator will not be able to execute operations.`; + 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}}...`); + connectionString.toString(); + await this.emulatorClient.configureEmulator({ + connectionString: connectionString.toString(), + database, + serviceId, + maxOpenConnections: 1, // PGlite only supports a single open connection at a time - otherwise, prepared statements will misbehave. + }); + this.logger.logLabeled( + "DEBUG", + "Data Connect", + `Successfully connected to ${connectionString}}`, + ); + 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, 2000)); + } + } + return false; + } + + static async getEnv( + account?: Account, + extraEnv: Record = {}, + ): Promise { + const credsEnv: Record = {}; + if (account) { + // If Firebase CLI is logged in, always pass in the credentials to FDC emulator. + const defaultCredPath = await getCredentialPathAsync(account); + if (defaultCredPath) { + credsEnv.GOOGLE_APPLICATION_CREDENTIALS = defaultCredPath; + } + } + return { ...process.env, ...extraEnv, ...credsEnv }; + } +} + +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; + // The max number of simultaneous Postgres connections the emulator may open + maxOpenConnections?: number; +}; + +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); + } +} + +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/dataconnectToolkitController.ts b/src/emulator/dataconnectToolkitController.ts new file mode 100644 index 00000000000..2c8c5d13d99 --- /dev/null +++ b/src/emulator/dataconnectToolkitController.ts @@ -0,0 +1,64 @@ +import { EmulatorInfo, Emulators } from "./types"; +import { FirebaseError } from "../error"; +import * as portUtils from "./portUtils"; +import { connectableHostname } from "../utils"; +import { DataConnectEmulator, DataConnectEmulatorArgs } from "./dataconnectEmulator"; +import { getDownloadDetails } from "./downloadableEmulators"; + +const name = "Data Connect Toolkit"; +/** + * Static controller for the VSCode Data Connect Toolkit + */ +export class DataConnectToolkitController { + static instance: DataConnectEmulator; + static isRunning = false; + + static async start(args: DataConnectEmulatorArgs): Promise { + if (this.isRunning || this.instance) { + throw new FirebaseError(`${name} is already running!`, {}); + } + this.instance = new DataConnectEmulator(args); + + // must be before we start as else on a quick 'Ctrl-C' after starting we could skip this emulator in cleanShutdown + this.isRunning = true; + + // Start the emulator and wait for it to grab its assigned port. + await this.instance.start(); + const info = this.instance.getInfo(); + await portUtils.waitForPortUsed(info.port, connectableHostname(info.host), info.timeout); + } + + static async stop(): Promise { + if (!this.isRunning) { + return; + } + + try { + await this.instance.stop(); + this.isRunning = false; + } catch (e: any) { + throw new FirebaseError(`Data Connect Toolkit failed to stop with error: ${e}`); + } + } + + static getVersion(): string { + return getDownloadDetails(Emulators.DATACONNECT).version; + } + + /** + * Get information about an emulator. + */ + static getInfo(): EmulatorInfo | undefined { + return this.instance.getInfo(); + } + + static getUrl(): string { + const info = this.instance.getInfo(); + + // handle ipv6 + if (info.host.includes(":")) { + return `http://[${info.host}]:${info.port}`; + } + return `http://${info.host}:${info.port}`; + } +} 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 f88629e1baa..76bd0a26008 100644 --- a/src/emulator/download.ts +++ b/src/emulator/download.ts @@ -1,29 +1,35 @@ -import { URL } from "url"; import * as crypto from "crypto"; import * as fs from "fs-extra"; import * as path from "path"; -import * as ProgressBar from "progress"; import * as tmp from "tmp"; -import * as unzipper from "unzipper"; -import { Client } from "../apiv2"; 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"; 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 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); @@ -45,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; @@ -79,51 +107,13 @@ function removeOldFiles( EmulatorLogger.forEmulator(name).logLabeled( "BULLET", name, - `Removing outdated emulator files: ${file}` + `Removing outdated emulator files: ${file}`, ); fs.removeSync(fullFilePath); } } } -/** - * Downloads the resource at `remoteUrl` to a temporary file. - * Resolves to the temporary file's name, rejects if there's any error. - * @param remoteUrl URL to download. - */ -async function downloadToTmp(remoteUrl: string): Promise { - const u = new URL(remoteUrl); - const c = new Client({ urlPrefix: u.origin, auth: false }); - const tmpfile = tmp.fileSync(); - const writeStream = fs.createWriteStream(tmpfile.name); - - const res = await c.request({ - method: "GET", - path: u.pathname, - queryParams: u.searchParams, - responseType: "stream", - resolveOnHTTPError: true, - }); - if (res.status !== 200) { - throw new FirebaseError(`download failed, status ${res.status}`, { exit: 1 }); - } - - const total = parseInt(res.response.headers.get("content-length") || "0", 10); - const totalMb = Math.ceil(total / 1000000); - const bar = new ProgressBar(`Progress: :bar (:percent of ${totalMb}MB)`, { total, head: ">" }); - - res.body.on("data", (chunk) => { - bar.tick(chunk.length); - }); - - await new Promise((resolve) => { - writeStream.on("finish", resolve); - res.body.pipe(writeStream); - }); - - return tmpfile.name; -} - /** * Checks whether the file at `filepath` has the expected size. */ @@ -135,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 }, + ), ); }); } @@ -156,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 e268b9f4ff8..bd3f7489f8c --- a/src/emulator/downloadableEmulators.ts +++ b/src/emulator/downloadableEmulators.ts @@ -1,116 +1,135 @@ +const lsofi = require("lsofi"); import { Emulators, DownloadableEmulators, DownloadableEmulatorCommand, DownloadableEmulatorDetails, EmulatorDownloadDetails, + EmulatorUpdateDetails, } from "./types"; import { Constants } from "./constants"; -import { FirebaseError } from "../error"; +import { FirebaseError, hasMessage } from "../error"; 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"; +import * as process from "process"; +import * as emulatorUpdateDetails from "./downloadableEmulatorInfo.json"; 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: { + database: EmulatorUpdateDetails; + firestore: EmulatorUpdateDetails; + storage: EmulatorUpdateDetails; + pubsub: EmulatorUpdateDetails; + ui: { + main: EmulatorUpdateDetails; + snapshot: EmulatorUpdateDetails; + }; + dataconnect: { + darwin: EmulatorUpdateDetails; + win32: EmulatorUpdateDetails; + linux: EmulatorUpdateDetails; + }; +} = emulatorUpdateDetails; + +const emulatorUiDetails = experiments.isEnabled("emulatoruisnapshot") + ? EMULATOR_UPDATE_DETAILS.ui.snapshot + : EMULATOR_UPDATE_DETAILS.ui.main; +const dataconnectDetails = + process.platform === "darwin" + ? EMULATOR_UPDATE_DETAILS.dataconnect.darwin + : process.platform === "win32" + ? EMULATOR_UPDATE_DETAILS.dataconnect.win32 + : EMULATOR_UPDATE_DETAILS.dataconnect.linux; 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, + EMULATOR_UPDATE_DETAILS.database.downloadPathRelativeToCacheDir, + ), + version: EMULATOR_UPDATE_DETAILS.database.version, opts: { + ...EMULATOR_UPDATE_DETAILS.database, cacheDir: CACHE_DIR, - remoteUrl: - "https://storage.googleapis.com/firebase-preview-drop/emulator/firebase-database-emulator-v4.7.2.jar", - expectedSize: 28910604, - expectedChecksum: "264e5df0c0661c064ef7dc9ce8179aba", namePrefix: "firebase-database-emulator", }, }, firestore: { - downloadPath: path.join(CACHE_DIR, "cloud-firestore-emulator-v1.11.14.jar"), - version: "1.11.14", + downloadPath: path.join( + CACHE_DIR, + EMULATOR_UPDATE_DETAILS.firestore.downloadPathRelativeToCacheDir, + ), + version: EMULATOR_UPDATE_DETAILS.firestore.version, opts: { + ...EMULATOR_UPDATE_DETAILS.firestore, cacheDir: CACHE_DIR, - remoteUrl: - "https://storage.googleapis.com/firebase-preview-drop/emulator/cloud-firestore-emulator-v1.11.14.jar", - expectedSize: 61723157, - expectedChecksum: "c3b7560e226faf0dfb36383e68ef66a2", namePrefix: "cloud-firestore-emulator", }, }, storage: { - downloadPath: path.join(CACHE_DIR, "cloud-storage-rules-runtime-v1.0.0.jar"), - version: "1.0.0", + downloadPath: path.join( + CACHE_DIR, + EMULATOR_UPDATE_DETAILS.storage.downloadPathRelativeToCacheDir, + ), + version: EMULATOR_UPDATE_DETAILS.storage.version, opts: { + ...EMULATOR_UPDATE_DETAILS.storage, cacheDir: CACHE_DIR, - remoteUrl: - "https://storage.googleapis.com/firebase-preview-drop/emulator/cloud-storage-rules-runtime-v1.0.0.jar", - expectedSize: 63857175, - expectedChecksum: "fd8577f82d42ee1c03ae9d12b888049c", namePrefix: "cloud-storage-rules-emulator", - skipChecksumAndSize: true, }, }, - ui: previews.storageemulator - ? { - version: "1.4.1-STORAGE", - downloadPath: path.join(CACHE_DIR, "ui-v1.4.1-STORAGE.zip"), - unzipDir: path.join(CACHE_DIR, "ui-v1.4.1-STORAGE"), - binaryPath: path.join(CACHE_DIR, "ui-v1.4.1-STORAGE", "server.bundle.js"), - opts: { - cacheDir: CACHE_DIR, - remoteUrl: - "https://storage.googleapis.com/firebase-preview-drop/emulator/ui-vStorageSnapshot.zip", - expectedSize: 3374020, - expectedChecksum: "7a82fed575a2b9a008d96080a7dcccdb", - namePrefix: "ui", - skipCache: true, - skipChecksumAndSize: true, - }, - } - : { - version: "1.4.2", - downloadPath: path.join(CACHE_DIR, "ui-v1.4.2.zip"), - unzipDir: path.join(CACHE_DIR, "ui-v1.4.2"), - binaryPath: path.join(CACHE_DIR, "ui-v1.4.2", "server.bundle.js"), - opts: { - cacheDir: CACHE_DIR, - remoteUrl: "https://storage.googleapis.com/firebase-preview-drop/emulator/ui-v1.4.2.zip", - expectedSize: 3259556, - expectedChecksum: "651b9f2e548c319a615a5f8c03f76d02", - namePrefix: "ui", - }, - }, + ui: { + version: emulatorUiDetails.version, + downloadPath: path.join(CACHE_DIR, emulatorUiDetails.downloadPathRelativeToCacheDir), + unzipDir: path.join(CACHE_DIR, `ui-v${emulatorUiDetails.version}`), + binaryPath: path.join(CACHE_DIR, emulatorUiDetails.binaryPathRelativeToCacheDir!), + opts: { + ...emulatorUiDetails, + cacheDir: CACHE_DIR, + 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"), - binaryPath: path.join( + downloadPath: path.join( CACHE_DIR, - "pubsub-emulator-0.1.0", - `pubsub-emulator/bin/cloud-pubsub-emulator${process.platform === "win32" ? ".bat" : ""}` + EMULATOR_UPDATE_DETAILS.pubsub.downloadPathRelativeToCacheDir, ), + version: EMULATOR_UPDATE_DETAILS.pubsub.version, + unzipDir: path.join(CACHE_DIR, `pubsub-emulator-${EMULATOR_UPDATE_DETAILS.pubsub.version}`), + binaryPath: path.join(CACHE_DIR, EMULATOR_UPDATE_DETAILS.pubsub.binaryPathRelativeToCacheDir!), opts: { + ...EMULATOR_UPDATE_DETAILS.pubsub, cacheDir: CACHE_DIR, - remoteUrl: - "https://storage.googleapis.com/firebase-preview-drop/emulator/pubsub-emulator-0.1.0.zip", - expectedSize: 36623622, - expectedChecksum: "81704b24737d4968734d3e175f4cde71", namePrefix: "pubsub-emulator", }, }, + dataconnect: { + downloadPath: path.join(CACHE_DIR, dataconnectDetails.downloadPathRelativeToCacheDir), + version: dataconnectDetails.version, + binaryPath: path.join(CACHE_DIR, dataconnectDetails.downloadPathRelativeToCacheDir), + opts: { + ...dataconnectDetails, + cacheDir: CACHE_DIR, + skipChecksumAndSize: false, + namePrefix: "dataconnect-emulator", + auth: false, + }, + }, }; const EmulatorDetails: { [s in DownloadableEmulators]: DownloadableEmulatorDetails } = { @@ -139,14 +158,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", @@ -161,28 +192,61 @@ 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", getExecPath(Emulators.STORAGE), "serve"], + args: [ + // 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)], + binary: "", + args: [], optionalArgs: [], joinArgs: false, + shell: false, + }, + dataconnect: { + binary: `${getExecPath(Emulators.DATACONNECT)}`, + args: ["--logtostderr", "-v=2", "dev"], + optionalArgs: [ + "listen", + "config_dir", + "enable_output_schema_extensions", + "enable_output_generated_sdk", + // Additional flags that CLI shouldn't pass: + // rpc_retry_count, + // resolvers_emulator, + ], + joinArgs: true, + shell: false, }, }; @@ -203,12 +267,11 @@ export function getLogFileName(name: string): string { * @param emulator - string identifier for the emulator to start. * @param args - map of addittional args */ -function _getCommand( +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; @@ -258,19 +321,21 @@ function _getCommand( args: cmdLineArgs, optionalArgs: baseCmd.optionalArgs, joinArgs: baseCmd.joinArgs, + shell: baseCmd.shell, + port: args.port, }; } -async function _fatal(emulator: DownloadableEmulatorDetails, errorMsg: string): Promise { +async function _fatal(emulator: Emulators, errorMsg: string): Promise { // if we do not issue a stopAll here and _fatal is called during startup, we could leave emulators running // that did start already // for example: JAVA_HOME=/does/not/exist firebase emulators:start try { - const logger = EmulatorLogger.forEmulator(emulator.name); + const logger = EmulatorLogger.forEmulator(emulator); logger.logLabeled( "WARN", - emulator.name, - `Fatal error occurred: \n ${errorMsg}, \n stopping all running emulators` + emulator, + `Fatal error occurred: \n ${errorMsg}, \n stopping all running emulators`, ); await EmulatorRegistry.stopAll(); } finally { @@ -278,16 +343,53 @@ async function _fatal(emulator: DownloadableEmulatorDetails, errorMsg: string): } } +/** + * Handle errors in an emulator process. + */ +export async function handleEmulatorProcessError( + emulator: Emulators, + err: any, + port?: number, +): 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/`, + ); + } else if (err.code === "EADDRINUSE") { + const ps = port ? await lsofi(port) : false; + await _fatal( + emulator, + `${description} has exited because its configured port is already in use${ + ps ? ` by process number ${ps}` : "" + }. Are you running another copy of the emulator suite?`, + ); + } 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. @@ -295,18 +397,33 @@ 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.`, + ); + } else if (isIncomaptibleArchError(e)) { + logger.logLabeled( + "WARN", + emulator.name, + `Unknown system error when starting emulator binary. ` + + `You may be able to fix this by installing Rosetta: ` + + `softwareupdate --install-rosetta`, ); } - _fatal(emulator, e); + _fatal(emulator.name, e); } const description = Constants.description(emulator.name); @@ -319,14 +436,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); @@ -334,26 +451,25 @@ 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.", ); } - }); - emulator.instance.on("error", async (err: any) => { - 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/` - ); - } else { - await _fatal(emulator, `${description} has exited: ${err}`); + if (data.toString().includes("address already in use")) { + const message = `${description} has exited because its configured port ${command.port} is already in use. Are you running another copy of the emulator suite?`; + logger.logLabeled("ERROR", emulator.name, message); } }); + + emulator.instance.on("error", (err: any) => { + void handleEmulatorProcessError(emulator.name, err, command.port); + }); + emulator.instance.once("exit", async (code, signal) => { if (signal) { utils.logWarning(`${description} has exited upon receiving signal: ${signal}`); } else if (code && code !== 0 && code !== /* SIGINT */ 130) { - await _fatal(emulator, `${description} has exited with code: ${code}`); + await _fatal(emulator.name, `${description} has exited with code: ${code}`); } }); resolve(); @@ -364,7 +480,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; } /** @@ -390,7 +520,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 = @@ -398,7 +530,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(); @@ -413,14 +544,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]; } /** @@ -430,10 +562,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); @@ -442,8 +579,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.`, ); } @@ -458,7 +595,15 @@ 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); } + +export function isIncomaptibleArchError(err: unknown): boolean { + return ( + hasMessage(err) && + /Unknown system error/.test(err.message ?? "") && + process.platform === "darwin" + ); +} diff --git a/src/emulator/emulatorLogger.ts b/src/emulator/emulatorLogger.ts index 3cb20c20599..8468d001fb0 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,18 +255,19 @@ 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); break; case "function-runtimeconfig-json-invalid": this.log("WARN", "Found .runtimeconfig.json but the JSON format is invalid."); + break; default: // Silence } @@ -252,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; @@ -273,6 +310,9 @@ You can probably fix this by running "npm install ${systemLog.data.name}@latest" case "BULLET": utils.logLabeledBullet(label, text, "info", mergedData); break; + case "INFO": + utils.logLabeledBullet(label, text, "info", mergedData); + break; case "SUCCESS": utils.logLabeledSuccess(label, text, "info", mergedData); break; @@ -285,6 +325,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..9392207cb72 --- /dev/null +++ b/src/emulator/env.ts @@ -0,0 +1,115 @@ +import { Constants } from "./constants"; +import { EmulatorInfo, Emulators } from "./types"; +import { formatHost } from "./functionsEmulatorShared"; +import { Account } from "../types/auth/index"; +import { EmulatorLogger } from "./emulatorLogger"; +import { getCredentialPathAsync, hasDefaultCredentials } from "../defaultCredentials"; +import { FirestoreEmulatorInfo } from "./firestoreEmulator"; + +/** + * 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; + case Emulators.TASKS: + env[Constants.CLOUD_TASKS_EMULATOR_HOST] = host; + break; + case Emulators.DATACONNECT: + // Right now, the JS SDK requires a protocol within the env var. + // https://github.com/firebase/firebase-js-sdk/blob/88a8055808bdbd1c75011a94d11062460027d931/packages/data-connect/src/api/DataConnect.ts#L74 + env[Constants.FIREBASE_DATACONNECT_EMULATOR_HOST] = `http://${host}`; + // The alternative env var, right now only read by the Node.js Admin SDK, does not work if a protocol is appended. + // https://github.com/firebase/firebase-admin-node/blob/a46086b61f58f07426a6ca103e00385ae216691d/src/data-connect/data-connect-api-client-internal.ts#L220 + env[Constants.FIREBASE_DATACONNECT_ENV_ALT] = host; + // A previous CLI release set the following env var as well but it is missing an underscore between `DATA` and `CONNECT`. + // We'll keep setting this for customers who depends on this misspelled name. Its value is also kept protocol-less. + env["FIREBASE_DATACONNECT_EMULATOR_HOST"] = host; + } + } +} + +/** + * getCredentialsEnvironment returns any extra env vars beyond process.env that should be provided to emulators to ensure they have credentials. + */ +export async function getCredentialsEnvironment( + account: Account | undefined, + logger: EmulatorLogger, + logLabel: string, + silent: boolean = false, +): Promise> { + // Provide default application credentials when appropriate + const credentialEnv: Record = {}; + if (await hasDefaultCredentials()) { + !silent && + logger.logLabeled( + "WARN", + logLabel, + `Application Default Credentials detected. Non-emulated services will access production using these credentials. Be careful!`, + ); + } else if (account) { + const defaultCredPath = await getCredentialPathAsync(account); + if (defaultCredPath) { + logger.log("DEBUG", `Setting GAC to ${defaultCredPath}`); + credentialEnv.GOOGLE_APPLICATION_CREDENTIALS = defaultCredPath; + } + } + return credentialEnv; +} + +export function maybeUsePortForwarding(i: EmulatorInfo): EmulatorInfo { + const portForwardingHost = process.env.WEB_HOST; + if (portForwardingHost) { + const info = { ...i }; + if (info.host.includes(portForwardingHost)) { + // Never double apply this. Added as a safety check against sloppy usage. + return info; + } + const url = `${info.port}-${portForwardingHost}`; + info.host = url; + info.listen = info.listen?.map((listen) => { + const l = { ...listen }; + l.address = url; + l.port = 443; + return l; + }); + info.port = 443; + const fsInfo = info as FirestoreEmulatorInfo; + if (fsInfo.webSocketPort) { + fsInfo.webSocketHost = `${fsInfo.webSocketPort}-${portForwardingHost}`; + fsInfo.webSocketPort = 443; + } + return info; + } + return i; +} diff --git a/src/emulator/eventarcEmulator.ts b/src/emulator/eventarcEmulator.ts new file mode 100644 index 00000000000..12bbc5200a8 --- /dev/null +++ b/src/emulator/eventarcEmulator.ts @@ -0,0 +1,252 @@ +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"; +import * as cors from "cors"; + +interface EmulatedEventTrigger { + projectId: string; + triggerName: string; + eventTrigger: EventTrigger; +} + +interface RequestWithRawBody extends express.Request { + rawBody: Buffer; +} + +export interface EventarcEmulatorArgs { + port?: number; + host?: string; +} +const GOOGLE_CHANNEL = "google"; + +export class EventarcEmulator implements EmulatorInstance { + private destroyServer?: () => Promise; + + private logger = EmulatorLogger.forEmulator(Emulators.EVENTARC); + private events: { [key: string]: EmulatedEventTrigger[] } = {}; + + constructor(private args: EventarcEmulatorArgs) {} + + createHubServer(): express.Application { + const registerTriggerRoute = `/emulator/v1/projects/:project_id/triggers/:trigger_name(*)`; + const registerTriggerHandler: express.RequestHandler = (req, res) => { + try { + const { projectId, triggerName, eventTrigger, key } = getTriggerIdentifiers(req); + this.logger.logLabeled( + "BULLET", + "eventarc", + `Registering Eventarc event trigger for ${key} with trigger name ${triggerName}.`, + ); + const eventTriggers = this.events[key] || []; + eventTriggers.push({ projectId, triggerName, eventTrigger }); + this.events[key] = eventTriggers; + res.status(200).send({ res: "OK" }); + } catch (error) { + res.status(400).send({ error }); + } + }; + + const getTriggerIdentifiers = (req: express.Request) => { + 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); + throw error; + } + 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); + throw error; + } + const channel = eventTrigger.channel || GOOGLE_CHANNEL; + const key = `${eventTrigger.eventType}-${channel}`; + return { projectId, triggerName, eventTrigger, key }; + }; + + const removeTriggerRoute = `/emulator/v1/remove/projects/:project_id/triggers/:trigger_name(*)`; + const removeTriggerHandler: express.RequestHandler = (req, res) => { + try { + const { projectId, triggerName, eventTrigger, key } = getTriggerIdentifiers(req); + this.logger.logLabeled( + "BULLET", + "eventarc", + `Removing Eventarc event trigger for ${key} with trigger name ${triggerName}.`, + ); + const eventTriggers = this.events[key] || []; + const triggerIdentifier = { projectId, triggerName, eventTrigger }; + const removeIdx = eventTriggers.findIndex( + (e) => JSON.stringify(triggerIdentifier) === JSON.stringify(e), + ); + if (removeIdx === -1) { + this.logger.logLabeled("ERROR", "eventarc", "Tried to remove nonexistent trigger"); + throw new Error(`Unable to delete function trigger ${triggerName}`); + } + eventTriggers.splice(removeIdx, 1); + if (eventTriggers.length === 0) { + delete this.events[key]; + } else { + this.events[key] = eventTriggers; + } + res.status(200).send({ res: "OK" }); + } catch (error) { + res.status(400).send({ error }); + } + }; + + const getTriggersRoute = `/google/getTriggers`; + const getTriggersHandler: express.RequestHandler = (req, res) => { + res.status(200).send(this.events); + }; + + const publishEventsRoute = `/projects/:project_id/locations/:location/channels/:channel::publishEvents`; + const publishNativeEventsRoute = `/google/publishEvents`; + + const publishEventsHandler: express.RequestHandler = (req, res) => { + const isCustom = req.params.project_id && req.params.channel; + + const channel = isCustom + ? `projects/${req.params.project_id}/locations/${req.params.location}/channels/${req.params.channel}` + : GOOGLE_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 event at channel ${channel}: ${JSON.stringify(event, null, 2)}`, + ); + this.triggerEventFunction(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.post( + [publishNativeEventsRoute], + dataMiddleware, + cors({ origin: true }), + publishEventsHandler, + ); + hub.post([removeTriggerRoute], dataMiddleware, removeTriggerHandler); + hub.get([getTriggersRoute], cors({ origin: true }), getTriggersHandler); + hub.all("*", (req, res) => { + this.logger.log("DEBUG", `Eventarc emulator received unknown request at path ${req.path}`); + res.sendStatus(404); + }); + return hub; + } + + async triggerEventFunction(channel: string, event: CloudEvent): Promise { + 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.events[key] || []; + const eventPayload = channel === GOOGLE_CHANNEL ? event : cloudEventFromProtoToJson(event); + return await Promise.all( + triggers + .filter( + (trigger) => + !trigger.eventTrigger.eventFilters || + this.matchesAll(event, trigger.eventTrigger.eventFilters), + ) + .map((trigger) => this.callFunctionTrigger(trigger, eventPayload)), + ); + } + + callFunctionTrigger(trigger: EmulatedEventTrigger, event: CloudEvent): Promise { + return EmulatorRegistry.client(Emulators.FUNCTIONS) + .request, NodeJS.ReadableStream>({ + method: "POST", + path: `/functions/projects/${trigger.projectId}/triggers/${trigger.triggerName}`, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(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 7b3dcf187fe..17f14c940d7 100644 --- a/src/emulator/events/types.ts +++ b/src/emulator/events/types.ts @@ -5,10 +5,8 @@ * * 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"; /** * Wire formal for v1beta1 EventFlow. @@ -38,6 +36,57 @@ export interface Event { data: any; } +/** + * A CloudEvent is a cross-platform format for encoding a serverless event. + * More information can be found in https://github.com/cloudevents/spec + */ +export interface CloudEvent { + /** Version of the CloudEvents spec for this event. */ + specversion: string; + + /** A globally unique ID for this event. */ + id: string; + + /** The resource which published this event. */ + source: string; + + /** The resource, provided by source, that this event relates to */ + subject?: string; + + /** The type of event that this represents. */ + type: string; + + /** When this event occurred. */ + time: string; + + /** Information about this specific event. */ + data: T; + + /** + * A map of template parameter name to value for subject strings. + * + * This map is only available on some event types that allow templates + * in the subject string, such as Firestore. When listening to a document + * template "/users/{uid}", an event with subject "/documents/users/1234" + * would have a params of {"uid": "1234"}. + * + * Params are generated inside the firebase-functions SDK and are not + * part of the CloudEvents spec nor the payload that a Cloud Function + * 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">; + /** * Legacy AuthMode format. */ @@ -46,15 +95,47 @@ 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 determining event types. + * 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 { + return !!( + req.header("ce-type") && + req.header("ce-specversion") && + req.header("ce-source") && + req.header("ce-id") + ); + } + + static extractBinaryCloudEventContext(req: express.Request): CloudEventContext { + const context: Partial = {}; + for (const name of Object.keys(req.headers)) { + if (name.startsWith("ce-")) { + const attributeName = name.substr("ce-".length) as keyof CloudEventContext; + context[attributeName] = req.header(name); + } + } + return context as CloudEventContext; } } diff --git a/src/emulator/extensions/postinstall.spec.ts b/src/emulator/extensions/postinstall.spec.ts new file mode 100644 index 00000000000..27f2b133d27 --- /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()); + }); + } +}).timeout(2000); 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..56060e25d0d --- /dev/null +++ b/src/emulator/extensionsEmulator.spec.ts @@ -0,0 +1,147 @@ +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({ + options: {} as any, + 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..58dc5cc20a6 --- /dev/null +++ b/src/emulator/extensionsEmulator.ts @@ -0,0 +1,414 @@ +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"; +import * as Table from "cli-table3"; + +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"; +import { Build } from "../deploy/functions/build"; +import { extractExtensionsFromBuilds } from "../extensions/runtimes/common"; +import { populateDefaultParams } from "../extensions/paramHelper"; + +export interface ExtensionEmulatorArgs { + options: Options; + projectId: string; + projectNumber: string; + aliases?: string[]; + extensions: Record; + projectDir: string; +} + +export class ExtensionsEmulator implements EmulatorInstance { + private want: planner.DeploymentInstanceSpec[] = []; + private wantDynamic: Record = {}; + private backends: EmulatableBackend[] = []; + private staticBackends: EmulatableBackend[] = []; + private dynamicBackends: Record = {}; + 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 { + this.backends = await this.getStaticExtensionBackends(); + for (const backends of Object.values(this.dynamicBackends)) { + this.backends.push(...backends); + } + return this.backends; + } + + async getStaticExtensionBackends(): Promise { + await this.readManifest(); + await this.checkAndWarnAPIs(this.want); + this.staticBackends = await Promise.all( + this.want.map((i: planner.DeploymentInstanceSpec) => { + return this.toEmulatableBackend(i); + }), + ); + return this.staticBackends; + } + + public getDynamicExtensionBackends(): EmulatableBackend[] { + const dynamicBackends: EmulatableBackend[] = []; + for (const backends of Object.values(this.dynamicBackends)) { + dynamicBackends.push(...backends); + } + + return dynamicBackends; + } + + public async addDynamicExtensions(codebase: string, build: Build): Promise { + const extensions = extractExtensionsFromBuilds({ build }); + this.wantDynamic[codebase] = await planner.wantDynamic({ + projectId: this.args.projectId, + projectNumber: this.args.projectNumber, + extensions, + emulatorMode: true, + }); + await this.checkAndWarnAPIs(this.wantDynamic[codebase]); + this.dynamicBackends[codebase] = await Promise.all( + this.wantDynamic[codebase].map((i: planner.DeploymentInstanceSpec) => { + return this.toEmulatableBackend(i); + }), + ); + // Make sure the new entries are in this.backends + await this.getExtensionBackends(); + } + + /** + * 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 params = populateDefaultParams(instance.params, await planner.getExtensionSpec(instance)); + const env = Object.assign(this.autoPopulatedParams(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.join(", "), + 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(backends: EmulatableBackend[]): EmulatableBackend[] { + let foundUnemulatedTrigger = false; + const filteredBackends = backends.filter((backend) => { + const unemulatedServices = checkForUnemulatedTriggerTypes(backend, this.args.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(): string { + const filtedBackends = this.filterUnemulatedTriggers(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 39b736d52f2..88055cb30f3 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -1,56 +1,71 @@ -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 { EmulatedTriggerDefinition, - EmulatedTriggerType, - FunctionsRuntimeArgs, - FunctionsRuntimeBundle, + SignatureType, + EventSchedule, + EventTrigger, + formatHost, FunctionsRuntimeFeatures, - getFunctionRegion, getFunctionService, + getSignatureType, HttpConstants, - EventTrigger, - EventSchedule, + 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 { getCredentialPathAsync } from "../defaultCredentials"; +import { WorkQueue, Work } from "./workQueue"; +import { allSettled, connectableHostname, createDestroyer, debounce, randomInt } from "../utils"; import { - getProjectAdminSdkConfigOrCached, AdminSdkConfig, constructDefaultAdminSdkConfig, + getProjectAdminSdkConfigOrCached, } from "./adminSdkConfig"; - -const EVENT_INVOKE = "functions:invoke"; +import { functionIdsAreValid } from "../deploy/functions/validate"; +import { Extension, ExtensionSpec, ExtensionVersion } from "../extensions/types"; +import { accessSecretVersion } from "../gcp/secretManager"; +import * as backend from "../deploy/functions/backend"; +import * as build from "../deploy/functions/build"; +import * as runtimes from "../deploy/functions/runtimes"; +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 { getCredentialsEnvironment, setEnvVarsForEmulators } from "./env"; +import { runWithVirtualEnv } from "../functions/python"; +import { Runtime } from "../deploy/functions/runtimes/supported"; +import { ExtensionsEmulator } from "./extensionsEmulator"; + +const EVENT_INVOKE_GA4 = "functions_invoke"; // event name GA4 (alphanumertic) /* * The Realtime Database emulator expects the `path` field in its trigger @@ -64,43 +79,100 @@ 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; + configDir?: string; + env: Record; + secretEnv: backend.SecretEnvVar[]; + codebase: string; + prefix?: 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 + ignore?: string[]; +} + +/** + * 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 + labels?: Record; +} + 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?: { [key: string]: string }; - remoteEmulators?: { [key: string]: EmulatorInfo }; - predefinedTriggers?: EmulatedTriggerDefinition[]; - nodeMajorVersion?: number; // Lets us specify the node version when emulating extensions. + remoteEmulators?: Record; + adminSdkConfig?: AdminSdkConfig; + projectAlias?: string; + extensionsEmulator?: ExtensionsEmulator; +} + +/** + * 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?: EmulatedTriggerDefinition[]; - env?: { [key: string]: string }; + extensionTriggers?: ParsedTriggerDefinition[]; ignore_warnings?: boolean; } @@ -109,89 +181,128 @@ 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 } = {}; + private triggers: Record = {}; // 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 = {}; + + private staticBackends: EmulatableBackend[] = []; + private dynamicBackends: EmulatableBackend[] = []; + private watchers: chokidar.FSWatcher[] = []; + + debugMode = false; + constructor(private args: FunctionsEmulatorArgs) { + this.staticBackends = args.emulatableBackends; + // 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.staticBackends.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.staticBackends) { + const pool = new RuntimeWorkerPool(mode); + this.workerPools[backend.codebase] = pool; + } this.workQueue = new WorkQueue(mode); } - private async getCredentialsEnvironment(): Promise> { - // Provide default application credentials when appropriate - const credentialEnv: Record = {}; - if (process.env.GOOGLE_APPLICATION_CREDENTIALS) { - this.logger.logLabeled( - "WARN", + private async loadDynamicExtensionBackends(): Promise { + // New extensions defined in functions codebase create new backends + if (this.args.extensionsEmulator) { + const unfilteredBackends = this.args.extensionsEmulator.getDynamicExtensionBackends(); + this.dynamicBackends = + this.args.extensionsEmulator.filterUnemulatedTriggers(unfilteredBackends); + const mode = this.debugMode ? FunctionsExecutionMode.SEQUENTIAL : FunctionsExecutionMode.AUTO; + const credentialEnv = await getCredentialsEnvironment( + this.args.account, + this.logger, "functions", - `Your GOOGLE_APPLICATION_CREDENTIALS environment variable points to ${process.env.GOOGLE_APPLICATION_CREDENTIALS}. Non-emulated services will access production using these credentials. Be careful!` ); - } else if (this.args.account) { - const defaultCredPath = await getCredentialPathAsync(this.args.account); - if (defaultCredPath) { - this.logger.log("DEBUG", `Setting GAC to ${defaultCredPath}`); - credentialEnv.GOOGLE_APPLICATION_CREDENTIALS = defaultCredPath; + for (const backend of this.dynamicBackends) { + backend.env = { ...credentialEnv, ...backend.env }; + if (this.workerPools[backend.codebase]) { + // Make sure we don't have stale workers. + if (this.debugMode) { + this.workerPools[backend.codebase].exit(); + } else { + this.workerPools[backend.codebase].refresh(); + } + } else { + const pool = new RuntimeWorkerPool(mode); + this.workerPools[backend.codebase] = pool; + } + // They need to be force loaded because otherwise + // changes in parameters that don't otherwise affect the + // trigger might be missed. + await this.loadTriggers(backend, /* force */ true); } - } else { - // TODO: It would be safer to set GOOGLE_APPLICATION_CREDENTIALS to /dev/null here but we can't because some SDKs don't work - // without credentials even when talking to the emulator: https://github.com/firebase/firebase-js-sdk/issues/3144 - this.logger.logLabeled( - "WARN", - "functions", - "You are not signed in to the Firebase CLI. If you have authorized this machine using gcloud application-default credentials those may be discovered and used to access production services." - ); } - - return credentialEnv; } createHubServer(): 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(); @@ -209,7 +320,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`; @@ -220,57 +331,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 triggerId = req.params.trigger_name; - const projectId = req.params.project_id; - const reqBody = (req as RequestWithRawBody).rawBody; - const proto = JSON.parse(reqBody.toString()); - - 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 reqBody = (req as RequestWithRawBody).rawBody; - const proto = JSON.parse(reqBody.toString()); - const triggers = this.multicastTriggers[`${this.args.projectId}:${proto.eventType}`] || []; + const multicastHandler: express.RequestHandler = (req: express.Request, res) => { const projectId = req.params.project_id; + 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}:${event.type}`; + } else { + triggerKey = `${this.args.projectId}:${event.eventType}`; + } + if (event.data.bucket) { + triggerKey += `:${event.data.bucket}`; + } + const triggers = this.multicastTriggers[triggerKey] || []; + const { host, port } = this.getInfo(); triggers.forEach((triggerId) => { - 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) => { @@ -280,57 +398,66 @@ export class FunctionsEmulator implements EmulatorInstance { return hub; } - startFunctionRuntime( - triggerId: string, - triggerType: EmulatedTriggerType, - 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), - }, - nodeMajorVersion: this.args.nodeMajorVersion, - proto, - triggerId, - triggerType, - }; - const opts = runtimeOpts || { - nodeBinary: this.nodeBinary, - env: this.args.env, - 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); - 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 getCredentialsEnvironment( + this.args.account, + this.logger, + "functions", ); + for (const e of this.staticBackends) { + e.env = { ...credentialEnv, ...e.env }; + } - const credentialEnv = await this.getCredentialsEnvironment(); - this.args.env = { - ...credentialEnv, - ...this.args.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(); @@ -341,77 +468,185 @@ 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.staticBackends) { + 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 + ...(backend.ignore?.map((i) => `**/${i}`) ?? []), + ], + persistent: true, + }); - const debouncedLoadTriggers = _.debounce(() => this.loadTriggers(), 1000); - watcher.on("change", (filePath) => { - this.logger.log("DEBUG", `File ${filePath} changed, reloading triggers`); - return debouncedLoadTriggers(); - }); + this.watchers.push(watcher); + + 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(); + } + + for (const watcher of this.watchers) { + await watcher.close(); + } + this.watchers = []; + 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, + configDir: emulatableBackend.configDir, + }; + const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt); + const discoveredBuild = await runtimeDelegate.discoverBuild(runtimeConfig, environment); + if (discoveredBuild.extensions && this.args.extensionsEmulator) { + await this.args.extensionsEmulator.addDynamicExtensions( + emulatableBackend.codebase, + discoveredBuild, + ); + await this.loadDynamicExtensionBackends(); + } + build.applyPrefix(discoveredBuild, emulatableBackend.prefix || ""); + const resolution = await resolveBackend({ + build: discoveredBuild, + firebaseConfig: JSON.parse(firebaseConfig), + userEnvs, + nonInteractive: false, + isEmulator: true, + }); + + functionsEnv.writeResolvedParams(resolution.envs, userEnvs, userEnvOpt); + 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: boolean = false) { + 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(); + 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(); + } - const worker = this.invokeRuntime(this.getBaseBundle(), { - nodeBinary: this.nodeBinary, - env: this.args.env, - extensionTriggers: this.args.predefinedTriggers, + // Remove any old trigger definitions + const toRemove = Object.keys(this.triggers).filter((recordKey) => { + const record = this.getTriggerRecordByKey(recordKey); + if (record.backend.codebase !== emulatableBackend.codebase) { + // Order is important here. This needs to go before any other checks. + // We are only loading one codebase, don't delete triggers from another. + return false; + } + if (force) { + return true; // We are going to load all of the triggers anyway, so we can remove everything + } + return !triggerDefinitions.some( + (def) => + record.def.entryPoint === def.entryPoint && + JSON.stringify(record.def.eventTrigger) === JSON.stringify(def.eventTrigger), + ); }); - - const triggerParseEvent = await EmulatorLog.waitForLog( - worker.runtime.events, - "SYSTEM", - "triggers-parsed" - ); - const triggerDefinitions = triggerParseEvent.data - .triggerDefinitions as EmulatedTriggerDefinition[]; + await this.removeTriggers(toRemove); // When force is true we set up all triggers, otherwise we only set up // triggers which have a unique function name @@ -419,49 +654,80 @@ 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. + // with the same entryPoint / trigger. const anyEnabledMatch = Object.values(this.triggers).some((record) => { - return record.def.entryPoint === definition.entryPoint && record.enabled; + const sameEntryPoint = record.def.entryPoint === definition.entryPoint; + + // If they both have event triggers, make sure they match + const sameEventTrigger = + JSON.stringify(record.def.eventTrigger) === JSON.stringify(definition.eventTrigger); + + if (sameEntryPoint && !sameEventTrigger) { + this.logger.log( + "DEBUG", + `Definition for trigger ${definition.entryPoint} changed from ${JSON.stringify( + record.def.eventTrigger, + )} to ${JSON.stringify(definition.eventTrigger)}`, + ); + } + + return record.enabled && sameEntryPoint && sameEventTrigger; }); return !anyEnabledMatch; }); for (const definition of toSetup) { + // Skip function with invalid id. + try { + // 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) { - // TODO(samstern): Right now we only emulate each function in one region, but it's possible - // that a developer is running the same function in multiple regions. - const region = getFunctionRegion(definition); - const { host, port } = this.getInfo(); added = true; url = FunctionsEmulator.getHttpFunctionUrl( - host, - port, this.args.projectId, definition.name, - region + definition.region, ); + if (definition.taskQueueTrigger) { + added = await this.addTaskQueueTrigger( + this.args.projectId, + definition.region, + definition.name, + url, + definition.taskQueueTrigger, + ); + } } else if (definition.eventTrigger) { const service: string = getFunctionService(definition); const key = this.getTriggerKey(definition); + const signature = getSignatureType(definition); switch (service) { case Constants.SERVICE_FIRESTORE: 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: @@ -469,7 +735,15 @@ export class FunctionsEmulator implements EmulatorInstance { definition.name, key, definition.eventTrigger, - definition.schedule + signature, + definition.schedule, + ); + break; + case Constants.SERVICE_EVENTARC: + added = await this.addEventarcTrigger( + this.args.projectId, + key, + definition.eventTrigger, ); break; case Constants.SERVICE_AUTH: @@ -478,138 +752,442 @@ export class FunctionsEmulator implements EmulatorInstance { case Constants.SERVICE_STORAGE: added = this.addStorageTrigger(this.args.projectId, key, definition.eventTrigger); break; + case Constants.SERVICE_FIREALERTS: + added = await this.addFirealertsTrigger( + this.args.projectId, + key, + definition.eventTrigger, + ); + break; default: this.logger.log("DEBUG", `Unsupported trigger: ${JSON.stringify(definition)}`); break; } + } else if (definition.blockingTrigger) { + url = FunctionsEmulator.getHttpFunctionUrl( + this.args.projectId, + definition.name, + definition.region, + ); + added = this.addBlockingTrigger(url, definition.blockingTrigger); } else { this.logger.log( "WARN", - `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 + const triggerType = definition.httpsTrigger ? "http" : Constants.getServiceName(getFunctionService(definition)); if (ignored) { - const msg = `function ignored because the ${type} emulator does not exist or is not running.`; - this.logger.logLabeled("BULLET", `functions[${definition.name}]`, msg); + const msg = `function ignored because the ${triggerType} emulator does not exist or is not running.`; + this.logger.logLabeled("BULLET", `functions[${definition.id}]`, msg); } else { const msg = url - ? `${clc.bold(type)} function initialized (${url}).` - : `${clc.bold(type)} function initialized.`; - this.logger.logLabeled("SUCCESS", `functions[${definition.name}]`, msg); + ? `${clc.bold(triggerType)} function initialized (${url}).` + : `${clc.bold(triggerType)} function initialized.`; + 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}`, + ); + } + } + } + } + + // Currently only cleans up eventarc and firealerts triggers + async removeTriggers(toRemove: string[]) { + for (const triggerKey of toRemove) { + const definition = this.triggers[triggerKey].def; + const service = getFunctionService(definition); + const key = this.getTriggerKey(definition); + + switch (service) { + case Constants.SERVICE_EVENTARC: + await this.removeEventarcTrigger( + this.args.projectId, + key, + definition.eventTrigger as EventTrigger, + ); + delete this.triggers[key]; + break; + case Constants.SERVICE_FIREALERTS: + await this.removeFirealertsTrigger( + this.args.projectId, + key, + definition.eventTrigger as EventTrigger, + ); + delete this.triggers[key]; + break; + default: + break; } } } - addRealtimeDatabaseTrigger( + async addEventarcTrigger( projectId: string, key: string, - eventTrigger: EventTrigger + eventTrigger: EventTrigger, ): Promise { - const databaseEmu = EmulatorRegistry.get(Emulators.DATABASE); - if (!databaseEmu) { - return Promise.resolve(false); + if (!EmulatorRegistry.isRunning(Emulators.EVENTARC)) { + return false; } + const bundle = { + eventTrigger: { + ...eventTrigger, + service: "eventarc.googleapis.com", + }, + }; + logger.debug(`addEventarcTrigger`, JSON.stringify(bundle)); - 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}` + try { + await EmulatorRegistry.client(Emulators.EVENTARC).post( + `/emulator/v1/projects/${projectId}/triggers/${key}`, + bundle, ); - return Promise.reject(); + return true; + } catch (err: unknown) { + this.logger.log("WARN", "Error adding Eventarc function: " + err); } + return false; + } - const instance = result[1]; - const bundle = JSON.stringify({ - name: `projects/${projectId}/locations/_/functions/${key}`, - path: result[2], // path stored in the second capture group - event: eventTrigger.eventType, - topic: `projects/${projectId}/topics/${key}`, - }); - - logger.debug(`addRealtimeDatabaseTrigger[${instance}]`, JSON.stringify(bundle)); - - let setTriggersPath = "/.settings/functionTriggers.json"; - if (instance !== "") { - setTriggersPath += `?ns=${instance}`; - } else { - this.logger.log( - "WARN", - `No project in use. Registering function trigger for sentinel namespace '${Constants.DEFAULT_DATABASE_EMULATOR_NAMESPACE}'` + async removeEventarcTrigger( + 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(`removeEventarcTrigger`, JSON.stringify(bundle)); + try { + await EmulatorRegistry.client(Emulators.EVENTARC).post( + `/emulator/v1/remove/projects/${projectId}/triggers/${key}`, + bundle, ); + return true; + } catch (err: unknown) { + this.logger.log("WARN", "Error removing Eventarc function: " + err); } - - 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 false; } - addFirestoreTrigger( + async addFirealertsTrigger( projectId: string, key: string, - eventTrigger: EventTrigger + eventTrigger: EventTrigger, ): Promise { - const firestoreEmu = EmulatorRegistry.get(Emulators.FIRESTORE); - if (!firestoreEmu) { - return Promise.resolve(false); + if (!EmulatorRegistry.isRunning(Emulators.EVENTARC)) { + return false; } - - const bundle = JSON.stringify({ 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 bundle = { + eventTrigger: { + ...eventTrigger, + service: "firebasealerts.googleapis.com", + }, + }; + logger.debug(`addFirealertsTrigger`, JSON.stringify(bundle)); + try { + await EmulatorRegistry.client(Emulators.EVENTARC).post( + `/emulator/v1/projects/${projectId}/triggers/${key}`, + bundle, + ); + return true; + } catch (err: unknown) { + this.logger.log("WARN", "Error adding FireAlerts function: " + err); + } + return false; } - async addPubsubTrigger( - triggerName: string, + async removeFirealertsTrigger( + projectId: string, key: string, eventTrigger: EventTrigger, - schedule: EventSchedule | undefined ): Promise { - const pubsubPort = EmulatorRegistry.getPort(Emulators.PUBSUB); - if (!pubsubPort) { + if (!EmulatorRegistry.isRunning(Emulators.EVENTARC)) { return false; } + const bundle = { + eventTrigger: { + ...eventTrigger, + service: "firebasealerts.googleapis.com", + }, + }; + logger.debug(`removeFirealertsTrigger`, JSON.stringify(bundle)); + try { + await EmulatorRegistry.client(Emulators.EVENTARC).post( + `/emulator/v1/remove/projects/${projectId}/triggers/${key}`, + bundle, + ); + return true; + } catch (err: unknown) { + this.logger.log("WARN", "Error removing FireAlerts function: " + err); + } + return false; + } - const pubsubEmulator = EmulatorRegistry.get(Emulators.PUBSUB) as PubsubEmulator; + 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; + } + } + + 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 function "${key}" has malformed "resource" member. ` + `${eventTrigger.resource!}`, + ); + throw new FirebaseError(`Event function ${key} has malformed resource member`); + } + + const instance = result[1]; + const bundle = JSON.stringify({ + name: `projects/${projectId}/locations/_/functions/${key}`, + path: result[2], // path stored in the second capture group + event: eventTrigger.eventType, + topic: `projects/${projectId}/topics/${key}`, + }); + + let apiPath = "/.settings/functionTriggers.json"; + if (instance !== "") { + apiPath += `?ns=${instance}`; + } else { + this.logger.log( + "WARN", + `No project in use. Registering function for sentinel namespace '${Constants.DEFAULT_DATABASE_EMULATOR_NAMESPACE}'`, + ); + } + + 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; + } + + 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 for event trigger ${key}`); + } + const namespace = eventTrigger.eventFilters?.namespace; + if (!namespace) { + throw new FirebaseError(`A namespace must be supplied for event trigger ${key}`); + } + 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, + signature: SignatureType, + ): Promise { + if (!EmulatorRegistry.isRunning(Emulators.FIRESTORE)) { + return Promise.resolve(false); + } + const { bundle, path } = + signature === "cloudevent" + ? this.getV2FirestoreAttributes(projectId, key, eventTrigger) + : this.getV1FirestoreAttributes(projectId, key, eventTrigger); + + logger.debug(`addFirestoreTrigger`, JSON.stringify(bundle)); + + 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( + triggerName: string, + key: string, + eventTrigger: EventTrigger, + signatureType: SignatureType, + schedule: EventSchedule | undefined, + ): Promise { + const pubsubEmulator = EmulatorRegistry.get(Emulators.PUBSUB) as PubsubEmulator | undefined; + if (!pubsubEmulator) { + return false; + } 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 @@ -622,9 +1200,9 @@ export class FunctionsEmulator implements EmulatorInstance { } try { - await pubsubEmulator.addTrigger(topic, key); + await pubsubEmulator.addTrigger(topic, key, signatureType); return true; - } catch (e) { + } catch (e: any) { return false; } } @@ -642,19 +1220,84 @@ export class FunctionsEmulator implements EmulatorInstance { addStorageTrigger(projectId: string, key: string, eventTrigger: EventTrigger): boolean { logger.debug(`addStorageTrigger`, JSON.stringify({ eventTrigger })); - const eventTriggerId = `${projectId}:${eventTrigger.eventType}`; + const bucket = eventTrigger.resource!.startsWith("projects/_/buckets/") + ? eventTrigger.resource!.split("/")[3] + : eventTrigger.resource; + const eventTriggerId = `${projectId}:${eventTrigger.eventType}:${bucket}`; const triggers = this.multicastTriggers[eventTriggerId] || []; triggers.push(key); this.multicastTriggers[eventTriggerId] = triggers; return true; } + addBlockingTrigger(url: 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; + } + + async addTaskQueueTrigger( + projectId: string, + location: string, + entryPoint: string, + defaultUri: string, + taskQueueTrigger: backend.TaskQueueTrigger, + ): Promise { + logger.debug(`addTaskQueueTrigger`, JSON.stringify(taskQueueTrigger)); + if (!EmulatorRegistry.isRunning(Emulators.TASKS)) { + logger.debug(`addTaskQueueTrigger`, "TQ not running"); + return Promise.resolve(false); + } + const bundle = { + ...taskQueueTrigger, + defaultUri, + }; + + try { + await EmulatorRegistry.client(Emulators.TASKS).post( + `/projects/${projectId}/locations/${location}/queues/${entryPoint}`, + bundle, + ); + return true; + } catch (err) { + this.logger.log("WARN", "Error adding Task Queue function: " + err); + return false; + } + } + getProjectId(): 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 { @@ -672,155 +1315,280 @@ 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; - } - - getTriggerDefinitionByName(triggerName: string): EmulatedTriggerDefinition | undefined { - const record = Object.values(this.triggers).find((r) => r.def.name === triggerName); - return record?.def; + return record; } getTriggerKey(def: EmulatedTriggerDefinition): string { // For background triggers we attach the current generation as a suffix - return def.eventTrigger ? def.name + "-" + this.triggerGeneration : def.name; + 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(); + const backendInfo = this.staticBackends.map((e: EmulatableBackend) => { + return toBackendInfo(e, cf3Triggers); + }); + const dynamicInfo = this.dynamicBackends.map((e: EmulatableBackend) => { + return toBackendInfo(e, cf3Triggers, { createdBy: "SDK" }); + }); + backendInfo.push(...dynamicInfo); + return backendInfo; + } + + 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 }; + this.triggers[key] = { + def, + enabled: true, + backend: opts.backend, + ignored: opts.ignored, + url: opts.url, + }; } - setTriggersForTesting(triggers: EmulatedTriggerDefinition[]) { - triggers.forEach((def) => this.addTriggerRecord(def, { ignored: false })); + setTriggersForTesting(triggers: EmulatedTriggerDefinition[], backend: EmulatableBackend) { + this.triggers = {}; + triggers.forEach((def) => this.addTriggerRecord(def, { backend, ignored: false })); } - getBaseBundle(): FunctionsRuntimeBundle { - return { - cwd: this.args.functionsDir, + getRuntimeConfig(backend: EmulatableBackend): Record { + const configPath = `${backend.functionsDir}/.runtimeconfig.json`; + try { + const configContent = fs.readFileSync(configPath, "utf8"); + return JSON.parse(configContent.toString()); + } catch (e) { + // This is fine - runtime config is optional. + } + return {}; + } + + getUserEnvs(backend: EmulatableBackend): Record { + const projectInfo: functionsEnv.UserEnvsOpts = { + functionsSource: backend.functionsDir, + configDir: backend.configDir, projectId: this.args.projectId, - triggerId: "", - triggerType: undefined, - emulators: { - firestore: EmulatorRegistry.getInfo(Emulators.FIRESTORE), - database: EmulatorRegistry.getInfo(Emulators.DATABASE), - pubsub: EmulatorRegistry.getInfo(Emulators.PUBSUB), - auth: EmulatorRegistry.getInfo(Emulators.AUTH), - }, - adminSdkConfig: { - databaseURL: this.adminSdkConfig.databaseURL, - storageBucket: this.adminSdkConfig.storageBucket, - }, - disabled_features: this.args.disabledRuntimeFeatures, + projectAlias: this.args.projectAlias, + isEmulator: true, }; + + if (functionsEnv.hasUserEnvs(projectInfo)) { + try { + return functionsEnv.loadUserEnvs(projectInfo); + } catch (e: any) { + // Ignore - user envs are optional. + logger.debug("Failed to load local environment variables", e); + } + } + return {}; } - /** - * 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); + + getSystemEnvs(trigger?: EmulatedTriggerDefinition): Record { + const envs: Record = {}; + + // Env vars guaranteed by GCF platform. + // https://cloud.google.com/functions/docs/env-var + 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 (trigger) { + const target = trigger.entryPoint; + envs.FUNCTION_TARGET = target; + envs.FUNCTION_SIGNATURE_TYPE = getSignatureType(trigger); + envs.K_SERVICE = trigger.name; + } + return envs; } - /** - * 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; + + getEmulatorEnvs(): Record { + const envs: Record = {}; + + 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, + enableCors: true, + }); + + let emulatorInfos = EmulatorRegistry.listRunningWithInfo(); + if (this.args.remoteEmulators) { + emulatorInfos = emulatorInfos.concat(Object.values(this.args.remoteEmulators)); + } + setEnvVarsForEmulators(envs, emulatorInfos); + + if (this.debugMode) { + // Start runtime in debug mode to allow triggers to share single runtime process. + envs["FUNCTION_DEBUG_MODE"] = "true"; + } + return envs; + } + + getFirebaseConfig(): string { + const databaseEmulator = this.getEmulatorInfo(Emulators.DATABASE); + + let emulatedDatabaseURL: string | undefined = undefined; + if (databaseEmulator) { + // Database URL will look like one of: + // - https://${namespace}.firebaseio.com + // - https://${namespace}.${location}.firebasedatabase.app + let ns = this.args.projectId; + if (this.adminSdkConfig.databaseURL) { + const asUrl = new URL(this.adminSdkConfig.databaseURL); + ns = asUrl.hostname.split(".")[0]; + } + emulatedDatabaseURL = `http://${formatHost(databaseEmulator)}/?ns=${ns}`; } + return JSON.stringify({ + storageBucket: this.adminSdkConfig.storageBucket, + databaseURL: emulatedDatabaseURL || this.adminSdkConfig.databaseURL, + projectId: this.args.projectId, + }); + } - 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"); + getRuntimeEnvs( + backend: EmulatableBackend, + trigger?: EmulatedTriggerDefinition, + ): Record { + return { + ...this.getUserEnvs(backend), + ...this.getSystemEnvs(trigger), + ...this.getEmulatorEnvs(), + FIREBASE_CONFIG: this.getFirebaseConfig(), + ...backend.env, + }; + } - // Next check if we have a Node install in the node_modules folder + async resolveSecretEnvs( + backend: EmulatableBackend, + trigger?: EmulatedTriggerDefinition, + ): Promise> { + let secretEnvs: Record = {}; + + const secretPath = getSecretLocalPath(backend, this.args.projectDir); 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 + 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); - // 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; + 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 the requested version is the same as the host, let's use that - if (requestedMajorVersion === hostMajorVersion) { + if (errs.length > 0) { this.logger.logLabeled( - "SUCCESS", + "ERROR", "functions", - `Using node@${requestedMajorVersion} from host.` + "Unable to access secret environment variables from Google Cloud Secret Manager. " + + "Make sure the credential used for the Functions Emulator have access " + + `or provide override values in ${secretPath}:\n\t` + + errs.join("\n\t"), ); - return process.execPath; } - // 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 secretEnvs; } - invokeRuntime(frb: FunctionsRuntimeBundle, opts: InvokeRuntimeOpts): 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); - } - - const emitter = new EventEmitter(); + async startNode( + backend: EmulatableBackend, + envs: Record, + ): Promise { const args = [path.join(__dirname, "functionsEmulatorRuntime")]; - - if (opts.ignore_warnings) { - args.unshift("--no-warnings"); - } - - if (this.args.debugPort) { - if (process.env.FIREPIT_VERSION && process.execPath == opts.nodeBinary) { - const requestedMajorNodeVersion = this.getRequestedNodeRuntimeVersion(frb); + 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}`); } } @@ -828,67 +1596,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, ...opts.env, ...process.env }, - 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() { @@ -897,7 +1700,7 @@ export class FunctionsEmulator implements EmulatorInstance { this.logger.logLabeled( "BULLET", `functions[${record.def.entryPoint}]`, - "function temporarily disabled." + "function temporarily disabled.", ); record.enabled = false; } @@ -908,79 +1711,29 @@ 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.staticBackends.concat(this.dynamicBackends)) { + await this.loadTriggers(backend); } - - const trigger = this.getTriggerDefinitionByKey(triggerKey); - const service = getFunctionService(trigger); - const worker = this.startFunctionRuntime(trigger.name, EmulatedTriggerType.BACKGROUND, 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 { - if (this.args.remoteEmulators) { - if (this.args.remoteEmulators[emulator]) { - return this.args.remoteEmulators[emulator]; - } + if (this.args.remoteEmulators?.[emulator]) { + return this.args.remoteEmulators[emulator]; } - return EmulatorRegistry.getInfo(emulator); } private tokenFromAuthHeader(authHeader: string) { - const match = authHeader.match(/^Bearer (.*)$/); + const match = /^Bearer (.*)$/.exec(authHeader); if (!match) { return; } @@ -997,7 +1750,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; @@ -1005,39 +1758,56 @@ 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 triggerId = req.params.trigger_name; + 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"; const authHeader = req.header("Authorization"); - if (authHeader && isCallable) { + if (authHeader && isCallable && trigger.platform !== "gcfv2") { const token = this.tokenFromAuthHeader(authHeader); if (token) { const contextAuth = { @@ -1051,128 +1821,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.name, EmulatedTriggerType.HTTPS, 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}\/[^\/]*\/${triggerId}\/?`), - "/" + 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 bfe9b6750a2..8720c3b08ab 100644 --- a/src/emulator/functionsEmulatorRuntime.ts +++ b/src/emulator/functionsEmulatorRuntime.ts @@ -1,46 +1,54 @@ -import { EmulatorLog } from "./types"; +import * as fs from "fs"; + import { CloudFunction, DeploymentOptions, https } from "firebase-functions"; -import { - EmulatedTrigger, - EmulatedTriggerDefinition, - EmulatedTriggerMap, - EmulatedTriggerType, - findModuleRoot, - FunctionsRuntimeBundle, - FunctionsRuntimeFeatures, - getEmulatedTriggersFromDefinitions, - FunctionsRuntimeArgs, - HttpConstants, -} from "./functionsEmulatorShared"; -import { Constants } from "./constants"; -import { parseVersionString, compareVersionStrings } from "./functionsEmulatorUtils"; import * as express from "express"; import * as path from "path"; import * as admin from "firebase-admin"; import * as bodyParser from "body-parser"; -import * as fs from "fs"; -import { URL } from "url"; +import { pathToFileURL, URL } from "url"; import * as _ from "lodash"; -let triggers: EmulatedTriggerMap | undefined; -let developerPkgJSON: PackageJSON | 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"; -function isFeatureEnabled( - frb: FunctionsRuntimeBundle, - feature: keyof FunctionsRuntimeFeatures -): boolean { - return frb.disabled_features ? !frb.disabled_features[feature] : true; +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; + +/** + * Dynamically load import function to prevent TypeScript from + * transpiling into a require. + * + * See https://github.com/microsoft/TypeScript/issues/43329. + */ +// eslint-disable-next-line @typescript-eslint/no-implied-eval +const dynamicImport = new Function("modulePath", "return import(modulePath)"); + function noOp(): false { return false; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any function requireAsync(moduleName: string, opts?: { paths: string[] }): Promise { return new Promise((res, rej) => { try { - res(require(require.resolve(moduleName, opts))); - } catch (e) { + res(require(require.resolve(moduleName, opts))); // eslint-disable-line @typescript-eslint/no-var-requires + } catch (e: any) { rej(e); } }); @@ -50,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); } }); @@ -58,8 +66,8 @@ function requireResolveAsync(moduleName: string, opts?: { paths: string[] }): Pr interface PackageJSON { engines?: { node?: string }; - dependencies: { [name: string]: any }; - devDependencies: { [name: string]: any }; + dependencies: { [name: string]: any }; // eslint-disable-line @typescript-eslint/no-explicit-any + devDependencies: { [name: string]: any }; // eslint-disable-line @typescript-eslint/no-explicit-any } interface ModuleResolution { @@ -77,7 +85,7 @@ interface SuccessfulModuleResolution { } interface ProxyTarget extends Object { - [key: string]: any; + [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any } /* @@ -90,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 { /** @@ -126,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; } = {}; @@ -153,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); } @@ -189,15 +197,12 @@ class Proxied { * Return the final proxied object. */ finalize(): T { - return this.proxy as T; + return this.proxy; } } -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"); @@ -213,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 }; } @@ -231,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.3.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 @@ -281,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; } } @@ -311,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"] }, @@ -340,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") { @@ -352,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", "", { @@ -369,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; } @@ -396,17 +398,20 @@ 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 httpsProvider = require(httpsProviderResolution); + const httpsProviderV1Resolution = path.join(firebaseFunctionsRoot, "lib/v1/providers/https"); + let httpsProvider: any; + try { + httpsProvider = require(httpsProviderV1Resolution); + } catch (e: any) { + httpsProvider = require(httpsProviderResolution); + } // TODO: Remove this logic and stop relying on internal APIs. See #1480 for reasoning. const onRequestInnerMethodName = "_onRequestWithOptions"; @@ -431,14 +436,39 @@ async function initializeFirebaseFunctionsStubs(frb: FunctionsRuntimeBundle): Pr const onCallInnerMethodName = "_onCallWithOptions"; const onCallMethodOriginal = httpsProvider[onCallInnerMethodName]; - httpsProvider[onCallInnerMethodName] = (handler: CallableHandler, opts: DeploymentOptions) => { - const wrapped = wrapCallableHandler(handler); - const cf = onCallMethodOriginal(wrapped, opts); - return cf; - }; + // Newer versions of the firebase-functions package's _onCallWithOptions method expects 3 arguments. + if (onCallMethodOriginal.length === 3) { + httpsProvider[onCallInnerMethodName] = ( + opts: any, + handler: any, + deployOpts: DeploymentOptions, + ) => { + const wrapped = wrapCallableHandler(handler); + const cf = onCallMethodOriginal(opts, wrapped, deployOpts); + return cf; + }; + } else { + httpsProvider[onCallInnerMethodName] = (handler: any, opts: DeploymentOptions) => { + const wrapped = wrapCallableHandler(handler); + const cf = onCallMethodOriginal(wrapped, opts); + return cf; + }; + } - httpsProvider.onCall = (handler: CallableHandler) => { - return httpsProvider[onCallInnerMethodName](handler, {}); + // Newer versions of the firebase-functions package's onCall method can accept upto 2 arguments. + httpsProvider.onCall = function (optsOrHandler: any, handler: CallableHandler) { + if (onCallMethodOriginal.length === 3) { + let opts; + if (arguments.length === 1) { + opts = {}; + handler = optsOrHandler as CallableHandler; + } else { + opts = optsOrHandler; + } + return httpsProvider[onCallInnerMethodName](opts, handler, {}); + } else { + return httpsProvider[onCallInnerMethodName](optsOrHandler, {}); + } }; } @@ -479,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. @@ -488,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 @@ -514,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); @@ -524,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 @@ -548,193 +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." + "The Cloud Firestore emulator is not running, so calls to Firestore will affect production.", ).log(); } -function warnAboutDatabaseProd(frb: FunctionsRuntimeBundle): void { - if (frb.emulators.database) { +function warnAboutDatabaseProd(): void { + if (process.env[Constants.FIREBASE_DATABASE_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 Realtime Database emulator is not running, so calls to Realtime Database will affect production.", ).log(); } -function warnAboutAuthProd(frb: FunctionsRuntimeBundle): void { - if (frb.emulators.auth) { +function warnAboutAuthProd(): void { + if (process.env[Constants.FIREBASE_AUTH_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 Authentication emulator is not running, so calls to Firebase Authentication will affect production.", ).log(); } -async function initializeEnvironmentalVariables(frb: FunctionsRuntimeBundle): Promise { - process.env.TZ = "UTC"; - process.env.GCLOUD_PROJECT = frb.projectId; - process.env.FUNCTIONS_EMULATOR = "true"; - - // Look for .runtimeconfig.json in the functions directory - const configPath = `${frb.cwd}/.runtimeconfig.json`; - try { - const configContent = fs.readFileSync(configPath, "utf8"); - if (configContent) { - // try JSON.parse for .runtimeconfig.json and notice if parsing is failed - 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 - } - - // Before firebase-functions version 3.8.0 the Functions SDK would reject non-prod database URLs. - const functionsResolution = await assertResolveDeveloperNodeModule(frb, "firebase-functions"); - const functionsGt380 = compareVersionStrings(functionsResolution.version, "3.8.0") >= 0; - let emulatedDatabaseURL = undefined; - if (frb.emulators.database && functionsGt380) { - // Database URL will look like one of: - // - https://${namespace}.firebaseio.com - // - https://${namespace}.${location}.firebasedatabase.app - let ns = frb.projectId; - if (frb.adminSdkConfig.databaseURL) { - const asUrl = new URL(frb.adminSdkConfig.databaseURL); - ns = asUrl.hostname.split(".")[0]; - } - - emulatedDatabaseURL = `http://${formatHost(frb.emulators.database)}/?ns=${ns}`; - } - - process.env.FIREBASE_CONFIG = JSON.stringify({ - storageBucket: frb.adminSdkConfig.storageBucket, - databaseURL: emulatedDatabaseURL || frb.adminSdkConfig.databaseURL, - projectId: frb.projectId, - }); - - if (frb.triggerId) { - // Runtime values are based on information from the bundle. Proper information for this is - // available once the target code has been loaded, which is too late. - const service = frb.triggerId || ""; - const target = service.replace(/-/g, "."); - const mode = frb.triggerType === EmulatedTriggerType.BACKGROUND ? "event" : "http"; - - let nodeVersion = 0; - if (frb.nodeMajorVersion) { - // If nodeMajorVersion is set, we ignore pkg.engines.node - nodeVersion = frb.nodeMajorVersion; - } else { - const pkg = requirePackageJson(frb); - if (pkg?.engines?.node) { - const nodeSemVer = parseVersionString(pkg.engines.node); - nodeVersion = nodeSemVer.major; - } - } - - // Setup predefined environment variables for Node.js 10 and subsequent runtimes - // https://cloud.google.com/functions/docs/env-var - if (nodeVersion >= 10) { - setNode10EnvVars(target, mode, service); - } - } - - // Make firebase-admin point at the Firestore emulator - if (frb.emulators.firestore) { - process.env[Constants.FIRESTORE_EMULATOR_HOST] = formatHost(frb.emulators.firestore); - } - - // Make firebase-admin point at the Database emulator - if (frb.emulators.database) { - process.env[Constants.FIREBASE_DATABASE_EMULATOR_HOST] = formatHost(frb.emulators.database); - } - - // Make firebase-admin point at the Auth emulator - if (frb.emulators.auth) { - process.env[Constants.FIREBASE_AUTH_EMULATOR_HOST] = formatHost(frb.emulators.auth); - } - - if (frb.emulators.pubsub) { - const pubsubHost = formatHost(frb.emulators.pubsub); - process.env.PUBSUB_EMULATOR_HOST = pubsubHost; - logDebug(`Set PUBSUB_EMULATOR_HOST to ${pubsubHost}`); +function warnAboutStorageProd(): void { + if (process.env[Constants.FIREBASE_STORAGE_EMULATOR_HOST]) { + return; } -} -// This is a duplicate of the helper we use elsewhere but it's important not to -// add dependencies to this runtime. -function formatHost(info: { host: string; port: number }) { - if (info.host.includes(":")) { - return `[${info.host}]:${info.port}`; - } else { - return `${info.host}:${info.port}`; - } + new EmulatorLog( + "WARN_ONCE", + "runtime-status", + "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()", { @@ -757,33 +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. @@ -792,101 +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 + trigger: CloudFunction, + reqBody: any, + signature: SignatureType, ): Promise { - const proto = frb.proto; - logDebug("ProcessBackground", proto); + if (signature === "cloudevent") { + 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 }); } /** @@ -896,34 +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 runHTTPS( - args: any[], - func: (a: express.Request, b: express.Response) => Promise -): Promise { +async function runCloudEvent(trigger: CloudFunction, event: unknown): Promise { + logDebug("RunCloudEvent", event); + + await runFunction(() => { + return trigger(event); + }); +} + +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]); }); } @@ -931,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 = { @@ -961,113 +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"); - } +async function initializeRuntime(): Promise { + FUNCTION_DEBUG_MODE = process.env.FUNCTION_DEBUG_MODE || ""; - new EmulatorLog("INFO", "runtime-status", `Beginning execution of "${frb.triggerId}"`, { - frb, - }).log(); - - const trigger = triggers[frb.triggerId]; - logDebug("triggerDefinition", trigger.definition); - const mode = trigger.definition.httpsTrigger ? "HTTPS" : "BACKGROUND"; - - logDebug(`Running ${frb.triggerId} in mode ${mode}`); - - 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 (mode) { - case "BACKGROUND": - await processBackground(frb, triggers[frb.triggerId]); - break; - case "HTTPS": - 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?: EmulatedTriggerDefinition[] -): 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; } - await initializeEnvironmentalVariables(frb); - initializeNetworkFiltering(frb); - await initializeFunctionsConfigHelper(frb); - await initializeFirebaseFunctionsStubs(frb); - await initializeFirebaseAdminStubs(frb); + initializeRuntimeConfig(); + initializeNetworkFiltering(); + await initializeFunctionsConfigHelper(); + await initializeFirebaseFunctionsStubs(); + await initializeFirebaseAdminStubs(); +} - let triggerDefinitions: EmulatedTriggerDefinition[] = []; +async function loadTriggers(): Promise { let triggerModule; - - if (serializedFunctionTrigger) { - /* tslint:disable:no-eval */ - triggerModule = eval(serializedFunctionTrigger)(); - } else { - try { - triggerModule = require(frb.cwd); - } catch (err) { - await moduleResolutionDetective(frb, err); - return; + 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) { - triggerDefinitions = extensionTriggers; - } else { - require("../extractTriggers")(triggerModule, triggerDefinitions); - } - - const triggers = getEmulatedTriggersFromDefinitions(triggerDefinitions, triggerModule); - - new EmulatorLog("SYSTEM", "triggers-parsed", "", { triggers, triggerDefinitions }).log(); - return triggers; + return triggerModule; } async function flushAndExit(code: number) { @@ -1075,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. @@ -1155,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 @@ -1180,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 666ee7c917d..033fe4db335 100644 --- a/src/emulator/functionsEmulatorShared.ts +++ b/src/emulator/functionsEmulatorShared.ts @@ -1,26 +1,66 @@ -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 { InvokeRuntimeOpts } from "./functionsEmulator"; +import { randomBytes } from "crypto"; +import * as _ from "lodash"; +import * as express from "express"; +import { CloudFunction } from "firebase-functions"; -export enum EmulatedTriggerType { - BACKGROUND = "BACKGROUND", - HTTPS = "HTTPS", -} +import * as backend from "../deploy/functions/backend"; +import { Constants } from "./constants"; +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.FIREALERTS_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 interface EmulatedTriggerDefinition { +export type SignatureType = "http" | "event" | "cloudevent"; + +export interface ParsedTriggerDefinition { entryPoint: string; + 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; + taskQueueTrigger?: backend.TaskQueueTrigger; 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 { @@ -29,9 +69,13 @@ export interface EventSchedule { } export interface EventTrigger { - resource: string; - service: string; + resource?: string; eventType: string; + channel?: string; + eventFilters?: Record; + eventFilterPathPatterns?: Record; + // Deprecated + service?: string; } export interface EmulatedTriggerMap { @@ -44,51 +88,24 @@ export interface FunctionsRuntimeArgs { } export interface FunctionsRuntimeBundle { - projectId: string; - proto?: any; - triggerId?: string; - triggerType?: EmulatedTriggerType; - emulators: { - firestore?: { - host: string; - port: number; - }; - database?: { - host: string; - port: number; - }; - pubsub?: { - host: string; - port: number; - }; - auth?: { - 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"; @@ -100,18 +117,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 { @@ -124,17 +140,186 @@ 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)) { + def.httpsTrigger = {}; + def.taskQueueTrigger = { + retryConfig: { + maxAttempts: endpoint.taskQueueTrigger.retryConfig?.maxAttempts, + maxRetrySeconds: endpoint.taskQueueTrigger.retryConfig?.maxRetrySeconds, + maxBackoffSeconds: endpoint.taskQueueTrigger.retryConfig?.maxBackoffSeconds, + maxDoublings: endpoint.taskQueueTrigger.retryConfig?.maxDoublings, + minBackoffSeconds: endpoint.taskQueueTrigger.retryConfig?.minBackoffSeconds, + }, + rateLimits: { + maxConcurrentDispatches: endpoint.taskQueueTrigger.rateLimits?.maxConcurrentDispatches, + maxDispatchesPerSecond: endpoint.taskQueueTrigger.rateLimits?.maxDispatchesPerSecond, + }, + }; + } 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[], + secretEnvVariables: backend.SecretEnvVar[] = [], +): EmulatedTriggerDefinition[] { + const regionDefinitions: EmulatedTriggerDefinition[] = []; + for (const def of definitions) { + if (!def.regions) { + def.regions = ["us-central1"]; + } + // Create a separate CloudFunction for + // each region we deploy a function to + for (const region of def.regions) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const defDeepCopy: EmulatedTriggerDefinition = JSON.parse(JSON.stringify(def)); + defDeepCopy.regions = [region]; + defDeepCopy.region = region; + defDeepCopy.id = `${region}-${defDeepCopy.name}`; + defDeepCopy.platform = defDeepCopy.platform || "gcfv1"; + defDeepCopy.secretEnvironmentVariables = secretEnvVariables; + + regionDefinitions.push(defDeepCopy); + } + } + return regionDefinitions; +} + +/** + * 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 + * @return a map of trigger ids to EmulatedTriggers + */ export function getEmulatedTriggersFromDefinitions( definitions: EmulatedTriggerDefinition[], - module: any + module: any, // eslint-disable-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any ): EmulatedTriggerMap { - return definitions.reduce((obj: { [triggerName: string]: any }, definition: any) => { - obj[definition.name] = new EmulatedTrigger(definition, module); - return obj; - }, {}); + 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 // @@ -148,29 +333,83 @@ 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 getFunctionRegion(def: EmulatedTriggerDefinition): string { - if (def.regions && def.regions.length > 0) { - return def.regions[0]; +/** + * 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"; + } + if (def.taskQueueTrigger) { + return Constants.SERVICE_CLOUD_TASKS; } - return "us-central1"; + return "unknown"; } -export function getFunctionService(def: EmulatedTriggerDefinition): string { - if (def.eventTrigger) { - return def.eventTrigger.service; +/** + * 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; + } + if (eventType.includes("database")) { + return Constants.SERVICE_REALTIME_DATABASE; + } + if (eventType.includes("pubsub")) { + return Constants.SERVICE_PUBSUB; + } + if (eventType.includes("storage")) { + return Constants.SERVICE_STORAGE; + } + if (eventType.includes("firebasealerts")) { + return Constants.SERVICE_FIREALERTS; + } + // Below this point are services that do not have a emulator. + if (eventType.includes("analytics")) { + return Constants.SERVICE_ANALYTICS; + } + if (eventType.includes("auth")) { + return Constants.SERVICE_AUTH; + } + if (eventType.includes("crashlytics")) { + return Constants.SERVICE_CRASHLYTICS; + } + if (eventType.includes("remoteconfig")) { + return Constants.SERVICE_REMOTE_CONFIG; + } + if (eventType.includes("testing")) { + return Constants.SERVICE_TEST_LAB; } - return "unknown"; + 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) => { @@ -184,6 +423,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); @@ -201,10 +443,108 @@ export function findModuleRoot(moduleName: string, filepath: string): string { return chunks.join("/"); } break; - } catch (err) { + } catch (err: any) { /**/ } } 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 { + const host = connectableHostname(info.host); + if (host.includes(":")) { + return `[${host}]:${info.port}`; + } else { + 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 || 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 + // functions cannot receive events in legacy format. This conflicts with our goal of introducing a 'compat' layer + // 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[], + labels?: Record, +): 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 + labels, + 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 b1369d0c39e..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, - EmulatedTriggerType, - getFunctionRegion, -} 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 { @@ -21,67 +18,94 @@ export class FunctionsEmulatorShell implements FunctionsShellController { constructor(private emu: FunctionsEmulator) { this.triggers = emu.getTriggerDefinitions(); - this.emulatedFunctions = this.triggers.map((t) => t.name); + this.emulatedFunctions = this.triggers.map((t) => t.id); const entryPoints = this.triggers.map((t) => t.entryPoint); utils.logLabeledBullet("functions", `Loaded functions: ${entryPoints.join(", ")}`); for (const trigger of this.triggers) { - const name = trigger.name; - if (trigger.httpsTrigger) { - this.urls[name] = FunctionsEmulator.getHttpFunctionUrl( - this.emu.getInfo().host, - this.emu.getInfo().port, + this.urls[trigger.id] = FunctionsEmulator.getHttpFunctionUrl( this.emu.getProjectId(), - name, - getFunctionRegion(trigger) + trigger.name, + 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(name, EmulatedTriggerType.BACKGROUND, 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..5c32326f296 100644 --- a/src/emulator/functionsRuntimeWorker.ts +++ b/src/emulator/functionsRuntimeWorker.ts @@ -1,18 +1,21 @@ +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"; +import { getFunctionDiscoveryTimeout } from "../deploy/functions/runtimes/discovery"; 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 +30,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 +214,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 +239,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.")); + }, getFunctionDiscoveryTimeout() || 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 +296,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 +310,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 +328,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 +351,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 +397,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 +444,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..4467b919873 100644 --- a/src/emulator/hub.ts +++ b/src/emulator/hub.ts @@ -1,48 +1,50 @@ -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"; +import { DataConnectEmulator } from "./dataconnectEmulator"; // 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 type GetEmulatorsResponse = Partial>; -export class EmulatorHub implements EmulatorInstance { +export class EmulatorHub extends ExpressBasedEmulator { + static MISSING_PROJECT_PLACEHOLDER = "demo-no-project"; static CLI_VERSION = pkg.version; static PATH_EXPORT = "/_admin/export"; static PATH_DISABLE_FUNCTIONS = "/functions/disableBackgroundTriggers"; static PATH_ENABLE_FUNCTIONS = "/functions/enableBackgroundTriggers"; static PATH_EMULATORS = "/emulators"; + static PATH_CLEAR_DATA_CONNECT = "/dataconnect/clearData"; /** * Given a project ID, find and read the Locator file for the emulator hub. * This is useful so that multiple copies of the Firebase CLI can discover * each other. */ - static readLocatorFile(projectId: string): Locator | undefined { + static readLocatorFile(projectId: string | undefined): Locator | undefined { const locatorPath = this.getLocatorFilePath(projectId); if (!fs.existsSync(locatorPath)) { return undefined; @@ -52,54 +54,81 @@ export class EmulatorHub implements EmulatorInstance { const locator = JSON.parse(data) as Locator; if (locator.version !== this.CLI_VERSION) { - logger.debug(`Found locator with mismatched version, ignoring: ${JSON.stringify(locator)}`); - return undefined; + logger.debug( + `Found emulator locator with different version: ${JSON.stringify(locator)}, CLI_VERSION: ${this.CLI_VERSION}`, + ); } return locator; } - static getLocatorFilePath(projectId: string): string { + static getLocatorFilePath(projectId: string | undefined): string { const dir = os.tmpdir(); + if (!projectId) { + projectId = EmulatorHub.MISSING_PROJECT_PLACEHOLDER; + } const filename = `hub-${projectId}.json`; - return path.join(dir, filename); + const locatorPath = path.join(dir, filename); + logger.debug(`Emulator locator file path: ${locatorPath}`); + return locatorPath; } - 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(); + } + + getRunningEmulatorsMapping(): GetEmulatorsResponse { + const emulators: GetEmulatorsResponse = {}; + for (const info of EmulatorRegistry.listRunningWithInfo()) { + emulators[info.name] = { + listen: this.args.listenForEmulator[info.name], + ...info, + }; + } + return emulators; + } + + 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, }); - 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.get(EmulatorHub.PATH_EMULATORS, (req, res) => { + res.json(this.getRunningEmulatorsMapping()); + }); + + 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 +137,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 +154,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 +170,50 @@ 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(); - } + app.post(EmulatorHub.PATH_CLEAR_DATA_CONNECT, async (req, res) => { + if (req.headers.origin) { + res.status(403).json({ + message: `Clear Data Connect cannot be triggered by external callers.`, + }); + } + utils.logLabeledBullet("emulators", `Clearing data from Data Connect data sources.`); + + const instance = EmulatorRegistry.get(Emulators.DATACONNECT) as DataConnectEmulator; + if (!instance) { + res.status(400).json({ error: "The Data Connect emulator is not running." }); + return; + } + + await instance.clearData(); + res.status(200).json({ success: true }); + }); - 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 +225,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 +245,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..52f036f6ada 100644 --- a/src/emulator/hubClient.ts +++ b/src/emulator/hubClient.ts @@ -1,11 +1,12 @@ -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; - constructor(private projectId: string) { + constructor(private projectId: string | undefined) { this.locator = EmulatorHub.readLocatorFile(projectId); } @@ -13,36 +14,53 @@ 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; - }); - } - - postExport(path: string): Promise { - return api.request("POST", EmulatorHub.PATH_EXPORT, { - origin: this.origin, - json: true, - data: { - path, - }, - }); + 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(" ")); + } + + async getEmulators(): Promise { + const res = await this.tryOrigins((client) => + client.get(EmulatorHub.PATH_EMULATORS), + ); + return res.body; + } + + async clearDataConnectData(): 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_CLEAR_DATA_CONNECT); } - 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 7984fb728ac..c61cd9b81bc 100644 --- a/src/emulator/hubExport.ts +++ b/src/emulator/hubExport.ts @@ -1,8 +1,8 @@ import * as path from "path"; 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"; @@ -10,6 +10,9 @@ import { FirebaseError } from "../error"; import { EmulatorHub } from "./hub"; import { getDownloadDetails } from "./downloadableEmulators"; import { DatabaseEmulator } from "./databaseEmulator"; +import { DataConnectEmulator } from "./dataconnectEmulator"; +import { rmSync } from "node:fs"; +import { trackEmulator } from "../track"; export interface FirestoreExportMetadata { version: string; @@ -27,25 +30,57 @@ export interface AuthExportMetadata { path: string; } +export interface StorageExportMetadata { + version: string; + path: string; +} + +export interface DataConnectExportMetadata { + version: string; + path: string; +} + export interface ExportMetadata { version: string; firestore?: FirestoreExportMetadata; database?: DatabaseExportMetadata; auth?: AuthExportMetadata; + storage?: StorageExportMetadata; + dataconnect?: DataConnectExportMetadata; +} + +export interface ExportOptions { + path: string; + initiatedBy: string; } export class HubExport { static METADATA_FILE_NAME = "firebase-export-metadata.json"; - constructor(private projectId: string, private exportPath: string) {} + private tmpDir: string; + private exportPath: string; + + constructor( + private projectId: string, + private options: ExportOptions, + ) { + this.exportPath = options.path; + this.tmpDir = fs.mkdtempSync(`firebase-export-${new Date().getTime()}`); + } public static readMetadata(exportPath: string): ExportMetadata | undefined { const metadataPath = path.join(exportPath, this.METADATA_FILE_NAME); if (!fs.existsSync(metadataPath)) { return undefined; } - - return JSON.parse(fs.readFileSync(metadataPath, "utf8").toString()) as ExportMetadata; + let mdString: string = ""; + try { + mdString = fs.readFileSync(metadataPath, "utf8").toString(); + return JSON.parse(mdString) as ExportMetadata; + } catch (err: any) { + // JSON parse errors are unreadable. Throw the original. + throw new FirebaseError(`Unable to parse metadata file ${metadataPath}: ${mdString}`); + } } public async exportAll(): Promise { @@ -86,43 +121,82 @@ export class HubExport { await this.exportAuth(metadata); } - const metadataPath = path.join(this.exportPath, HubExport.METADATA_FILE_NAME); + if (shouldExport(Emulators.STORAGE)) { + metadata.storage = { + version: EmulatorHub.CLI_VERSION, + path: "storage_export", + }; + await this.exportStorage(metadata); + } + + if (shouldExport(Emulators.DATACONNECT)) { + metadata.dataconnect = { + version: EmulatorHub.CLI_VERSION, + path: "dataconnect_export", + }; + await this.exportDataConnect(metadata); + } + + // Make sure the export directory exists + if (!fs.existsSync(this.exportPath)) { + 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)); + + // Remove any existing data in the directory and then swap it with the + // temp directory. + logger.debug(`hubExport: swapping ${this.tmpDir} with ${this.exportPath}`); + rmSync(this.exportPath, { recursive: true }); + fse.moveSync(this.tmpDir, this.exportPath); } 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)`, - export_directory: this.exportPath, + export_directory: this.tmpDir, 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); @@ -138,13 +212,13 @@ export class HubExport { namespacesToExport.push(ns); } } + void trackEmulator("emulator_export", { + initiated_by: this.options.initiatedBy, + emulator_name: Emulators.DATABASE, + count: namespacesToExport.length, + }); - // Make sure the export directory exists - if (!fs.existsSync(this.exportPath)) { - fs.mkdirSync(this.exportPath); - } - - const dbExportPath = path.join(this.exportPath, metadata.database!.path); + const dbExportPath = path.join(this.tmpDir, metadata.database!.path); if (!fs.existsSync(dbExportPath)) { fs.mkdirSync(dbExportPath); } @@ -161,15 +235,19 @@ 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.exportPath, metadata.auth!.path); + const authExportPath = path.join(this.tmpDir, metadata.auth!.path); if (!fs.existsSync(authExportPath)) { fs.mkdirSync(authExportPath); } @@ -182,10 +260,10 @@ export class HubExport { { host, port, - path: `/identitytoolkit.googleapis.com/v1/projects/${this.projectId}/accounts:batchGet?limit=-1`, + 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"); @@ -197,9 +275,57 @@ export class HubExport { path: `/emulator/v1/projects/${this.projectId}/config`, headers: { Authorization: "Bearer owner" }, }, - configFile + configFile, ); } + + private async exportStorage(metadata: ExportMetadata): Promise { + // Clear the export + const storageExportPath = path.join(this.tmpDir, metadata.storage!.path); + if (fs.existsSync(storageExportPath)) { + fse.removeSync(storageExportPath); + } + fs.mkdirSync(storageExportPath, { recursive: true }); + + const storageExportBody = { + path: storageExportPath, + initiatedBy: this.options.initiatedBy, + }; + + 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()}`); + } + } + + private async exportDataConnect(metadata: ExportMetadata): Promise { + void trackEmulator("emulator_export", { + initiated_by: this.options.initiatedBy, + emulator_name: Emulators.DATACONNECT, + }); + + const instance = EmulatorRegistry.get(Emulators.DATACONNECT) as DataConnectEmulator; + if (!instance) { + throw new FirebaseError( + "Unable to export Data Connect emulator data: the Data Connect emulator is not running.", + ); + } + + const dataconnectExportPath = path.join(this.tmpDir, metadata.dataconnect!.path); + if (fs.existsSync(dataconnectExportPath)) { + fse.removeSync(dataconnectExportPath); + } + fs.mkdirSync(dataconnectExportPath); + + await instance.exportData(dataconnectExportPath); + } } function fetchToFile(options: http.RequestOptions, path: fs.PathLike): Promise { diff --git a/src/emulator/initEmulators.ts b/src/emulator/initEmulators.ts new file mode 100644 index 00000000000..9a056564be8 --- /dev/null +++ b/src/emulator/initEmulators.ts @@ -0,0 +1,106 @@ +// specific initialization steps for an emulator + +import * as clc from "colorette"; +import { join } from "path"; +import { input, confirm } from "../prompt"; +import { detectPackageManagerStartCommand } from "./apphosting/developmentServer"; +import { EmulatorLogger } from "./emulatorLogger"; +import { Emulators } from "./types"; +import { Env, maybeGenerateEmulatorYaml } from "../apphosting/config"; +import { Config } from "../config"; +import { getProjectId } from "../projectUtils"; +import { grantEmailsSecretAccess } from "../apphosting/secrets"; + +type InitFn = (config: Config) => Promise | null>; +type AdditionalInitFnsType = Partial>; + +export const AdditionalInitFns: AdditionalInitFnsType = { + [Emulators.APPHOSTING]: async (config: Config) => { + const cwd = process.cwd(); + const additionalConfigs = new Map(); + const logger = EmulatorLogger.forEmulator(Emulators.APPHOSTING); + logger.logLabeled("INFO", "Initializing Emulator"); + + const backendRelativeDir = await input({ + message: "Specify your app's root directory relative to your repository", + default: "./", + }); + additionalConfigs.set("rootDirectory", backendRelativeDir); + + const backendRoot = join(cwd, backendRelativeDir); + try { + const startCommand = await detectPackageManagerStartCommand(backendRoot); + additionalConfigs.set("startCommand", startCommand); + } catch (e) { + logger.log( + "WARN", + "Failed to auto-detect your project's start command. Consider manually setting the start command by setting `firebase.json#emulators.apphosting.startCommand`", + ); + } + + const projectId = getProjectId(config.options); + let env: Env[] | null = []; + try { + env = await maybeGenerateEmulatorYaml(projectId, backendRoot); + } catch (e) { + logger.log("WARN", "failed to export app hosting configs"); + } + + const secretIds = env?.filter((e) => "secret" in e)?.map((e) => e.secret) as string[] | null; + if (secretIds?.length) { + if (!projectId) { + logger.log( + "WARN", + "Cannot grant developers access to secrets for local development without knowing what project the secret is in. " + + `Run ${clc.bold(`firebase apphosting:secrets:grantaccess ${secretIds.join(",")} --project [project] --emails [email list]`)}`, + ); + } else { + const users = await input( + "Your config has secret values. Please provide a comma-separated list of users or groups who should have access to secrets for local development: ", + ); + if (users.length) { + await grantEmailsSecretAccess( + projectId, + secretIds, + users.split(",").map((u) => u.trim()), + ); + } else { + logger.log( + "INFO", + "Skipping granting developers access to secrets for local development. To grant access in the future, run " + + `Run ${clc.bold(`firebase apphosting:secrets:grantaccess ${secretIds.join(",")} --emails [email list]`)}`, + ); + } + } + } + + return mapToObject(additionalConfigs); + }, + + [Emulators.DATACONNECT]: async (config: Config) => { + const additionalConfig: Record = {}; + const defaultDataConnectDir = config.get("dataconnect.source", "dataconnect"); + const defaultDataDir = config.get( + "emulators.dataconnect.dataDir", + `${defaultDataConnectDir}/.dataconnect/pgliteData`, + ); + if ( + await confirm( + "Do you want to persist Postgres data from the Data Connect emulator between runs? " + + `Data will be saved to ${defaultDataDir}. ` + + `You can change this directory by editing 'firebase.json#emulators.dataconnect.dataDir'.`, + ) + ) { + additionalConfig["dataDir"] = defaultDataDir; + } + return additionalConfig; + }, +}; + +function mapToObject(map: Map): Record { + const newObject: Record = {}; + for (const [key, value] of map) { + newObject[key] = value; + } + return newObject; +} diff --git a/src/emulator/loggingEmulator.ts b/src/emulator/loggingEmulator.ts index 38d1666416e..c77e7c8bcf2 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"); +import { stripVTControlCharacters } from "node:util"; 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 = stripVTControlCharacters(bundle.message); this.history.push(bundle); this.connections.forEach((ws) => { diff --git a/src/emulator/portUtils.ts b/src/emulator/portUtils.ts index e9a6a6a6278..045101afff2 100644 --- a/src/emulator/portUtils.ts +++ b/src/emulator/portUtils.ts @@ -1,13 +1,20 @@ -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"; // 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 +82,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 +108,382 @@ 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: boolean = 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" | "dataconnect.postgres"; + +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, + "dataconnect.postgres": 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, + tasks: true, + + // Only one hostname possible in .server mode, can switch to middleware later. + hosting: true, + + apphosting: 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 = 30000; - 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 === "dataconnect.postgres" + ? Emulators.DATACONNECT + : 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 (!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}` + : name === "dataconnect.postgres" + ? `postgres server for ${Emulators.DATACONNECT}` + : 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 e3e8897575e..eda76b9ca75 100644 --- a/src/emulator/pubsubEmulator.ts +++ b/src/emulator/pubsubEmulator.ts @@ -1,13 +1,24 @@ import * as uuid from "uuid"; +import { MessagePublishedData } from "@google/events/cloud/pubsub/v1/MessagePublishedData"; import { Message, PubSub, Subscription } from "@google-cloud/pubsub"; -import * as api from "../api"; import * as downloadableEmulators from "./downloadableEmulators"; +import { Client } from "../apiv2"; import { EmulatorLogger } from "./emulatorLogger"; import { EmulatorInfo, EmulatorInstance, Emulators } from "../emulator/types"; import { Constants } from "./constants"; 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; @@ -16,26 +27,38 @@ export interface PubsubEmulatorArgs { auto_download?: boolean; } +interface Trigger { + triggerKey: string; + signatureType: SignatureType; +} + export class PubsubEmulator implements EmulatorInstance { - pubsub: PubSub; + private _pubsub: PubSub | undefined; // Map of topic name to a list of functions to trigger - triggers: Map>; + triggersForTopic: Map; // Map of topic name to a PubSub subscription object - subscriptions: Map; + subscriptionForTopic: Map; + + // Client for communicating with the Functions Emulator + private client?: Client; private logger = EmulatorLogger.forEmulator(Emulators.PUBSUB); - constructor(private args: PubsubEmulatorArgs) { - const { host, port } = this.getInfo(); - this.pubsub = new PubSub({ - apiEndpoint: `${host}:${port}`, - projectId: this.args.projectId, - }); + get pubsub(): PubSub { + if (!this._pubsub) { + this._pubsub = new PubSub({ + apiEndpoint: EmulatorRegistry.url(Emulators.PUBSUB).host, + projectId: this.args.projectId, + }); + } + return this._pubsub; + } - this.triggers = new Map(); - this.subscriptions = new Map(); + constructor(private args: PubsubEmulatorArgs) { + this.triggersForTopic = new Map(); + this.subscriptionForTopic = new Map(); } async start(): Promise { @@ -47,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 { @@ -66,20 +97,13 @@ export class PubsubEmulator implements EmulatorInstance { return Emulators.PUBSUB; } - async addTrigger(topicName: string, trigger: string) { - this.logger.logLabeled("DEBUG", "pubsub", `addTrigger(${topicName}, ${trigger})`); - - const topicTriggers = this.triggers.get(topicName) || new Set(); - if (topicTriggers.has(topicName) && this.subscriptions.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 { @@ -88,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 }); } @@ -105,72 +130,130 @@ export class PubsubEmulator implements EmulatorInstance { this.onMessage(topicName, message); }); - topicTriggers.add(trigger); - this.triggers.set(topicName, topicTriggers); - this.subscriptions.set(topicName, sub); + return sub; } - private async onMessage(topicName: string, message: Message) { - this.logger.logLabeled("DEBUG", "pubsub", `onMessage(${topicName}, ${message.id})`); - const topicTriggers = this.triggers.get(topicName); - if (!topicTriggers || topicTriggers.size === 0) { - throw new FirebaseError(`No trigger for topic: ${topicName}`); + 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; } - const functionsEmu = EmulatorRegistry.get(Emulators.FUNCTIONS); - if (!functionsEmu) { + triggers.push({ triggerKey, signatureType }); + this.triggersForTopic.set(topicName, triggers); + this.subscriptionForTopic.set(topicName, sub); + } + + private ensureFunctionsClient() { + if (this.client !== undefined) return; + + if (!EmulatorRegistry.isRunning(Emulators.FUNCTIONS)) { throw new FirebaseError( - `Attempted to execute pubsub trigger for topic ${topicName} but could not find Functions emulator` + `Attempted to execute pubsub trigger but could not find the Functions emulator`, ); } + this.client = EmulatorRegistry.client(Emulators.FUNCTIONS); + } + + private createLegacyEventRequestBody(topic: string, message: Message) { + return { + context: { + eventId: uuid.v4(), + resource: { + service: "pubsub.googleapis.com", + name: `projects/${this.args.projectId}/topics/${topic}`, + }, + eventType: "google.pubsub.topic.publish", + timestamp: message.publishTime.toISOString(), + }, + data: { + data: message.data, + attributes: message.attributes, + }, + }; + } + + private createCloudEventRequestBody( + topic: string, + 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.getTime()).toISOString(); + const data: MessagePublishedData = { + message: { + messageId: message.id, + 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.0", + id: uuid.v4(), + time: truncatedPublishTime, + type: "google.cloud.pubsub.topic.v1.messagePublished", + source: `//pubsub.googleapis.com/projects/${this.args.projectId}/topics/${topic}`, + data, + }; + } + + private async onMessage(topicName: string, message: Message) { + this.logger.logLabeled("DEBUG", "pubsub", `onMessage(${topicName}, ${message.id})`); + const triggers = this.triggersForTopic.get(topicName); + if (!triggers || triggers.length === 0) { + throw new FirebaseError(`No trigger for topic: ${topicName}`); + } this.logger.logLabeled( "DEBUG", "pubsub", - `Executing ${topicTriggers.size} matching triggers (${JSON.stringify( - Array.from(topicTriggers) - )})` + `Executing ${triggers.length} matching triggers (${JSON.stringify( + triggers.map((t) => t.triggerKey), + )})`, ); - // We need to do one POST request for each matching trigger and only - // 'ack' the message when they are all complete. - let remaining = topicTriggers.size; - for (const trigger of topicTriggers) { - const body = { - context: { - eventId: uuid.v4(), - resource: { - service: "pubsub.googleapis.com", - name: `projects/${this.args.projectId}/topics/${topicName}`, - }, - eventType: "google.pubsub.topic.publish", - timestamp: message.publishTime.toISOString(), - }, - data: { - data: message.data, - attributes: message.attributes, - }, - }; + this.ensureFunctionsClient(); + for (const { triggerKey, signatureType } of triggers) { try { - await api.request( - "POST", - `/functions/projects/${this.args.projectId}/triggers/${trigger}`, - { - origin: `http://${EmulatorRegistry.getInfoHostString(functionsEmu.getInfo())}`, - data: body, - } - ); - } catch (e) { + const path = `/functions/projects/${this.args.projectId}/triggers/${triggerKey}`; + if (signatureType === "event") { + await this.client!.post(path, this.createLegacyEventRequestBody(topicName, message)); + } else if (signatureType === "cloudevent") { + await this.client!.post, unknown>( + path, + this.createCloudEventRequestBody(topicName, message), + { headers: { "Content-Type": "application/cloudevents+json; charset=UTF-8" } }, + ); + } else { + throw new FirebaseError(`Unsupported trigger signature: ${signatureType}`); + } + } catch (e: any) { this.logger.logLabeled("DEBUG", "pubsub", e); } - - // If this is the last trigger we need to run, ack the message. - remaining--; - if (remaining <= 0) { - this.logger.logLabeled("DEBUG", "pubsub", `Acking message ${message.id}`); - message.ack(); - } } + this.logger.logLabeled("DEBUG", "pubsub", `Acking message ${message.id}`); + message.ack(); } } diff --git a/src/emulator/registry.spec.ts b/src/emulator/registry.spec.ts new file mode 100644 index 00000000000..9356bc336b5 --- /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); + }); + }); +}).timeout(2000); diff --git a/src/emulator/registry.ts b/src/emulator/registry.ts index 7c9ac400678..e1fef0e1229 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 block 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,13 +68,23 @@ 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, + /** App Hosting should be shut down next. Users should not be interacting + * with their app while its being shut down as the app may using the + * background trigger emulators below. + */ + apphosting: 2.1, + // All background trigger emulators are equal here, so we choose // an order for consistency. database: 3.0, @@ -62,6 +92,9 @@ export class EmulatorRegistry { pubsub: 3.2, auth: 3.3, storage: 3.5, + eventarc: 3.6, + dataconnect: 3.7, + tasks: 3.8, // Hub shuts down once almost everything else is done hub: 4, @@ -75,19 +108,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 +135,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 73c20e3bcab..710d7a9df07 100644 --- a/src/emulator/storage/apis/firebase.ts +++ b/src/emulator/storage/apis/firebase.ts @@ -1,68 +1,33 @@ 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"; -import * as path from "path"; - -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; - } +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"; - 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; -} - -/* TODO: IOS - Hash is base64 for some reason - */ /** * @param emulator */ 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)); @@ -106,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: { @@ -119,488 +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(); }); - // get metadata and get object handler firebaseStorageAPI.get("/b/:bucketId/o/:objectId", async (req, res) => { - const decodedObjectId = decodeURIComponent(req.params.objectId); - const operationPath = path.join("b", req.params.bucketId, "o", decodedObjectId); - const md = storageLayer.getMetadata(req.params.bucketId, decodedObjectId); - - if (!md) { - res.sendStatus(404); - return; - } - - const isPermittedViaHeader = await isPermitted({ - ruleset: emulator.rules, - method: RulesetOperationMethod.GET, - path: operationPath, - file: { - before: md.asRulesResource(), - }, - authorization: req.header("authorization"), - }); - - 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; - } - - const isPermittedViaToken = - req.query.token && md.downloadTokens.includes(req.query.token.toString()); - - if ( - // Token headers are used for GETs from Mobile SDKs - !isPermittedViaHeader && - // Query values are used for GETs from Web SDKs - !isPermittedViaToken - ) { - res.sendStatus(403); - return; - } - - 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)); - } 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; } - setObjectHeaders(res, md, { "Content-Encoding": isGZipped ? "identity" : undefined }); - - 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; + if (metadata.downloadTokens.length === 0) { + metadata.addDownloadToken(/* shouldTrigger = */ true); } - const decodedObjectId = decodeURIComponent(req.params.objectId); - const operationPath = path.join("b", req.params.bucketId, "o", decodedObjectId); - - 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 = path.join("b", req.params.bucketId, "o", prefix); - - 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 = path.join("b", req.params.bucketId, "o", decodedObjectId); - - 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)); + return res.status(200).json(new OutgoingFirebaseMetadata(metadata)); } - if (!req.query.name) { - res.sendStatus(400); - return; - } - - 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 metadata = storageLayer.oneShotUpload( - req.params.bucketId, - name, - blobContentType, - JSON.parse(metadataString), - Buffer.from(blobBytes) - ); - - if (!metadata) { - res.sendStatus(400); - return; - } - - const operationPath = path.join("b", req.params.bucketId, "o", name); - - 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: metadata?.asRulesResource(), - }, - })) - ) { - storageLayer.deleteFile(metadata?.bucket, metadata?.name); - return res.status(403).json({ - error: { - code: 403, - message: `Permission denied. No WRITE permission.`, - }, - }); - } - - res.json(metadata); - return; - } else { - const operationPath = path.join("b", req.params.bucketId, "o", name); + // 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 - ); - - 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.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; - - // 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(), + throw err; + } + const upload = uploadService.multipartUpload({ + bucketId, + objectId, + metadata: JSON.parse(metadataRaw), + dataRaw: dataRaw, + authorization: req.header("authorization"), + }); + return await finalizeOneShotUpload(upload); + } + + // 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); + }; + + 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.`, }, }); } + if (err instanceof NotFoundError) { + return res.sendStatus(404); + } + throw err; + } + } + setObjectHeaders(res, metadata); + return res.json(new OutgoingFirebaseMetadata(metadata)); + }; - res.header("x-goog-upload-status", "final"); - res.json(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 = path.join("b", req.params.bucketId, "o", decodedObjectId); + 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) => { @@ -610,21 +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("Cache-Control", metadata.cacheControl); - 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; +} - res.setHeader("Content-Language", metadata.contentLanguage); +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 013d8d472f4..99d9fd45766 100644 --- a/src/emulator/storage/apis/gcloud.ts +++ b/src/emulator/storage/apis/gcloud.ts @@ -1,241 +1,464 @@ import { Router } from "express"; -import { EmulatorLogger } from "../../emulatorLogger"; import { Emulators } from "../../types"; -import { CloudStorageObjectMetadata } from "../metadata"; +import { + CloudStorageObjectAccessControlMetadata, + CloudStorageObjectMetadata, + IncomingMetadata, + StoredFileMetadata, +} from "../metadata"; +import { sendFileBytes } from "./shared"; import { EmulatorRegistry } from "../../registry"; import { StorageEmulator } from "../index"; +import { EmulatorLogger } from "../../emulatorLogger"; +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 type { 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)"); + } - gcloudStorageAPI.get("/b", (req, res) => { - res.json({ - kind: "storage#buckets", - items: storageLayer.listBuckets(), + 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/:bucketId/o/:objectId", (req, res) => { - const md = storageLayer.getMetadata(req.params.bucketId, req.params.objectId); - - if (!md) { - res.sendStatus(404); - return; - } - - EmulatorLogger.forEmulator(Emulators.STORAGE).log( - "WARN", - `Returning metadata: ${JSON.stringify(md)}` - ); - - const outgoingMd = new CloudStorageObjectMetadata(md); - - res.json(outgoingMd).status(200).send(); - return; + gcloudStorageAPI.get("/b", async (req, res) => { + res.json({ + kind: "storage#buckets", + items: await adminStorageLayer.listBuckets(), + }); }); - gcloudStorageAPI.patch("/b/:bucketId/o/:objectId", (req, res) => { - const md = storageLayer.getMetadata(req.params.bucketId, req.params.objectId); + gcloudStorageAPI.get( + [ + "/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 (!md) { - res.sendStatus(404); - return; + if (req.query.alt === "media") { + return sendFileBytes(getObjectResponse.metadata, getObjectResponse.data, req, res); + } + return res.json(new CloudStorageObjectMetadata(getObjectResponse.metadata)); + }, + ); + + 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); - EmulatorLogger.forEmulator(Emulators.STORAGE).log( - "WARN", - `Returning metadata: ${JSON.stringify(outgoingMetadata)}` - ); - 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() : ""; - EmulatorLogger.forEmulator(Emulators.STORAGE).log( - "WARN", - `Received list objects request for bucket: ${req.params.bucketId}, with prefix: ${prefix} and delimiter: ${delimiter} and pageToken: ${pageToken} and maxResults: ${req.params.maxResults}` - ); - - 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/:object", (req, res) => { - const decodedObjectId = decodeURIComponent(req.params.objectId); - const md = storageLayer.getMetadata(req.params.bucketId, decodedObjectId); - - 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) { - EmulatorLogger.forEmulator(Emulators.STORAGE).log( - "WARN", - `No upload id passed as query parameter!` - ); res.sendStatus(400); return; } 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) { - EmulatorLogger.forEmulator(Emulators.STORAGE).log( - "WARN", - `No upload found for finalizeUpload:${uploadId}` - ); - 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("/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) { - EmulatorLogger.forEmulator(Emulators.STORAGE).log("WARN", `Missing content type`); - res.sendStatus(400); - return; + 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.", + ); + 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; + 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); + }); - 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) { - EmulatorLogger.forEmulator(Emulators.STORAGE).log( - "WARN", - `Can't generate upload URL, no running storage emulator?` - ); - 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 upload = uploadService.mediaUpload({ + bucketId: req.params.bucketId, + objectId: name!.toString(), + dataRaw: await reqBodyToBuffer(req), + authorization: req.header("authorization"), + }); + return await finalizeOneShotUpload(upload); + }); - const blobBytes = Buffer.from(bodyBuffer.slice(bufferOffset, -`\r\n${boundary}--`.length)); + 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; + } + return sendFileBytes(getObjectResponse.metadata, getObjectResponse.data, req, res); + }); - const metadata = storageLayer.oneShotUpload( - req.params.bucketId, - name, - blobContentType, - JSON.parse(metadataString), - blobBytes - ); + 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; + } - if (!metadata) { - res.sendStatus(400); - return; + 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.status(501).json("endpoint not implemented"); + } else { + res.sendStatus(501); } - - res.status(200).json(new CloudStorageObjectMetadata(metadata)).send(); - return; }); return gcloudStorageAPI; } + +/** 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.json({ + error: { + code: 404, + message, + errors: [ + { + message, + domain: "global", + reason: "notFound", + }, + ], + }, + }); + } +} + +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 0074a950a4f..0c4a0482dd9 100644 --- a/src/emulator/storage/cloudFunctions.ts +++ b/src/emulator/storage/cloudFunctions.ts @@ -1,64 +1,84 @@ +import * as uuid from "uuid"; + import { EmulatorRegistry } from "../registry"; -import { EmulatorInfo, Emulators } from "../types"; -import * as request from "request"; +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 } from "../events/types"; type StorageCloudFunctionAction = "finalize" | "metadataUpdate" | "delete" | "archive"; +const STORAGE_V2_ACTION_MAP: Record = { + finalize: "finalized", + metadataUpdate: "metadataUpdated", + delete: "deleted", + archive: "archived", +}; 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 = EmulatorRegistry.client(Emulators.FUNCTIONS); } } public async dispatch( action: StorageCloudFunctionAction, - object: CloudStorageObjectMetadata + object: CloudStorageObjectMetadata, ): Promise { - if (!this.enabled) return; - - const multicastEventBody = this.createEventRequestBody(action, object); + if (!this.enabled) { + return; + } - const c = new Client({ urlPrefix: this.multicastOrigin, auth: false }); - let res; + const errStatus: Array = []; let err: Error | undefined; try { - res = await c.post(this.multicastPath, multicastEventBody); - } catch (e) { - err = e; + /** Legacy Google Events */ + const eventBody = this.createLegacyEventRequestBody(action, object); + const eventRes = await this.client!.post(this.multicastPath, eventBody); + if (eventRes.status !== 200) { + errStatus.push(eventRes.status); + } + /** Modern CloudEvents */ + const cloudEventBody = this.createCloudEventRequestBody(action, object); + const cloudEventRes = await this.client!.post, any>( + this.multicastPath, + cloudEventBody, + { + headers: { "Content-Type": "application/cloudevents+json; charset=UTF-8" }, + }, + ); + if (cloudEventRes.status !== 200) { + errStatus.push(cloudEventRes.status); + } + } catch (e: any) { + err = e as Error; } - if (err || res?.status != 200) { + if (err || errStatus.length > 0) { 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.`, ); } } - private createEventRequestBody( + /** Legacy Google Events type */ + private createLegacyEventRequestBody( action: StorageCloudFunctionAction, - objectMetadataPayload: ObjectMetadataPayload - ): string { + objectMetadataPayload: ObjectMetadataPayload, + ) { const timestamp = new Date(); - return JSON.stringify({ + return { eventId: `${timestamp.getTime()}`, timestamp: toSerializedDate(timestamp), eventType: `google.storage.object.${action}`, @@ -68,7 +88,31 @@ export class StorageCloudFunctions { type: "storage#object", }, // bucket data: objectMetadataPayload, - }); + }; + } + + /** Modern CloudEvents type */ + private createCloudEventRequestBody( + action: StorageCloudFunctionAction, + 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; + let time = new Date().toISOString(); + if (data.updated) { + time = typeof data.updated === "string" ? data.updated : data.updated.toISOString(); + } + return { + specversion: "1.0", + id: uuid.v4(), + type: `google.cloud.storage.object.v1.${ceAction}`, + source: `//storage.googleapis.com/projects/_/buckets/${objectMetadataPayload.bucket}/objects/${objectMetadataPayload.name}`, + time, + data, + }; } } @@ -185,7 +229,7 @@ export interface ObjectMetadataPayload { team?: string; }; etag?: string; - } + }, ]; owner?: { @@ -211,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..8a13845650e --- /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`; + }); +}).timeout(2000); diff --git a/src/emulator/storage/files.ts b/src/emulator/storage/files.ts index cd3e0b78435..d66084cccdb 100644 --- a/src/emulator/storage/files.ts +++ b/src/emulator/storage/files.ts @@ -1,17 +1,31 @@ -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 rimraf from "rimraf"; +import * as fse from "fs-extra"; 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: { + id: string; + }[]; +} export class StoredFile { private _metadata!: StoredFileMetadata; @@ -21,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)) { @@ -132,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); @@ -151,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); @@ -165,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) { @@ -221,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); @@ -232,325 +252,412 @@ 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( - upload.bucketId, - upload.objectId, - bytes, - "", - upload.metadata.contentEncoding, - upload.metadata, - this._cloudFunctions + // 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: getIncomingMetadata("contentType"), + contentDisposition: getIncomingMetadata("contentDisposition"), + contentEncoding: getIncomingMetadata("contentEncoding"), + contentLanguage: getIncomingMetadata("contentLanguage"), + cacheControl: getIncomingMetadata("cacheControl"), + customMetadata: getIncomingMetadata("metadata"), + }, + this._cloudFunctions, + this._persistence.readBytes(upload.path, upload.size), ); - const file = new StoredFile(finalMetadata, filePath); - this._files.set(filePath, file); - 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.appendBytes(filePath, bytes); - const md = new StoredFileMetadata( - bucket, - object, - bytes, - "", - incomingMetadata.contentEncoding, - incomingMetadata, - this._cloudFunctions + 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, ); - const file = new StoredFile(md, this._persistence.getDiskPath(filePath)); - this._files.set(filePath, 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 (!authorized) { + this._persistence.deleteFile(upload.path); + throw new ForbiddenError(); } - if (!prefix) { - prefix = ""; + // 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(); } - - if (!prefix.endsWith(delimiter)) { - prefix += delimiter; + const sourceMetadata = this.getMetadata(sourceBucket, sourceObject); + if (!sourceMetadata) { + throw new NotFoundError(); } + const sourceBytes = this.getBytes(sourceBucket, sourceObject) as Buffer; - if (!prefix.startsWith(delimiter)) { - prefix = delimiter + prefix; - } + const destinationFilePath = this.path(destinationBucket, destinationObject); + this._persistence.deleteFile(destinationFilePath, /* failSilently = */ true); + this._persistence.appendBytes(destinationFilePath, sourceBytes); - let items = []; - const prefixes = new Set(); - for (const [, file] of this._files) { - if (file.metadata.bucket != bucket) { - continue; - } - - let name = `${delimiter}${file.metadata.name}`; - if (!name.startsWith(prefix)) { - continue; - } - - name = name.substring(prefix.length); - if (name.startsWith(delimiter)) { - name = name.substring(prefix.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); - } + 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(","); } - - items.sort(); - if (pageToken) { - const idx = items.findIndex((v) => v == pageToken); - if (idx != -1) { - items = items.slice(idx); + 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] = ""; } } - if (!maxResults) { - maxResults = 1000; - } - - let nextPageToken = undefined; - if (items.length > maxResults) { - nextPageToken = items[maxResults]; - items = items.slice(0, maxResults); + // 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; } - - return new ListResponse( - [...prefixes].sort(), - items.map((i) => new ListItem(i, bucket)), - nextPageToken + const copiedFileMetadata = new StoredFileMetadata( + { + name: destinationObject, + bucket: destinationBucket, + contentType: getMetadata("contentType"), + contentDisposition: getMetadata("contentDisposition"), + contentEncoding: getMetadata("contentEncoding"), + contentLanguage: getMetadata("contentLanguage"), + cacheControl: getMetadata("cacheControl"), + customMetadata: getMetadata("metadata"), + }, + this._cloudFunctions, + sourceBytes, ); - } + const file = new StoredFile(copiedFileMetadata); + this._files.set(destinationFilePath, file); - public listItems( - bucket: string, - prefix: string, - delimiter: string, - pageToken: string | undefined, - maxResults: number | undefined - ) { - if (!delimiter) { - delimiter = "/"; - } - - if (!prefix) { - prefix = ""; - } + this._cloudFunctions.dispatch("finalize", new CloudStorageObjectMetadata(file.metadata)); + return file.metadata; + } - if (!prefix.endsWith(delimiter)) { - prefix += delimiter; + /** + * 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 = 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)); + } } - items.push(this.path(file.metadata.bucket, file.metadata.name)); + 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].name; + items = items.slice(0, maxResults); } 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 md = val.metadata; - md.addDownloadToken(); - return md; - } - - 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; + const metadata = this.getMetadata(request.bucketId, request.decodedObjectId); + if (!metadata) { + throw new NotFoundError(); + } + metadata.addDownloadToken(); + return metadata; + } + + /** + * 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 { return this._persistence.dirPath; } -} -export class Persistence { - private _dirPath: string; - constructor(dirPath: string) { - this._dirPath = dirPath; - if (!existsSync(dirPath)) { - mkdirSync(dirPath, { - recursive: true, - }); + /** + * Export is implemented using async operations so that it does not block + * the hub when invoked. + */ + 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 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)); + + // Create blobs directory + const blobsDirPath = path.join(storageExportPath, "blobs"); + await fse.ensureDir(blobsDirPath); + + // Create metadata directory + const metadataDirPath = path.join(storageExportPath, "metadata"); + await fse.ensureDir(metadataDirPath); + + // 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.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)); } } - public get dirPath(): string { - return this._dirPath; - } - - appendBytes(fileName: string, bytes: Buffer, fileOffset?: number): string { - const path = this.getDiskPath(fileName); - const dirPath = path.substring(0, path.lastIndexOf("/")); + /** + * Import can be implemented using sync operations because the emulator should + * not be handling any other requests during import. + */ + import(storageExportPath: string, options: { initiatedBy: string }): void { + // Restore list of buckets + const bucketsFile = path.join(storageExportPath, "buckets.json"); + 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, + }); - if (!existsSync(dirPath)) { - mkdirSync(dirPath, { - recursive: true, - }); + for (const b of bucketsList.buckets) { + const bucketMetadata = new CloudStorageBucketMetadata(b.id); + this._buckets.set(b.id, bucketMetadata); } - let fd; - try { - // TODO: This is more technically correct, but corrupts multipart files - // fd = openSync(path, "w+"); - // writeSync(fd, bytes, 0, bytes.byteLength, fileOffset); + const metadataDir = path.join(storageExportPath, "metadata"); + const blobsDir = path.join(storageExportPath, "blobs"); - fs.appendFileSync(path, bytes); - return path; - } finally { - if (fd) { - closeSync(fd); - } + // 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; } - } - 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); + // Restore all metadata + const metadataList = this.walkDirSync(metadataDir); + + const dotJson = ".json"; + for (const f of metadataList) { + if (path.extname(f) !== dotJson) { + logger.debug(`Skipping unexpected storage metadata file: ${f}`); + continue; } - } - } + const metadata = StoredFileMetadata.fromJSON(readFileSync(f, "utf-8"), this._cloudFunctions); - deleteFile(fileName: string): void { - unlinkSync(this.getDiskPath(fileName)); - } + // To get the blob path from the metadata path: + // 1) Get the relative path to the metadata export dir + // 2) Subtract .json from the end + const metadataRelPath = path.relative(metadataDir, f); + const blobPath = metadataRelPath.substring(0, metadataRelPath.length - dotJson.length); - deleteAll(): Promise { - return new Promise((resolve, reject) => { - rimraf(this._dirPath, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - } + const blobAbsPath = path.join(blobsDir, blobPath); + if (!existsSync(blobAbsPath)) { + logger.warn(`Could not find file "${blobPath}" in storage export.`); + continue; + } - renameFile(oldName: string, newName: string): void { - const dirPath = this.getDiskPath(path.dirname(newName)); + 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); + } - if (!existsSync(dirPath)) { - mkdirSync(dirPath, { - recursive: true, - }); - } + const filepath = this.path(metadata.bucket, fileName); - renameSync(this.getDiskPath(oldName), this.getDiskPath(newName)); + this._persistence.copyFromExternalPath(blobAbsPath, filepath); + this._files.set(filepath, new StoredFile(metadata)); + } } - getDiskPath(fileName: string): string { - return path.join(this._dirPath, fileName); + private *walkDirSync(dir: string): Generator { + const files = readdirSync(dir); + for (const file of files) { + const p = path.join(dir, file); + if (statSync(p).isDirectory()) { + yield* this.walkDirSync(p); + } else { + yield p; + } + } } } + +/** 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 1090cbebde7..08c648d8c87 100644 --- a/src/emulator/storage/index.ts +++ b/src/emulator/storage/index.ts @@ -1,156 +1,130 @@ +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 } 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(downloadDetails.downloadPath); - this._storageLayer = new StorageLayer(args.projectId); + this._rulesRuntime = new StorageRulesRuntime(); + 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); - this._storageLayer = new StorageLayer(this.args.projectId); - - 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(), - }, - ], - }; - } - - private async loadRuleset(): Promise { - if (!this._rulesetSource) { - this._logger.log("WARN", "Attempting to update ruleset without a source."); - return; - } - - 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; - } - } - 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 { @@ -167,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 daaf1952b7b..1116fc9bf38 100644 --- a/src/emulator/storage/metadata.ts +++ b/src/emulator/storage/metadata.ts @@ -3,160 +3,284 @@ import * as crypto from "crypto"; import { EmulatorRegistry } from "../registry"; import { Emulators } from "../types"; import { StorageCloudFunctions } from "./cloudFunctions"; -import { crc32c } from "./crc"; +import { crc32c, crc32cToString } from "./crc"; -type RulesResourceMetadataOverrides = { - [Property in keyof RulesResourceMetadata]?: RulesResourceMetadata[Property]; +type SerializedFileMetadata = Omit & { + timeCreated: string; + updated: string; }; +/** + * Note: all fields of this object which do not begin with _ are serialized + * during export, so add/remove/modify fields with caution. + */ export class StoredFileMetadata { name: string; bucket: string; generation: number; metageneration: number; - contentType: string; + contentType?: string; timeCreated: Date; updated: Date; storageClass: string; size: number; md5Hash: string; - contentEncoding: string; - contentDisposition: string; - contentLanguage: string; - cacheControl: string; + contentEncoding?: string; + contentDisposition?: string; + contentLanguage?: string; + cacheControl?: string; + customTime?: Date; crc32c: string; etag: string; - downloadTokens: string; - customMetadata: { [s: string]: string }; + downloadTokens: string[]; + customMetadata?: { [s: string]: string }; constructor( - bucketId: string, - objectId: string, - bytes: Buffer, - contentType: string, - contentEncoding: string | undefined, - incomingMetadata: IncomingMetadata, - private _cloudFunctions: StorageCloudFunctions + opts: Partial & { + name: string; + bucket: string; + }, + private _cloudFunctions: StorageCloudFunctions, + bytes?: Buffer, ) { - this.name = objectId; - this.bucket = bucketId; - this.timeCreated = new Date(); - this.size = bytes.byteLength; - this.metageneration = 1; - this.generation = Date.now(); - this.md5Hash = generateMd5Hash(bytes); - this.storageClass = "STANDARD"; - this.downloadTokens = ""; - this.etag = "someETag"; - this.crc32c = `${crc32c(bytes)}`; - this.contentDisposition = "inline"; - this.updated = this.timeCreated; - this.contentType = contentType; - this.cacheControl = "no-cache"; - this.contentLanguage = "en-us"; - this.contentEncoding = contentEncoding || "identity"; - this.customMetadata = incomingMetadata.metadata || {}; - - this.addDownloadToken(); - this.update(incomingMetadata); + // Required fields + this.name = opts.name; + this.bucket = opts.bucket; + + // 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.contentDisposition = opts.contentDisposition; + this.cacheControl = opts.cacheControl; + this.contentLanguage = opts.contentLanguage; + this.customTime = opts.customTime; + 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(); + this.updated = opts.updated ? new Date(opts.updated) : this.timeCreated; + + // Fields derived from bytes + if (bytes) { + this.size = bytes.byteLength; + this.md5Hash = generateMd5Hash(bytes); + this.crc32c = `${crc32c(bytes)}`; + } else if (opts.size !== undefined && opts.md5Hash && opts.crc32c) { + this.size = opts.size; + this.md5Hash = opts.md5Hash; + this.crc32c = opts.crc32c; + } else { + throw new Error("Must pass bytes array or opts object with size, md5hash, and crc32c"); + } + + 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++; - } + proposedMetadata.update(proposedChanges, /* shouldTrigger = */ false); + } + 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 || {}, + }; + } - rulesResource = { - ...rulesResource, - ...proposedChanges, - }; + private setDownloadTokensFromCustomMetadata() { + if (!this.customMetadata) { + return; } - return rulesResource; + if (this.customMetadata.firebaseStorageDownloadTokens) { + this.downloadTokens = [ + ...new Set([ + ...this.downloadTokens, + ...this.customMetadata.firebaseStorageDownloadTokens.split(","), + ]), + ]; + delete this.customMetadata.firebaseStorageDownloadTokens; + } } - update(incoming: IncomingMetadata): void { - if (incoming.contentDisposition) { - this.contentDisposition = incoming.contentDisposition; - } + private deleteFieldsSetAsNull() { + const deletableFields: (keyof this)[] = [ + "contentDisposition", + "contentType", + "contentLanguage", + "contentEncoding", + "cacheControl", + ]; + + deletableFields.map((field: keyof this) => { + if (this[field] === null) { + delete this[field]; + } + }); - if (incoming.contentType) { - this.contentType = incoming.contentType; + if (this.customMetadata) { + Object.keys(this.customMetadata).map((key: string) => { + if (!this.customMetadata) return; + if (this.customMetadata[key] === null) { + delete this.customMetadata[key]; + } + }); } + } - if (incoming.metadata) { - this.customMetadata = incoming.metadata; + // 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.contentLanguage) { - this.contentLanguage = incoming.contentLanguage; + if (incoming.contentType !== undefined) { + this.contentType = incoming.contentType === null ? undefined : incoming.contentType; } - if (incoming.contentEncoding) { - this.contentEncoding = incoming.contentEncoding; + if (incoming.contentLanguage !== undefined) { + this.contentLanguage = + incoming.contentLanguage === null ? undefined : incoming.contentLanguage; } - if (this.generation) { - this.generation++; + if (incoming.contentEncoding !== undefined) { + this.contentEncoding = + incoming.contentEncoding === null ? undefined : incoming.contentEncoding; } - this.updated = new Date(); + if (incoming.cacheControl !== undefined) { + this.cacheControl = incoming.cacheControl === null ? undefined : incoming.cacheControl; + } - if (incoming.cacheControl) { - this.cacheControl = incoming.cacheControl; + 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._cloudFunctions.dispatch("metadataUpdate", new CloudStorageObjectMetadata(this)); + this.metageneration++; + this.updated = new Date(); + this.setDownloadTokensFromCustomMetadata(); + if (shouldTrigger) { + this._cloudFunctions.dispatch("metadataUpdate", new CloudStorageObjectMetadata(this)); + } } - addDownloadToken(): void { - if (!this.downloadTokens || this.downloadTokens === "") { - this.downloadTokens = uuid.v4(); - return; - } - const tokens = this.downloadTokens.split(","); - this.downloadTokens = [...tokens, uuid.v4()].join(","); - this.update({}); + addDownloadToken(shouldTrigger = true): void { + this.downloadTokens = [...(this.downloadTokens || []), uuid.v4()]; + this.update({}, shouldTrigger); } deleteDownloadToken(token: string): void { - if (!this.downloadTokens || this.downloadTokens === "") { + if (!this.downloadTokens.length) { return; } - const tokens = this.downloadTokens.split(","); - const remainingTokens = tokens.filter((t) => t != token); - this.downloadTokens = remainingTokens.join(","); - if (remainingTokens.length == 0) { + + const remainingTokens = this.downloadTokens.filter((t) => t !== token); + this.downloadTokens = remainingTokens; + 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({}); } + + static fromJSON(data: string, cloudFunctions: StorageCloudFunctions): StoredFileMetadata { + const opts = JSON.parse(data) as SerializedFileMetadata; + return new StoredFileMetadata(opts, cloudFunctions); + } + + public static toJSON(metadata: StoredFileMetadata): string { + return JSON.stringify( + metadata, + (key, value) => { + if (key.startsWith("_")) { + return undefined; + } + + return value; + }, + 2, + ); + } } export interface RulesResourceMetadata { @@ -170,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 { @@ -190,41 +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; - this.contentEncoding = md.contentEncoding; - this.contentDisposition = md.contentDisposition; - this.metadata = md.customMetadata; + 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; @@ -240,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"; @@ -250,17 +381,32 @@ export class CloudStorageBucketMetadata { this.location = "US"; this.storageClass = "STANDARD"; this.etag = "===="; - this.locationType = "mutli-region"; + this.locationType = "multi-region"; } } +export class CloudStorageObjectAccessControlMetadata { + kind = "storage#objectAccessControl"; + + constructor( + public object: string, + public generation: string, + public selfLink: string, + public id: string, + public role: string, + public entity: string, + public bucket: 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; @@ -268,42 +414,87 @@ export class CloudStorageObjectMetadata { md5Hash: string; crc32c: string; etag: string; - metadata: { [s: string]: 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; - this.metadata = { - firebaseStorageDownloadTokens: md.downloadTokens, - ...md.customMetadata, - }; + 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(metadata.customMetadata || {})) { + this.metadata = { + ...this.metadata, + ...metadata.customMetadata, + }; + } + + if (metadata.downloadTokens.length) { + this.metadata = { + ...this.metadata, + firebaseStorageDownloadTokens: metadata.downloadTokens.join(","), + }; + } + + if (!Object.keys(this.metadata).length) { + delete this.metadata; + } + + if (metadata.contentLanguage) { + this.contentLanguage = metadata.contentLanguage; + } + + if (metadata.cacheControl) { + this.cacheControl = metadata.cacheControl; + } - // 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.contentDisposition) { + this.contentDisposition = metadata.contentDisposition; + } + + if (metadata.contentEncoding) { + this.contentEncoding = metadata.contentEncoding; + } + + 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(); } } @@ -314,21 +505,29 @@ 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`; } function generateMd5Hash(bytes: Buffer): string { const hash = crypto.createHash("md5"); hash.update(bytes); - return hash.digest("hex"); + 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..fb02bd360e1 --- /dev/null +++ b/src/emulator/storage/persistence.ts @@ -0,0 +1,97 @@ +import { openSync, closeSync, readSync, unlinkSync, mkdirSync } from "fs"; +import { rm } from "node:fs/promises"; +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 rm(this._dirPath, { recursive: true }); + 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 1ade8abc3e7..e58aaa6c7ed 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, @@ -21,7 +25,15 @@ import * as utils from "../../../utils"; import { Constants } from "../../constants"; import { downloadEmulator } from "../../download"; import * as fs from "fs-extra"; -import { DownloadDetails } from "../../downloadableEmulators"; +import { + _getCommand, + getDownloadDetails, + handleEmulatorProcessError, +} from "../../downloadableEmulators"; +import { EmulatorRegistry } from "../../registry"; + +const lock = new AsyncLock(); +const synchonizationKey = "key"; export interface RulesetVerificationOpts { file: { @@ -31,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, @@ -67,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 || []); @@ -80,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 { @@ -94,23 +116,24 @@ export class StorageRulesRuntime { private _childprocess?: ChildProcess; private _alive = false; - constructor(private readonly _jarPath: string) {} - get alive() { return this._alive; } - async start(auto_download = true) { - const hasEmulator = fs.existsSync(this._jarPath); - 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.`, ); } @@ -122,15 +145,15 @@ export class StorageRulesRuntime { } this._alive = true; - this._childprocess = spawn("java", ["-jar", this._jarPath, "serve"], { + const command = _getCommand(Emulators.STORAGE, {}); + this._childprocess = spawn(command.binary, command.args, { 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) => { @@ -143,36 +166,50 @@ export class StorageRulesRuntime { }; }); - this._childprocess.stderr.on("data", (buf: Buffer) => { + // This catches error when spawning the java process + this._childprocess.on("error", (err: any) => { + void handleEmulatorProcessError(Emulators.STORAGE, err); + }); + + // This catches errors from the java process (i.e. missing jar file) + this._childprocess.stderr?.on("data", (buf: Buffer) => { const error = buf.toString(); - if (error.includes("Invalid or corrupt jarfile")) { + 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 output: ${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; @@ -189,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"); } @@ -216,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; }> { @@ -236,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), }; @@ -249,7 +310,7 @@ export class StorageRulesRuntime { ruleset: new StorageRulesetInstance( this, response.result.rulesVersion, - runtimeActionRequest.context.rulesetName + runtimeActionRequest.context.rulesetName, ), }; } @@ -258,7 +319,7 @@ export class StorageRulesRuntime { async verifyWithRuleset( rulesetName: string, opts: RulesetVerificationOpts, - runtimeVariableOverrides: { [s: string]: ExpressionValue } = {} + runtimeVariableOverrides: { [s: string]: ExpressionValue } = {}, ): Promise< Promise<{ permitted?: boolean; @@ -286,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 = []; @@ -309,12 +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 == "number") { - if (Math.floor(obj) == obj) { + if (typeof obj === "boolean") { + return { bool_value: obj }; + } + + if (typeof obj === "number") { + if (Math.floor(obj) === obj) { return { int_value: obj }; } else { return { float_value: obj }; @@ -345,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]); @@ -362,15 +447,34 @@ function toExpressionValue(obj: any): ExpressionValue { }; } - console.warn(obj); - throw new FirebaseError(`Can not convert variables for Cloud Storage rules runtime`); + throw new FirebaseError( + `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, @@ -388,7 +492,6 @@ function createRequestExpressionValue(opts: RulesetVerificationOpts): Expression segments: opts.path .split("/") .filter((s) => s) - .slice(3) .map((simple) => ({ simple, })), @@ -396,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 245a70e7523..3462b9687f4 100644 --- a/src/emulator/storage/server.ts +++ b/src/emulator/storage/server.ts @@ -1,10 +1,13 @@ +import * as cors from "cors"; import * as express from "express"; 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 @@ -12,29 +15,37 @@ 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}`, ); - // Allow all origins and headers for CORS requests to Storage Emulator. - // This is safe since Storage Emulator does not use cookies. - app.use((req, res, next) => { - res.set("Access-Control-Allow-Origin", "*"); - res.set("Access-Control-Allow-Headers", "*"); - res.set("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH"); - res.set( - "Access-Control-Expose-Headers", - [ + // 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( + cors({ + origin: true, + 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", @@ -43,16 +54,9 @@ export function createApp( "x-goog-upload-status", "x-goog-upload-chunk-granularity", "x-goog-upload-control-url", - ].join(",") - ); - - if (req.method === "OPTIONS") { - // This is a CORS preflight request. Just handle it. - res.end(); - } else { - next(); - } - }); + ], + }), + ); app.use(bodyParser.raw({ limit: "130mb", type: "application/x-www-form-urlencoded" })); app.use(bodyParser.raw({ limit: "130mb", type: "multipart/related" })); @@ -60,26 +64,125 @@ export function createApp( app.use( express.json({ type: ["application/json"], - }) + }), ); - app.post("/internal/reset", (req, res) => { - storageLayer.reset(); + app.post("/internal/export", async (req, res) => { + 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, { initiatedBy }); res.sendStatus(200); }); - app.use("/v0", createFirebaseEndpoints(emulator)); - app.use("/", createCloudEndpoints(emulator)); + /** + * 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) => { + 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 { files } = rulesRaw; - app.all("**", (req, res) => { - if (process.env.STORAGE_EMULATOR_DEBUG) { - console.table(req.headers); - console.log(req.method, req.url); - res.json("endpoint not implemented"); - } else { - res.sendStatus(404).json("endpoint not implemented"); + 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; } + + 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", + }); + return; + } + + res.status(200).json({ + message: "Rules updated successfully", + }); + }); + + app.post("/internal/reset", (req, res) => { + emulator.reset(); + res.sendStatus(200); }); + app.use("/v0", createFirebaseEndpoints(emulator)); + app.use("/", createCloudEndpoints(emulator)); + 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/taskQueue.spec.ts b/src/emulator/taskQueue.spec.ts new file mode 100644 index 00000000000..1973d484cbb --- /dev/null +++ b/src/emulator/taskQueue.spec.ts @@ -0,0 +1,450 @@ +import * as _ from "lodash"; +import * as sinon from "sinon"; +import * as nodeFetch from "node-fetch"; +import AbortController from "abort-controller"; +import { expect } from "chai"; +import { EmulatedTask, EmulatedTaskMetadata, Queue, TaskQueue, TaskStatus } from "./taskQueue"; +import { RateLimits, RetryConfig, Task, TaskQueueConfig } from "./tasksEmulator"; + +describe("Queue Test", () => { + it("should create an empty task queue", () => { + const taskQueue = new Queue(); + expect(taskQueue).to.not.be.null; + }); + + it("should enqueue an element to a task queue", () => { + const taskQueue = new Queue(); + taskQueue.enqueue("1", 1); + taskQueue.enqueue("2", 2); + }); + + it("should dequeue an element to a task queue", () => { + const taskQueue = new Queue(); + taskQueue.enqueue("1", 1); + taskQueue.enqueue("2", 2); + const first = taskQueue.dequeue(); + const second = taskQueue.dequeue(); + expect(first).to.eq(1); + expect(second).to.eq(2); + }); + + it("should handle enqueueing and dequeueing elements from a task queue", () => { + const taskQueue = new Queue(); + taskQueue.enqueue("1", 1); + taskQueue.enqueue("2", 2); + const first = taskQueue.dequeue(); + taskQueue.enqueue("3", 3); + const second = taskQueue.dequeue(); + const third = taskQueue.dequeue(); + expect(first).to.eq(1); + expect(second).to.eq(2); + expect(third).to.eq(3); + }); + + it("should properly remove items from a queue", () => { + const taskQueue = new Queue(); + taskQueue.enqueue("1", 1); + taskQueue.enqueue("2", 2); + taskQueue.enqueue("3", 3); + taskQueue.enqueue("4", 4); + taskQueue.enqueue("5", 5); + taskQueue.remove("1"); + taskQueue.remove("3"); + taskQueue.remove("5"); + const first = taskQueue.dequeue(); + const second = taskQueue.dequeue(); + + expect(first).to.eq(2); + expect(second).to.eq(4); + + expect(() => taskQueue.dequeue()).to.throw("Trying to dequeue from an empty queue"); + }); + + it("should error when trying to peek or remove from an empty task queue", () => { + const taskQueue = new Queue(); + expect(() => taskQueue.peek()).to.throw("Trying to peek into an empty queue"); + expect(() => taskQueue.dequeue()).to.throw("Trying to dequeue from an empty queue"); + }); + + it("should error when trying to remove a task that doesn't exist in the queue", () => { + const taskQueue = new Queue(); + expect(() => taskQueue.peek()).to.throw("Trying to peek into an empty queue"); + expect(() => taskQueue.dequeue()).to.throw("Trying to dequeue from an empty queue"); + }); + + it("should be able to peek into a queue", () => { + const taskQueue = new Queue(); + taskQueue.enqueue("1", 1); + expect(taskQueue.peek()).to.eq(1); + expect(taskQueue.peek()).to.eq(1); + }); + + it("should only allow unique IDs", () => { + const taskQueue = new Queue(); + taskQueue.enqueue("1", 1); + expect(() => taskQueue.enqueue("1", 1)).to.throw("Queue IDs must be unique"); + }); + + it("should error when trying to remove a non-existent item", () => { + const taskQueue = new Queue(); + expect(() => taskQueue.remove("1")).to.throw("Trying to remove a task that doesn't exist"); + }); + + it("should be able to remove an item when it is the only thing in the queue", () => { + const taskQueue = new Queue(); + taskQueue.enqueue("1", 1); + taskQueue.remove("1"); + taskQueue.enqueue("1", 1); + expect(taskQueue.dequeue()).to.eq(1); + }); + + it("should properly determine if the queue is empty", () => { + const taskQueue = new Queue(); + expect(taskQueue.isEmpty()).to.eq(true); + taskQueue.enqueue("1", 1); + expect(taskQueue.isEmpty()).to.eq(false); + taskQueue.dequeue(); + expect(taskQueue.isEmpty()).to.eq(true); + }); + + it("should report the correct size", () => { + const taskQueue = new Queue(); + taskQueue.enqueue("1", 1); + taskQueue.remove("1"); + taskQueue.enqueue("1", 1); + expect(taskQueue.size()).to.eq(1); + }); + + it("should error if at capacity", () => { + const taskQueue = new Queue(1); + taskQueue.enqueue("1", 1); + expect(() => taskQueue.enqueue("2", 2)).to.throw("Queue has reached capacity"); + }); + + it("should return all items", () => { + const taskQueue = new Queue(); + taskQueue.enqueue("1", 1); + taskQueue.enqueue("2", 2); + expect(taskQueue.getAll()).to.deep.eq([1, 2]); + }); +}); + +describe("Task Queue", () => { + const TEST_RETRY_CONFIG: RetryConfig = { + maxAttempts: 10, + maxRetrySeconds: 30, + maxBackoffSeconds: 40, + maxDoublings: 2, + minBackoffSeconds: 2, + }; + + const TEST_RATE_LIMITS: RateLimits = { + maxConcurrentDispatches: 1, + maxDispatchesPerSecond: 2, + }; + + const TEST_TASK_QUEUE_CONFIG: TaskQueueConfig = { + retryConfig: TEST_RETRY_CONFIG, + rateLimits: TEST_RATE_LIMITS, + timeoutSeconds: 0, + retry: false, + defaultUri: "http://website.com/", + }; + + const TEST_TASK_QUEUE_NAME = "task-queue"; + + const mockTask: Task = { + name: "", + httpRequest: { + url: "", + oidcToken: { serviceAccountEmail: "test-user@email.com" }, + body: { test: "test" }, + headers: {}, + }, + }; + + const mockMetadata: EmulatedTaskMetadata = { + currentAttempt: 1, + currentBackoff: 0, + startTime: 0, + status: TaskStatus.NOT_STARTED, + lastRunTime: null, + executionCount: 0, + previousResponse: null, + }; + + const mockEmulatedTask: EmulatedTask = { + task: mockTask, + metadata: mockMetadata, + }; + + let TEST_TASK: EmulatedTask; + const NOW = 1000 * 60; + + const stubs: sinon.SinonStub[] = []; + + before(() => { + sinon.stub(Date, "now").returns(NOW); + }); + + after(() => { + sinon.restore(); + }); + + beforeEach(() => { + TEST_TASK = _.cloneDeep(mockEmulatedTask); + TEST_TASK.metadata.currentBackoff = 0; + }); + + afterEach(() => { + stubs.forEach((s) => s.restore()); + }); + + // Handle Retry Tests + describe("Retry", () => { + it("should update retried task status to not started", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + TEST_TASK.metadata.status = TaskStatus.RETRY; + taskQueue.setDispatch([TEST_TASK]); + taskQueue.handleRetry(0); + expect(TEST_TASK.metadata.status).to.be.eq(TaskStatus.NOT_STARTED); + }); + + it("should update retried task status to failed when max attempts are reached", () => { + const config = _.cloneDeep(TEST_TASK_QUEUE_CONFIG); + config.retryConfig.maxRetrySeconds = null; + + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, config); + TEST_TASK.metadata.status = TaskStatus.RETRY; + TEST_TASK.metadata.currentAttempt = 11; + taskQueue.setDispatch([TEST_TASK]); + taskQueue.handleRetry(0); + expect(TEST_TASK.metadata.status).to.be.eq(TaskStatus.FAILED); + }); + + it("should update retried task status to failed when max attempts and max time are reached", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + TEST_TASK.metadata.status = TaskStatus.RETRY; + TEST_TASK.metadata.currentAttempt = 11; + TEST_TASK.metadata.startTime = NOW - (1000 * 30 + 1); + taskQueue.setDispatch([TEST_TASK]); + taskQueue.handleRetry(0); + expect(TEST_TASK.metadata.status).to.be.eq(TaskStatus.FAILED); + }); + + it("should double retry time", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + TEST_TASK.metadata.status = TaskStatus.RETRY; + taskQueue.setDispatch([TEST_TASK]); + taskQueue.handleRetry(0); + expect(TEST_TASK.metadata.currentBackoff).to.be.eq(2); + taskQueue.handleRetry(0); + expect(TEST_TASK.metadata.currentBackoff).to.be.eq(4); + taskQueue.handleRetry(0); + expect(TEST_TASK.metadata.currentBackoff).to.be.eq(8); + }); + + it("should increment the attempt number", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + TEST_TASK.metadata.status = TaskStatus.RETRY; + taskQueue.setDispatch([TEST_TASK]); + taskQueue.handleRetry(0); + expect(TEST_TASK.metadata.currentAttempt).to.be.eq(2); + }); + + it("shouldn't exceed the max backoff seconds", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + TEST_TASK.metadata.status = TaskStatus.RETRY; + TEST_TASK.metadata.currentAttempt = 9; + taskQueue.setDispatch([TEST_TASK]); + taskQueue.handleRetry(0); + expect(TEST_TASK.metadata.currentBackoff).to.be.eq(40); + }); + + it("should increase by a constant when doublings have maxxed", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + TEST_TASK.metadata.status = TaskStatus.RETRY; + TEST_TASK.metadata.currentAttempt = 5; + // 1 -> 2 + // 2 -> 4 + // 3 -> 8 + // 4 -> 16 + // 5 -> 24 + taskQueue.setDispatch([TEST_TASK]); + taskQueue.handleRetry(0); + expect(TEST_TASK.metadata.currentBackoff).to.be.eq(24); + taskQueue.handleRetry(0); + expect(TEST_TASK.metadata.currentBackoff).to.be.eq(32); + }); + + it("should throw if task doesn't exist", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + taskQueue.setDispatch([TEST_TASK, null]); + expect(() => taskQueue.handleRetry(1)).to.throw("Trying to retry a nonexistent task"); + }); + }); + + describe("Run Task", () => { + it("should call the task url", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + const response = new nodeFetch.Response(undefined, { status: 200 }); + const fetchStub = sinon.stub(nodeFetch, "default").resolves(response); + stubs.push(fetchStub); + taskQueue.setDispatch([TEST_TASK]); + const res = taskQueue.runTask(0).then(() => { + expect(fetchStub).to.have.been.calledOnce.and.calledWith(TEST_TASK.task.httpRequest.url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CloudTasks-QueueName": "task-queue", + "X-CloudTasks-TaskName": "", + "X-CloudTasks-TaskRetryCount": "0", + "X-CloudTasks-TaskExecutionCount": "0", + "X-CloudTasks-TaskETA": "60000", + ...TEST_TASK.task.httpRequest.headers, + }, + signal: new AbortController().signal, + body: JSON.stringify(TEST_TASK.task.httpRequest.body), + }); + }); + return res; + }); + + it("Should wait until the backoff time has elapsed", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + const response = new nodeFetch.Response(undefined, { status: 200 }); + + TEST_TASK.metadata.lastRunTime = NOW - 1000; + TEST_TASK.metadata.currentBackoff = 3; + + const fetchStub = sinon.stub(nodeFetch, "default").resolves(response); + stubs.push(fetchStub); + taskQueue.setDispatch([TEST_TASK]); + + const res = taskQueue.runTask(0).then(() => { + expect(fetchStub).to.not.have.been.called; + }); + return res; + }); + + it("Should run if the backoff time has elapsed", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + const response = new nodeFetch.Response(undefined, { status: 200 }); + TEST_TASK.metadata.lastRunTime = NOW - 3 * 1000; + TEST_TASK.metadata.currentBackoff = 2; + + const fetchStub = sinon.stub(nodeFetch, "default").resolves(response); + stubs.push(fetchStub); + taskQueue.setDispatch([TEST_TASK]); + const res = taskQueue.runTask(0).then(() => { + expect(fetchStub).to.have.been.calledOnce.and.calledWith(TEST_TASK.task.httpRequest.url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CloudTasks-QueueName": "task-queue", + "X-CloudTasks-TaskName": "", + "X-CloudTasks-TaskRetryCount": "0", + "X-CloudTasks-TaskExecutionCount": "0", + "X-CloudTasks-TaskETA": "60000", + ...TEST_TASK.task.httpRequest.headers, + }, + signal: new AbortController().signal, + body: JSON.stringify(TEST_TASK.task.httpRequest.body), + }); + }); + return res; + }); + + it("should properly update metadata on success", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + const response = new nodeFetch.Response(undefined, { status: 200 }); + + const fetchStub = sinon.stub(nodeFetch, "default").resolves(response); + stubs.push(fetchStub); + taskQueue.setDispatch([TEST_TASK]); + const res = taskQueue.runTask(0).then(() => { + expect(TEST_TASK.metadata.status).to.be.eq(TaskStatus.FINISHED); + }); + return res; + }); + + it("should properly update metadata on failure", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + const response = new nodeFetch.Response(undefined, { status: 500 }); + + const fetchStub = sinon.stub(nodeFetch, "default").resolves(response); + stubs.push(fetchStub); + taskQueue.setDispatch([TEST_TASK]); + const res = taskQueue.runTask(0).then(() => { + expect(TEST_TASK.metadata.status).to.be.eq(TaskStatus.RETRY); + expect(TEST_TASK.metadata.lastRunTime).to.be.eq(NOW); + }); + return res; + }); + + it("should throw if task doesn't exist", async () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + taskQueue.setDispatch([TEST_TASK, null]); + await expect(taskQueue.runTask(1)).to.be.rejectedWith( + "Trying to dispatch a nonexistent task", + ); + }); + }); + + describe("enqueue", () => { + it("should set the proper task uri", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + const task = _.cloneDeep(TEST_TASK.task); + taskQueue.enqueue(task); + expect(task.httpRequest.url).to.be.eq(TEST_TASK_QUEUE_CONFIG.defaultUri); + }); + }); + + describe("Dispatch Tasks", () => { + it("should move the first task in the queue to the dispatch", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + const task = _.cloneDeep(TEST_TASK.task); + taskQueue.enqueue(task); + taskQueue.setTokens(1); + taskQueue.dispatchTasks(); + expect(taskQueue.getDispatch()[0]!.task).to.deep.eq(task); + }); + + it("should not dispatch tasks if the queue has no tokens", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + const task = _.cloneDeep(TEST_TASK.task); + taskQueue.enqueue(task); + taskQueue.setTokens(0); + taskQueue.dispatchTasks(); + expect(taskQueue.getDispatch()[0]).to.be.eq(null); + }); + + it("should not dispatch tasks if the dispatch is full", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + const task1 = _.cloneDeep(TEST_TASK.task); + const task2 = _.cloneDeep(TEST_TASK.task); + task1.name = "task1"; + task2.name = "task2"; + taskQueue.enqueue(task1); + taskQueue.enqueue(task2); + taskQueue.setTokens(1); + taskQueue.dispatchTasks(); + expect(taskQueue.getDispatch().map((et) => et?.task)).to.deep.eq([task1]); + }); + + it("should fill up empty dispatch slots", () => { + const taskQueue = new TaskQueue(TEST_TASK_QUEUE_NAME, TEST_TASK_QUEUE_CONFIG); + const task1 = _.cloneDeep(TEST_TASK.task); + task1.name = "task1"; + taskQueue.enqueue(task1); + taskQueue.setDispatch([TEST_TASK, null, TEST_TASK]); + taskQueue.setTokens(1); + taskQueue.dispatchTasks(); + expect(taskQueue.getDispatch().map((et) => et?.task)).to.deep.eq([ + TEST_TASK.task, + task1, + TEST_TASK.task, + ]); + }); + }); +}); diff --git a/src/emulator/taskQueue.ts b/src/emulator/taskQueue.ts new file mode 100644 index 00000000000..9cb3bfd92a3 --- /dev/null +++ b/src/emulator/taskQueue.ts @@ -0,0 +1,464 @@ +import AbortController from "abort-controller"; +import { EmulatorLogger } from "./emulatorLogger"; +import { RetryConfig, Task, TaskQueueConfig } from "./tasksEmulator"; +import { Emulators } from "./types"; +import fetch from "node-fetch"; + +class Node { + public data: T; + public next: Node | null; + public prev: Node | null; + + constructor(data: T) { + this.data = data; + this.next = null; + this.prev = null; + } +} + +// A FIFO queue that supports enqueueing, dequeueing, and deleting elements in O(1) time. +export class Queue { + private first: Node | null; + private last: Node | null; + private nodeMap: Record> = {}; + private capacity; + private count = 0; + + constructor(capacity = 10000) { + this.first = null; + this.last = null; + this.capacity = capacity; + } + + enqueue(id: string, item: T): void { + if (this.count >= this.capacity) { + throw new Error("Queue has reached capacity"); + } + + const newNode = new Node(item); + if (this.nodeMap[id] !== undefined) { + throw new Error("Queue IDs must be unique"); + } + this.nodeMap[id] = newNode; + if (!this.first) { + this.first = newNode; + } + if (this.last) { + this.last.next = newNode; + } + newNode.prev = this.last; + this.last = newNode; + + this.count++; + } + + peek(): T { + if (this.first) { + return this.first.data; + } else { + throw new Error("Trying to peek into an empty queue"); + } + } + + dequeue(): T { + if (this.first) { + const currentFirst = this.first; + this.first = this.first.next; + if (this.last === currentFirst) { + this.last = null; + } + this.count--; + return currentFirst.data; + } else { + throw new Error("Trying to dequeue from an empty queue"); + } + } + + remove(id: string): void { + if (this.nodeMap[id] === undefined) { + throw new Error("Trying to remove a task that doesn't exist"); + } + const toRemove = this.nodeMap[id]; + + if (toRemove.next === null && toRemove.prev === null) { + this.first = null; + this.last = null; + } else if (toRemove.next === null) { + this.last = toRemove.prev; + toRemove.prev!.next = null; + } else if (toRemove.prev === null) { + this.first = toRemove.next; + toRemove.next.prev = null; + } else { + const prev = toRemove.prev; + const next = toRemove.next; + prev.next = next; + next.prev = prev; + } + delete this.nodeMap[id]; + this.count--; + } + + getAll(): T[] { + const all = []; + let curr = this.first; + while (curr) { + all.push(curr.data); + curr = curr.next; + } + return all; + } + + isEmpty(): boolean { + return this.first === null; + } + + size(): number { + return this.count; + } +} + +export enum TaskStatus { + // When a task has been created/retried and should check if it should run + NOT_STARTED, + // When a task has been dispatched and is currently running + RUNNING, + // When a task has been dispatched and failed, but will be tried again + RETRY, + // When a task has failed and exhausted it's retry parameters and will not be tried again. + FAILED, + // When a task has been completed successfully + FINISHED, +} + +export interface EmulatedTaskMetadata { + currentAttempt: number; + currentBackoff: number; + startTime: number; + status: TaskStatus; + lastRunTime: number | null; + previousResponse: number | null; + executionCount: number; +} + +export interface EmulatedTask { + task: Task; + metadata: EmulatedTaskMetadata; +} + +export interface QueueStatistics { + // number of tasks currently in the queue + numberOfTasks: number; + // Tasks added/min (last 5 min) + tasksAdded: number; + // Tasks executed (failed or completed) (last min) + completedLastMin: number; + // Number of tasks failed/min (last 5 min) + failedTasks: number; + // Number of tasks currently running + runningTasks: number; + // max rate of dispatch (per second) + maxRate: number; + // max dispatched at a time + maxConcurrent: number; +} + +export class TaskQueue { + queue: Queue = new Queue(); + logger = EmulatorLogger.forEmulator(Emulators.TASKS); + static TASK_QUEUE_INTERVAL = 1000; + + // Current number of tokens the queue has + private tokens = 0; + // The maximum number of tokens that can fit in the "bucket" + private maxTokens; + // The last time the token bucket was updated, used in calculations of how many tokens to add. + private lastTokenUpdate; + // The IDs of all the tasks ever queued in this session to allow for deduplication + private queuedIds: Set; + // The tasks that have been dispatched that the queue is waiting on + private dispatches: (EmulatedTask | null)[]; + // The indexes of the open slots in the dispatch array + private openDispatches: number[]; + + private addedTimes: number[] = []; + private completedTimes: number[] = []; + private failedTimes: number[] = []; + + constructor( + private key: string, + private config: TaskQueueConfig, + ) { + this.maxTokens = Math.max(this.config.rateLimits.maxDispatchesPerSecond, 1.1); + this.lastTokenUpdate = Date.now(); + this.queuedIds = new Set(); + this.dispatches = new Array( + this.config.rateLimits.maxConcurrentDispatches, + ).fill(null); + this.openDispatches = Array.from(this.dispatches.keys()); + } + + // Moves tasks from the queue to the dispatch if the following requirements are met: + // - There are tasks within the queue + // - There is space in the dispatch + // - There are tokens available (used for rate limiting) + + dispatchTasks(): void { + while (!this.queue.isEmpty() && this.openDispatches.length > 0 && this.tokens >= 1) { + const dispatchLocation = this.openDispatches.pop(); + if (dispatchLocation !== undefined) { + const dispatch = this.queue.dequeue(); + + dispatch.metadata.lastRunTime = null; + dispatch.metadata.currentAttempt = 1; + dispatch.metadata.status = TaskStatus.NOT_STARTED; + dispatch.metadata.startTime = Date.now(); + + this.dispatches[dispatchLocation] = dispatch; + this.tokens--; + } + } + } + + // Used for testing + setDispatch(dispatches: (EmulatedTask | null)[]): void { + this.dispatches = dispatches; + const open = []; + for (let i = 0; i < this.dispatches.length; i++) { + if (dispatches[i] === null) { + open.push(i); + } + } + this.openDispatches = open; + } + + getDispatch(): (EmulatedTask | null)[] { + return this.dispatches; + } + + // Updates the status of all tasks that are currently in the task dispatch + processDispatch(): void { + for (let i = 0; i < this.dispatches.length; i++) { + if (this.dispatches[i] !== null) { + switch (this.dispatches[i]?.metadata.status) { + case TaskStatus.FAILED: + this.dispatches[i] = null; + this.openDispatches.push(i); + this.completedTimes.push(Date.now()); + this.failedTimes.push(Date.now()); + break; + case TaskStatus.NOT_STARTED: + void this.runTask(i); + break; + case TaskStatus.RETRY: + this.handleRetry(i); + break; + case TaskStatus.FINISHED: + this.dispatches[i] = null; + this.openDispatches.push(i); + this.completedTimes.push(Date.now()); + break; + } + } + } + } + + async runTask(dispatchIndex: number): Promise { + if (this.dispatches[dispatchIndex] === null) { + throw new Error("Trying to dispatch a nonexistent task"); + } + + const emulatedTask = this.dispatches[dispatchIndex] as EmulatedTask; + + if ( + emulatedTask.metadata.lastRunTime !== null && + Date.now() - emulatedTask.metadata.lastRunTime < emulatedTask.metadata.currentBackoff * 1000 + ) { + // Task is not yet ready to run + return; + } + + emulatedTask.metadata.status = TaskStatus.RUNNING; + try { + const headers: Record = { + "Content-Type": "application/json", + "X-CloudTasks-QueueName": this.key, + "X-CloudTasks-TaskName": emulatedTask.task.name.split("/").pop()!, + "X-CloudTasks-TaskRetryCount": `${emulatedTask.metadata.currentAttempt - 1}`, + "X-CloudTasks-TaskExecutionCount": `${emulatedTask.metadata.executionCount}`, + "X-CloudTasks-TaskETA": `${emulatedTask.task.scheduleTime || Date.now()}`, + ...emulatedTask.task.httpRequest.headers, + }; + if (emulatedTask.metadata.previousResponse) { + headers["X-CloudTasks-TaskPreviousResponse"] = `${emulatedTask.metadata.previousResponse}`; + } + const controller = new AbortController(); + const signal = controller.signal; + const request = fetch(emulatedTask.task.httpRequest.url, { + method: "POST", + headers: headers, + body: JSON.stringify(emulatedTask.task.httpRequest.body), + signal: signal, + }); + + const dispatchDeadline = emulatedTask.task.dispatchDeadline; + const dispatchDeadlineSeconds = dispatchDeadline + ? parseInt(dispatchDeadline.substring(0, dispatchDeadline.length - 1)) + : 60; + + const abortId = setTimeout(() => { + // TODO: Set X-CloudTasks-TaskRetryReason + controller.abort(); + }, dispatchDeadlineSeconds * 1000); + + const response = await request; + clearTimeout(abortId); + if (response.ok) { + emulatedTask.metadata.status = TaskStatus.FINISHED; + return; + } else { + if (!(response.status >= 500 && response.status <= 599)) { + emulatedTask.metadata.executionCount++; + } + emulatedTask.metadata.previousResponse = response.status; + emulatedTask.metadata.status = TaskStatus.RETRY; + emulatedTask.metadata.lastRunTime = Date.now(); + } + } catch (e) { + this.logger.logLabeled("WARN", `${e}`); + emulatedTask.metadata.status = TaskStatus.RETRY; + emulatedTask.metadata.lastRunTime = Date.now(); + } + } + + handleRetry(dispatchIndex: number): void { + if (this.dispatches[dispatchIndex] === null) { + throw new Error("Trying to retry a nonexistent task"); + } + const { metadata } = this.dispatches[dispatchIndex] as EmulatedTask; + const { retryConfig } = this.config; + + // Determine if the task has failed + if (this.shouldStopRetrying(metadata, retryConfig)) { + metadata.status = TaskStatus.FAILED; + return; + } + + // Compute Retry Parameters + this.updateMetadata(metadata, retryConfig); + metadata.status = TaskStatus.NOT_STARTED; + } + + shouldStopRetrying(metadata: EmulatedTaskMetadata, retryOptions: RetryConfig): boolean { + if (metadata.currentAttempt > retryOptions.maxAttempts) { + if (retryOptions.maxRetrySeconds === null || retryOptions.maxRetrySeconds === 0) { + return true; + } + if (Date.now() - metadata.startTime > retryOptions.maxRetrySeconds * 1000) { + return true; + } + } + return false; + } + + updateMetadata(metadata: EmulatedTaskMetadata, retryOptions: RetryConfig): void { + const timeMultplier = + // Exponential increase + Math.pow(2, Math.min(metadata.currentAttempt - 1, retryOptions.maxDoublings)) + + // Constant increase (once max doublings is passed) + Math.max(0, metadata.currentAttempt - retryOptions.maxDoublings - 1) * + Math.pow(2, retryOptions.maxDoublings); + + metadata.currentBackoff = Math.min( + retryOptions.maxBackoffSeconds, + timeMultplier * retryOptions.minBackoffSeconds, + ); + metadata.currentAttempt++; + } + + isActive(): boolean { + return !this.queue.isEmpty() || this.dispatches.some((e) => e !== null); + } + + refillTokens(): void { + const tokensToAdd = + ((Date.now() - this.lastTokenUpdate) / 1000) * this.config.rateLimits.maxDispatchesPerSecond; + this.addTokens(tokensToAdd); + this.lastTokenUpdate = Date.now(); + } + + addTokens(t: number): void { + this.tokens += t; + this.tokens = Math.min(this.tokens, this.maxTokens); + } + + setTokens(t: number): void { + this.tokens = t; + } + + getTokens(): number { + return this.tokens; + } + + enqueue(task: Task): void { + if (this.queuedIds.has(task.name)) { + throw new Error(`A task has already been queued with id ${task.name}`); + } + const emulatedTask: EmulatedTask = { + task: task, + metadata: { + currentAttempt: 0, + currentBackoff: 0, + startTime: 0, + status: TaskStatus.NOT_STARTED, + lastRunTime: null, + previousResponse: null, + executionCount: 0, + }, + }; + + emulatedTask.task.httpRequest.url = + emulatedTask.task.httpRequest.url === "" + ? this.config.defaultUri + : emulatedTask.task.httpRequest.url; + + this.queue.enqueue(emulatedTask.task.name, emulatedTask); + this.queuedIds.add(task.name); + this.addedTimes.push(Date.now()); + } + + delete(taskId: string): void { + this.queue.remove(taskId); + } + + getDebugInfo(): string { + return ` + Task Queue (${this.key}): + - Active: ${this.isActive().toString()} + - Tokens: ${this.tokens} + - In Queue: ${this.queue.size()} + - Dispatch: [ + ${this.dispatches.map((t) => (t === null ? "empty" : t.task.name)).join(",\n")} + ] + - Open Locations: [${this.openDispatches.join(", ")}] + `; + } + + getStatistics(): QueueStatistics { + const fiveMinutesAgo = Date.now() - 5 * 60 * 1000; + const oneMinuteAgo = Date.now() - 60 * 1000; + this.addedTimes = this.addedTimes.filter((t) => t > fiveMinutesAgo); + this.failedTimes = this.failedTimes.filter((t) => t > fiveMinutesAgo); + this.completedTimes = this.completedTimes.filter((t) => t > oneMinuteAgo); + + return { + numberOfTasks: this.queue.size(), + tasksAdded: this.addedTimes.length / 5, + completedLastMin: this.completedTimes.length, + failedTasks: this.failedTimes.length / 5, + runningTasks: this.dispatches.length, + maxRate: this.config.rateLimits.maxDispatchesPerSecond, + maxConcurrent: this.config.rateLimits.maxConcurrentDispatches, + }; + } +} diff --git a/src/emulator/tasksEmulator.ts b/src/emulator/tasksEmulator.ts new file mode 100644 index 00000000000..af3bb3c940f --- /dev/null +++ b/src/emulator/tasksEmulator.ts @@ -0,0 +1,356 @@ +import * as express from "express"; + +import { Constants } from "./constants"; +import { EmulatorInfo, EmulatorInstance, Emulators } from "./types"; +import { createDestroyer } from "../utils"; +import { EmulatorLogger } from "./emulatorLogger"; +import { TaskQueue } from "./taskQueue"; +import * as cors from "cors"; + +export interface TasksEmulatorArgs { + port?: number; + host?: string; +} + +export interface Task { + name: string; + // A timestamp in RFC3339 UTC "Zulu" format, with nanosecond resolution and up to nine fractional + // digits. Examples: "2014-10-02T15:01:23Z" and "2014-10-02T15:01:23.045123456Z". + scheduleTime?: string; + // A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". + dispatchDeadline?: string; + httpRequest: { + url: string; + oidcToken?: { + serviceAccountEmail: string; + }; + body: any; + headers: { [key: string]: string }; + }; +} + +export interface TaskQueueConfig { + retryConfig: RetryConfig; + rateLimits: RateLimits; + timeoutSeconds: number; + retry: boolean; + defaultUri: string; + + // The configurations below this point do not currently have any effect on how the task queues are handled within the emulator + secrets?: string[]; // TODO(gburroughs): look into how we can handle this + region?: string | ResetValue; + memory?: string | ResetValue; + minInstances?: number | ResetValue; + maxInstances?: number | ResetValue; + concurrency?: number | ResetValue; + labels?: Record; +} + +export interface RetryConfig { + maxAttempts: number; + maxRetrySeconds: number | null; + maxBackoffSeconds: number; + maxDoublings: number; + minBackoffSeconds: number; +} + +export interface RateLimits { + maxConcurrentDispatches: number; + maxDispatchesPerSecond: number; +} + +type ResetValue = null; + +const RETRY_CONFIG_DEFAULTS: RetryConfig = { + maxAttempts: 3, + maxRetrySeconds: null, + maxBackoffSeconds: 60 * 60, + maxDoublings: 16, + minBackoffSeconds: 0.1, +}; + +const RATE_LIMITS_DEFAULT: RateLimits = { + maxConcurrentDispatches: 1000, + maxDispatchesPerSecond: 500, +}; + +/** + * A controller class which manages: + * - The creation of task queues + * - Enqueueing tasks to the correct queue + * - The timing for when task queue methods are run + */ +export class TaskQueueController { + static UPDATE_TIMEOUT = 0; + static LISTEN_TIMEOUT = 1000; + static TOKEN_REFRESH_INTERVAL = 1000; + queues: { [key: string]: TaskQueue } = {}; + private listenId: NodeJS.Timeout | null; + private tokenRefillIds: NodeJS.Timeout[] = []; + private running = false; + + constructor() { + this.listenId = null; + } + + enqueue(key: string, task: Task): void { + if (!this.queues[key]) { + throw new Error("Queue does not exist"); + } + this.queues[key].enqueue(task); + } + + delete(key: string, taskId: string): void { + if (!this.queues[key]) { + throw new Error("Queue does not exist"); + } + this.queues[key].delete(taskId); + } + + createQueue(key: string, config: TaskQueueConfig): void { + const newQueue = new TaskQueue(key, config); + const intervalID = setInterval( + () => newQueue.refillTokens(), + TaskQueueController.TOKEN_REFRESH_INTERVAL, + ); + this.tokenRefillIds.push(intervalID); + this.queues[key] = newQueue; + } + + /** + * If there are no active queues (a queue is active if it has tasks in the queue or dispatch) then + * wait longer (1s) before checking the status of the queues again. If there are active queues, + * continuously (1ms) call their methods to handle dispatching tasks + */ + listen(): void { + let shouldUpdate = false; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const [_key, queue] of Object.entries(this.queues)) { + shouldUpdate = shouldUpdate || queue.isActive(); + } + if (shouldUpdate) { + this.updateQueues(); + this.listenId = setTimeout(() => this.listen(), TaskQueueController.UPDATE_TIMEOUT); + } else { + this.listenId = setTimeout(() => this.listen(), TaskQueueController.LISTEN_TIMEOUT); + } + } + + updateQueues(): void { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const [_key, queue] of Object.entries(this.queues)) { + if (queue.isActive()) { + queue.dispatchTasks(); + queue.processDispatch(); + } + } + } + + start(): void { + this.running = true; + this.listen(); + } + + stop(): void { + if (this.listenId) { + clearTimeout(this.listenId); + } + this.tokenRefillIds.forEach(clearInterval); + this.running = false; + } + + isRunning(): boolean { + return this.running; + } + + getStatistics() { + const stats: Record = {}; + for (const [key, queue] of Object.entries(this.queues)) { + stats[key] = queue.getStatistics(); + } + return stats; + } +} + +export class TasksEmulator implements EmulatorInstance { + private destroyServer?: () => Promise; + private controller: TaskQueueController; + + constructor(private args: TasksEmulatorArgs) { + this.controller = new TaskQueueController(); + } + + logger = EmulatorLogger.forEmulator(Emulators.TASKS); + + validateQueueId(queueId: string): boolean { + if (typeof queueId !== "string") { + return false; + } + + if (queueId.length > 100) { + return false; + } + + const regex = /^[A-Za-z0-9-]+$/; + return regex.test(queueId); + } + createHubServer(): express.Application { + const hub = express(); + + const createTaskQueueRoute = `/projects/:project_id/locations/:location_id/queues/:queue_name`; + const createTaskQueueHandler: express.Handler = (req, res) => { + const projectId = req.params.project_id; + const locationId = req.params.location_id; + const queueName = req.params.queue_name; + if (!this.validateQueueId(queueName)) { + res.status(400).json({ + error: + "Queue ID must start with a letter followed by up to 62 letters, numbers, " + + "hyphens, or underscores and must end with a letter or a number", + }); + return; + } + + const key = `queue:${projectId}-${locationId}-${queueName}`; + this.logger.logLabeled("SUCCESS", "tasks", `Created queue with key: ${key}`); + const body = req.body as TaskQueueConfig; + + const taskQueueConfig: TaskQueueConfig = { + retryConfig: { + maxAttempts: body.retryConfig?.maxAttempts ?? RETRY_CONFIG_DEFAULTS.maxAttempts, + maxRetrySeconds: + body.retryConfig?.maxRetrySeconds ?? RETRY_CONFIG_DEFAULTS.maxRetrySeconds, + maxBackoffSeconds: + body.retryConfig?.maxBackoffSeconds ?? RETRY_CONFIG_DEFAULTS.maxBackoffSeconds, + maxDoublings: body.retryConfig?.maxDoublings ?? RETRY_CONFIG_DEFAULTS.maxDoublings, + minBackoffSeconds: + body.retryConfig?.minBackoffSeconds ?? RETRY_CONFIG_DEFAULTS.minBackoffSeconds, + }, + rateLimits: { + maxConcurrentDispatches: + body.rateLimits?.maxConcurrentDispatches ?? RATE_LIMITS_DEFAULT.maxConcurrentDispatches, + maxDispatchesPerSecond: + body.rateLimits?.maxDispatchesPerSecond ?? RATE_LIMITS_DEFAULT.maxDispatchesPerSecond, + }, + timeoutSeconds: body.timeoutSeconds ?? 10, + retry: body.retry ?? false, + defaultUri: body.defaultUri, + }; + if (taskQueueConfig.rateLimits.maxConcurrentDispatches > 5000) { + res.status(400).json({ error: "cannot set maxConcurrentDispatches to a value over 5000" }); + return; + } + + this.controller.createQueue(key, taskQueueConfig); + this.logger.log( + "DEBUG", + `Created task queue ${key} with configuration: ${JSON.stringify(taskQueueConfig)}`, + ); + + res.status(200).send({ taskQueueConfig }); + }; + + const enqueueTasksRoute = `/projects/:project_id/locations/:location_id/queues/:queue_name/tasks`; + const enqueueTasksHandler: express.Handler = (req, res) => { + if (!this.controller.isRunning()) { + this.controller.start(); + } + const projectId = req.params.project_id; + const locationId = req.params.location_id; + const queueName = req.params.queue_name; + const queueKey = `queue:${projectId}-${locationId}-${queueName}`; + if (!this.controller.queues[queueKey]) { + this.logger.log("WARN", "Tried to queue a task into a non-existent queue"); + res.status(404).send("Tried to queue a task from a non-existent queue"); + return; + } + + req.body.task.name = + req.body.task.name ?? + `/projects/${projectId}/locations/${locationId}/queues/${queueName}/tasks/${Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)}`; + req.body.task.httpRequest.body = JSON.parse( + Buffer.from(req.body.task.httpRequest.body, "base64").toString("utf-8"), + ); + + const task = req.body.task as Task; + + try { + this.controller.enqueue(queueKey, task); + this.logger.log("DEBUG", `Enqueueing task ${task.name} onto ${queueKey}`); + res.status(200).send({ task: task }); + } catch (e) { + res.status(409).send("A task with the same name already exists"); + } + }; + + const deleteTasksRoute = `/projects/:project_id/locations/:location_id/queues/:queue_name/tasks/:task_id`; + const deleteTasksHandler: express.Handler = (req, res) => { + const projectId = req.params.project_id; + const locationId = req.params.location_id; + const queueName = req.params.queue_name; + const taskId = req.params.task_id; + const queueKey = `queue:${projectId}-${locationId}-${queueName}`; + + if (!this.controller.queues[queueKey]) { + this.logger.log("WARN", "Tried to remove a task from a non-existent queue"); + res.status(404).send("Tried to remove a task from a non-existent queue"); + return; + } + + try { + const taskName = `projects/${projectId}/locations/${locationId}/queues/${queueName}/tasks/${taskId}`; + this.logger.log("DEBUG", `removing: ${taskName}`); + this.controller.delete(queueKey, taskName); + res.status(200).send({ res: "OK" }); + } catch (e) { + this.logger.log("WARN", "Tried to remove a task that doesn't exist"); + res.status(404).send("Tried to remove a task that doesn't exist"); + } + }; + + const getStatsRoute = `/queueStats`; + const getStatsHandler: express.Handler = (req, res) => { + res.json(this.controller.getStatistics()); + }; + + hub.get([getStatsRoute], cors({ origin: true }), getStatsHandler); + hub.post([createTaskQueueRoute], express.json(), createTaskQueueHandler); + hub.post([enqueueTasksRoute], express.json(), enqueueTasksHandler); + hub.delete([deleteTasksRoute], express.json(), deleteTasksHandler); + + return hub; + } + + 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(); + } + this.controller.stop(); + } + + getInfo(): EmulatorInfo { + const host = this.args.host || Constants.getDefaultHost(); + const port = this.args.port || Constants.getDefaultPort(Emulators.TASKS); + + return { + name: this.getName(), + host, + port, + }; + } + + getName(): Emulators { + return Emulators.TASKS; + } +} 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 8584c7c7670..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,21 +37,8 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = }, }, }, - triggerId: "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: { @@ -80,21 +63,8 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = }, }, }, - triggerId: "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: { @@ -119,21 +89,8 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = }, }, }, - triggerId: "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: { @@ -170,23 +127,9 @@ export const FunctionRuntimeBundles: { [key: string]: FunctionsRuntimeBundle } = timestamp: "2019-05-15T16:21:15.148831Z", }, }, - triggerId: "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: "function_id", - projectId: "fake-project-id", + proto: {}, }, }; @@ -289,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 f27ef7246ac..af4afac2b6c 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", @@ -9,10 +8,15 @@ export enum Emulators { FIRESTORE = "firestore", DATABASE = "database", HOSTING = "hosting", + APPHOSTING = "apphosting", PUBSUB = "pubsub", UI = "ui", LOGGING = "logging", STORAGE = "storage", + EXTENSIONS = "extensions", + EVENTARC = "eventarc", + DATACONNECT = "dataconnect", + TASKS = "tasks", } export type DownloadableEmulators = @@ -20,33 +24,53 @@ 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; -export const IMPORT_EXPORT_EMULATORS = [Emulators.FIRESTORE, Emulators.DATABASE, Emulators.AUTH]; +export type ImportExportEmulators = + | Emulators.FIRESTORE + | Emulators.DATABASE + | Emulators.AUTH + | Emulators.STORAGE + | Emulators.DATACONNECT; +export const IMPORT_EXPORT_EMULATORS = [ + Emulators.FIRESTORE, + Emulators.DATABASE, + Emulators.AUTH, + Emulators.STORAGE, + Emulators.DATACONNECT, +]; export const ALL_SERVICE_EMULATORS = [ + Emulators.APPHOSTING, Emulators.AUTH, Emulators.FUNCTIONS, Emulators.FIRESTORE, Emulators.DATABASE, Emulators.HOSTING, Emulators.PUBSUB, - previews.storageemulator && Emulators.STORAGE, -].filter((v) => v) as Emulators[]; + Emulators.STORAGE, + Emulators.EVENTARC, + Emulators.DATACONNECT, + Emulators.TASKS, +].filter((v) => v); export const EMULATORS_SUPPORTED_BY_FUNCTIONS = [ Emulators.FIRESTORE, Emulators.DATABASE, Emulators.PUBSUB, Emulators.STORAGE, + Emulators.EVENTARC, + Emulators.TASKS, ]; export const EMULATORS_SUPPORTED_BY_UI = [ @@ -55,6 +79,7 @@ export const EMULATORS_SUPPORTED_BY_UI = [ Emulators.FIRESTORE, Emulators.FUNCTIONS, Emulators.STORAGE, + Emulators.EXTENSIONS, ]; export const EMULATORS_SUPPORTED_BY_USE_EMULATOR = [ @@ -62,6 +87,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? @@ -69,6 +95,7 @@ export const ALL_EMULATORS = [ Emulators.HUB, Emulators.UI, Emulators.LOGGING, + Emulators.EXTENSIONS, ...ALL_SERVICE_EMULATORS, ]; @@ -120,9 +147,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 { @@ -130,6 +166,8 @@ export interface DownloadableEmulatorCommand { args: string[]; optionalArgs: string[]; joinArgs: boolean; + shell: boolean; + port?: number; } export interface EmulatorDownloadOptions { @@ -140,6 +178,17 @@ export interface EmulatorDownloadOptions { namePrefix: string; skipChecksumAndSize?: boolean; skipCache?: boolean; + auth?: boolean; +} + +export interface EmulatorUpdateDetails { + version: string; + expectedSize: number; + expectedChecksum: string; + expectedChecksumSHA256: string; // TODO: Use this for validation within the CLI as well. + remoteUrl: string; + downloadPathRelativeToCacheDir: string; + binaryPathRelativeToCacheDir?: string; } export interface EmulatorDownloadDetails { @@ -158,6 +207,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 { @@ -166,9 +218,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 { @@ -203,9 +256,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; @@ -227,7 +280,7 @@ export class EmulatorLog { let isNotJSON = false; try { parsedLog = JSON.parse(json); - } catch (err) { + } catch (err: any) { isNotJSON = true; } @@ -251,7 +304,7 @@ export class EmulatorLog { parsedLog.type, parsedLog.text, parsedLog.data, - parsedLog.timestamp + parsedLog.timestamp, ); } @@ -263,7 +316,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 || {}; @@ -313,7 +366,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.", ); } } @@ -328,7 +381,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..5633400bc04 100644 --- a/src/emulator/ui.ts +++ b/src/emulator/ui.ts @@ -1,57 +1,113 @@ -import { EmulatorInstance, EmulatorInfo, Emulators } from "./types"; +import * as express from "express"; +import * as path from "path"; +import { Emulators, ListenSpec } from "./types"; import * as downloadableEmulators from "./downloadableEmulators"; import { EmulatorRegistry } from "./registry"; import { FirebaseError } from "../error"; +import { EmulatorLogger } from "./emulatorLogger"; import { Constants } from "./constants"; +import { AnalyticsSession, emulatorSession } from "../track"; +import { ExpressBasedEmulator } from "./ExpressBasedEmulator"; +import { ALL_EXPERIMENTS, ExperimentName, isEnabled } from "../experiments"; +import { EmulatorHub, GetEmulatorsResponse } from "./hub"; +import { mapObject } from "../functional"; +import { maybeUsePortForwarding } from "./env"; export interface EmulatorUIOptions { - port: number; - host: string; + listen: ListenSpec[]; projectId: string; - auto_download?: boolean; } -export class EmulatorUI implements EmulatorInstance { - constructor(private args: EmulatorUIOptions) {} +// Response shape for /api/config endpoint. Contains info about which emulators are running and where. +interface EmulatorConfigInfo extends GetEmulatorsResponse { + projectId: string; + experiments: string[]; + analytics?: AnalyticsSession; +} - start(): Promise { +export class EmulatorUI extends ExpressBasedEmulator { + constructor(private args: EmulatorUIOptions) { + super({ + listen: args.listen, + }); + } + + override async start(): Promise { + await super.start(); + } + + protected override async createExpressApp(): Promise { 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(), - GCLOUD_PROJECT: projectId, - [Constants.FIREBASE_EMULATOR_HUB]: EmulatorRegistry.getInfoHostString(hubInfo), - }; + const hub = EmulatorRegistry.get(Emulators.HUB); + const app = await super.createExpressApp(); + const { projectId } = this.args; + const enabledExperiments: Array = ( + Object.keys(ALL_EXPERIMENTS) as Array + ).filter((experimentName) => isEnabled(experimentName)); + const emulatorGaSession = emulatorSession(); + + await downloadableEmulators.downloadIfNecessary(Emulators.UI); + const downloadDetails = downloadableEmulators.getDownloadDetails(Emulators.UI); + const webDir = path.join(downloadDetails.unzipDir!, "client"); + + // Exposes the host and port of various emulators to facilitate accessing + // them using client SDKs. For features that involve multiple emulators or + // hard to accomplish using client SDKs, consider adding an API below + app.get( + "/api/config", + this.jsonHandler(() => { + const emulatorInfos = mapObject( + (hub! as EmulatorHub).getRunningEmulatorsMapping(), + maybeUsePortForwarding, + ); + const json: EmulatorConfigInfo = { + projectId, + experiments: enabledExperiments ?? [], + analytics: emulatorGaSession, + ...emulatorInfos, + }; + return Promise.resolve(json); + }), + ); - return downloadableEmulators.start(Emulators.UI, { auto_download }, env); + app.use(express.static(webDir)); + // Required for the router to work properly. + app.get("*", (_, res) => { + res.sendFile(path.join(webDir, "index.html")); + }); + + return app; } connect(): Promise { return Promise.resolve(); } - stop(): Promise { - return downloadableEmulators.stop(Emulators.UI); + getName(): Emulators { + return Emulators.UI; } - getInfo(): EmulatorInfo { - return { - name: this.getName(), - host: this.args.host, - port: this.args.port, - pid: downloadableEmulators.getPID(Emulators.UI), + jsonHandler(handler: (req: express.Request) => Promise): express.Handler { + return (req, res) => { + handler(req).then( + (body) => { + res.status(200).json(body); + }, + (err) => { + EmulatorLogger.forEmulator(Emulators.UI).log("ERROR", err); + res.status(500).json({ + message: err.message, + stack: err.stack, + raw: err, + }); + }, + ); }; } - - getName(): Emulators { - return 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..3afe22f2eac --- /dev/null +++ b/src/ensureApiEnabled.spec.ts @@ -0,0 +1,179 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import * as sinon from "sinon"; +import { configstore } from "./configstore"; +import { check, ensure, POLL_SETTINGS } from "./ensureApiEnabled"; + +const FAKE_PROJECT_ID = "my_project"; +const FAKE_API = "myapi.googleapis.com"; +const FAKE_CACHE: Record> = { + my_project: { "myapi.googleapis.com": true }, +}; + +describe("ensureApiEnabled", () => { + describe("check", () => { + const sandbox = sinon.createSandbox(); + let configstoreGetMock: sinon.SinonStub; + let configstoreSetMock: sinon.SinonStub; + before(() => { + nock.disableNetConnect(); + }); + + after(() => { + nock.enableNetConnect(); + }); + + beforeEach(() => { + configstoreGetMock = sandbox.stub(configstore, "get"); + configstoreSetMock = sandbox.stub(configstore, "set"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + for (const prefix of ["", "https://", "http://"]) { + it("should call the API to check if it's enabled", async () => { + configstoreGetMock.returns(undefined); + configstoreSetMock.returns(undefined); + 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; + expect(configstoreSetMock.calledWith(FAKE_PROJECT_ID, FAKE_API)); + }); + + it("should return the value from the API", async () => { + configstoreGetMock.returns(undefined); + configstoreSetMock.returns(undefined); + 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; + }); + it("should skip the API call if the enablement is saved in the cache", async () => { + configstoreGetMock.returns(FAKE_CACHE); + configstoreSetMock.returns(undefined); + + await expect(check(FAKE_PROJECT_ID, prefix + FAKE_API, "", true)).to.eventually.be.true; + }); + } + }); + + describe("ensure", () => { + const sandbox = sinon.createSandbox(); + let configstoreGetMock: sinon.SinonStub; + let configstoreSetMock: sinon.SinonStub; + 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". + + configstoreGetMock = sandbox.stub(configstore, "get"); + configstoreSetMock = sandbox.stub(configstore, "set"); + }); + + afterEach(() => { + nock.enableNetConnect(); + POLL_SETTINGS.pollInterval = originalPollInterval; + POLL_SETTINGS.pollsBeforeRetry = originalPollsBeforeRetry; + sandbox.restore(); + }); + + for (const prefix of ["", "https://", "http://"]) { + it("should verify that the API is enabled, and stop if it is", async () => { + configstoreGetMock.returns(undefined); + configstoreSetMock.returns(undefined); + 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 verify that the API is enabled (in the cache), and stop if it is", async () => { + configstoreGetMock.returns(FAKE_CACHE); + configstoreSetMock.returns(undefined); + + 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 () => { + configstoreGetMock.returns(undefined); + configstoreSetMock.returns(undefined); + 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; + expect(configstoreSetMock.calledWith(FAKE_PROJECT_ID, FAKE_API)); + }); + + it("should retry enabling the API if it does not enable in time", async () => { + configstoreGetMock.returns(undefined); + configstoreSetMock.returns(undefined); + 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 adb4c1fff98..3ddda342904 100644 --- a/src/ensureApiEnabled.ts +++ b/src/ensureApiEnabled.ts @@ -1,64 +1,111 @@ -import * as _ from "lodash"; -import { bold } from "cli-color"; +import { bold } from "colorette"; -import * as track from "./track"; -import * as api from "./api"; +import { trackGA4 } from "./track"; +import { serviceUsageOrigin } from "./api"; +import { Client } from "./apiv2"; import * as utils from "./utils"; import { FirebaseError, isBillingError } from "./error"; +import { logger } from "./logger"; +import { configstore } from "./configstore"; export const POLL_SETTINGS = { pollInterval: 10000, pollsBeforeRetry: 12, }; +const apiClient = new Client({ + 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 response = await api.request("GET", `/v1/projects/${projectId}/services/${apiName}`, { - auth: true, - origin: api.serviceUsageOrigin, + const apiName = apiUri.startsWith("http") ? new URL(apiUri).hostname : apiUri; + if (checkAPIEnablementCache(projectId, apiName)) { + return true; + } + const res = await apiClient.get<{ state: string }>(`/projects/${projectId}/services/${apiName}`, { + headers: { "x-goog-quota-user": `projects/${projectId}` }, + skipLog: { resBody: true }, }); - - const isEnabled = _.get(response.body, "state") === "ENABLED"; + const isEnabled = res.body.state === "ENABLED"; if (isEnabled && !silent) { utils.logLabeledSuccess(prefix, `required API ${bold(apiName)} is enabled`); } + if (isEnabled) { + cacheEnabledAPI(projectId, apiName); + } 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). * + * If enabling an API for a customer, prefer `ensure` which will check for the + * API first, which is a seperate permission than enabling. + * * @param projectId The project in which to enable the API. * @param apiName The name of the API e.g. `someapi.googleapis.com`. */ -export async function enable(projectId: string, apiName: string): Promise { +async function enable(projectId: string, apiName: string): Promise { try { - await api.request("POST", `/v1/projects/${projectId}/services/${apiName}:enable`, { - auth: true, - origin: api.serviceUsageOrigin, - }); - } catch (err) { + await apiClient.post( + `/projects/${projectId}/services/${apiName}:enable`, + undefined, + { + headers: { "x-goog-quota-user": `projects/${projectId}` }, + skipLog: { resBody: true }, + }, + ); + cacheEnabledAPI(projectId, apiName); + } 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; } } @@ -68,7 +115,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 @@ -80,7 +127,9 @@ async function pollCheckEnabled( }); const isEnabled = await check(projectId, apiName, prefix, silent); if (isEnabled) { - track("api_enabled", apiName); + void trackGA4("api_enabled", { + api_name: apiName, + }); return; } if (!silent) { @@ -94,11 +143,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); @@ -109,25 +158,84 @@ 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, hostname, prefix, silent); +} + +export async function bestEffortEnsure( + projectId: string, + apiUri: string, + prefix: string, + silent = false, +): Promise { + try { + await ensure(projectId, apiUri, prefix, silent); + } catch (err: any) { + logger.debug( + `Unable to check that ${apiUri} is enabled on ${projectId}. Calls to it will fail if it is not enabled`, + ); + } +} + +/** + * 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}`; +} + +/** + * To reduce serviceusage quota burn, we cache API enablement status in configstore. + * Once we see that an API is enabled, we skip future checks. This is safe, because: + * A - It's rare to disable APIs + * B - If the API actually is disabled, the user gets a clear error message with a link to enable it. + * + * We intentionally do not cache when we see an API is not enabled - some users need to have admins enable APIS, + * so we expect APIs to get enabled out of band frequently. + */ + +const API_ENABLEMENT_CACHE_KEY = "apiEnablementCache"; +function checkAPIEnablementCache(projectId: string, apiName: string): boolean { + const cache = configstore.get(API_ENABLEMENT_CACHE_KEY) as Record< + string, + Record + >; + return !!cache?.[projectId]?.[apiName]; +} + +function cacheEnabledAPI(projectId: string, apiName: string) { + const cache = (configstore.get(API_ENABLEMENT_CACHE_KEY) || {}) as Record< + string, + Record + >; + if (!cache[projectId]) { + cache[projectId] = {}; } - return enableApiWithRetries(projectId, apiName, prefix, silent); + cache[projectId][apiName] = true; + configstore.set(API_ENABLEMENT_CACHE_KEY, cache); } diff --git a/src/ensureCloudResourceLocation.ts b/src/ensureCloudResourceLocation.ts deleted file mode 100644 index 4262a56bd43..00000000000 --- a/src/ensureCloudResourceLocation.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { FirebaseError } from "./error"; - -/** - * Simple helper function that returns an error with a helpful - * message on event of cloud resource location that is not set. - * This was made into its own file because this error gets thrown - * in several places: in init for Firestore, Storage, and for scheduled - * function deployments. - * @param location cloud resource location, like "us-central1" - * @throws { FirebaseError } if location is not set - */ -export function ensureLocationSet(location: string, feature: string): void { - if (!location) { - 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` - ); - } -} diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 00000000000..637dfb7084d --- /dev/null +++ b/src/env.ts @@ -0,0 +1,13 @@ +import { dirExistsSync } from "./fsutils"; + +let googleIdxFolderExists: boolean | undefined; +export function isFirebaseStudio() { + if (googleIdxFolderExists === true || process.env.MONOSPACE_ENV) return true; + if (googleIdxFolderExists === false) return false; + googleIdxFolderExists = dirExistsSync("/google/idx"); + return googleIdxFolderExists; +} + +export function isFirebaseMcp() { + return !!process.env.IS_FIREBASE_MCP; +} 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..e85ddeaef71 100644 --- a/src/error.ts +++ b/src/error.ts @@ -33,6 +33,70 @@ export class FirebaseError extends Error { } } +/** + * Safely gets an error message from an unknown object + * @param err an unknown error type + * @param defaultMsg an optional message to return if the err is not Error or string + * @return An error string + */ +export function getErrMsg(err: unknown, defaultMsg?: string): string { + if (err instanceof Error) { + return err.message; + } else if (typeof err === "string") { + return err; + } else if (defaultMsg) { + return defaultMsg; + } + return JSON.stringify(err); +} + +/** + * Safely gets an error stack (or error message if no stack is available) + * from an unknown object + * @param err The potential error object + * @return a string representing the error stack or the error message. + */ +export function getErrStack(err: unknown): string { + if (err instanceof Error) { + return err.stack || err.message; + } + return getErrMsg(err); +} + +/** + * A typeguard for objects + * @param value The value to check + */ +export function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +/** + * Safely gets a status from an unknown object if it has one. + * @param err The error to get the status of + * @param defaultStatus a default status if there is none + * @return the err status, a default status or DEFAULT_STATUS + */ +export function getErrStatus(err: unknown, defaultStatus?: number): number { + if (isObject(err) && err.status && typeof err.status === "number") { + return err.status; + } + + return defaultStatus || DEFAULT_STATUS; +} + +/** + * Safely gets an error object from an unknown object + * @param err The error to get an Error for. + * @return an Error object + */ +export function getError(err: unknown): Error { + if (err instanceof Error) { + return err; + } + return Error(getErrMsg(err)); +} + /** * Checks if a FirebaseError is caused by attempting something * that requires billing enabled while billing is not enabled. @@ -53,7 +117,12 @@ 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" ); }); } + +/** + * Checks whether an unknown object (such as an error) has a message field + */ +export const hasMessage = (e: any): e is { message: string } => !!e?.message; diff --git a/src/errorOut.spec.ts b/src/errorOut.spec.ts new file mode 100644 index 00000000000..616c09208b9 --- /dev/null +++ b/src/errorOut.spec.ts @@ -0,0 +1,52 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { errorOut } from "./errorOut"; +import { FirebaseError } from "./error"; +import * as logError from "./logError"; + +describe("errorOut", () => { + let sandbox: sinon.SinonSandbox; + let logErrorStub: sinon.SinonStub; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + logErrorStub = sandbox.stub(logError, "logError"); + clock = sandbox.useFakeTimers(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should log a FirebaseError and exit with the correct code", () => { + const error = new FirebaseError("A Firebase error has occurred.", { exit: 123 }); + const processExitStub = sandbox.stub(process, "exit"); + errorOut(error); + expect(logErrorStub).to.have.been.calledWith(error); + expect(process.exitCode).to.equal(123); + clock.tick(251); + expect(processExitStub).to.have.been.calledOnce; + }); + + it("should wrap a standard Error in a FirebaseError and exit with code 2", () => { + const error = new Error("A standard error has occurred."); + const processExitStub = sandbox.stub(process, "exit"); + errorOut(error); + expect(logErrorStub).to.have.been.calledWith(sinon.match.instanceOf(FirebaseError)); + expect(logErrorStub.getCall(0).args[0].original).to.equal(error); + expect(process.exitCode).to.equal(2); + clock.tick(251); + expect(processExitStub).to.have.been.calledOnce; + }); + + it("should exit with code 2 if exit code is 0", () => { + const error = new FirebaseError("An error with exit code 0.", { exit: 0 }); + const processExitStub = sandbox.stub(process, "exit"); + errorOut(error); + expect(logErrorStub).to.have.been.calledWith(error); + expect(process.exitCode).to.equal(2); + clock.tick(251); + expect(processExitStub).to.have.been.calledOnce; + }); +}); 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..8c16408cb06 --- /dev/null +++ b/src/experiments.ts @@ -0,0 +1,283 @@ +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: true, + }, + 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, + }, + dangerouslyAllowFunctionsConfig: { + shortDescription: "Allows the use of deprecated functions.config() API", + fullDescription: + "The functions.config() API is deprecated and will be removed on December 31, 2025. " + + "This experiment allows continued use of the API during the migration period.", + default: true, + public: true, + }, + runfunctions: { + shortDescription: + "Functions created using the V2 API target Cloud Run Functions (not production ready)", + public: false, + }, + + // Emulator experiments + emulatoruisnapshot: { + shortDescription: "Load pre-release versions of the emulator UI", + }, + emulatorapphosting: { + shortDescription: "App Hosting emulator", + public: false, + }, + + // 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, + }, + appsinit: { + shortDescription: "Adds experimental `apps:init` command.", + fullDescription: + "Adds experimental `apps:init` command. When run from an app directory, this command detects the app's platform and configures required files.", + default: false, + public: true, + }, + mcp: { + shortDescription: "Adds experimental `firebase mcp` command for running a Firebase MCP server.", + default: true, + public: false, + }, + mcpalpha: { + shortDescription: "Opt-in to early MCP features before they're widely released.", + default: false, + public: true, + }, + apptesting: { + shortDescription: "Adds experimental App Testing feature", + public: true, + }, + ailogic: { + shortDescription: "Enable Firebase AI Logic feature for existing apps", + fullDescription: + "Enables the AI Logic initialization feature that provisions AI Logic for existing Firebase apps.", + public: true, + default: 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 93a093b371d..00000000000 --- a/src/extensions/askUserForConsent.ts +++ /dev/null @@ -1,103 +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 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 and corresponding descriptions and asks user for consent. - * @param extensionName name of extension to install/update - * @param projectId ID of user's project - * @param roles roles that require user approval - * @return {Promise} returns promise - */ -export async function prompt(extensionName: string, projectId: string, roles: string[]) { - if (!roles || !roles.length) { - return; - } - - const message = await formatDescription(extensionName, projectId, roles); - utils.logLabeledBullet(logPrefix, message); - const question: Question = { - name: "consent", - type: "confirm", - message: "Would you like to continue?", - default: true, - }; - const consented = await promptOnce(question); - if (!consented) { - throw new FirebaseError( - "Without explicit consent for the roles listed, we cannot deploy this extension." - ); - } -} - -/** - * 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..22d07044f1e --- /dev/null +++ b/src/extensions/askUserForEventsConfig.spec.ts @@ -0,0 +1,86 @@ +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 checkboxStub: sinon.SinonStub; + + beforeEach(() => { + checkboxStub = sinon.stub(prompt, "checkbox"); + }); + + afterEach(() => { + checkboxStub.restore(); + }); + + it("should keep prompting user until valid input is given", async () => { + checkboxStub.onCall(0).resolves(["invalid"]); + checkboxStub.onCall(1).resolves(["stillinvalid"]); + checkboxStub.onCall(2).resolves(["google.firebase.custom-event-occurred"]); + await askForAllowedEventTypes([ + { type: "google.firebase.custom-event-occurred", description: "A custom event occurred" }, + ]); + expect(checkboxStub).to.be.calledThrice; + }); +}); + +describe("askForEventarcLocation", () => { + let selectStub: sinon.SinonStub; + + beforeEach(() => { + selectStub = sinon.stub(prompt, "select"); + }); + + afterEach(() => { + selectStub.restore(); + }); + + it("should keep prompting user until valid input is given", async () => { + selectStub.onCall(0).returns("invalid-region"); + selectStub.onCall(1).returns("still-invalid-region"); + selectStub.onCall(2).returns("us-central1"); + await askForEventArcLocation(); + expect(selectStub).to.be.calledThrice; + }); +}); diff --git a/src/extensions/askUserForEventsConfig.ts b/src/extensions/askUserForEventsConfig.ts new file mode 100644 index 00000000000..6a581173c3d --- /dev/null +++ b/src/extensions/askUserForEventsConfig.ts @@ -0,0 +1,159 @@ +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"; +import { checkbox, select, confirm } from "../prompt"; + +export interface InstanceEventsConfig { + channel: string; + allowedEventTypes: string[]; +} + +/** + * Validates the user's selected events against the list of valid events + * @param response The user's selected events + * @param validEvents The list of valid events + * @return True if the response is valid + */ +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; +} + +/** + * Asks the user if events should be enabled, and if yes, for the EventArc + * channel and also the events to enable + * @param events The list of possible events + * @param projectId The projectId for the EventArc channel + * @param instanceId The instanceId to get predefined events and location from + * @return The instance events config or undefined if the user doesn't want events + */ +export async function askForEventsConfig( + events: EventDescriptor[], + projectId: string, + instanceId: string, +): Promise { + logger.info( + `\n${clc.bold("Enable Events")}: ${await 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 = getEventArcChannel(projectId, location); + const allowedEventTypes = await askForAllowedEventTypes(events, preselectedTypes); + return { channel, allowedEventTypes }; +} + +/** + * Creates an EventArc channel resource name + * @param projectId The projectId for the channel + * @param location The location for the channel + * @return The resource name for the EventArc channel + */ +export function getEventArcChannel(projectId: string, location: string): string { + return `projects/${projectId}/locations/${location}/channels/firebase`; +} + +/** + * Asks the user which event types they would like to enable + * @param eventDescriptors The list of possible events + * @param preselectedTypes The list of preselected events + * @return A list of strings indicating the event types + */ +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 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 !== ""); +} + +/** + * Asks the user if they want to enable events + * @return A boolean indicating if they want to enable events + */ +export function askShouldCollectEventsConfig(): Promise { + return confirm("Would you like to enable events?"); +} + +export const ALLOWED_EVENT_ARC_REGIONS = [ + "us-central1", + "us-west1", + "europe-west4", + "asia-northeast1", +]; +export type ExtensionsEventArcRegions = (typeof ALLOWED_EVENT_ARC_REGIONS)[number]; +export const EXTENSIONS_DEFAULT_EVENT_ARC_REGION: ExtensionsEventArcRegions = "us-central1"; + +/** + * Asks the user to select an EventArc location + * @param preselectedLocation (Optional) A preselected option + * @return A string representing the EventArc location. + */ +export async function askForEventArcLocation(preselectedLocation?: string): Promise { + let valid = false; + let location = ""; + while (!valid) { + location = await select({ + default: preselectedLocation ?? EXTENSIONS_DEFAULT_EVENT_ARC_REGION, + 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: ALLOWED_EVENT_ARC_REGIONS, + }); + valid = ALLOWED_EVENT_ARC_REGIONS.includes(location); + if (!valid) { + utils.logWarning( + `Unexpected EventArc region '${location}' was specified. Allowed regions: ${ALLOWED_EVENT_ARC_REGIONS.join( + ", ", + )}`, + ); + } + } + return location; +} diff --git a/src/extensions/askUserForParam.spec.ts b/src/extensions/askUserForParam.spec.ts new file mode 100644 index 00000000000..a05af162a9d --- /dev/null +++ b/src/extensions/askUserForParam.spec.ts @@ -0,0 +1,402 @@ +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 inputStub: sinon.SinonStub; + + beforeEach(() => { + inputStub = sinon.stub(prompt, "input"); + inputStub.onCall(0).returns("Invalid123"); + inputStub.onCall(1).returns("InvalidStill123"); + inputStub.onCall(2).returns("ValidName"); + }); + + afterEach(() => { + inputStub.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(inputStub).to.be.calledThrice; + }); + }); + + 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 checkboxStub: sinon.SinonStub; + let inputStub: sinon.SinonStub; + let confirmStub: sinon.SinonStub; + let passwordStub: sinon.SinonStub; + let createSecret: sinon.SinonStub; + let secretExists: sinon.SinonStub; + let addVersion: sinon.SinonStub; + let grantRole: sinon.SinonStub; + + beforeEach(() => { + checkboxStub = sinon.stub(prompt, "checkbox"); + inputStub = sinon.stub(prompt, "input"); + confirmStub = sinon.stub(prompt, "confirm"); + passwordStub = sinon.stub(prompt, "password"); + 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(() => { + checkboxStub.restore(); + inputStub.restore(); + confirmStub.restore(); + passwordStub.restore(); + secretExists.restore(); + createSecret.restore(); + addVersion.restore(); + grantRole.restore(); + }); + + it("should return the correct user input for secret stored with Secret Manager", async () => { + confirmStub.onFirstCall().resolves(true); + checkboxStub.onFirstCall().resolves([SecretLocation.CLOUD.toString()]); + passwordStub.onFirstCall().resolves("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(checkboxStub).to.be.calledOnce; + expect(passwordStub).to.be.calledOnce; + expect(grantRole).to.be.calledOnce; + 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 () => { + confirmStub.onFirstCall().resolves(true); + checkboxStub.onFirstCall().resolves([SecretLocation.LOCAL.toString()]); + inputStub.onFirstCall().resolves("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(checkboxStub).to.be.called.calledOnce; + expect(inputStub).to.be.calledOnce; + expect(passwordStub).to.not.have.been.called; + // Shouldn't make any api calls. + expect(grantRole).to.not.have.been.called; + expect(result).to.be.eql({ + baseValue: "", + local: "ABC.123", + }); + }); + + it("should handle cloud & local secret storage at the same time", async () => { + checkboxStub + .onFirstCall() + .returns([SecretLocation.CLOUD.toString(), SecretLocation.LOCAL.toString()]); + inputStub.onFirstCall().returns("local"); + passwordStub.onFirstCall().returns("ABC.123"); + inputStub.onFirstCall().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(checkboxStub).to.have.been.calledOnce; + expect(passwordStub).to.have.been.calledOnce; + expect(inputStub).to.have.been.calledOnce; + expect(grantRole).to.have.been.calledOnce; + 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, "input").resolves("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 57ad0ba9772..288c8442dd3 100644 --- a/src/extensions/askUserForParam.ts +++ b/src/extensions/askUserForParam.ts @@ -1,19 +1,45 @@ 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"; -import { convertExtensionOptionToLabeledList, onceWithJoin } from "./utils"; +import { convertExtensionOptionToLabeledList, getRandomString } from "./utils"; import { logger } from "../logger"; -import { promptOnce } from "../prompt"; +import { input, password, confirm, select, checkbox } 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 = 1, + SET_NEW, +} + +/** + * Validates the user's response for param value against the param spec + * @param response The user's response + * @param spec The param spec + * @return True if the user's response is valid + */ export function checkResponse(response: string, spec: Param): boolean { let valid = true; let responses: string[]; - if (spec.required && !response) { + if (spec.required && (response === "" || response === undefined)) { utils.logWarning(`Param ${spec.param} is required, but no value was provided.`); return false; } @@ -27,7 +53,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` + @@ -35,119 +61,358 @@ 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(paramSpec: Param): Promise { +/** + * Prompt users for params based on paramSpecs defined by the extension developer. + * @param args.projectId The projectId for the params + * @param args.instanceId The instanceId for the params + * @param args.paramSpecs Array of params to ask the user about, parsed from extension.yaml. + * @param args.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 confirm( + "Do you want to configure any advanced parameters for this instance?", + ); + 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; +} + +/** + * Asks the user for values for the extension parameter. + * @param args.projectId The projectId we are installing into + * @param args.instanceId The instanceId we are creating/updating/configuring + * @param args.paramSpec The spec for the param we are asking about + * @param args.reconfiguring If true we will reconfigure a secret + * @return ParamBindingOptions to specify the selected value(s) for the parameter. + */ +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()}` + `\n${clc.bold(label)}${clc.bold(paramSpec.required ? "" : " (Optional)")}: ${( + await marked(description) + ).trim()}`, ); while (!valid) { switch (paramSpec.type) { case ParamType.SELECT: - response = await promptOnce({ - name: "input", - type: "list", - default: () => { - if (paramSpec.default) { - return getInquirerDefault(_.get(paramSpec, "options", []), paramSpec.default); - } - }, + response = await select({ + default: paramSpec.default + ? getInquirerDefault(_.get(paramSpec, "options", []), paramSpec.default) + : undefined, message: "Which option do you want enabled for this parameter? " + "Select an option with the arrow keys, and use Enter to confirm your choice. " + "You may only select one option.", choices: convertExtensionOptionToLabeledList(paramSpec.options as ParamOption[]), }); + valid = checkResponse(response, paramSpec); break; case ParamType.MULTISELECT: - response = await onceWithJoin({ - name: "input", - type: "checkbox", - default: () => { - if (paramSpec.default) { - const defaults = paramSpec.default.split(","); - return defaults.map((def) => { - return getInquirerDefault(_.get(paramSpec, "options", []), def); - }); - } - }, - 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.", - choices: convertExtensionOptionToLabeledList(paramSpec.options as ParamOption[]), - }); + response = ( + await checkbox({ + default: paramSpec.default + ? paramSpec.default.split(",").map((def) => { + return getInquirerDefault(_.get(paramSpec, "options", []), def); + }) + : undefined, + message: + "Which options do you want enabled for this parameter? " + + "Press Space to select, then Enter to confirm your choices. ", + choices: convertExtensionOptionToLabeledList(paramSpec.options as ParamOption[]), + }) + ).join(","); + valid = checkResponse(response, paramSpec); + break; + case ParamType.SECRET: + 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: // Default to ParamType.STRING - response = await promptOnce({ - name: paramSpec.param, - type: "input", + response = await input({ default: paramSpec.default, message: `Enter a value for ${label}:`, }); + valid = checkResponse(response, paramSpec); } + } + return { baseValue: response, ...(responseForLocal ? { local: responseForLocal } : {}) }; +} - valid = checkResponse(response, paramSpec); +function isValidSecretLocations(secretLocations: string[], paramSpec: Param): boolean { + if (paramSpec.required) { + return !!secretLocations.length; } - return response; + return true; } -export function getInquirerDefault(options: ParamOption[], def: string): string { - const defaultOption = _.find(options, (option) => { - return option.value === def; +async function promptSecretLocations(paramSpec: Param): Promise { + if (paramSpec.required) { + return await 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 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(), + }, + ], }); - return defaultOption ? defaultOption.label || defaultOption.value : ""; +} + +async function promptLocalSecret(instanceId: string, paramSpec: Param): Promise { + let value; + do { + utils.logLabeledBullet(logPrefix, "Configure a local secret value for Extensions Emulator"); + value = await input( + `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, +): Promise { + const action = await select({ + message: `Choose what you would like to do with this secret:`, + choices: [ + { name: "Leave unchanged", value: SecretUpdateAction.LEAVE }, + { name: "Set new value", value: SecretUpdateAction.SET_NEW }, + ], + }); + switch (action) { + case SecretUpdateAction.SET_NEW: { + let secret; + let secretName; + if (paramSpec.default) { + secret = secretManagerApi.parseSecretResourceName(paramSpec.default); + secretName = secret.name; + } else { + secretName = await generateSecretName(projectId, instanceId, paramSpec.param); + } + const secretValue = await password( + `This secret will be stored in Cloud Secret Manager as ${secretName}.\nEnter new value for ${paramSpec.label.trim()}:`, + ); + if (secretValue === "" && paramSpec.required) { + logger.info(`Secret value cannot be empty for required param ${paramSpec.param}`); + return promptReconfigureSecret(projectId, instanceId, paramSpec); + } else if (secretValue !== "") { + if (checkResponse(secretValue, paramSpec)) { + if (!secret) { + secret = await secretManagerApi.createSecret( + projectId, + secretName, + secretsUtils.getSecretLabels(instanceId), + ); + } + return addNewSecretVersion(projectId, instanceId, secret, paramSpec, secretValue); + } else { + return promptReconfigureSecret(projectId, instanceId, paramSpec); + } + } else { + return ""; + } + } + case SecretUpdateAction.LEAVE: + default: + return paramSpec.default || ""; + } } /** - * 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. + * Prompts the user to create a secret + * @param projectId The projectId to create the secret in + * @param instanceId The instanceId for the secret + * @param paramSpec The secret param spec + * @param secretName (Optional) The name to store the secret as + * @return The resource name of a new secret version or empty string if no secret is created. */ -export async function ask( - paramSpecs: Param[], - firebaseProjectParams: { [key: string]: string } -): Promise<{ [key: string]: string }> { - if (_.isEmpty(paramSpecs)) { - logger.debug("No params were specified for this extension."); - return {}; +export async function promptCreateSecret( + projectId: string, + instanceId: string, + paramSpec: Param, + secretName?: string, +): Promise { + const name = secretName ?? (await generateSecretName(projectId, instanceId, paramSpec.param)); + // N.B. Is it actually possible to have a default value for a password?! + const secretValue = + (await password({ + message: `This secret will be stored in Cloud Secret Manager (https://cloud.google.com/secret-manager/pricing) as ${name} and managed by Firebase Extensions (Firebase Extensions Service Agent will be granted Secret Admin role on this secret).\nEnter a value for ${paramSpec.label.trim()}:`, + })) || + paramSpec.default || + ""; + if (secretValue === "" && paramSpec.required) { + logger.info(`Secret value cannot be empty for required param ${paramSpec.param}`); + return promptCreateSecret(projectId, instanceId, paramSpec, name); + } else if (secretValue !== "") { + if (checkResponse(secretValue, paramSpec)) { + const secret = await secretManagerApi.createSecret( + projectId, + name, + secretsUtils.getSecretLabels(instanceId), + ); + return addNewSecretVersion(projectId, instanceId, secret, paramSpec, secretValue); + } else { + return promptCreateSecret(projectId, instanceId, paramSpec, name); + } + } else { + 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(paramSpec); - }; - }); - // 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; +async function generateSecretName( + projectId: string, + instanceId: string, + paramName: string, +): Promise { + let secretName = `ext-${instanceId}-${paramName}`; + while (await secretManagerApi.secretExists(projectId, secretName)) { + secretName += `-${getRandomString(3)}`; + } + return secretName; +} + +async function addNewSecretVersion( + projectId: string, + instanceId: string, + secret: secretManagerApi.Secret, + paramSpec: Param, + secretValue: string, +): Promise { + 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}`; +} + +/** + * Finds the label or value of a default option if the option is found in options + * @param options The param options to search for default + * @param def The value of the default to search for + * @return The label or value of the default if present or empty string if not. + */ +export function getInquirerDefault(options: ParamOption[], def: string): string { + const defaultOption = options.find((o) => o.value === def); + return defaultOption ? defaultOption.label || defaultOption.value : ""; } diff --git a/src/extensions/billingMigrationHelper.ts b/src/extensions/billingMigrationHelper.ts deleted file mode 100644 index b48c97f307f..00000000000 --- a/src/extensions/billingMigrationHelper.ts +++ /dev/null @@ -1,97 +0,0 @@ -import * as marked from "marked"; -import TerminalRenderer = require("marked-terminal"); - -import { FirebaseError } from "../error"; -import * as extensionsApi from "./extensionsApi"; -import { logPrefix } from "./extensionsHelper"; -import { promptOnce } from "../prompt"; -import * as utils from "../utils"; - -marked.setOptions({ - renderer: new TerminalRenderer(), -}); - -const urlPricingExamples = "https://cloud.google.com/functions/pricing#pricing_examples"; -const urlFAQ = "https://firebase.google.com/support/faq/#extensions-pricing"; - -const billingMsgUpdate = - "This update includes an upgrade to Node.js 10 from Node.js 8, which is no" + - " longer maintained. Starting with this update, you will be charged a" + - " small amount (typically around $0.01/month) for the Firebase resources" + - " required by this extension (even if it is not used), in addition to any" + - " charges associated with its usage.\n\n" + - `See pricing examples: **[${urlPricingExamples}](${urlPricingExamples})**\n` + - `See the FAQ: **[${urlFAQ}](${urlFAQ})**\n`; -const billingMsgCreate = - "You will be charged around $0.01/month for the Firebase resources" + - " required by this extension (even if it is not used). Additionally," + - " using this extension will contribute to your project's overall usage" + - " level of Firebase services. However, you'll only be charged for usage" + - " that exceeds Firebase's free tier for those services.\n\n" + - `See pricing examples: **[${urlPricingExamples}](${urlPricingExamples})**\n` + - `See the FAQ: **[${urlFAQ}](${urlFAQ})**\n`; - -const defaultSpecVersion = "v1beta"; -const defaultRuntimes: { [key: string]: string } = { - v1beta: "nodejs8", -}; - -function hasRuntime(spec: extensionsApi.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)); -} - -/** - * Displays billing changes if the update contains new billing requirements. - * - * @param curSpec A current extensionSpec - * @param newSpec A extensionSpec to compare to - * @param prompt If true, prompts user for confirmation - */ -export async function displayNode10UpdateBillingNotice( - curSpec: extensionsApi.ExtensionSpec, - newSpec: extensionsApi.ExtensionSpec, - prompt: boolean -): Promise { - if (hasRuntime(curSpec, "nodejs8") && hasRuntime(newSpec, "nodejs10")) { - utils.logLabeledWarning(logPrefix, marked(billingMsgUpdate)); - - if (prompt) { - const continueUpdate = await promptOnce({ - type: "confirm", - message: "Do you wish to continue?", - default: true, - }); - if (!continueUpdate) { - throw new FirebaseError(`Cancelled.`, { exit: 2 }); - } - } - } -} - -/** - * Displays billing changes if the extension contains new billing requirements. - * - * @param spec A currenta extensionSpec - * @param prompt If true, prompts user for confirmation - */ -export async function displayNode10CreateBillingNotice( - spec: extensionsApi.ExtensionSpec, - prompt: boolean -): Promise { - if (hasRuntime(spec, "nodejs10")) { - utils.logLabeledWarning(logPrefix, marked(billingMsgCreate)); - if (prompt) { - const continueUpdate = await promptOnce({ - type: "confirm", - message: "Do you wish to continue?", - default: true, - }); - if (!continueUpdate) { - throw new FirebaseError(`Cancelled.`, { exit: 2 }); - } - } - } -} 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..2eaad4146aa --- /dev/null +++ b/src/extensions/change-log.ts @@ -0,0 +1,94 @@ +import { marked } from "marked"; +import * as path from "path"; +import * as semver from "semver"; +import { markedTerminal } from "marked-terminal"; + +import { listExtensionVersions } from "./extensionsApi"; +import { readFile } from "./localHelper"; +import * as refs from "./refs"; + +marked.use(markedTerminal() as any); + +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; +} + +/** + * 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/checkProjectBilling.js b/src/extensions/checkProjectBilling.js deleted file mode 100644 index 38d9030c09c..00000000000 --- a/src/extensions/checkProjectBilling.js +++ /dev/null @@ -1,155 +0,0 @@ -"use strict"; - -const _ = require("lodash"); -const clc = require("cli-color"); -const opn = require("open"); - -const cloudbilling = require("../gcp/cloudbilling"); -const { FirebaseError } = require("../error"); -const { logger } = require("../logger"); -const extensionsHelper = require("./extensionsHelper"); -const prompt = require("../prompt"); -const utils = require("../utils"); - -/** - * Logs to console if setting up billing was successful. - * @param {boolean} enabled - * @param {string} projectId - */ -function _logBillingStatus(enabled, projectId) { - return enabled - ? utils.logLabeledSuccess( - extensionsHelper.logPrefix, - `${clc.bold(projectId)} has successfully been upgraded.` - ) - : Promise.reject( - new FirebaseError( - `${extensionsHelper.logPrefix}: ${clc.bold( - projectId - )} could not be upgraded. Please add a billing account via the Firebase console before proceeding.` - ) - ); -} - -/** - * Opens URL if applicable and stalls until user responds. - * @param {string} projectId - * @param {string} url - * @param {boolean} open - * @return {Promise} - */ -function _openBillingAccount(projectId, url, open) { - if (open) { - opn(url).catch((err) => { - logger.debug("Unable to open billing URL: " + err.stack); - }); - } - - return prompt - .promptOnce({ - name: "continue", - type: "confirm", - message: - "Press enter when finished upgrading your project to continue setting up your extension.", - default: true, - }) - .then(() => { - return cloudbilling.checkBillingEnabled(projectId); - }); -} - -/** - * Question prompts user to select billing account for project. - * @param {string} projectId - * @param {string} extensionName - * @param {Object} accounts - * @return {Promise} - */ -function _chooseBillingAccount(projectId, extensionName, accounts) { - const choices = _.map(accounts, "displayName"); - choices.push("Add new billing account"); - - return prompt - .promptOnce({ - name: "billing", - type: "list", - message: `The extension ${clc.underline( - extensionName - )} requires your project to be upgraded to the Blaze plan. You have access to the following billing accounts. - Please select the one that you would like to associate with this project:`, - choices: choices, - }) - .then((answer) => { - if (answer === "Add new billing account") { - const billingURL = `https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`; - return _openBillingAccount(projectId, billingURL, true); - } else { - const billingAccount = _.find(accounts, ["displayName", answer]); - return cloudbilling.setBillingAccount(projectId, billingAccount.name); - } - }) - .then((enabled) => { - return _logBillingStatus(enabled, projectId); - }); -} - -/** - * Directs user to set up billing account over the web and stalls until - * user responds. - * @param {string} projectId - * @param {string} extensionName - * @return {Promise} - */ -function _setUpBillingAccount(projectId, extensionName) { - const billingURL = `https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`; - - logger.info(); - logger.info( - `The extension ${clc.bold( - extensionName - )} requires 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(); - - return prompt - .promptOnce({ - name: "open-url", - type: "confirm", - message: "Press enter to open the URL.", - default: true, - }) - .then((open) => { - return _openBillingAccount(projectId, billingURL, open); - }) - .then((enabled) => { - return _logBillingStatus(enabled, projectId); - }); -} - -/** - * Checks whether billing is enabled on the given project. - * @param {string} projectId - * @returns {Promise} True if billing is enabled - */ -export function isBillingEnabled(projectId) { - return cloudbilling.checkBillingEnabled(projectId); -} - -/** - * Sets up billing for the given project. - * @param {string} projectId - * @param {string} extensionName - * @return {Promise} - */ -export function enableBilling(projectId, extensionName) { - return cloudbilling.listBillingAccounts().then((billingAccounts) => { - if (billingAccounts) { - const accounts = _.filter(billingAccounts, ["open", true]); - return accounts.length > 0 - ? _chooseBillingAccount(projectId, extensionName, accounts) - : _setUpBillingAccount(projectId, extensionName); - } - }); -} diff --git a/src/extensions/checkProjectBilling.spec.ts b/src/extensions/checkProjectBilling.spec.ts new file mode 100644 index 00000000000..b1bfdfe53f1 --- /dev/null +++ b/src/extensions/checkProjectBilling.spec.ts @@ -0,0 +1,97 @@ +import * as chai from "chai"; +chai.use(require("chai-as-promised")); +import * as sinon from "sinon"; + +import * as checkProjectBilling from "./checkProjectBilling"; +import * as prompt from "../prompt"; +import * as cloudbilling from "../gcp/cloudbilling"; + +const expect = chai.expect; + +describe("checkProjectBilling", () => { + let confirmStub: sinon.SinonStub; + let selectStub: sinon.SinonStub; + let checkBillingEnabledStub: sinon.SinonStub; + let listBillingAccountsStub: sinon.SinonStub; + let setBillingAccountStub: sinon.SinonStub; + + beforeEach(() => { + confirmStub = sinon.stub(prompt, "confirm"); + selectStub = sinon.stub(prompt, "select"); + + checkBillingEnabledStub = sinon.stub(cloudbilling, "checkBillingEnabled"); + checkBillingEnabledStub.resolves(); + + listBillingAccountsStub = sinon.stub(cloudbilling, "listBillingAccounts"); + listBillingAccountsStub.resolves(); + + setBillingAccountStub = sinon.stub(cloudbilling, "setBillingAccount"); + setBillingAccountStub.resolves(); + }); + + afterEach(() => { + confirmStub.restore(); + selectStub.restore(); + checkBillingEnabledStub.restore(); + listBillingAccountsStub.restore(); + setBillingAccountStub.restore(); + }); + + it("should resolve if billing enabled", async () => { + const projectId = "already enabled"; + + checkBillingEnabledStub.resolves(true); + + const enabled = await cloudbilling.checkBillingEnabled(projectId); + if (!enabled) { + await checkProjectBilling.enableBilling(projectId); + } + + expect(listBillingAccountsStub.notCalled); + expect(setBillingAccountStub.notCalled); + expect(confirmStub.notCalled); + expect(selectStub.notCalled); + }); + + it("should list accounts if no billing account set, but accounts available.", async () => { + const projectId = "not set, but have list"; + const accounts = [ + { + name: "test-cloud-billing-account-name", + open: true, + displayName: "test-account", + }, + ]; + + checkBillingEnabledStub.resolves(false); + listBillingAccountsStub.resolves(accounts); + setBillingAccountStub.resolves(true); + selectStub.resolves("test-account"); + + const enabled = await cloudbilling.checkBillingEnabled(projectId); + if (!enabled) { + await checkProjectBilling.enableBilling(projectId); + } + + expect(listBillingAccountsStub.calledOnce); + expect(setBillingAccountStub.calledOnce); + expect(setBillingAccountStub.calledWith(projectId, "test-cloud-billing-account-name")); + }); + + it("should not list accounts if no billing accounts set or available.", async () => { + const projectId = "not set, not available"; + + checkBillingEnabledStub.onCall(0).resolves(false); + checkBillingEnabledStub.onCall(1).resolves(true); + listBillingAccountsStub.resolves([]); + + const enabled = await cloudbilling.checkBillingEnabled(projectId); + if (!enabled) { + await checkProjectBilling.enableBilling(projectId); + } + + expect(listBillingAccountsStub.calledOnce); + expect(setBillingAccountStub.notCalled); + expect(checkBillingEnabledStub.callCount).to.equal(2); + }); +}); diff --git a/src/extensions/checkProjectBilling.ts b/src/extensions/checkProjectBilling.ts new file mode 100644 index 00000000000..dd9e1cf1df4 --- /dev/null +++ b/src/extensions/checkProjectBilling.ts @@ -0,0 +1,109 @@ +import * as clc from "colorette"; +import * as opn from "open"; + +import * as cloudbilling from "../gcp/cloudbilling"; +import { FirebaseError } from "../error"; +import { logger } from "../logger"; +import { logPrefix } from "./extensionsHelper"; +import * as prompt from "../prompt"; +import * as utils from "../utils"; + +const ADD_BILLING_ACCOUNT = "Add new billing account"; +/** + * Logs to console if setting up billing was successful. + */ +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.`, + ); + } + utils.logLabeledSuccess(logPrefix, `${clc.bold(projectId)} has successfully been upgraded.`); +} + +/** + * Opens URL if applicable and stalls until user responds. + */ +async function openBillingAccount(projectId: string, url: string, open: boolean): Promise { + if (open) { + try { + opn(url); + } catch (err: any) { + logger.debug("Unable to open billing URL: " + err.stack); + } + } + + await prompt.confirm({ + message: + "Press enter when finished upgrading your project to continue setting up your extension.", + default: true, + }); + return cloudbilling.checkBillingEnabled(projectId); +} + +/** + * Question prompts user to select billing account for project. + */ +async function chooseBillingAccount( + projectId: string, + accounts: cloudbilling.BillingAccount[], +): Promise { + const choices = accounts.map((m) => m.displayName); + choices.push(ADD_BILLING_ACCOUNT); + + const answer = await prompt.select({ + message: `Extensions require your project to be upgraded to the Blaze plan. You have access to the following billing accounts. +Please select the one that you would like to associate with this project:`, + choices: choices, + }); + + let billingEnabled: boolean; + if (answer === ADD_BILLING_ACCOUNT) { + 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); + billingEnabled = await cloudbilling.setBillingAccount(projectId, billingAccount!.name); + } + + return logBillingStatus(billingEnabled, projectId); +} + +/** + * Directs user to set up billing account over the web and stalls until + * user responds. + */ +async function setUpBillingAccount(projectId: string) { + const billingURL = `https://console.cloud.google.com/billing/linkedaccount?project=${projectId}`; + + 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:`, + ); + logger.info(); + logger.info(clc.bold(clc.underline(billingURL))); + logger.info(); + + const open = await prompt.confirm({ + message: "Press enter to open the URL.", + default: true, + }); + const billingEnabled = await openBillingAccount(projectId, billingURL, open); + return logBillingStatus(billingEnabled, projectId); +} + +/** + * Sets up billing for the given project. + * @param {string} projectId + */ +export async function enableBilling(projectId: string): Promise { + const billingAccounts = await cloudbilling.listBillingAccounts(); + if (billingAccounts) { + const accounts = billingAccounts.filter((account) => account.open); + return accounts.length > 0 + ? chooseBillingAccount(projectId, accounts) + : setUpBillingAccount(projectId); + } +} diff --git a/src/extensions/diagnose.spec.ts b/src/extensions/diagnose.spec.ts new file mode 100644 index 00000000000..77892d62c66 --- /dev/null +++ b/src/extensions/diagnose.spec.ts @@ -0,0 +1,94 @@ +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 confirmStub: 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"); + confirmStub = sinon.stub(prompt, "confirm").throws("unexpected call to prompt.confirm"); + 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], + }); + confirmStub.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: [], + }); + confirmStub.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(); + confirmStub.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..f4845aa90c2 --- /dev/null +++ b/src/extensions/diagnose.ts @@ -0,0 +1,72 @@ +import { logPrefix } from "./extensionsHelper"; +import { getProjectNumber } from "../getProjectNumber"; +import * as utils from "../utils"; +import * as resourceManager from "../gcp/resourceManager"; +import { confirm } from "../prompt"; +import { listInstances } from "./extensionsApi"; +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 confirm( + "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 cecf0c0b358..141a80d6598 100644 --- a/src/extensions/displayExtensionInfo.ts +++ b/src/extensions/displayExtensionInfo.ts @@ -1,234 +1,240 @@ -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 * as semver from "semver"; +import * as path from "path"; -import * as extensionsApi from "./extensionsApi"; -import * as utils from "../utils"; -import { logPrefix } from "./extensionsHelper"; +import * as refs from "../extensions/refs"; import { logger } from "../logger"; -import { FirebaseError } from "../error"; -import { promptOnce } from "../prompt"; - -marked.setOptions({ - renderer: new TerminalRenderer(), -}); - -const additionColor = clc.green; -const deletionColor = clc.red; - -/** - * displayExtInfo prints the extension info displayed when running ext:install. - * - * @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, - spec: extensionsApi.ExtensionSpec, - published = false -): string[] { - const lines = []; - lines.push(`**Name**: ${spec.displayName}`); - const url = spec.author?.url; - const urlMarkdown = url ? `(**[${url}](${url})**)` : ""; - lines.push(`**Author**: ${spec.author?.authorName} ${urlMarkdown}`); - if (spec.description) { - lines.push(`**Description**: ${spec.description}`); - } - if (published) { - if (spec.license) { - lines.push(`**License**: ${spec.license}`); - } - lines.push(`**Source code**: ${spec.sourceUrl}`); - } - 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, - }, - } - ); - } -} +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"; + +const TASKS_ROLE = "cloudtasks.enqueuer"; +const TASKS_API = "cloudtasks.googleapis.com"; /** - * 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 + * Displays info about an extension version, whether it is uploaded to the registry or a local spec. + * @param args.spec the extension spec + * @param args.extensionVersion (Optional) the extension version + * @param args.latestApprovedVersion (Optional) The latest approved version + * @param args.latestVersion (Optional) The latest version + * @return A string containing extension version info ready to display */ -export function displayUpdateChangesNoInput( - spec: extensionsApi.ExtensionSpec, - newSpec: extensionsApi.ExtensionSpec, - published = false -): string[] { +export async function displayExtensionVersionInfo(args: { + spec: ExtensionSpec; + extensionVersion?: ExtensionVersion; + latestApprovedVersion?: string; + latestVersion?: string; +}): Promise { + const { spec, extensionVersion, latestApprovedVersion, latestVersion } = args; const lines: string[] = []; - if (spec.displayName !== newSpec.displayName) { - lines.push( - "", - "**Name:**", - deletionColor(`- ${spec.displayName}`), - additionColor(`+ ${newSpec.displayName}`) - ); + 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(`${clc.bold("Description:")} ${spec.description}`); } - - if (spec.author?.authorName !== newSpec.author?.authorName) { - lines.push( - "", - "**Author:**", - deletionColor(`- ${spec.author?.authorName}`), - additionColor(`+ ${spec.author?.authorName}`) - ); + let versionNote = ""; + const latestRelevantVersion = latestApprovedVersion || latestVersion; + if (latestRelevantVersion && semver.eq(spec.version, latestRelevantVersion)) { + versionNote = `- ${clc.green("Latest")}`; } - - if (spec.description !== newSpec.description) { - lines.push( - "", - "**Description:**", - deletionColor(`- ${spec.description}`), - additionColor(`+ ${newSpec.description}`) - ); + if (extensionVersion?.state === "DEPRECATED") { + versionNote = `- ${clc.red("Deprecated")}`; } - - if (published) { - if (spec.sourceUrl !== newSpec.sourceUrl) { + 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.toString()}`); + } else { lines.push( - "", - "**Source code:**", - deletionColor(`- ${spec.sourceUrl}`), - additionColor(`+ ${newSpec.sourceUrl}`) + `${clc.bold("Source download URI:")} ${extensionVersion.sourceDownloadUri ?? "-"}`, ); } } - - if (spec.billingRequired && !newSpec.billingRequired) { - lines.push("", "**Billing is no longer required for this extension.**"); + lines.push(`${clc.bold("License:")} ${spec.license ?? "-"}`); + lines.push(displayResources(spec)); + if (spec.events?.length) { + lines.push(displayEvents(spec)); + } + if (spec.externalServices?.length) { + lines.push(displayExternalServices(spec)); + } + const apis = impliedApis(spec); + if (apis.length) { + lines.push(displayApis(apis)); } - logger.info(marked(lines.join("\n"))); + const roles = impliedRoles(spec); + if (roles.length) { + lines.push(await displayRoles(roles)); + } + 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 + * Gets a display string of the external services an extension would use + * @param spec The extension spec containing the external services + * @return A string ready to be displayed in the terminal */ -export async function displayUpdateChangesRequiringConfirmation( - spec: extensionsApi.ExtensionSpec, - newSpec: extensionsApi.ExtensionSpec -): Promise { - if (spec.license !== newSpec.license) { - const message = - "\n" + - "**License**\n" + - deletionColor(spec.license ? `- ${spec.license}\n` : "- None\n") + - additionColor(newSpec.license ? `+ ${newSpec.license}\n` : "+ None\n") + - "Do you wish to continue?"; - await getConsent("license", marked(message)); - } - // eslint-disable-next-line @typescript-eslint/unbound-method - const apisDiffDeletions = _.differenceWith(spec.apis, _.get(newSpec, "apis", []), _.isEqual); - // eslint-disable-next-line @typescript-eslint/unbound-method - const apisDiffAdditions = _.differenceWith(newSpec.apis, _.get(spec, "apis", []), _.isEqual); - 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`); - }); - message += "Do you wish to continue?"; - await getConsent("apis", marked(message)); - } +export function displayExternalServices(spec: ExtensionSpec): string { + const lines = + spec.externalServices?.map((service: ExternalService) => { + return ` - ${clc.cyan(`${service.name} (${service.pricingUri})`)}`; + }) ?? []; + return clc.bold("External services used:\n") + lines.join("\n"); +} - const resourcesDiffDeletions = _.differenceWith( - spec.resources, - _.get(newSpec, "resources", []), - compareResources +/** + * Gets a display string of the events an extension could emit + * @param spec The extension spec containing the events + * @return A string ready to be displayed in the terminal + */ +export function displayEvents(spec: ExtensionSpec): string { + 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"); +} + +/** + * Gets a display string of the resources an extension could create + * @param spec The extension spec + * @return A string ready to be displayed in the terminal + */ +export function displayResources(spec: ExtensionSpec): string { + 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: + } + return ` - ${clc.blueBright(`${resource.name} (${type})`)}${ + resource.description ? `: ${resource.description}` : "" + }`; + }); + lines.push( + ...new Set( + spec.lifecycleEvents?.map((event: LifecycleEvent) => { + return ` - ${clc.blueBright(`${event.taskQueueTriggerFunction} (Cloud Task queue)`)}`; + }), + ), ); - const resourcesDiffAdditions = _.differenceWith( - newSpec.resources, - _.get(spec, "resources", []), - compareResources + lines.push( + ...spec.params + .filter((param: Param) => { + return param.type === "SECRET"; + }) + .map((param: Param) => { + return ` - ${clc.blueBright(`${param.param} (Cloud Secret Manager secret)`)}`; + }), ); - if (resourcesDiffDeletions.length || resourcesDiffAdditions.length) { - let message = "\n**Resources:**\n"; - resourcesDiffDeletions.forEach((resource) => { - message += deletionColor(` - ${getResourceReadableName(resource)}`); - }); - resourcesDiffAdditions.forEach((resource) => { - message += additionColor(`+ ${getResourceReadableName(resource)}`); - }); - message += "Do you wish to continue?"; - await getConsent("resources", marked(message)); - } + return clc.bold("Resources created:\n") + (lines.length ? lines.join("\n") : " - None"); +} - // eslint-disable-next-line @typescript-eslint/unbound-method - const rolesDiffDeletions = _.differenceWith(spec.roles, _.get(newSpec, "roles", []), _.isEqual); - // eslint-disable-next-line @typescript-eslint/unbound-method - const rolesDiffAdditions = _.differenceWith(newSpec.roles, _.get(spec, "roles", []), _.isEqual); - if (rolesDiffDeletions.length || rolesDiffAdditions.length) { - let message = "\n**Permissions:**\n"; - rolesDiffDeletions.forEach((role) => { - message += deletionColor(`- ${role.role} (${role.reason})\n`); - }); - rolesDiffAdditions.forEach((role) => { - message += additionColor(`+ ${role.role} (${role.reason})\n`); - }); - message += "Do you wish to continue?"; - await getConsent("apis", marked(message)); - } +/** + * 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): Promise { + const res = await iam.getRole(role); + return ` - ${clc.yellow(res.title || res.name)}${res.description ? `: ${res.description}` : ""}`; +} - if (!spec.billingRequired && newSpec.billingRequired) { - await getConsent( - "billingRequired", - "Billing is now required for the new version of this extension. Would you like to continue?" - ); - } +async function displayRoles(roles: Role[]): Promise { + const lines: string[] = await Promise.all( + roles.map((role: Role) => { + return retrieveRoleInfo(role.role); + }), + ); + return clc.bold("Roles granted:\n") + lines.join("\n"); } -function compareResources(resource1: extensionsApi.Resource, resource2: extensionsApi.Resource) { - return resource1.name == resource2.name && resource1.type == resource2.type; +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 getResourceReadableName(resource: extensionsApi.Resource): string { - return resource.type === "firebaseextensions.v1beta.function" - ? `${resource.name} (Cloud Function): ${resource.description}\n` - : `${resource.name} (${resource.type})\n`; +function usesTasks(spec: ExtensionSpec): boolean { + return spec.resources.some( + (r: Resource) => + r.type === FUNCTIONS_RESOURCE_TYPE && r.properties?.taskQueueTrigger !== undefined, + ); } -/** - * Asks the user to provide permission to update the instance. - * @param field - * @param message - */ -export async function getConsent(field: string, message: string): Promise { - const consent = await promptOnce({ - type: "confirm", - message, - default: true, - }); - if (!consent) { - throw new FirebaseError( - `Without explicit consent for the change to ${field}, we cannot update this extension instance.`, - { exit: 2 } - ); +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.", + }); } + 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 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.", + }); + } + + 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 6427f250ac6..aca084c904d 100644 --- a/src/extensions/emulator/optionsHelper.ts +++ b/src/extensions/emulator/optionsHelper.ts @@ -1,181 +1,111 @@ -import * as fs from "fs-extra"; -import * as _ from "lodash"; -import * as path from "path"; +import { ParsedTriggerDefinition } from "../../emulator/functionsEmulatorShared"; import * as paramHelper from "../paramHelper"; import * as specHelper from "./specHelper"; -import * as localHelper from "../localHelper"; import * as triggerHelper from "./triggerHelper"; -import { Resource } from "../extensionsApi"; +import { ExtensionSpec, Param, ParamType } from "../types"; import * as extensionsHelper from "../extensionsHelper"; -import * as Config from "../../config"; -import { FirebaseError } from "../../error"; -import { EmulatorLogger } from "../../emulator/emulatorLogger"; -import * as getProjectId from "../../getProjectId"; -import { Emulators } from "../../emulator/types"; - -export async function buildOptions(options: any): Promise { - const extensionDir = localHelper.findExtensionYaml(process.cwd()); - options.extensionDir = extensionDir; - const extensionYaml = await specHelper.readExtensionYaml(extensionDir); - extensionsHelper.validateSpec(extensionYaml); - - const params = await paramHelper.readParamsFile(options.testParams); - extensionsHelper.validateCommandLineParams(params, extensionYaml.params || []); - params["PROJECT_ID"] = getProjectId(options, false); - params["EXT_INSTANCE_ID"] = params["EXT_INSTANCE_ID"] || extensionYaml.name; - params["DATABASE_INSTANCE"] = params["PROJECT_ID"]; - params["DATABASE_URL"] = `https://${params["DATABASE_INSTANCE"]}.firebaseio.com`; - params["STORAGE_BUCKET"] = `${params["PROJECT_ID"]}.appspot.com`; - const functionResources = specHelper.getFunctionResourcesWithParamSubstitution( - extensionYaml, - params - ) as Resource[]; - let testConfig; - if (options.testConfig) { - testConfig = readTestConfigFile(options.testConfig); - checkTestConfig(testConfig, functionResources); - } - options.config = buildConfig(functionResources, testConfig); - options.extensionEnv = params; - const functionEmuTriggerDefs = functionResources.map((r) => - triggerHelper.functionResourceToEmulatedTriggerDefintion(r) - ); - options.extensionTriggers = functionEmuTriggerDefs; - options.extensionNodeVersion = specHelper.getNodeVersion(functionResources); - return options; -} +import * as planner from "../../deploy/extensions/planner"; +import { needProjectId } from "../../projectUtils"; +import { SecretEnvVar } from "../../deploy/functions/backend"; +import { Runtime } from "../../deploy/functions/runtimes/supported"; /** - * Checks and warns if the test config is missing fields - * that are relevant for the extension being emulated. + * TODO: Better name? Also, should this be in extensionsEmulator instead? */ -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." - ); - } +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); - 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." - ); - } + const nonSecretEnv = getNonSecretEnv(spec.params ?? [], paramValues); + const secretEnvVariables = getSecretEnvVars(spec.params ?? [], paramValues); + return { + extensionTriggers, + runtime, + nonSecretEnv, + secretEnvVariables, + }; } +const isSecretParam = (p: Param) => + p.type === extensionsHelper.SpecParamType.SECRET || p.type === ParamType.SECRET; + /** - * Reads a test config file. - * @param testConfigPath filepath to a firebase.json style config file. + * 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 */ -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, - }); +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]; } -} - -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 (config.get("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; + return getNonSecretEnv; } /** - * 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 + * 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 */ -function getFunctionSourceDirectory(functionResources: Resource[]): string { - let sourceDirectory; - for (const r of functionResources) { - let dir = _.get(r, "properties.sourceDirectory"); - if (!dir) { - EmulatorLogger.forEmulator(Emulators.FUNCTIONS).log( - "INFO", - `No sourceDirectory was specified for function ${r.name}, defaulting to 'functions'` - ); - 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.` - ); +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? } - return sourceDirectory; + return secretEnvVar; } -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); -} +/** + * Exported for testing + */ +export function getParams(options: any, extensionSpec: ExtensionSpec) { + const projectId = needProjectId(options); + const userParams = paramHelper.readEnvFile(options.testParams); + const autoParams = { + PROJECT_ID: projectId, + EXT_INSTANCE_ID: extensionSpec.name, + DATABASE_INSTANCE: projectId, + DATABASE_URL: `https://${projectId}.firebaseio.com`, + STORAGE_BUCKET: `${projectId}.appspot.com`, + }; + const unsubbedParamsWithoutDefaults = Object.assign(autoParams, userParams); -function shouldEmulatePubsub(resources: Resource[]): boolean { - return shouldEmulate("google.pubsub", resources); + const unsubbedParams = extensionsHelper.populateDefaultParams( + unsubbedParamsWithoutDefaults, + extensionSpec.params, + ); + // Run a substitution to support params that reference other params. + return extensionsHelper.substituteParams>(unsubbedParams, unsubbedParams); } 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 b6f90ece82a..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); + 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 40a1b97b7f6..9acb024e011 100644 --- a/src/extensions/emulator/triggerHelper.ts +++ b/src/extensions/emulator/triggerHelper.ts @@ -1,69 +1,158 @@ -import * as _ from "lodash"; -import { EmulatedTriggerDefinition } from "../../emulator/functionsEmulatorShared"; -import { Constants } from "../../emulator/constants"; +import * as backend from "../../deploy/functions/backend"; import { EmulatorLogger } from "../../emulator/emulatorLogger"; +import { + EventSchedule, + getServiceFromEventType, + ParsedTriggerDefinition, +} from "../../emulator/functionsEmulatorShared"; 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"; +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: any -): EmulatedTriggerDefinition { - const etd: EmulatedTriggerDefinition = { - name: resource.name, - entryPoint: resource.name, - }; - 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.` + 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; } - return etd; -} - -function getServiceFromEventType(eventType: string): string { - if (eventType.includes("firestore")) { - return Constants.SERVICE_FIRESTORE; - } - if (eventType.includes("database")) { - return Constants.SERVICE_REALTIME_DATABASE; - } - if (eventType.includes("pubsub")) { - return Constants.SERVICE_PUBSUB; - } - // Below this point are services that do not have a emulator. - if (eventType.includes("analytics")) { - return Constants.SERVICE_ANALYTICS; - } - if (eventType.includes("auth")) { - return Constants.SERVICE_AUTH; - } - if (eventType.includes("crashlytics")) { - return Constants.SERVICE_CRASHLYTICS; - } - if (eventType.includes("remoteconfig")) { - return Constants.SERVICE_REMOTE_CONFIG; - } - if (eventType.includes("storage")) { - return Constants.SERVICE_STORAGE; - } - if (eventType.includes("testing")) { - return Constants.SERVICE_TEST_LAB; + 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; + } + } + if (properties.eventTrigger.eventType.includes("google.cloud.firestore")) { + // Fall back to '(default)' if unset, to match https://github.com/firebase/firebase-functions/blob/e3f9772a530860f7469434a91d344e3faa371765/src/v2/providers/firestore.ts#L511 + eventFilters["database"] = eventFilters["database"] ?? "(default)"; + eventFilters["namespace"] = eventFilters["namespace"] ?? "(default)"; + } + 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 ""; + 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/extensions/export.spec.ts b/src/extensions/export.spec.ts new file mode 100644 index 00000000000..51050820ff3 --- /dev/null +++ b/src/extensions/export.spec.ts @@ -0,0 +1,120 @@ +import { expect } from "chai"; + +import { parameterizeProject, setSecretParamsToLatest } from "./export"; +import { DeploymentInstanceSpec } from "../deploy/extensions/planner"; +import { ParamType } from "./types"; + +describe("ext:export helpers", () => { + describe("parameterizeProject", () => { + const TEST_PROJECT_ID = "test-project"; + const TEST_PROJECT_NUMBER = "123456789"; + const tests: { + desc: string; + in: Record; + expected: Record; + }[] = [ + { + desc: "should strip projectId", + in: { + param1: TEST_PROJECT_ID, + param2: `${TEST_PROJECT_ID}.appspot.com`, + }, + expected: { + param1: "${param:PROJECT_ID}", + param2: "${param:PROJECT_ID}.appspot.com", + }, + }, + { + desc: "should strip projectNumber", + in: { + param1: TEST_PROJECT_NUMBER, + param2: `projects/${TEST_PROJECT_NUMBER}/secrets/my-secret/versions/1`, + }, + expected: { + param1: "${param:PROJECT_NUMBER}", + param2: "projects/${param:PROJECT_NUMBER}/secrets/my-secret/versions/1", + }, + }, + { + desc: "should not affect other params", + in: { + param1: "A param", + param2: `Another param`, + }, + expected: { + param1: "A param", + param2: `Another param`, + }, + }, + ]; + for (const t of tests) { + it(t.desc, () => { + 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: {}, + }); + }); + } + }); + + describe("setSecretVersionsToLatest", () => { + const testSecretVersion = "projects/my-proj/secrets/secret-1/versions/3"; + const tests: { + desc: string; + params: Record; + expected: string; + }[] = [ + { + desc: "Should set active secrets to latest", + params: { blah: testSecretVersion, notSecret: "something else" }, + expected: "projects/my-proj/secrets/secret-1/versions/latest", + }, + ]; + for (const t of tests) { + it(t.desc, async () => { + const testSpec: DeploymentInstanceSpec = { + instanceId: "my-instance", + params: t.params, + systemParams: {}, + extensionVersion: { + name: "test", + ref: "test/test@0.1.0", + state: "PUBLISHED", + hash: "abc123", + sourceDownloadUri: "test.com", + spec: { + name: "blah", + version: "0.1.0", + sourceUrl: "blah.com", + resources: [], + params: [ + { + param: "blah", + label: "blah", + type: ParamType.SECRET, + }, + { + param: "notSecret", + label: "blah", + }, + ], + systemParams: [], + }, + }, + }; + + const res = await setSecretParamsToLatest(testSpec); + + expect(res.params["blah"]).to.equal(t.expected); + expect(res.params["notSecret"]).to.equal(t.params["notSecret"]); + }); + } + }); +}); diff --git a/src/extensions/export.ts b/src/extensions/export.ts new file mode 100644 index 00000000000..2f4317c2049 --- /dev/null +++ b/src/extensions/export.ts @@ -0,0 +1,86 @@ +import { getExtensionVersion, DeploymentInstanceSpec } from "../deploy/extensions/planner"; +import { humanReadable } from "../deploy/extensions/deploymentSummary"; +import { logger } from "../logger"; +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. + * For example, 'my-project-id.appspot.com' becomes '${param:PROJECT_ID}.appspot.com` + */ +export function parameterizeProject( + projectId: string, + projectNumber: string, + spec: DeploymentInstanceSpec, +): DeploymentInstanceSpec { + const newParams: Record = {}; + for (const [key, val] of Object.entries(spec.params)) { + const p1 = val.replace(projectId, "${param:PROJECT_ID}"); + const p2 = p1.replace(projectNumber, "${param:PROJECT_NUMBER}"); + newParams[key] = p2; + } + const newSpec = { ...spec }; + newSpec.params = newParams; + return newSpec; +} + +/** + * 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: DeploymentInstanceSpec, +): Promise { + const newParams = { ...spec.params }; + const extensionVersion = await getExtensionVersion(spec); + const activeSecrets = getActiveSecrets(extensionVersion.spec, newParams); + for (const [key, val] of Object.entries(newParams)) { + if (activeSecrets.includes(val)) { + const parsed = parseSecretVersionResourceName(val); + parsed.versionId = "latest"; + newParams[key] = toSecretVersionResourceName(parsed); + } + } + return { ...spec, params: newParams }; +} + +export function displayExportInfo( + withRef: DeploymentInstanceSpec[], + withoutRef: DeploymentInstanceSpec[], +): void { + logger.info("The following Extension instances will be saved locally:"); + logger.info(""); + + displaySpecs(withRef); + + if (withoutRef.length) { + logger.info( + `Your project also has the following instances installed from local sources. These will not be saved to firebase.json:`, + ); + for (const spec of withoutRef) { + logger.info(spec.instanceId); + } + } +} + +/** + * Displays a summary of the Extension instances and configurations that will be saved locally. + * @param specs The instances that will be saved locally. + */ +function displaySpecs(specs: DeploymentInstanceSpec[]): void { + for (let i = 0; i < specs.length; i++) { + const spec = specs[i]; + logger.info(`${i + 1}. ${humanReadable(spec)}`); + logger.info(`Configuration will be written to 'extensions/${spec.instanceId}.env'`); + for (const p of Object.entries(spec.params)) { + logger.info(`\t${p[0]}=${p[1]}`); + } + if (spec.allowedEventTypes?.length) { + logger.info(`\tALLOWED_EVENTS=${spec.allowedEventTypes}`); + } + 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 b334434c1e6..13fbe670d9b 100644 --- a/src/extensions/extensionsApi.ts +++ b/src/extensions/extensionsApi.ts @@ -1,238 +1,137 @@ -import * as semver from "semver"; -import * as yaml from "js-yaml"; -import * as _ from "lodash"; -import * as clc from "cli-color"; +import * as yaml from "yaml"; +import * as clc from "colorette"; -import * as api from "../api"; +import { Client } from "../apiv2"; +import { extensionsOrigin } from "../api"; +import { FirebaseError, getErrMsg, getErrStatus } 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, + isExtensionInstance, +} from "./types"; + +const EXTENSIONS_API_VERSION = "v1beta"; const PAGE_SIZE_MAX = 100; -const refRegex = new RegExp(/^([^/@\n]+)\/{1}([^/@\n]+)(@{1}([a-z0-9.-]+)|)$/); - -export interface Extension { - name: string; - ref: string; - state: "STATE_UNSPECIFIED" | "PUBLISHED"; - createTime: string; - latestVersion?: string; - latestVersionCreateTime?: string; -} - -export interface ExtensionVersion { - name: string; - ref: string; - spec: ExtensionSpec; - state?: "STATE_UNSPECIFIED" | "PUBLISHED"; - hash: string; - createTime?: 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; -} - -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", -} -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. - * * @param projectId the project to create the instance in * @param instanceId the id to set for the instance * @param config instance configuration + * @param labels labels for the instance + * @param validateOnly if true we only perform validation, not the actual creation */ -export async function createInstance( +async function createInstanceHelper( projectId: string, instanceId: string, - config: any + config: Record, + labels: Record | undefined, + 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; labels: Record | undefined }, + ExtensionInstance + >( + `/projects/${projectId}/instances/`, + { name: `projects/${projectId}/instances/${instanceId}`, - config: config, + config, + labels, }, - }); + { + queryParams: { + validateOnly: validateOnly ? "true" : "false", + }, + }, + ); + if (validateOnly) { + 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; } -/** - * Create a new extension instance, given a extension source path, a set of params, and a service account. - * - * @param projectId the project to create the instance in - * @param instanceId the id to set for the instance - * @param extensionSource the ExtensionSource to create an instance of - * @param params params to configure the extension instance - */ -export async function createInstanceFromSource( - projectId: string, - instanceId: string, - extensionSource: ExtensionSource, - params: { [key: string]: string } -): Promise { - const config = { - source: { name: extensionSource.name }, - params, - }; - return createInstance(projectId, instanceId, config); -} +export type CreateInstanceArgs = { + projectId: string; + instanceId: string; + extensionSource?: ExtensionSource; + extensionVersionRef?: string; + params: Record; + systemParams?: Record; + allowedEventTypes?: string[]; + eventarcChannel?: string; + validateOnly?: boolean; + labels?: Record; +}; /** * Create a new extension instance, given a extension source path, a set of params, and a service account. - * - * @param projectId the project to create the instance in - * @param instanceId the id to set for the instance - * @param extensionVersion the ExtensionVersion ref - * @param params params to configure the extension instance + * @param args the args for creating the instance */ -export async function createInstanceFromExtensionVersion( - projectId: string, - instanceId: string, - extensionVersion: ExtensionVersion, - params: { [key: string]: string } -): Promise { - const { publisherId, extensionId, version } = parseRef(extensionVersion.ref); - const config = { - extensionRef: `${publisherId}/${extensionId}`, - extensionVersion: version || "", - params, +export async function createInstance(args: CreateInstanceArgs): Promise { + const config: Record = { + params: args.params, + systemParams: args.systemParams ?? {}, + allowedEventTypes: args.allowedEventTypes, + eventarcChannel: args.eventarcChannel, }; - return createInstance(projectId, instanceId, config); + + if (args.extensionSource && args.extensionVersionRef) { + throw new FirebaseError( + "ExtensionSource and ExtensionVersion both provided, but only one should be.", + ); + } else if (args.extensionSource) { + config.source = { name: args.extensionSource?.name }; + } else if (args.extensionVersionRef) { + const ref = refs.parse(args.extensionVersionRef); + config.extensionRef = refs.toExtensionRef(ref); + config.extensionVersion = ref.version ?? ""; + } 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 await createInstanceHelper( + args.projectId, + args.instanceId, + config, + args.labels, + args.validateOnly, + ); } /** * Delete an instance and all of the associated resources and its service account. - * * @param projectId the project where the instance exists * @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, - } +export async function deleteInstance(projectId: string, instanceId: string): Promise { + 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, }); @@ -243,39 +142,41 @@ 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; +): Promise { + try { + const res = await extensionsApiClient.get(`/projects/${projectId}/instances/${instanceId}`); + if (isExtensionInstance(res.body)) { + return res.body; + } + } catch (err: unknown) { + if (getErrStatus(err) === 404) { + throw new FirebaseError( + `Extension instance '${clc.bold(instanceId)}' not found in project '${clc.bold( + projectId, + )}'.`, + { status: 404 }, + ); + } + throw err; + } } /** * Returns a list of all installed extension instances on the project with projectId. - * * @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, }, @@ -293,125 +194,222 @@ export async function listInstances(projectId: string): Promise { - const res = await patchInstance(projectId, instanceId, "config.params", { - config: { - params, +export async function configureInstance(args: { + projectId: string; + instanceId: string; + params: Record; + systemParams?: Record; + canEmitEvents: boolean; + allowedEventTypes?: string[]; + eventarcChannel?: string; + validateOnly?: boolean; +}): Promise { + const reqBody = { + projectId: args.projectId, + instanceId: args.instanceId, + updateMask: "config.params", + validateOnly: args.validateOnly ?? false, + data: { + config: { + params: args.params, + } as Record, }, - }); - 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); } /** * Update the version of a extension instance, given an project id, instance id, and a set of params - * - * @param projectId the project the instance is in - * @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 args The update instance args + * @param args.projectId the project the instance is in + * @param args.instanceId the id of the instance to configure + * @param args.extensionSource the source for the version of the extension to update to + * @param args.params params to update the extension instance + * @param args.systemParams system params to update the extension instance + * @param args.canEmitEvents if the instance can emit events + * @param args.allowedEventTypes types of events (selected by consumer) that the extension is allowed to emit + * @param args.eventarcChannel fully qualified eventarc channel resource name to emit events to + * @param args.validateOnly if true, only validates the update and makes no changes */ -export async function updateInstance( - projectId: string, - instanceId: string, - extensionSource: ExtensionSource, - params?: { [option: string]: string } -): Promise { - const body: any = { +export async function updateInstance(args: { + projectId: string; + instanceId: string; + extensionSource: ExtensionSource; + params?: Record; + systemParams?: Record; + canEmitEvents: boolean; + allowedEventTypes?: string[]; + eventarcChannel?: string; + validateOnly?: boolean; +}): Promise { + const body: Record> = { config: { - source: { name: extensionSource.name }, + source: { name: args.extensionSource.name }, }, }; let updateMask = "config.source.name"; - if (params) { - body.params = params; + if (args.params) { + body.config.params = args.params; updateMask += ",config.params"; } - return await patchInstance(projectId, instanceId, updateMask, body); + 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, + validateOnly: args.validateOnly ?? false, + data: body, + }); } /** * Update the version of a extension instance, given an project id, instance id, and a set of params - * - * @param projectId the project the instance is in - * @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 args the update args + * @param args.projectId the project the instance is in + * @param args.instanceId the id of the instance to configure + * @param args.extRef reference for the extension to update to + * @param args.params params to configure the extension instance + * @param args.systemParams system params to configure the extension instance + * @param args.canEmitEvents if the instance can emit events + * @param args.allowedEventTypes types of events (selected by consumer) that the extension is allowed to emit + * @param args.eventarcChannel fully qualified eventarc channel resource name to emit events to + * @param args.validateOnly if true, only validates the update and makes no changes */ -export async function updateInstanceFromRegistry( - projectId: string, - instanceId: string, - extRef: string, - params?: { [option: string]: string } -): Promise { - const { publisherId, extensionId, version } = parseRef(extRef); - const body: any = { +export async function updateInstanceFromRegistry(args: { + projectId: string; + instanceId: string; + extRef: string; + params?: Record; + systemParams?: Record; + canEmitEvents: boolean; + allowedEventTypes?: string[]; + eventarcChannel?: string; + validateOnly?: boolean; +}): Promise { + const ref = refs.parse(args.extRef); + const body: Record> = { config: { - extensionRef: `${publisherId}/${extensionId}`, - extensionVersion: version, + extensionRef: refs.toExtensionRef(ref), + extensionVersion: ref.version, }, }; let updateMask = "config.extension_ref,config.extension_version"; - if (params) { - body.params = params; + if (args.params) { + body.config.params = args.params; updateMask += ",config.params"; } - return await patchInstance(projectId, instanceId, updateMask, body); + 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, + validateOnly: args.validateOnly ?? false, + data: body, + }); } -async function patchInstance( - projectId: string, - instanceId: string, - updateMask: string, - data: any -): Promise { - const updateRes = await api.request( - "PATCH", - `/${VERSION}/projects/${projectId}/instances/${instanceId}`, +async function patchInstance(args: { + projectId: string; + instanceId: string; + updateMask: string; + validateOnly: boolean; + data: unknown; +}): Promise { + const updateRes = await extensionsApiClient.patch( + `/projects/${args.projectId}/instances/${args.instanceId}`, + args.data, { - auth: true, - origin: api.extensionsOrigin, - query: { - updateMask, + queryParams: { + updateMask: args.updateMask, + validateOnly: args.validateOnly ? "true" : "false", }, - 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(source: ExtensionSource): void { - const spec: ExtensionSpec = source.spec; +/** + * populates the spec by parsing yaml properties into real properties + * @param spec The spec to populate + */ +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) { - logger.debug(`[ext] failed to parse resource properties yaml: ${err}`); + } catch (err: unknown) { + logger.debug(`[ext] failed to parse resource properties yaml: ${getErrMsg(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 ?? []; } } /** * Create a new extension source - * * @param projectId The project to create the source in * @param packageUri A URI for a zipper archive of a extension source * @param extensionRoot A directory inside the archive to look for extension.yaml @@ -419,95 +417,82 @@ function populateResourceProperties(source: ExtensionSource): 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, }); - populateResourceProperties(pollRes); + if (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) => { - populateResourceProperties(res.body); - 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; } /** - * @param ref user-friendly identifier for the ExtensionVersion (publisher-id/extension-id@1.0.0) + * @param extensionVersionRef user-friendly identifier for the ExtensionVersion (publisher-id/extension-id@1.0.0) */ -export async function getExtensionVersion(ref: string): Promise { - const { publisherId, extensionId, version } = parseRef(ref); - if (!version) { - throw new FirebaseError(`ExtensionVersion ref "${ref}" must supply a version.`); +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 api.request( - "GET", - `/${VERSION}/publishers/${publisherId}/extensions/${extensionId}/versions/${version}`, - { - auth: true, - origin: api.extensionsOrigin, - } + const res = await extensionsApiClient.get( + `/${refs.toExtensionVersionName(ref)}`, ); + if (res.body.spec) { + populateSpec(res.body.spec); + } return res.body; - } catch (err) { - if (err.status === 404) { - throw new FirebaseError( - `The extension reference '${clc.bold( - ref - )}' doesn't exist. This could happen for two reasons:\n` + - ` -The publisher ID '${clc.bold(publisherId)}' doesn't exist or could be misspelled\n` + - ` -The name of the extension version '${clc.bold( - `${extensionId}@${version}` - )}' doesn't exist or could be misspelled\n` + - `Please correct the extension reference and try again.` - ); + } catch (err: unknown) { + if (getErrStatus(err) === 404) { + throw refNotFoundError(ref); } else if (err instanceof FirebaseError) { throw err; } - throw new FirebaseError(`Failed to query the extension version '${clc.bold(ref)}': ${err}`); + throw new FirebaseError( + `Failed to query the extension version '${clc.bold(extensionVersionRef)}': ${getErrMsg(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 = ""): Promise => { + 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); } @@ -521,24 +506,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): Promise { - const { publisherId, extensionId } = parseRef(ref); +export async function listExtensionVersions( + ref: 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: { - pageSize: PAGE_SIZE_MAX, - pageToken, - }, - } - ); + const getNextPage = async (pageToken = ""): Promise => { + 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); } @@ -551,176 +538,46 @@ export async function listExtensionVersions(ref: string): Promise { - const res = await api.request( - "POST", - `/${VERSION}/projects/${projectId}/publisherProfile:register`, - { - auth: true, - origin: api.extensionsOrigin, - data: { publisherId }, - } - ); - return res.body; -} - -/** - * @param packageUri public URI of a zip or tarball of the extension source code - * @param ref 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( - ref: string, - packageUri: string, - extensionRoot?: string -): Promise { - const { publisherId, extensionId, version } = parseRef(ref); - if (!version) { - throw new FirebaseError(`ExtensionVersion ref "${ref}" must supply a version.`); - } - - const publishRes = await api.request( - "POST", - `/${VERSION}/publishers/${publisherId}/extensions/${extensionId}/versions:publish`, - { - auth: true, - origin: api.extensionsOrigin, - data: { - versionId: version, - packageUri, - extensionRoot: extensionRoot || "/", - }, - } - ); - const pollRes = await operationPoller.pollOperation({ - apiOrigin: api.extensionsOrigin, - apiVersion: VERSION, - operationResourceName: publishRes.body.name, - masterTimeout: 600000, - }); - return pollRes; -} - -/** - * @param ref user-friendly identifier for the Extension (publisher-id/extension-id) - */ -export async function unpublishExtension(ref: string): Promise { - const { publisherId, extensionId, version } = parseRef(ref); - if (version) { - throw new FirebaseError(`Extension reference "${ref}" must not contain a version.`); - } - const url = `/${VERSION}/publishers/${publisherId}/extensions/${extensionId}: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( - ref - )}' and don’t have the correct permissions to unpublish this extension.` - ); - } else if (err instanceof FirebaseError) { - throw err; - } - throw new FirebaseError(`Error occurred unpublishing extension '${ref}': ${err}`); - } -} - -/** - * @param ref user-friendly identifier for the Extension (publisher-id/extension-id) + * @param extensionRef user-friendly identifier for the Extension (publisher-id/extension-id) * @return the extension */ -export async function getExtension(ref: string): Promise { - const { publisherId, extensionId } = parseRef(ref); +export async function getExtension(extensionRef: string): Promise { + const ref = refs.parse(extensionRef); try { - const res = await api.request( - "GET", - `/${VERSION}/publishers/${publisherId}/extensions/${extensionId}`, - { - auth: true, - origin: api.extensionsOrigin, - } - ); + const res = await extensionsApiClient.get(`/${refs.toExtensionName(ref)}`); return res.body; - } catch (err) { - if (err.status === 404) { - throw new FirebaseError( - `The extension reference '${clc.bold( - ref - )}' doesn't exist. This could happen for two reasons:\n` + - ` -The publisher ID '${clc.bold(publisherId)}' doesn't exist or could be misspelled\n` + - ` -The name of the extension '${clc.bold( - extensionId - )}' doesn't exist or could be misspelled\n` + - `Please correct the extension reference and try again.` - ); + } catch (err: unknown) { + if (getErrStatus(err) === 404) { + throw refNotFoundError(ref); } else if (err instanceof FirebaseError) { throw err; } - throw new FirebaseError(`Failed to query the extension '${clc.bold(ref)}': ${err}`); + throw new FirebaseError( + `Failed to query the extension '${clc.bold(extensionRef)}': ${getErrMsg(err)}`, + { + status: getErrStatus(err), + }, + ); } } /** - * @param ref user-friendly identifier - * @return array of ref split into publisher id, extension id, and version id (if applicable) + * refNotFoundError returns a nicely formatted error when a reference is not found + * @param ref The reference that is missing + * @return a formatted FirebaseError */ -export function parseRef( - ref: string -): { - publisherId: string; - extensionId: string; - version?: string; -} { - const parts = refRegex.exec(ref); - // Exec additionally returns original string, index, & input values. - if (parts && (parts.length == 5 || parts.length == 7)) { - const publisherId = parts[1]; - const extensionId = parts[2]; - const version = parts[4]; - if (version && !semver.valid(version) && version !== "latest") { - throw new FirebaseError(`Extension reference ${ref} contains an invalid version ${version}.`); - } - return { publisherId, extensionId, version }; - } - throw new FirebaseError( - "Extension reference must be in format '{publisher}/{extension}(@{version})'." +export function refNotFoundError(ref: refs.Ref): FirebaseError { + return new FirebaseError( + `The extension reference '${clc.bold( + 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, + )}' doesn't exist or could be misspelled\n\n` + + `Please correct the extension reference and try again. If you meant to reference an extension from a local source, please provide a relative path prefixed with '${clc.bold( + "./", + )}', '${clc.bold("../")}', or '${clc.bold("~/")}'.}`, + { status: 404 }, ); } - -/** - * @param extensionVersionName resource name of the format `publishers//extensions//versions/` - * @return array of ref split into publisher id, extension id, and version id (if applicable) - */ -export function parseExtensionVersionName( - extensionVersionName: string -): { - publisherId: string; - extensionId: string; - version?: string; -} { - const parts = extensionVersionName.split("/"); - if ( - parts.length !== 6 || - parts[0] !== "publishers" || - parts[2] !== "extensions" || - parts[4] !== "versions" - ) { - throw new FirebaseError( - "Extension version name must be in the format `publishers//extensions//versions/`." - ); - } - const publisherId = parts[1]; - const extensionId = parts[3]; - const version = parts[5]; - return { publisherId, extensionId, version }; -} diff --git a/src/extensions/extensionsHelper.spec.ts b/src/extensions/extensionsHelper.spec.ts new file mode 100644 index 00000000000..8f5b93f0bb1 --- /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 inputStub: sinon.SinonStub; + + beforeEach(() => { + inputStub = sinon.stub(prompt, "input"); + }); + + 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"; + inputStub.resolves(userInput); + + const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); + + expect(instanceId).to.equal(userInput); + expect(inputStub).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"; + inputStub.onCall(0).returns(userInput1); + inputStub.onCall(1).returns(userInput2); + + const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); + + expect(instanceId).to.equal(userInput2); + expect(inputStub).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"; + inputStub.onCall(0).returns(userInput1); + inputStub.onCall(1).returns(userInput2); + + const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); + + expect(instanceId).to.equal(userInput2); + expect(inputStub).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"; + inputStub.onCall(0).returns(userInput1); + inputStub.onCall(1).returns(userInput2); + inputStub.onCall(2).returns(userInput3); + + const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); + + expect(instanceId).to.equal(userInput3); + expect(inputStub).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"; + inputStub.onCall(0).returns(userInput1); + inputStub.onCall(1).returns(userInput2); + + const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); + + expect(instanceId).to.equal(userInput2); + expect(inputStub).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"; + inputStub.onCall(0).returns(userInput1); + inputStub.onCall(1).returns(userInput2); + + const instanceId = await extensionsHelper.promptForValidInstanceId(extensionName); + + expect(instanceId).to.equal(userInput2); + expect(inputStub).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: 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 4962abd30d3..7892895bcfa 100644 --- a/src/extensions/extensionsHelper.ts +++ b/src/extensions/extensionsHelper.ts @@ -1,35 +1,44 @@ -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 fs from "fs"; -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 { markedTerminal } from "marked-terminal"; -import { storageOrigin } from "../api"; +import { createUnzipTransform } from "./../unzip"; +marked.use(markedTerminal() as any); + +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 { FirebaseError } from "../error"; +import { diagnose } from "./diagnose"; import { checkResponse } from "./askUserForParam"; -import { ensure } from "../ensureApiEnabled"; +import { ensure, check } from "../ensureApiEnabled"; import { deleteObject, uploadObject } from "../gcp/storage"; -import * as getProjectId from "../getProjectId"; +import { getProjectId } from "../projectUtils"; +import { createSource, getInstance } from "./extensionsApi"; import { - createSource, - ExtensionSource, - ExtensionVersion, + createExtensionVersionFromGitHubSource, + createExtensionVersionFromLocalSource, getExtension, - getInstance, - getSource, - Param, - parseRef, - publishExtensionVersion, -} from "./extensionsApi"; -import { getLocalExtensionSpec } from "./localHelper"; -import { promptOnce } from "../prompt"; + getExtensionVersion, + listExtensionVersions, +} from "./publisherApi"; +import { Choice, confirm, input, select } from "../prompt"; +import { Extension, ExtensionSource, ExtensionSpec, ExtensionVersion, Param } from "./types"; +import * as refs from "./refs"; +import { EXTENSIONS_SPEC_FILE, readFile, getLocalExtensionSpec } from "./localHelper"; import { logger } from "../logger"; -import { envOverride } from "../utils"; +import { envOverride, logLabeledError } from "../utils"; +import { getLocalChangelog } from "./change-log"; +import { getProjectNumber } from "../getProjectNumber"; +import { Constants } from "../emulator/constants"; +import { defineSecret } from "firebase-functions/params"; /** * SpecParamType represents the exact strings that the extensions @@ -41,6 +50,8 @@ export enum SpecParamType { SELECT = "select", MULTISELECT = "multiSelect", STRING = "string", + SELECTRESOURCE = "selectResource", + SECRET = "secret", } export enum SourceOrigin { @@ -53,24 +64,34 @@ export enum SourceOrigin { } export const logPrefix = "extensions"; -export const validLicenses = ["apache-2.0"]; +const VALID_LICENSES = ["apache-2.0"]; // Extension archive URLs must be HTTPS. -export const urlRegex = /^https:/; +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", + "STORAGE_BUCKET", + "EXT_INSTANCE_ID", + "DATABASE_INSTANCE", + "DATABASE_URL", +]; // Placeholders that can be used whever param substitution is needed, but are not available. -export const AUTOPOULATED_PARAM_PLACEHOLDERS = { +export const AUTOPOPULATED_PARAM_PLACEHOLDERS = { PROJECT_ID: "project-id", STORAGE_BUCKET: "project-id.appspot.com", EXT_INSTANCE_ID: "extension-id", DATABASE_INSTANCE: "project-id-default-rtdb", DATABASE_URL: "https://project-id-default-rtdb.firebaseio.com", }; -export const resourceTypeToNiceName: { [key: string]: string } = { +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 @@ -79,7 +100,7 @@ export const resourceTypeToNiceName: { [key: string]: string } = { */ 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]; } @@ -89,22 +110,47 @@ export function getDBInstanceFromURL(databaseUrl = ""): string { /** * Gets Firebase project specific param values. */ -export async function getFirebaseProjectParams(projectId: string): Promise { - const body = await getFirebaseConfig({ project: projectId }); +export async function getFirebaseProjectParams( + projectId: string | undefined, + emulatorMode = false, +): Promise> { + if (!projectId) { + return {}; + } + const body = emulatorMode + ? await getProjectAdminSdkConfigOrCached(projectId) + : await getFirebaseConfig({ project: projectId }); + let projectNumber = Constants.FAKE_PROJECT_NUMBER; + if (!Constants.isDemoProject(projectId)) { + try { + projectNumber = await getProjectNumber({ projectId }); + } catch (err: any) { + logLabeledError( + "extensions", + `Unable to look up project number for ${projectId}.\n` + + " If this is a real project, ensure that you are logged in and have access to it.\n" + + " If this is a fake project, please use a project ID starting with 'demo-' to skip production calls.\n" + + " Continuing with a fake project number - secrets and other features that require production access may behave unexpectedly.", + ); + } + } + 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, - DATABASE_URL: body.databaseURL, - STORAGE_BUCKET: body.storageBucket, + PROJECT_ID: projectId, + PROJECT_NUMBER: projectNumber, + DATABASE_URL: databaseURL, + STORAGE_BUCKET: storageBucket, FIREBASE_CONFIG, - DATABASE_INSTANCE: getDBInstanceFromURL(body.databaseURL), + DATABASE_INSTANCE: getDBInstanceFromURL(databaseURL), }; } @@ -116,7 +162,7 @@ export async function getFirebaseProjectParams(projectId: string): Promise * @param params params to substitute the placeholders for * @return Resources object with substituted params */ -export function substituteParams(original: object[], params: { [key: string]: string }): Param[] { +export function substituteParams(original: T, params: Record): T { const startingString = JSON.stringify(original); const applySubstitution = (str: string, paramVal: string, paramKey: string): string => { const exp1 = new RegExp("\\$\\{" + paramKey + "\\}", "g"); @@ -125,33 +171,63 @@ export function substituteParams(original: object[], params: { [key: string]: st 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); +} + +type SecretParam = ReturnType; + +/** + * Substitutes any secret parameters with the correct format + * @param projectNumber The project number we are installing into + * @param params the full list of params to check for substitution. + * @returns The substituted list of params + */ +export async function substituteSecretParams( + projectNumber: string, + params: Record, +): Promise> { + const newParams: Record = {}; + for await (const [key, value] of Object.entries(params)) { + if (typeof value !== "string") { + newParams[key] = + `projects/${projectNumber}/secrets/${(value as SecretParam).name}/versions/latest`; + } else { + newParams[key] = value as string; + } + } + return newParams; } /** * Sets params equal to defaults given in extension.yaml if not already set in .env file. - * * @param paramVars JSON object of params to values parsed from .env file * @param paramSpec information on params parsed from extension.yaml * @return JSON object of params */ -export function populateDefaultParams(paramVars: any, paramSpec: any): any { +export function populateDefaultParams( + paramVars: Record, + paramSpecs: Param[], +): Record { const newParams = paramVars; - _.forEach(paramSpec, (env) => { - if (!paramVars[env.param]) { - if (env.default) { - newParams[env.param] = env.default; - } else if (env.required) { + for (const param of paramSpecs) { + if (!paramVars[param.param]) { + if (param.default !== undefined && param.required) { + newParams[param.param] = param.default; + } else if (param.required) { throw new FirebaseError( - `${env.param} has not been set in the given params file` + - " and there is no default available. Please set this variable before installing again." + `${param.param} has not been set in the given params file` + + " and there is no default available. Please set this variable before installing again.", ); } } - }); + } return newParams; } @@ -162,28 +238,26 @@ export function populateDefaultParams(paramVars: any, paramSpec: any): any { * @param paramSpec information on params parsed from extension.yaml */ export function validateCommandLineParams( - envVars: { [key: string]: string }, - paramSpec: any[] + envVars: Record, + paramSpec: Param[], ): void { - if (_.size(envVars) > _.size(paramSpec)) { - const paramList = _.map(paramSpec, (param) => { - return param.param; - }); - const misnamedParams = Object.keys(envVars).filter((key: any) => { - return !paramList.includes(key); - }); - logger.info( + const paramNames = paramSpec.map((p) => p.param); + const misnamedParams = Object.keys(envVars).filter((key: string) => { + return !paramNames.includes(key) && !AUTOPOPULATED_PARAM_NAMES.includes(key); + }); + 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; - _.forEach(paramSpec, (param) => { + for (const param of paramSpec) { // Warns if invalid response was found in environment file. if (!checkResponse(envVars[param.param], param)) { allParamsValid = false; } - }); + } if (!allParamsValid) { throw new FirebaseError(`Some param values are not valid. Please check your params file.`); } @@ -204,14 +278,23 @@ 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"); } else { const formattedLicense = String(spec.license).toLocaleLowerCase(); - if (!validLicenses.includes(formattedLicense)) { + if (!VALID_LICENSES.includes(formattedLicense)) { errors.push( - `license field in extension.yaml is invalid. Valid value(s): ${validLicenses.join(", ")}` + `license field in extension.yaml is invalid. Valid value(s): ${VALID_LICENSES.join(", ")}`, ); } } @@ -224,7 +307,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`, ); } } @@ -246,62 +329,60 @@ 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` - ); - } - if ( - param.default && - param.validationRegex && - !RegExp(param.validationRegex).test(param.default) - ) { - errors.push( - `Param${param.param ? ` ${param.param}` : ""} has default value '${ - param.default - }', which does not pass the validationRegex ${param.validationRegex}` + } 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.resourceType) { + errors.push( + `Param${param.param ? ` ${param.param}` : ""} must have resourceType because it is type ${ + param.type + }`, + ); + } + } } if (errors.length) { const formatted = errors.map((error) => ` - ${error}`); @@ -315,11 +396,10 @@ export function validateSpec(spec: any) { */ export async function promptForValidInstanceId(instanceId: string): Promise { let instanceIdIsValid = false; - let newInstanceId; + let newInstanceId = ""; const instanceIdRegex = /^[a-z][a-z\d\-]*[a-z\d]$/; while (!instanceIdIsValid) { - newInstanceId = await promptOnce({ - type: "input", + newInstanceId = await input({ default: instanceId, message: `Please enter a new name for this instance:`, }); @@ -328,7 +408,7 @@ export async function promptForValidInstanceId(instanceId: string): Promise { + let repoIsValid = false; + let extensionRoot = ""; + while (!repoIsValid) { + extensionRoot = await input( + "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 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 args.versionByStage map from stage to the next version to upload + * @param args.autoReview whether the stable version will be automatically sent for review on upload + * @param args.allowStable whether to allow stable versions + * @param args.hasVersions whether there have been any pre-release versions uploaded already + * @param args.nonInteractive whether the prompt is interactive or not + * @param args.force whether to assume the default instead of asking the user + */ +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: Choice[] = [ + { 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" as ReleaseStage, + }; + choices.push(stableChoice); + } + stage = await select({ + message: "Choose the release stage:", + choices: choices, + default: stage, + }); + if (stage === "stable" && !args.hasVersions) { + const confirmed = await confirm({ + message: `${clc.bold( + clc.yellow("Warning:"), + )} It's highly recommended to first upload a pre-release version before choosing stable.`, + nonInteractive: args.nonInteractive, + force: args.force, + default: false, + }); + if (!confirmed) { + stage = await select({ + message: "Choose the release stage:", + choices: choices, + default: stage, + }); + } + } + } + return stage; +} + +export async function checkExtensionsApiEnabled(options: any): Promise { + const projectId = getProjectId(options); + if (!projectId) { + return false; + } + return await check(projectId, extensionsOrigin(), "extensions", options.markdown); +} + export async function ensureExtensionsApiEnabled(options: any): Promise { const projectId = getProjectId(options); - return await ensure( - projectId, - "firebaseextensions.googleapis.com", - "extensions", - options.markdown - ); + 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); } /** @@ -358,107 +544,484 @@ async function archiveAndUploadSource(extPath: string, bucketName: string): Prom type: "zip", ignore: ["node_modules", ".git"], }); - return await uploadObject(zippedSource, bucketName); + const res = await uploadObject(zippedSource, bucketName); + return `/${res.bucket}/${res.object}`; } /** + * 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( - publisherId: string, +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) { + // 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, - rootDirectory: string -): Promise { +): Promise { const extensionSpec = await getLocalExtensionSpec(rootDirectory); - if (extensionSpec.name != extensionId) { + if (extensionSpec.name !== extensionId) { throw new FirebaseError( `Extension ID '${clc.bold( - 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); + subbedSpec.params = substituteParams( + extensionSpec.params || [], + AUTOPOPULATED_PARAM_PLACEHOLDERS, + ); validateSpec(subbedSpec); + return extensionSpec; +} - const consent = await confirmExtensionVersion(publisherId, extensionId, extensionSpec.version); - if (!consent) { - return; +/** + * 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(rootDirectory); + notes = changes[newVersion]; + } catch (err) { + throw new FirebaseError( + "No CHANGELOG.md file found. " + + "Please create one and add an entry for this version. " + + "See https://firebase.google.com/docs/extensions/publishers/user-documentation#writing-changelog for more details.", + ); } + // Notes are required for all stable versions after the initial release. + if (!notes && !semver.prerelease(newVersion) && extension) { + throw new FirebaseError( + `No entry for version ${newVersion} found in CHANGELOG.md. ` + + "Please add one so users know what has changed in this version. " + + "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.blueBright("New"))}\n`, + ); + } +} - let extension; +/** + * 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 { - extension = await getExtension(`${publisherId}/${extensionId}`); + const response = await fetch(archiveUri); + if (response.ok) { + await response.body.pipe(createUnzipTransform(tempDirectory.name)).promise(); + } } catch (err) { - // Silently fail and continue the publish flow if extension not found. + 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) { + throw new FirebaseError( + `Failed to find ${clc.bold(EXTENSIONS_SPEC_FILE)} in directory ${clc.bold( + extensionRoot, + )}. Please verify the root and try again.`, + ); + } + return rootDirectory; +} - if ( - extension && - extension.latestVersion && - semver.lt(extensionSpec.version, extension.latestVersion) - ) { - // publisher's version is less than current latest version. +/** + * Uploads an extension version from a GitHub repo. + * @param args.publisherId the ID of the Publisher this Extension will be published under + * @param args.extensionId the ID of the Extension to be published + * @param args.repoUri the URI of the repo where this Extension's source exists + * @param args.sourceRef the commit hash, branch, or tag name in the repo to publish from + * @param args.extensionRoot the root directory that contains this Extension's source + * @param args.stage the release stage to publish + * @param args.nonInteractive whether to display prompts + * @param args.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) { + // 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( - `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( - `${publisherId}/${extensionId}` - )}'. Please make sure this version is greater than the current version (${clc.bold( - extension.latestVersion - )}) inside of extension.yaml.\n` + `--stage only supports the following values: ${stageOptions.join(", ")}`, ); - } else if ( - extension && - extension.latestVersion && - semver.eq(extensionSpec.version, extension.latestVersion) - ) { - // publisher's version is equal to the current latest version. + } + + // 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 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, + }); + } + + 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({ + message: "Continue?", + nonInteractive: args.nonInteractive, + force: args.force, + default: false, + }); + if (!confirmed) { + return; + } + + // 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 args.publisherId the ID of the Publisher this Extension will be published under + * @param args.extensionId the ID of the Extension to be published + * @param args.rootDirectory the root directory that contains this Extension's source + * @param args.stage the release stage to publish + * @param args.nonInteractive whether to display prompts + * @param args.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) { + // 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( - `${publisherId}/${extensionId}` - )}'. Please increment the version inside of extension.yaml.\n` + `--stage only supports the following values when used with --local: ${localStageOptions.join( + ", ", + )}`, ); } - const ref = `${publisherId}/${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({ + message: "Continue?", + 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(rootDirectory, EXTENSIONS_BUCKET_NAME); - uploadSpinner.succeed(" Uploaded extension source code"); - packageUri = storageOrigin + objectPath + "?alt=media"; - } catch (err) { + objectPath = await archiveAndUploadSource(args.rootDirectory, EXTENSIONS_BUCKET_NAME); + 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( - 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; } @@ -466,6 +1029,14 @@ export async function publishExtensionVersionFromLocalSource( return res; } +export function getMissingPublisherError(publisherId: string): FirebaseError { + return new FirebaseError( + `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. @@ -474,31 +1045,34 @@ export async function publishExtensionVersionFromLocalSource( */ export async function createSourceFromLocation( projectId: string, - sourceUri: string + sourceUri: string, ): Promise { + const extensionRoot = "/"; let packageUri: string; - let extensionRoot: string; let objectPath = ""; - if (!urlRegex.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; } /** @@ -516,87 +1090,75 @@ async function deleteUploadedSource(objectPath: string) { } /** - * 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 . - * - * @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 + * Displays the release notes and confirmation message for the extension to be uploaded. + * @param args.extensionRef the ref of the extension + * @param args.newVersion the new version of the extension + * @param args.autoReview the autoreview + * @param args.releaseNotes the release notes for the version being uploaded (if any) + * @param args.sourceUri the source URI from which the extension will be uploaded */ -export async function confirmExtensionVersion( - publisherId: string, - extensionId: string, - versionId: string -): Promise { +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.\n\n` + - "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. If you are a member of the Extensions EAP group, your published extensions will only be accessible to other members of the EAP group.\n\n" + - "Do you wish to continue?"; - return await promptOnce({ - type: "confirm", - message, - default: false, // Force users to explicitly type 'yes' - }); + `\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); } -/** - * Display list of all official extensions and prompt user to select one. - * @param message The prompt message to display - * @return Promise that resolves to the extension name (e.g. storage-resize-images) - */ -export async function promptForOfficialExtension(message: string): Promise { - const officialExts = await getExtensionRegistry(true); - return await promptOnce({ - name: "input", - type: "list", - message, - choices: convertOfficialExtensionsToList(officialExts), - pageSize: _.size(officialExts), - }); -} +// TODO(inlined): Fix prompt library so that a choices array doesn't assume all values +// must be the same type as the literal of the first +type repeateInstanceResponse = "updateExisting" | "installNew" | "cancel"; /** * Confirm if the user wants to install another instance of an extension when they already have one. - * @param extensionName The name of the extension being installed. * @param projectName The name of the project in use. + * @param extensionName The name of the extension being installed. */ export async function promptForRepeatInstance( projectName: string, - extensionName: string -): Promise { - const message = - `An extension with the ID '${clc.bold( - extensionName - )}' already exists in the project '${clc.bold(projectName)}'.\n` + - `Do you want to proceed with installing another instance of extension '${clc.bold( - extensionName - )}' in this project?`; - return await promptOnce({ - type: "confirm", + extensionName: string, +): Promise { + const message = `An extension with the ID '${clc.bold( + extensionName, + )}' already exists in the project '${clc.bold(projectName)}'. What would you like to do?`; + return await select({ message, + choices: [ + { name: "Update or reconfigure the existing instance", value: "updateExisting" }, + { name: "Install a new instance with a different ID", value: "installNew" }, + { name: "Cancel extension installation", value: "cancel" }, + ], }); } @@ -606,58 +1168,66 @@ 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.message}`; + throw new FirebaseError(msg, { + original: err, + }); + } else { + throw err; + } } return true; } +export function isUrlPath(extInstallPath: string): boolean { + return extInstallPath.startsWith("https:"); +} + +export function isLocalPath(extInstallPath: string): boolean { + const trimmedPath = extInstallPath.trim(); + return ( + trimmedPath.startsWith(`~${path.sep}`) || + trimmedPath.startsWith(`.${path.sep}`) || + trimmedPath.startsWith(`..${path.sep}`) || + trimmedPath.startsWith(`${path.sep}`) || + // Windows generally supports both forward and back slashes (even though the path.sep is \, so always check) + // https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#canonicalize-separators + trimmedPath.startsWith(`~/`) || + trimmedPath.startsWith(`./`) || + trimmedPath.startsWith(`../`) || + trimmedPath.startsWith(`/`) || + [".", ".."].includes(trimmedPath) + ); +} + +export function isLocalOrURLPath(extInstallPath: string): boolean { + return isLocalPath(extInstallPath) || isUrlPath(extInstallPath); +} + /** * Given an update source, return where the update source came from. * @param sourceOrVersion path to a source or reference to a source version */ -export async function getSourceOrigin(sourceOrVersion: string): Promise { - if (!sourceOrVersion) { - return SourceOrigin.OFFICIAL_EXTENSION; - } - - // NOTE: If a semver is passed in, we automatically asssume it is an official extension version. - // If this was meant to be an extension from the Registry, please pass in the full reference instead. - // This is just an interim solution - when official extensions are migrated to use the Registry, the - // SourceOrigin types will be the same, and we won't have to worry about this nuance. - if (semver.valid(sourceOrVersion)) { - return SourceOrigin.OFFICIAL_EXTENSION_VERSION; - } - // First, check if the input matches a local or URL first. - if (fs.existsSync(sourceOrVersion)) { +export function getSourceOrigin(sourceOrVersion: string): SourceOrigin { + // First, check if the input matches a local or URL. + if (isLocalPath(sourceOrVersion)) { return SourceOrigin.LOCAL; } - if (urlRegex.test(sourceOrVersion)) { + if (isUrlPath(sourceOrVersion)) { return SourceOrigin.URL; } - // Next, check if the source matches an extension in the official extensions registry (registry.json). - try { - await resolveRegistryEntry(sourceOrVersion); - return SourceOrigin.OFFICIAL_EXTENSION; - } catch { - // Silently fail. - } // Next, check if the source is an extension reference. if (sourceOrVersion.includes("/")) { let ref; try { - ref = parseRef(sourceOrVersion); + ref = refs.parse(sourceOrVersion); } catch (err) { // Silently fail. } @@ -669,18 +1239,18 @@ export async function getSourceOrigin(sourceOrVersion: string): Promise { - const message = `Would you like to continue installing this extension?`; - return await promptOnce({ - type: "confirm", - message, - }); +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/extensions/listExtensions.spec.ts b/src/extensions/listExtensions.spec.ts new file mode 100644 index 00000000000..0d50ad60c15 --- /dev/null +++ b/src/extensions/listExtensions.spec.ts @@ -0,0 +1,93 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import * as extensionsApi from "./extensionsApi"; +import { listExtensions } from "./listExtensions"; + +const MOCK_INSTANCES = [ + { + name: "projects/my-test-proj/instances/image-resizer", + createTime: "2019-05-19T00:20:10.416947Z", + updateTime: "2019-05-19T00:20:10.416947Z", + state: "ACTIVE", + config: { + extensionRef: "firebase/image-resizer", + name: "projects/my-test-proj/instances/image-resizer/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", + createTime: "2019-05-19T00:20:10.416947Z", + source: { + state: "ACTIVE", + spec: { + version: "0.1.0", + author: { + authorName: "Firebase", + url: "https://firebase.google.com", + }, + }, + }, + }, + }, + { + name: "projects/my-test-proj/instances/image-resizer-1", + createTime: "2019-06-19T00:20:10.416947Z", + updateTime: "2019-06-19T00:21:06.722782Z", + state: "ACTIVE", + config: { + extensionRef: "firebase/image-resizer", + name: "projects/my-test-proj/instances/image-resizer-1/configurations/5b1fb749-764d-4bd1-af60-bb7f22d27860", + createTime: "2019-06-19T00:21:06.722782Z", + source: { + spec: { + version: "0.1.0", + }, + }, + }, + }, +]; + +const PROJECT_ID = "my-test-proj"; + +describe("listExtensions", () => { + let listInstancesStub: sinon.SinonStub; + + beforeEach(() => { + listInstancesStub = sinon.stub(extensionsApi, "listInstances"); + }); + + afterEach(() => { + listInstancesStub.restore(); + }); + + it("should return an empty array if no extensions have been installed", async () => { + listInstancesStub.returns(Promise.resolve([])); + + const result = await listExtensions(PROJECT_ID); + + expect(result).to.eql([]); + }); + + it("should return a sorted array of extension instances", async () => { + listInstancesStub.returns(Promise.resolve(MOCK_INSTANCES)); + + const result = await listExtensions(PROJECT_ID); + + const expected = [ + { + extension: "firebase/image-resizer", + instanceId: "image-resizer-1", + publisher: "firebase", + state: "ACTIVE", + updateTime: "2019-06-19 00:21:06", + version: "0.1.0", + }, + { + extension: "firebase/image-resizer", + instanceId: "image-resizer", + publisher: "firebase", + state: "ACTIVE", + updateTime: "2019-05-19 00:20:10", + version: "0.1.0", + }, + ]; + expect(result).to.eql(expected); + }); +}); diff --git a/src/extensions/listExtensions.ts b/src/extensions/listExtensions.ts index 17fffc8f684..fe4140d39fc 100644 --- a/src/extensions/listExtensions.ts +++ b/src/extensions/listExtensions.ts @@ -1,53 +1,63 @@ -import * as _ from "lodash"; -import * as clc from "cli-color"; -import Table = require("cli-table"); +import * as clc from "colorette"; +import * as Table from "cli-table3"; -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<{ instances: ExtensionInstance[] }> { +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 { instances: [] }; + return []; } const table = new Table({ - head: ["Extension", "Author", "Instance ID", "State", "Version", "Your last update"], + head: ["Extension", "Publisher", "Instance ID", "State", "Version", "Your last update"], 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]; } - table.push([ - extension, - _.get(instance, "config.source.spec.author.authorName", ""), - _.last(instance.name.split("/")), + const instanceId = last(instance.name.split("/")) ?? ""; + const state = instance.state + - (_.get(instance, "config.source.state", "ACTIVE") === "DELETED" ? " (UNPUBLISHED)" : ""), - _.get(instance, "config.source.spec.version", ""), - extensionsUtils.formatTimestamp(instance.updateTime), - ]); + ((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]); + formatted.push({ + extension, + publisher, + instanceId, + state, + version, + updateTime, + }); }); - 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 { instances: sorted }; + return formatted; } diff --git a/src/extensions/localHelper.spec.ts b/src/extensions/localHelper.spec.ts new file mode 100644 index 00000000000..a579b19b6a5 --- /dev/null +++ b/src/extensions/localHelper.spec.ts @@ -0,0 +1,95 @@ +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"; +import { FIXTURE_DIR as EXT_INVALID_SPEC } from "../test/fixtures/extension-yamls/valid-yaml-invalid-spec"; + +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 validate that the yaml is a valid extension spec", async () => { + await expect(localHelper.getLocalExtensionSpec(EXT_INVALID_SPEC)).to.be.rejectedWith( + FirebaseError, + /.+Resources field must contain at least one resource/, + ); + }); + + 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..1717d59c2a9 100644 --- a/src/extensions/localHelper.ts +++ b/src/extensions/localHelper.ts @@ -1,13 +1,14 @@ 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 { FirebaseError, isObject } from "../error"; +import { ExtensionSpec, isExtensionSpec, LifecycleEvent, LifecycleStage } from "./types"; import { logger } from "../logger"; +import { validateSpec } from "./extensionsHelper"; -const EXTENSIONS_SPEC_FILE = "extension.yaml"; +export const EXTENSIONS_SPEC_FILE = "extension.yaml"; const EXTENSIONS_PREINSTALL_FILE = "PREINSTALL.md"; /** @@ -16,6 +17,18 @@ const EXTENSIONS_PREINSTALL_FILE = "PREINSTALL.md"; */ export async function getLocalExtensionSpec(directory: string): Promise { const spec = await parseYAML(readFile(path.resolve(directory, EXTENSIONS_SPEC_FILE))); + + // lifecycleEvents are formatted differently once they have been uploaded + if (spec.lifecycleEvents as Object) { + spec.lifecycleEvents = fixLifecycleEvents(spec.lifecycleEvents); + } + + if (!isExtensionSpec(spec)) { + validateSpec(spec); // Maybe throw with more details + throw new FirebaseError( + "Error: extension.yaml does not contain a valid extension specification.", + ); + } try { const preinstall = readFile(path.resolve(directory, EXTENSIONS_PREINSTALL_FILE)); spec.preinstallContent = preinstall; @@ -25,6 +38,31 @@ export async function getLocalExtensionSpec(directory: string): Promise = { + onInstall: "ON_INSTALL", + onUpdate: "ON_UPDATE", + onConfigure: "ON_CONFIGURE", + stageUnspecified: "STAGE_UNSPECIFIED", + }; + const arrayLifecycle = [] as LifecycleEvent[]; + if (isObject(lifecycleEvents)) { + for (const [key, val] of Object.entries(lifecycleEvents)) { + if ( + isObject(val) && + typeof val.function === "string" && + typeof val.processingMessage === "string" + ) { + arrayLifecycle.push({ + stage: stages[key] || stages["stageUnspecified"], + taskQueueTriggerFunction: val.function, + }); + } + } + } + return arrayLifecycle; +} + /** * Climbs directories loking for an extension.yaml file, and return the first * directory that contains one. Throws an error if none is found. @@ -35,7 +73,7 @@ export function findExtensionYaml(directory: string): string { const parentDir = path.dirname(directory); if (parentDir === directory) { throw new FirebaseError( - "Couldn't find an extension.yaml file. Check that you are in the root directory of your extension." + "Couldn't find an extension.yaml file. Check that you are in the root directory of your extension.", ); } directory = parentDir; @@ -45,15 +83,14 @@ export function findExtensionYaml(directory: string): string { /** * Retrieves a file from the directory. - * @param directory the directory containing the file - * @param file the name of the file + * @param pathToFile the path to the file to read */ export function readFile(pathToFile: string): string { try { return fs.readFileSync(pathToFile, "utf8"); - } catch (err) { + } catch (err: any) { if (err.code === "ENOENT") { - throw new FirebaseError(`Could not find "${pathToFile}""`, { original: err }); + throw new FirebaseError(`Could not find "${pathToFile}"`, { original: err }); } throw new FirebaseError(`Failed to read file at "${pathToFile}"`, { original: err }); } @@ -66,22 +103,22 @@ export function readFile(pathToFile: string): string { export function isLocalExtension(extensionName: string): boolean { try { fs.readdirSync(extensionName); - } catch (err) { + } catch (err: any) { return false; } return true; } /** - * Wraps `yaml.safeLoad` with an error handler to present better YAML parsing + * Wraps `yaml.parse` with an error handler to present better YAML parsing * errors. * @param source an unparsed YAML string */ function parseYAML(source: string): any { try { - return yaml.safeLoad(source); - } catch (err) { - if (err instanceof yaml.YAMLException) { + return yaml.parse(source); + } catch (err: any) { + if (err instanceof yaml.YAMLParseError) { throw new FirebaseError(`YAML Error: ${err.message}`, { original: err }); } throw new FirebaseError(err.message); diff --git a/src/extensions/manifest.spec.ts b/src/extensions/manifest.spec.ts new file mode 100644 index 00000000000..c0fc8081da6 --- /dev/null +++ b/src/extensions/manifest.spec.ts @@ -0,0 +1,912 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import * as manifest from "./manifest"; +import * as paramHelper from "./paramHelper"; +import * as refs from "./refs"; + +import { Config } from "../config"; +import * as prompt from "../prompt"; +import { FirebaseError } from "../error"; +import { ParamType } from "./types"; + +/** + * Returns a base Config with some extensions data. + * + * The inner content cannot be a constant because Config edits in-place and mutates + * the state between tests. + */ +function generateBaseConfig(): Config { + return new Config( + { + extensions: { + "delete-user-data": "firebase/delete-user-data@0.1.12", + "delete-user-data-gm2h": "firebase/delete-user-data@0.1.12", + }, + }, + {}, + ); +} +function generateConfigWithLocal(): Config { + return new Config( + { + extensions: { + "delete-user-data": "firebase/delete-user-data@0.1.12", + "delete-user-data-gm2h": "firebase/delete-user-data@0.1.12", + "delete-user-data-local": "./delete-user-data", + }, + }, + {}, + ); +} + +describe("manifest", () => { + 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, "select").resolves("overwrite"); + sandbox.stub(prompt, "confirm").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, "select").resolves("overwrite"); + sandbox.stub(prompt, "confirm").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..980eb69c02c --- /dev/null +++ b/src/extensions/manifest.ts @@ -0,0 +1,288 @@ +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, select } from "../prompt"; +import { readEnvFile } from "./paramHelper"; +import { FirebaseError } from "../error"; +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 select({ + 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); +} + +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..bed36e9cc2a --- /dev/null +++ b/src/extensions/paramHelper.spec.ts @@ -0,0 +1,366 @@ +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 promptImport 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 prompt: sinon.SinonStubbedInstance; + + beforeEach(() => { + sinon.stub(fs, "readFileSync").returns(""); + sinon.stub(extensionsHelper, "getFirebaseProjectParams").resolves({ PROJECT_ID }); + prompt = sinon.stub(promptImport); + prompt.input.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(prompt.input).to.have.been.calledTwice; + expect(prompt.input.firstCall.args[0]).to.eql({ + default: undefined, + message: "Enter a value for Param:", + }); + expect(prompt.input.secondCall.args[0]).to.eql({ + default: "default", + message: "Enter a value for Another Param:", + }); + }); + }); + + describe("promptForNewParams", () => { + let prompt: sinon.SinonStubbedInstance; + + beforeEach(() => { + prompt = sinon.stub(promptImport); + prompt.input.rejects("Unexpected input call"); + prompt.confirm.rejects("Unexpected confirm call"); + prompt.select.rejects("Unexpected select call"); + 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 () => { + prompt.input.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(prompt.input).to.have.been.called.calledTwice; + expect(prompt.input.firstCall.args).to.eql([ + { + default: "test-proj", + message: "Enter a value for New Param:", + }, + ]); + expect(prompt.input.secondCall.args).to.eql([ + { + default: "default", + message: "Enter a value for 3:", + }, + ]); + }); + + it("should prompt for params that are not currently populated", async () => { + prompt.input.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 () => { + 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(prompt.input).not.to.have.been.called; + }); + + it("should not prompt the user for params that did not change type or param", async () => { + 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(prompt.input).not.to.have.been.called; + }); + + it("should populate the spec with the default value if it is returned by prompt", async () => { + prompt.input.onFirstCall().resolves("test-proj"); + prompt.input.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(prompt.input).to.be.calledTwice; + expect(prompt.input.firstCall.args).to.eql([ + { + default: "test-proj", + message: "Enter a value for New Param:", + }, + ]); + expect(prompt.input.secondCall.args).to.eql([ + { + default: "default", + message: "Enter a value for 3:", + }, + ]); + }); + + it("shouldn't prompt if there are no new params", async () => { + 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(prompt.input).not.to.have.been.called; + }); + + it("should exit if a prompt fails", async () => { + prompt.input.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(prompt.input).to.have.been.calledOnce; + }); + }); +}); diff --git a/src/extensions/paramHelper.ts b/src/extensions/paramHelper.ts index f63ab3d8bf0..3e4e23b9d80 100644 --- a/src/extensions/paramHelper.ts +++ b/src/extensions/paramHelper.ts @@ -1,20 +1,56 @@ -import * as _ from "lodash"; import * as path from "path"; -import * as clc from "cli-color"; -import * as dotenv from "dotenv"; +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. @@ -23,66 +59,72 @@ import * as track from "../track"; * @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 * @param envFilePath a path to an env file containing param values * @throws FirebaseError if an invalid env file is passed in */ -export async function getParams( - projectId: string, - paramSpecs: extensionsApi.Param[], - envFilePath?: string -): Promise<{ [key: string]: string }> { - let commandLineParams; - if (envFilePath) { - try { - const buf = fs.readFileSync(path.resolve(envFilePath), "utf8"); - commandLineParams = dotenv.parse(buf.toString().trim(), { debug: true }); - track("Extension Env File", "Present"); - } catch (err) { - track("Extension Env File", "Invalid"); - throw new FirebaseError(`Error reading env file: ${err.message}\n`, { original: err }); - } +export async function getParams(args: { + projectId?: string; + instanceId: string; + paramSpecs: Param[]; + nonInteractive?: boolean; + reconfiguring?: boolean; +}): Promise> { + let params: Record; + if (args.nonInteractive) { + throw new FirebaseError(NONINTERACTIVE_ERROR_MESSAGE); } else { - track("Extension Env File", "Not Present"); + const firebaseProjectParams = await getFirebaseProjectParams(args.projectId); + params = await askUserForParam.ask({ + projectId: args.projectId, + instanceId: args.instanceId, + paramSpecs: args.paramSpecs, + firebaseProjectParams, + reconfiguring: !!args.reconfiguring, + }); } - const firebaseProjectParams = await getFirebaseProjectParams(projectId); - let params: any; - if (commandLineParams) { - params = populateDefaultParams(commandLineParams, paramSpecs); - validateCommandLineParams(params, paramSpecs); + return params; +} + +export async function getParamsForUpdate(args: { + spec: ExtensionSpec; + newSpec: ExtensionSpec; + currentParams: { [option: string]: string }; + projectId?: string; + nonInteractive?: boolean; + instanceId: string; +}): Promise> { + let params: Record; + if (args.nonInteractive) { + throw new FirebaseError(NONINTERACTIVE_ERROR_MESSAGE); } else { - params = await askUserForParam.ask(paramSpecs, firebaseProjectParams); + params = await promptForNewParams({ + spec: args.spec, + newSpec: args.newSpec, + currentParams: args.currentParams, + projectId: args.projectId, + instanceId: args.instanceId, + }); } - track("Extension Params", _.isEmpty(params) ? "Not Present" : "Present", _.size(params)); return params; } @@ -94,46 +136,106 @@ export async function getParams( * @param newSpec A extensionSpec to compare to * @param currentParams A set of current params and their values */ -export async function promptForNewParams( - spec: extensionsApi.ExtensionSpec, - newSpec: extensionsApi.ExtensionSpec, - currentParams: { [option: string]: string }, - projectId: string -): Promise { - const firebaseProjectParams = await getFirebaseProjectParams(projectId); - const comparer = (param1: extensionsApi.Param, param2: extensionsApi.Param) => { +export async function promptForNewParams(args: { + spec: ExtensionSpec; + newSpec: ExtensionSpec; + currentParams: { [option: string]: string }; + projectId?: string; + instanceId: string; +}): Promise<{ [option: string]: ParamBindingOptions }> { + const newParamBindingOptions = buildBindingOptionsWithBaseValue(args.currentParams); + + const firebaseProjectParams = await getFirebaseProjectParams(args.projectId); + const sameParam = (param1: Param) => (param2: Param) => { return param1.type === param2.type && param1.param === param2.param; }; - let paramsDiffDeletions = _.differenceWith(spec.params, _.get(newSpec, "params", []), comparer); - paramsDiffDeletions = substituteParams(paramsDiffDeletions, firebaseProjectParams); + 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) ?? [], + ); + let combinedNewParams = args.newSpec.params.concat( + args.newSpec.systemParams.filter((p) => !p.advanced) ?? [], + ); + + // 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 = _.differenceWith(newSpec.params, _.get(spec, "params", []), comparer); - paramsDiffAdditions = substituteParams(paramsDiffAdditions, 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) => { - logger.info(clc.red(`- ${param.param}: ${currentParams[param.param.toUpperCase()]}`)); - delete currentParams[param.param.toUpperCase()]; - }); + for (const param of paramsDiffDeletions) { + logger.info(clc.red(`- ${param.param}: ${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(param); - currentParams[param.param] = chosenValue; + const chosenValue = await askUserForParam.askForParam({ + projectId: args.projectId, + instanceId: args.instanceId, + paramSpec: param, + reconfiguring: false, + }); + newParamBindingOptions[param.param] = chosenValue; } } - return currentParams; + + return newParamBindingOptions; } -export function readParamsFile(envFilePath: string): any { - try { - const buf = fs.readFileSync(path.resolve(envFilePath), "utf8"); - return dotenv.parse(buf.toString().trim(), { debug: true }); - } catch (err) { - throw new FirebaseError(`Error reading --test-params file: ${err.message}\n`, { - original: err, - }); +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", + )}`, + ); + } + return result.envs; +} + +export function isSystemParam(paramName: string): boolean { + const regex = /^firebaseextensions\.[a-zA-Z0-9\.]*\//; + return regex.test(paramName); +} + +// Populate default values for missing params. +// This is only needed when emulating extensions - when deploying, this is handled in the back end. +export function populateDefaultParams( + params: Record, + spec: ExtensionSpec, +): Record { + const ret = { ...params }; + for (const p of spec.params) { + ret[p.param] = ret[p.param] ?? p.default; } + return ret; } diff --git a/src/extensions/provisioningHelper.spec.ts b/src/extensions/provisioningHelper.spec.ts new file mode 100644 index 00000000000..df36f2dc7f8 --- /dev/null +++ b/src/extensions/provisioningHelper.spec.ts @@ -0,0 +1,345 @@ +import * as nock from "nock"; +import { expect } from "chai"; + +import * as api from "../api"; +import * as provisioningHelper from "./provisioningHelper"; +import { Api, ExtensionSpec, Resource, Role } from "./types"; +import { FirebaseError } from "../error"; + +const PROJECT_ID = "test-project"; + +const SPEC_WITH_NOTHING = { + apis: [] as Api[], + resources: [] as Resource[], +} as ExtensionSpec; + +const SPEC_WITH_STORAGE = { + apis: [ + { + apiName: "storage-component.googleapis.com", + }, + ] as Api[], + resources: [] as Resource[], +} as ExtensionSpec; + +const SPEC_WITH_AUTH = { + apis: [ + { + apiName: "identitytoolkit.googleapis.com", + }, + ] as Api[], + resources: [] as Resource[], +} as ExtensionSpec; + +const SPEC_WITH_STORAGE_AND_AUTH = { + apis: [ + { + apiName: "storage-component.googleapis.com", + }, + { + apiName: "identitytoolkit.googleapis.com", + }, + ] as Api[], + resources: [] as Resource[], +} as ExtensionSpec; + +const FIREDATA_AUTH_ACTIVATED_RESPONSE = { + activation: [ + { + service: "FIREBASE_AUTH", + }, + ], +}; + +const FIREBASE_STORAGE_DEFAULT_BUCKET_LINKED_RESPONSE = { + buckets: [ + { + name: `projects/12345/bucket/${PROJECT_ID}.appspot.com`, + }, + ], +}; + +const extensionVersionResponse = (version: string, spec: ExtensionSpec) => { + return { + name: `publishers/test/extensions/test/version/${version}`, + ref: `test/test@${version}`, + hash: "abc", + sourceDownloadUri: "https://firebase.com", + spec, + }; +}; + +const instanceSpec = (version: string) => { + return { + instanceId: "test", + params: {}, + ref: { + publisherId: "test", + extensionId: "test", + version, + }, + }; +}; + +describe("provisioningHelper", () => { + afterEach(() => { + nock.cleanAll(); + }); + + describe("getUsedProducts", () => { + let testSpec: ExtensionSpec; + + beforeEach(() => { + testSpec = { + apis: [ + { + apiName: "unrelated.googleapis.com", + }, + ] as Api[], + roles: [ + { + role: "unrelated.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 Resource[], + } as ExtensionSpec; + }); + + it("returns empty array when nothing is used", () => { + expect(provisioningHelper.getUsedProducts(testSpec)).to.be.empty; + }); + + it("returns STORAGE when Storage API is used", () => { + testSpec.apis?.push({ + apiName: "storage-component.googleapis.com", + reason: "whatever", + }); + expect(provisioningHelper.getUsedProducts(testSpec)).to.be.deep.eq([ + provisioningHelper.DeferredProduct.STORAGE, + ]); + }); + + it("returns STORAGE when Storage Role is used", () => { + testSpec.roles?.push({ + role: "storage.object.admin", + reason: "whatever", + }); + expect(provisioningHelper.getUsedProducts(testSpec)).to.be.deep.eq([ + provisioningHelper.DeferredProduct.STORAGE, + ]); + }); + + it("returns STORAGE when Storage trigger is used", () => { + 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 Resource); + expect(provisioningHelper.getUsedProducts(testSpec)).to.be.deep.eq([ + provisioningHelper.DeferredProduct.STORAGE, + ]); + }); + + it("returns AUTH when Authentication API is used", () => { + testSpec.apis?.push({ + apiName: "identitytoolkit.googleapis.com", + reason: "whatever", + }); + expect(provisioningHelper.getUsedProducts(testSpec)).to.be.deep.eq([ + provisioningHelper.DeferredProduct.AUTH, + ]); + }); + + it("returns AUTH when Authentication Role is used", () => { + testSpec.roles?.push({ + role: "firebaseauth.user.admin", + reason: "whatever", + }); + expect(provisioningHelper.getUsedProducts(testSpec)).to.be.deep.eq([ + provisioningHelper.DeferredProduct.AUTH, + ]); + }); + + it("returns AUTH when Auth trigger is used", () => { + 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 Resource); + expect(provisioningHelper.getUsedProducts(testSpec)).to.be.deep.eq([ + provisioningHelper.DeferredProduct.AUTH, + ]); + }); + }); + + describe("checkProductsProvisioned", () => { + it("passes provisioning check status when nothing is used", async () => { + await expect( + provisioningHelper.checkProductsProvisioned(PROJECT_ID, { + resources: [] as Resource[], + } as ExtensionSpec), + ).to.be.fulfilled; + }); + + it("passes provisioning check when all is provisioned", async () => { + 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, FIREBASE_STORAGE_DEFAULT_BUCKET_LINKED_RESPONSE); + + await expect( + 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()) + .get(`/v1/projects/${PROJECT_ID}/products`) + .reply(200, FIREDATA_AUTH_ACTIVATED_RESPONSE); + nock(api.firebaseStorageOrigin()) + .get(`/v1beta/projects/${PROJECT_ID}/buckets`) + .reply(200, { + buckets: [ + { + name: `projects/12345/bucket/some-other-bucket`, + }, + ], + }); + + await expect( + 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()) + .get(`/v1/projects/${PROJECT_ID}/products`) + .reply(200, FIREDATA_AUTH_ACTIVATED_RESPONSE); + nock(api.firebaseStorageOrigin()) + .get(`/v1beta/projects/${PROJECT_ID}/buckets`) + .reply(200, {}); + + await expect( + 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()) + .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), + ).to.be.rejectedWith( + FirebaseError, + "Firebase Authentication: authenticate and manage users from", + ); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("bulkCheckProductsProvisioned", () => { + it("passes provisioning check status when nothing is used", async () => { + 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")]), + ).to.be.fulfilled; + }); + + it("passes provisioning check when all is provisioned", async () => { + 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()) + .get(`/v1/projects/${PROJECT_ID}/products`) + .reply(200, FIREDATA_AUTH_ACTIVATED_RESPONSE); + 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")]), + ).to.be.fulfilled; + + expect(nock.isDone()).to.be.true; + }); + + it("checks all products for multiple versions", async () => { + 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()) + .get(`/v1beta/publishers/test/extensions/test/versions/0.1.1`) + .reply(200, extensionVersionResponse("0.1.1", SPEC_WITH_AUTH)); + 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, FIREBASE_STORAGE_DEFAULT_BUCKET_LINKED_RESPONSE); + + await expect( + 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()) + .get(`/v1beta/publishers/test/extensions/test/versions/0.1.0`) + .reply(200, extensionVersionResponse("0.1.0", SPEC_WITH_STORAGE)); + nock(api.firebaseStorageOrigin()) + .get(`/v1beta/projects/${PROJECT_ID}/buckets`) + .reply(200, { + buckets: [ + { + name: `projects/12345/bucket/some-other-bucket`, + }, + ], + }); + + await expect( + 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()) + .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, {}); + + await expect( + provisioningHelper.bulkCheckProductsProvisioned(PROJECT_ID, [instanceSpec("0.1.0")]), + ).to.be.rejectedWith( + FirebaseError, + "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 new file mode 100644 index 00000000000..01814197da8 --- /dev/null +++ b/src/extensions/provisioningHelper.ts @@ -0,0 +1,144 @@ +import { marked } from "marked"; + +import { ExtensionSpec } from "./types"; +import { firebaseStorageOrigin, firedataOrigin } from "../api"; +import { Client } from "../apiv2"; +import { flattenArray } from "../functional"; +import { FirebaseError } from "../error"; +import { getExtensionSpec, InstanceSpec } from "../deploy/extensions/planner"; +import { logger } from "../logger"; + +/** Product for which provisioning can be (or is) deferred */ +export enum DeferredProduct { + STORAGE, + AUTH, +} + +/** + * Checks whether products used by the extension require provisioning. + * + * @param spec extension spec + */ +export async function checkProductsProvisioned( + projectId: string, + spec: ExtensionSpec, +): Promise { + const usedProducts = getUsedProducts(spec); + await checkProducts(projectId, usedProducts); +} + +/** + * Checks whether products used for any extension version in a deploy requires provisioning. + * + * @param extensionVersionRefs + */ +export async function bulkCheckProductsProvisioned( + projectId: string, + instanceSpecs: InstanceSpec[], +): Promise { + const usedProducts = await Promise.all( + instanceSpecs.map(async (i) => { + const extensionSpec = await getExtensionSpec(i); + return getUsedProducts(extensionSpec); + }), + ); + await checkProducts(projectId, [...flattenArray(usedProducts)]); +} + +async function checkProducts(projectId: string, usedProducts: DeferredProduct[]) { + const needProvisioning = [] as DeferredProduct[]; + let isStorageProvisionedPromise; + let isAuthProvisionedPromise; + if (usedProducts.includes(DeferredProduct.STORAGE)) { + isStorageProvisionedPromise = isStorageProvisioned(projectId); + } + if (usedProducts.includes(DeferredProduct.AUTH)) { + isAuthProvisionedPromise = isAuthProvisioned(projectId); + } + 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) { + let errorMessage = + "Some services used by this extension have not been set up on your " + + "Firebase project. To ensure this extension works as intended, you must enable these " + + "services by following the provided links, then retry this command\n\n"; + if (needProvisioning.includes(DeferredProduct.STORAGE)) { + errorMessage += + " - Firebase Storage: store and retrieve user-generated files like images, audio, and " + + "video without server-side code.\n"; + errorMessage += ` https://console.firebase.google.com/project/${projectId}/storage`; + errorMessage += "\n"; + } + if (needProvisioning.includes(DeferredProduct.AUTH)) { + errorMessage += + " - Firebase Authentication: authenticate and manage users from a variety of providers " + + "without server-side code.\n"; + errorMessage += ` https://console.firebase.google.com/project/${projectId}/authentication/users`; + } + throw new FirebaseError(await marked(errorMessage), { exit: 2 }); + } +} + +/** + * From the spec determines which products are used by the extension and + * returns the list. + */ +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]); + const usedTriggers = spec.resources.map((r) => getTriggerType(r.propertiesYaml)); + if ( + usedApis?.includes("storage-component.googleapis.com") || + usedRoles?.includes("storage") || + usedTriggers.find((t) => t?.startsWith("google.storage.")) + ) { + usedProducts.push(DeferredProduct.STORAGE); + } + if ( + usedApis?.includes("identitytoolkit.googleapis.com") || + usedRoles?.includes("firebaseauth") || + usedTriggers.find((t) => t?.startsWith("providers/firebase.auth/")) + ) { + usedProducts.push(DeferredProduct.AUTH); + } + return usedProducts; +} + +/** + * Parses out trigger eventType from the propertiesYaml. + */ +function getTriggerType(propertiesYaml: string | undefined) { + return propertiesYaml?.match(/eventType:\ ([\S]+)/)?.[1]; +} + +async function isStorageProvisioned(projectId: string): Promise { + 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 + // and we just need the BUCKET_NAME part. + const bucketResourceNameTokens = bucketResourceName.split("/"); + const pattern = "^" + projectId + "(.[[a-z0-9]+)*.(appspot.com|firebasestorage.app)$"; + return new RegExp(pattern).test(bucketResourceNameTokens[bucketResourceNameTokens.length - 1]); + }); +} + +async function isAuthProvisioned(projectId: string): Promise { + 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 new file mode 100644 index 00000000000..b5d0fddabb3 --- /dev/null +++ b/src/extensions/publishHelpers.ts @@ -0,0 +1,5 @@ +import { consoleOrigin } from "../api"; + +export function consoleInstallLink(extVersionRef: string): string { + 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 new file mode 100644 index 00000000000..ce2742cbb67 --- /dev/null +++ b/src/extensions/refs.ts @@ -0,0 +1,122 @@ +import * as semver from "semver"; + +import { FirebaseError } from "../error"; + +const refRegex = new RegExp(/^([^/@\n]+)\/{1}([^/@\n]+)(@{1}([^\n]+)|)$/); + +/** + * Ref is a type for converting between the various string representations of an extension or version. + */ +export interface Ref { + publisherId: string; + extensionId: string; + version?: string; +} + +/** + * Parse a extension ref or name into a Ref + * @param refOrName an extension or extension version + * ref (publisher/extension@version) + * or fully qualified name + */ +export function parse(refOrName: string): Ref { + const ret = parseRef(refOrName) || parseName(refOrName); + if (!ret || !ret.publisherId || !ret.extensionId) { + throw new FirebaseError( + `Unable to parse ${refOrName} as an extension ref.\n` + + "Expected format is either publisherId/extensionId@version or " + + "publishers/publisherId/extensions/extensionId/versions/version. If you " + + "are referring to a local extension directory, please ensure the directory exists.", + ); + } + if ( + ret.version && + !semver.valid(ret.version) && + !semver.validRange(ret.version) && + !["latest", "latest-approved"].includes(ret.version) + ) { + throw new FirebaseError( + `Extension reference ${JSON.stringify(ret, null, 2)} contains an invalid version ${ret.version}.`, + ); + } + return ret; +} + +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)) { + return { + publisherId: parts[1], + extensionId: parts[2], + version: parts[4], + }; + } +} + +function parseName(name: string): Ref | undefined { + const parts = name.split("/"); + if (parts[0] !== "publishers" || parts[2] !== "extensions") { + return; + } + if (parts.length === 4) { + return { + publisherId: parts[1], + extensionId: parts[3], + }; + } + if (parts.length === 6 && parts[4] === "versions") { + return { + publisherId: parts[1], + extensionId: parts[3], + version: parts[5], + }; + } +} + +/** + * To an extension ref: publisherId/extensionId + */ +export function toExtensionRef(ref: Ref): string { + return `${ref.publisherId}/${ref.extensionId}`; +} + +/** + * To an extension version ref: publisherId/extensionId@version + */ +export function toExtensionVersionRef(ref: Ref): string { + if (!ref.version) { + throw new FirebaseError(`Ref does not have a version`); + } + return `${ref.publisherId}/${ref.extensionId}@${ref.version}`; +} + +/** + * To a fully qualified extension name : publishers/publisherId/extensions/extensionId + */ +export function toExtensionName(ref: Ref): string { + return `publishers/${ref.publisherId}/extensions/${ref.extensionId}`; +} + +/** + * To a fully qualified extension version name : publishers/publisherId/extensions/extensionId/version/versionId + */ +export function toExtensionVersionName(ref: Ref): string { + if (!ref.version) { + throw new FirebaseError(`Ref does not have a version`); + } + return `publishers/${ref.publisherId}/extensions/${ref.extensionId}/versions/${ref.version}`; +} + +/** + * Checks if two refs refer to the same extensionVersion. + */ +export function equal(a?: Ref, b?: Ref): boolean { + return ( + !!a && + !!b && + a.publisherId === b.publisherId && + a.extensionId === b.extensionId && + a.version === b.version + ); +} diff --git a/src/extensions/resolveSource.ts b/src/extensions/resolveSource.ts deleted file mode 100644 index 2d7ad345ae0..00000000000 --- a/src/extensions/resolveSource.ts +++ /dev/null @@ -1,188 +0,0 @@ -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"; - -const EXTENSIONS_REGISTRY_ENDPOINT = "/extensions.json"; -const AUDIENCE_WARNING_MESSAGES: { [key: string]: string } = { - "open-alpha": marked( - `${clc.bold("Important")}: This extension is part of the ${clc.bold( - "preliminary-release program" - )} for extensions.\n Its functionality might change in backward-incompatible ways before its official release. Learn more: https://github.com/firebase/extensions/tree/master/.preliminary-release-extensions` - ), - "closed-alpha": marked( - `${clc.yellow.bold("Important")}: This extension is part of the ${clc.bold( - "Firebase Alpha program" - )}.\n This extension is strictly confidential, and its functionality might change in backward-incompatible ways before its official, public release. Learn more: https://dev-partners.googlesource.com/samples/firebase/extensions-alpha/+/refs/heads/master/README.md` - ), - experimental: 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. Learn more: https://github.com/FirebaseExtended/experimental-extensions` - ), -}; - -export interface RegistryEntry { - icons?: { [key: string]: string }; - labels: { [key: string]: string }; - versions: { [key: string]: string }; - updateWarnings?: { [key: string]: UpdateWarning[] }; - audience?: 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. - */ -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"]); -} - -/** - * Checks for and prompts the user to accept updateWarnings that apply to the given start and end versions. - * @param registryEntry the registry entry to check for updateWarnings. - * @param startVersion the version that you are updating from. - * @param endVersion the version you are updating to. - * @throws FirebaseError if the user doesn't accept the update warning prompt. - */ -export async function promptForUpdateWarnings( - registryEntry: RegistryEntry, - startVersion: string, - endVersion: string -): Promise { - if (registryEntry.updateWarnings) { - for (const targetRange in registryEntry.updateWarnings) { - if (semver.satisfies(endVersion, targetRange)) { - const updateWarnings = registryEntry.updateWarnings[targetRange]; - for (const updateWarning of updateWarnings) { - if (semver.satisfies(startVersion, updateWarning.from)) { - await module.exports.confirmUpdateWarning(updateWarning); - break; - } - } - break; - } - } - } -} - -/** - * Checks the audience field of a RegistryEntry, displays a warning text - * for closed and open alpha extensions, and prompts the user to accept. - */ -export async function promptForAudienceConsent(registryEntry: RegistryEntry): Promise { - let consent = true; - if (registryEntry.audience && AUDIENCE_WARNING_MESSAGES[registryEntry.audience]) { - logger.info(AUDIENCE_WARNING_MESSAGES[registryEntry.audience]); - consent = await promptOnce({ - type: "confirm", - message: "Do you acknowledge the status of this extension?", - default: true, - }); - } - return consent; -} - -/** - * Fetches the published extensions registry. - * @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 }; - - if (onlyFeatured) { - const featuredList = _.get(res, "body.featured.discover"); - return _.pickBy(extensions, (_entry, extensionName: string) => { - return _.includes(featuredList, extensionName); - }); - } - return extensions; -} diff --git a/src/extensions/runtimes/common.ts b/src/extensions/runtimes/common.ts new file mode 100644 index 00000000000..e4b878b5911 --- /dev/null +++ b/src/extensions/runtimes/common.ts @@ -0,0 +1,359 @@ +import * as fs from "fs"; +import * as path from "path"; +import { confirm } from "../../prompt"; +import * as fsutils from "../../fsutils"; +import { logLabeledBullet } from "../../utils"; +import { FirebaseError, getErrMsg } from "../../error"; +import { Options } from "../../options"; +import { + DEFAULT_CODEBASE, + configForCodebase, + normalizeAndValidate, + requireLocal, +} from "../../functions/projectConfig"; +import { Build, DynamicExtension } from "../../deploy/functions/build"; +import { EndpointFilter as Filter } from "../../deploy/functions/functionsDeployHelper"; +import { ExtensionSpec } from "../types"; +import * as functionRuntimes from "../../deploy/functions/runtimes"; +import * as nodeRuntime from "./node"; + +export { DynamicExtension } from "../../deploy/functions/build"; + +/** + * Fixes unreadable dark blue on black background to be cyan + * @param txt The formatted text containing color codes + * @return The formatted text with blue replaced by cyan. + */ +export function fixDarkBlueText(txt: string): string { + // default hyperlinks etc. are not readable on black. + const DARK_BLUE = "\u001b[34m"; + const BRIGHT_CYAN = "\u001b[36;1m"; + return txt.replaceAll(DARK_BLUE, BRIGHT_CYAN); +} + +/** + * Extracts extensions from build records + * @param builds The builds to examine + * @param filters The filters to use + * @return a record of extensions by extensionId + */ +export function extractExtensionsFromBuilds( + builds: Record, + filters?: Filter[], +): Record { + const extRecords: Record = {}; + for (const [codebase, build] of Object.entries(builds)) { + if (build.extensions) { + for (const [id, ext] of Object.entries(build.extensions)) { + if (extensionMatchesAnyFilter(codebase, id, filters)) { + if (extRecords[id]) { + // Duplicate definitions of the same instance + throw new FirebaseError(`Duplicate extension id found: ${id}`); + } + extRecords[id] = { ...ext, labels: { createdBy: "SDK", codebase } }; + } + } + } + } + + return extRecords; +} + +/** + * Checks if the extension matches any filter + * @param codebase The codebase to check + * @param extensionId The extension to check + * @param filters The filters to check against + * @return true if the extension matches any of the filters. + */ +export function extensionMatchesAnyFilter( + codebase: string | undefined, + extensionId: string, + filters?: Filter[], +): boolean { + if (!filters) { + return true; + } + return filters.some((f) => extensionMatchesFilter(codebase, extensionId, f)); +} + +/** + * Checks if the extension matches a filter + * @param codebase The codebase to check + * @param extensionId The extension to check + * @param filter The fitler to check against + * @return true if the extension matches the filter. + */ +function extensionMatchesFilter( + codebase: string | undefined, + extensionId: string, + filter: Filter, +): boolean { + if (codebase && filter.codebase) { + if (codebase !== filter.codebase) { + return false; + } + } + + if (!filter.idChunks) { + // If idChunks are not provided, we match all extensions. + return true; + } + + // Extension instance ids are not nested. They are unique to a project. + // They are allowed to have hyphens, so in the functions filter this will be + // interpreted as nested chunks, so we join them again to get the original id. + const filterId = filter.idChunks.join("-"); + + return extensionId === filterId; +} + +/** + * Looks for the tsconfig.json file + * @param codebaseDir The codebase directory to check + * @return true iff the codebase directory is typescript. + */ +export function isTypescriptCodebase(codebaseDir: string): boolean { + return fsutils.fileExistsSync(path.join(codebaseDir, "tsconfig.json")); +} + +/** + * Writes a file containing data. Asks permission based on options + * @param filePath Where the create a file + * @param data What to put into the file + * @param options options for force or nonInteractive to skip permission requests + */ +export async function writeFile(filePath: string, data: string, options: Options): Promise { + const shortFilePath = filePath.replace(process.cwd(), "."); + if (fsutils.fileExistsSync(filePath)) { + if ( + await confirm({ + message: `${shortFilePath} already exists. Overwite it?`, + nonInteractive: options.nonInteractive, + force: options.force, + default: false, + }) + ) { + // overwrite + try { + await fs.promises.writeFile(filePath, data, { flag: "w" }); + logLabeledBullet("extensions", `successfully wrote ${shortFilePath}`); + } catch (err: unknown) { + throw new FirebaseError(`Failed to write ${shortFilePath}:\n ${getErrMsg(err)}`); + } + } else { + // don't overwrite + return; + } + } else { + // write new file + // Make sure the directories exist + try { + await fs.promises.mkdir(path.dirname(filePath), { recursive: true }); + try { + await fs.promises.writeFile(`${filePath}`, data, { flag: "w" }); + logLabeledBullet("extensions", `successfully created ${shortFilePath}`); + } catch (err: unknown) { + throw new FirebaseError(`Failed to create ${shortFilePath}:\n ${getErrMsg(err)}`); + } + } catch (err: unknown) { + throw new FirebaseError(`Error during SDK file creation:\n ${getErrMsg(err)}`); + } + } +} + +/** + * copies one directory to another recursively creating directories as needed. + * It will ask for permission before overwriting any existing files. + * @param src The source path + * @param dest The destination path + * @param options The command options + */ +export async function copyDirectory(src: string, dest: string, options: Options): Promise { + const shortDestPath = dest.replace(process.cwd(), ","); + if (fsutils.dirExistsSync(dest)) { + if ( + await confirm({ + message: `${shortDestPath} already exists. Copy anyway?`, + nonInteractive: options.nonInteractive, + force: options.force, + default: false, + }) + ) { + // copy anyway + const entries = await fs.promises.readdir(src, { withFileTypes: true }); + for (const entry of entries) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + if (entry.isDirectory()) { + if (srcPath.includes("node_modules")) { + // skip these + continue; + } + // We already have permission. Don't ask again. + await copyDirectory(srcPath, destPath, { ...options, force: true }); + } else if (entry.isFile()) + try { + await fs.promises.copyFile(srcPath, destPath); + } catch (err: unknown) { + throw new FirebaseError( + `Failed to copy ${destPath.replace(process.cwd(), ".")}:\n ${getErrMsg(err)}`, + ); + } + } + } else { + // Don't overwrite + return; + } + } else { + await fs.promises.mkdir(dest, { recursive: true }); + await copyDirectory(src, dest, { ...options, force: true }); + } +} + +/** + * getCodebaseRuntime determines the runtime from the specified optoins + * @param options The options passed to the command + * @return as string like 'nodejs18' or 'python312' representing the runtime. + */ +export async function getCodebaseRuntime(options: Options): Promise { + const config = normalizeAndValidate(options.config.src.functions); + const codebaseConfig = configForCodebase( + config, + (options.codebase as string) || DEFAULT_CODEBASE, + ); + const localCfg = requireLocal(codebaseConfig); + const sourceDirName = localCfg.source; + const sourceDir = options.config.path(sourceDirName); + const delegateContext: functionRuntimes.DelegateContext = { + projectId: "", // not needed to determine the runtime in the function below + sourceDir, + projectDir: options.config.projectDir, + runtime: localCfg.runtime, + }; + let delegate: functionRuntimes.RuntimeDelegate; + try { + delegate = await functionRuntimes.getRuntimeDelegate(delegateContext); + } catch (err: unknown) { + throw new FirebaseError(`Could not detect target language for SDK at ${sourceDir}`); + } + + return delegate.runtime; +} + +/** + * writeSDK figures out which runtime we are using and then calls + * that runtime's implementation of writeSDK. + * @param extensionRef The extension reference of a published extension + * @param localPath The localPath of a local extension + * @param spec The spec for the extension + * @param options The options passed from the ext:sdk:install command + * @return Usage instructions for the SDK. + */ +export async function writeSDK( + extensionRef: string | undefined, + localPath: string | undefined, + spec: ExtensionSpec, + options: Options, +): Promise { + // Figure out which runtime we need + const runtime = await getCodebaseRuntime(options); + + // If the delegate is NodeJS, write the SDK + // If we have more options, it would be better to have an extensions delegate + if (runtime.startsWith("nodejs")) { + let sampleImport = await nodeRuntime.writeSDK(extensionRef, localPath, spec, options); + sampleImport = fixDarkBlueText(sampleImport); + return sampleImport; + } else { + throw new FirebaseError( + `Extension SDK generation is currently only supported for NodeJs. We detected the target source to be: ${runtime}`, + ); + } +} + +/** + * getCodebaseDir gets the codebase directory based on the options passed + * @param options are used to determine which codebase and the config for it + * @return a functions codebase directory + */ +export function getCodebaseDir(options: Options): string { + if (!options.projectRoot) { + throw new FirebaseError("Unable to determine root directory of project"); + } + const config = normalizeAndValidate(options.config.src.functions); + const codebaseConfig = configForCodebase( + config, + (options.codebase as string) || DEFAULT_CODEBASE, + ); + return `${options.projectRoot}/${codebaseConfig.source}/`; +} + +/** + * getInstallPathPrefix gets a prefix under the codebase directory + * for where extension SDKs should be installed. + * @param options are used to get the functions codebase directory + * @return an SDK install path prefix + */ +export function getInstallPathPrefix(options: Options): string { + return `${getCodebaseDir(options)}generated/extensions/`; +} + +/** + * toTitleCase takes the input string, capitalizes the first letter, and + * lowercases the rest of the letters aBcdEf -> Abcdef + * @param txt The text to transform + * @return The title cased string + */ +export function toTitleCase(txt: string): string { + return txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase(); +} + +/** + * capitalizeFirstLetter capitalizes the first letter of the input string + * @param txt the string to transform + * @return the input string with the first letter capitalized + */ +export function capitalizeFirstLetter(txt: string): string { + return txt.charAt(0).toUpperCase() + txt.substring(1); +} + +/** + * lowercaseFirstLetter makes the first letter of a string lowercase + * @param txt a string to transform + * @return the input string but with the first letter lowercase + */ +export function lowercaseFirstLetter(txt: string): string { + return txt.charAt(0).toLowerCase() + txt.substring(1); +} + +/** + * snakeToCamelCase transforms text from snake_case to camelCase. + * @param txt the snake_case string to transform + * @return a camelCase string + */ +export function snakeToCamelCase(txt: string): string { + let ret = txt.toLowerCase(); + ret = ret.replace(/_/g, " "); + ret = ret.replace(/\w\S*/g, toTitleCase); + ret = ret.charAt(0).toLowerCase() + ret.substring(1); + return ret; +} + +/** + * longestCommonPrefix extracts the longest common prefix from an array of string + * @param arr The array to find a longest common prefix in. + * @return A string that is the longest common prefix + */ +export function longestCommonPrefix(arr: string[]): string { + if (arr.length === 0) { + return ""; + } + let prefix = ""; + for (let pos = 0; pos < arr[0].length; pos++) { + if (arr.every((s) => s.charAt(pos) === arr[0][pos])) { + prefix += arr[0][pos]; + } else break; + } + return prefix; +} diff --git a/src/extensions/runtimes/node.ts b/src/extensions/runtimes/node.ts new file mode 100644 index 00000000000..c2b5904feb9 --- /dev/null +++ b/src/extensions/runtimes/node.ts @@ -0,0 +1,534 @@ +import * as path from "path"; +import { markedTerminal } from "marked-terminal"; +import { marked } from "marked"; + +import { Options } from "../../options"; +import { isObject } from "../../error"; +import { ExtensionSpec, ParamType } from "../types"; +import { confirm } from "../../prompt"; +import * as secretsUtils from "../secretsUtils"; +import { logLabeledBullet } from "../../utils"; +import { + writeFile, + copyDirectory, + toTitleCase, + longestCommonPrefix, + lowercaseFirstLetter, + fixDarkBlueText, + getInstallPathPrefix, + getCodebaseDir, + isTypescriptCodebase, +} from "./common"; +import { ALLOWED_EVENT_ARC_REGIONS } from "../askUserForEventsConfig"; +import { SpecParamType } from "../extensionsHelper"; +import { FirebaseError, getErrMsg } from "../../error"; +import { spawnWithOutput } from "../../init/spawn"; + +marked.use(markedTerminal() as any); + +export const SDK_GENERATION_VERSION = "1.0.0"; +export const FIREBASE_FUNCTIONS_VERSION = ">=5.1.0"; +export const TYPESCRIPT_VERSION = "^4.9.0"; + +function makePackageName(extensionRef: string | undefined, name: string): string { + if (!extensionRef) { + return `@firebase-extensions/local-${name}-sdk`; + } + const pub = extensionRef.split("/")[0]; + return `@firebase-extensions/${pub}-${name}-sdk`; +} + +function makeTypeName(name: string): string { + let typeName = name.replace(/_/g, " "); + typeName = typeName.replace(/\w\S*/g, toTitleCase); + return typeName.replace(/ /g, "") + "Param"; +} + +// A convenient map for converting prefixes back and forth +const systemPrefixes: Record = { + "firebaseextensions.v1beta.function": "_FUNCTION", + "firebaseextensions.v1beta.v2function": "_V2FUNCTION", + FUNCTION: "firebaseextensions.v1beta.function", + V2FUNCTION: "firebaseextensions.v1beta.v2function", +}; + +// Goes both forwards and reverse +function convertSystemPrefix(prefix: string): string { + return systemPrefixes[prefix]; +} + +function makeSystemTypeName(name: string): string { + if (name.includes("/")) { + const prefix = name.split("/")[0]; + let typeName = name.split("/")[1]; + typeName = typeName.replace(/([A-Z])/g, " $1").trim(); + typeName = `${convertSystemPrefix(prefix)}_${typeName}`; + return `System${makeTypeName(typeName)}`; + } + // This shouldn't happen. All system params should have a name format like + // "firebaseextensions.v1beta.function/location". + return makeTypeName(name); +} + +function makeSystemParamName(name: string): string { + if (name.includes("/")) { + const prefix = name.split("/")[0]; + let paramName = name.split("/")[1]; + paramName = paramName.replace(/([A-Z])/g, " $1").trim(); + paramName = paramName.toUpperCase(); + paramName = paramName.replace(/ /g, "_"); + return `${convertSystemPrefix(prefix)}_${paramName}`; + } + return name; +} + +function makeClassName(name: string): string { + let className = name.replace(/[_-]/g, " "); + className = className.replace(/\w\S*/g, toTitleCase); + return className.replace(/ /g, ""); +} + +// A multi approach method to figure out the event name. +function makeEventName(name: string, prefix: string): string { + let eventName: string; + const versionedEvent = /^(?:[^.]+[.])+(?:[vV]\d+[.])(?.*)$/; + const match = versionedEvent.exec(name); + if (match) { + // Most reliable: event is the thing after the version. + eventName = match[1]; + } else if (prefix.length < name.length) { + // No version, go with removing the longest common prefix instead. + eventName = name.substring(prefix.length); + } else { + // Take the last part of the event name. + const parts = name.split("."); + eventName = parts[parts.length - 1]; + } + const allCaps = /^[A-Z._-]+$/; + eventName = allCaps.exec(eventName) ? eventName : eventName.replace(/([A-Z])/g, " $1").trim(); + eventName = eventName.replace(/[._-]/g, " "); + eventName = eventName.toLowerCase().startsWith("on") ? eventName : "on " + eventName; + eventName = eventName.replace(/\w\S*/g, toTitleCase); + eventName = eventName.replace(/ /g, ""); + eventName = eventName.charAt(0).toLowerCase() + eventName.substring(1); + + return eventName; +} + +function addPeerDependency( + pkgJson: Record, + dependency: string, + version: string, +): void { + if (!pkgJson.peerDependencies) { + pkgJson.peerDependencies = {}; + } + if (!isObject(pkgJson.peerDependencies)) { + throw new FirebaseError("Internal error generating peer dependencies."); + } + pkgJson.peerDependencies[dependency] = version; +} + +/** + * writeSDK generates and writes SDK files for the given extension + * @param extensionRef The extension ref of a published extension + * @param localPath The localPath of a local extension + * @param spec The spec for the extension + * @param options The options from the ext:sdk:install command + * @return Usage instructions to print on screen after install completes + */ +export async function writeSDK( + extensionRef: string | undefined, + localPath: string | undefined, + spec: ExtensionSpec, + options: Options, +): Promise { + const sdkLines: string[] = []; // index.ts file + const className = makeClassName(spec.name); + + let dirPath; + if (extensionRef) { + dirPath = path.join(getInstallPathPrefix(options), extensionRef.replace("@", "/")); + } else if (localPath) { + dirPath = path.join(getInstallPathPrefix(options), "local", spec.name, spec.version); + // In order to deploy a local extension, it needs to be copied to the server. + // So we need to copy the localPath directory to the dirPath/source directory. + if ( + await confirm({ + message: `Copy local extension source to deployment directory? (required for successful deploy)`, + nonInteractive: options.nonInteractive, + force: options.force, + default: true, + }) + ) { + const newLocalPath = path.join(dirPath, "src"); + await copyDirectory(localPath, newLocalPath, options); + localPath = newLocalPath.replace(options.projectRoot || ".", "."); + } + } + + if (!dirPath) { + // This shouldn't be possible + throw new FirebaseError( + "Invalid extension definition. Must have either extensionRef or localPath", + ); + } + + const packageName = makePackageName(extensionRef, spec.name); + // package.json + const pkgJson: Record = { + name: packageName, + version: `${SDK_GENERATION_VERSION}`, + description: `Generated SDK for ${spec.displayName || spec.name}@${spec.version}`, + main: "./output/index.js", + private: true, + scripts: { + build: "tsc", + "build:watch": "npm run build && tsc --watch", + }, + devDependencies: { + typescript: TYPESCRIPT_VERSION, + }, + }; + + // tsconfig.json + const tsconfigJson = { + compilerOptions: { + declaration: true, + declarationMap: true, + module: "commonjs", + strict: true, + target: "es2017", + removeComments: false, + outDir: "output", + }, + }; + + // index.ts file + sdkLines.push("/**"); + sdkLines.push(` * ${spec.displayName || spec.name} SDK for ${spec.name}@${spec.version}`); + sdkLines.push(" *"); + sdkLines.push(" * When filing bugs or feature requests please specify:"); + if (extensionRef) { + sdkLines.push( + ` * "Extensions SDK v${SDK_GENERATION_VERSION} for ${spec.name}@${spec.version}"`, + ); + } else { + sdkLines.push(` * "Extensions SDK v${SDK_GENERATION_VERSION} for Local extension.`); + } + sdkLines.push(" * https://github.com/firebase/firebase-tools/issues/new/choose"); + sdkLines.push(" *"); + sdkLines.push(" * GENERATED FILE. DO NOT EDIT."); + sdkLines.push(" */\n"); + + // Imports + const hasEvents = spec.events && spec.events.length > 0; + if (hasEvents) { + sdkLines.push(`import { CloudEvent } from "firebase-functions/v2";`); + sdkLines.push( + `import { onCustomEventPublished, EventarcTriggerOptions } from "firebase-functions/v2/eventarc";`, + ); + addPeerDependency(pkgJson, "firebase-functions", FIREBASE_FUNCTIONS_VERSION); + } + const usesSecrets = secretsUtils.usesSecrets(spec); + if (usesSecrets) { + sdkLines.push(`import { defineSecret } from "firebase-functions/params";`); + addPeerDependency(pkgJson, "firebase-functions", FIREBASE_FUNCTIONS_VERSION); + } + if (hasEvents || usesSecrets) { + sdkLines.push(""); + } + + // Types + if (hasEvents) { + sdkLines.push( + `export type EventCallback = (event: CloudEvent) => unknown | Promise;`, + ); + sdkLines.push( + `export type SimpleEventarcTriggerOptions = Omit;`, + ); + sdkLines.push(`export type EventArcRegionType = "${ALLOWED_EVENT_ARC_REGIONS.join('" | "')}";`); + } + if (usesSecrets) { + sdkLines.push("export type SecretParam = ReturnType;"); + } + + // Define types for any (multi)select parameters + if (spec.params && Array.isArray(spec.params) && spec.params.length > 0) { + for (const param of spec.params) { + let line: string; + if ( + param.type === ParamType.SELECT || + param.type === ParamType.MULTISELECT || + param.type === SpecParamType.SELECT || + param.type === SpecParamType.MULTISELECT + ) { + line = `export type ${makeTypeName(param.param)} =`; + param.options?.forEach((opt, i) => { + if (i === 0) { + line = line.concat(` "${opt.value}"`); + } else { + line = line.concat(` | "${opt.value}"`); + } + }); + line = line.concat(";"); + sdkLines.push(line); + } + } + } + sdkLines.push(""); + + // Define types for system param (multi)select parameters + if (spec.systemParams && Array.isArray(spec.systemParams) && spec.systemParams.length > 0) { + for (const sysParam of spec.systemParams) { + let line: string; + if (sysParam.type === ParamType.SELECT || sysParam.type === ParamType.MULTISELECT) { + line = `export type ${makeSystemTypeName(sysParam.param)} =`; + sysParam.options?.forEach((opt, i) => { + if (i === 0) { + line = line.concat(` "${opt.value}"`); + } else { + line = line.concat(` | "${opt.value}"`); + } + }); + line = line.concat(";"); + sdkLines.push(line); + } + } + } + sdkLines.push(""); + + // Define the params + sdkLines.push("/**"); + sdkLines.push(` * Parameters for ${spec.name}@${spec.version} extension`); + sdkLines.push(" */"); + sdkLines.push(`export interface ${className}Params {`); + + for (const param of spec.params) { + const opt = param.required ? "" : "?"; + + sdkLines.push(" /**"); + sdkLines.push(` * ${param.label}`); + if (param.validationRegex && !param.validationRegex.includes("*/")) { + sdkLines.push(` * - Validation regex: ${param.validationRegex}`); + } + sdkLines.push(" */"); + + switch (param.type) { + case ParamType.STRING: + case SpecParamType.STRING: + sdkLines.push(` ${param.param}${opt}: string;`); + break; + case ParamType.MULTISELECT: + case SpecParamType.MULTISELECT: + sdkLines.push(` ${param.param}${opt}: ${makeTypeName(param.param)}[];`); + break; + case ParamType.SELECT: + case SpecParamType.SELECT: + sdkLines.push(` ${param.param}${opt}: ${makeTypeName(param.param)};`); + break; + case ParamType.SECRET: + case SpecParamType.SECRET: + sdkLines.push(` ${param.param}${opt}: SecretParam;`); + break; + case ParamType.SELECT_RESOURCE: + case SpecParamType.SELECTRESOURCE: + // We can't really do anything better. There are no + // typescript types based on regex. Maybe we could make a + // class with a setter, but it would be a runtime error. I'm + // not sure how helpful that would be. + sdkLines.push(` ${param.param}${opt}: string;`); + break; + + default: + // This is technically possible since param.type is not a required field. + // Assume string, and add a comment + sdkLines.push(` ${param.param}${opt}: string; // Assuming string for unknown type`); + } + sdkLines.push(""); + } + + if (hasEvents) { + sdkLines.push(" /**"); + sdkLines.push(` * Event Arc Region`); + sdkLines.push(" */"); + sdkLines.push(" _EVENT_ARC_REGION?: EventArcRegionType\n"); + } + + for (const sysParam of spec.systemParams) { + const opt = sysParam.required ? "" : "?"; + + sdkLines.push(" /**"); + sdkLines.push(` * ${sysParam.label}`); + if (sysParam.validationRegex && !sysParam.validationRegex.includes("*/")) { + sdkLines.push(` * - Validation regex: ${sysParam.validationRegex}`); + } + sdkLines.push(" */"); + + switch (sysParam.type) { + case ParamType.STRING: + sdkLines.push(` ${makeSystemParamName(sysParam.param)}${opt}: string;`); + break; + case ParamType.MULTISELECT: + sdkLines.push( + ` ${makeSystemParamName(sysParam.param)}${opt}: ${makeSystemTypeName(sysParam.param)}[];`, + ); + break; + case ParamType.SELECT: + sdkLines.push( + ` ${makeSystemParamName(sysParam.param)}${opt}: ${makeSystemTypeName(sysParam.param)};`, + ); + break; + case ParamType.SECRET: + sdkLines.push(` ${makeSystemParamName(sysParam.param)}${opt}: SecretParam;`); + break; + case ParamType.SELECT_RESOURCE: + // We can't really do anything better. There are no + // typescript types based on regex. Maybe we could make a + // class with a setter, but it would be a runtime error. I'm + // not sure how helpful that would be. + sdkLines.push(` ${sysParam.param}${opt}: string;`); + break; + default: + throw new FirebaseError( + `Error: Unknown systemParam type: ${sysParam.type || "undefined"}.`, + ); + } + sdkLines.push(""); + } + sdkLines.push("}\n"); + + const lowerClassName = lowercaseFirstLetter(className); + // The function that returns the main class + sdkLines.push( + `export function ${lowerClassName}(instanceId: string, params: ${className}Params) {`, + ); + sdkLines.push(` return new ${className}(instanceId, params);`); + sdkLines.push("}\n"); + + // The main class + sdkLines.push(`/**`); + sdkLines.push(` * ${spec.displayName || spec.name}`); + spec.description?.split("\n").forEach((val: string) => { + sdkLines.push(` * ${val.replace(/\*\//g, "* /")}`); // don't end the comment + }); + sdkLines.push(` */`); + sdkLines.push(`export class ${className} {`); + if (hasEvents) { + sdkLines.push(` events: string[] = [];`); + } + if (extensionRef) { + sdkLines.push(` readonly FIREBASE_EXTENSION_REFERENCE = "${extensionRef}";`); + sdkLines.push(` readonly EXTENSION_VERSION = "${extensionRef.split("@")[1]}";\n`); + } else if (localPath) { + sdkLines.push(` readonly FIREBASE_EXTENSION_LOCAL_PATH = "${localPath}";`); + } + sdkLines.push( + ` constructor(private instanceId: string, private params: ${className}Params) {}\n`, + ); + + // These 2 accessors are more about stopping the compiler from complaining + // about "declared but never used" variables. (We do use them, it's how + // we know what to call the instance and what parameters it has when we deploy). + sdkLines.push(` getInstanceId(): string { return this.instanceId; }\n`); + sdkLines.push(` getParams(): ${className}Params { return this.params; }\n`); + + if (spec.events) { + const prefix = longestCommonPrefix(spec.events.map((e) => e.type)); + for (const event of spec.events) { + const eventName = makeEventName(event.type, prefix); + sdkLines.push(" /**"); + sdkLines.push(` * ${event.description}`); + sdkLines.push(` */`); + sdkLines.push( + ` ${eventName}(callback: EventCallback, options?: SimpleEventarcTriggerOptions) {`, + ); + sdkLines.push(` this.events.push("${event.type}");`); + sdkLines.push(` return onCustomEventPublished({`); + sdkLines.push(` ...options,`); + sdkLines.push(` "eventType": "${event.type}",`); + // The projectId will be filled in during deploy when we know which project we are deploying to. + sdkLines.push( + ' "channel": `projects/locations/${this.params._EVENT_ARC_REGION}/channels/firebase`,', + ); + sdkLines.push(' "region": `${this.params._EVENT_ARC_REGION}`'); + sdkLines.push(" },"); + sdkLines.push(" callback);"); + sdkLines.push(` }\n`); + } + } + sdkLines.push(`}`); // End of class + + // Write the files + // shortDirPath so it's easier to read + const shortDirPath = dirPath.replace(process.cwd(), "."); + + await writeFile(`${dirPath}/index.ts`, sdkLines.join("\n"), options); + await writeFile(`${dirPath}/package.json`, JSON.stringify(pkgJson, null, 2), options); + await writeFile(`${dirPath}/tsconfig.json`, JSON.stringify(tsconfigJson, null, 2), options); + + // We don't ask for permissions for the next 2 commands because they only + // really affect the generated directory and their effects can be negated + // by just removing that directory. + + // NPM install dependencies (since we will be adding this link locally) + logLabeledBullet("extensions", `running 'npm --prefix ${shortDirPath} install'`); + try { + await spawnWithOutput("npm", ["--prefix", dirPath, "install"]); + } catch (err: unknown) { + const errMsg = getErrMsg(err, "unknown error"); + throw new FirebaseError(`Error during npm install in ${shortDirPath}: ${errMsg}`); + } + + // Build it + logLabeledBullet("extensions", `running 'npm --prefix ${shortDirPath} run build'`); + try { + await spawnWithOutput("npm", ["--prefix", dirPath, "run", "build"]); + } catch (err: unknown) { + const errMsg = getErrMsg(err, "unknown error"); + throw new FirebaseError(`Error during npm run build in ${shortDirPath}: ${errMsg}`); + } + + const codebaseDir = getCodebaseDir(options); + const shortCodebaseDir = codebaseDir.replace(process.cwd(), "."); + let installCmd = ""; + if ( + await confirm({ + message: `Do you want to install the SDK with npm now?`, + nonInteractive: options.nonInteractive, + force: options.force, + default: true, + }) + ) { + logLabeledBullet( + "extensions", + `running 'npm --prefix ${shortCodebaseDir} install --save ${shortDirPath}'`, + ); + try { + await spawnWithOutput("npm", ["--prefix", codebaseDir, "install", "--save", dirPath]); + } catch (err: unknown) { + const errMsg = getErrMsg(err, "unknown error"); + throw new FirebaseError(`Error during npm install in ${codebaseDir}: ${errMsg}`); + } + } else { + installCmd = `npm --prefix ${shortCodebaseDir} install --save ${shortDirPath}`; + } + + let sampleImport; + if (isTypescriptCodebase(codebaseDir)) { + sampleImport = + "```typescript\n" + `import { ${lowerClassName} } from "${packageName}";` + "\n```"; + } else { + sampleImport = "```js\n" + `const { ${lowerClassName} } = require("${packageName}");` + "\n```"; + } + const prefix = installCmd + ? `\nTo install the SDK to your project run:\n ${installCmd}\n\nThen you ` + : "\nYou "; + const instructions = + prefix + + `can add this to your codebase to begin using the SDK:\n\n` + + fixDarkBlueText(await marked(sampleImport)) + + `See also: ${fixDarkBlueText(await marked("[Extension SDKs documentation](https://firebase.google.com/docs/extensions/install-extensions?interface=sdk#config)"))}`; + + return instructions; +} 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 new file mode 100644 index 00000000000..0cbfcc75dc1 --- /dev/null +++ b/src/extensions/secretsUtils.ts @@ -0,0 +1,72 @@ +import { getProjectNumber } from "../getProjectNumber"; +import * as utils from "../utils"; +import { ensure } from "../ensureApiEnabled"; +import { needProjectId } from "../projectUtils"; +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, secretManagerOrigin(), "extensions", options.markdown); +} + +export function usesSecrets(spec: ExtensionSpec): boolean { + return spec.params && !!spec.params.find((p) => p.type === ParamType.SECRET); +} + +export async function grantFirexServiceAgentSecretAdminRole( + secret: secretManagerApi.Secret, +): Promise { + const projectNumber = await getProjectNumber({ projectId: secret.projectId }); + const firexSaProjectId = utils.envOverride( + "FIREBASE_EXTENSIONS_SA_PROJECT_ID", + "gcp-sa-firebasemods", + ); + const saEmail = `service-${projectNumber}@${firexSaProjectId}.iam.gserviceaccount.com`; + + return secretManagerApi.ensureServiceAgentRole(secret, [saEmail], "roles/secretmanager.admin"); +} + +export async function getManagedSecrets(instance: ExtensionInstance): Promise { + return ( + await Promise.all( + getActiveSecrets(instance.config.source.spec, instance.config.params).map( + async (secretResourceName) => { + const secret = secretManagerApi.parseSecretResourceName(secretResourceName); + const labels = (await secretManagerApi.getSecret(secret.projectId, secret.name)).labels; + if (labels && labels[SECRET_LABEL]) { + return secretResourceName; + } + return Promise.resolve(""); + }, + ), + ) + ).filter((secretId) => !!secretId); +} + +export function getActiveSecrets(spec: ExtensionSpec, params: Record): string[] { + return spec.params + .map((p) => (p.type === ParamType.SECRET ? params[p.param] : "")) + .filter((pv) => !!pv); +} + +export function getSecretLabels(instanceId: string): Record { + const labels: Record = {}; + labels[SECRET_LABEL] = instanceId; + return labels; +} + +export function prettySecretName(secretResourceName: string): string { + const nameTokens = secretResourceName.split("/"); + if (nameTokens.length !== 4 && nameTokens.length !== 6) { + // not a familiar format, return as is + logger.debug(`unable to parse secret secretResourceName: ${secretResourceName}`); + return secretResourceName; + } + return nameTokens.slice(0, 4).join("/"); +} 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..3e75cee72a9 --- /dev/null +++ b/src/extensions/tos.ts @@ -0,0 +1,142 @@ +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({ + message: "Do you accept the Firebase Extensions Publisher Terms of Service?", + nonInteractive: options.nonInteractive, + force: options.force, + }) + ) { + 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({ + message: "Do you accept the Firebase Extensions User Terms of Service?", + nonInteractive: options.nonInteractive, + force: options.force, + })) + ) { + 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..007d1350f8e --- /dev/null +++ b/src/extensions/types.ts @@ -0,0 +1,334 @@ +import { MemoryOptions } from "../deploy/functions/backend"; +import { Runtime } from "../deploy/functions/runtimes/supported"; +import * as proto from "../gcp/proto"; +import { SpecParamType } from "./extensionsHelper"; +import { isObject } from "../error"; + +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; +} + +const extensionInstanceState = [ + "STATE_UNSPECIFIED", + "DEPLOYING", + "UNINSTALLING", + "ACTIVE", + "ERRORED", + "PAUSED", +] as const; +export type ExtensionInstanceState = (typeof extensionInstanceState)[number]; +export interface ExtensionInstance { + name: string; + createTime: string; + updateTime: string; + state: ExtensionInstanceState; + config: ExtensionConfig; + serviceAccountEmail: string; + errorStatus?: string; + lastOperationName?: string; + lastOperationType?: string; + etag?: string; + extensionRef?: string; + extensionVersion?: string; + labels?: Record; +} + +export const isExtensionInstance = (value: unknown): value is ExtensionInstance => { + if (!isObject(value) || typeof value.name !== "string") { + return false; + } + + // TODO: complete validation for any fields we use + return true; +}; + +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[]; +} + +const lifecycleStages = ["STAGE_UNSPECIFIED", "ON_INSTALL", "ON_UPDATE", "ON_CONFIGURE"] as const; +export type LifecycleStage = (typeof lifecycleStages)[number]; +export interface LifecycleEvent { + stage: LifecycleStage; + 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", + SELECT_RESOURCE = "SELECT_RESOURCE", + SECRET = "SECRET", +} + +export interface ParamOption { + value: string; + label?: string; +} + +export const isParam = (param: unknown): param is Param => { + return ( + isObject(param) && typeof param["param"] === "string" && typeof param["label"] === "string" + ); +}; + +export const isResource = (res: unknown): res is Resource => { + return isObject(res) && typeof res["name"] === "string"; +}; + +// Typeguard for ExtensionSpec. (We often get "specs" from parsing yaml). +// This helps decide if it's actually a spec or just some random yaml. +export const isExtensionSpec = (spec: unknown): spec is ExtensionSpec => { + if (!isObject(spec) || typeof spec.name !== "string" || typeof spec.version !== "string") { + return false; + } + + if (spec.resources && Array.isArray(spec.resources)) { + for (const res of spec.resources) { + if (!isResource(res)) { + return false; + } + } + } else { + return false; + } + + if (spec.params && Array.isArray(spec.params)) { + for (const param of spec.params) { + if (!isParam(param)) { + return false; + } + } + } else { + return false; + } + + if (spec.systemParams && Array.isArray(spec.systemParams)) { + for (const param of spec.systemParams) { + if (!isParam(param)) { + return false; + } + } + } else { + // Allow systemParams to be missing for local + return !spec.systemParams; + } + + return true; +}; diff --git a/src/extensions/updateHelper.spec.ts b/src/extensions/updateHelper.spec.ts new file mode 100644 index 00000000000..8be0ad45ecb --- /dev/null +++ b/src/extensions/updateHelper.spec.ts @@ -0,0 +1,257 @@ +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 { + ExtensionInstance, + ExtensionInstanceState, + ExtensionSource, + 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: ExtensionSource = { + state: "ACTIVE", + name: "projects/firebasemods/sources/new-test-source", + packageUri: "https://firebase-fake-bucket.com", + hash: "1234567", + spec: SPEC, +}; + +const INSTANCE: ExtensionInstance = { + 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" as ExtensionInstanceState, + config: { + name: "projects/invader-zim/instances/instance-of-official-ext/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", + createTime: "2019-05-19T00:20:10.416947Z", + source: { + ...SOURCE, + name: "projects/firebasemods/sources/fake-official-source", + }, + params: {}, + systemParams: {}, + }, + serviceAccountEmail: "name@org.com", +}; + +const REGISTRY_INSTANCE: ExtensionInstance = { + 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" as ExtensionInstanceState, + serviceAccountEmail: "name@org.com", + config: { + name: "projects/invader-zim/instances/instance-of-registry-ext/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", + createTime: "2019-05-19T00:20:10.416947Z", + extensionRef: "test-publisher/test", + source: { + ...SOURCE, + name: "projects/firebasemods/sources/fake-registry-source", + }, + params: {}, + systemParams: {}, + }, +}; + +const LOCAL_INSTANCE: ExtensionInstance = { + 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" as ExtensionInstanceState, + serviceAccountEmail: "name@org.com", + config: { + name: "projects/invader-zim/instances/instance-of-local-ext/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", + createTime: "2019-05-19T00:20:10.416947Z", + source: { + ...SOURCE, + name: "projects/firebasemods/sources/fake-local-source", + }, + params: {}, + systemParams: {}, + }, +}; + +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 7f9b44a317a..84f16e64443 100644 --- a/src/extensions/updateHelper.ts +++ b/src/extensions/updateHelper.ts @@ -1,64 +1,45 @@ -import * as clc from "cli-color"; +import * as clc from "colorette"; import * as semver from "semver"; import { FirebaseError } from "../error"; import { logger } from "../logger"; -import * as resolveSource from "./resolveSource"; import * as extensionsApi from "./extensionsApi"; -import { promptOnce } from "../prompt"; -import { createSourceFromLocation, logPrefix, SourceOrigin, urlRegex } from "./extensionsHelper"; -import * as utils from "../utils"; +import { ExtensionSource, ExtensionSpec } from "./types"; import { - displayUpdateChangesNoInput, - displayUpdateChangesRequiringConfirmation, - getConsent, -} from "./displayExtensionInfo"; + createSourceFromLocation, + logPrefix, + SourceOrigin, + isLocalOrURLPath, +} from "./extensionsHelper"; +import * as utils from "../utils"; +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); - if (instance && instance.config.extensionRef) { - return SourceOrigin.PUBLISHED_EXTENSION; - } - let existingSourceOrigin: SourceOrigin; - try { - const registryEntry = await resolveSource.resolveRegistryEntry(extensionName); - if (resolveSource.isOfficialSource(registryEntry, existingSource)) { - existingSourceOrigin = SourceOrigin.OFFICIAL_EXTENSION; - } else { - existingSourceOrigin = SourceOrigin.PUBLISHED_EXTENSION; - } - } catch { - // If registry entry does not exist, assume existing source was from local directory or URL. - if (urlRegex.test(existingSource)) { - existingSourceOrigin = SourceOrigin.URL; - } else { - existingSourceOrigin = SourceOrigin.LOCAL; - } - } - return existingSourceOrigin; + return instance && instance.config.extensionRef + ? SourceOrigin.PUBLISHED_EXTENSION + : SourceOrigin.LOCAL; } -async function showUpdateVersionInfo( +function showUpdateVersionInfo( instanceId: string, from: string, to: string, - source?: string -): Promise { + source?: string, +): void { if (source) { source = clc.bold(source); } else { @@ -66,96 +47,55 @@ async 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.logLabeledBullet( + 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 await getConsent("version", "Do you wish to continue?"); } return; } /** - * Prints out warning messages and requires user to consent before continuing with update. - * @param projectId name of the project - * @param instanceId name of the instance - * @param extensionName name of the extension being updated - * @param existingSource current source of the extension instance - * @param nextSourceOrigin new source of the extension instance (to be updated to) - * @param warning source origin specific warning message - * @param additionalMsg any additional warnings associated with this update + * Prints out informational message about what code the instance will be updated to.. + * @param sourceOrigin source origin */ -export async function warningUpdateToOtherSource( - existingSourceOrigin: SourceOrigin, - warning: string, - nextSourceOrigin: SourceOrigin, - additionalMsg?: string -): Promise { - let msg = warning; - let joinText = ""; - if (existingSourceOrigin === nextSourceOrigin) { - joinText = "also "; +export function warningUpdateToOtherSource(sourceOrigin: SourceOrigin) { + let targetText; + if ( + [SourceOrigin.PUBLISHED_EXTENSION, SourceOrigin.PUBLISHED_EXTENSION_VERSION].includes( + sourceOrigin, + ) + ) { + targetText = "published extension"; + } else if (sourceOrigin === SourceOrigin.LOCAL) { + targetText = "local directory"; + } else if (sourceOrigin === SourceOrigin.URL) { + targetText = "URL"; } - msg += - `The current source for this instance is a(n) ${existingSourceOrigin}. The new source for this instance will ${joinText}be a(n) ${nextSourceOrigin}.\n\n` + - `${additionalMsg || ""}`; - const updateWarning = { - from: existingSourceOrigin, - description: msg, - }; - return await resolveSource.confirmUpdateWarning(updateWarning); -} - -/** - * 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( - spec: extensionsApi.ExtensionSpec, - newSpec: extensionsApi.ExtensionSpec, - published = false -): Promise { - logger.info( - "This update contains the following changes (in green and red). " + - "If at any point you choose not to continue, the extension will not be updated and the changes will be discarded:\n" - ); - displayUpdateChangesNoInput(spec, newSpec, published); - await displayUpdateChangesRequiringConfirmation(spec, newSpec); -} - -/** - * Prompts the user to confirm before continuing to update. - */ -export async function retryUpdate(): Promise { - return promptOnce({ - type: "confirm", - message: "Are you sure you wish to continue with updating anyways?", - default: false, - }); + const warning = `All the instance's resources and logic will be overwritten to use the source code and files from the ${targetText}.\n`; + logger.info(warning); } /** * @param projectId Id of the project containing the instance to update * @param instanceId Id of the instance to update - * @param source A ExtensionSource to update to - * @param params A new set of params to set on the instance - * @param billingRequired Whether the extension requires billing + * @param extRef Extension reference + * @param source An ExtensionSource to update to (if extRef is not passed in) + * @param params Actual fields to update */ export interface UpdateOptions { projectId: string; instanceId: string; - source?: extensionsApi.ExtensionSource; + source?: ExtensionSource; extRef?: string; params?: { [key: string]: string }; + canEmitEvents: boolean; + allowedEventTypes?: string[]; + eventarcChannel?: string; } /** @@ -167,14 +107,39 @@ 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); + return await extensionsApi.updateInstanceFromRegistry({ + projectId, + instanceId, + extRef, + params, + canEmitEvents, + allowedEventTypes, + eventarcChannel, + }); } else if (source) { - return await extensionsApi.updateInstance(projectId, instanceId, source, params); + return await extensionsApi.updateInstance({ + projectId, + 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.`, ); } @@ -184,42 +149,26 @@ export async function update(updateOptions: UpdateOptions): Promise { * @param instanceId Id of the instance to update * @param localSource path to the new local source * @param existingSpec ExtensionSpec of existing instance source - * @param existingSource name of existing instance source */ export async function updateFromLocalSource( projectId: string, instanceId: string, localSource: string, - existingSpec: extensionsApi.ExtensionSpec, - existingSource: string + existingSpec: ExtensionSpec, ): Promise { + 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.")}` - ); - await showUpdateVersionInfo(instanceId, existingSpec.version, source.spec.version, localSource); - const warning = - "All the instance's extension-specific resources and logic will be overwritten to use the source code and files from the local directory.\n\n"; - const additionalMsg = - "After updating from a local source, this instance cannot be updated in the future to use a published source from Firebase's registry of extensions."; - const existingSourceOrigin = await getExistingSourceOrigin( - projectId, - instanceId, - existingSpec.name, - existingSource - ); - await module.exports.warningUpdateToOtherSource( - existingSourceOrigin, - warning, - SourceOrigin.LOCAL, - additionalMsg + `${clc.bold("You are updating this extension instance to a local source.")}`, ); + showUpdateVersionInfo(instanceId, existingSpec.version, source.spec.version, localSource); + warningUpdateToOtherSource(SourceOrigin.LOCAL); return source.name; } @@ -235,187 +184,38 @@ export async function updateFromUrlSource( projectId: string, instanceId: string, urlSource: string, - existingSpec: extensionsApi.ExtensionSpec, - existingSource: string + existingSpec: ExtensionSpec, ): Promise { + 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.")}` - ); - await showUpdateVersionInfo(instanceId, existingSpec.version, source.spec.version, urlSource); - const warning = - "All the instance's extension-specific resources and logic will be overwritten to use the source code and files from the URL.\n\n"; - const additionalMsg = - "After updating from a URL source, this instance cannot be updated in the future to use a published source from Firebase's registry of extensions."; - const existingSourceOrigin = await getExistingSourceOrigin( - projectId, - instanceId, - existingSpec.name, - existingSource - ); - await module.exports.warningUpdateToOtherSource( - existingSourceOrigin, - warning, - SourceOrigin.URL, - additionalMsg + `${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, - existingSource: string -): Promise { - let source; - try { - source = await extensionsApi.getExtensionVersion(extVersionRef); - } catch (err) { - const refObj = extensionsApi.parseRef(extVersionRef); - const version = refObj.version; - const extension = await extensionsApi.getExtension( - `${refObj.publisherId}/${refObj.extensionId}` - ); - 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 - )}).` - ); +export function inferUpdateSource(updateSource: string, existingRef: string): string { + if (!updateSource) { + return `${existingRef}@latest`; } - utils.logLabeledBullet( - logPrefix, - `${clc.bold("You are updating this extension instance to a published source.")}` - ); - await showUpdateVersionInfo(instanceId, existingSpec.version, source.spec.version, extVersionRef); - const warning = - "All the instance's extension-specific resources and logic will be overwritten to use the source code and files from the published extension.\n\n"; - const existingSourceOrigin = await getExistingSourceOrigin( - projectId, - instanceId, - existingSpec.name, - existingSource - ); - await module.exports.warningUpdateToOtherSource( - existingSourceOrigin, - warning, - SourceOrigin.PUBLISHED_EXTENSION - ); - 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, - existingSource: string -): Promise { - return updateToVersionFromPublisherSource( - projectId, - instanceId, - `${extRef}@latest`, - existingSpec, - existingSource - ); -} - -/** - * Preparatory work for updating an published extension instance to the given version. - * - * @param instanceId Id of the instance to update - * @param existingSpec ExtensionSpec of the existing instance source - * @param existingSource name of existing instance source - * @param version Version to update the instance to - */ -export async function updateToVersionFromRegistry( - projectId: string, - instanceId: string, - existingSpec: extensionsApi.ExtensionSpec, - existingSource: string, - version: string -): Promise { - if (version !== "latest" && !semver.valid(version)) { - throw new FirebaseError(`cannot update to invalid version ${version}`); + if (semver.valid(updateSource)) { + return `${existingRef}@${updateSource}`; } - // Updating to a version from an published source - let registryEntry; - try { - registryEntry = await resolveSource.resolveRegistryEntry(existingSpec.name); - } catch (err) { - // If registry entry does not exist, assume existing source was from local directory or URL. - throw new FirebaseError( - `Cannot find the latest version of this extension. To update this instance to a local source or URL source, run "firebase ext:update ${instanceId} ".` - ); + if (!isLocalOrURLPath(updateSource) && updateSource.split("/").length < 2) { + return updateSource.includes("@") + ? `firebase/${updateSource}` + : `firebase/${updateSource}@latest`; } - utils.logLabeledBullet( - logPrefix, - clc.bold("You are updating this extension instance to an official source.") - ); - - // Do not allow user to "downgrade" to a version lower than the minimum required version. - const minVer = resolveSource.getMinRequiredVersion(registryEntry); - if (minVer) { - if (version !== "latest" && semver.gt(minVer, version)) { - throw new FirebaseError( - `The version you are trying to upgrade to (${clc.bold( - version - )}) is less than the minimum version required (${clc.bold(minVer)}) to use this extension.` - ); - } + if (!isLocalOrURLPath(updateSource) && !updateSource.includes("@")) { + return `${updateSource}@latest`; } - const targetVersion = resolveSource.getTargetVersion(registryEntry, version); - await showUpdateVersionInfo(instanceId, existingSpec.version, targetVersion); - const warning = - "All the instance's extension-specific resources and logic will be overwritten to use the source code and files from the latest released version.\n\n"; - const existingSourceOrigin = await getExistingSourceOrigin( - projectId, - instanceId, - existingSpec.name, - existingSource - ); - await module.exports.warningUpdateToOtherSource( - existingSourceOrigin, - warning, - SourceOrigin.OFFICIAL_EXTENSION - ); - await resolveSource.promptForUpdateWarnings(registryEntry, existingSpec.version, targetVersion); - return resolveSource.resolveSourceUrl(registryEntry, existingSpec.name, targetVersion); -} - -/** - * Preparatory work for updating an published extension instance to the latest version. - * - * @param instanceId Id of the instance to update - * @param existingSpec ExtensionSpec of the existing instance source - * @param existingSource name of existing instance source - */ -export async function updateFromRegistry( - projectId: string, - instanceId: string, - existingSpec: extensionsApi.ExtensionSpec, - existingSource: string -): Promise { - return updateToVersionFromRegistry(projectId, instanceId, existingSpec, existingSource, "latest"); + return updateSource; } 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..e911ae223f1 100644 --- a/src/extensions/utils.ts +++ b/src/extensions/utils.ts @@ -1,44 +1,21 @@ -import * as _ from "lodash"; -import { promptOnce } from "../prompt"; -import { ParamOption } from "./extensionsApi"; -import { RegistryEntry } from "./resolveSource"; +import { + ParamOption, + Resource, + FUNCTIONS_RESOURCE_TYPE, + FUNCTIONS_V2_RESOURCE_TYPE, +} from "./types"; +import { Runtime } from "../deploy/functions/runtimes/supported"; +import { Choice } from "../prompt"; -// 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)) { - return response.join(","); - } - return response; -} - -interface ListItem { - name?: string; // User friendly display name for the option - value: string; // Value of the option - checked: boolean; // Whether the option should be checked by default -} - -// 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, - }; - } - ); -} - -// 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) => { +/** + * Convert extension option to Inquirer-friendly list for the prompt, with all items unchecked. + */ +export function convertExtensionOptionToLabeledList(options: ParamOption[]): Choice[] { + return options.map((option: ParamOption): Choice => { return { checked: false, - value: key, + name: option.label, + value: option.value, }; }); } @@ -68,3 +45,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/versionHelper.spec.ts b/src/extensions/versionHelper.spec.ts new file mode 100644 index 00000000000..0f8c7836f7e --- /dev/null +++ b/src/extensions/versionHelper.spec.ts @@ -0,0 +1,28 @@ +import { expect } from "chai"; + +import { parseVersionPredicate } from "./versionHelper"; + +describe("versionHelper", () => { + describe("parseVersionPredicate", () => { + it("should parse a version predicate with a comparator", () => { + const predicate = ">=1.2.3"; + const result = parseVersionPredicate(predicate); + expect(result.comparator).to.equal(">="); + expect(result.targetSemVer).to.equal("1.2.3"); + }); + + it("should parse a version predicate without a comparator", () => { + const predicate = "1.2.3"; + const result = parseVersionPredicate(predicate); + expect(result.comparator).to.equal("="); + expect(result.targetSemVer).to.equal("1.2.3"); + }); + + it("should not throw an error for an invalid predicate", () => { + const predicate = "not-a-version"; + const result = parseVersionPredicate(predicate); + expect(result.comparator).to.equal("="); + expect(result.targetSemVer).to.equal("not-a-version"); + }); + }); +}); diff --git a/src/extensions/versionHelper.ts b/src/extensions/versionHelper.ts new file mode 100644 index 00000000000..0d27f8b6968 --- /dev/null +++ b/src/extensions/versionHelper.ts @@ -0,0 +1,22 @@ +import { FirebaseError } from "../error"; + +export interface VersionPredicate { + comparator: string; + targetSemVer: string; +} + +/** + * Converts the string version predicate into a parsed object. + * + * @param versionPredicate a combined comparator and semver (e.g. ">=1.0.1") + * @returns the parsed version predicate + */ +export function parseVersionPredicate(versionPredicate: string): VersionPredicate { + const versionPredicateRegex = "^(?>=|<=|>|<)?(?.*)"; + const matches = versionPredicate.match(versionPredicateRegex); + if (!matches || !matches.groups!.targetSemVer) { + throw new FirebaseError("Invalid version predicate."); + } + const comparator = matches.groups!.comparator || "="; + return { comparator, targetSemVer: matches.groups!.targetSemVer }; +} 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 new file mode 100644 index 00000000000..594c09a3f02 --- /dev/null +++ b/src/extensions/warnings.ts @@ -0,0 +1,55 @@ +import * as clc from "colorette"; + +import { logPrefix } from "./extensionsHelper"; +import { humanReadable } from "../deploy/extensions/deploymentSummary"; +import { InstanceSpec, getExtensionVersion } from "../deploy/extensions/planner"; +import { logger } from "../logger"; +import * as utils from "../utils"; + +const toListEntry = (i: InstanceSpec) => { + const idAndRef = humanReadable(i); + 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})` + : ""; + return `${idAndRef}${sourceCodeLink}${githubLink}`; +}; + +/** + * Display a single, grouped warning about extension status for all instances in a deployment. + * Returns true if any instances triggered a warning. + * @param instancesToCreate A list of instances that will be created in this deploy + */ +export async function displayWarningsForDeploy(instancesToCreate: InstanceSpec[]) { + const uploadedExtensionInstances = instancesToCreate.filter((i) => i.ref); + for (const i of uploadedExtensionInstances) { + await getExtensionVersion(i); + } + const unpublishedExtensions = uploadedExtensionInstances.filter( + (i) => i.extensionVersion?.listing?.state !== "APPROVED", + ); + + if (unpublishedExtensions.length) { + const humanReadableList = unpublishedExtensions.map(toListEntry).join("\n"); + utils.logLabeledBullet( + logPrefix, + `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.", + ); + } + return unpublishedExtensions.length > 0; +} + +export function outOfBandChangesWarning(instanceIds: string[], isDynamic: boolean) { + const extra = isDynamic + ? "" + : " To avoid this, run `firebase ext:export` to sync these changes to your local extensions manifest."; + 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.${extra}`, + ); +} 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/fetchWebSetup.spec.ts b/src/fetchWebSetup.spec.ts new file mode 100644 index 00000000000..acb6fe3c878 --- /dev/null +++ b/src/fetchWebSetup.spec.ts @@ -0,0 +1,116 @@ +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"; + +describe("fetchWebSetup module", () => { + before(() => { + nock.disableNetConnect(); + }); + + after(() => { + nock.enableNetConnect(); + }); + + afterEach(() => { + expect(nock.isDone()).to.be.true; + }); + + describe("fetchWebSetup", () => { + let configSetStub: sinon.SinonStub; + + beforeEach(() => { + sinon.stub(configstore, "get"); + configSetStub = sinon.stub(configstore, "set").returns(); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should fetch the web app config", async () => { + const projectId = "foo"; + nock(firebaseApiOrigin()) + .get(`/v1beta1/projects/${projectId}/webApps/-/config`) + .reply(200, { some: "config" }); + + const config = await fetchWebSetup({ project: projectId }); + + expect(config).to.deep.equal({ some: "config" }); + }); + + it("should store the fetched config", async () => { + const projectId = "projectId"; + nock(firebaseApiOrigin()) + .get(`/v1beta1/projects/${projectId}/webApps/-/config`) + .reply(200, { projectId, some: "config" }); + + await fetchWebSetup({ project: projectId }); + + expect(configSetStub).to.have.been.calledOnceWith("webconfig", { + [projectId]: { + projectId, + some: "config", + }, + }); + expect(nock.isDone()).to.be.true; + }); + + it("should throw an error if the request fails", async () => { + const projectId = "foo"; + 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", + ); + }); + + it("should return a fake config for a demo project id", async () => { + const projectId = "demo-project-1234"; + await expect(fetchWebSetup({ project: projectId })).to.eventually.deep.equal({ + projectId: "demo-project-1234", + databaseURL: "https://demo-project-1234.firebaseio.com", + storageBucket: "demo-project-1234.appspot.com", + apiKey: "fake-api-key", + authDomain: "demo-project-1234.firebaseapp.com", + }); + }); + }); + + describe("getCachedWebSetup", () => { + let configGetStub: sinon.SinonStub; + + beforeEach(() => { + sinon.stub(configstore, "set").returns(); + configGetStub = sinon.stub(configstore, "get"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return no config if none is cached", () => { + configGetStub.returns(undefined); + + const config = getCachedWebSetup({ project: "foo" }); + + expect(config).to.be.undefined; + }); + + it("should return a stored config", () => { + const projectId = "projectId"; + configGetStub.returns({ [projectId]: { project: projectId, some: "config" } }); + + const config = getCachedWebSetup({ project: projectId }); + + expect(config).to.be.deep.equal({ project: projectId, some: "config" }); + }); + }); +}); diff --git a/src/fetchWebSetup.ts b/src/fetchWebSetup.ts index d1f12738946..82bb8c21526 100644 --- a/src/fetchWebSetup.ts +++ b/src/fetchWebSetup.ts @@ -1,7 +1,9 @@ import { Client } from "./apiv2"; import { configstore } from "./configstore"; -import { firebaseApiOrigin } from "./api"; -import * as getProjectId from "./getProjectId"; +import { firebaseApiOrigin, hostingApiOrigin } from "./api"; +import { needProjectId } from "./projectUtils"; +import { logger } from "./logger"; +import { Constants } from "./emulator/constants"; export interface WebConfig { projectId: string; @@ -14,7 +16,29 @@ export interface WebConfig { messagingSenderId?: string; } -const apiClient = new Client({ urlPrefix: firebaseApiOrigin, auth: true, apiVersion: "v1beta1" }); +/** + * See + * https://firebase.google.com/docs/reference/hosting/rest/v1beta1/projects.sites#Site + */ +interface Site { + name: string; + defaultUrl: string; + appId?: string; + labels?: Record; + type: "DEFAULT_SITE" | "USER_SITE" | "SITE_UNSPECIFIED"; +} + +interface ListSitesResponse { + sites: Site[]; + nextPageToken: string; +} + +const apiClient = new Client({ urlPrefix: firebaseApiOrigin(), auth: true, apiVersion: "v1beta1" }); +const hostingApiClient = new Client({ + urlPrefix: hostingApiOrigin(), + auth: true, + apiVersion: "v1beta1", +}); const CONFIGSTORE_KEY = "webconfig"; @@ -30,20 +54,77 @@ function setCachedWebSetup(projectId: string, config: WebConfig): void { * @return web app configuration, or undefined. */ export function getCachedWebSetup(options: any): WebConfig | undefined { - const projectId = getProjectId(options, false); + const projectId = needProjectId(options); const allConfigs = configstore.get(CONFIGSTORE_KEY) || {}; return allConfigs[projectId]; } +/** + * Recursively list all hosting sites for a given project. + */ +async function listAllSites(projectId: string, nextPageToken?: string): Promise { + const queryParams: Record = nextPageToken ? { pageToken: nextPageToken } : {}; + const res = await hostingApiClient.get(`/projects/${projectId}/sites`, { + queryParams, + }); + + const sites = res.body.sites; + if (res.body.nextPageToken) { + const remainder = await listAllSites(projectId, res.body.nextPageToken); + return [...sites, ...remainder]; + } + + return sites; +} + +/** + * Construct a fake configuration based on the project ID. + */ +export function constructDefaultWebSetup(projectId: string): WebConfig { + return { + projectId, + databaseURL: `https://${projectId}.firebaseio.com`, + storageBucket: `${projectId}.appspot.com`, + apiKey: "fake-api-key", + authDomain: `${projectId}.firebaseapp.com`, + }; +} + /** * TODO: deprecate this function in favor of `getAppConfig()` in `/src/management/apps.ts` * @param options CLI options. * @return web app configuration. */ export async function fetchWebSetup(options: any): Promise { - const projectId = getProjectId(options, false); - const res = await apiClient.get(`/projects/${projectId}/webApps/-/config`); + const projectId = needProjectId(options); + + // When using the emulators with a fake project ID, use a fake web config + if (Constants.isDemoProject(projectId)) { + return constructDefaultWebSetup(projectId); + } + + // Try to determine the appId from the default Hosting site, if it is linked. + let hostingAppId: string | undefined = undefined; + try { + const sites = await listAllSites(projectId); + const defaultSite = sites.find((s) => s.type === "DEFAULT_SITE"); + if (defaultSite && defaultSite.appId) { + hostingAppId = defaultSite.appId; + } + } catch (e: any) { + logger.debug("Failed to list hosting sites"); + logger.debug(e); + } + + // Get the web app config for the appId, or use the '-' special value if the appId is not known + const appId = hostingAppId || "-"; + const res = await apiClient.get(`/projects/${projectId}/webApps/${appId}/config`); const config = res.body; + + if (!config.appId && hostingAppId) { + config.appId = hostingAppId; + } + setCachedWebSetup(config.projectId, config); return config; } diff --git a/src/filterTargets.js b/src/filterTargets.js deleted file mode 100644 index 036396866d7..00000000000 --- a/src/filterTargets.js +++ /dev/null @@ -1,38 +0,0 @@ -"use strict"; - -var _ = require("lodash"); -var { FirebaseError } = require("./error"); - -module.exports = function (options, validTargets) { - var targets = validTargets.filter(function (t) { - return options.config.has(t); - }); - if (options.only) { - targets = _.intersection( - targets, - options.only.split(",").map(function (opt) { - return opt.split(":")[0]; - }) - ); - } else if (options.except) { - targets = _.difference(targets, options.except.split(",")); - } - - if (targets.length === 0) { - let msg = "Cannot understand what targets to deploy/serve."; - - if (options.only) { - msg += ` No targets in firebase.json match '--only ${options.only}'.`; - } else if (options.except) { - msg += ` No targets in firebase.json match '--except ${options.except}'.`; - } - - if (process.platform === "win32") { - msg += - ' If you are using PowerShell make sure you place quotes around any comma-separated lists (ex: --only "functions,firestore").'; - } - - throw new FirebaseError(msg, { exit: 1 }); - } - return targets; -}; diff --git a/src/filterTargets.spec.ts b/src/filterTargets.spec.ts new file mode 100644 index 00000000000..43e1316977d --- /dev/null +++ b/src/filterTargets.spec.ts @@ -0,0 +1,53 @@ +import { expect } from "chai"; +import { filterTargets } from "./filterTargets"; +import { Options } from "./options"; +import { RC } from "./rc"; + +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 VALID_TARGETS = ["hosting", "functions"]; + +describe("filterTargets", () => { + it("should leave targets alone if no filtering is specified", () => { + const o = Object.assign(SAMPLE_OPTIONS, { + config: { + has: () => true, + }, + }); + expect(filterTargets(o, VALID_TARGETS)).to.deep.equal(["hosting", "functions"]); + }); + + it("should filter targets from --only", () => { + const o = Object.assign(SAMPLE_OPTIONS, { + config: { + has: () => true, + }, + only: "hosting", + }); + expect(filterTargets(o, VALID_TARGETS)).to.deep.equal(["hosting"]); + }); + + it("should filter out targets with --except", () => { + const o = Object.assign(SAMPLE_OPTIONS, { + config: { + has: () => true, + }, + except: "functions", + }); + expect(filterTargets(o, VALID_TARGETS)).to.deep.equal(["hosting"]); + }); +}); diff --git a/src/filterTargets.ts b/src/filterTargets.ts new file mode 100644 index 00000000000..6593968f27e --- /dev/null +++ b/src/filterTargets.ts @@ -0,0 +1,42 @@ +import { intersection, difference } from "lodash"; +import { FirebaseError } from "./error"; +import { Options } from "./options"; + +/** + * Filters targets from options with valid targets as specified. + * @param options CLI options. + * @param validTargets Targets that are valid. + * @return List of targets as specified and filtered by options and validTargets. + */ +export function filterTargets(options: Options, validTargets: string[]): string[] { + let targets = validTargets.filter((t) => { + return options.config.has(t); + }); + if (options.only) { + targets = intersection( + targets, + options.only.split(",").map((opt: string) => { + return opt.split(":")[0]; + }), + ); + } else if (options.except) { + targets = difference(targets, options.except.split(",")); + } + if (targets.length === 0) { + let msg = "Cannot understand what targets to deploy/serve."; + + if (options.only) { + msg += ` No targets in firebase.json match '--only ${options.only}'.`; + } else if (options.except) { + msg += ` No targets in firebase.json match '--except ${options.except}'.`; + } + + if (process.platform === "win32") { + msg += + ' If you are using PowerShell make sure you place quotes around any comma-separated lists (ex: --only "functions,firestore").'; + } + + throw new FirebaseError(msg); + } + return targets; +} diff --git a/src/firebaseConfig.ts b/src/firebaseConfig.ts new file mode 100644 index 00000000000..fc94a368351 --- /dev/null +++ b/src/firebaseConfig.ts @@ -0,0 +1,333 @@ +// +// NOTE: +// The contents of this file are used to generate the JSON Schema documents in +// the schema/ directory. After changing this file you will need to run +// 'npm run generate:json-schema' to regenerate the schema files. +// + +import type { HttpsOptions } from "firebase-functions/v2/https"; +import { IngressSetting, MemoryOption, VpcEgressSetting } from "firebase-functions/v2/options"; +import { ActiveRuntime } 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]; + +export type Deployable = { + predeploy?: string | string[]; + postdeploy?: string | string[]; +}; + +type DatabaseSingle = { + rules: string; +} & Deployable; + +type DatabaseMultiple = ({ + rules: string; +} & RequireAtLeastOne<{ + instance: string; + target: string; +}> & + Deployable)[]; + +type FirestoreSingle = { + database?: string; + location?: string; + edition?: string; + rules?: string; + indexes?: string; +} & Deployable; + +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; +}; + +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 & + ( + | DestinationRewrite + | LegacyFunctionsRewrite + | FunctionsRewrite + | RunRewrite + | DynamicLinksRewrite + ); + +export type HostingHeaders = HostingSource & { + headers: { + key: string; + value: string; + }[]; +}; + +// 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?: "AUTO" | "NONE"; + cleanUrls?: boolean; + trailingSlash?: boolean; + redirects?: HostingRedirects[]; + rewrites?: HostingRewrites[]; + headers?: HostingHeaders[]; + i18n?: { + root: string; + }; + frameworksBackend?: FrameworksBackendOptions; +}; + +export type HostingSingle = HostingBase & { + site?: string; + target?: string; +} & Deployable; + +// 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; + }> & + Deployable)[]; + +type StorageSingle = { + rules: string; + target?: string; +} & Deployable; + +type StorageMultiple = ({ + rules: string; + bucket: string; + target?: string; +} & Deployable)[]; + +// Full Configs +export type DatabaseConfig = DatabaseSingle | DatabaseMultiple; + +export type FirestoreConfig = FirestoreSingle | FirestoreMultiple; + +type FunctionConfigBase = { + // Optional: Directory containing the .env files for this codebase. + // Defaults to the same directory as source if not specified. + configDir?: string; + // Optional: List of glob patterns for files and directories to ignore during deployment. + // Uses gitignore-style syntax. Commonly includes node_modules, .git, etc. + ignore?: string[]; + // Optional: The Node.js/Python runtime version to use for Cloud Functions. + // Example: "nodejs20", "python312". Must be a supported runtime version. + runtime?: ActiveRuntime; + // Optional: A unique identifier for this functions codebase when using multiple codebases. + // Must be unique across all codebases in firebase.json. + codebase?: string; + // Optional: Applies a prefix to all function IDs (and secret names) discovered for this codebase. + // Must start with a lowercase letter; may contain lowercase letters, numbers, and dashes; + // cannot start or end with a dash; maximum length 30 characters. + prefix?: string; +} & Deployable; + +export type LocalFunctionConfig = FunctionConfigBase & { + // Directory containing the Cloud Functions source code. + source: string; + // Forbid remoteSource when local source is provided + remoteSource?: never; +}; + +export type RemoteFunctionConfig = FunctionConfigBase & { + // Deploy functions from a remote Git repository. + remoteSource: { + // The URL of the Git repository. + repository: string; + // The git ref (tag, branch, or commit hash) to deploy. + ref: string; + // The directory within the repository containing the functions source. + dir?: string; + }; + // Required for remote sources + runtime: ActiveRuntime; + // Forbid local source when remoteSource is provided + source?: never; +}; + +export type FunctionConfig = LocalFunctionConfig | RemoteFunctionConfig; + +export type FunctionsConfig = FunctionConfig | FunctionConfig[]; + +export type HostingConfig = HostingSingle | HostingMultiple; + +export type StorageConfig = StorageSingle | StorageMultiple; + +export type RemoteConfigConfig = { + template: string; +} & Deployable; + +export type EmulatorsConfig = { + auth?: { + host?: string; + port?: number; + }; + database?: { + host?: string; + port?: number; + }; + firestore?: { + host?: string; + port?: number; + websocketPort?: number; + }; + functions?: { + host?: string; + port?: number; + }; + hosting?: { + host?: string; + port?: number; + }; + apphosting?: { + host?: string; + port?: number; + startCommand?: string; + /** + * @deprecated + */ + startCommandOverride?: string; + rootDirectory?: string; + }; + pubsub?: { + host?: string; + port?: number; + }; + storage?: { + host?: string; + port?: number; + }; + logging?: { + host?: string; + port?: number; + }; + hub?: { + host?: string; + port?: number; + }; + ui?: { + enabled?: boolean; + host?: string; + port?: number; + }; + extensions?: {}; + eventarc?: { + host?: string; + port?: number; + }; + singleProjectMode?: boolean; + dataconnect?: { + host?: string; + port?: number; + postgresHost?: string; + postgresPort?: number; + dataDir?: string; + }; + tasks?: { + 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 AppHostingSingle = { + backendId: string; + rootDir: string; + ignore: string[]; + alwaysDeployFromSource?: boolean; + localBuild?: boolean; +}; + +export type AppHostingMultiple = AppHostingSingle[]; + +export type AppHostingConfig = AppHostingSingle | AppHostingMultiple; + +export type FirebaseConfig = { + /** + * @TJS-format uri + */ + $schema?: string; + database?: DatabaseConfig; + firestore?: FirestoreConfig; + functions?: FunctionsConfig; + hosting?: HostingConfig; + storage?: StorageConfig; + remoteconfig?: RemoteConfigConfig; + emulators?: EmulatorsConfig; + extensions?: ExtensionsConfig; + dataconnect?: DataConnectConfig; + apphosting?: AppHostingConfig; +}; diff --git a/src/firebaseConfigValidate.spec.ts b/src/firebaseConfigValidate.spec.ts new file mode 100644 index 00000000000..327011a577b --- /dev/null +++ b/src/firebaseConfigValidate.spec.ts @@ -0,0 +1,114 @@ +import { expect } from "chai"; +import { getValidator } from "./firebaseConfigValidate"; +import { FirebaseConfig } from "./firebaseConfig"; + +describe("firebaseConfigValidate", () => { + it("should accept a basic, valid config", () => { + const config: FirebaseConfig = { + database: { + rules: "myrules.json", + }, + hosting: { + public: "public", + }, + emulators: { + database: { + port: 8080, + }, + }, + }; + + const validator = getValidator(); + const isValid = validator(config); + + expect(isValid).to.be.true; + }); + + it("should report an extra top-level field", () => { + // This config has an extra 'bananas' top-level property + const config = { + database: { + rules: "myrules.json", + }, + bananas: {}, + }; + + const validator = getValidator(); + const isValid = validator(config); + + expect(isValid).to.be.false; + expect(validator.errors).to.exist; + expect(validator.errors!.length).to.eq(1); + + const firstError = validator.errors![0]; + expect(firstError.keyword).to.eq("additionalProperties"); + expect(firstError.instancePath).to.eq(""); + expect(firstError.params).to.deep.equal({ additionalProperty: "bananas" }); + }); + + it("should report a missing required field", () => { + // This config is missing 'storage.rules' + const config = { + storage: {}, + }; + + const validator = getValidator(); + const isValid = validator(config); + + expect(isValid).to.be.false; + expect(validator.errors).to.exist; + expect(validator.errors!.length).to.eq(3); + + const [firstError, secondError, thirdError] = validator.errors!; + + // Missing required param + expect(firstError.keyword).to.eq("required"); + expect(firstError.instancePath).to.eq("/storage"); + expect(firstError.params).to.deep.equal({ missingProperty: "rules" }); + + // Because it doesn't match the object type, we also get an "is not an array" + // error since JSON Schema can't tell which type it is closest to. + expect(secondError.keyword).to.eq("type"); + expect(secondError.instancePath).to.eq("/storage"); + expect(secondError.params).to.deep.equal({ type: "array" }); + + // Finally we get an error saying that 'storage' is not any of the known types + expect(thirdError.keyword).to.eq("anyOf"); + expect(thirdError.instancePath).to.eq("/storage"); + expect(thirdError.params).to.deep.equal({}); + }); + + it("should report a field with an incorrect type", () => { + // This config has a number where it should have a string + const config = { + storage: { + rules: 1234, + }, + }; + + const validator = getValidator(); + const isValid = validator(config); + + expect(isValid).to.be.false; + expect(validator.errors).to.exist; + expect(validator.errors!.length).to.eq(3); + + const [firstError, secondError, thirdError] = validator.errors!; + + // Wrong type + expect(firstError.keyword).to.eq("type"); + expect(firstError.instancePath).to.eq("/storage/rules"); + expect(firstError.params).to.deep.equal({ type: "string" }); + + // Because it doesn't match the object type, we also get an "is not an array" + // error since JSON Schema can't tell which type it is closest to. + expect(secondError.keyword).to.eq("type"); + expect(secondError.instancePath).to.eq("/storage"); + expect(secondError.params).to.deep.equal({ type: "array" }); + + // Finally we get an error saying that 'storage' is not any of the known types + expect(thirdError.keyword).to.eq("anyOf"); + expect(thirdError.instancePath).to.eq("/storage"); + expect(thirdError.params).to.deep.equal({}); + }); +}); diff --git a/src/firebaseConfigValidate.ts b/src/firebaseConfigValidate.ts new file mode 100644 index 00000000000..1ed2b1932cb --- /dev/null +++ b/src/firebaseConfigValidate.ts @@ -0,0 +1,44 @@ +// Note: Upgraded ajv from 6 to 8 as we upgraded from Typescript 3 +import { ValidateFunction, ErrorObject } from "ajv"; +import * as fs from "fs"; +import * as path from "path"; +import { Ajv } from "ajv"; +import addFormats from "ajv-formats"; + +// We need to allow union types becuase typescript-json-schema generates them sometimes. +const ajv = new Ajv({ allowUnionTypes: true }); +addFormats(ajv); +let _VALIDATOR: ValidateFunction | undefined = undefined; + +/** + * Lazily load the 'schema/firebase-config.json' file and return an AJV validation + * function. By doing this lazily we don't impose this I/O cost on those using + * the CLI as a Node module. + */ +export function getValidator(): ValidateFunction { + if (!_VALIDATOR) { + const schemaStr = fs.readFileSync( + path.resolve(__dirname, "../schema/firebase-config.json"), + "utf-8", + ); + const schema = JSON.parse(schemaStr); + + _VALIDATOR = ajv.compile(schema); + } + + return _VALIDATOR!; +} + +export function getErrorMessage(e: ErrorObject) { + if (e.keyword === "additionalProperties") { + return `Object "${e.instancePath}" in "firebase.json" has unknown property: ${JSON.stringify( + e.params, + )}`; + } else if (e.keyword === "required") { + return `Object "${ + e.instancePath + }" in "firebase.json" is missing required property: ${JSON.stringify(e.params)}`; + } else { + return `Field "${e.instancePath}" in "firebase.json" is possibly invalid: ${e.message}`; + } +} 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.spec.ts b/src/firestore/api-sort.spec.ts new file mode 100644 index 00000000000..c11da278a35 --- /dev/null +++ b/src/firestore/api-sort.spec.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..e4df69762f7 --- /dev/null +++ b/src/firestore/api-sort.ts @@ -0,0 +1,394 @@ +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 API_SCOPE_SEQUENCE = [ + API.ApiScope.ANY_API, + API.ApiScope.DATASTORE_MODE_API, + API.ApiScope.MONGODB_COMPATIBLE_API, + undefined, +]; + +const DENSITY_SEQUENCE = [ + API.Density.DENSITY_UNSPECIFIED, + API.Density.SPARSE_ALL, + API.Density.SPARSE_ANY, + API.Density.DENSE, + 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. + * 4) The API scope. + * 5) The index density. + * 6) Whether it's multikey. + * 7) Whether it's unique. + */ +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); + } + + let cmp = compareArrays(a.fields, b.fields, compareIndexField); + if (cmp !== 0) { + return cmp; + } + + cmp = compareApiScope(a.apiScope, b.apiScope); + if (cmp !== 0) { + return cmp; + } + + cmp = compareDensity(a.density, b.density); + if (cmp !== 0) { + return cmp; + } + + cmp = compareBoolean(a.multikey, b.multikey); + if (cmp !== 0) { + return cmp; + } + + return compareBoolean(a.unique, b.unique); +} + +/** + * Compare two Index api entries for sorting. + * + * Comparisons: + * 1) The collection group. + * 2) The query scope. + * 3) The fields list. + * 4) The API scope. + * 5) The index density. + * 6) Whether it's multikey. + * 7) Whether it's unique. + */ +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); + } + + let cmp = compareArrays(a.fields, b.fields, compareIndexField); + if (cmp !== 0) { + return cmp; + } + + cmp = compareApiScope(a.apiScope, b.apiScope); + if (cmp !== 0) { + return cmp; + } + + cmp = compareDensity(a.density, b.density); + if (cmp !== 0) { + return cmp; + } + + cmp = compareBoolean(a.multikey, b.multikey); + if (cmp !== 0) { + return cmp; + } + + return compareBoolean(a.unique, b.unique); +} + +/** + * 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); + } + + let cmp = compareApiScope(a.apiScope, b.apiScope); + if (cmp !== 0) { + return cmp; + } + + cmp = compareDensity(a.density, b.density); + if (cmp !== 0) { + return cmp; + } + + cmp = compareBoolean(a.multikey, b.multikey); + if (cmp !== 0) { + return cmp; + } + + return compareBoolean(a.unique, b.unique); +} + +function compareQueryScope(a: API.QueryScope, b: API.QueryScope): number { + return QUERY_SCOPE_SEQUENCE.indexOf(a) - QUERY_SCOPE_SEQUENCE.indexOf(b); +} + +function compareApiScope(a?: API.ApiScope, b?: API.ApiScope): number { + if (a === b) { + return 0; + } + if (a === undefined) { + return -1; + } + if (b === undefined) { + return 1; + } + return API_SCOPE_SEQUENCE.indexOf(a) - API_SCOPE_SEQUENCE.indexOf(b); +} + +function compareDensity(a?: API.Density, b?: API.Density): number { + if (a === b) { + return 0; + } + if (a === undefined) { + return -1; + } + if (b === undefined) { + return 1; + } + return DENSITY_SEQUENCE.indexOf(a) - DENSITY_SEQUENCE.indexOf(b); +} + +function compareOrder(a?: API.Order, b?: API.Order): number { + return ORDER_SEQUENCE.indexOf(a) - ORDER_SEQUENCE.indexOf(b); +} + +function compareBoolean(a?: boolean, b?: boolean): number { + if (a === b) { + return 0; + } + if (a === undefined) { + return -1; + } + if (b === undefined) { + return 1; + } + return Number(a) - Number(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..c3ed1f52984 --- /dev/null +++ b/src/firestore/api-spec.ts @@ -0,0 +1,51 @@ +/** + * 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[]; + apiScope?: API.ApiScope; + density?: API.Density; + multikey?: boolean; + unique?: boolean; +} + +/** + * 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; + apiScope?: API.ApiScope; + density?: API.Density; + multikey?: boolean; + unique?: boolean; +} + +/** + * 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..3809913486e --- /dev/null +++ b/src/firestore/api-types.ts @@ -0,0 +1,241 @@ +/** + * 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 ApiScope { + ANY_API = "ANY_API", + DATASTORE_MODE_API = "DATASTORE_MODE_API", + MONGODB_COMPATIBLE_API = "MONGODB_COMPATIBLE_API", +} + +export enum Density { + DENSITY_UNSPECIFIED = "DENSITY_UNSPECIFIED", + SPARSE_ALL = "SPARSE_ALL", + SPARSE_ANY = "SPARSE_ANY", + DENSE = "DENSE", +} + +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; + apiScope?: ApiScope; + density?: Density; + multikey?: boolean; + unique?: boolean; +} + +/** + * 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 enum DatabaseEdition { + DATABASE_EDITION_UNSPECIFIED = "DATABASE_EDITION_UNSPECIFIED", + STANDARD = "STANDARD", + ENTERPRISE = "ENTERPRISE", +} + +export interface DatabaseReq { + locationId?: string; + type?: DatabaseType; + databaseEdition?: DatabaseEdition; + deleteProtectionState?: DatabaseDeleteProtectionState; + pointInTimeRecoveryEnablement?: PointInTimeRecoveryEnablement; + cmekConfig?: CmekConfig; +} + +export interface CreateDatabaseReq { + project: string; + databaseId: string; + locationId: string; + type: DatabaseType; + databaseEdition?: DatabaseEdition; + deleteProtectionState: DatabaseDeleteProtectionState; + pointInTimeRecoveryEnablement: PointInTimeRecoveryEnablement; + cmekConfig?: CmekConfig; +} + +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; + cmekConfig?: CmekConfig; + databaseEdition?: DatabaseEdition; +} + +export interface BulkDeleteDocumentsRequest { + // Database to operate. Should be of the form: + // `projects/{project_id}/databases/{database_id}`. + name: string; + // IDs of the collection groups to delete. Unspecified means *all* collection groups. + // Each collection group in this list must be unique. + collectionIds?: string[]; +} + +export type BulkDeleteDocumentsResponse = { + name?: string; +}; + +export interface Operation { + name: string; + done: boolean; + metadata: Record; + response?: Record; + error?: { + name: string; + message: string; + code: number; + details?: any[]; + }; +} + +export interface ListOperationsResponse { + operations: Operation[]; +} + +export interface RestoreDatabaseReq { + databaseId: string; + backup: string; + encryptionConfig?: EncryptionConfig; +} + +export enum RecurrenceType { + DAILY = "DAILY", + WEEKLY = "WEEKLY", +} + +export interface CmekConfig { + kmsKeyName: string; + activeKeyVersion?: string[]; +} + +type UseGoogleDefaultEncryption = { googleDefaultEncryption: Record }; +type UseSourceEncryption = { useSourceEncryption: Record }; +type UseCustomerManagedEncryption = { customerManagedEncryption: CustomerManagedEncryptionOptions }; +type CustomerManagedEncryptionOptions = { + kmsKeyName: string; +}; +export type EncryptionConfig = + | UseCustomerManagedEncryption + | UseSourceEncryption + | UseGoogleDefaultEncryption; diff --git a/src/firestore/api.ts b/src/firestore/api.ts new file mode 100644 index 00000000000..f3cce409e9f --- /dev/null +++ b/src/firestore/api.ts @@ -0,0 +1,972 @@ +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 { DatabaseEdition, Density } 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"; +import { optionalValueMatches } from "../functional"; +import { pollOperation } from "../operation-poller"; + +export class FirestoreApi { + apiClient = new Client({ urlPrefix: firestoreOrigin(), apiVersion: "v1" }); + printer = new PrettyPrint(); + + /** + * Process indexes by appending the implicit __name__ fields with default order for STANDARD edition database. + * No-op if exists __name__ field at the end. + * No-op is ENTERPRISE edition databases. + * @param index Spec index to process + * @return Processed spec index with potential additional __name__ suffix + */ + public static processIndex(index: Spec.Index): Spec.Index { + // Per https://firebase.google.com/docs/firestore/query-data/index-overview#default_ordering_and_the_name_field + // this matches the direction of the last non-name field in the index. + const fields = index.fields; + const lastField = index.fields?.[index.fields.length - 1]; + if (lastField?.fieldPath !== "__name__") { + const defaultDirection = index.fields?.[index.fields.length - 1]?.order; + const nameSuffix = { fieldPath: "__name__", order: defaultDirection } as types.IndexField; + fields.push(nameSuffix); + } + return { + ...index, + fields, + }; + } + + /** + * 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 database = await this.getDatabase(options.project, databaseId); + const edition = database.databaseEdition ?? DatabaseEdition.STANDARD; + const indexesToDelete = existingIndexes.filter((index) => { + return !indexesToDeploy.some((spec) => this.indexMatchesSpec(index, spec, edition)); + }); + + // 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, edition)); + 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; + } + + /** + * 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, + apiScope: index.apiScope, + density: index.density, + multikey: index.multikey, + unique: index.unique, + }; + }); + + 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, + apiScope: index.apiScope, + density: index.density, + multikey: index.multikey, + unique: index.unique, + }; + }), + }; + }); + + 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)); + + if (index.apiScope) { + validator.assertEnum(index, "apiScope", Object.keys(types.ApiScope)); + } + + if (index.density) { + validator.assertEnum(index, "density", Object.keys(types.Density)); + } + + if (index.multikey) { + validator.assertType("multikey", index.multikey, "boolean"); + } + + if (index.unique !== undefined) { + validator.assertType("unique", index.unique, "boolean"); + // TODO(b/439901837): Remove this check and update indexMatchesSpec once + // unique index configuration is supported. + throw new FirebaseError("The `unique` index configuration is not supported yet."); + } + + 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)); + } + + if (index.apiScope) { + validator.assertEnum(index, "apiScope", Object.keys(types.ApiScope)); + } + + if (index.density) { + validator.assertEnum(index, "density", Object.keys(types.Density)); + } + + if (index.multikey) { + validator.assertType("multikey", index.multikey, "boolean"); + } + + if (index.unique) { + validator.assertType("unique", index.unique, "boolean"); + } + }); + } + + /** + * 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, + apiScope: index.apiScope, + density: index.density, + multikey: index.multikey, + unique: index.unique, + 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, + apiScope: index.apiScope, + density: index.density, + multikey: index.multikey, + unique: index.unique, + }); + } + + /** + * Delete an existing index on the specified project. + */ + deleteIndex(index: types.Index): Promise { + const url = index.name!; + return this.apiClient.delete(`/${url}`); + } + + /** + * Returns true if the given ApiScope values match. + * If either one is undefined, the default value is used for comparison. + * @param lhs the first ApiScope value. + * @param rhs the second ApiScope value. + */ + optionalApiScopeMatches( + lhs: types.ApiScope | undefined, + rhs: types.ApiScope | undefined, + ): boolean { + return optionalValueMatches(lhs, rhs, types.ApiScope.ANY_API); + } + + /** + * Returns true if the given Density values match. + * If either one is undefined, the default value is used for comparison based on Database Edition. + * @param lhs the first Density value. + * @param rhs the second Density value. + * @param edition the database edition used to determine the default value. + */ + optionalDensityMatches( + lhs: Density | undefined, + rhs: Density | undefined, + edition: types.DatabaseEdition, + ): boolean { + const defaultValue = + edition === DatabaseEdition.STANDARD ? types.Density.SPARSE_ALL : types.Density.DENSE; + return optionalValueMatches(lhs, rhs, defaultValue); + } + + /** + * Returns true if the given Multikey values match. + * If either one is undefined, the default value is used for comparison. + * @param lhs the first Multikey value. + * @param rhs the second Multikey value. + */ + optionalMultikeyMatches(lhs: boolean | undefined, rhs: boolean | undefined): boolean { + const defaultValue = false; + return optionalValueMatches(lhs, rhs, defaultValue); + } + + /** + * Determine if an API Index and a Spec Index are functionally equivalent. + */ + indexMatchesSpec(index: types.Index, spec: Spec.Index, edition: types.DatabaseEdition): boolean { + const collection = util.parseIndexName(index.name).collectionGroupId; + if (collection !== spec.collectionGroup) { + return false; + } + + if (index.queryScope !== spec.queryScope) { + return false; + } + + // apiScope is an optional value and may be missing in firestore.indexes.json, + // and may also be missing from the server value (when default is picked). + if (!this.optionalApiScopeMatches(index.apiScope, spec.apiScope)) { + return false; + } + + // density is an optional value and may be missing in firestore.indexes.json, + // and may also be missing from the server value (when default is picked). + if (!this.optionalDensityMatches(index.density, spec.density, edition)) { + return false; + } + // multikey is an optional value and may be missing in firestore.indexes.json, + // and may also be missing from the server value (when default is picked). + if (!this.optionalMultikeyMatches(index.multikey, spec.multikey)) { + return false; + } + + // TODO(b/439901837): Compare `unique` index configuration when it's supported. + + let specIdx = spec; + if (edition === DatabaseEdition.STANDARD) { + specIdx = FirestoreApi.processIndex(specIdx); + } + + if (index.fields.length !== specIdx.fields.length) { + return false; + } + + let i = 0; + while (i < index.fields.length) { + const iField = index.fields[i]; + const sField = specIdx.fields[i]; + + if (iField.fieldPath !== sField.fieldPath) { + return false; + } + + if (iField.order !== sField.order) { + return false; + } + + if (iField.arrayConfig !== sField.arrayConfig) { + return false; + } + + // Note: vectorConfig is an object, and using '!==' should not be used. + if (!utils.deepEqual(iField.vectorConfig, sField.vectorConfig)) { + 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: any = { + collectionGroup: index.collectionGroup || index.collectionId, + queryScope: index.queryScope || types.QueryScope.COLLECTION, + }; + + if (index.apiScope) { + i.apiScope = index.apiScope; + } + if (index.density) { + i.density = index.density; + } + if (index.multikey !== undefined) { + i.multikey = index.multikey; + } + if (index.unique !== undefined) { + i.unique = index.unique; + } + + 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 req the request to create a database + */ + async createDatabase(req: types.CreateDatabaseReq): Promise { + const url = `/projects/${req.project}/databases`; + const payload: types.DatabaseReq = { + locationId: req.locationId, + type: req.type, + databaseEdition: req.databaseEdition, + deleteProtectionState: req.deleteProtectionState, + pointInTimeRecoveryEnablement: req.pointInTimeRecoveryEnablement, + cmekConfig: req.cmekConfig, + }; + const options = { queryParams: { databaseId: req.databaseId } }; + const res = await this.apiClient.post< + types.DatabaseReq, + { name: string; response?: types.DatabaseResp } + >(url, payload, options); + await pollOperation({ + apiOrigin: firestoreOrigin(), + apiVersion: "v1", + operationResourceName: res.body.name, + masterTimeout: 600000, + }); + 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; + } + + /** + * Bulk delete documents from a Firestore database. + * @param project the Firebase project id. + * @param databaseId the id of the Firestore Database. + * @param collectionIds the collection IDs to delete. + */ + async bulkDeleteDocuments( + project: string, + databaseId: string, + collectionIds: string[], + ): Promise { + const name = `/projects/${project}/databases/${databaseId}`; + const url = `${name}:bulkDeleteDocuments`; + const payload: types.BulkDeleteDocumentsRequest = { + name, + collectionIds, + }; + const res = await this.apiClient.post< + types.BulkDeleteDocumentsRequest, + types.BulkDeleteDocumentsResponse + >(url, payload); + return { + name: res.body?.name, + }; + } + + /** + * 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 + * @param encryptionConfig the encryption configuration of the restored database + */ + async restoreDatabase( + project: string, + databaseId: string, + backupName: string, + encryptionConfig?: types.EncryptionConfig, + ): Promise { + const url = `/projects/${project}/databases:restore`; + const payload: types.RestoreDatabaseReq = { + databaseId, + backup: backupName, + encryptionConfig: encryptionConfig, + }; + 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; + } + + /** + * List the long-running Firestore operations. + * @param project the Firebase project id. + * @param databaseId the id of the Firestore Database. + * @param limit The maximum number of operations to list. + */ + async listOperations( + project: string, + databaseId: string, + limit: number, + ): Promise { + const url = `/projects/${project}/databases/${databaseId}/operations`; + const res = await this.apiClient.get(url, { + queryParams: { + pageSize: limit, + }, + }); + return res.body; + } + + /** + * Retrieves the information related to the LRO with the given name. + * @param project the Firebase project id. + * @param databaseId the id of the Firestore Database. + * @param operationName the name of the LRO. + */ + async describeOperation( + project: string, + databaseId: string, + operationName: string, + ): Promise { + const url = `/projects/${project}/databases/${databaseId}/operations/${operationName}`; + const res = await this.apiClient.get(url); + return res.body; + } + + /** + * Cancels the LRO with the given name. + * @param project the Firebase project id. + * @param databaseId the id of the Firestore Database. + * @param operationName the name of the LRO. + */ + async cancelOperation( + project: string, + databaseId: string, + operationName: string, + ): Promise<{ success: boolean }> { + const url = `/projects/${project}/databases/${databaseId}/operations/${operationName}:cancel`; + try { + const res = await this.apiClient.post(url); + return { success: res.status === 200 }; + } catch (error) { + // For the cases when the user is trying to cancel an operation that has + // already completed, the response is not very useful. The error message is + // "Precondition check failed.". And one has to parse the details of the error + // stack to find out the real reason. We try to improve the error message here. + const reason = "Cannot cancel an operation that is completed."; + const details = (error as any).context?.body?.error?.details || []; + for (const detail of details) { + if (detail.detail?.includes(reason)) { + throw new FirebaseError(reason); + } + } + // If we weren't able to provide a better reason, rethrow the original error. + throw error; + } + } +} diff --git a/src/firestore/backupUtils.spec.ts b/src/firestore/backupUtils.spec.ts new file mode 100644 index 00000000000..de338efd402 --- /dev/null +++ b/src/firestore/backupUtils.spec.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..7c0b0195b40 100644 --- a/src/firestore/checkDatabaseType.ts +++ b/src/firestore/checkDatabaseType.ts @@ -1,25 +1,42 @@ -import * as api from "../api"; +import { firestoreOrigin } from "../api"; +import { Client } from "../apiv2"; import { logger } from "../logger"; +import { FirebaseError } from "../error"; /** * 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) + * - DATABASE_DOES_NOT_EXIST (Database does not exist on specified project) * * @param projectId the Firebase project ID. + * @param databaseId the Firestore database ID. */ -export async function checkDatabaseType(projectId: string): Promise { +export async function checkDatabaseType( + projectId: string, + databaseId: string = "(default)", +): Promise< + | "DATASTORE_MODE" + | "FIRESTORE_NATIVE" + | "DATABASE_TYPE_UNSPECIFIED" + | "DATABASE_DOES_NOT_EXIST" + | undefined +> { try { - const resp = await api.request("GET", "/v1/apps/" + projectId, { - auth: true, - origin: api.appengineOrigin, - }); - - return resp.body.databaseType; - } catch (err) { - logger.debug("error getting database type", 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/${databaseId}`); + return resp.body.type; + } catch (err: any) { + logger.debug("error getting database type: ", err); + if (err instanceof FirebaseError) { + if (err.status === 404) { + logger.info(`${databaseId} does not exist in project ${projectId}.`); + return "DATABASE_DOES_NOT_EXIST"; + } + } return undefined; } } diff --git a/src/firestore/delete.ts b/src/firestore/delete.ts index e72f9fdf457..34bf6d1909c 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"; @@ -28,6 +28,7 @@ export class FirestoreDelete { total: Number.MAX_SAFE_INTEGER, }); + private urlPrefix: string; private apiClient: apiv2.Client; public isDocumentPath: boolean; @@ -38,6 +39,7 @@ export class FirestoreDelete { private recursive: boolean; private shallow: boolean; private allCollections: boolean; + private databaseId: string; private readBatchSize: number; private maxPendingDeletes: number; @@ -57,17 +59,26 @@ export class FirestoreDelete { * - options.recursive true if the delete should be recursive. * - options.shallow true if the delete should be shallow (non-recursive). * - options.allCollections true if the delete should universally remove all collections and docs. + * - options.urlPrefix if specified initializes the client to use the given url, otherwise determine from environment */ constructor( project: string, path: string | undefined, - options: { recursive?: boolean; shallow?: boolean; allCollections?: boolean } + options: { + recursive?: boolean; + shallow?: boolean; + allCollections?: boolean; + databaseId: string; + urlPrefix?: 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; + this.urlPrefix = options.urlPrefix ?? firestoreOriginOrEmulator(); // Tunable deletion parameters this.readBatchSize = 7500; @@ -79,7 +90,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 +117,7 @@ export class FirestoreDelete { this.apiClient = new apiv2.Client({ auth: true, apiVersion: "v1", - urlPrefix: firestoreOriginOrEmulator, + urlPrefix: this.urlPrefix, }); } @@ -158,7 +170,7 @@ export class FirestoreDelete { private collectionDescendantsQuery( allDescendants: boolean, batchSize: number, - startAfter?: string + startAfter?: string, ) { const nullChar = String.fromCharCode(0); @@ -277,7 +289,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 +398,7 @@ export class FirestoreDelete { numPendingDeletes++; firestore - .deleteDocuments(this.project, toDelete) + .deleteDocuments(this.project, toDelete, this.databaseId, this.urlPrefix) .then((numDeleted) => { FirestoreDelete.progressBar.tick(numDeleted); numDocsDeleted += numDeleted; @@ -411,7 +423,25 @@ 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); + } + + // Retry this batch + queue.unshift(...toDelete); + } else if ( + e.status === 429 && + this.deleteBatchSize >= 10 && + e.message.includes("database has exceeded their maximum bandwidth") + ) { + logger.debug("Database has exceeded maximum write bandwidth", e); + const newBatchSize = Math.floor(toDelete.length / 2); + + if (newBatchSize < this.deleteBatchSize) { + utils.logLabeledWarning( + "firestore", + `delete rate exceeding maximum bandwidth, reducing batch size from ${this.deleteBatchSize} to ${newBatchSize}`, ); this.setDeleteBatchSize(newBatchSize); } @@ -446,7 +476,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 +503,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, this.urlPrefix).catch((err) => { logger.debug("deletePath:initialDelete:error", err); if (this.allDescendants) { // On a recursive delete, we are insensitive to @@ -500,7 +530,7 @@ export class FirestoreDelete { */ public deleteDatabase(): Promise { return firestore - .listCollectionIds(this.project) + .listCollectionIds(this.project, this.databaseId, this.urlPrefix) .catch((err) => { logger.debug("deleteDatabase:listCollectionIds:error", err); return utils.reject("Unable to list collection IDs"); @@ -514,6 +544,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 +558,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 +585,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..9ae6466cf7c --- /dev/null +++ b/src/firestore/indexes.spec.ts @@ -0,0 +1,1357 @@ +import { expect } from "chai"; +import { FirestoreApi } from "./api"; +import { FirebaseError } from "../error"; +import * as API from "./api-types"; +import { ApiScope, DatabaseEdition, Density } 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 accept a valid index spec with apiScope, density, and multikey", () => { + const spec = { + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + apiScope: "ANY_API", + density: "DENSE", + multikey: true, + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "bar", order: "DESCENDING" }, + { fieldPath: "baz", arrayConfig: "CONTAINS" }, + ], + }, + ], + }; + idx.validateSpec(spec); + }); + + it("should reject an index spec with invalid apiScope", () => { + const spec = { + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + apiScope: "UNKNOWN", + fields: [], + }, + ], + }; + expect(() => { + idx.validateSpec(spec); + }).to.throw( + FirebaseError, + /Field "apiScope" must be one of ANY_API, DATASTORE_MODE_API, MONGODB_COMPATIBLE_API/, + ); + }); + + it("should reject an index spec with invalid density", () => { + const spec = { + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + apiScope: "ANY_API", + density: "UNKNOWN", + fields: [], + }, + ], + }; + expect(() => { + idx.validateSpec(spec); + }).to.throw( + FirebaseError, + /Field "density" must be one of DENSITY_UNSPECIFIED, SPARSE_ALL, SPARSE_ANY, DENSE/, + ); + }); + + it("should reject an index spec with invalid multikey", () => { + const spec = { + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + apiScope: "ANY_API", + density: "DENSE", + multikey: "multikey", + fields: [], + }, + ], + }; + expect(() => { + idx.validateSpec(spec); + }).to.throw(FirebaseError, /Property "multikey" must be of type boolean/); + }); + + it("should reject an index spec with invalid unique", () => { + const spec = { + indexes: [ + { + collectionGroup: "collection", + queryScope: "COLLECTION", + apiScope: "ANY_API", + density: "DENSE", + multikey: true, + unique: "true", + fields: [], + }, + ], + }; + expect(() => { + idx.validateSpec(spec); + }).to.throw(FirebaseError, /Property "unique" must be of type boolean/); + }); + + 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", () => { + const baseApiIndex: API.Index = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: API.QueryScope.COLLECTION, + fields: [{ fieldPath: "__name__", order: API.Order.ASCENDING }], + }; + + const baseSpecIndex: Spec.Index = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [{ fieldPath: "__name__", order: API.Order.ASCENDING }], + } as Spec.Index; + + 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 }, + { fieldPath: "baz", vectorConfig: { dimension: 384, flat: {} } }, + { fieldPath: "__name__", order: API.Order.ASCENDING }, + ], + state: API.State.READY, + }; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "bar", arrayConfig: "CONTAINS" }, + { fieldPath: "baz", vectorConfig: { dimension: 384, flat: {} } }, + { fieldPath: "__name__", order: API.Order.ASCENDING }, + ], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true); + }); + + it("should identify a negative index spec match with different vector config dimension", () => { + const apiIndex = { + ...baseApiIndex, + fields: [{ fieldPath: "baz", vectorConfig: { dimension: 384, flat: {} } }], + }; + + const specIndex = { + ...baseSpecIndex, + fields: [{ fieldPath: "baz", vectorConfig: { dimension: 382, flat: {} } }], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false); + }); + + it("should identify a positive index spec match with apiScope, density, multikey, and unique", () => { + const apiIndex: API.Index = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: API.QueryScope.COLLECTION, + apiScope: API.ApiScope.ANY_API, + density: API.Density.DENSE, + multikey: true, + unique: true, + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "bar", arrayConfig: API.ArrayConfig.CONTAINS }, + ], + state: API.State.READY, + }; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + apiScope: "ANY_API", + density: "DENSE", + multikey: true, + unique: true, + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "bar", arrayConfig: "CONTAINS" }, + ], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true); + }); + + it("should identify a positive index spec match when missing apiScope and density and multikey in both", () => { + expect(idx.indexMatchesSpec(baseApiIndex, baseSpecIndex, DatabaseEdition.STANDARD)).to.eql( + true, + ); + expect(idx.indexMatchesSpec(baseApiIndex, baseSpecIndex, DatabaseEdition.ENTERPRISE)).to.eql( + true, + ); + }); + + describe("ApiScope", () => { + it("should identify a negative index spec match with different apiScope", () => { + const apiIndex = { ...baseApiIndex, apiScope: API.ApiScope.ANY_API }; + const specIndex = { ...baseSpecIndex, apiScope: "MONGODB_COMPATIBLE_API" } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false); + }); + + it("should identify a positive index spec match with same apiScope ANY_API", () => { + const apiIndex = { ...baseApiIndex, apiScope: API.ApiScope.ANY_API }; + const specIndex = { ...baseSpecIndex, apiScope: "ANY_API" } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true); + }); + + it("should identify a positive index spec match with same apiScope MONGODB_COMPATIBLE_API", () => { + const apiIndex = { ...baseApiIndex, apiScope: API.ApiScope.MONGODB_COMPATIBLE_API }; + const specIndex = { ...baseSpecIndex, apiScope: "MONGODB_COMPATIBLE_API" } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true); + }); + + it("should identify a positive match, apiScope missing in index, default in spec", () => { + const apiIndex = baseApiIndex; + const specIndex = { ...baseSpecIndex, apiScope: "ANY_API" } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true); + }); + + it("should identify a positive match, apiScope missing in spec, default in index", () => { + const apiIndex = { ...baseApiIndex, apiScope: ApiScope.ANY_API }; + const specIndex = baseSpecIndex; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true); + }); + + it("should identify a negative match, apiScope missing in index, non-default in spec", () => { + const apiIndex = baseApiIndex; + const specIndex = { ...baseSpecIndex, apiScope: "MONGODB_COMPATIBLE_API" } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false); + }); + + it("should identify a negative match, apiScope missing in spec, non-default in index", () => { + const apiIndex = { ...baseApiIndex, apiScope: ApiScope.MONGODB_COMPATIBLE_API }; + const specIndex = baseSpecIndex; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false); + }); + }); + + describe("Density", () => { + it("should identify a negative index spec match with different density", () => { + const apiIndex = { ...baseApiIndex, density: API.Density.DENSE }; + const specIndex = { ...baseSpecIndex, density: "SPARSE_ALL" } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false); + }); + + it("should identify a positive index spec match with same density DENSE", () => { + const apiIndex = { ...baseApiIndex, density: API.Density.DENSE }; + const specIndex = { ...baseSpecIndex, density: "DENSE" } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true); + }); + + it("should identify a positive index spec match with same density SPARSE_ALL", () => { + const apiIndex = { ...baseApiIndex, density: API.Density.SPARSE_ALL }; + const specIndex = { ...baseSpecIndex, density: "SPARSE_ALL" } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true); + }); + + it("should identify a positive index spec match with same density SPARSE_ANY", () => { + const apiIndex = { ...baseApiIndex, density: API.Density.SPARSE_ANY }; + const specIndex = { ...baseSpecIndex, density: "SPARSE_ANY" } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true); + }); + + it("should identify a positive match, density missing in index, default in spec", () => { + // The default value for Enterprise is DENSE + const apiIndex1 = baseApiIndex; + const specIndex1 = { ...baseSpecIndex, density: "DENSE" } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex1, specIndex1, DatabaseEdition.STANDARD)).to.eql(false); + expect(idx.indexMatchesSpec(apiIndex1, specIndex1, DatabaseEdition.ENTERPRISE)).to.eql(true); + + // The default value for Standard is SPARSE_ALL + const apiIndex = baseApiIndex; + const specIndex = { ...baseSpecIndex, density: "SPARSE_ALL" } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false); + }); + + it("should identify a positive match, density missing in spec, default in index", () => { + // The default value for Enterprise is DENSE + const apiIndex1 = { ...baseApiIndex, density: Density.DENSE }; + const specIndex1 = baseSpecIndex; + expect(idx.indexMatchesSpec(apiIndex1, specIndex1, DatabaseEdition.STANDARD)).to.eql(false); + expect(idx.indexMatchesSpec(apiIndex1, specIndex1, DatabaseEdition.ENTERPRISE)).to.eql(true); + + // The default value for Standard is SPARSE_ALL + const apiIndex2 = { ...baseApiIndex, density: Density.SPARSE_ALL }; + const specIndex2 = baseSpecIndex; + expect(idx.indexMatchesSpec(apiIndex2, specIndex2, DatabaseEdition.STANDARD)).to.eql(true); + expect(idx.indexMatchesSpec(apiIndex2, specIndex2, DatabaseEdition.ENTERPRISE)).to.eql(false); + }); + + it("should identify a negative match, density missing in index, non-default in spec", () => { + // The default value for Enterprise is DENSE, and the default value for Standard is SPARSE_ALL + // so using SPARSE_ANY should fail both. + const apiIndex = baseApiIndex; + const specIndex = { ...baseSpecIndex, density: "SPARSE_ANY" } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false); + }); + + it("should identify a negative match, density missing in spec, non-default in index", () => { + // The default value for Enterprise is DENSE, and the default value for Standard is SPARSE_ALL + // so using SPARSE_ANY should fail both. + const apiIndex1 = { ...baseApiIndex, density: Density.SPARSE_ANY }; + const specIndex1 = baseSpecIndex; + expect(idx.indexMatchesSpec(apiIndex1, specIndex1, DatabaseEdition.STANDARD)).to.eql(false); + expect(idx.indexMatchesSpec(apiIndex1, specIndex1, DatabaseEdition.ENTERPRISE)).to.eql(false); + }); + }); + + describe("Multikey", () => { + it("should identify a negative index spec match with different multikey", () => { + const apiIndex = { ...baseApiIndex, multikey: true }; + const specIndex = { ...baseSpecIndex, multikey: false } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false); + }); + + it("should identify a positive index spec match with same multikey true", () => { + const apiIndex = { ...baseApiIndex, multikey: true }; + const specIndex = { ...baseSpecIndex, multikey: true } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true); + }); + + it("should identify a positive index spec match with same multikey false", () => { + const apiIndex = { ...baseApiIndex, multikey: false }; + const specIndex = { ...baseSpecIndex, multikey: false } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true); + }); + + it("should identify a positive match, multikey missing in index, default in spec", () => { + const apiIndex = baseApiIndex; + const specIndex = { ...baseSpecIndex, multikey: false } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true); + }); + + it("should identify a positive match, multikey missing in spec, default in index", () => { + const apiIndex = { ...baseApiIndex, multikey: false }; + const specIndex = baseSpecIndex; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true); + }); + + it("should identify a negative match, multikey missing in index, non-default in spec", () => { + const apiIndex = baseApiIndex; + const specIndex = { ...baseSpecIndex, multikey: true } as Spec.Index; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false); + }); + + it("should identify a negative match, multikey missing in spec, non-default in index", () => { + const apiIndex = { ...baseApiIndex, multikey: true }; + const specIndex = baseSpecIndex; + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false); + }); + }); + + describe("With __name__ field in index", () => { + it("__name__ field with default sort order, identified as matching", () => { + const apiIndex = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "__name__", order: "ASCENDING" }, + ], + state: API.State.READY, + } as API.Index; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "__name__", order: "ASCENDING" }, + ], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true); + }); + + it("__name__ field with default sort order stripped off, identified as matching if STANDARD", () => { + const apiIndex = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "__name__", order: "ASCENDING" }, + ], + state: API.State.READY, + } as API.Index; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [{ fieldPath: "foo", order: "ASCENDING" }], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true); + }); + + it("__name__ field with non-default sort order, identified as matching", () => { + const apiIndex = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "__name__", order: "DESCENDING" }, + ], + state: API.State.READY, + } as API.Index; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "__name__", order: "DESCENDING" }, + ], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(true); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(true); + }); + + it("__name__ field sort order mismatch, identified as not matching", () => { + const apiIndex = { + name: "/projects/myproject/databases/(default)/collectionGroups/collection/indexes/abc123", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "__name__", order: "ASCENDING" }, + ], + state: API.State.READY, + } as API.Index; + + const specIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: "ASCENDING" }, + { fieldPath: "__name__", order: "DESCENDING" }, + ], + } as Spec.Index; + + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.STANDARD)).to.eql(false); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).to.eql(false); + }); + }); + + 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, DatabaseEdition.STANDARD)).to.eql(false); + expect(idx.indexMatchesSpec(apiIndex, specIndex, DatabaseEdition.ENTERPRISE)).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("Normalize __name__ field for database indexes", () => { + it("No-op if exists __name__ field as the last field with default sort order", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "__name__", order: API.Order.ASCENDING }, + ], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(2); + expect(result.fields[0].fieldPath).to.equal("foo"); + expect(result.fields[1].fieldPath).to.equal("__name__"); + expect(result.fields[1].order).to.equal(API.Order.ASCENDING); + }); + + it("No-op if exists __name__ field as the last field with non-default sort order", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "__name__", order: API.Order.DESCENDING }, + ], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(2); + expect(result.fields[0].fieldPath).to.equal("foo"); + expect(result.fields[1].fieldPath).to.equal("__name__"); + expect(result.fields[1].order).to.equal(API.Order.DESCENDING); + }); + + it("should attach __name__ suffix with the default order if not exists, ascending", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [{ fieldPath: "foo", order: API.Order.ASCENDING }], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(2); + expect(result.fields[0].fieldPath).to.equal("foo"); + expect(result.fields[1].fieldPath).to.equal("__name__"); + expect(result.fields[1].order).to.equal(API.Order.ASCENDING); + }); + + it("should attach __name__ suffix with the default order if not exists, descending", () => { + const mockSpecIndex = { + collectionGroup: "collection", + queryScope: "COLLECTION", + fields: [ + { fieldPath: "foo", order: API.Order.ASCENDING }, + { fieldPath: "bar", order: API.Order.DESCENDING }, + ], + } as Spec.Index; + + const result = FirestoreApi.processIndex(mockSpecIndex); + + expect(result.fields).to.have.length(3); + expect(result.fields[0].fieldPath).to.equal("foo"); + expect(result.fields[1].fieldPath).to.equal("bar"); + expect(result.fields[2].fieldPath).to.equal("__name__"); + expect(result.fields[2].order).to.equal(API.Order.DESCENDING); + }); +}); + +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 correctly sort an array of Spec indexes with apiScope, density, multikey, and unique", () => { + const a: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.ANY_API, + }; + + const b: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.ANY_API, + }; + + const c: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.DATASTORE_MODE_API, + }; + + const d: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + }; + + const e: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + density: API.Density.DENSITY_UNSPECIFIED, + }; + + const f: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + density: API.Density.SPARSE_ALL, + }; + + const g: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + density: API.Density.SPARSE_ANY, + }; + + const h: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + density: API.Density.DENSE, + }; + + const i: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + density: API.Density.DENSE, + multikey: false, + }; + + const j: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + density: API.Density.DENSE, + multikey: true, + }; + + const k: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + density: API.Density.DENSE, + multikey: true, + unique: false, + }; + + const l: Spec.Index = { + collectionGroup: "collectionA", + queryScope: API.QueryScope.COLLECTION, + fields: [], + apiScope: API.ApiScope.MONGODB_COMPATIBLE_API, + density: API.Density.DENSE, + multikey: true, + unique: true, + }; + + expect([l, k, j, i, h, g, f, e, d, c, b, a].sort(sort.compareSpecIndex)).to.eql([ + a, + b, + c, + d, + e, + f, + g, + h, + i, + j, + k, + l, + ]); + }); + + 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..99024df0269 --- /dev/null +++ b/src/firestore/options.ts @@ -0,0 +1,41 @@ +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; + edition?: string; + + // backup schedules + backupSchedule?: string; + retention?: `${number}${"h" | "d" | "m" | "w"}`; + recurrence?: types.RecurrenceType; + dayOfWeek?: DayOfWeek; + + // backups + backup?: string; + + // CMEK + encryptionType?: EncryptionType; + kmsKeyName?: string; +} + +export enum EncryptionType { + CUSTOMER_MANAGED_ENCRYPTION = "CUSTOMER_MANAGED_ENCRYPTION", + USE_SOURCE_ENCRYPTION = "USE_SOURCE_ENCRYPTION", + GOOGLE_DEFAULT_ENCRYPTION = "GOOGLE_DEFAULT_ENCRYPTION", +} diff --git a/src/firestore/pretty-print.spec.ts b/src/firestore/pretty-print.spec.ts new file mode 100644 index 00000000000..17eb755b34c --- /dev/null +++ b/src/firestore/pretty-print.spec.ts @@ -0,0 +1,170 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as API from "./api-types"; +import { PrettyPrint } from "./pretty-print"; +import { logger } from "../logger"; + +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", + ); + }); +}); + +describe("prettyStringArray", () => { + it("should correctly print an array of strings", () => { + expect(printer.prettyStringArray(["kms-key-1", "kms-key-2"])).to.equal( + "kms-key-1\nkms-key-2\n", + ); + }); + + it("should print nothing if the array is empty", () => { + expect(printer.prettyStringArray([])).to.equal(""); + }); +}); + +describe("prettyPrintDatabase", () => { + let loggerInfoStub: sinon.SinonStub; + + const BASE_DATABASE: API.DatabaseResp = { + name: "projects/my-project/databases/(default)", + uid: "uid", + createTime: "2020-01-01T00:00:00Z", + updateTime: "2020-01-01T00:00:00Z", + locationId: "us-central1", + type: API.DatabaseType.FIRESTORE_NATIVE, + concurrencyMode: "OPTIMISTIC", + appEngineIntegrationMode: "ENABLED", + keyPrefix: "prefix", + deleteProtectionState: API.DatabaseDeleteProtectionState.DISABLED, + pointInTimeRecoveryEnablement: API.PointInTimeRecoveryEnablement.DISABLED, + etag: "etag", + versionRetentionPeriod: "1h", + earliestVersionTime: "2020-01-01T00:00:00Z", + }; + + beforeEach(() => { + loggerInfoStub = sinon.stub(logger, "info"); + }); + + afterEach(() => { + loggerInfoStub.restore(); + }); + + it("should display STANDARD edition when databaseEdition is not provided", () => { + const database: API.DatabaseResp = { ...BASE_DATABASE }; + + printer.prettyPrintDatabase(database); + + expect(loggerInfoStub.firstCall.args[0]).to.include("Edition"); + expect(loggerInfoStub.firstCall.args[0]).to.include("STANDARD"); + }); + + it("should display STANDARD edition when databaseEdition is UNSPECIFIED", () => { + const database: API.DatabaseResp = { + ...BASE_DATABASE, + databaseEdition: API.DatabaseEdition.DATABASE_EDITION_UNSPECIFIED, + }; + + printer.prettyPrintDatabase(database); + + expect(loggerInfoStub.firstCall.args[0]).to.include("Edition"); + expect(loggerInfoStub.firstCall.args[0]).to.include("STANDARD"); + }); + + it("should display ENTERPRISE edition when databaseEdition is ENTERPRISE", () => { + const database: API.DatabaseResp = { + ...BASE_DATABASE, + databaseEdition: API.DatabaseEdition.ENTERPRISE, + }; + + printer.prettyPrintDatabase(database); + + expect(loggerInfoStub.firstCall.args[0]).to.include("Edition"); + expect(loggerInfoStub.firstCall.args[0]).to.include("ENTERPRISE"); + }); + + it("should display STANDARD edition when databaseEdition is STANDARD", () => { + const database: API.DatabaseResp = { + ...BASE_DATABASE, + databaseEdition: API.DatabaseEdition.STANDARD, + }; + + printer.prettyPrintDatabase(database); + + expect(loggerInfoStub.firstCall.args[0]).to.include("Edition"); + expect(loggerInfoStub.firstCall.args[0]).to.include("STANDARD"); + }); +}); diff --git a/src/firestore/pretty-print.ts b/src/firestore/pretty-print.ts new file mode 100644 index 00000000000..52f412161e2 --- /dev/null +++ b/src/firestore/pretty-print.ts @@ -0,0 +1,402 @@ +import * as clc from "colorette"; +import * as Table from "cli-table3"; + +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"; +import { Operation } from "./api-types"; + +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 { + let colValueWidth = Math.max(50, 5 + database.name.length); + if (database.cmekConfig) { + colValueWidth = Math.max(140, 20 + database.cmekConfig.kmsKeyName.length); + } + + const table = new Table({ + head: ["Field", "Value"], + colWidths: [30, colValueWidth], + }); + + const edition = + !database.databaseEdition || + database.databaseEdition === types.DatabaseEdition.DATABASE_EDITION_UNSPECIFIED + ? types.DatabaseEdition.STANDARD + : database.databaseEdition; + 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)], + ["Edition", clc.yellow(edition)], + ["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)], + ); + + if (database.cmekConfig) { + table.push(["KMS Key Name", clc.yellow(database.cmekConfig.kmsKeyName)]); + + if (database.cmekConfig.activeKeyVersion) { + table.push([ + "Active Key Versions", + clc.yellow(this.prettyStringArray(database.cmekConfig.activeKeyVersion)), + ]); + } + } + + logger.info(table.toString()); + } + + /** + * Returns a pretty representation of a String array. + * @param stringArray the string array to be formatted. + */ + prettyStringArray(stringArray: string[]): string { + let result = ""; + stringArray.forEach((str) => { + result += `${str}\n`; + }); + return result; + } + + /** + * 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 a Firestore operation as an ASCII table. + */ + prettyPrintOperation(operation: Operation) { + const table = new Table({ + head: ["Operation", ""], + }); + + table.push( + ["Name", clc.yellow(operation.name)], + ["Done?", clc.yellow(operation.done ? "YES" : "NO")], + ["Metadata", clc.yellow(JSON.stringify(operation.metadata, undefined, 2))], + ); + + if (operation.response) { + table.push(["Response", clc.yellow(JSON.stringify(operation.response, undefined, 2))]); + } + + logger.info(table.toString()); + } + + /** + * Print Firestore operations as an ASCII table. + */ + prettyPrintOperations(operations: Operation[]) { + if (operations.length === 0) { + logger.info("No operations found."); + return; + } + const table = new Table({ + head: ["Operation Name", "Done"], + }); + + for (const op of operations) { + table.push([clc.yellow(op.name), op.done ? clc.green("YES") : clc.yellow("NO")]); + } + + 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}) `; + }); + + result += " -- "; + if (index.density !== undefined) { + result += clc.cyan(`Density:${index.density} `); + } + if (index.multikey !== undefined) { + result += clc.cyan(`Multikey:${index.multikey ? "YES" : "NO"}`); + } + + 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.spec.ts b/src/firestore/util.spec.ts new file mode 100644 index 00000000000..a12fbd8e4a1 --- /dev/null +++ b/src/firestore/util.spec.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..b3d4a7a43a2 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"; /** @@ -36,6 +36,16 @@ export function assertHasOneOf(obj: any, props: string[]): void { export function assertEnum(obj: any, prop: string, valid: any[]): void { const objString = clc.cyan(JSON.stringify(obj)); if (valid.indexOf(obj[prop]) < 0) { - throw new FirebaseError(`Field "${prop}" must be one of ${valid.join(", ")}: ${objString}`); + 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..b2528a8e3ee --- /dev/null +++ b/src/frameworks/angular/index.ts @@ -0,0 +1,274 @@ +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 = "16 - 20"; + +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 = new Promise((resolve, reject) => { + setTimeout(() => { + const port = process.env.PORT; + const socket = 'express.sock'; + process.env.PORT = socket; + + ${ + serverEntry?.endsWith(".mjs") + ? `import(\`./${serverOutputPath}/${serverEntry}\`)` + : `Promise.resolve(require('./${serverOutputPath}/${serverEntry}'))` + }.then(({ default: defHandler, reqHandler, app }) => { + const handler = app?.() ?? reqHandler ?? defHandler; + if (!handler) { + reject(\`The file at "./${serverOutputPath}/${serverEntry}" did not export a valid request handler. Expected exports: 'app', 'default', or 'reqHandler'.\`); + } else { + process.env.PORT = port; + resolve(handler); + } + }); + }, 0); + }); +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.spec.ts b/src/frameworks/angular/utils.spec.ts new file mode 100644 index 00000000000..2c2a06503db --- /dev/null +++ b/src/frameworks/angular/utils.spec.ts @@ -0,0 +1,39 @@ +import { getBuilderType, BuilderType } from "./utils"; +import { expect } from "chai"; + +describe("Angular utils", () => { + describe("getBuilderType", () => { + it("should return the correct builder type for valid builders", () => { + expect(getBuilderType("@angular-devkit/build-angular:browser")).to.equal(BuilderType.BROWSER); + expect(getBuilderType("@angular-devkit/build-angular:server")).to.equal(BuilderType.SERVER); + expect(getBuilderType("@angular-devkit/build-angular:dev-server")).to.equal( + BuilderType.DEV_SERVER, + ); + expect(getBuilderType("@angular-devkit/build-angular:ssr-dev-server")).to.equal( + BuilderType.SSR_DEV_SERVER, + ); + expect(getBuilderType("@angular-devkit/build-angular:prerender")).to.equal( + BuilderType.PRERENDER, + ); + expect(getBuilderType("@angular-devkit/build-angular:application")).to.equal( + BuilderType.APPLICATION, + ); + expect(getBuilderType("@angular-devkit/build-angular:browser-esbuild")).to.equal( + BuilderType.BROWSER_ESBUILD, + ); + expect(getBuilderType("@angular-devkit/build-angular:deploy")).to.equal(BuilderType.DEPLOY); + }); + + it("should return null for invalid builders", () => { + expect(getBuilderType("@angular-devkit/build-angular:invalid")).to.be.null; + expect(getBuilderType("invalid")).to.be.null; + expect(getBuilderType(":")).to.be.null; + expect(getBuilderType("::")).to.be.null; + expect(getBuilderType("random:string")).to.be.null; + }); + + it("should handle builders with no colon", () => { + expect(getBuilderType("@angular-devkit/build-angular")).to.be.null; + }); + }); +}); diff --git a/src/frameworks/angular/utils.ts b/src/frameworks/angular/utils.ts new file mode 100644 index 00000000000..e537d2ed16d --- /dev/null +++ b/src/frameworks/angular/utils.ts @@ -0,0 +1,603 @@ +import type { JsonObject } from "@angular-devkit/core"; +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 }; +} + +export enum BuilderType { + DEPLOY = "deploy", + DEV_SERVER = "dev-server", + SSR_DEV_SERVER = "ssr-dev-server", + SERVER = "server", + BROWSER = "browser", + BROWSER_ESBUILD = "browser-esbuild", + APPLICATION = "application", + PRERENDER = "prerender", +} + +const DEV_SERVER_TARGETS: BuilderType[] = [BuilderType.DEV_SERVER, BuilderType.SSR_DEV_SERVER]; + +function getValidBuilderTypes(purpose: BUILD_TARGET_PURPOSE): BuilderType[] { + return [ + BuilderType.APPLICATION, + BuilderType.BROWSER_ESBUILD, + BuilderType.DEPLOY, + BuilderType.BROWSER, + BuilderType.PRERENDER, + ...(purpose === "deploy" ? [] : DEV_SERVER_TARGETS), + ]; +} + +export async function getAllTargets(purpose: BUILD_TARGET_PURPOSE, dir: string) { + const validBuilderTypes = getValidBuilderTypes(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) => { + const builderType = getBuilderType(targetDefinition.builder); + if (builderType && !validBuilderTypes.includes(builderType)) { + 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) { + throwCannotDetermineTarget(); + } + + 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 builderType = getBuilderType(target.builder); + switch (builderType) { + case BuilderType.DEPLOY: + deployTarget = overrideTarget; + break; + case BuilderType.APPLICATION: + buildTarget = overrideTarget; + break; + case BuilderType.BROWSER: + case BuilderType.BROWSER_ESBUILD: + browserTarget = overrideTarget; + break; + case BuilderType.PRERENDER: + prerenderTarget = overrideTarget; + break; + case BuilderType.DEV_SERVER: + case BuilderType.SSR_DEV_SERVER: + serveTarget = overrideTarget; + break; + default: + throw new FirebaseError(`builder type ${builderType} not known.`); + } + } else if (workspaceProject.targets.has("deploy")) { + const { builder, defaultConfiguration = "production" } = + workspaceProject.targets.get("deploy")!; + if (getBuilderType(builder) === BuilderType.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 builderType = getBuilderType(builder); + const target = { + project, + target: "build", + configuration: configuration || defaultConfiguration, + }; + if (builderType === BuilderType.BROWSER || builderType === BuilderType.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; + const builderType = getBuilderType(builder); + if (target === deployTarget && builderType === BuilderType.DEPLOY) continue; + if (target === buildTarget && builderType === BuilderType.APPLICATION) continue; + if (target === buildTarget && builderType === BuilderType.BROWSER) continue; + if (target === browserTarget && builderType === BuilderType.BROWSER_ESBUILD) continue; + if (target === browserTarget && builderType === BuilderType.BROWSER) continue; + if (target === browserTarget && builderType === BuilderType.APPLICATION) continue; + if (target === prerenderTarget && builderType === BuilderType.PRERENDER) continue; + if (target === prerenderTarget && builderType === BuilderType.PRERENDER) continue; + if (target === serverTarget && builderType === BuilderType.SERVER) continue; + if (target === serveTarget && builderType === BuilderType.SSR_DEV_SERVER) continue; + if (target === serveTarget && builderType === BuilderType.DEV_SERVER) continue; + if (target === serveTarget && builderType === BuilderType.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 tryToGetOptionsForTarget(architectHost, 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 tryToGetOptionsForTarget(architectHost, 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), + ]); + + const buildOutputPath = + typeof targetOptions?.outputPath === "string" + ? targetOptions.outputPath + : join("dist", buildOrBrowserTarget.project); + + const outputPath = join( + buildOutputPath, + buildTarget && getBuilderType(builderName) === BuilderType.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); + + const buildOutputPath = + typeof browserTargetOptions?.outputPath === "string" + ? browserTargetOptions.outputPath + : join("dist", buildOrBrowserTarget.project); + + const browserOutputPath = join(buildOutputPath, 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); + if (!serverTargetOptions) { + throw new AssertionError({ + message: `expected "JsonObject" but got "${typeof serverTargetOptions}"`, + }); + } + const serverTargetOutputPath = + typeof serverTargetOptions?.outputPath === "string" + ? serverTargetOptions.outputPath + : join("dist", buildOrServerTarget.project); + + const serverOutputPath = join(serverTargetOutputPath, 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(); +} + +/** + * Try to get options for target, throw an error when expected target doesn't exist in the configuration. + */ +export async function tryToGetOptionsForTarget( + architectHost: WorkspaceNodeModulesArchitectHost, + target: Target, +): Promise { + return await architectHost.getOptionsForTarget(target).catch(throwCannotDetermineTarget); +} + +function throwCannotDetermineTarget(error?: Error): never { + throw new FirebaseError( + `Unable to determine the application to deploy, specify a target via the FIREBASE_FRAMEWORKS_BUILD_TARGET environment variable.`, + { original: error }, + ); +} + +/** + * Extracts the builder type from a full builder string (everything after the colon) + * @example + * getBuilderType("@angular-devkit/build-angular:browser") // returns "browser" + */ +export function getBuilderType(builder: string): BuilderType | null { + const colonIndex = builder.lastIndexOf(":"); + const builderType = colonIndex >= 0 ? builder.slice(colonIndex + 1) : undefined; + if (!builderType || !Object.values(BuilderType).includes(builderType as BuilderType)) { + return null; + } + return builderType as BuilderType; +} 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..b98584e0f2e --- /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 = 60_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 = "^6.0.1"; +export const FIREBASE_ADMIN_VERSION = "^11.11.1"; +export const SHARP_VERSION = "^0.32 || ^0.33"; +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..687fa070229 --- /dev/null +++ b/src/frameworks/docs/angular.md @@ -0,0 +1,153 @@ +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>> + +Caution: For developers creating a full-stack Angular app, we strongly +recommend [Firebase App Hosting](/docs/app-hosting/). +If you're already using the frameworks experiment in the Firebase CLI, we +recommend "graduating" to +{{app_hosting}}. With {{app_hosting}}, you'll have a unified solution to manage +everything from CDN to server-side rendering, along with improved GitHub +integration. + +<<_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/ssr) +for more information. + +### Optional: add a server module + +#### 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..ad51e78124e --- /dev/null +++ b/src/frameworks/docs/frameworks-overview.md @@ -0,0 +1,68 @@ +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. + +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). + +## Configure different environments + +You can deploy multiple sets of environment variables for different project environments, such as staging and production. + +Like Cloud Functions for Firebase, this tooling supports the [dotenv](https://www.npmjs.com/package/dotenv) file format for loading environment variables specified in a .env file. + +* If you have a `staging` project alias, you can deploy environment variables from a `.env.staging` file. +* If you have a `production` project alias, you can deploy environment variables from a `.env.production` file. +* If you have a project with id `PROJECT_ID`, you can deploy environment variables from a `.env.PROJECT_ID` file. + +See the [Cloud Functions documentation](https://firebase.google.com/docs/functions/config-env?gen=2nd#deploying_multiple_sets_of_environment_variables) for a detailed guide. + +## 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..d0e5643d3d6 --- /dev/null +++ b/src/frameworks/docs/nextjs.md @@ -0,0 +1,123 @@ +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>> + +Caution: For developers creating a full-stack Next.js app, we strongly +recommend [Firebase App Hosting](/docs/app-hosting/). +If you're already using the frameworks experiment in the Firebase CLI, we +recommend "graduating" to +{{app_hosting}}. With {{app_hosting}}, you'll have a unified solution to manage +everything from CDN to server-side rendering, along with improved GitHub +integration. + +<<_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 is 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..3ab873842e2 --- /dev/null +++ b/src/frameworks/flutter/index.ts @@ -0,0 +1,64 @@ +import { sync as spawnSync } from "cross-spawn"; +import { copy, pathExists } from "fs-extra"; +import { join } from "path"; + +import { BuildResult, Discovery, FrameworkType, SupportLevel } from "../interfaces"; +import { FirebaseError } from "../../error"; +import { assertFlutterCliExists, getPubSpec, getAdditionalBuildArgs } 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 pubSpec = await getPubSpec(dir); + 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 async function build(cwd: string): Promise { + assertFlutterCliExists(); + + const pubSpec = await getPubSpec(cwd); + const otherArgs = getAdditionalBuildArgs(pubSpec); + const buildArgs = ["build", "web", ...otherArgs].filter(Boolean); + + const build = spawnSync("flutter", buildArgs, { 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..bdefec15d07 --- /dev/null +++ b/src/frameworks/flutter/utils.spec.ts @@ -0,0 +1,118 @@ +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, getAdditionalBuildArgs, getPubSpec } from "./utils"; +import * as fs from "fs/promises"; +import * as path from "path"; +import * as fsExtra from "fs-extra"; + +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); + + expect(assertFlutterCliExists()).to.be.undefined; + sinon.assert.calledWith(stub, "flutter", ["--version"], { stdio: "ignore" }); + }); + }); + + describe("getAdditionalBuildArgs", () => { + it("should return '--no-tree-shake-icons' when a tree-shake package is present", () => { + const pubSpec = { + dependencies: { + material_icons_named: "^1.0.0", + }, + }; + expect(getAdditionalBuildArgs(pubSpec)).to.deep.equal(["--no-tree-shake-icons"]); + }); + + it("should return an empty array when no tree-shake package is present", () => { + const pubSpec = { + dependencies: { + some_other_package: "^1.0.0", + }, + }; + expect(getAdditionalBuildArgs(pubSpec)).to.deep.equal([]); + }); + + it("should return an empty array when dependencies is undefined", () => { + const pubSpec = {}; + expect(getAdditionalBuildArgs(pubSpec)).to.deep.equal([]); + }); + }); + + describe("getPubSpec", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should read and parse pubspec.yaml file when both pubspec.yaml and web directory exist", async () => { + const mockYamlContent = "dependencies:\n flutter:\n sdk: flutter\n some_package: ^1.0.0"; + const expectedParsedYaml = { + dependencies: { + flutter: { sdk: "flutter" }, + some_package: "^1.0.0", + }, + }; + + sandbox.stub(fsExtra, "pathExists").resolves(true); + sandbox.stub(fs, "readFile").resolves(Buffer.from(mockYamlContent)); + sandbox.stub(path, "join").callsFake((...args) => args.join("/")); + + const result = await getPubSpec("/path"); + expect(result).to.deep.equal(expectedParsedYaml); + }); + + it("should return an empty object if pubspec.yaml doesn't exist", async () => { + const pathExistsStub = sandbox.stub(fsExtra, "pathExists"); + pathExistsStub.withArgs("/path/pubspec.yaml", () => null).resolves(false); + + const result = await getPubSpec("/path"); + expect(result).to.deep.equal({}); + }); + + it("should return an empty object if web directory doesn't exist", async () => { + const pathExistsStub = sandbox.stub(fsExtra, "pathExists"); + pathExistsStub.withArgs("/path/pubspec.yaml", () => null).resolves(true); + pathExistsStub.withArgs("/path/web", () => null).resolves(false); + + const result = await getPubSpec("/path"); + expect(result).to.deep.equal({}); + }); + + it("should return an empty object and log a message if there's an error reading pubspec.yaml", async () => { + sandbox.stub(fsExtra, "pathExists").resolves(true); + sandbox.stub(fs, "readFile").rejects(new Error("File read error")); + sandbox.stub(path, "join").callsFake((...args) => args.join("/")); + const consoleInfoStub = sandbox.stub(console, "info"); + + const result = await getPubSpec("/path"); + expect(result).to.deep.equal({}); + expect(consoleInfoStub.calledOnceWith("Failed to read pubspec.yaml")).to.be.true; + }); + }); +}); diff --git a/src/frameworks/flutter/utils.ts b/src/frameworks/flutter/utils.ts new file mode 100644 index 00000000000..c3cff5abb55 --- /dev/null +++ b/src/frameworks/flutter/utils.ts @@ -0,0 +1,71 @@ +import { sync as spawnSync } from "cross-spawn"; +import { FirebaseError } from "../../error"; +import { readFile } from "fs/promises"; +import { pathExists } from "fs-extra"; +import { join } from "path"; +import * as yaml from "yaml"; + +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.", + ); +} + +/** + * Determines additional build arguments for Flutter based on the project's dependencies. + * @param {Record} pubSpec - The parsed pubspec.yaml file contents. + * @return {string[]} An array of additional build arguments. + * @description + * This function checks if the project uses certain packages that might require additional + * flags to be added to the build step. If any of these packages are present in the + * project's dependencies, the function returns an array with these flags. + * Otherwise, it returns an empty array. + * This change is inspired from the following issue: + * https://github.com/firebase/firebase-tools/issues/6197 + */ +export function getAdditionalBuildArgs(pubSpec: Record): string[] { + /* + These packages are known to require the --no-tree-shake-icons flag + when building for web. + More dependencies might need to add here in the future. + Related issue: https://github.com/firebase/firebase-tools/issues/6197 + */ + const treeShakePackages = [ + "material_icons_named", + "material_symbols_icons", + "material_design_icons_flutter", + "flutter_iconpicker", + "font_awesome_flutter", + "ionicons_named", + ]; + + const hasTreeShakePackage = treeShakePackages.some((pkg) => pubSpec.dependencies?.[pkg]); + const treeShakeFlags = hasTreeShakePackage ? ["--no-tree-shake-icons"] : []; + return [...treeShakeFlags]; +} + +/** + * Reads and parses the pubspec.yaml file from a given directory. + * @param {string} dir - The directory path where pubspec.yaml is located. + * @return {Promise>} A promise that resolves to the parsed contents of pubspec.yaml. + * @description + * This function checks for the existence of both pubspec.yaml and the 'web' directory + * in the given path. If either is missing, it returns an empty object. + * If both exist, it reads the pubspec.yaml file, parses its contents, and returns + * the parsed object. In case of any errors during this process, it logs a message + * and returns an empty object. + */ +export async function getPubSpec(dir: string): Promise> { + if (!(await pathExists(join(dir, "pubspec.yaml")))) return {}; + if (!(await pathExists(join(dir, "web")))) return {}; + + try { + const pubSpecBuffer = await readFile(join(dir, "pubspec.yaml")); + return yaml.parse(pubSpecBuffer.toString()); + } catch (error) { + console.info("Failed to read pubspec.yaml"); + return {}; + } +} 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..d9fca0fe123 --- /dev/null +++ b/src/frameworks/index.ts @@ -0,0 +1,610 @@ +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 { confirm } 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 { IS_WINDOWS, 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 isDemoProject = Constants.isDemoProject(project); + 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 (isDemoProject) { + options.site = project; + } + 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 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 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.*"), { windowsPathsNoEscape: IS_WINDOWS }); + + 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..ef507a72189 --- /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..b1becc11ded --- /dev/null +++ b/src/frameworks/next/index.ts @@ -0,0 +1,779 @@ +import { execSync } from "child_process"; +import { spawn } 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, readFile } 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 { select } 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, + getAppMetadataFromMetaFiles, + cleanI18n, + getNextVersion, + hasStaticAppNotFoundComponent, + getRoutesWithServerAction, + getProductionDistDirFiles, + whichNextConfigFile, + installEsbuild, + findEsbuildPath, +} from "./utils"; +import { NODE_VERSION, NPM_COMMAND_TIMEOUT_MILLIES, SHARP_VERSION, I18N_ROOT } from "../constants"; +import type { + AppPathRoutesManifest, + AppPathsManifest, + HostingHeadersWithSource, + RoutesManifest, + NpmLsDepdendency, + MiddlewareManifest, + ActionManifest, + CustomBuildOptions, +} from "./interfaces"; +import { + MIDDLEWARE_MANIFEST, + PAGES_MANIFEST, + PRERENDER_MANIFEST, + ROUTES_MANIFEST, + APP_PATH_ROUTES_MANIFEST, + APP_PATHS_MANIFEST, + SERVER_REFERENCE_MANIFEST, + ESBUILD_VERSION, +} from "./constants"; +import { getAllSiteDomains, getDeploymentDomain } from "../../hosting/api"; +import { logger } from "../../logger"; +import { parseStrict } from "../../functions/env"; + +const DEFAULT_BUILD_SCRIPT = ["next build"]; +const PUBLIC_DIR = "public"; + +export const supportedRange = "12 - 15.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 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"; + } + + let env = { ...process.env }; + + // Check if the .env. file exists and make it available for the build process + if (context?.projectId) { + const projectEnvPath = join(dir, `.env.${context.projectId}`); + + if (await pathExists(projectEnvPath)) { + const projectEnvVars = parseStrict((await readFile(projectEnvPath)).toString()); + + // Merge the parsed variables with the existing environment variables + env = { ...projectEnvVars, ...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 { headers: headersFromMetaFiles, pprRoutes } = await getAppMetadataFromMetaFiles( + dir, + distDir, + baseUrl, + appPathRoutesManifest, + ); + headers.push(...headersFromMetaFiles); + + for (const route of pprRoutes) { + reasonsForBackend.add(`route with PPR ${route}`); + } + + 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 select({ + default: "TypeScript", + message: "What language would you like to use?", + choices: [ + { name: "JavaScript", value: "js" }, + { name: "TypeScript", value: "ts" }, + ], + }); + execSync( + `npx --yes create-next-app@"${supportedRange}" -e hello-world ` + + `${setup.hosting.source} --use-npm --${language}`, + { 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, + }; + + const { pprRoutes } = await getAppMetadataFromMetaFiles( + sourceDir, + distDir, + basePath, + appPathRoutesManifest, + ); + + 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 (pprRoutes.includes(path)) { + logger.debug(`skipping ${path} due to ppr`); + return; + } + + 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 { + // Check if esbuild is installed using `npx which`, if not, install it + let esbuildPath = findEsbuildPath(); + if (!esbuildPath || !pathExistsSync(esbuildPath)) { + console.warn("esbuild not found, installing..."); + installEsbuild(ESBUILD_VERSION); + esbuildPath = findEsbuildPath(); + if (!esbuildPath || !pathExistsSync(esbuildPath)) { + throw new FirebaseError("Failed to locate esbuild after installation."); + } + } + + // Dynamically require esbuild from the found path + const esbuild = require(esbuildPath); + if (!esbuild) { + throw new FirebaseError(`Failed to load esbuild from path: ${esbuildPath}`); + } + + 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: CustomBuildOptions = { + entryPoints: [join(sourceDir, configFile)], + outfile: join(destDir, configFile), + bundle: true, + platform: "node", + target: `node${NODE_VERSION}`, + logLevel: "error", + external: productionDeps, + }; + if (configFile === "next.config.mjs") { + // ensure generated file is .mjs if the config is .mjs + esbuildArgs.format = "esm"; + } + + const bundle = await esbuild.build(esbuildArgs); + + if (bundle.errors && bundle.errors.length > 0) { + throw new FirebaseError(bundle.errors.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..bd9ae9fb7e4 --- /dev/null +++ b/src/frameworks/next/interfaces.ts @@ -0,0 +1,172 @@ +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]; + +export type CustomBuildOptions = { + entryPoints: string[]; + outfile: string; + bundle: boolean; + platform: "node"; + target: string; + logLevel: "silent" | "verbose" | "debug" | "info" | "warning" | "error"; + external: string[]; + format?: "iife" | "cjs" | "esm"; +}; diff --git a/src/frameworks/next/testing/app.ts b/src/frameworks/next/testing/app.ts new file mode 100644 index 00000000000..e29d402db88 --- /dev/null +++ b/src/frameworks/next/testing/app.ts @@ -0,0 +1,93 @@ +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", + "/ppr/page": "/ppr", +}; + +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..65eea3f100e --- /dev/null +++ b/src/frameworks/next/utils.spec.ts @@ -0,0 +1,654 @@ +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 * as childProcess from "child_process"; +import { FirebaseError } from "../../error"; + +import { + EXPORT_MARKER, + IMAGES_MANIFEST, + APP_PATH_ROUTES_MANIFEST, + ESBUILD_VERSION, +} from "./constants"; + +import { + cleanEscapedChars, + isRewriteSupportedByHosting, + isRedirectSupportedByHosting, + isHeaderSupportedByHosting, + getNextjsRewritesToUse, + usesAppDirRouter, + usesNextImage, + hasUnoptimizedImage, + isUsingMiddleware, + isUsingImageOptimization, + isUsingAppDirectory, + cleanCustomRouteI18n, + I18N_SOURCE, + allDependencyNames, + getMiddlewareMatcherRegexes, + getNonStaticRoutes, + getNonStaticServerComponents, + getAppMetadataFromMetaFiles, + isUsingNextImageInAppDirectory, + getNextVersion, + getRoutesWithServerAction, + findEsbuildPath, + installEsbuild, +} 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("getAppMetadataFromMetaFiles", () => { + let sandbox: sinon.SinonSandbox; + beforeEach(() => (sandbox = sinon.createSandbox())); + afterEach(() => sandbox.restore()); + + it("should return the correct headers and pprRoutes 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"); + + // /api/static + 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); + + // /ppr + dirExistsSyncStub.withArgs(`${distDir}/server/app/ppr`).returns(true); + fileExistsSyncStub.withArgs(`${distDir}/server/app/ppr.meta`).returns(true); + readJsonStub.withArgs(`${distDir}/server/app/ppr.meta`).resolves({ + ...metaFileContents, + postponed: "true", + }); + + expect( + await getAppMetadataFromMetaFiles(".", distDir, "/asdf", appPathRoutesManifest), + ).to.deep.equal({ + headers: [ + { + source: "/asdf/api/static", + headers: [ + { + key: "content-type", + value: "application/json", + }, + { + key: "custom-header", + value: "custom-value", + }, + ], + }, + { + source: "/asdf/ppr", + headers: [ + { + key: "content-type", + value: "application/json", + }, + { + key: "custom-header", + value: "custom-value", + }, + ], + }, + ], + pprRoutes: ["/ppr"], + }); + }); + }); + + 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"]); + }); + }); + + describe("findEsbuildPath", () => { + let execSyncStub: sinon.SinonStub; + + beforeEach(() => { + execSyncStub = sinon.stub(childProcess, "execSync"); + }); + + afterEach(() => { + execSyncStub.restore(); + }); + + it("should return the correct esbuild path when esbuild is found", () => { + const mockBinaryPath = "/path/to/.bin/esbuild"; + const expectedResolvedPath = "/path/to/esbuild"; + execSyncStub + .withArgs("npx which esbuild", { encoding: "utf8" }) + .returns(mockBinaryPath + "\n"); + + const esbuildPath = findEsbuildPath(); + + expect(esbuildPath).to.equal(expectedResolvedPath); + }); + + it("should return null if esbuild is not found", () => { + execSyncStub + .withArgs("npx which esbuild", { encoding: "utf8" }) + .throws(new Error("not found")); + + const esbuildPath = findEsbuildPath(); + expect(esbuildPath).to.be.null; + }); + + it("should warn if global esbuild version does not match required version", () => { + const mockBinaryPath = "/path/to/.bin/esbuild"; + const mockGlobalVersion = "1.2.3"; + execSyncStub + .withArgs("npx which esbuild", { encoding: "utf8" }) + .returns(mockBinaryPath + "\n"); + execSyncStub + .withArgs(`"${mockBinaryPath}" --version`, { encoding: "utf8" }) + .returns(`${mockGlobalVersion}\n`); + + const consoleWarnStub = sinon.stub(console, "warn"); + + findEsbuildPath(); + expect( + consoleWarnStub.calledWith( + `Warning: Global esbuild version (${mockGlobalVersion}) does not match the required version (${ESBUILD_VERSION}).`, + ), + ).to.be.true; + + consoleWarnStub.restore(); + }); + }); + + describe("installEsbuild", () => { + let execSyncStub: sinon.SinonStub; + + beforeEach(() => { + execSyncStub = sinon.stub(childProcess, "execSync"); + }); + afterEach(() => execSyncStub.restore()); + + it("should successfully install esbuild", () => { + execSyncStub + .withArgs(`npm install esbuild@${ESBUILD_VERSION} --no-save`, { stdio: "inherit" }) + .returns(""); + + installEsbuild(ESBUILD_VERSION); + expect(execSyncStub.calledOnce).to.be.true; + }); + + it("should throw a FirebaseError if installation fails", () => { + execSyncStub + .withArgs(`npm install esbuild@${ESBUILD_VERSION} --no-save`, { stdio: "inherit" }) + .throws(new Error("Installation failed")); + + try { + installEsbuild(ESBUILD_VERSION); + expect.fail("Expected installEsbuild to throw"); + } catch (error) { + const typedError = error as FirebaseError; + expect(typedError).to.be.instanceOf(FirebaseError); + expect(typedError.message).to.include("Failed to install esbuild"); + } + }); + }); +}); diff --git a/src/frameworks/next/utils.ts b/src/frameworks/next/utils.ts new file mode 100644 index 00000000000..50e02d020db --- /dev/null +++ b/src/frameworks/next/utils.ts @@ -0,0 +1,555 @@ +import { existsSync } from "fs"; +import { pathExists } from "fs-extra"; +import { basename, extname, join, posix, sep, resolve, dirname } 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, satisfies } 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, + ESBUILD_VERSION, +} from "./constants"; +import { dirExistsSync, fileExistsSync } from "../../fsutils"; +import { IS_WINDOWS } from "../../utils"; +import { execSync } from "child_process"; +import { FirebaseError } from "../../error"; + +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 metadata from .meta files + */ +export async function getAppMetadataFromMetaFiles( + sourceDir: string, + distDir: string, + basePath: string, + appPathRoutesManifest: AppPathRoutesManifest, +): Promise<{ headers: HostingHeadersWithSource[]; pprRoutes: string[] }> { + const headers: HostingHeadersWithSource[] = []; + const pprRoutes: string[] = []; + + 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; postponed?: string }>( + metadataPath, + ); + if (meta.headers) + headers.push({ + source: posix.join(basePath, source), + headers: Object.entries(meta.headers).map(([key, value]) => ({ key, value })), + }); + if (meta.postponed) pprRoutes.push(source); + } + }), + ); + + return { headers, pprRoutes }; +} + +/** + * 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; +} + +/** + * Helper function to find the path of esbuild using `npm which` + */ +export function findEsbuildPath(): string | null { + try { + const esbuildBinPath = execSync("npx which esbuild", { encoding: "utf8" })?.trim(); + if (!esbuildBinPath) { + return null; + } + + const globalVersion = getGlobalEsbuildVersion(esbuildBinPath); + if (globalVersion && !satisfies(globalVersion, ESBUILD_VERSION)) { + console.warn( + `Warning: Global esbuild version (${globalVersion}) does not match the required version (${ESBUILD_VERSION}).`, + ); + } + return resolve(dirname(esbuildBinPath), "../esbuild"); + } catch (error) { + console.error(`Failed to find esbuild with npx which: ${error}`); + return null; + } +} + +/** + * Helper function to get the global esbuild version + */ +export function getGlobalEsbuildVersion(binPath: string): string | null { + try { + const versionOutput = execSync(`"${binPath}" --version`, { encoding: "utf8" })?.trim(); + if (!versionOutput) { + return null; + } + + const versionMatch = versionOutput.match(/(\d+\.\d+\.\d+)/); + return versionMatch ? versionMatch[0] : null; + } catch (error) { + console.error(`Failed to get global esbuild version: ${error}`); + return null; + } +} + +/** + * Helper function to install esbuild dynamically + */ +export function installEsbuild(version: string): void { + const installCommand = `npm install esbuild@${version} --no-save`; + try { + execSync(installCommand, { stdio: "inherit" }); + } catch (error: any) { + if (error instanceof FirebaseError) { + throw error; + } else { + throw new FirebaseError(`Failed to install esbuild: ${error}`, { original: error }); + } + } +} 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..2ee5339099c --- /dev/null +++ b/src/frameworks/utils.ts @@ -0,0 +1,476 @@ +import { readJSON as originalReadJSON, readJsonSync } 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, () => { + // This next function is called when the proxied response is a 404 + // In that case we want to let the handler to use the original response + void hostOrRequestHandler(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; + try { + const json = JSON.parse(result.stdout.toString()); + return scanDependencyTree(name, json.dependencies); + } catch (e) { + // fallback to reading the version directly from package.json if npm list times out + const packageJson = readJsonSync(join(cwd, name, "package.json"), { throws: false }); + return packageJson?.version ? { version: packageJson.version } : undefined; + } +} + +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..f99d2ae968a --- /dev/null +++ b/src/frameworks/vite/index.ts @@ -0,0 +1,143 @@ +import { execSync } from "child_process"; +import { spawn } from "cross-spawn"; +import { existsSync } from "fs"; +import { copy, pathExists } from "fs-extra"; +import { join } from "path"; +import { stripVTControlCharacters } from "node:util"; +import { FrameworkType, SupportLevel } from "../interfaces"; +import { select } 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 - 6"; + +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 select({ + 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 = stripVTControlCharacters(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 76% rename from src/test/fsAsync.spec.ts rename to src/fsAsync.spec.ts index d75d7b15d53..b4fee377249 100644 --- a/src/test/fsAsync.spec.ts +++ b/src/fsAsync.spec.ts @@ -3,9 +3,9 @@ import * as crypto from "crypto"; import * as fs from "fs-extra"; import * as os from "os"; import * as path from "path"; -import { sync as rimraf } from "rimraf"; +import { rmSync } from "node:fs"; -import * as fsAsync from "../fsAsync"; +import * as fsAsync from "./fsAsync"; // These tests work on the following directory structure: // @@ -13,10 +13,11 @@ import * as fsAsync from "../fsAsync"; // visible // subdir/ // subfile -// nesteddir/ -// nestedfile -// node_modules/ -// nestednodemodules +// nesteddir/ +// nestedfile +// nestedfile2 +// node_modules/ +// nestednodemodules // node_modules // subfile describe("fsAsync", () => { @@ -26,6 +27,7 @@ describe("fsAsync", () => { "visible", "subdir/subfile", "subdir/nesteddir/nestedfile", + "subdir/nesteddir/nestedfile2", "subdir/node_modules/nestednodemodules", "node_modules/subfile", ]; @@ -43,10 +45,10 @@ describe("fsAsync", () => { }); after(() => { - rimraf(baseDir); + rmSync(baseDir, { recursive: true }); expect(() => { fs.statSync(baseDir); - }).to.throw; + }).to.throw(); }); describe("readdirRecursive", () => { @@ -93,5 +95,20 @@ describe("fsAsync", () => { .sort(); return expect(gotFileNames).to.deep.equal(expectFiles); }); + + it("should support .gitignore rules via options", async () => { + const results = await fsAsync.readdirRecursive({ + path: baseDir, + ignore: ["subdir/nesteddir/*", "!subdir/nesteddir/nestedfile"], + isGitIgnore: true, + }); + + const gotFileNames = results.map((r) => r.name).sort(); + const expectFiles = files + .map((file) => path.join(baseDir, file)) + .filter((file) => file !== path.join(baseDir, "subdir/nesteddir/nestedfile2")) + .sort(); + return expect(gotFileNames).to.deep.equal(expectFiles); + }); }); }); diff --git a/src/fsAsync.ts b/src/fsAsync.ts index fc4256f6dfd..26123f9636b 100644 --- a/src/fsAsync.ts +++ b/src/fsAsync.ts @@ -1,13 +1,17 @@ -import { join } from "path"; import { readdirSync, statSync } from "fs-extra"; +import ignorePkg from "ignore"; import * as _ from "lodash"; import * as minimatch from "minimatch"; +import { join, relative } from "path"; export interface ReaddirRecursiveOpts { // The directory to recurse. path: string; // Files to ignore. ignore?: string[]; + isGitIgnore?: boolean; + // Files in the ignore array to include. + include?: string[]; } export interface ReaddirRecursiveFile { @@ -21,7 +25,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 +40,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,18 +50,27 @@ 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 gitIgnoreRules = ignorePkg() + .add(options.ignore || []) + .createFilter(); + const filter = (t: string): boolean => { + if (options.isGitIgnore) { + // the git ignore filter will return true if given path should be included, + // so we need to negative that return false to avoid filtering it. + return !gitIgnoreRules(relative(options.path, t)); + } return rules.some((rule) => { return rule(t); }); }; - return readdirRecursiveHelper({ + return await readdirRecursiveHelper({ path: options.path, filter: filter, }); diff --git a/src/fsutils.spec.ts b/src/fsutils.spec.ts new file mode 100644 index 00000000000..e4b7ede6ad5 --- /dev/null +++ b/src/fsutils.spec.ts @@ -0,0 +1,127 @@ +import { expect } from "chai"; +import * as fs from "fs"; +import * as path from "path"; +import * as tmp from "tmp"; +import { fileExistsSync, dirExistsSync, readFile, listFiles, moveAll } from "./fsutils"; + +describe("fsutils", () => { + let tmpDir: tmp.DirResult; + + beforeEach(() => { + tmpDir = tmp.dirSync({ unsafeCleanup: true }); + }); + + afterEach(() => { + tmpDir.removeCallback(); + }); + + describe("fileExistsSync", () => { + it("should return true if the file exists", () => { + fs.writeFileSync(path.join(tmpDir.name, "test.txt"), "hello"); + expect(fileExistsSync(path.join(tmpDir.name, "test.txt"))).to.be.true; + }); + + it("should return false if a file does not exist", () => { + expect(fileExistsSync(path.join(tmpDir.name, "test.txt"))).to.be.false; + }); + + it("should return false for a directory", () => { + fs.mkdirSync(path.join(tmpDir.name, "test-dir")); + expect(fileExistsSync(path.join(tmpDir.name, "test-dir"))).to.be.false; + }); + }); + + describe("dirExistsSync", () => { + it("should return true if a directory exists", () => { + fs.mkdirSync(path.join(tmpDir.name, "test-dir")); + expect(dirExistsSync(path.join(tmpDir.name, "test-dir"))).to.be.true; + }); + + it("should return false if a directory does not exist", () => { + expect(dirExistsSync(path.join(tmpDir.name, "test-dir"))).to.be.false; + }); + + it("should return false for a file", () => { + fs.writeFileSync(path.join(tmpDir.name, "test.txt"), "hello"); + expect(dirExistsSync(path.join(tmpDir.name, "test.txt"))).to.be.false; + }); + }); + + describe("readFile", () => { + it("should read a file", () => { + fs.writeFileSync(path.join(tmpDir.name, "test.txt"), "hello world"); + expect(readFile(path.join(tmpDir.name, "test.txt"))).to.equal("hello world"); + }); + + it("should throw an error if the file does not exist", () => { + expect(() => readFile(path.join(tmpDir.name, "test.txt"))).to.throw("File not found"); + }); + }); + + describe("listFiles", () => { + it("should list files in a directory", () => { + fs.writeFileSync(path.join(tmpDir.name, "test1.txt"), ""); + fs.writeFileSync(path.join(tmpDir.name, "test2.txt"), ""); + fs.mkdirSync(path.join(tmpDir.name, "test-dir")); + const files = listFiles(tmpDir.name).sort(); + expect(files).to.deep.equal(["test-dir", "test1.txt", "test2.txt"]); + }); + + it("should throw an error if the directory does not exist", () => { + expect(() => listFiles(path.join(tmpDir.name, "non-existent-dir"))).to.throw( + "Directory not found", + ); + }); + + it("should throw an error for a file", () => { + fs.writeFileSync(path.join(tmpDir.name, "test.txt"), ""); + expect(() => listFiles(path.join(tmpDir.name, "test.txt"))).to.throw(); + }); + }); + + describe("readFile", () => { + it("should read a file", () => { + fs.writeFileSync(path.join(tmpDir.name, "test.txt"), "hello world"); + expect(readFile(path.join(tmpDir.name, "test.txt"))).to.equal("hello world"); + }); + + it("should throw an error if the file does not exist", () => { + expect(() => readFile(path.join(tmpDir.name, "test.txt"))).to.throw("File not found"); + }); + + it("should throw an error for a directory", () => { + fs.mkdirSync(path.join(tmpDir.name, "test-dir")); + expect(() => readFile(path.join(tmpDir.name, "test-dir"))).to.throw(); + }); + }); + + describe("moveAll", () => { + it("should move all files and directories from one directory to another", () => { + const srcDir = path.join(tmpDir.name, "src"); + const destDir = path.join(tmpDir.name, "dest"); + fs.mkdirSync(srcDir); + fs.writeFileSync(path.join(srcDir, "file1.txt"), "hello"); + fs.mkdirSync(path.join(srcDir, "dir1")); + fs.writeFileSync(path.join(srcDir, "dir1", "file2.txt"), "world"); + + moveAll(srcDir, destDir); + + expect(fs.existsSync(path.join(destDir, "file1.txt"))).to.be.true; + expect(fs.existsSync(path.join(destDir, "dir1"))).to.be.true; + expect(fs.existsSync(path.join(destDir, "dir1", "file2.txt"))).to.be.true; + }); + + it("should not move the destination directory into itself", () => { + const srcDir = path.join(tmpDir.name, "src"); + const destDir = path.join(srcDir, "dest"); + fs.mkdirSync(srcDir); + fs.mkdirSync(destDir); + fs.writeFileSync(path.join(srcDir, "file1.txt"), "hello"); + + moveAll(srcDir, destDir); + + expect(fs.existsSync(path.join(destDir, "file1.txt"))).to.be.true; + expect(fs.existsSync(path.join(destDir, "dest"))).to.be.false; + }); + }); +}); diff --git a/src/fsutils.ts b/src/fsutils.ts index 98300331461..06c5ea9c1b3 100644 --- a/src/fsutils.ts +++ b/src/fsutils.ts @@ -1,9 +1,12 @@ -import { statSync } from "fs"; +import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "fs"; +import * as path from "path"; +import { FirebaseError } from "./error"; +import { moveSync } from "fs-extra"; export function fileExistsSync(path: string): boolean { try { return statSync(path).isFile(); - } catch (e) { + } catch (e: any) { return false; } } @@ -11,7 +14,42 @@ 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; + } +} + +export function listFiles(path: string): string[] { + try { + return readdirSync(path); + } catch (e: unknown) { + if ((e as NodeJS.ErrnoException).code === "ENOENT") { + throw new FirebaseError(`Directory not found: ${path}`); + } + throw e; + } +} + +// Move all files and directories inside srcDir to destDir +export function moveAll(srcDir: string, destDir: string) { + if (!existsSync(destDir)) { + mkdirSync(destDir, { recursive: true }); + } + const files = listFiles(srcDir); + for (const f of files) { + const srcPath = path.join(srcDir, f); + if (srcPath === destDir) continue; + moveSync(srcPath, path.join(destDir, f)); + } +} 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 new file mode 100644 index 00000000000..392e7bee126 --- /dev/null +++ b/src/functional.ts @@ -0,0 +1,165 @@ +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}}) + * returns {"the.answser": 42} + * @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: 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); + } + } + } + + yield* helper([], obj); +} + +/** + * Yields each non-array element recursively in arr. + * Useful for for-of loops. + * [...flatten([[[1]], [2], 3])] = [1, 2, 3] + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function* flattenArray(arr: T): Generator> { + for (const val of arr) { + if (Array.isArray(val)) { + yield* flattenArray(val); + } else { + yield val as LeafElems; + } + } +} + +/** Shorthand for flattenObject. */ +export function flatten(obj: T): Generator<[string, string]>; +/** Shorthand for flattenArray. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function flatten(arr: T): Generator>; + +/** Flattens an object or array. */ +export function flatten(objOrArr: T): unknown { + if (Array.isArray(objOrArr)) { + return flattenArray(objOrArr); + } else { + return flattenObject(objOrArr); + } +} + +/** + * Used with reduce to flatten in place. + * Due to the quirks of TypeScript, callers must pass [] as the + * second argument to reduce. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function reduceFlat(accum: T[] | undefined, next: unknown): T[] { + return [...(accum || []), ...(flatten([next]) as Generator)]; +} + +/** + * Yields each element from left and right in tandem + * [...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) { + throw new Error("Cannot zip between two lists of differen lengths"); + } + for (let i = 0; i < left.length; i++) { + yield [left[i], right[i]]; + } +} + +/** + * 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]]; + }; + +/** Used with type guards to guarantee that all cases have been covered. */ +export function assertExhaustive(val: never, message?: string): never { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(message || `Never has a value (${val}).`); +} + +/** + * 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[], predicate: (elem: T) => boolean): [T[], T[]] { + return arr.reduce<[T[], T[]]>( + (acc, 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); + }; + +/** + * Returns true if the given values match. If either one is undefined, the default value is used for comparison. + * @param lhs the first value. + * @param rhs the second value. + * @param defaultValue the value to use if either input is undefined. + */ +export function optionalValueMatches( + lhs: T | undefined, + rhs: T | undefined, + defaultValue: T, +): boolean { + lhs = lhs === undefined ? defaultValue : lhs; + rhs = rhs === undefined ? defaultValue : rhs; + return lhs === rhs; +} diff --git a/src/functions/artifacts.spec.ts b/src/functions/artifacts.spec.ts new file mode 100644 index 00000000000..e6124c17c6d --- /dev/null +++ b/src/functions/artifacts.spec.ts @@ -0,0 +1,783 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import * as artifactregistry from "../gcp/artifactregistry"; +import * as artifacts from "./artifacts"; + +describe("functions artifacts", () => { + describe("makeRepoPath", () => { + it("should construct a valid repo path", () => { + const path = artifacts.makeRepoPath("my-project", "us-central1"); + expect(path).to.equal("projects/my-project/locations/us-central1/repositories/gcf-artifacts"); + }); + }); + + describe("findExistingPolicy", () => { + it("should return undefined if repository has no cleanup policies", () => { + const repo: artifactregistry.Repository = { + name: "projects/my-project/locations/us-central1/repositories/gcf-artifacts", + format: "DOCKER", + description: "Cloud Functions container artifacts", + createTime: "", + updateTime: "", + }; + + const policy = artifacts.findExistingPolicy(repo); + expect(policy).to.be.undefined; + }); + + it("should return undefined if policy doesn't exist", () => { + const repo: artifactregistry.Repository = { + name: "projects/my-project/locations/us-central1/repositories/gcf-artifacts", + format: "DOCKER", + description: "Cloud Functions container artifacts", + createTime: "", + updateTime: "", + cleanupPolicies: { + "other-policy": { + id: "other-policy", + action: "DELETE", + condition: { + tagState: "ANY", + olderThan: `${5 * 60 * 60 * 24}s`, + }, + }, + }, + }; + + const policy = artifacts.findExistingPolicy(repo); + expect(policy).to.be.undefined; + }); + + it("should return the policy if it exists", () => { + const expectedPolicy: artifactregistry.CleanupPolicy = { + id: artifacts.CLEANUP_POLICY_ID, + action: "DELETE", + condition: { + tagState: "ANY", + olderThan: `${5 * 60 * 60 * 24}s`, + }, + }; + + const repo: artifactregistry.Repository = { + name: "projects/my-project/locations/us-central1/repositories/gcf-artifacts", + format: "DOCKER", + description: "Cloud Functions container artifacts", + createTime: "", + updateTime: "", + cleanupPolicies: { + [artifacts.CLEANUP_POLICY_ID]: expectedPolicy, + }, + }; + + const policy = artifacts.findExistingPolicy(repo); + expect(policy).to.deep.equal(expectedPolicy); + }); + }); + + describe("daysToSeconds", () => { + it("should convert days to seconds with 's' suffix", () => { + expect(artifacts.daysToSeconds(1)).to.equal("86400s"); + expect(artifacts.daysToSeconds(5)).to.equal("432000s"); + expect(artifacts.daysToSeconds(30)).to.equal("2592000s"); + }); + }); + + describe("parseDaysFromPolicy", () => { + it("should correctly parse seconds into days", () => { + expect(artifacts.parseDaysFromPolicy("86400s")).to.equal(1); + expect(artifacts.parseDaysFromPolicy("432000s")).to.equal(5); + expect(artifacts.parseDaysFromPolicy("2592000s")).to.equal(30); + }); + + it("should return undefined for invalid formats", () => { + expect(artifacts.parseDaysFromPolicy("5d")).to.be.undefined; + expect(artifacts.parseDaysFromPolicy("invalid")).to.be.undefined; + expect(artifacts.parseDaysFromPolicy("")).to.be.undefined; + }); + }); + + describe("generateCleanupPolicy", () => { + it("should generate a valid cleanup policy with the correct days", () => { + const policy = artifacts.generateCleanupPolicy(7); + expect(policy).to.have.property(artifacts.CLEANUP_POLICY_ID); + expect(policy[artifacts.CLEANUP_POLICY_ID].id).to.equal(artifacts.CLEANUP_POLICY_ID); + expect(policy[artifacts.CLEANUP_POLICY_ID].action).to.equal("DELETE"); + expect(policy[artifacts.CLEANUP_POLICY_ID].condition).to.deep.include({ + tagState: "ANY", + olderThan: `${7 * 60 * 60 * 24}s`, + }); + }); + }); + + describe("updateRepository", () => { + let sandbox: sinon.SinonSandbox; + let updateRepositoryStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + updateRepositoryStub = sandbox.stub(artifactregistry, "updateRepository").resolves(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should call artifactregistry.updateRepository with the correct parameters", async () => { + const repoUpdate: artifactregistry.RepositoryInput = { + name: "projects/my-project/locations/us-central1/repositories/gcf-artifacts", + labels: { key: "value" }, + }; + + await artifacts.updateRepository(repoUpdate); + expect(updateRepositoryStub).to.have.been.calledOnceWith(repoUpdate); + }); + + it("should handle 403 errors with a descriptive error message", async () => { + const error = new Error("Permission denied"); + Object.assign(error, { status: 403 }); + updateRepositoryStub.rejects(error); + + const repoUpdate: artifactregistry.RepositoryInput = { + name: "projects/my-project/locations/us-central1/repositories/gcf-artifacts", + }; + + try { + await artifacts.updateRepository(repoUpdate); + expect.fail("Should have thrown an error"); + } catch (err: any) { + expect(err.message).to.include("You don't have permission to update this repository"); + expect(err.message).to.include("Artifact Registry Administrator"); + expect(err.exit).to.equal(1); + } + }); + }); + + describe("optOutRepository", () => { + let sandbox: sinon.SinonSandbox; + let updateRepositoryStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + updateRepositoryStub = sandbox.stub(artifacts, "updateRepository").resolves(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should delete an existing cleanup policy and add the opt-out label", async () => { + const repository: artifactregistry.Repository = { + name: "projects/my-project/locations/us-central1/repositories/gcf-artifacts", + format: "DOCKER", + description: "Cloud Functions container artifacts", + createTime: "", + updateTime: "", + cleanupPolicies: { + [artifacts.CLEANUP_POLICY_ID]: { + id: artifacts.CLEANUP_POLICY_ID, + action: "DELETE", + condition: { + tagState: "ANY", + olderThan: `${3 * 60 * 60 * 24}s`, + }, + }, + }, + labels: { existingLabel: "value" }, + }; + + await artifacts.optOutRepository(repository); + + expect(updateRepositoryStub).to.have.been.calledOnce; + const updateArg = updateRepositoryStub.firstCall.args[0]; + expect(updateArg.name).to.equal(repository.name); + expect(updateArg.labels).to.deep.include({ + existingLabel: "value", + [artifacts.OPT_OUT_LABEL_KEY]: "true", + }); + expect(updateArg.cleanupPolicies).to.not.have.property(artifacts.CLEANUP_POLICY_ID); + }); + + it("should add the opt-out label even when no policy exists", async () => { + const repository: artifactregistry.Repository = { + name: "projects/my-project/locations/us-central1/repositories/gcf-artifacts", + format: "DOCKER", + description: "Cloud Functions container artifacts", + createTime: "", + updateTime: "", + labels: { existingLabel: "value" }, + }; + + await artifacts.optOutRepository(repository); + + expect(updateRepositoryStub).to.have.been.calledOnce; + const updateArg = updateRepositoryStub.firstCall.args[0]; + expect(updateArg.name).to.equal(repository.name); + expect(updateArg.labels).to.deep.include({ + existingLabel: "value", + [artifacts.OPT_OUT_LABEL_KEY]: "true", + }); + }); + }); + + describe("setCleanupPolicy", () => { + let sandbox: sinon.SinonSandbox; + let updateRepositoryStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + updateRepositoryStub = sandbox.stub(artifacts, "updateRepository").resolves(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should set a cleanup policy with the specified days", async () => { + const repository: artifactregistry.Repository = { + name: "projects/my-project/locations/us-central1/repositories/gcf-artifacts", + format: "DOCKER", + description: "Cloud Functions container artifacts", + createTime: "", + updateTime: "", + labels: { existingLabel: "value" }, + }; + + const daysToKeep = 7; + await artifacts.setCleanupPolicy(repository, daysToKeep); + + expect(updateRepositoryStub).to.have.been.calledOnce; + const updateArg = updateRepositoryStub.firstCall.args[0]; + expect(updateArg.name).to.equal(repository.name); + expect(updateArg.cleanupPolicies).to.have.property(artifacts.CLEANUP_POLICY_ID); + expect(updateArg.cleanupPolicies[artifacts.CLEANUP_POLICY_ID].condition.olderThan).to.equal( + `${60 * 60 * 24 * 7}s`, + ); + }); + + it("should preserve existing cleanup policies", async () => { + const repository: artifactregistry.Repository = { + name: "projects/my-project/locations/us-central1/repositories/gcf-artifacts", + format: "DOCKER", + description: "Cloud Functions container artifacts", + createTime: "", + updateTime: "", + cleanupPolicies: { + "other-policy": { + id: "other-policy", + action: "DELETE", + condition: { + tagState: "ANY", + olderThan: `${5 * 60 * 60 * 24}s`, + }, + }, + }, + labels: { existingLabel: "value" }, + }; + + const daysToKeep = 7; + await artifacts.setCleanupPolicy(repository, daysToKeep); + + expect(updateRepositoryStub).to.have.been.calledOnce; + const updateArg = updateRepositoryStub.firstCall.args[0]; + expect(updateArg.cleanupPolicies).to.have.property("other-policy"); + expect(updateArg.cleanupPolicies).to.have.property(artifacts.CLEANUP_POLICY_ID); + }); + + it("should remove the opt-out label if it exists", async () => { + const repository: artifactregistry.Repository = { + name: "projects/my-project/locations/us-central1/repositories/gcf-artifacts", + format: "DOCKER", + description: "Cloud Functions container artifacts", + createTime: "", + updateTime: "", + labels: { + existingLabel: "value", + [artifacts.OPT_OUT_LABEL_KEY]: "true", + }, + }; + + const daysToKeep = 7; + await artifacts.setCleanupPolicy(repository, daysToKeep); + + expect(updateRepositoryStub).to.have.been.calledOnce; + const updateArg = updateRepositoryStub.firstCall.args[0]; + expect(updateArg.labels).to.have.property("existingLabel"); + expect(updateArg.labels).to.not.have.property(artifacts.OPT_OUT_LABEL_KEY); + }); + }); + + describe("hasSameCleanupPolicy", () => { + it("should return true if policy exists with same settings", () => { + const repo: artifactregistry.Repository = { + name: "test-repo", + format: "DOCKER", + description: "Test repo", + createTime: "", + updateTime: "", + cleanupPolicies: { + [artifacts.CLEANUP_POLICY_ID]: { + id: artifacts.CLEANUP_POLICY_ID, + action: "DELETE", + condition: { + tagState: "ANY", + olderThan: `${5 * 60 * 60 * 24}s`, + }, + }, + }, + }; + + expect(artifacts.hasSameCleanupPolicy(repo, 5)).to.be.true; + }); + + it("should return false if policy doesn't exist", () => { + const repo: artifactregistry.Repository = { + name: "test-repo", + format: "DOCKER", + description: "Test repo", + createTime: "", + updateTime: "", + }; + + expect(artifacts.hasSameCleanupPolicy(repo, 5)).to.be.false; + }); + + it("should return false if policy exists with different days", () => { + const repo: artifactregistry.Repository = { + name: "test-repo", + format: "DOCKER", + description: "Test repo", + createTime: "", + updateTime: "", + cleanupPolicies: { + [artifacts.CLEANUP_POLICY_ID]: { + id: artifacts.CLEANUP_POLICY_ID, + action: "DELETE", + condition: { + tagState: "ANY", + olderThan: `${3 * 60 * 60 * 24}s`, + }, + }, + }, + }; + + expect(artifacts.hasSameCleanupPolicy(repo, 5)).to.be.false; + }); + + it("should return false if policy exists with different tag state", () => { + const repo: artifactregistry.Repository = { + name: "test-repo", + format: "DOCKER", + description: "Test repo", + createTime: "", + updateTime: "", + cleanupPolicies: { + [artifacts.CLEANUP_POLICY_ID]: { + id: artifacts.CLEANUP_POLICY_ID, + action: "DELETE", + condition: { + tagState: "TAGGED", + olderThan: `${5 * 60 * 60 * 24}s`, + }, + }, + }, + }; + + expect(artifacts.hasSameCleanupPolicy(repo, 5)).to.be.false; + }); + + it("should return false if policy exists without olderThan condition", () => { + const repo: artifactregistry.Repository = { + name: "test-repo", + format: "DOCKER", + description: "Test repo", + createTime: "", + updateTime: "", + cleanupPolicies: { + [artifacts.CLEANUP_POLICY_ID]: { + id: artifacts.CLEANUP_POLICY_ID, + action: "DELETE", + condition: { + tagState: "ANY", + olderThan: "", + }, + }, + }, + }; + + expect(artifacts.hasSameCleanupPolicy(repo, 5)).to.be.false; + }); + }); + + describe("hasCleanupOptOut", () => { + it("should return true if the repository has the opt-out label set to true", () => { + const repo: artifactregistry.Repository = { + name: "test-repo", + format: "DOCKER", + description: "Test repo", + createTime: "", + updateTime: "", + labels: { + [artifacts.OPT_OUT_LABEL_KEY]: "true", + "other-label": "value", + }, + }; + + expect(artifacts.hasCleanupOptOut(repo)).to.be.true; + }); + + it("should return false if the repository has the opt-out label set to a different value", () => { + const repo: artifactregistry.Repository = { + name: "test-repo", + format: "DOCKER", + description: "Test repo", + createTime: "", + updateTime: "", + labels: { + [artifacts.OPT_OUT_LABEL_KEY]: "false", + "other-label": "value", + }, + }; + + expect(artifacts.hasCleanupOptOut(repo)).to.be.false; + }); + + it("should return false if the repository doesn't have the opt-out label", () => { + const repo: artifactregistry.Repository = { + name: "test-repo", + format: "DOCKER", + description: "Test repo", + createTime: "", + updateTime: "", + labels: { + "other-label": "value", + }, + }; + + expect(artifacts.hasCleanupOptOut(repo)).to.be.false; + }); + + it("should return false if the repository has no labels", () => { + const repo: artifactregistry.Repository = { + name: "test-repo", + format: "DOCKER", + description: "Test repo", + createTime: "", + updateTime: "", + }; + + expect(artifacts.hasCleanupOptOut(repo)).to.be.false; + }); + }); + describe("getRepo", () => { + const projectId = "my-project"; + let sandbox: sinon.SinonSandbox; + let getRepositoryStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + getRepositoryStub = sandbox.stub(artifactregistry, "getRepository"); + artifacts.getRepoCache.clear(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should fetch and cache the repository when not cached", async () => { + const repoPath = artifacts.makeRepoPath(projectId, "us-central1"); + const mockRepo: artifactregistry.Repository = { + name: repoPath, + format: "DOCKER", + description: "Test repo", + createTime: "", + updateTime: "", + }; + getRepositoryStub.resolves(mockRepo); + + const result = await artifacts.getRepo(projectId, "us-central1"); + expect(result).to.deep.equal(mockRepo); + const cachedResult = await artifacts.getRepo(projectId, "us-central1"); + expect(getRepositoryStub).to.have.been.calledOnce; + expect(cachedResult).to.deep.equal(mockRepo); + }); + + it("should fetch fresh repository when forceRefresh is true", async () => { + const repoPath = artifacts.makeRepoPath(projectId, "us-central1"); + const mockRepo: artifactregistry.Repository = { + name: repoPath, + format: "DOCKER", + description: "Test repo", + createTime: "", + updateTime: "", + }; + getRepositoryStub.resolves(mockRepo); + + const result = await artifacts.getRepo(projectId, "us-central1"); + expect(getRepositoryStub).to.have.been.calledOnce; + expect(result).to.deep.equal(mockRepo); + const cachedResult = await artifacts.getRepo( + projectId, + "us-central1", + true /* forceRefresh */, + ); + expect(getRepositoryStub).to.have.been.calledTwice; + expect(cachedResult).to.deep.equal(mockRepo); + }); + }); + + describe("checkCleanupPolicy", () => { + const projectId = "my-project"; + let sandbox: sinon.SinonSandbox; + let getRepoStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + getRepoStub = sandbox.stub(artifacts, "getRepo"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return empty arrays when no locations are provided", async () => { + const result = await artifacts.checkCleanupPolicy(projectId, []); + + expect(result).to.deep.equal({ locationsToSetup: [], locationsWithErrors: [] }); + expect(getRepoStub).not.to.have.been.called; + }); + + it("should identify locations that need cleanup policies", async () => { + const locations = ["us-central1", "us-east1", "europe-west1"]; + + const repos: Record = { + "us-central1": { + name: artifacts.makeRepoPath(projectId, "us-central1"), + format: "DOCKER", + description: "Repo with no policy or opt-out", + createTime: "", + updateTime: "", + }, + "us-east1": { + name: artifacts.makeRepoPath(projectId, "us-east1"), + format: "DOCKER", + description: "Repo with policy", + createTime: "", + updateTime: "", + cleanupPolicies: { + [artifacts.CLEANUP_POLICY_ID]: { + id: artifacts.CLEANUP_POLICY_ID, + action: "DELETE", + condition: { + tagState: "ANY", + olderThan: "86400s", + }, + }, + }, + }, + "europe-west1": { + name: artifacts.makeRepoPath(projectId, "europe-west1"), + format: "DOCKER", + description: "Repo with other policies", + createTime: "", + updateTime: "", + cleanupPolicies: { + "other-policy": { + id: "other-policy", + action: "DELETE", + condition: { + tagState: "ANY", + olderThan: "86400s", + }, + }, + }, + }, + }; + + getRepoStub.callsFake((projectId: string, location: string) => { + return repos[location]; + }); + + const result = await artifacts.checkCleanupPolicy(projectId, locations); + + expect(result.locationsToSetup).to.deep.equal(["us-central1"]); + expect(result.locationsWithErrors).to.deep.equal([]); + }); + + it("should identify locations with opt-out", async () => { + const locations = ["us-central1"]; + + const repo = { + name: artifacts.makeRepoPath(projectId, "us-central1"), + format: "DOCKER", + description: "Repo with opt-out", + createTime: "", + updateTime: "", + labels: { [artifacts.OPT_OUT_LABEL_KEY]: "true" }, + }; + + getRepoStub.resolves(repo); + + const result = await artifacts.checkCleanupPolicy(projectId, locations); + + expect(result.locationsToSetup).to.deep.equal([]); + expect(result.locationsWithErrors).to.deep.equal([]); + }); + + it("should handle locations with errors", async () => { + const locations = ["us-central1", "error-location"]; + + getRepoStub.callsFake((projectId, location) => { + if (location === "error-location") { + throw new Error("Test error"); + } + return { + name: artifacts.makeRepoPath(projectId, location), + format: "DOCKER", + description: "Test repo", + createTime: "", + updateTime: "", + }; + }); + + const result = await artifacts.checkCleanupPolicy(projectId, locations); + + expect(result.locationsToSetup).to.deep.equal(["us-central1"]); + expect(result.locationsWithErrors).to.deep.equal(["error-location"]); + }); + }); + + describe("setCleanupPolicies", () => { + const projectId = "my-project"; + let sandbox: sinon.SinonSandbox; + let getRepoStub: sinon.SinonStub; + let setCleanupPolicyStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + getRepoStub = sandbox.stub(artifacts, "getRepo"); + setCleanupPolicyStub = sandbox.stub(artifacts, "setCleanupPolicy").resolves(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return empty arrays when no locations are provided", async () => { + const result = await artifacts.setCleanupPolicies(projectId, [], 1); + + expect(result).to.deep.equal({ locationsWithPolicy: [], locationsWithErrors: [] }); + expect(getRepoStub).not.to.have.been.called; + }); + + it("should set cleanup policies for all provided locations", async () => { + const locations = ["us-central1", "us-east1"]; + const daysToKeep = 7; + + const repos: Record = { + "us-central1": { + name: artifacts.makeRepoPath(projectId, "us-central1"), + format: "DOCKER", + description: "Test repo 1", + createTime: "", + updateTime: "", + }, + "us-east1": { + name: artifacts.makeRepoPath(projectId, "us-east1"), + format: "DOCKER", + description: "Test repo 2", + createTime: "", + updateTime: "", + }, + }; + + getRepoStub.callsFake((projectId: string, location: string) => { + return repos[location]; + }); + + const result = await artifacts.setCleanupPolicies(projectId, locations, daysToKeep); + + expect(result).to.deep.equal({ + locationsWithPolicy: ["us-central1", "us-east1"], + locationsWithErrors: [], + }); + + expect(setCleanupPolicyStub).to.have.been.calledTwice; + expect(setCleanupPolicyStub).to.have.been.calledWith(repos["us-central1"], daysToKeep); + expect(setCleanupPolicyStub).to.have.been.calledWith(repos["us-east1"], daysToKeep); + }); + + it("should handle errors when getting repositories", async () => { + const locations = ["us-central1", "error-location"]; + const daysToKeep = 7; + + const repo: artifactregistry.Repository = { + name: artifacts.makeRepoPath(projectId, "us-central1"), + format: "DOCKER", + description: "Test repo", + createTime: "", + updateTime: "", + }; + + getRepoStub.callsFake((projectId: string, location: string) => { + if (location === "error-location") { + throw new Error("Test error"); + } + return repo; + }); + + const result = await artifacts.setCleanupPolicies(projectId, locations, daysToKeep); + + expect(result).to.deep.equal({ + locationsWithPolicy: ["us-central1"], + locationsWithErrors: ["error-location"], + }); + + expect(setCleanupPolicyStub).to.have.been.calledOnce; + expect(setCleanupPolicyStub).to.have.been.calledWith(repo, daysToKeep); + }); + + it("should handle errors when applying cleanup policy to repository", async () => { + const locations = ["us-central1", "us-east1"]; + const daysToKeep = 7; + + const repos: Record = { + "us-central1": { + name: artifacts.makeRepoPath(projectId, "us-central1"), + format: "DOCKER", + description: "Test repo 1", + createTime: "", + updateTime: "", + }, + "us-east1": { + name: artifacts.makeRepoPath(projectId, "us-east1"), + format: "DOCKER", + description: "Test repo 2", + createTime: "", + updateTime: "", + }, + }; + + getRepoStub.callsFake((projectId: string, location: string) => { + return repos[location]; + }); + + // Make setCleanupPolicy fail for us-east1 + setCleanupPolicyStub.callsFake((repo) => { + if (repo.name.includes("us-east1")) { + throw new Error("Failed to set policy"); + } + }); + + const result = await artifacts.setCleanupPolicies(projectId, locations, daysToKeep); + + expect(result).to.deep.equal({ + locationsWithPolicy: ["us-central1"], + locationsWithErrors: ["us-east1"], + }); + + expect(getRepoStub).to.have.been.calledTwice; + expect(setCleanupPolicyStub).to.have.been.calledTwice; + }); + }); +}); diff --git a/src/functions/artifacts.ts b/src/functions/artifacts.ts new file mode 100644 index 00000000000..94d6b138038 --- /dev/null +++ b/src/functions/artifacts.ts @@ -0,0 +1,309 @@ +import * as artifactregistry from "../gcp/artifactregistry"; +import { logger } from "../logger"; +import { FirebaseError } from "../error"; + +/** + * Repository id used by Cloud Run functions for storing artifacts. + * See https://cloud.google.com/functions/docs/building#image_registry + */ +export const GCF_REPO_ID = "gcf-artifacts"; + +/** ID used for cleanup policies created by Firebase CLI */ +export const CLEANUP_POLICY_ID = "firebase-functions-cleanup"; + +/** + * Label key used to mark repositories as opted-out from cleanup policy + * This prevents prompts from being printed to encourage developers to + * set up cleanup policies. + */ +export const OPT_OUT_LABEL_KEY = "firebase-functions-cleanup-opted-out"; + +/** + * Default number of days to keep container images for cleanup policies + */ +export const DEFAULT_CLEANUP_DAYS = 1; + +const SECONDS_IN_DAY = 24 * 60 * 60; + +/** + * Construct the full path to a repository in Artifact Registry + * + * @returns The full path to the repository + */ +export function makeRepoPath( + projectId: string, + location: string, + repoName: string = GCF_REPO_ID, +): string { + return `projects/${projectId}/locations/${location}/repositories/${repoName}`; +} + +/** + * For testing purposes + * TODO: Consider using cache object stored in deploy context instead of using globals. + */ +export const getRepoCache = new Map(); + +/** + * Returns AR repository (cached) + */ +export async function getRepo( + projectId: string, + location: string, + forceRefresh = false, + repoName: string = GCF_REPO_ID, +): Promise { + const repoPath = makeRepoPath(projectId, location, repoName); + if (!forceRefresh && getRepoCache.has(repoPath)) { + return getRepoCache.get(repoPath)!; + } + const repo = await artifactregistry.getRepository(repoPath); + getRepoCache.set(repoPath, repo); + return repo; +} + +/** + * Extract an existing cleanup policy from the repository if it exists + * + * @returns The existing policy if found, undefined otherwise + */ +export function findExistingPolicy( + repository: artifactregistry.Repository, +): artifactregistry.CleanupPolicy | undefined { + return repository?.cleanupPolicies?.[CLEANUP_POLICY_ID]; +} + +/** + * Convert days to seconds for olderThan property in cleanup policy. + * + * @returns String representation of seconds with 's' suffix (e.g., "432000s") + */ +export function daysToSeconds(days: number): string { + const seconds = days * SECONDS_IN_DAY; + return `${seconds}s`; +} + +/** + * Extract the number of days from a policy's olderThan string + * + * @example "432000s" -> 5 (5 days in seconds) + * @returns The number of days, or undefined if format is invalid + */ +export function parseDaysFromPolicy(olderThan: string): number | undefined { + const match = olderThan.match(/^(\d+)s$/); + if (match && match[1]) { + const seconds = parseInt(match[1], 10); + return Math.floor(seconds / SECONDS_IN_DAY); + } + return; +} + +/** + * Generate a cleanup policy configuration for Artifact Registry. + * + * @returns A basic cleanup policy configuration with given days. + */ +export function generateCleanupPolicy( + daysToKeep: number, +): Record { + return { + [CLEANUP_POLICY_ID]: { + id: CLEANUP_POLICY_ID, + condition: { + tagState: "ANY", + olderThan: daysToSeconds(daysToKeep), + }, + action: "DELETE", + }, + }; +} + +/** + * Helper function to handle common error handling for repository operations + */ +export async function updateRepository(repo: artifactregistry.RepositoryInput): Promise { + try { + await artifactregistry.updateRepository(repo); + } catch (err: any) { + if (err.status === 403) { + throw new FirebaseError( + `You don't have permission to update this repository.\n` + + `To update repository settings, ask your administrator to grant you the ` + + `Artifact Registry Administrator (roles/artifactregistry.admin) IAM role on the repository project.`, + { original: err, exit: 1 }, + ); + } else { + throw new FirebaseError("Failed to update artifact registry repository", { + original: err, + }); + } + } +} + +/** + * Opt out a repository from cleanup policies and delete any existing policy + */ +export async function optOutRepository(repository: artifactregistry.Repository): Promise { + const policies: artifactregistry.Repository["cleanupPolicies"] = { + ...repository.cleanupPolicies, + }; + if (CLEANUP_POLICY_ID in policies) { + delete policies[CLEANUP_POLICY_ID]; + } + const update: artifactregistry.RepositoryInput = { + name: repository.name, + labels: { ...repository.labels, [OPT_OUT_LABEL_KEY]: "true" }, + cleanupPolicies: policies, + }; + await exports.updateRepository(update); +} + +/** + * Set cleanup policy on a repository + */ +export async function setCleanupPolicy( + repository: artifactregistry.Repository, + daysToKeep: number, +): Promise { + const labels = { ...repository.labels }; + delete labels[OPT_OUT_LABEL_KEY]; + const update: artifactregistry.RepositoryInput = { + name: repository.name, + cleanupPolicies: { + ...repository.cleanupPolicies, + ...generateCleanupPolicy(daysToKeep), + }, + cleanupPolicyDryRun: false, + labels, + }; + await exports.updateRepository(update); +} + +/** + * Check if a repository has a cleanup policy with the exact same settings + * + * @returns True if the policy exists with the same settings, false otherwise + */ +export function hasSameCleanupPolicy( + repository: artifactregistry.Repository, + daysToKeep: number, +): boolean { + const existingPolicy = findExistingPolicy(repository); + if (!existingPolicy) { + return false; + } + if (existingPolicy.condition?.tagState !== "ANY" || !existingPolicy.condition?.olderThan) { + return false; + } + const existingSeconds = parseDaysFromPolicy(existingPolicy.condition.olderThan); + return existingSeconds === daysToKeep; +} + +/** + * Checks if a repository has the designated label indicating it should be + * opted out of the cleanup process. + * + * @returns True if the user explicilty opted out from cleanup policy. + */ +export function hasCleanupOptOut(repo: artifactregistry.Repository): boolean { + return !!(repo.labels && repo.labels[OPT_OUT_LABEL_KEY] === "true"); +} + +/** + * Checks whether a clean up policy is required for Artifact Registry in given locations. + */ +export async function checkCleanupPolicy( + projectId: string, + locations: string[], +): Promise<{ locationsToSetup: string[]; locationsWithErrors: string[] }> { + if (locations.length === 0) { + return { locationsToSetup: [], locationsWithErrors: [] }; + } + + const checkRepos = await Promise.allSettled( + locations.map(async (location) => { + try { + const repository = await exports.getRepo(projectId, location); + const hasPolicy = !!findExistingPolicy(repository); + const hasOptOut = hasCleanupOptOut(repository); + const hasOtherPolicies = + repository.cleanupPolicies && + Object.keys(repository.cleanupPolicies).some((key) => key !== CLEANUP_POLICY_ID); + + return { + location, + repository, + hasPolicy, + hasOptOut, + hasOtherPolicies, + }; + } catch (err) { + logger.debug(`Failed to check artifact cleanup policy for region ${location}:`, err); + throw err; + } + }), + ); + + const locationsToSetup = []; + const locationsWithErrors = []; + + for (let i = 0; i < checkRepos.length; i++) { + const result = checkRepos[i]; + if (result.status === "fulfilled") { + if (!(result.value.hasPolicy || result.value.hasOptOut || result.value.hasOtherPolicies)) { + locationsToSetup.push(result.value.location); + } + } else { + locationsWithErrors.push(locations[i]); + } + } + return { locationsToSetup, locationsWithErrors }; +} + +/** + * Sets Artifact Registry cleanup policies for given locations. + */ +export async function setCleanupPolicies( + projectId: string, + locations: string[], + daysToKeep: number, +): Promise<{ locationsWithPolicy: string[]; locationsWithErrors: string[] }> { + if (locations.length === 0) return { locationsWithPolicy: [], locationsWithErrors: [] }; + + const locationsWithPolicy: string[] = []; + const locationsWithErrors: string[] = []; + + const setupRepos = await Promise.allSettled( + locations.map(async (location) => { + try { + logger.debug(`Setting up artifact cleanup policy for region ${location}`); + const repo = await exports.getRepo(projectId, location); + await exports.setCleanupPolicy(repo, daysToKeep); + return location; + } catch (err: unknown) { + throw new FirebaseError("Failed to set up artifact cleanup policy", { + original: err as Error, + }); + } + }), + ); + + for (let i = 0; i < locations.length; i++) { + const location = locations[i]; + const result = setupRepos[i]; + if (result.status === "rejected") { + logger.debug( + `Failed to set up artifact cleanup policy for region ${location}:`, + result.reason, + ); + locationsWithErrors.push(location); + } else { + locationsWithPolicy.push(location); + } + } + + return { + locationsWithPolicy, + locationsWithErrors, + }; +} diff --git a/src/functions/constants.ts b/src/functions/constants.ts new file mode 100644 index 00000000000..744cc1cb429 --- /dev/null +++ b/src/functions/constants.ts @@ -0,0 +1,18 @@ +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", + "before-send-email": "providers/cloud.auth/eventTypes/user.beforeSendEmail", + "before-send-sms": "providers/cloud.auth/eventTypes/user.beforeSendSms", +}; + +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", + "providers/cloud.auth/eventTypes/user.beforeSendEmail": "before-send-email", + "providers/cloud.auth/eventTypes/user.beforeSendSms": "before-send-sms", +}; diff --git a/src/functions/deprecationWarnings.ts b/src/functions/deprecationWarnings.ts new file mode 100644 index 00000000000..68c3c0b38d4 --- /dev/null +++ b/src/functions/deprecationWarnings.ts @@ -0,0 +1,22 @@ +import { logWarningToStderr } from "../utils"; + +const FUNCTIONS_CONFIG_DEPRECATION_MESSAGE = `DEPRECATION NOTICE: Action required to deploy after March 2026 + + functions.config() API is deprecated. + Cloud Runtime Configuration API, the Google Cloud service used to store function configuration data, will be shut down in March 2026. As a result, you must migrate away from using functions.config() to continue deploying your functions after March 2026. + + What this means for you: + + - The Firebase CLI commands for managing this configuration (functions:config:set, get, unset, clone, and export) are deprecated. These commands will no longer work after March 2026. + - firebase deploy command will fail for functions that use the legacy functions.config() API after March 2026. + + Existing deployments will continue to work with their current configuration. + + See your migration options at: https://firebase.google.com/docs/functions/config-env#migrate-to-dotenv`; + +/** + * Logs a deprecation warning for functions.config() usage + */ +export function logFunctionsConfigDeprecationWarning(): void { + logWarningToStderr(FUNCTIONS_CONFIG_DEPRECATION_MESSAGE); +} 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..957bea92bd7 --- /dev/null +++ b/src/functions/env.spec.ts @@ -0,0 +1,845 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import { rmSync } from "node:fs"; +import { expect } from "chai"; + +import * as env from "./env"; +import { FirebaseError } from "../error"; +import { ParamValue } from "../deploy/functions/params"; + +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(() => { + rmSync(tmpdir, { recursive: true }); + 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(() => { + rmSync(tmpdir, { recursive: true }); + 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"); + }); + + it("loads envs from a different configDir", () => { + const configDir = path.join(tmpdir, "config"); + fs.mkdirSync(configDir); + createEnvFiles(configDir, { + ".env": "FOO=foo\nBAR=bar", + }); + + expect( + env.loadUserEnvs({ ...projectInfo, functionsSource: tmpdir, configDir: configDir }), + ).to.be.deep.equal({ + FOO: "foo", + BAR: "bar", + }); + }); + }); + + describe("parseStrict", () => { + it("should parse valid environment variables", () => { + const input = ` + FOO=foo + BAR="bar" + BAZ='baz' + `; + const expected = { FOO: "foo", BAR: "bar", BAZ: "baz" }; + expect(env.parseStrict(input)).to.deep.equal(expected); + }); + + it("should throw an error for invalid lines", () => { + const input = ` + FOO=foo + INVALID LINE + BAR=bar + `; + expect(() => env.parseStrict(input)).to.throw(FirebaseError, /Invalid dotenv file/); + }); + + it("should throw an error for reserved keys", () => { + const input = ` + FIREBASE_CONFIG=config + FOO=foo + `; + expect(() => env.parseStrict(input)).to.throw(FirebaseError, /Validation failed/); + }); + + it("should throw an error for invalid key formats", () => { + const input = ` + foo=bar + BAR=baz + `; + expect(() => env.parseStrict(input)).to.throw(FirebaseError, /Validation failed/); + }); + + it("should handle escape sequences correctly", () => { + const input = ` + FOO="foo\\nbar" + BAR="bar\\tfoo" + BAZ="baz\\rfoo" + `; + const expected = { FOO: "foo\nbar", BAR: "bar\tfoo", BAZ: "baz\rfoo" }; + expect(env.parseStrict(input)).to.deep.equal(expected); + }); + }); + + describe("writeResolvedParams", () => { + let tmpdir: string; + + beforeEach(() => { + tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "firebase-functions-test-")); + }); + + afterEach(() => { + rmSync(tmpdir, { recursive: true, force: true }); + }); + + it("should write only new, non-internal params", () => { + const resolvedEnvs = { + EXISTING_PARAM: new ParamValue("existing", false, { string: true }), + NEW_PARAM: new ParamValue("new_value", false, { string: true }), + INTERNAL_PARAM: new ParamValue("internal", true, { string: true }), + }; + const userEnvs = { EXISTING_PARAM: "old_value" }; + const userEnvOpt = { + projectId: "test-project", + functionsSource: tmpdir, + }; + + env.writeResolvedParams(resolvedEnvs, userEnvs, userEnvOpt); + + const writtenContent = fs.readFileSync(path.join(tmpdir, ".env.test-project"), "utf-8"); + expect(writtenContent).to.include("NEW_PARAM=new_value"); + expect(writtenContent).not.to.include("EXISTING_PARAM"); + expect(writtenContent).not.to.include("INTERNAL_PARAM"); + }); + + it("should not create file when no params to write", () => { + const resolvedEnvs = { + EXISTING_PARAM: new ParamValue("existing", false, { string: true }), + INTERNAL_PARAM: new ParamValue("internal", true, { string: true }), + }; + const userEnvs = { EXISTING_PARAM: "old_value" }; + const userEnvOpt = { + projectId: "test-project", + functionsSource: tmpdir, + }; + + env.writeResolvedParams(resolvedEnvs, userEnvs, userEnvOpt); + + const envFile = path.join(tmpdir, ".env.test-project"); + expect(fs.existsSync(envFile)).to.be.false; + }); + + it("should write to .env.local for emulator", () => { + const resolvedEnvs = { + NEW_PARAM: new ParamValue("emulator_value", false, { string: true }), + }; + const userEnvs = {}; + const userEnvOpt = { + projectId: "test-project", + functionsSource: tmpdir, + isEmulator: true, + }; + + env.writeResolvedParams(resolvedEnvs, userEnvs, userEnvOpt); + + const writtenContent = fs.readFileSync(path.join(tmpdir, ".env.local"), "utf-8"); + expect(writtenContent).to.include("NEW_PARAM=emulator_value"); + expect(fs.existsSync(path.join(tmpdir, ".env.test-project"))).to.be.false; + }); + + it("should handle params with special characters in values", () => { + const resolvedEnvs = { + NEW_PARAM: new ParamValue("value with\nnewline", false, { string: true }), + }; + const userEnvs = {}; + const userEnvOpt = { + projectId: "test-project", + functionsSource: tmpdir, + }; + + env.writeResolvedParams(resolvedEnvs, userEnvs, userEnvOpt); + + const writtenContent = fs.readFileSync(path.join(tmpdir, ".env.test-project"), "utf-8"); + expect(writtenContent).to.include('NEW_PARAM="value with\\nnewline"'); + }); + }); +}); diff --git a/src/functions/env.ts b/src/functions/env.ts new file mode 100644 index 00000000000..12fc0f4129c --- /dev/null +++ b/src/functions/env.ts @@ -0,0 +1,438 @@ +import * as clc from "colorette"; +import * as fs from "fs"; +import * as path from "path"; +import { ParamValue } from "../deploy/functions/params"; + +import { FirebaseError } from "../error"; +import { logger } from "../logger"; +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", + "GCP_PROJECT", + "GCLOUD_PROJECT", + "GOOGLE_CLOUD_PROJECT", + "FUNCTION_TRIGGER_TYPE", + "FUNCTION_NAME", + "FUNCTION_MEMORY_MB", + "FUNCTION_TIMEOUT_SEC", + "FUNCTION_IDENTITY", + "FUNCTION_REGION", + // Cloud Functions - new runtimes: + // https://cloud.google.com/functions/docs/env-var#newer_runtimes + "FUNCTION_TARGET", + "FUNCTION_SIGNATURE_TYPE", + "K_SERVICE", + "K_REVISION", + "PORT", + // Cloud Run: + // https://cloud.google.com/run/docs/reference/container-contract#env-vars + "K_CONFIGURATION", +]; + +// Regex to capture key, value pair in a dotenv file. +// Inspired by: +// https://github.com/bkeepers/dotenv/blob/master/lib/dotenv/parser.rb +// prettier-ignore +const LINE_RE = new RegExp( + "^" + // begin line + "\\s*" + // leading whitespaces + "(?: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 + '\\s*"(?:\\\\"|[^"])*"|' + // double quoted or + "[^#\\r\\n]*" + // unquoted + ")?" + // end optional value + "\\s*" + // trailing whitespaces + "(?:#[^\\n]*)?" + // optional comment + "$", // end line + "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[]; +} + +/** + * Parse contents of a dotenv file. + * + * Each line should contain key, value pairs, e.g.: + * + * SERVICE_URL=https://example.com + * + * Values can be double quoted, e.g.: + * + * SERVICE_URL="https://example.com" + * + * Double quoted values can include newlines, e.g.: + * + * PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nABC\nEFG\n-----BEGIN PUBLIC KEY-----"" + * + * or span multiple lines, e.g.: + * + * PUBLIC_KEY="-----BEGIN PUBLIC KEY----- + * ABC + * EFG + * -----BEGIN PUBLIC KEY-----" + * + * See test for more examples. + * + * @return {ParseResult} Result containing parsed key, value pairs and errored lines. + */ +export function parse(data: string): ParseResult { + const envs: Record = {}; + const errors: string[] = []; + + data = data.replace(/\r\n?/, "\n"); // For Windows support. + let match; + while ((match = LINE_RE.exec(data))) { + let [, k, v] = match; + v = (v || "").trim(); + + let quotesMatch; + if ((quotesMatch = /^(["'])(.*)\1$/ms.exec(v)) != null) { + // Remove surrounding single/double quotes. + v = quotesMatch[2]; + if (quotesMatch[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]); + } + } + + envs[k] = v; + } + + const nonmatches = data.replace(LINE_RE, ""); + for (let line of nonmatches.split(/[\r\n]+/)) { + line = line.trim(); + if (line.startsWith("#")) { + // Ignore comments + continue; + } + if (line.length) errors.push(line); + } + + return { envs, errors }; +} + +export class KeyValidationError extends Error { + constructor( + public key: string, + public message: string, + ) { + super(`Failed to validate key ${key}: ${message}`); + } +} + +/** + * Validates string for use as an env var key. + * + * We restrict key names to ones that conform to POSIX standards. + * This is more restrictive than what is allowed in Cloud Functions or Cloud Run. + */ +export function validateKey(key: string): void { + if (RESERVED_KEYS.includes(key)) { + throw new KeyValidationError(key, `Key ${key} is reserved for internal use.`); + } + if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) { + 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.", + ); + } + if (RESERVED_PREFIXES.some((prefix) => key.startsWith(prefix))) { + throw new KeyValidationError( + key, + `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. + */ +export function parseStrict(data: string): Record { + const { envs, errors } = parse(data); + + if (errors.length) { + throw new FirebaseError(`Invalid dotenv file, error on lines: ${errors.join(",")}`); + } + + const validationErrors: KeyValidationError[] = []; + for (const key of Object.keys(envs)) { + try { + validateKey(key); + } catch (err: any) { + logger.debug(`Failed to validate key ${key}: ${err}`); + if (err instanceof KeyValidationError) { + validationErrors.push(err); + } else { + // Unexpected error. Throw. + throw err; + } + } + } + if (validationErrors.length > 0) { + throw new FirebaseError("Validation failed", { children: validationErrors }); + } + + return envs; +} + +function findEnvfiles( + configDir: string, + projectId: string, + projectAlias?: string, + isEmulator?: boolean, +): string[] { + const files: string[] = [".env"]; + files.push(`.env.${projectId}`); + if (projectAlias) { + files.push(`.env.${projectAlias}`); + } + if (isEmulator) { + files.push(FUNCTIONS_EMULATOR_DOTENV); + } + + return files + .map((f) => path.join(configDir, f)) + .filter(fs.existsSync) + .map((p) => path.basename(p)); +} + +export interface UserEnvsOpts { + functionsSource: string; + configDir?: string; + projectId: string; + projectAlias?: string; + isEmulator?: boolean; +} + +/** + * Checks if user has specified any environment variables for their functions. + * + * @return True if there are any user-specified environment variables + */ +export function hasUserEnvs(opts: UserEnvsOpts): boolean { + const configDir = opts.configDir || opts.functionsSource; + return findEnvfiles(configDir, opts.projectId, opts.projectAlias, opts.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 { projectId, projectAlias, isEmulator } = envOpts; + const configDir = envOpts.configDir || envOpts.functionsSource; + + // Determine which .env file to write to, and create it if it doesn't exist + const allEnvFiles = findEnvfiles(configDir, projectId, projectAlias, isEmulator); + const targetEnvFile = envOpts.isEmulator + ? FUNCTIONS_EMULATOR_DOTENV + : `.env.${envOpts.projectId}`; + const targetEnvFileExists = allEnvFiles.includes(targetEnvFile); + if (!targetEnvFileExists) { + fs.writeFileSync(path.join(configDir, 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(configDir, 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. + * + * Look for .env files at the root of functions source directory + * and load the contents of the .env files. + * + * .env files are searched and merged in the following order: + * + * 1. .env + * 2. .env. + * + * If both .env. and .env. files are found, an error is thrown. + * + * @return {Record} Environment variables for the project. + */ +export function loadUserEnvs(opts: UserEnvsOpts): Record { + const configDir = opts.configDir || opts.functionsSource; + const envFiles = findEnvfiles(configDir, opts.projectId, opts.projectAlias, opts.isEmulator); + if (envFiles.length === 0) { + return {}; + } + + // Disallow setting both .env. and .env. + if (opts.projectAlias) { + if ( + envFiles.includes(`.env.${opts.projectId}`) && + envFiles.includes(`.env.${opts.projectAlias}`) + ) { + throw new FirebaseError( + `Can't have both dotenv files with projectId (env.${opts.projectId}) ` + + `and projectAlias (.env.${opts.projectAlias}) as extensions.`, + ); + } + } + + let envs: Record = {}; + for (const f of envFiles) { + try { + const data = fs.readFileSync(path.join(configDir, f), "utf8"); + envs = { ...envs, ...parseStrict(data) }; + } catch (err: any) { + throw new FirebaseError(`Failed to load environment variables from ${f}.`, { + exit: 2, + children: err.children?.length > 0 ? err.children : [err], + }); + } + } + logBullet( + clc.cyan(clc.bold("functions: ")) + `Loaded environment variables from ${envFiles.join(", ")}.`, + ); + + return envs; +} + +/** + * Load Firebase-set environment variables. + * + * @return Environment varibles for functions. + */ +export function loadFirebaseEnvs( + firebaseConfig: Record, + projectId: string, +): Record { + return { + FIREBASE_CONFIG: JSON.stringify(firebaseConfig), + GCLOUD_PROJECT: projectId, + }; +} + +/** + * Writes newly resolved params to the appropriate .env file. + * Skips internal params and params that already exist in userEnvs. + */ +export function writeResolvedParams( + resolvedEnvs: Readonly>, + userEnvs: Readonly>, + userEnvOpt: UserEnvsOpts, +): void { + const toWrite: Record = {}; + + for (const paramName of Object.keys(resolvedEnvs)) { + const paramValue = resolvedEnvs[paramName]; + if (!paramValue.internal && !Object.prototype.hasOwnProperty.call(userEnvs, paramName)) { + toWrite[paramName] = paramValue.toString(); + } + } + + writeUserEnvs(toWrite, userEnvOpt); +} 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..d4f201b2358 --- /dev/null +++ b/src/functions/events/v1.ts @@ -0,0 +1,16 @@ +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 BEFORE_SEND_EMAIL_EVENT = "providers/cloud.auth/eventTypes/user.beforeSendEmail"; + +export const BEFORE_SEND_SMS_EVENT = "providers/cloud.auth/eventTypes/user.beforeSendSms"; + +export const AUTH_BLOCKING_EVENTS = [ + BEFORE_CREATE_EVENT, + BEFORE_SIGN_IN_EVENT, + BEFORE_SEND_EMAIL_EVENT, + BEFORE_SEND_SMS_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..cd897d9a5ee --- /dev/null +++ b/src/functions/events/v2.ts @@ -0,0 +1,58 @@ +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 FIREALERTS_EVENT = "google.firebase.firebasealerts.alerts.v1.published"; + +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] + | typeof FIREALERTS_EVENT; + +// Why can't auth context be removed? This is map was added to correct a bug where a regex +// allowed any non-auth type to be converted to any auth type, but we should follow up for why +// a functon can't opt into reducing PII. +export const CONVERTABLE_EVENTS: Partial> = { + "google.cloud.firestore.document.v1.created": + "google.cloud.firestore.document.v1.created.withAuthContext", + "google.cloud.firestore.document.v1.updated": + "google.cloud.firestore.document.v1.updated.withAuthContext", + "google.cloud.firestore.document.v1.deleted": + "google.cloud.firestore.document.v1.deleted.withAuthContext", + "google.cloud.firestore.document.v1.written": + "google.cloud.firestore.document.v1.written.withAuthContext", +}; 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 new file mode 100644 index 00000000000..102f76b47d4 --- /dev/null +++ b/src/functions/functionslog.ts @@ -0,0 +1,47 @@ +import { logger } from "../logger"; +import { LogEntry } from "../gcp/cloudlogging"; + +/** + * The correct API filter to use when GCFv2 is enabled and/or we want specific function logs + * @param functionList list of functions seperated by comma + * @return the correct filter for use when calling the list api + */ +export function getApiFilter(functionList?: string) { + 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 `resource.labels.function_name="${fn}" ` + `OR resource.labels.service_name="${fn}"`; + }); + return baseFilter + `\n(${apiFuncFilters.join(" OR ")})`; + } + + return baseFilter; +} + +/** + * Logs all entires with info severity to the CLI + * @param entries a list of {@link LogEntry} + */ +export function logEntries(entries: LogEntry[]): void { + if (!entries || entries.length === 0) { + logger.info("No log entries found."); + return; + } + for (let i = entries.length - 1; i >= 0; i--) { + const entry = entries[i]; + const timestamp = entry.timestamp || "---"; + const severity = (entry.severity || "?").substring(0, 1); + const name = entry.resource.labels.function_name || entry.resource.labels.service_name; + const message = + entry.textPayload || + JSON.stringify(entry.jsonPayload) || + JSON.stringify(entry.protoPayload) || + ""; + + logger.info(`${timestamp} ${severity} ${name}: ${message}`); + } +} diff --git a/src/functions/projectConfig.spec.ts b/src/functions/projectConfig.spec.ts new file mode 100644 index 00000000000..5c956b5cc5d --- /dev/null +++ b/src/functions/projectConfig.spec.ts @@ -0,0 +1,357 @@ +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, codebase: "default" }, + ]); + }); + + it("fails validation given config w/o source", () => { + // @ts-expect-error invalid function config for test + expect(() => projectConfig.validate([{ runtime: "nodejs22" }])).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("passes validation for multi-instance config with same source", () => { + const config: projectConfig.NormalizedConfig = [ + { source: "foo", codebase: "bar" }, + { source: "foo", codebase: "baz", prefix: "prefix-two" }, + ]; + expect(projectConfig.validate(config)).to.deep.equal(config); + }); + + it("passes validation for multi-instance config with one missing codebase", () => { + const config: projectConfig.NormalizedConfig = [ + { source: "foo", codebase: "bar", prefix: "bar-prefix" }, + { source: "foo" }, + ]; + const expected = [ + { source: "foo", codebase: "bar", prefix: "bar-prefix" }, + { source: "foo", codebase: "default" }, + ]; + expect(projectConfig.validate(config)).to.deep.equal(expected); + }); + + it("fails validation for multi-instance config with missing codebase and a default codebase", () => { + const config: projectConfig.NormalizedConfig = [ + { source: "foo", codebase: "default" }, + { source: "foo" }, + ]; + expect(() => projectConfig.validate(config)).to.throw( + FirebaseError, + /functions.codebase must be unique but 'default' was used more than once./, + ); + }); + + it("fails validation for multi-instance config with multiple missing codebases", () => { + const config: projectConfig.NormalizedConfig = [{ source: "foo" }, { source: "foo" }]; + expect(() => projectConfig.validate(config)).to.throw( + FirebaseError, + /functions.codebase must be unique but 'default' was used more than once./, + ); + }); + + 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/); + }); + + it("fails validation given prefix with invalid characters", () => { + expect(() => projectConfig.validate([{ ...TEST_CONFIG_0, prefix: "abc.efg" }])).to.throw( + FirebaseError, + /Invalid prefix/, + ); + }); + + it("fails validation given prefix with capital letters", () => { + expect(() => projectConfig.validate([{ ...TEST_CONFIG_0, prefix: "ABC" }])).to.throw( + FirebaseError, + /Invalid prefix/, + ); + }); + + it("fails validation given prefix starting with a digit", () => { + expect(() => projectConfig.validate([{ ...TEST_CONFIG_0, prefix: "1abc" }])).to.throw( + FirebaseError, + /Invalid prefix/, + ); + }); + + it("fails validation given a duplicate source/prefix pair", () => { + const config: projectConfig.NormalizedConfig = [ + { source: "foo", codebase: "bar", prefix: "a" }, + { source: "foo", codebase: "baz", prefix: "a" }, + ]; + expect(() => projectConfig.validate(config)).to.throw( + FirebaseError, + /More than one functions config specifies the same source directory \('foo'\) and prefix \('a'\)/, + ); + }); + + it("fails validation for multi-instance config with same source and no prefixes", () => { + const config: projectConfig.NormalizedConfig = [ + { source: "foo", codebase: "bar" }, + { source: "foo", codebase: "baz" }, + ]; + expect(() => projectConfig.validate(config)).to.throw( + FirebaseError, + /More than one functions config specifies the same source directory \('foo'\) and prefix \(''\)/, + ); + }); + + it("should allow a single function in an array to have a default codebase", () => { + const config: projectConfig.NormalizedConfig = [{ source: "foo" }]; + const expected = [{ source: "foo", codebase: "default" }]; + expect(projectConfig.validate(config)).to.deep.equal(expected); + }); + + describe("remoteSource", () => { + const VALID_REMOTE_CONFIG = { + remoteSource: { repository: "repo", ref: "main" }, + runtime: "nodejs20", + } as const; + + it("passes validation for a valid remoteSource config", () => { + const config: projectConfig.NormalizedConfig = [VALID_REMOTE_CONFIG]; + const expected = [{ ...VALID_REMOTE_CONFIG, codebase: "default" }]; + expect(projectConfig.validate(config)).to.deep.equal(expected); + }); + + it("passes validation for a mixed local and remote source config", () => { + const config: projectConfig.NormalizedConfig = [ + { source: "local/path", codebase: "local" }, + { ...VALID_REMOTE_CONFIG, codebase: "remote" }, + ]; + expect(projectConfig.validate(config)).to.deep.equal(config); + }); + + it("fails validation if both source and remoteSource are present", () => { + const config = [{ ...VALID_REMOTE_CONFIG, source: "local" }]; + // @ts-expect-error Should not be able to specify both source and remoteSource + expect(() => projectConfig.validate(config)).to.throw( + FirebaseError, + /Cannot specify both 'source' and 'remoteSource'/, + ); + }); + + it("fails validation if neither source nor remoteSource are present", () => { + const config = [{ runtime: "nodejs20" }]; + // @ts-expect-error Must specify either source or remoteSource + expect(() => projectConfig.validate(config)).to.throw( + FirebaseError, + /Must specify either 'source' or 'remoteSource'/, + ); + }); + + it("fails validation if remoteSource is missing runtime", () => { + const config = [{ remoteSource: { repository: "repo", ref: "main" } }]; + // @ts-expect-error remoteSource requires runtime + expect(() => projectConfig.validate(config)).to.throw( + FirebaseError, + /functions.runtime is required when using remoteSource/, + ); + }); + + it("fails validation if remoteSource is missing repository", () => { + const config = [{ remoteSource: { ref: "main" }, runtime: "nodejs20" }]; + // @ts-expect-error remoteSource requires repository + expect(() => projectConfig.validate(config)).to.throw( + FirebaseError, + /remoteSource requires 'repository' and 'ref'/, + ); + }); + + it("fails validation for duplicate remote source/prefix pairs", () => { + const config: projectConfig.NormalizedConfig = [ + { ...VALID_REMOTE_CONFIG, codebase: "bar" }, + { ...VALID_REMOTE_CONFIG, codebase: "baz" }, + ]; + expect(() => projectConfig.validate(config)).to.throw( + FirebaseError, + /More than one functions config specifies the same remote source \('repo'\) and prefix \(''\)/, + ); + }); + + it("passes validation for different remote sources with the same prefix", () => { + const config: projectConfig.NormalizedConfig = [ + { ...VALID_REMOTE_CONFIG, codebase: "bar" }, + { + remoteSource: { repository: "repo2", ref: "main" }, + runtime: "nodejs20", + codebase: "baz", + }, + ]; + expect(projectConfig.validate(config)).to.deep.equal(config); + }); + }); + }); + + describe("normalizeAndValidate", () => { + it("returns normalized config for singleton config", () => { + expect(projectConfig.normalizeAndValidate(TEST_CONFIG_0)).to.deep.equal([ + { ...TEST_CONFIG_0, codebase: "default" }, + ]); + }); + + it("returns normalized config for multi-resource config", () => { + expect(projectConfig.normalizeAndValidate([TEST_CONFIG_0])).to.deep.equal([ + { ...TEST_CONFIG_0, codebase: "default" }, + ]); + }); + + it("fails validation given singleton config w/o source", () => { + // @ts-expect-error invalid function config for test + expect(() => projectConfig.normalizeAndValidate({ runtime: "nodejs22" })).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", () => { + // @ts-expect-error invalid function config for test + expect(() => projectConfig.normalizeAndValidate([{ runtime: "nodejs22" }])).to.throw( + FirebaseError, + /codebase source must be specified/, + ); + }); + + 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/); + }); + }); + + describe("isLocalConfig/isRemoteConfig", () => { + const localCfg = { source: "local" }; + const remoteCfg = { + remoteSource: { repository: "repo", ref: "main" }, + runtime: "nodejs20" as const, + }; + it("isLocalConfig narrow correctly", () => { + const local = projectConfig.validate([localCfg])[0]; + const remote = projectConfig.validate([remoteCfg])[0]; + + expect(projectConfig.isLocalConfig(local)).to.equal(true); + expect(projectConfig.isRemoteConfig(local)).to.equal(false); + expect(projectConfig.isLocalConfig(remote)).to.equal(false); + expect(projectConfig.isRemoteConfig(remote)).to.equal(true); + }); + + it("isRemoteConfig narrow correctly", () => { + const local = projectConfig.validate([localCfg])[0]; + const remote = projectConfig.validate([remoteCfg])[0]; + + expect(projectConfig.isRemoteConfig(local)).to.equal(false); + expect(projectConfig.isRemoteConfig(remote)).to.equal(true); + }); + }); + + describe("requireLocal", () => { + it("does not throw for local cfg and throws for remote", () => { + const local = projectConfig.validate([{ source: "local" }])[0]; + expect(() => projectConfig.requireLocal(local)).to.not.throw(); + }); + + it("throws for remote", () => { + const remote = projectConfig.validate([ + { remoteSource: { repository: "repo", ref: "main" }, runtime: "nodejs20" }, + ])[0]; + expect(() => projectConfig.requireLocal(remote, "msg")).to.throw(FirebaseError, /msg/); + }); + }); + + describe("resolveConfigDir", () => { + it("prefers configDir, falls back to source, and returns undefined for remote without configDir", () => { + const cfg = projectConfig.validate([{ source: "functions", configDir: "cfg" }])[0]; + expect(projectConfig.resolveConfigDir(cfg)).to.equal("cfg"); + + const remoteCfg = projectConfig.validate([ + { + remoteSource: { repository: "repo", ref: "main" }, + runtime: "nodejs20", + configDir: "cfg", + }, + ])[0]; + expect(projectConfig.resolveConfigDir(remoteCfg)).to.equal("cfg"); + }); + + it("falls back to source if configDir is missing", () => { + const cfg = projectConfig.validate([{ source: "functions" }])[0]; + expect(projectConfig.resolveConfigDir(cfg)).to.equal("functions"); + }); + + it("returns undefined for remote w/o configDir", () => { + const cfg = projectConfig.validate([ + { + remoteSource: { repository: "repo", ref: "main" }, + runtime: "nodejs20", + }, + ])[0]; + expect(projectConfig.resolveConfigDir(cfg)).to.be.undefined; + }); + }); +}); diff --git a/src/functions/projectConfig.ts b/src/functions/projectConfig.ts new file mode 100644 index 00000000000..1a14d236b6f --- /dev/null +++ b/src/functions/projectConfig.ts @@ -0,0 +1,241 @@ +import { FunctionsConfig, FunctionConfig } from "../firebaseConfig"; +import { FirebaseError } from "../error"; +import type { ActiveRuntime } from "../deploy/functions/runtimes/supported/types"; + +export type NormalizedConfig = [FunctionConfig, ...FunctionConfig[]]; +// Stronger validated variants: local vs remote. +type FunctionConfigCommon = Omit< + FunctionConfig, + "source" | "remoteSource" | "codebase" | "runtime" +>; + +export type ValidatedLocalSingle = FunctionConfigCommon & { + source: string; + codebase: string; + // runtime optional for local (auto-detected if not provided) + runtime?: ActiveRuntime; + remoteSource?: never; +}; + +export type ValidatedRemoteSingle = FunctionConfigCommon & { + remoteSource: { repository: string; ref: string; dir?: string }; + // runtime required for remote + runtime: ActiveRuntime; + codebase: string; + source?: never; +}; + +export type ValidatedSingle = ValidatedLocalSingle | ValidatedRemoteSingle; +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.", + ); + } +} + +/** + * Check that the prefix contains only allowed characters. + */ +export function validatePrefix(prefix: string): void { + if (prefix.length > 30) { + throw new FirebaseError("Invalid prefix. Prefix must be 30 characters or less."); + } + // Must start with a letter so that the resulting function id also starts with a letter. + if (!/^[a-z](?:[a-z0-9-]*[a-z0-9])?$/.test(prefix)) { + throw new FirebaseError( + "Invalid prefix. Prefix must start with a lowercase letter, can contain only lowercase letters, numeric characters, and dashes, and cannot start or end with a dash.", + ); + } +} + +function validateSingle(config: FunctionConfig): ValidatedSingle { + const { source, remoteSource, runtime, codebase: providedCodebase, ...rest } = config; + + // Exactly one of source or remoteSource must be specified + if (source && remoteSource) { + throw new FirebaseError( + "Cannot specify both 'source' and 'remoteSource' in a single functions config. Please choose one.", + ); + } + if (!source && !remoteSource) { + throw new FirebaseError( + "codebase source must be specified. Must specify either 'source' or 'remoteSource' in a functions config.", + ); + } + + const codebase = providedCodebase ?? DEFAULT_CODEBASE; + validateCodebase(codebase); + if (config.prefix) { + validatePrefix(config.prefix); + } + const commonConfig = { codebase, ...rest }; + if (source) { + return { + ...commonConfig, + source, + ...(runtime ? { runtime } : {}), + }; + } else if (remoteSource) { + if (!remoteSource.repository || !remoteSource.ref) { + throw new FirebaseError("remoteSource requires 'repository' and 'ref' to be specified."); + } + if (!runtime) { + // TODO: Once functions.yaml can provide a runtime, relax this requirement. + throw new FirebaseError( + "functions.runtime is required when using remoteSource in firebase.json.", + ); + } + return { + ...commonConfig, + remoteSource, + runtime, + }; + } + + // Unreachable due to XOR guard + throw new FirebaseError("Invalid functions config."); +} + +/** + * 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); + } +} + +function assertUniqueSourcePrefixPair(config: ValidatedConfig): void { + const sourcePrefixPairs = new Set(); + for (const c of config) { + let sourceIdentifier: string; + let sourceDescription: string; + if (c.source) { + sourceIdentifier = c.source; + sourceDescription = `source directory ('${c.source}')`; + } else if (c.remoteSource) { + sourceIdentifier = `remote:${c.remoteSource.repository}#${c.remoteSource.ref}@dir:${ + c.remoteSource.dir || "." + }`; + sourceDescription = `remote source ('${c.remoteSource.repository}')`; + } else { + // This case should be prevented by `validateSingle`. + continue; + } + + const key = JSON.stringify({ source: sourceIdentifier, prefix: c.prefix || "" }); + if (sourcePrefixPairs.has(key)) { + throw new FirebaseError( + `More than one functions config specifies the same ${sourceDescription} and prefix ('${ + c.prefix ?? "" + }'). Please add a unique 'prefix' to each function configuration that shares this source to resolve the conflict.`, + ); + } + sourcePrefixPairs.add(key); + } +} + +/** + * Validate functions config. + */ +export function validate(config: NormalizedConfig): ValidatedConfig { + const validated = config.map((cfg) => validateSingle(cfg)) as ValidatedConfig; + assertUnique(validated, "codebase"); + assertUniqueSourcePrefixPair(validated); + 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; +} + +/** Returns true if the codebase uses a local source. */ +export function isLocalConfig(c: ValidatedSingle): c is ValidatedLocalSingle { + return (c as ValidatedLocalSingle).source !== undefined; +} + +/** Returns true if the codebase uses a remote source. */ +export function isRemoteConfig(c: ValidatedSingle): c is ValidatedRemoteSingle { + return (c as ValidatedRemoteSingle).remoteSource !== undefined; +} + +/** + * Require a local functions config. Throws a FirebaseError if the config is remote. + * @param c The validated functions config entry. + * @param purpose Optional message to use in the error. + */ +export function requireLocal(c: ValidatedSingle, purpose?: string): ValidatedLocalSingle { + if (!isLocalConfig(c)) { + const msg = + purpose ?? + "This operation requires a local functions source directory, but the codebase is configured with a remote source."; + throw new FirebaseError(msg); + } + return c; +} + +/** + * Resolve the directory used for .env files. + * - Local: returns `configDir` if set, otherwise `source`. + * - Remote: returns `configDir` if set, otherwise `undefined`. + */ +export function resolveConfigDir(c: ValidatedSingle): string | undefined { + return c.configDir || c.source; +} 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/functions/runtimeConfigExport.spec.ts b/src/functions/runtimeConfigExport.spec.ts new file mode 100644 index 00000000000..73ce06c1ae1 --- /dev/null +++ b/src/functions/runtimeConfigExport.spec.ts @@ -0,0 +1,159 @@ +import { expect } from "chai"; + +import * as configExport from "./runtimeConfigExport"; +import * as env from "./env"; +import * as sinon from "sinon"; +import * as rc from "../rc"; + +describe("functions-config-export", () => { + describe("getAllProjects", () => { + let loadRCStub: sinon.SinonStub; + + beforeEach(() => { + loadRCStub = sinon.stub(rc, "loadRC").returns({} as any); // eslint-disable-line @typescript-eslint/no-explicit-any + }); + + afterEach(() => { + loadRCStub.restore(); + }); + + it("should include projectId from the options", () => { + expect(configExport.getProjectInfos({ projectId: "project-0" })).to.have.deep.members([ + { + projectId: "project-0", + }, + ]); + }); + + it("should include project and its alias from firebaserc", () => { + loadRCStub.returns({ projects: { dev: "project-0", prod: "project-1" } }); + expect(configExport.getProjectInfos({ projectId: "project-0" })).to.have.deep.members([ + { + projectId: "project-0", + alias: "dev", + }, + { + projectId: "project-1", + alias: "prod", + }, + ]); + }); + }); + + describe("convertKey", () => { + it("should converts valid config key", () => { + expect(configExport.convertKey("service.api.url", "")).to.be.equal("SERVICE_API_URL"); + expect(configExport.convertKey("foo-bar.car", "")).to.be.equal("FOO_BAR_CAR"); + }); + + it("should throw error if conversion is invalid", () => { + expect(() => { + configExport.convertKey("1.api.url", ""); + }).to.throw(); + expect(() => { + configExport.convertKey("x.google.env", ""); + }).to.throw(); + expect(() => { + configExport.convertKey("k.service", ""); + }).to.throw(); + }); + + it("should use prefix to fix invalid config keys", () => { + expect(configExport.convertKey("1.api.url", "CONFIG_")).to.equal("CONFIG_1_API_URL"); + expect(configExport.convertKey("x.google.env", "CONFIG_")).to.equal("CONFIG_X_GOOGLE_ENV"); + expect(configExport.convertKey("k.service", "CONFIG_")).to.equal("CONFIG_K_SERVICE"); + }); + + it("should throw error if prefix is invalid", () => { + expect(() => { + configExport.convertKey("1.api.url", "X_GOOGLE_"); + }).to.throw(); + expect(() => { + configExport.convertKey("x.google.env", "FIREBASE_"); + }).to.throw(); + expect(() => { + configExport.convertKey("k.service", "123_"); + }).to.throw(); + }); + }); + + describe("configToEnv", () => { + 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" }, + { origKey: "service.api.name", newKey: "SERVICE_API_NAME", value: "a service" }, + { origKey: "foo.bar", newKey: "FOO_BAR", value: "foobar" }, + ]); + expect(errors).to.be.empty; + }); + + 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" }, + { origKey: "service.api.name", newKey: "SERVICE_API_NAME", value: "a service" }, + ]); + expect(errors).to.not.be.empty; + }); + + it("should use prefix to fix invalid keys", () => { + const { success, errors } = configExport.configToEnv( + { firebase: { name: "foobar" }, service: { api: { url: "foobar", name: "a service" } } }, + "CONFIG_", + ); + expect(success).to.have.deep.members([ + { origKey: "service.api.url", newKey: "SERVICE_API_URL", value: "foobar" }, + { origKey: "service.api.name", newKey: "SERVICE_API_NAME", value: "a service" }, + { origKey: "firebase.name", newKey: "CONFIG_FIREBASE_NAME", value: "foobar" }, + ]); + expect(errors).to.be.empty; + }); + }); + + describe("toDotenvFormat", () => { + it("should produce valid dotenv file with keys", () => { + const dotenv = configExport.toDotenvFormat([ + { origKey: "service.api.url", newKey: "SERVICE_API_URL", value: "hello" }, + { origKey: "service.api.name", newKey: "SERVICE_API_NAME", value: "world" }, + ]); + const { envs, errors } = env.parse(dotenv); + expect(envs).to.be.deep.equal({ + SERVICE_API_URL: "hello", + SERVICE_API_NAME: "world", + }); + expect(errors).to.be.empty; + }); + + it("should preserve newline characters", () => { + const dotenv = configExport.toDotenvFormat([ + { 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\nthere\nworld", + }); + expect(errors).to.be.empty; + }); + }); + + describe("generateDotenvFilename", () => { + it("should generate dotenv filename using project alias", () => { + expect( + 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", + ); + }); + }); +}); diff --git a/src/functions/runtimeConfigExport.ts b/src/functions/runtimeConfigExport.ts new file mode 100644 index 00000000000..cc4b4be47ac --- /dev/null +++ b/src/functions/runtimeConfigExport.ts @@ -0,0 +1,201 @@ +import * as clc from "colorette"; + +import * as env from "./env"; +import * as functionsConfig from "../functionsConfig"; +import { FirebaseError } from "../error"; +import { logger } from "../logger"; +import { getProjectId } from "../projectUtils"; +import { loadRC } from "../rc"; +import { logWarning } from "../utils"; +import { flatten } from "../functional"; + +export interface ProjectConfigInfo { + projectId: string; + alias?: string; + config?: Record; + envs?: EnvMap[]; +} + +export interface EnvMap { + origKey: string; + newKey: string; + value: string; + err?: string; +} + +export interface ConfigToEnvResult { + success: EnvMap[]; + errors: Required[]; +} + +/** + * Find all projects (and its alias) associated with the current directory. + */ +export function getProjectInfos(options: { + project?: string; + projectId?: string; + cwd?: string; +}): ProjectConfigInfo[] { + const result: Record = {}; + + const rc = loadRC(options); + if (rc.projects) { + for (const [alias, projectId] of Object.entries(rc.projects)) { + if (Object.keys(result).includes(projectId)) { + logWarning( + `Multiple aliases found for ${clc.bold(projectId)}. ` + + `Preferring alias (${clc.bold(result[projectId])}) over (${clc.bold(alias)}).`, + ); + continue; + } + result[projectId] = alias; + } + } + + // We export runtime config of a --project set via CLI flag, allowing export command to run on projects that's + // never been added to the .firebaserc file. + const projectId = getProjectId(options); + if (projectId && !Object.keys(result).includes(projectId)) { + result[projectId] = projectId; + } + + return Object.entries(result).map(([k, v]) => { + const result: ProjectConfigInfo = { projectId: k }; + if (k !== v) { + result.alias = v; + } + return result; + }); +} + +/** + * Fetch and fill in runtime config for each project. + */ +export async function hydrateConfigs(pInfos: ProjectConfigInfo[]): Promise { + const hydrate = pInfos.map((info) => { + return functionsConfig + .materializeAll(info.projectId) + .then((config) => { + info.config = config; + return; + }) + .catch((err) => { + logger.debug( + `Failed to fetch runtime config for project ${info.projectId}: ${err.message}`, + ); + }); + }); + await Promise.all(hydrate); +} + +/** + * Converts functions config key from runtime config to env var key. + * If the original config key fails to convert, try again with provided prefix. + * + * Throws KeyValidationError if the converted key is invalid. + */ +export function convertKey(configKey: string, prefix: string): string { + /* prettier-ignore */ + const baseKey = configKey + .toUpperCase() // 1. Uppercase all characters (e.g. SOME-SERVICE.KEY) + .replace(/\./g, "_") // 2. Dots to underscores (e.g. SOME-SERVICE_KEY) + .replace(/-/g, "_"); // 3. Dashses to underscores (e.g. SOME_SERVICE_KEY) + + let envKey = baseKey; + try { + env.validateKey(envKey); + } catch (err: any) { + if (err instanceof env.KeyValidationError) { + envKey = prefix + envKey; + env.validateKey(envKey); + } + } + return envKey; +} + +/** + * Convert runtime config into a map of env vars. + */ +export function configToEnv(configs: Record, prefix: string): ConfigToEnvResult { + const success = []; + const errors = []; + + for (const [configKey, value] of flatten(configs)) { + try { + const envKey = convertKey(configKey, prefix); + success.push({ origKey: configKey, newKey: envKey, value: value as string }); + } catch (err: any) { + if (err instanceof env.KeyValidationError) { + errors.push({ + origKey: configKey, + newKey: err.key, + err: err.message, + value: value as string, + }); + } else { + throw new FirebaseError("Unexpected error while converting config", { + exit: 2, + original: err, + }); + } + } + } + return { success, errors }; +} + +/** + * Fill in environment variables for each project by converting project's runtime config. + * + * @return {ConfigToEnvResult} Collection of successful and errored conversion. + */ +export function hydrateEnvs(pInfos: ProjectConfigInfo[], prefix: string): string { + let errMsg = ""; + for (const pInfo of pInfos) { + const { success, errors } = configToEnv(pInfo.config!, prefix); + if (errors.length > 0) { + const msg = + `${pInfo.projectId} ` + + `${pInfo.alias ? "(" + pInfo.alias + ")" : ""}:\n` + + errors.map((err) => `\t${err.origKey} => ${clc.bold(err.newKey)} (${err.err})`).join("\n") + + "\n"; + errMsg += msg; + } else { + pInfo.envs = success; + } + } + return errMsg; +} + +const CHARACTERS_TO_ESCAPE_SEQUENCES: Record = { + "\n": "\\n", + "\r": "\\r", + "\t": "\\t", + "\v": "\\v", + "\\": "\\\\", + '"': '\\"', + "'": "\\'", +}; + +function escape(s: string): string { + // Escape newlines, tabs, backslashes and quotes + return s.replace(/[\n\r\t\v\\"']/g, (ch) => CHARACTERS_TO_ESCAPE_SEQUENCES[ch]); +} + +/** + * Convert env var mapping to dotenv compatible string. + */ +export function toDotenvFormat(envs: EnvMap[], header = ""): string { + const lines = envs.map(({ newKey, value }) => `${newKey}="${escape(value)}"`); + const maxLineLen = Math.max(...lines.map((l) => l.length)); + return ( + `${header}\n` + + lines.map((line, idx) => `${line.padEnd(maxLineLen)} # from ${envs[idx].origKey}`).join("\n") + ); +} + +/** + * Generate dotenv filename for given project. + */ +export function generateDotenvFilename(pInfo: ProjectConfigInfo): string { + return `.env.${pInfo.alias ?? pInfo.projectId}`; +} diff --git a/src/functions/secrets.spec.ts b/src/functions/secrets.spec.ts new file mode 100644 index 00000000000..207ef48cfcf --- /dev/null +++ b/src/functions/secrets.spec.ts @@ -0,0 +1,570 @@ +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 confirmStub: sinon.SinonStub; + + beforeEach(() => { + warnStub = sinon.stub(utils, "logWarning").resolves(undefined); + confirmStub = sinon.stub(prompt, "confirm").resolves(true); + }); + + afterEach(() => { + warnStub.restore(); + confirmStub.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 confirmStub: 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"); + + confirmStub = sandbox.stub(prompt, "confirm").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); + confirmStub.resolves(true); + + await expect( + secrets.ensureSecret("project-id", "MY_SECRET", options), + ).to.eventually.deep.equal(secret); + expect(warnStub).to.have.been.calledOnce; + expect(confirmStub).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(confirmStub).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..9b58641ed8a --- /dev/null +++ b/src/functions/secrets.ts @@ -0,0 +1,387 @@ +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 { confirm } 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"; +import * as Table from "cli-table3"; + +// 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 useTransformed = await confirm({ + default: true, + message: `Would you like to use ${transformedKey} as key instead?`, + nonInteractive: options.nonInteractive, + force: options.force, + }); + if (!useTransformed) { + 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 confirm({ + message: "Do you wish to continue?", + nonInteractive: options.nonInteractive, + force: options.force, + }); + 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 updateLabels = await confirm({ + default: true, + message: `Would you like to have your secret ${secret.name} managed by Cloud Functions for Firebase?`, + nonInteractive: options.nonInteractive, + force: options.force, + }); + if (updateLabels) { + 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 if (endpoint.platform === "run") { + // This may be tricky because the image has been deleted. How does this work + // with GCF? + throw new FirebaseError("Updating Cloud Run functions is not yet implemented."); + } 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 e7f79b333da..eda054cf627 100644 --- a/src/functionsConfig.ts +++ b/src/functionsConfig.ts @@ -1,14 +1,18 @@ 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 * as getProjectId from "./getProjectId"; +import { needProjectId } from "./projectUtils"; import * as runtimeconfig from "./gcp/runtimeconfig"; +import * as args from "./deploy/functions/args"; export const RESERVED_NAMESPACES = ["firebase"]; +const apiClient = new Client({ urlPrefix: firebaseApiOrigin() }); + interface Id { config: string; variable: string; @@ -26,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)."; @@ -36,13 +40,13 @@ function setVariable( } function isReservedNamespace(id: Id) { - return _.some(RESERVED_NAMESPACES, (reserved) => { + return RESERVED_NAMESPACES.some((reserved) => { return id.config.toLowerCase().startsWith(reserved); }); } export async function ensureApi(options: any): Promise { - const projectId = getProjectId(options); + const projectId = needProjectId(options); return ensureApiEnabled(projectId, "runtimeconfig.googleapis.com", "runtimeconfig", true); } @@ -54,9 +58,10 @@ 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 export function getAppEngineLocation(config: any): string { let appEngineLocation = config.locationId; if (appEngineLocation && appEngineLocation.match(/[^\d]$/)) { @@ -66,12 +71,11 @@ export function getAppEngineLocation(config: any): string { return appEngineLocation || "us-central1"; } -export async function getFirebaseConfig(options: any): Promise { - const projectId = getProjectId(options, false); - const response = await api.request("GET", "/v1beta1/projects/" + projectId + "/adminSdkConfig", { - auth: true, - origin: api.firebaseApiOrigin, - }); +export async function getFirebaseConfig(options: any): Promise { + const projectId = needProjectId(options); + const response = await apiClient.get( + `/v1beta1/projects/${projectId}/adminSdkConfig`, + ); return response.body; } @@ -81,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); - }) + }), ); } @@ -109,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); - }) + }), ); }; @@ -127,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; } @@ -150,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) { @@ -171,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)); @@ -192,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/functionsDelete.ts b/src/functionsDelete.ts deleted file mode 100644 index 141ba744924..00000000000 --- a/src/functionsDelete.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as helper from "./functionsDeployHelper"; -import { Queue } from "./throttler/queue"; -import * as tasks from "./deploy/functions/tasks"; -import { DeploymentTimer } from "./deploy/functions/deploymentTimer"; -import { ErrorHandler } from "./deploy/functions/errorHandler"; - -export async function deleteFunctions( - functionsNamesToDelete: string[], - scheduledFunctionNamesToDelete: string[], - projectId: string, - appEngineLocation: string -): Promise { - const timer = new DeploymentTimer(); - const errorHandler = new ErrorHandler(); - const cloudFunctionsQueue = new Queue({ - handler: tasks.functionsDeploymentHandler(timer, errorHandler), - retries: 30, - backoff: 10000, - concurrency: 40, - maxBackoff: 40000, - }); - const schedulerQueue = new Queue({ - handler: tasks.schedulerDeploymentHandler(errorHandler), - }); - - const taskParams = { - projectId, - errorHandler, - }; - functionsNamesToDelete.forEach((fnName) => { - const deleteFunctionTask = tasks.deleteFunctionTask(taskParams, fnName); - cloudFunctionsQueue.run(deleteFunctionTask); - }); - scheduledFunctionNamesToDelete.forEach((fnName) => { - const deleteSchedulerTask = tasks.deleteScheduleTask(taskParams, fnName, appEngineLocation); - schedulerQueue.run(deleteSchedulerTask); - }); - const queuePromises = [cloudFunctionsQueue.wait(), schedulerQueue.wait()]; - - cloudFunctionsQueue.close(); - schedulerQueue.close(); - cloudFunctionsQueue.process(); - schedulerQueue.process(); - - await Promise.all(queuePromises); - - helper.logAndTrackDeployStats(cloudFunctionsQueue, errorHandler); - errorHandler.printErrors(); -} diff --git a/src/functionsDeployHelper.ts b/src/functionsDeployHelper.ts deleted file mode 100644 index b0b8f73617b..00000000000 --- a/src/functionsDeployHelper.ts +++ /dev/null @@ -1,230 +0,0 @@ -import * as _ from "lodash"; -import * as clc from "cli-color"; - -import { FirebaseError } from "./error"; -import { logger } from "./logger"; -import * as track from "./track"; -import * as utils from "./utils"; -import * as cloudfunctions from "./gcp/cloudfunctions"; -import { Job } from "./gcp/cloudscheduler"; -import { CloudFunctionTrigger } from "./deploy/functions/deploymentPlanner"; -import Queue from "./throttler/queue"; -import { ErrorHandler } from "./deploy/functions/errorHandler"; - -export function functionMatchesAnyGroup(fnName: string, filterGroups: string[][]) { - if (!filterGroups.length) { - return true; - } - for (const groupChunks of filterGroups) { - if (functionMatchesGroup(fnName, groupChunks)) { - return true; - } - } - return false; -} - -export function functionMatchesGroup(functionName: string, groupChunks: string[]): boolean { - const last = _.last(functionName.split("/")); - if (!last) { - return false; - } - const functionNameChunks = last.split("-").slice(0, groupChunks.length); - return _.isEqual(groupChunks, functionNameChunks); -} - -export function getFilterGroups(options: any): string[][] { - if (!options.only) { - return []; - } - - let opts; - return _.chain(options.only.split(",")) - .filter((filter) => { - opts = filter.split(":"); - return opts[0] === "functions" && opts[1]; - }) - .map((filter) => { - return filter.split(":")[1].split(/[.-]/); - }) - .value(); -} - -export function getReleaseNames( - uploadNames: string[], - existingNames: string[], - functionFilterGroups: string[][] -): string[] { - if (functionFilterGroups.length === 0) { - return uploadNames; - } - - const allFunctions = _.union(uploadNames, existingNames); - return _.filter(allFunctions, (functionName) => { - return _.some( - _.map(functionFilterGroups, (groupChunks) => { - return functionMatchesGroup(functionName, groupChunks); - }) - ); - }); -} - -export function logFilters( - existingNames: string[], - releaseNames: string[], - functionFilterGroups: string[][] -): void { - if (functionFilterGroups.length === 0) { - return; - } - - logger.debug("> [functions] filtering triggers to: " + JSON.stringify(releaseNames, null, 2)); - track("Functions Deploy with Filter", "", releaseNames.length); - - let list; - if (existingNames.length > 0) { - list = _.map(existingNames, (name) => { - return getFunctionId(name) + "(" + getRegion(name) + ")"; - }).join(", "); - utils.logBullet(clc.bold.cyan("functions: ") + "current functions in project: " + list); - } - if (releaseNames.length > 0) { - list = _.map(releaseNames, (name) => { - return getFunctionId(name) + "(" + getRegion(name) + ")"; - }).join(", "); - utils.logBullet(clc.bold.cyan("functions: ") + "uploading functions in project: " + list); - } - - const allFunctions = _.union(releaseNames, existingNames); - const unmatchedFilters = _.chain(functionFilterGroups) - .filter((filterGroup) => { - return !_.some( - _.map(allFunctions, (functionName) => { - return functionMatchesGroup(functionName, filterGroup); - }) - ); - }) - .map((group) => { - return group.join("-"); - }) - .value(); - if (unmatchedFilters.length > 0) { - utils.logWarning( - clc.bold.yellow("functions: ") + - "the following filters were specified but do not match any functions in the project: " + - unmatchedFilters.join(", ") - ); - } -} - -export function getFunctionTrigger(functionInfo: CloudFunctionTrigger) { - if (functionInfo.httpsTrigger) { - return { httpsTrigger: functionInfo.httpsTrigger }; - } else if (functionInfo.eventTrigger) { - const trigger = functionInfo.eventTrigger; - trigger.failurePolicy = functionInfo.failurePolicy; - return { eventTrigger: trigger }; - } - - logger.debug("Unknown trigger type found in:", functionInfo); - throw new FirebaseError("Could not parse function trigger, unknown trigger type."); -} - -export function getFunctionId(fullName: string): string { - return fullName.split("/")[5]; -} - -/* - ** getScheduleName transforms a full function name (projects/blah/locations/blah/functions/blah) - ** into a job name for cloud scheduler - ** DANGER: We use the pattern defined here to deploy and delete schedules, - ** and to display scheduled functions in the Firebase console - ** If you change this pattern, Firebase console will stop displaying schedule descriptions - ** and schedules created under the old pattern will no longer be cleaned up correctly - */ -export function getScheduleName(fullName: string, appEngineLocation: string): string { - const [projectsPrefix, project, regionsPrefix, region, , functionName] = fullName.split("/"); - return `${projectsPrefix}/${project}/${regionsPrefix}/${appEngineLocation}/jobs/firebase-schedule-${functionName}-${region}`; -} - -/* - ** getTopicName transforms a full function name (projects/blah/locations/blah/functions/blah) - ** into a topic name for pubsub - ** DANGER: We use the pattern defined here to deploy and delete topics - ** If you change this pattern, topics created under the old pattern will no longer be cleaned up correctly - */ -export function getTopicName(fullName: string): string { - const [projectsPrefix, project, , region, , functionName] = fullName.split("/"); - return `${projectsPrefix}/${project}/topics/firebase-schedule-${functionName}-${region}`; -} - -export function getRegion(fullName: string): string { - return fullName.split("/")[3]; -} - -export function getFunctionLabel(fullName: string): string { - return getFunctionId(fullName) + "(" + getRegion(fullName) + ")"; -} - -export function toJob(fn: CloudFunctionTrigger, appEngineLocation: string, projectId: string): Job { - return Object.assign(fn.schedule as { schedule: string }, { - name: getScheduleName(fn.name, appEngineLocation), - pubsubTarget: { - topicName: getTopicName(fn.name), - attributes: { - scheduled: "true", - }, - }, - }); -} - -export function logAndTrackDeployStats(queue: Queue, errorHandler: ErrorHandler) { - const stats = queue.stats(); - logger.debug(`Total Function Deployment time: ${stats.elapsed}`); - logger.debug(`${stats.total} Functions Deployed`); - logger.debug(`${errorHandler.errors.length} Functions Errored`); - logger.debug(`Average Function Deployment time: ${stats.avg}`); - if (stats.total > 0) { - if (errorHandler.errors.length === 0) { - track("functions_deploy_result", "success", stats.total); - } else if (errorHandler.errors.length < stats.total) { - track("functions_deploy_result", "partial_success", stats.total - errorHandler.errors.length); - track("functions_deploy_result", "partial_failure", errorHandler.errors.length); - track( - "functions_deploy_result", - "partial_error_ratio", - errorHandler.errors.length / stats.total - ); - } else { - track("functions_deploy_result", "failure", stats.total); - } - } - // TODO: Track other stats here - maybe time of full deployment? -} - -export function printSuccess(funcName: string, type: string) { - utils.logSuccess( - clc.bold.green("functions[" + getFunctionLabel(funcName) + "]: ") + - "Successful " + - type + - " operation. " - ); -} - -export async function printTriggerUrls(projectId: string, sourceUrl: string) { - const functions = await cloudfunctions.listAllFunctions(projectId); - const httpsFunctions = functions.filter((fn) => { - return fn.sourceUploadUrl === sourceUrl && fn.httpsTrigger; - }); - if (httpsFunctions.length === 0) { - return; - } - - httpsFunctions.forEach((httpsFunc) => { - logger.info( - clc.bold("Function URL"), - `(${getFunctionId(httpsFunc.name)}):`, - httpsFunc.httpsTrigger?.url - ); - }); - return; -} diff --git a/src/functionsShellCommandAction.ts b/src/functionsShellCommandAction.ts index e8329a850ef..fb6cefbba08 100644 --- a/src/functionsShellCommandAction.ts +++ b/src/functionsShellCommandAction.ts @@ -1,25 +1,27 @@ /* 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(); -export const actionFunction = async (options: any) => { - if (options.port) { +export const actionFunction = async (options: Options) => { + if (typeof options.port === "string") { options.port = parseInt(options.port, 10); } @@ -28,6 +30,7 @@ export const actionFunction = async (options: any) => { debugPort = commandUtils.parseInspectionPort(options); } + needProjectId(options); const hubClient = new EmulatorHubClient(options.project); let remoteEmulators: Record = {}; @@ -37,33 +40,46 @@ export const actionFunction = async (options: any) => { } 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.get(Constants.getPortKey(Emulators.FUNCTIONS), undefined); + 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, }) @@ -81,10 +97,10 @@ export const actionFunction = async (options: any) => { const initializeContext = (context: any) => { for (const trigger of emulator.triggers) { - if (emulator.emulatedFunctions.includes(trigger.name)) { + 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 = @@ -98,21 +114,19 @@ export const actionFunction = async (options: any) => { "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..57c02079b0d --- /dev/null +++ b/src/gcp/apphosting.ts @@ -0,0 +1,765 @@ +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 = "v1beta"; + +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; + managedResources?: ManagedResource[]; +} + +export interface ManagedResource { + runService: { service: 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 type Availability = "BUILD" | "RUNTIME"; + +export interface Env { + variable: string; + secret?: string; + value?: string; + availability?: Availability[]; +} + +export interface BuildConfig { + minInstances?: number; + memory?: string; + env?: Env[]; + runCommand?: string; +} + +interface BuildSource { + codebase?: CodebaseSource; + archive?: ArchiveSource; +} + +interface CodebaseSource { + // oneof reference + branch?: string; + commit?: string; + tag?: string; + // end oneof reference + displayName: string; + hash: string; + commitMessage: string; + uri: string; + commitTime: string; +} + +interface ArchiveSource { + // oneof reference + userStorageUri?: string; + externalSignedUri?: string; + // end oneof reference + rootDirectory?: string; + author?: SourceUserMetadata; + locallyBuiltSource?: boolean; +} + +interface SourceUserMetadata { + displayName: string; + email: string; + imageUri: 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; + 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"; + +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 + 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; +} + +/** + * Gets traffic details. + */ +export async function getTraffic( + projectId: string, + location: string, + backendId: string, +): Promise { + const name = `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`; + const res = await client.get(name); + return res.body; +} + +interface RpcStatus { + code?: number; + message?: string; + details?: unknown[]; +} + +type CustomDomainState = + | "CUSTOM_DOMAIN_STATE_UNSPECIFIED" + | "HOST_STATE" + | "OWNERSHIP_STATE" + | "CERT_STATE"; + +type HostState = + | "HOST_STATE_UNSPECIFIED" + | "HOST_UNHOSTED" + | "HOST_UNREACHABLE" + | "HOST_NON_FAH" + | "HOST_CONFLICT" + | "HOST_WRONG_SHARD" + | "HOST_ACTIVE"; + +type OwnershipState = + | "OWNERSHIP_STATE_UNSPECIFIED" + | "OWNERSHIP_MISSING" + | "OWNERSHIP_UNREACHABLE" + | "OWNERSHIP_MISMATCH" + | "OWNERSHIP_CONFLICT" + | "OWNERSHIP_PENDING" + | "OWNERSHIP_ACTIVE"; + +type CertState = + | "CERT_STATE_UNSPECIFIED" + | "CERT_PREPARING" + | "CERT_VALIDATING" + | "CERT_PROPAGATING" + | "CERT_ACTIVE" + | "CERT_EXPIRING_SOON" + | "CERT_EXPIRED"; + +type DnsRecordType = "TYPE_UNSPECIFIED" | "A" | "CNAME" | "TXT" | "AAAA" | "CAA"; + +type DnsRecordAction = "NONE" | "ADD" | "REMOVE"; + +interface DnsRecord { + domain_name: string; + type: DnsRecordType; + rdata: string; + required_action: DnsRecordAction; + relevant_state: CustomDomainState[]; +} + +interface DnsRecordSet { + domain_name: string; + check_error?: RpcStatus; + records: DnsRecord[]; +} + +interface DnsUpdates { + domain_name: string; + discovered: DnsRecordSet[]; + desired: DnsRecordSet[]; + check_time: string; +} + +interface CustomDomainStatus { + host_state: HostState; + ownership_state: OwnershipState; + cert_state: CertState; + required_dns_updates: DnsUpdates[]; + issues: RpcStatus[]; +} + +interface Redirect { + uri: string; + status?: number; +} + +interface ServingBehavior { + // oneof serving_behavior + redirect?: Redirect; +} + +type DomainType = "TYPE_UNSPECIFIED" | "DEFAULT" | "CUSTOM"; + +export interface Domain { + name: string; + display_name?: string; + create_time: string; + update_time: string; + type: DomainType; + disabled?: boolean; + serve?: ServingBehavior; + custom_domain_status?: CustomDomainStatus; + reconciling: boolean; + delete_time?: string; + purge_time?: string; + labels?: Record; + annotations?: Record; + uid: string; + etag: string; +} + +interface ListDomainsResponse { + domains: Domain[]; + next_page_token?: string; + unreachable?: string[]; +} + +/** + * Lists domains for a backend. + */ +export async function listDomains( + projectId: string, + location: string, + backendId: string, +): Promise { + const name = `projects/${projectId}/locations/${location}/backends/${backendId}/domains`; + const res = await client.get(name, { queryParams: { pageSize: 100 } }); + return Array.isArray(res.body.domains) ? res.body.domains : []; +} + +/** + * 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.spec.ts b/src/gcp/artifactregistry.spec.ts new file mode 100644 index 00000000000..36f24af1d86 --- /dev/null +++ b/src/gcp/artifactregistry.spec.ts @@ -0,0 +1,143 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import * as sinon from "sinon"; +import * as artifactRegistry from "./artifactregistry"; +import { artifactRegistryDomain } from "../api"; +import * as api from "../ensureApiEnabled"; + +const API_VERSION = "v1"; +const PROJECT_ID = "test-project"; +const REGION = "us-central1"; +const REPO = "test-repo"; +const REPO_NAME = `projects/${PROJECT_ID}/locations/${REGION}/repositories/${REPO}`; + +describe("artifactRegistry", () => { + afterEach(() => { + nock.cleanAll(); + }); + + describe("getRepository", () => { + it("should resolve with a repository object on success", async () => { + const repository: artifactRegistry.Repository = { + name: REPO_NAME, + format: "DOCKER", + description: "test repo", + createTime: "2022-01-01T00:00:00Z", + updateTime: "2022-01-01T00:00:00Z", + }; + nock(artifactRegistryDomain()).get(`/${API_VERSION}/${REPO_NAME}`).reply(200, repository); + + const result = await artifactRegistry.getRepository(REPO_NAME); + + expect(result).to.deep.equal(repository); + expect(nock.isDone()).to.be.true; + }); + + it("should reject if the API call fails", async () => { + nock(artifactRegistryDomain()) + .get(`/${API_VERSION}/${REPO_NAME}`) + .reply(404, { error: { message: "Not Found" } }); + + await expect(artifactRegistry.getRepository(REPO_NAME)).to.be.rejectedWith("Not Found"); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("deletePackage", () => { + const PKG_NAME = `${REPO_NAME}/packages/test-pkg`; + + it("should resolve with an operation object on success", async () => { + const operation: artifactRegistry.Operation = { + name: `projects/${PROJECT_ID}/locations/${REGION}/operations/test-op`, + done: true, + }; + nock(artifactRegistryDomain()).delete(`/${API_VERSION}/${PKG_NAME}`).reply(200, operation); + + const result = await artifactRegistry.deletePackage(PKG_NAME); + + expect(result).to.deep.equal(operation); + expect(nock.isDone()).to.be.true; + }); + + it("should reject if the API call fails", async () => { + nock(artifactRegistryDomain()) + .delete(`/${API_VERSION}/${PKG_NAME}`) + .reply(403, { error: { message: "Permission Denied" } }); + + await expect(artifactRegistry.deletePackage(PKG_NAME)).to.be.rejectedWith( + "Permission Denied", + ); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("updateRepository", () => { + it("should send a patch request with update mask if one is present", async () => { + const repo: artifactRegistry.RepositoryInput = { + name: REPO_NAME, + labels: { + foo: "bar", + }, + }; + const resultRepo: artifactRegistry.Repository = { + ...repo, + format: "DOCKER", + description: "test repo", + createTime: "2022-01-01T00:00:00Z", + updateTime: "2022-01-01T00:00:00Z", + }; + + nock(artifactRegistryDomain()) + .patch(`/${API_VERSION}/${REPO_NAME}?updateMask=name,labels`) + .reply(200, resultRepo); + + const result = await artifactRegistry.updateRepository(repo); + + expect(result).to.deep.equal(resultRepo); + expect(nock.isDone()).to.be.true; + }); + + it("should send a patch request if only name is present", async () => { + const repo: artifactRegistry.RepositoryInput = { + name: REPO_NAME, + }; + const resultRepo: artifactRegistry.Repository = { + ...repo, + format: "DOCKER", + description: "test repo", + createTime: "2022-01-01T00:00:00Z", + updateTime: "2022-01-01T00:00:00Z", + }; + + nock(artifactRegistryDomain()) + .patch(`/${API_VERSION}/${REPO_NAME}?updateMask=name`) + .reply(200, resultRepo); + + const result = await artifactRegistry.updateRepository(repo); + + expect(result).to.deep.equal(resultRepo); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("ensureApiEnabled", () => { + let sandbox: sinon.SinonSandbox; + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + it("should call the ensure api", async () => { + const ensureApiStub = sandbox.stub(api, "ensure").resolves(); + await artifactRegistry.ensureApiEnabled(PROJECT_ID); + expect(ensureApiStub).to.have.been.calledWith( + PROJECT_ID, + "https://artifactregistry.googleapis.com", + "artifactregistry", + true, + ); + }); + }); +}); diff --git a/src/gcp/artifactregistry.ts b/src/gcp/artifactregistry.ts new file mode 100644 index 00000000000..6c589ad21c2 --- /dev/null +++ b/src/gcp/artifactregistry.ts @@ -0,0 +1,97 @@ +import { Client } from "../apiv2"; +import { artifactRegistryDomain } from "../api"; +import { assertImplements, DeepOmit, RecursiveKeyOf } from "../metaprogramming"; +import * as api from "../ensureApiEnabled"; +import * as proto from "./proto"; + +export const API_VERSION = "v1"; + +const client = new Client({ + urlPrefix: artifactRegistryDomain(), + auth: true, + apiVersion: API_VERSION, +}); + +export function ensureApiEnabled(projectId: string): Promise { + return api.ensure(projectId, artifactRegistryDomain(), "artifactregistry", true); +} + +export interface Repository { + name: string; + format: string; + description: string; + createTime: string; + updateTime: string; + cleanupPolicies?: Record; + cleanupPolicyDryRun?: boolean; + labels?: Record; +} + +export type RepositoryOutputOnlyFields = "format" | "description" | "createTime" | "updateTime"; +// This line caues a compile-time error if RepositoryOutputOnlyFields has a field that is +// missing in Repository or incompatible with the type in Repository. +assertImplements>(); + +export type RepositoryInput = DeepOmit; + +export interface Operation { + name: string; + done: boolean; + error?: { code: number; message: string; details: unknown }; + response?: { + "@type": "type.googleapis.com/google.protobuf.Empty"; + }; + metadata?: { + "@type": "type.googleapis.com/google.devtools.artifactregistry.v1beta2.OperationMetadata"; + }; +} + +export interface CleanupPolicyCondition { + tagState: string; + olderThan: string; + packageNamePrefixes?: string[]; + tagPrefixes?: string[]; + versionNamePrefixes?: string[]; + newerThan?: string; +} + +export interface CleanupPolicyMostRecentVersions { + packageNamePrefixes?: string[]; + keepCount: number; +} + +export interface CleanupPolicy { + id: string; + action: string; + condition?: CleanupPolicyCondition; + mostRecentVersions?: CleanupPolicyMostRecentVersions; +} + +/** Delete a package. */ +export async function deletePackage(name: string): Promise { + const res = await client.delete(name); + return res.body; +} + +/** + * Get a repository from Artifact Registry. + */ +export async function getRepository(repoPath: string): Promise { + const res = await client.get(repoPath); + return res.body; +} + +/** + * Update an Artifact Registry repository. + */ +export async function updateRepository(repo: RepositoryInput): Promise { + const updateMask = proto.fieldMasks(repo, "cleanupPolicies", "cleanupPolicyDryRun", "labels"); + if (updateMask.length === 0) { + const res = await client.get(repo.name!); + return res.body; + } + const res = await client.patch(`/${repo.name}`, repo, { + queryParams: { updateMask: updateMask.join(",") }, + }); + return res.body; +} diff --git a/src/gcp/auth.spec.ts b/src/gcp/auth.spec.ts new file mode 100644 index 00000000000..7998ea18680 --- /dev/null +++ b/src/gcp/auth.spec.ts @@ -0,0 +1,361 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import * as auth from "./auth"; +import { identityOrigin } from "../api"; + +const PROJECT_ID = "test-project"; + +describe("auth", () => { + afterEach(() => { + nock.cleanAll(); + }); + + describe("getAuthDomains", () => { + it("should resolve with auth domains on success", async () => { + const authDomains = ["domain1.com", "domain2.com"]; + nock(identityOrigin()) + .get(`/admin/v2/projects/${PROJECT_ID}/config`) + .reply(200, { authorizedDomains: authDomains }); + + const result = await auth.getAuthDomains(PROJECT_ID); + + expect(result).to.deep.equal(authDomains); + expect(nock.isDone()).to.be.true; + }); + + it("should reject if the API call fails", async () => { + nock(identityOrigin()) + .get(`/admin/v2/projects/${PROJECT_ID}/config`) + .reply(404, { error: { message: "Not Found" } }); + + await expect(auth.getAuthDomains(PROJECT_ID)).to.be.rejectedWith("Not Found"); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("updateAuthDomains", () => { + const authDomains = ["domain1.com", "domain2.com"]; + it("should resolve with auth domains on success", async () => { + nock(identityOrigin()) + .patch(`/admin/v2/projects/${PROJECT_ID}/config?update_mask=authorizedDomains`, { + authorizedDomains: authDomains, + }) + .reply(200, { authorizedDomains: authDomains }); + + const result = await auth.updateAuthDomains(PROJECT_ID, authDomains); + + expect(result).to.deep.equal(authDomains); + expect(nock.isDone()).to.be.true; + }); + + it("should reject if the API call fails", async () => { + nock(identityOrigin()) + .patch(`/admin/v2/projects/${PROJECT_ID}/config?update_mask=authorizedDomains`, { + authorizedDomains: authDomains, + }) + .reply(404, { error: { message: "Not Found" } }); + + await expect(auth.updateAuthDomains(PROJECT_ID, authDomains)).to.be.rejectedWith("Not Found"); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("findUser", () => { + const userInfo = { localId: "test-uid", email: "test@test.com" }; + const expectedUserInfo = { uid: "test-uid", email: "test@test.com" }; + + it("should resolve with user info on success (email)", async () => { + nock(identityOrigin()) + .post(`/v1/projects/${PROJECT_ID}/accounts:query`, { + expression: [{ email: "test@test.com" }], + limit: "1", + }) + .reply(200, { userInfo: [userInfo] }); + + const result = await auth.findUser(PROJECT_ID, "test@test.com"); + + expect(result).to.deep.equal(expectedUserInfo); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with user info on success (phone)", async () => { + nock(identityOrigin()) + .post(`/v1/projects/${PROJECT_ID}/accounts:query`, { + expression: [{ phoneNumber: "+11234567890" }], + limit: "1", + }) + .reply(200, { userInfo: [userInfo] }); + + const result = await auth.findUser(PROJECT_ID, undefined, "+11234567890"); + + expect(result).to.deep.equal(expectedUserInfo); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with user info on success (uid)", async () => { + nock(identityOrigin()) + .post(`/v1/projects/${PROJECT_ID}/accounts:query`, { + expression: [{ userId: "test-uid" }], + limit: "1", + }) + .reply(200, { userInfo: [userInfo] }); + + const result = await auth.findUser(PROJECT_ID, undefined, undefined, "test-uid"); + + expect(result).to.deep.equal(expectedUserInfo); + expect(nock.isDone()).to.be.true; + }); + + it("should reject if no user is found", async () => { + nock(identityOrigin()) + .post(`/v1/projects/${PROJECT_ID}/accounts:query`, { + expression: [{ email: "test@test.com" }], + limit: "1", + }) + .reply(200, {}); + + await expect(auth.findUser(PROJECT_ID, "test@test.com")).to.be.rejectedWith("No users found"); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("listUsers", () => { + const userInfo1 = { localId: "test-uid1", email: "test1@test.com" }; + const userInfo2 = { localId: "test-uid2", email: "test2@test.com" }; + const expectedUserInfo1 = { uid: "test-uid1", email: "test1@test.com" }; + const expectedUserInfo2 = { uid: "test-uid2", email: "test2@test.com" }; + + it("should resolve with a list of users on success", async () => { + nock(identityOrigin()) + .post(`/v1/projects/${PROJECT_ID}/accounts:query`, { + offset: "0", + limit: "2", + }) + .reply(200, { + recordsCount: "2", + userInfo: [userInfo1, userInfo2], + }); + + const result = await auth.listUsers(PROJECT_ID, 2); + + expect(result).to.deep.equal([expectedUserInfo1, expectedUserInfo2]); + expect(nock.isDone()).to.be.true; + }); + + it("should handle pagination correctly", async () => { + nock(identityOrigin()) + .post(`/v1/projects/${PROJECT_ID}/accounts:query`, { + offset: "0", + limit: "500", + }) + .reply(200, { + recordsCount: "1", + userInfo: [userInfo1], + }); + + nock(identityOrigin()) + .post(`/v1/projects/${PROJECT_ID}/accounts:query`, { + offset: "1", + limit: "500", + }) + .reply(200, { + recordsCount: "1", + userInfo: [userInfo2], + }); + + nock(identityOrigin()) + .post(`/v1/projects/${PROJECT_ID}/accounts:query`, { + offset: "2", + limit: "500", + }) + .reply(200, { + recordsCount: "0", + }); + + const result = await auth.listUsers(PROJECT_ID, 1000); + + expect(result).to.deep.equal([expectedUserInfo1, expectedUserInfo2]); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("disableUser", () => { + it("should resolve with true on success", async () => { + nock(identityOrigin()) + .post("/v1/accounts:update", { + disableUser: true, + targetProjectId: PROJECT_ID, + localId: "test-uid", + }) + .reply(200, {}); + + const result = await auth.toggleUserEnablement(PROJECT_ID, "test-uid", true); + + expect(result).to.be.true; + expect(nock.isDone()).to.be.true; + }); + + it("should reject if the API call fails", async () => { + nock(identityOrigin()) + .post("/v1/accounts:update", { + disableUser: true, + targetProjectId: PROJECT_ID, + localId: "test-uid", + }) + .reply(404, { error: { message: "Not Found" } }); + + await expect(auth.toggleUserEnablement(PROJECT_ID, "test-uid", true)).to.be.rejectedWith( + "Not Found", + ); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("setCustomClaim", () => { + const uid = "test-uid"; + const claim = { admin: true }; + const userInfo = { + localId: uid, + email: "test@test.com", + customAttributes: "", + }; + const updatedUserInfo = { + uid: uid, + email: "test@test.com", + customAttributes: JSON.stringify(claim), + }; + + it("should resolve with updated user info on success (overwrite)", async () => { + nock(identityOrigin()) + .post(`/v1/projects/${PROJECT_ID}/accounts:query`) + .reply(200, { userInfo: [userInfo] }); + + nock(identityOrigin()) + .post("/v1/accounts:update", { + customAttributes: JSON.stringify(claim), + targetProjectId: PROJECT_ID, + localId: uid, + }) + .reply(200, {}); + + nock(identityOrigin()) + .post(`/v1/projects/${PROJECT_ID}/accounts:query`) + .reply(200, { userInfo: [{ ...userInfo, uid, customAttributes: JSON.stringify(claim) }] }); + + const result = await auth.setCustomClaim(PROJECT_ID, uid, claim); + + expect(result).to.deep.equal(updatedUserInfo); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with updated user info on success (merge)", async () => { + const existingClaim = { role: "user" }; + const mergedClaim = { ...existingClaim, ...claim }; + const userInfoWithClaim = { ...userInfo, customAttributes: JSON.stringify(existingClaim) }; + const updatedUserInfoWithMergedClaim = { + uid: uid, + email: "test@test.com", + customAttributes: JSON.stringify(mergedClaim), + }; + + nock(identityOrigin()) + .post(`/v1/projects/${PROJECT_ID}/accounts:query`) + .reply(200, { userInfo: [userInfoWithClaim] }); + + nock(identityOrigin()) + .post("/v1/accounts:update", { + customAttributes: JSON.stringify(mergedClaim), + targetProjectId: PROJECT_ID, + localId: uid, + }) + .reply(200, {}); + + nock(identityOrigin()) + .post(`/v1/projects/${PROJECT_ID}/accounts:query`) + .reply(200, { + userInfo: [{ ...userInfo, uid, customAttributes: JSON.stringify(mergedClaim) }], + }); + + const result = await auth.setCustomClaim(PROJECT_ID, uid, claim, { + merge: true, + }); + + expect(result).to.deep.equal(updatedUserInfoWithMergedClaim); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("setAllowSmsRegionPolicy", () => { + const countryCodes = ["US", "CA"]; + it("should resolve with true on success", async () => { + nock(identityOrigin()) + .patch(`/admin/v2/projects/${PROJECT_ID}/config?updateMask=sms_region_config`, { + sms_region_config: { + allowlist_only: { + allowed_regions: countryCodes, + }, + }, + }) + .reply(200, {}); + + const result = await auth.setAllowSmsRegionPolicy(PROJECT_ID, countryCodes); + + expect(result).to.be.true; + expect(nock.isDone()).to.be.true; + }); + + it("should reject if the API call fails", async () => { + nock(identityOrigin()) + .patch(`/admin/v2/projects/${PROJECT_ID}/config?updateMask=sms_region_config`, { + sms_region_config: { + allowlist_only: { + allowed_regions: countryCodes, + }, + }, + }) + .reply(400, { error: { message: "Bad Request" } }); + + await expect(auth.setAllowSmsRegionPolicy(PROJECT_ID, countryCodes)).to.be.rejectedWith( + "Bad Request", + ); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("setDenySmsRegionPolicy", () => { + const countryCodes = ["US", "CA"]; + it("should resolve with true on success", async () => { + nock(identityOrigin()) + .patch(`/admin/v2/projects/${PROJECT_ID}/config?updateMask=sms_region_config`, { + sms_region_config: { + allow_by_default: { + disallowed_regions: countryCodes, + }, + }, + }) + .reply(200, {}); + + const result = await auth.setDenySmsRegionPolicy(PROJECT_ID, countryCodes); + + expect(result).to.be.true; + expect(nock.isDone()).to.be.true; + }); + + it("should reject if the API call fails", async () => { + nock(identityOrigin()) + .patch(`/admin/v2/projects/${PROJECT_ID}/config?updateMask=sms_region_config`, { + sms_region_config: { + allow_by_default: { + disallowed_regions: countryCodes, + }, + }, + }) + .reply(400, { error: { message: "Bad Request" } }); + + await expect(auth.setDenySmsRegionPolicy(PROJECT_ID, countryCodes)).to.be.rejectedWith( + "Bad Request", + ); + expect(nock.isDone()).to.be.true; + }); + }); +}); diff --git a/src/gcp/auth.ts b/src/gcp/auth.ts index bd20fa84ad5..6a75de057a1 100644 --- a/src/gcp/auth.ts +++ b/src/gcp/auth.ts @@ -1,7 +1,76 @@ import { Client } from "../apiv2"; import { identityOrigin } from "../api"; -const apiClient = new Client({ urlPrefix: identityOrigin, auth: true }); +interface MfaEnrollment { + mfaEnrollmentId: string; + displayName: string; + enrolledAt: string; + phoneInfo?: string; + emailInfo?: { + emailAddress: string; + }; + unobfuscatedPhoneInfo?: string; +} + +export interface UserInfo { + uid?: string; + localId?: string; + email: string; + displayName: string; + language: string; + photoUrl: string; + timeZone: string; + dateOfBirth: string; + passwordHash: string; + salt: string; + version: number; + emailVerified: boolean; + passwordUpdatedAt: number; + providerUserInfo: { + providerId: string; + displayName: string; + photoUrl: string; + federatedId: string; + email: string; + rawId: string; + screenName: string; + phoneNumber: string; + }[]; + validSince: string; + disabled: boolean; + lastLoginAt: string; + createdAt: string; + screenName: string; + customAuth: boolean; + phoneNumber: string; + customAttributes?: string; + emailLinkSignin: boolean; + tenantId: string; + mfaInfo: MfaEnrollment[]; + initialEmail: string; + lastRefreshAt: string; +} + +interface SetAccountInfoResponse { + localId: string; + idToken: string; + providerUserInfo: { + providerId: string; + displayName: string; + photoUrl: string; + federatedId: string; + email: string; + rawId: string; + screenName: string; + phoneNumber: string; + }[]; + newEmail: string; + refreshToken: string; + expiresIn: string; + emailVerified: boolean; +} + +const apiClient = new Client({ urlPrefix: identityOrigin(), auth: true }); /** * Returns the list of authorized domains. @@ -10,7 +79,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 +98,214 @@ 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; } + +/** + * findUser searches for an auth user in a project. + * @param project project identifier. + * @param email the users email to lookup. + * @param phone the users phone number to lookup. + * @param uid the users id to lookup. + * @return an array of user info + */ +export async function findUser( + project: string, + email?: string, + phone?: string, + uid?: string, +): Promise { + const expression: { email?: string; phoneNumber?: string; userId?: string } = { + email, + phoneNumber: phone, + userId: uid, + }; + const res = await apiClient.post< + { + limit: string; + expression: { email?: string; phoneNumber?: string; userId?: string }[]; + }, + { + recordsCount: string; + userInfo: UserInfo[]; + } + >(`/v1/projects/${project}/accounts:query`, { + expression: [expression], + limit: "1", + }); + if (!res.body.userInfo?.length) { + throw new Error("No users found"); + } + const modifiedUserInfo = res.body.userInfo.map((ui) => { + ui.uid = ui.localId; + delete ui.localId; + return ui; + }); + return modifiedUserInfo[0]; +} + +/** + * listUsers returns all auth users in a project. + * @param project project identifier. + * @param limit the total number of users to return. + * @return an array of users info + */ +export async function listUsers(project: string, limit: number): Promise { + let queryLimit = limit; + let offset = 0; + if (limit > 500) { + queryLimit = 500; + } + const userInfo: UserInfo[] = []; + while (offset < limit) { + if (queryLimit + offset > limit) { + queryLimit = limit - offset; + } + const res = await apiClient.post< + { + limit: string; + offset: string; + }, + { + recordsCount: string; + userInfo: UserInfo[]; + } + >(`/v1/projects/${project}/accounts:query`, { + offset: offset.toString(), + limit: queryLimit.toString(), + }); + if (res.body.recordsCount === "0") { + break; + } + offset += Number(res.body.recordsCount); + const modifiedUserInfo = res.body.userInfo.map((ui) => { + ui.uid = ui.localId; + delete ui.localId; + return ui; + }); + userInfo.push(...modifiedUserInfo); + } + return userInfo; +} + +/** + * disableUser disables or enables a user from a particular project. + * @param project project identifier. + * @param uid the user id of the user from the firebase project. + * @param disabled sets whether the user is marked as disabled (true) or enabled (false). + * @return the call succeeded (true). + */ +export async function toggleUserEnablement( + project: string, + uid: string, + disabled: boolean, +): Promise { + const res = await apiClient.post< + { disableUser: boolean; targetProjectId: string; localId: string }, + SetAccountInfoResponse + >("/v1/accounts:update", { + disableUser: disabled, + targetProjectId: project, + localId: uid, + }); + return res.status === 200; +} + +/** + * setCustomClaim sets a new custom claim on the uid specified in the project. + * @param project project identifier. + * @param uid the user id of the user from the firebase project. + * @param claim the key value in the custom claim. + * @param options modifiers to setting custom claims + * @param options.merge whether to preserve the existing custom claims on the user + * @return the results of the accounts update request. + */ +export async function setCustomClaim( + project: string, + uid: string, + claim: Record, + options?: { merge?: boolean }, +): Promise { + let user = await findUser(project, undefined, undefined, uid); + if (user.uid !== uid) { + throw new Error(`Could not find ${uid} in the auth db, please check the uid again.`); + } + let reqClaim = JSON.stringify(claim); + if (options?.merge) { + let attributeJson = new Map(); + if (user.customAttributes !== undefined && user.customAttributes !== "") { + attributeJson = JSON.parse(user.customAttributes) as Map; + } + reqClaim = JSON.stringify({ ...attributeJson, ...claim }); + } + const res = await apiClient.post< + { customAttributes: string; targetProjectId: string; localId: string }, + SetAccountInfoResponse + >("/v1/accounts:update", { + customAttributes: reqClaim, + targetProjectId: project, + localId: uid, + }); + if (res.status !== 200) { + throw new Error("something went wrong in the request"); + } + user = await findUser(project, undefined, undefined, uid); + return user; +} + +/** + * setAllowSmsRegionPolicy updates the allowed regions for sms auth and MFA in Firebase. + * @param project project identifier. + * @param countryCodes the country codes to allow based on ISO 3166. + * @return call success. + */ +export async function setAllowSmsRegionPolicy( + project: string, + countryCodes: string[], +): Promise { + const res = await apiClient.patch< + { sms_region_config: { allowlist_only: { allowed_regions: string[] } } }, + {} + >(`/admin/v2/projects/${project}/config?updateMask=sms_region_config`, { + sms_region_config: { + allowlist_only: { + allowed_regions: countryCodes, + }, + }, + }); + if (res.status !== 200) { + throw new Error("SMS Region Policy failed to be configured"); + } + return true; +} + +/** + * setDenySmsRegionPolicy updates the deny regions for sms auth and MFA in Firebase. + * @param project project identifier. + * @param countryCodes the country codes to allow based on ISO 3166. + * @return call success. + */ +export async function setDenySmsRegionPolicy( + project: string, + countryCodes: string[], +): Promise { + const res = await apiClient.patch< + { sms_region_config: { allow_by_default: { disallowed_regions: string[] } } }, + {} + >(`/admin/v2/projects/${project}/config?updateMask=sms_region_config`, { + sms_region_config: { + allow_by_default: { + disallowed_regions: countryCodes, + }, + }, + }); + if (res.status !== 200) { + throw new Error("SMS Region Policy failed to be configured"); + } + return true; +} diff --git a/src/gcp/cloudbilling.js b/src/gcp/cloudbilling.js deleted file mode 100644 index 6cddac04277..00000000000 --- a/src/gcp/cloudbilling.js +++ /dev/null @@ -1,65 +0,0 @@ -"use strict"; - -var api = require("../api"); -var utils = require("../utils"); - -var API_VERSION = "v1"; - -/** - * Returns whether or not project has billing enabled. - * @param {string} projectId - * @return {!Promise} - */ -function _checkBillingEnabled(projectId) { - return api - .request("GET", utils.endpoint([API_VERSION, "projects", projectId, "billingInfo"]), { - auth: true, - origin: api.cloudbillingOrigin, - retryCodes: [500, 503], - }) - .then(function (response) { - return response.body.billingEnabled; - }); -} - -/** - * Sets billing account for project and returns whether or not action was successful. - * @param {string} projectId - * @return {!Promise} - */ -function _setBillingAccount(projectId, billingAccount) { - return api - .request("PUT", utils.endpoint([API_VERSION, "projects", projectId, "billingInfo"]), { - auth: true, - origin: api.cloudbillingOrigin, - retryCodes: [500, 503], - data: { - billingAccountName: billingAccount, - }, - }) - .then(function (response) { - return response.body.billingEnabled; - }); -} - -/** - * Lists the billing accounts that the current authenticated user has permission to view. - * @return {!Promise} - */ -function _listBillingAccounts() { - return api - .request("GET", utils.endpoint([API_VERSION, "billingAccounts"]), { - auth: true, - origin: api.cloudbillingOrigin, - retryCodes: [500, 503], - }) - .then(function (response) { - return response.body.billingAccounts || []; - }); -} - -module.exports = { - checkBillingEnabled: _checkBillingEnabled, - listBillingAccounts: _listBillingAccounts, - setBillingAccount: _setBillingAccount, -}; diff --git a/src/gcp/cloudbilling.spec.ts b/src/gcp/cloudbilling.spec.ts new file mode 100644 index 00000000000..a1272e6e0a6 --- /dev/null +++ b/src/gcp/cloudbilling.spec.ts @@ -0,0 +1,154 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import * as cloudbilling from "./cloudbilling"; +import { cloudbillingOrigin } from "../api"; +import { Setup } from "../init"; + +const PROJECT_ID = "test-project"; + +describe("cloudbilling", () => { + afterEach(() => { + nock.cleanAll(); + }); + + describe("checkBillingEnabled", () => { + it("should resolve with true if billing is enabled", async () => { + nock(cloudbillingOrigin()) + .get(`/v1/projects/${PROJECT_ID}/billingInfo`) + .reply(200, { billingEnabled: true }); + + const result = await cloudbilling.checkBillingEnabled(PROJECT_ID); + + expect(result).to.be.true; + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with false if billing is not enabled", async () => { + nock(cloudbillingOrigin()) + .get(`/v1/projects/${PROJECT_ID}/billingInfo`) + .reply(200, { billingEnabled: false }); + + const result = await cloudbilling.checkBillingEnabled(PROJECT_ID); + + expect(result).to.be.false; + expect(nock.isDone()).to.be.true; + }); + + it("should reject if the API call fails", async () => { + nock(cloudbillingOrigin()) + .get(`/v1/projects/${PROJECT_ID}/billingInfo`) + .reply(404, { error: { message: "Not Found" } }); + + await expect(cloudbilling.checkBillingEnabled(PROJECT_ID)).to.be.rejectedWith("Not Found"); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("isBillingEnabled", () => { + it("should return the cached value if it exists", async () => { + const setup: Setup = { + isBillingEnabled: true, + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + instructions: [], + }; + const result = await cloudbilling.isBillingEnabled(setup); + expect(result).to.be.true; + }); + + it("should return false if projectId is not set", async () => { + const setup: Setup = { + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + instructions: [], + }; + const result = await cloudbilling.isBillingEnabled(setup); + expect(result).to.be.false; + }); + + it("should call checkBillingEnabled if cache is empty", async () => { + const setup: Setup = { + projectId: PROJECT_ID, + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + instructions: [], + }; + nock(cloudbillingOrigin()) + .get(`/v1/projects/${PROJECT_ID}/billingInfo`) + .reply(200, { billingEnabled: true }); + + const result = await cloudbilling.isBillingEnabled(setup); + + expect(result).to.be.true; + expect(setup.isBillingEnabled).to.be.true; + expect(nock.isDone()).to.be.true; + }); + }); + + describe("setBillingAccount", () => { + const billingAccountName = "billingAccounts/test-billing-account"; + it("should resolve with true on success", async () => { + nock(cloudbillingOrigin()) + .put(`/v1/projects/${PROJECT_ID}/billingInfo`, { + billingAccountName: billingAccountName, + }) + .reply(200, { billingEnabled: true }); + + const result = await cloudbilling.setBillingAccount(PROJECT_ID, billingAccountName); + + expect(result).to.be.true; + expect(nock.isDone()).to.be.true; + }); + + it("should reject if the API call fails", async () => { + nock(cloudbillingOrigin()) + .put(`/v1/projects/${PROJECT_ID}/billingInfo`, { + billingAccountName: billingAccountName, + }) + .reply(403, { error: { message: "Permission Denied" } }); + + await expect( + cloudbilling.setBillingAccount(PROJECT_ID, billingAccountName), + ).to.be.rejectedWith("Permission Denied"); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("listBillingAccounts", () => { + const billingAccount = { + name: "billingAccounts/test-billing-account", + open: "true", + displayName: "Test Billing Account", + masterBillingAccount: "", + }; + + it("should resolve with a list of billing accounts on success", async () => { + nock(cloudbillingOrigin()) + .get("/v1/billingAccounts") + .reply(200, { billingAccounts: [billingAccount] }); + + const result = await cloudbilling.listBillingAccounts(); + + expect(result).to.deep.equal([billingAccount]); + expect(nock.isDone()).to.be.true; + }); + + it("should resolve with an empty list if no billing accounts are returned", async () => { + nock(cloudbillingOrigin()).get("/v1/billingAccounts").reply(200, {}); + + const result = await cloudbilling.listBillingAccounts(); + + expect(result).to.deep.equal([]); + expect(nock.isDone()).to.be.true; + }); + + it("should reject if the API call fails", async () => { + nock(cloudbillingOrigin()) + .get("/v1/billingAccounts") + .reply(404, { error: { message: "Not Found" } }); + + await expect(cloudbilling.listBillingAccounts()).to.be.rejectedWith("Not Found"); + expect(nock.isDone()).to.be.true; + }); + }); +}); diff --git a/src/gcp/cloudbilling.ts b/src/gcp/cloudbilling.ts new file mode 100644 index 00000000000..e80b2a01064 --- /dev/null +++ b/src/gcp/cloudbilling.ts @@ -0,0 +1,73 @@ +import { cloudbillingOrigin } from "../api"; +import { Client } from "../apiv2"; +import { Setup } from "../init"; +import * as utils from "../utils"; + +const API_VERSION = "v1"; +const client = new Client({ urlPrefix: cloudbillingOrigin(), apiVersion: API_VERSION }); + +export interface BillingAccount { + name: string; + open: string; + displayName: string; + masterBillingAccount: string; +} + +/** + * Returns whether or not project has billing enabled. + * Cache the result in the init Setup metadata. + * @param setup + */ +export async function isBillingEnabled(setup: Setup): Promise { + if (setup.isBillingEnabled !== undefined) { + return setup.isBillingEnabled; + } + if (!setup.projectId) { + return false; + } + setup.isBillingEnabled = await checkBillingEnabled(setup.projectId); + return setup.isBillingEnabled; +} + +/** + * Returns whether or not project has billing enabled. + * @param projectId + */ +export async function checkBillingEnabled(projectId: string): Promise { + const res = await client.get<{ billingEnabled: boolean }>( + utils.endpoint(["projects", projectId, "billingInfo"]), + { retryCodes: [500, 503] }, + ); + return res.body.billingEnabled; +} + +/** + * Sets billing account for project and returns whether or not action was successful. + * @param {string} projectId + * @return {!Promise} + */ +export async function setBillingAccount( + projectId: string, + billingAccountName: string, +): Promise { + const res = await client.put<{ billingAccountName: string }, { billingEnabled: boolean }>( + utils.endpoint(["projects", projectId, "billingInfo"]), + { + billingAccountName: billingAccountName, + }, + { retryCodes: [500, 503] }, + ); + return res.body.billingEnabled; +} + +/** + * Lists the billing accounts that the current authenticated user has permission to view. + * @return {!Promise} + */ +export async function listBillingAccounts(): Promise { + const res = await client.get<{ billingAccounts: BillingAccount[] }>( + utils.endpoint(["billingAccounts"]), + { retryCodes: [500, 503] }, + ); + return res.body.billingAccounts || []; +} diff --git a/src/gcp/cloudbuild.spec.ts b/src/gcp/cloudbuild.spec.ts new file mode 100644 index 00000000000..0af01d5ef52 --- /dev/null +++ b/src/gcp/cloudbuild.spec.ts @@ -0,0 +1,172 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import * as cloudbuild from "./cloudbuild"; +import { cloudbuildOrigin } from "../api"; + +const PROJECT_ID = "test-project"; +const LOCATION = "us-central1"; + +describe("cloudbuild", () => { + const CONNECTION_ID = "test-connection"; + const REPO_ID = "test-repo"; + const OP_NAME = "operations/test-op"; + + afterEach(() => { + nock.cleanAll(); + }); + + describe("createConnection", () => { + it("should resolve with an operation on success", async () => { + const operation = { name: OP_NAME, done: false }; + nock(cloudbuildOrigin()) + .post( + `/v2/projects/${PROJECT_ID}/locations/${LOCATION}/connections?connectionId=${CONNECTION_ID}`, + ) + .reply(200, operation); + + const result = await cloudbuild.createConnection(PROJECT_ID, LOCATION, CONNECTION_ID); + + expect(result).to.deep.equal(operation); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("getConnection", () => { + it("should resolve with a connection on success", async () => { + const connection = { name: "test-connection" }; + nock(cloudbuildOrigin()) + .get(`/v2/projects/${PROJECT_ID}/locations/${LOCATION}/connections/${CONNECTION_ID}`) + .reply(200, connection); + + const result = await cloudbuild.getConnection(PROJECT_ID, LOCATION, CONNECTION_ID); + + expect(result).to.deep.equal(connection); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("listConnections", () => { + it("should resolve with a list of connections on success", async () => { + const connections = [{ name: "test-connection" }]; + nock(cloudbuildOrigin()) + .get(`/v2/projects/${PROJECT_ID}/locations/${LOCATION}/connections`) + .query({ pageSize: 100, pageToken: "" }) + .reply(200, { connections: connections }); + + const result = await cloudbuild.listConnections(PROJECT_ID, LOCATION); + + expect(result).to.deep.equal(connections); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("deleteConnection", () => { + it("should resolve with an operation on success", async () => { + const operation = { name: OP_NAME, done: false }; + nock(cloudbuildOrigin()) + .delete(`/v2/projects/${PROJECT_ID}/locations/${LOCATION}/connections/${CONNECTION_ID}`) + .reply(200, operation); + + const result = await cloudbuild.deleteConnection(PROJECT_ID, LOCATION, CONNECTION_ID); + + expect(result).to.deep.equal(operation); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("createRepository", () => { + it("should resolve with an operation on success", async () => { + const operation = { name: OP_NAME, done: false }; + nock(cloudbuildOrigin()) + .post( + `/v2/projects/${PROJECT_ID}/locations/${LOCATION}/connections/${CONNECTION_ID}/repositories?repositoryId=${REPO_ID}`, + ) + .reply(200, operation); + + const result = await cloudbuild.createRepository( + PROJECT_ID, + LOCATION, + CONNECTION_ID, + REPO_ID, + "https://github.com/test/repo", + ); + + expect(result).to.deep.equal(operation); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("getRepository", () => { + it("should resolve with a repository on success", async () => { + const repository = { name: "test-repo" }; + nock(cloudbuildOrigin()) + .get( + `/v2/projects/${PROJECT_ID}/locations/${LOCATION}/connections/${CONNECTION_ID}/repositories/${REPO_ID}`, + ) + .reply(200, repository); + + const result = await cloudbuild.getRepository(PROJECT_ID, LOCATION, CONNECTION_ID, REPO_ID); + + expect(result).to.deep.equal(repository); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("deleteRepository", () => { + it("should resolve with an operation on success", async () => { + const operation = { name: OP_NAME, done: false }; + nock(cloudbuildOrigin()) + .delete( + `/v2/projects/${PROJECT_ID}/locations/${LOCATION}/connections/${CONNECTION_ID}/repositories/${REPO_ID}`, + ) + .reply(200, operation); + + const result = await cloudbuild.deleteRepository( + PROJECT_ID, + LOCATION, + CONNECTION_ID, + REPO_ID, + ); + + expect(result).to.deep.equal(operation); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("fetchLinkableRepositories", () => { + it("should resolve with a list of linkable repositories on success", async () => { + const repositories = { repositories: [{ name: "test-repo" }] }; + nock(cloudbuildOrigin()) + .get( + `/v2/projects/${PROJECT_ID}/locations/${LOCATION}/connections/${CONNECTION_ID}:fetchLinkableRepositories`, + ) + .query({ pageSize: 1000, pageToken: "" }) + .reply(200, repositories); + + const result = await cloudbuild.fetchLinkableRepositories( + PROJECT_ID, + LOCATION, + CONNECTION_ID, + ); + + expect(result).to.deep.equal(repositories); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("getDefaultServiceAccount", () => { + it("should return the default service account", () => { + const projectNumber = "123456789"; + const result = cloudbuild.getDefaultServiceAccount(projectNumber); + expect(result).to.equal("123456789@cloudbuild.gserviceaccount.com"); + }); + }); + + describe("getDefaultServiceAgent", () => { + it("should return the default service agent", () => { + const projectNumber = "123456789"; + const result = cloudbuild.getDefaultServiceAgent(projectNumber); + expect(result).to.equal("service-123456789@gcp-sa-cloudbuild.iam.gserviceaccount.com"); + }); + }); +}); 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..2e202aa99ae --- /dev/null +++ b/src/gcp/cloudfunctions.spec.ts @@ -0,0 +1,760 @@ +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"; + +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, + state: "ACTIVE", + }; + + 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" }, + }); + }); + + it("should expand shorthand service account", () => { + expect( + cloudfunctions.functionFromEndpoint( + { + ...ENDPOINT, + httpsTrigger: {}, + serviceAccount: "robot@", + }, + UPLOAD_URL, + ), + ).to.deep.equal({ + ...CLOUD_FUNCTION, + sourceUploadUrl: UPLOAD_URL, + httpsTrigger: {}, + serviceAccountEmail: `robot@${ENDPOINT.project}.iam.gserviceaccount.com`, + }); + }); + + it("should handle null service account", () => { + expect( + cloudfunctions.functionFromEndpoint( + { + ...ENDPOINT, + httpsTrigger: {}, + serviceAccount: null, + }, + UPLOAD_URL, + ), + ).to.deep.equal({ + ...CLOUD_FUNCTION, + sourceUploadUrl: UPLOAD_URL, + httpsTrigger: {}, + serviceAccountEmail: null, + }); + }); + }); + + 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; + }); + }); +}); diff --git a/src/gcp/cloudfunctions.ts b/src/gcp/cloudfunctions.ts index e8ef1a6d172..c3e5e3fd8fe 100644 --- a/src/gcp/cloudfunctions.ts +++ b/src/gcp/cloudfunctions.ts @@ -1,36 +1,170 @@ -import * as _ from "lodash"; -import * as clc from "cli-color"; +import * as clc from "colorette"; -import * as api from "../api"; import { FirebaseError } from "../error"; import { logger } from "../logger"; +import * as backend from "../deploy/functions/backend"; import * as utils from "../utils"; -import { CloudFunctionTrigger } from "../deploy/functions/deploymentPlanner"; +import * as proto from "./proto"; +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; type: string; - funcName: string; done: boolean; - eventType?: string; - trigger?: { - eventTrigger?: any; - httpsTrigger?: any; - }; error?: { code: number; message: string }; } -export const API_VERSION = "v1"; +export interface HttpsTrigger { + // output only + readonly url?: string; + securityLevel?: SecurityLevel; +} -export const DEFAULT_PUBLIC_POLICY = { - version: 3, - bindings: [ - { - role: "roles/cloudfunctions.invoker", - members: ["allUsers"], - }, - ], -}; +export interface EventTrigger { + eventType: string; + resource: string; + service?: string; + failurePolicy?: FailurePolicy; +} + +export interface CorsPolicy { + allowOrigin: string[]; + allowMethods?: string[]; + allowHeaders?: string[]; + exposeHeaders?: string[]; +} + +export interface SecretEnvVar { + key: string; + projectId: string; + secret: string; + version?: string; +} + +export interface SecretVolume { + mountPath: string; + projectId: string; + secret: string; + versions: { + version: string; + path: string; + }[]; +} + +export type CloudFunctionStatus = + | "ACTIVE" + | "OFFLINE" + | "DEPLOY_IN_PROGRESS" + | "DELETE_IN_PROGRESS" + | "UNKNOWN"; +export type SecurityLevel = "SECURE_ALWAYS" | "SECURE_OPTIONAL"; + +export interface FailurePolicy { + // oneof action + retry?: Record; + // 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; + + // oneof source_code + sourceArchiveUrl?: string; + sourceRepository?: { + url: string; + deployedUrl: string; + }; + sourceUploadUrl?: string; + // end oneof source_code + + // oneof trigger + httpsTrigger?: HttpsTrigger; + eventTrigger?: EventTrigger; + // end oneof trigger; + + entryPoint: string; + runtime: supported.Runtime; + // Default = 60s + timeout?: proto.Duration | null; + + // Default 256 + availableMemoryMb?: number | null; + + // Default @appspot.gserviceaccount.com + serviceAccountEmail?: string | null; + + labels?: Record; + environmentVariables?: Record | null; + buildEnvironmentVariables?: Record; + + network?: string | null; + maxInstances?: number | null; + minInstances?: number | null; + + corsPolicy?: CorsPolicy; + vpcConnector?: string | null; + vpcConnectorEgressSettings?: "PRIVATE_RANGES_ONLY" | "ALL_TRAFFIC" | null; + ingressSettings?: "ALLOW_ALL" | "ALLOW_INTERNAL_ONLY" | "ALLOW_INTERNAL_AND_GCLB" | null; + + 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. + sourceToken?: string; + + // Output parameters + status: CloudFunctionStatus; + buildId: string; + updateTime: Date; + versionId: number; +} + +export type OutputOnlyFields = "status" | "buildId" | "updateTime" | "versionId"; + +/** + * Returns the captured user-friendly message from a runtime validation error. + * @param errMessage Message from the runtime validation error. + */ +export function captureRuntimeValidationError(errMessage: string): string { + // Regex to capture the content of the 'message' field. + // The error messages will take this form: + // `Failed to create 1st Gen function projects/p/locations/l/functions/f: + // runtime: Runtime validation errors: [error_code: INVALID_RUNTIME\n + // message: \"Runtime \\\"nodejs22\\\" is not supported on GCF Gen1\"\n]` + const regex = /message: "((?:\\.|[^"\\])*)"/; + const match = errMessage.match(regex); + if (match && match[1]) { + // The captured string may still contain escaped quotes (e.g., \\"). + // This replaces them with a standard double quote. + const capturedMessage = match[1].replace(/\\"/g, '"'); + return capturedMessage; + } + return "invalid runtime detected, please see https://cloud.google.com/functions/docs/runtime-support for the latest supported runtimes"; +} /** * Logs an error from a failed function deployment. @@ -39,19 +173,27 @@ export const DEFAULT_PUBLIC_POLICY = { * @param err The error returned from the operation. */ function functionsOpLogReject(funcName: string, type: string, err: any): void { + // Sniff for runtime validation errors and log a more user-friendly warning. + if ((err?.message as string).includes("Runtime validation errors")) { + const capturedMessage = captureRuntimeValidationError(err.message); + utils.logWarning( + clc.bold(clc.yellow("functions:")) + " " + capturedMessage + " for function " + funcName, + ); + } 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 }, }); } @@ -64,20 +206,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; } @@ -85,65 +225,33 @@ export async function generateUploadUrl(projectId: string, location: string): Pr /** * Create a Cloud Function. - * @param options The function to deploy. + * @param cloudFunction The function to delete */ -export async function createFunction(options: any): Promise { - const location = "projects/" + options.projectId + "/locations/" + options.region; - const fullFuncName = location + "/functions/" + options.functionName; - const endpoint = "/" + API_VERSION + "/" + location + "/functions"; - - const data: CloudFunctionTrigger = { - sourceUploadUrl: options.sourceUploadUrl, - name: fullFuncName, - entryPoint: options.entryPoint, - labels: options.labels, - runtime: options.runtime, - environmentVariables: options.environmentVariables, +export async function createFunction( + 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 = `/${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: "", }; - if (options.vpcConnector) { - data.vpcConnector = options.vpcConnector; - // use implied project/location if only given connector id - if (!data.vpcConnector?.includes("/")) { - data.vpcConnector = `${location}/connectors/${data.vpcConnector}`; - } - } - if (options.vpcConnectorEgressSettings) { - data.vpcConnectorEgressSettings = options.vpcConnectorEgressSettings; - } - if (options.availableMemoryMb) { - data.availableMemoryMb = options.availableMemoryMb; - } - if (options.timeout) { - data.timeout = options.timeout; - } - if (options.maxInstances) { - data.maxInstances = Number(options.maxInstances); - } - if (options.serviceAccountEmail) { - data.serviceAccountEmail = options.serviceAccountEmail; - } - if (options.sourceToken) { - data.sourceToken = options.sourceToken; - } - if (options.ingressSettings) { - data.ingressSettings = options.ingressSettings; - } try { - const res = await api.request("POST", endpoint, { - auth: true, - data: _.assign(data, options.trigger), - origin: api.functionsOrigin, - }); + const res = await client.post, CloudFunction>( + endpoint, + cloudFunction, + ); return { name: res.body.name, type: "create", - funcName: fullFuncName, - eventType: options.eventType, done: false, }; - } catch (err) { - throw functionsOpLogReject(options.functionName, "create", err); + } catch (err: any) { + throw functionsOpLogReject(cloudFunction.name, "create", err); } } @@ -153,124 +261,166 @@ export async function createFunction(options: any): Promise { */ interface IamOptions { name: string; - policy: any; // TODO: Type this? + policy: iam.Policy; } /** * Sets the IAM policy of a Google Cloud Function. * @param options The Iam options to set. */ -export async function setIamPolicy(options: IamOptions) { - const endpoint = `/${API_VERSION}/${options.name}:setIamPolicy`; +export async function setIamPolicy(options: IamOptions): Promise { + 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, }); } } +// Response body policy - https://cloud.google.com/functions/docs/reference/rest/v1/Policy +interface GetIamPolicy { + bindings?: iam.Binding[]; + version?: number; + etag?: string; +} + /** - * Updates a Cloud Function. - * @param options The Cloud Function to update. + * Gets the IAM policy of a Google Cloud Function. + * @param fnName The full name and path of the Cloud Function. */ -export async function updateFunction(options: any): Promise { - const location = "projects/" + options.projectId + "/locations/" + options.region; - const fullFuncName = location + "/functions/" + options.functionName; - const endpoint = "/" + API_VERSION + "/" + fullFuncName; - - const data: CloudFunctionTrigger = _.assign( - { - sourceUploadUrl: options.sourceUploadUrl, - name: fullFuncName, - labels: options.labels, - }, - options.trigger - ); - let masks = ["sourceUploadUrl", "name", "labels"]; +export async function getIamPolicy(fnName: string): Promise { + const endpoint = `/${fnName}:getIamPolicy`; - if (options.vpcConnector) { - data.vpcConnector = options.vpcConnector; - // use implied project/location if only given connector id - if (!data.vpcConnector?.includes("/")) { - data.vpcConnector = `${location}/connectors/${data.vpcConnector}`; - } - masks.push("vpcConnector"); - } - if (options.vpcConnectorEgressSettings) { - data.vpcConnectorEgressSettings = options.vpcConnectorEgressSettings; - masks.push("vpcConnectorEgressSettings"); - } - if (options.runtime) { - data.runtime = options.runtime; - masks = _.concat(masks, "runtime"); - } - if (options.availableMemoryMb) { - data.availableMemoryMb = options.availableMemoryMb; - masks.push("availableMemoryMb"); - } - if (options.timeout) { - data.timeout = options.timeout; - masks.push("timeout"); - } - if (options.maxInstances) { - data.maxInstances = Number(options.maxInstances); - masks.push("maxInstances"); - } - if (options.environmentVariables) { - data.environmentVariables = options.environmentVariables; - masks.push("environmentVariables"); - } - if (options.serviceAccountEmail) { - data.serviceAccountEmail = options.serviceAccountEmail; - masks.push("serviceAccountEmail"); + try { + 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, + }); } - if (options.sourceToken) { - data.sourceToken = options.sourceToken; - masks.push("sourceToken"); +} + +/** + * Sets the invoker IAM policy for the function on function create + * @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[], +): Promise { + if (invoker.length === 0) { + throw new FirebaseError("Invoker cannot be an empty array"); } - if (options.ingressSettings) { - data.ingressSettings = options.ingressSettings; - masks.push("ingressSettings"); + const invokerMembers = proto.getInvokerMembers(invoker, projectId); + const invokerRole = "roles/cloudfunctions.invoker"; + const bindings = [{ role: invokerRole, members: invokerMembers }]; + + const policy: iam.Policy = { + bindings: bindings, + etag: "", + version: 3, + }; + await setIamPolicy({ name: fnName, policy: policy }); +} + +/** + * Gets the current IAM policy on function update, + * overrides the current invoker role with the supplied invoker members + * @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[], +): Promise { + if (invoker.length === 0) { + throw new FirebaseError("Invoker cannot be an empty array"); } - if (options.trigger.eventTrigger) { - masks = _.concat( - masks, - _.map(_.keys(options.trigger.eventTrigger), (subkey) => { - return "eventTrigger." + subkey; - }) - ); - } else { - masks = _.concat(masks, "httpsTrigger"); + 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, + ); + if ( + currentInvokerBinding && + JSON.stringify(currentInvokerBinding.members.sort()) === JSON.stringify(invokerMembers.sort()) + ) { + return; } + const bindings = (currentPolicy.bindings || []).filter((binding) => binding.role !== invokerRole); + bindings.push({ + role: invokerRole, + members: invokerMembers, + }); + + const policy: iam.Policy = { + bindings: bindings, + etag: currentPolicy.etag || "", + version: 3, + }; + await setIamPolicy({ name: fnName, policy: policy }); +} + +/** + * Updates a Cloud Function. + * @param cloudFunction The Cloud Function to update. + */ +export async function updateFunction( + cloudFunction: Omit, +): Promise { + const endpoint = `/${cloudFunction.name}`; + 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: "", + }; + // 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", + "secretEnvironmentVariables", + "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 res = await api.request("PATCH", endpoint, { - qs: { - updateMask: masks.join(","), + const res = await client.patch, CloudFunction>( + endpoint, + cloudFunction, + { + queryParams: { + updateMask: fieldMasks.join(","), + }, }, - auth: true, - data: data, - origin: api.functionsOrigin, - }); + ); return { - funcName: fullFuncName, - eventType: options.eventType, done: false, name: res.body.name, type: "update", }; - } catch (err) { - throw functionsOpLogReject(options.functionName, "update", err); + } catch (err: any) { + throw functionsOpLogReject(cloudFunction.name, "update", err); } } @@ -278,57 +428,46 @@ export async function updateFunction(options: any): Promise { * Delete a Cloud Function. * @param options the Cloud Function to delete. */ -export async function deleteFunction(options: any): Promise { - const endpoint = "/" + API_VERSION + "/" + options.functionName; +export async function deleteFunction(name: string): Promise { + const endpoint = `/${name}`; try { - const res = await api.request("DELETE", endpoint, { - auth: true, - origin: api.functionsOrigin, - }); + const res = await client.delete(endpoint); return { - funcName: options.funcName, - eventType: options.eventType, done: false, name: res.body.name, type: "delete", }; - } catch (err) { - throw functionsOpLogReject(options.functionName, "delete", err); + } catch (err: any) { + throw functionsOpLogReject(name, "delete", err); } } -/** - * List all existing Cloud Functions in a project and region. - * @param projectId the Id of the project to check. - * @param region the region to check in. - */ -export async function listFunctions( - projectId: string, - region: string -): Promise { - const endpoint = - "/" + API_VERSION + "/projects/" + projectId + "/locations/" + region + "/functions"; +export type ListFunctionsResponse = { + unreachable: string[]; + functions: CloudFunction[]; +}; + +async function list(projectId: string, region: string): Promise { + 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) { - throw new FirebaseError( - "Some Cloud Functions regions were unreachable, please try again later.", - { exit: 2 } + logger.debug( + `[functions] unable to reach the following regions: ${res.body.unreachable.join(", ")}`, ); } - const functionsList = res.body.functions || []; - _.forEach(functionsList, (f) => { - f.functionName = f.name.substring(f.name.lastIndexOf("/") + 1); + return { + functions: res.body.functions || [], + unreachable: res.body.unreachable || [], + }; + } catch (err: any) { + logger.debug(`[functions] failed to list functions for ${projectId}`); + logger.debug(`[functions] ${err?.message}`); + throw new FirebaseError(`Failed to list functions for ${projectId}`, { + original: err, + status: err instanceof FirebaseError ? err.status : undefined, }); - return functionsList; - } catch (err) { - logger.debug("[functions] failed to list functions for " + projectId); - logger.debug("[functions] " + err.message); - return Promise.reject(err.message); } } @@ -336,7 +475,266 @@ export async function listFunctions( * List all existing Cloud Functions in a project. * @param projectId the Id of the project to check. */ -export async function listAllFunctions(projectId: string): Promise { +export async function listAllFunctions(projectId: string): Promise { // "-" instead of a region string lists functions in all regions - return listFunctions(projectId, "-"); + return list(projectId, "-"); +} + +/** + * Converts a Cloud Function from the v1 API into a version-agnostic FunctionSpec struct. + * This API exists outside the GCF namespace because GCF returns an Operation + * and code may have to call this method explicitly. + */ +export function endpointFromFunction(gcfFunction: CloudFunction): backend.Endpoint { + const [, project, , region, , id] = gcfFunction.name.split("/"); + let trigger: backend.Triggered; + let uri: string | undefined; + let securityLevel: SecurityLevel | undefined; + if (gcfFunction.labels?.["deployment-scheduled"]) { + trigger = { + scheduleTrigger: {}, + }; + } else if (gcfFunction.labels?.["deployment-taskqueue"]) { + trigger = { + taskQueueTrigger: {}, + }; + } else if ( + gcfFunction.labels?.["deployment-callable"] || + // NOTE: "deployment-callabled" is a typo we introduced in https://github.com/firebase/firebase-tools/pull/4124. + // More than a month passed before we caught this typo, and we expect many callable functions in production + // to have this typo. It is convenient for users for us to treat the typo-ed label as a valid marker for callable + // function, so we do that here. + // + // The typo will be overwritten as callable functions are re-deployed. Eventually, there may be no callable + // functions with the typo-ed label, but we can't ever be sure. Sadly, we may have to carry this scar for a very long + // time. + gcfFunction.labels?.["deployment-callabled"] + ) { + trigger = { + callableTrigger: {}, + }; + } else if (gcfFunction.labels?.[BLOCKING_LABEL]) { + trigger = { + blockingTrigger: { + eventType: BLOCKING_LABEL_KEY_TO_EVENT[gcfFunction.labels[BLOCKING_LABEL]], + }, + }; + } else if (gcfFunction.httpsTrigger) { + trigger = { httpsTrigger: {} }; + } else { + trigger = { + eventTrigger: { + eventType: gcfFunction.eventTrigger!.eventType, + eventFilters: { resource: gcfFunction.eventTrigger!.resource }, + retry: !!gcfFunction.eventTrigger!.failurePolicy?.retry, + }, + }; + } + + if (gcfFunction.httpsTrigger) { + uri = gcfFunction.httpsTrigger.url; + securityLevel = gcfFunction.httpsTrigger.securityLevel; + } + + if (!supported.isRuntime(gcfFunction.runtime)) { + logger.debug( + "GCF 1st gen function has unsupported runtime:", + JSON.stringify(gcfFunction, null, 2), + ); + } + + const endpoint: backend.Endpoint = { + platform: "gcfv1", + id, + project, + region, + ...trigger, + entryPoint: gcfFunction.entryPoint, + runtime: gcfFunction.runtime, + }; + if (uri) { + endpoint.uri = uri; + } + if (securityLevel) { + endpoint.securityLevel = securityLevel; + } + proto.copyIfPresent( + endpoint, + gcfFunction, + "minInstances", + "maxInstances", + "ingressSettings", + "labels", + "environmentVariables", + "secretEnvironmentVariables", + "sourceUploadUrl", + ); + + proto.renameIfPresent(endpoint, gcfFunction, "serviceAccount", "serviceAccountEmail"); + proto.convertIfPresent( + endpoint, + gcfFunction, + "availableMemoryMb", + (raw) => 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]; + } + proto.convertIfPresent(endpoint, gcfFunction, "state", "status", (status) => { + if (status === "ACTIVE") { + return "ACTIVE"; + } else if (status === "OFFLINE") { + return "FAILED"; + } else if (status === "DEPLOY_IN_PROGRESS") { + return "DEPLOYING"; + } else if (status === "DELETE_IN_PROGRESS") { + return "DELETING"; + } + return "UNKONWN"; + }); + return endpoint; +} + +/** + * Convert the API agnostic FunctionSpec struct to a CloudFunction proto for the v1 API. + */ +export function functionFromEndpoint( + endpoint: backend.Endpoint, + sourceUploadUrl: string, +): Omit { + if (endpoint.platform !== "gcfv1") { + throw new FirebaseError( + "Trying to create a v1 CloudFunction with v2 API. This should never happen", + ); + } + + 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", + { exit: 1 }, + ); + } + const gcfFunction: Omit = { + name: backend.functionName(endpoint), + sourceUploadUrl: sourceUploadUrl, + entryPoint: endpoint.entryPoint, + runtime: endpoint.runtime, + dockerRegistry: "ARTIFACT_REGISTRY", + }; + + // 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, + // Service is unnecessary and deprecated + }; + + // For field masks to pick up a deleted failure policy we must inject an undefined + // when retry is false + gcfFunction.eventTrigger.failurePolicy = endpoint.eventTrigger.retry + ? { retry: {} } + : undefined; + } else if (backend.isScheduleTriggered(endpoint)) { + const id = backend.scheduleIdForFunction(endpoint); + gcfFunction.eventTrigger = { + eventType: "google.pubsub.topic.publish", + resource: `projects/${endpoint.project}/topics/${id}`, + }; + gcfFunction.labels = { ...gcfFunction.labels, "deployment-scheduled": "true" }; + } 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, + "minInstances", + "maxInstances", + "ingressSettings", + "environmentVariables", + "secretEnvironmentVariables", + ); + + proto.convertIfPresent(gcfFunction, endpoint, "serviceAccountEmail", "serviceAccount", (from) => + !from ? null : proto.formatServiceAccount(from, endpoint.project, true /* removeTypePrefix */), + ); + 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..c237af2db78 --- /dev/null +++ b/src/gcp/cloudfunctionsv2.spec.ts @@ -0,0 +1,867 @@ +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"; + +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 }, + state: "ACTIVE", + }; + + 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 GCF_URL = "https://region-project.cloudfunctions.net/id"; + const HAVE_CLOUD_FUNCTION_V2: cloudfunctionsv2.OutputCloudFunction = { + ...CLOUD_FUNCTION_V2, + serviceConfig: { + service: "service", + uri: RUN_URI, + }, + url: GCF_URL, + state: "ACTIVE", + updateTime: new Date(), + }; + + 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", + }, + }); + + expect( + cloudfunctionsv2.functionFromEndpoint({ + ...ENDPOINT, + platform: "gcfv2", + callableTrigger: { + genkitAction: "flows/flow", + }, + }), + ).to.deep.equal({ + ...CLOUD_FUNCTION_V2, + labels: { + ...CLOUD_FUNCTION_V2.labels, + "deployment-callable": "true", + "genkit-action": "true", + }, + }); + }); + + 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@google.com", + }; + + 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@google.com", + }, + serviceConfig: { + ...CLOUD_FUNCTION_V2.serviceConfig, + environmentVariables: { + FUNCTION_SIGNATURE_TYPE: "cloudevent", + }, + serviceAccountEmail: "sa@google.com", + }, + }; + + 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" }, + }); + }); + + it("should expand shorthand service account to full email", () => { + expect( + cloudfunctionsv2.functionFromEndpoint({ + ...ENDPOINT, + serviceAccount: "sa@", + httpsTrigger: {}, + }), + ).to.deep.equal({ + ...CLOUD_FUNCTION_V2, + serviceConfig: { + ...CLOUD_FUNCTION_V2.serviceConfig, + serviceAccountEmail: `sa@${ENDPOINT.project}.iam.gserviceaccount.com`, + }, + }); + }); + + it("should handle null service account", () => { + expect( + cloudfunctionsv2.functionFromEndpoint({ + ...ENDPOINT, + serviceAccount: null, + httpsTrigger: {}, + }), + ).to.deep.equal({ + ...CLOUD_FUNCTION_V2, + serviceConfig: { + ...CLOUD_FUNCTION_V2.serviceConfig, + serviceAccountEmail: null, + }, + }); + }); + }); + + describe("endpointFromFunction", () => { + it("should copy a minimal version", () => { + expect(cloudfunctionsv2.endpointFromFunction(HAVE_CLOUD_FUNCTION_V2)).to.deep.equal({ + ...ENDPOINT, + httpsTrigger: {}, + platform: "gcfv2", + uri: GCF_URL, + }); + }); + + 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: GCF_URL, + runServiceId: "service-id", + }); + }); + + it("should translate event triggers", () => { + let want: backend.Endpoint = { + ...ENDPOINT, + platform: "gcfv2", + uri: GCF_URL, + 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: GCF_URL, + 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: GCF_URL, + 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: GCF_URL, + 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: GCF_URL, + 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: GCF_URL, + ...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: GCF_URL, + 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: GCF_URL, + 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: GCF_URL, + 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: {}, + uri: GCF_URL, + }; + delete expectedEndpoint.runServiceId; + expect( + cloudfunctionsv2.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION_V2, + serviceConfig: undefined, + }), + ).to.deep.equal(expectedEndpoint); + }); + }); + + describe("createFunction", () => { + it("should set default environment variables", async () => { + const testFunction = { + ...CLOUD_FUNCTION_V2, + name: "projects/project/locations/region/functions/id", + serviceConfig: { + ...CLOUD_FUNCTION_V2.serviceConfig, + environmentVariables: {}, + }, + buildConfig: { + ...CLOUD_FUNCTION_V2.buildConfig, + environmentVariables: {}, + }, + }; + + const scope = nock(functionsV2Origin()) + .post("/v2/projects/project/locations/region/functions", (body) => { + expect(body.serviceConfig.environmentVariables).to.have.property( + "LOG_EXECUTION_ID", + "true", + ); + expect(body.serviceConfig.environmentVariables).to.have.property( + "FUNCTION_TARGET", + "function", + ); + expect(body.buildConfig.environmentVariables).to.have.property( + "GOOGLE_NODE_RUN_SCRIPTS", + "", + ); + return true; + }) + .query({ functionId: "id" }) + .reply(200, { name: "operations/123", done: true }); + + await cloudfunctionsv2.createFunction(testFunction); + expect(scope.isDone()).to.be.true; + }); + }); + + describe("updateFunction", () => { + it("should set default environment variables", async () => { + const scope = nock(functionsV2Origin()) + .patch("/v2/projects/project/locations/region/functions/id", (body) => { + expect(body.serviceConfig.environmentVariables).to.have.property( + "LOG_EXECUTION_ID", + "true", + ); + expect(body.serviceConfig.environmentVariables).to.have.property( + "FUNCTION_TARGET", + "function", + ); + expect(body.buildConfig.environmentVariables).to.have.property( + "GOOGLE_NODE_RUN_SCRIPTS", + "", + ); + return true; + }) + .query(true) // Accept any query parameters + .reply(200, { name: "operations/123", done: true }); + + await cloudfunctionsv2.updateFunction(CLOUD_FUNCTION_V2); + expect(scope.isDone()).to.be.true; + }); + }); +}); diff --git a/src/gcp/cloudfunctionsv2.ts b/src/gcp/cloudfunctionsv2.ts new file mode 100644 index 00000000000..37291f037c2 --- /dev/null +++ b/src/gcp/cloudfunctionsv2.ts @@ -0,0 +1,756 @@ +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 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"; +import { captureRuntimeValidationError } from "./cloudfunctions"; +import { mebibytes } from "./k8s"; + +export const API_VERSION = "v2"; + +// 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(), + auth: true, + apiVersion: API_VERSION, +}); + +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"; + +// 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: supported.Runtime; + entryPoint: string; + source: Source; + sourceToken?: string; + environmentVariables: Record; + + // Output only + build?: string; + workerPool?: string; +} + +export interface StorageSource { + bucket: string; + object: string; + generation?: number; +} + +export interface RepoSource { + projectId: string; + repoName: string; + + // oneof revision + branchName: string; + tagName: string; + commitSha: string; + // end oneof revision + + dir: string; + invertRegex: boolean; +} + +export interface Source { + // oneof source + storageSource?: StorageSource; + repoSource?: RepoSource; + // end oneof 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. */ +export interface ServiceConfig { + // Output only + service?: string; + // Output only. All Cloud Run services are HTTP services. So all Cloud + // Functions will have a URI. This URI will be different from the + // cloudfunctions.net URLs. + uri?: string; + + 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 | null; +} + +export interface EventTrigger { + // Output only. The resource name of the underlying EventArc trigger. + trigger?: string; + + // When unspecified will default to the region of the Cloud Function. + // single-region names must match the function name. + triggerRegion?: string; + + eventType: string; + eventFilters?: EventFilter[]; + pubsubTopic?: string; + + // The service account that a trigger runs as. Must have the + // 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; +} + +interface CloudFunctionBase { + name: string; + description?: string; + buildConfig: BuildConfig; + serviceConfig?: ServiceConfig; + eventTrigger?: EventTrigger; + labels?: Record | null; +} + +export type OutputCloudFunction = CloudFunctionBase & { + state: FunctionState; + updateTime: Date; + serviceConfig?: RequireKeys; + url: string; +}; + +export type InputCloudFunction = CloudFunctionBase & { + // serviceConfig is required. + serviceConfig: ServiceConfig; +}; + +export interface OperationMetadata { + createTime: string; + endTime: string; + target: string; + verb: string; + statusDetail: string; + cancelRequested: boolean; + apiVersion: string; +} + +export interface Operation { + name: string; + // Note: this field is always present, but not used in prod and is a PITA + // to add in tests. + metadata?: OperationMetadata; + done: boolean; + error?: { code: number; message: string; details: unknown }; + response?: OutputCloudFunction; +} + +// Private API interface for ListFunctionsResponse. listFunctions returns +// a CloudFunction[] +interface ListFunctionsResponse { + functions: OutputCloudFunction[]; + unreachable: string[]; +} + +interface GenerateUploadUrlResponse { + uploadUrl: string; + storageSource: StorageSource; +} + +/** + * Logs an error from a failed function deployment. + * @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(func: InputCloudFunction, type: string, err: any): void { + // Sniff for runtime validation errors and log a more user-friendly warning. + if (err?.message?.includes("Runtime validation errors")) { + const capturedMessage = captureRuntimeValidationError(err.message); + utils.logLabeledWarning("functions", capturedMessage + " for function " + func.name); + } + 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.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 ${func.name}`, { + original: err, + status: err?.context?.response?.statusCode, + context: { function: func.name }, + }); +} + +/** + * Creates an upload URL and pre-provisions a StorageSource. + */ +export async function generateUploadUrl( + projectId: string, + location: string, +): Promise { + try { + const res = await client.post( + `projects/${projectId}/locations/${location}/functions:generateUploadUrl`, + ); + return res.body; + } 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.", + ); + throw err; + } +} + +/** + * Creates a new Cloud Function. + */ +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("-", "."), + // Enable logging execution id by default for better debugging + LOG_EXECUTION_ID: "true", + }; + + try { + const res = await client.post( + components.join("/"), + cloudFunction, + { queryParams: { functionId } }, + ); + return res.body; + } catch (err: any) { + throw functionsOpLogReject(cloudFunction, "create", err); + } +} + +/** + * Gets the definition of a Cloud Function + */ +export async function getFunction( + projectId: string, + location: string, + functionId: string, +): Promise { + const name = `projects/${projectId}/locations/${location}/functions/${functionId}`; + const res = await client.get(name); + return res.body; +} + +/** + * List all functions in all regions + * Customers should generally use backend.existingBackend and backend.checkAvailability. + */ +export async function listAllFunctions(projectId: string): Promise { + return await listFunctionsInternal(projectId, /* region=*/ "-"); +} + +async function listFunctionsInternal( + projectId: string, + region: string, +): Promise { + type Response = ListFunctionsResponse & { nextPageToken?: string }; + const functions: OutputCloudFunction[] = []; + const unreacahble = new Set(); + let pageToken = ""; + while (true) { + const url = `projects/${projectId}/locations/${region}/functions`; + // 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 || []) { + unreacahble.add(region); + } + + if (!res.body.nextPageToken) { + return { + functions, + unreachable: Array.from(unreacahble), + }; + } + pageToken = res.body.nextPageToken; + } +} + +/** + * Updates a Cloud Function. + * Customers can force a field to be deleted by setting that field to `undefined` + */ +export async function updateFunction(cloudFunction: InputCloudFunction): Promise { + 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("-", "."), + // Enable logging execution id by default for better debugging + LOG_EXECUTION_ID: "true", + }; + // 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", + "buildConfig.environmentVariables", + ); + + try { + const queryParams = { + updateMask: fieldMasks.join(","), + }; + const res = await client.patch( + cloudFunction.name, + cloudFunction, + { queryParams }, + ); + return res.body; + } catch (err: any) { + throw functionsOpLogReject(cloudFunction, "update", err); + } +} + +/** + * Deletes a Cloud Function. + * It is safe, but should be unnecessary, to delete a Cloud Function by just its name. + */ +export async function deleteFunction(cloudFunction: string): Promise { + try { + const res = await client.delete(cloudFunction); + return res.body; + } catch (err: any) { + throw functionsOpLogReject({ name: cloudFunction } as InputCloudFunction, "update", err); + } +} + +/** + * 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", + ); + } + + 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", + ); + } + + const gcfFunction: InputCloudFunction = { + name: backend.functionName(endpoint), + buildConfig: { + runtime: endpoint.runtime, + entryPoint: endpoint.entryPoint, + source: { + storageSource: endpoint.source?.storageSource, + }, + // We don't use build environment variables, + environmentVariables: {}, + }, + serviceConfig: {}, + }; + + proto.copyIfPresent(gcfFunction, endpoint, "labels"); + proto.copyIfPresent( + gcfFunction.serviceConfig, + endpoint, + "environmentVariables", + "secretEnvironmentVariables", + "ingressSettings", + "timeoutSeconds", + ); + proto.convertIfPresent( + gcfFunction.serviceConfig, + endpoint, + "serviceAccountEmail", + "serviceAccount", + (from) => + !from + ? null + : proto.formatServiceAccount(from, endpoint.project, true /* removeTypePrefix */), + ); + // 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, + "maxInstanceRequestConcurrency", + "concurrency", + ); + 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 (gcfFunction.serviceConfig.serviceAccountEmail) { + gcfFunction.eventTrigger.serviceAccountEmail = gcfFunction.serviceConfig.serviceAccountEmail; + } + if (gcfFunction.eventTrigger.eventType === PUBSUB_PUBLISH_EVENT) { + 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", + ); + 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" }; + if (endpoint.callableTrigger.genkitAction) { + gcfFunction.labels["genkit-action"] = "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; +} + +/** + * 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") { + trigger = { + scheduleTrigger: {}, + }; + } else if (gcfFunction.labels?.["deployment-taskqueue"] === "true") { + trigger = { + taskQueueTrigger: {}, + }; + } else if (gcfFunction.labels?.["deployment-callable"] === "true") { + trigger = { + callableTrigger: {}, + }; + } else if (gcfFunction.labels?.[BLOCKING_LABEL]) { + trigger = { + blockingTrigger: { + eventType: BLOCKING_LABEL_KEY_TO_EVENT[gcfFunction.labels[BLOCKING_LABEL]], + }, + }; + } 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 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", + ); + } else { + trigger = { httpsTrigger: {} }; + } + + if (!supported.isRuntime(gcfFunction.buildConfig.runtime)) { + logger.debug("GCFv2 function has a deprecated runtime:", JSON.stringify(gcfFunction, null, 2)); + } + + const endpoint: backend.Endpoint = { + platform: "gcfv2", + id, + project, + region, + ...trigger, + entryPoint: gcfFunction.buildConfig.entryPoint, + runtime: gcfFunction.buildConfig.runtime, + source: gcfFunction.buildConfig.source, + }; + 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("/")); + } + } + proto.renameIfPresent(endpoint, gcfFunction, "uri", "url"); + endpoint.codebase = gcfFunction.labels?.[CODEBASE_LABEL] || projectConfig.DEFAULT_CODEBASE; + if (gcfFunction.labels?.[HASH_LABEL]) { + endpoint.hash = gcfFunction.labels[HASH_LABEL]; + } + proto.copyIfPresent(endpoint, gcfFunction, "state"); + return endpoint; +} diff --git a/src/gcp/cloudlogging.js b/src/gcp/cloudlogging.js deleted file mode 100644 index ad504c8658a..00000000000 --- a/src/gcp/cloudlogging.js +++ /dev/null @@ -1,26 +0,0 @@ -"use strict"; - -var api = require("../api"); - -var version = "v2beta1"; - -var _listEntries = function (projectId, filter, pageSize, order) { - return api - .request("POST", "/" + version + "/entries:list", { - auth: true, - data: { - projectIds: [projectId], - filter: filter, - orderBy: "timestamp " + order, - pageSize: pageSize, - }, - origin: api.cloudloggingOrigin, - }) - .then(function (result) { - return Promise.resolve(result.body.entries); - }); -}; - -module.exports = { - listEntries: _listEntries, -}; diff --git a/src/gcp/cloudlogging.spec.ts b/src/gcp/cloudlogging.spec.ts new file mode 100644 index 00000000000..f5cce38d2e8 --- /dev/null +++ b/src/gcp/cloudlogging.spec.ts @@ -0,0 +1,57 @@ +import { expect } from "chai"; +import * as nock from "nock"; + +import * as cloudlogging from "./cloudlogging"; +import { FirebaseError } from "../error"; +import { cloudloggingOrigin } from "../api"; + +describe("cloudlogging", () => { + before(() => { + nock.disableNetConnect(); + }); + + after(() => { + nock.enableNetConnect(); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + describe("listEntries", () => { + it("should resolve with a list of log entries on success", async () => { + const entries = [{ logName: "log1" }, { logName: "log2" }]; + nock(cloudloggingOrigin()).post("/v2/entries:list").reply(200, { entries }); + + await expect( + cloudlogging.listEntries("project", "filter", 10, "desc"), + ).to.eventually.deep.equal({ entries, nextPageToken: undefined }); + }); + + it("should reject if the API call fails", async () => { + nock(cloudloggingOrigin()).post("/v2/entries:list").reply(404, { error: "not found" }); + + await expect(cloudlogging.listEntries("project", "filter", 10, "desc")).to.be.rejectedWith( + FirebaseError, + "Failed to retrieve log entries from Google Cloud.", + ); + }); + + it("should include nextPageToken when provided", async () => { + const entries = [{ logName: "log1" }]; + nock(cloudloggingOrigin()) + .post("/v2/entries:list", (body) => { + expect(body.pageToken).to.equal("token"); + return true; + }) + .reply(200, { entries, nextPageToken: "next" }); + + await expect( + cloudlogging.listEntries("project", "filter", 10, "asc", "token"), + ).to.eventually.deep.equal({ + entries, + nextPageToken: "next", + }); + }); + }); +}); diff --git a/src/gcp/cloudlogging.ts b/src/gcp/cloudlogging.ts new file mode 100644 index 00000000000..d32867458aa --- /dev/null +++ b/src/gcp/cloudlogging.ts @@ -0,0 +1,75 @@ +import { cloudloggingOrigin } from "../api"; +import { Client } from "../apiv2"; +import { FirebaseError } from "../error"; + +const API_VERSION = "v2"; + +export interface LogEntry { + logName: string; + resource: any; + timestamp?: string; + receiveTimestamp: string; + severity?: any; + insertId?: string; + httpRequest?: any; + labels?: any; + metadata?: any; + operation?: any; + trace?: string; + spanId?: string; + traceSampled?: boolean; + sourceLocation?: any; + protoPayload?: any; + textPayload?: string; + jsonPayload?: any; +} + +interface ListEntriesRequest { + resourceNames: string[]; + filter: string; + orderBy: string; + pageSize: number; + pageToken?: string; +} + +interface ListEntriesResponse { + entries?: LogEntry[]; + nextPageToken?: string; +} + +/** + * Lists Cloud Logging entries with optional pagination support. + * Ref: https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/list + */ +export async function listEntries( + projectId: string, + filter: string, + pageSize: number, + order: string, + pageToken?: string, +): Promise<{ entries: LogEntry[]; nextPageToken?: string }> { + const client = new Client({ urlPrefix: cloudloggingOrigin(), apiVersion: API_VERSION }); + const body: ListEntriesRequest = { + resourceNames: [`projects/${projectId}`], + filter, + orderBy: `timestamp ${order}`, + pageSize, + }; + if (pageToken) { + body.pageToken = pageToken; + } + try { + const result = await client.post( + "/entries:list", + body, + ); + return { + entries: result.body.entries ?? [], + nextPageToken: result.body.nextPageToken, + }; + } 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..6e4d4095ec6 --- /dev/null +++ b/src/gcp/cloudmonitoring.ts @@ -0,0 +1,155 @@ +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, + project: number | string, +): Promise { + const client = new Client({ + urlPrefix: cloudMonitoringOrigin(), + apiVersion: CLOUD_MONITORING_VERSION, + }); + try { + const res = await client.get<{ timeSeries: TimeSeriesResponse }>( + `/projects/${project}/timeSeries/`, + { + queryParams: query as { [key: string]: any }, + }, + ); + return res.body.timeSeries; + } catch (err: any) { + throw new FirebaseError(`Failed to get Cloud Monitoring metric: ${err}`, { + status: err.status, + }); + } +} diff --git a/src/gcp/cloudscheduler.spec.ts b/src/gcp/cloudscheduler.spec.ts new file mode 100644 index 00000000000..b2801ba11f6 --- /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: Request to https://cloudscheduler.googleapis.com/v1/projects/test-project/locations/us-east1/jobs had 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", async () => { + expect( + await 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", async () => { + expect( + await 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", async () => { + expect( + await 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", async () => { + expect( + await 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 29f9666cf47..8682a021117 100644 --- a/src/gcp/cloudscheduler.ts +++ b/src/gcp/cloudscheduler.ts @@ -1,44 +1,91 @@ import * as _ from "lodash"; -import * as api from "../api"; + import { FirebaseError } from "../error"; -import { logLabeledBullet, logLabeledSuccess } from "../utils"; +import { logger } from "../logger"; +import { cloudschedulerOrigin } from "../api"; +import { Client } from "../apiv2"; +import * as backend from "../deploy/functions/backend"; +import * as proto from "./proto"; +import * as gce from "../gcp/computeEngine"; +import { assertExhaustive, nullsafeVisitor } from "../functional"; + +const VERSION = "v1"; +const DEFAULT_TIME_ZONE_V1 = "America/Los_Angeles"; +const DEFAULT_TIME_ZONE_V2 = "UTC"; + +export interface PubsubTarget { + topicName: string; + data?: string; + attributes?: Record; +} + +export type HttpMethod = "POST" | "GET" | "HEAD" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"; + +export interface OauthToken { + serviceAccountEmail: string; + scope: string; +} + +export interface OidcToken { + serviceAccountEmail: string; + audience?: string; +} + +export interface HttpTarget { + uri: string; + httpMethod: HttpMethod; + headers?: Record; + body?: string; -const VERSION = "v1beta1"; -const DEFAULT_TIME_ZONE = "America/Los_Angeles"; + // oneof authorizationHeader + oauthToken?: OauthToken; + oidcToken?: OidcToken; + // end oneof authorizationHeader; +} + +export interface RetryConfig { + retryCount?: number; + maxRetryDuration?: proto.Duration; + maxBackoffDuration?: proto.Duration; + maxDoublings?: number; +} export interface Job { name: string; schedule: string; description?: string; - timeZone?: string; - httpTarget?: { - uri: string; - httpMethod: string; - }; + timeZone?: string | null; + + // oneof target + httpTarget?: HttpTarget; + pubsubTarget?: PubsubTarget; + // 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; }; } +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); } /** @@ -47,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}`); } /** @@ -59,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, }); } @@ -71,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(","), + }, }); } @@ -98,43 +151,148 @@ 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 (_.get(err, "context.response.statusCode") === 404) { + 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}`); } - logLabeledSuccess("functions", `created scheduler job ${jobName}`); + logger.debug(`created scheduler job ${jobName}`); return newJob; } 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)) { - logLabeledBullet("functions", `scheduler job ${jobName} is up to date, no changes required`); + if (!needUpdate(existingJob.body, job)) { + logger.debug(`scheduler job ${jobName} is up to date, no changes required`); return; } const updatedJob = await updateJob(job); - logLabeledBullet("functions", `updated scheduler job ${jobName}`); + logger.debug(`updated scheduler job ${jobName}`); return updatedJob; } /** * 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 async function jobFromEndpoint( + endpoint: backend.Endpoint & backend.ScheduleTriggered, + location: string, + projectNumber: string, +): Promise { + const job: Partial = {}; + job.name = jobNameForEndpoint(endpoint, location); + if (endpoint.platform === "gcfv1") { + job.timeZone = endpoint.scheduleTrigger.timeZone || DEFAULT_TIME_ZONE_V1; + job.pubsubTarget = { + topicName: topicNameForEndpoint(endpoint), + attributes: { + scheduled: "true", + }, + }; + } else if (endpoint.platform === "gcfv2" || endpoint.platform === "run") { + job.timeZone = endpoint.scheduleTrigger.timeZone || DEFAULT_TIME_ZONE_V2; + job.httpTarget = { + uri: endpoint.uri!, + httpMethod: "POST", + oidcToken: { + serviceAccountEmail: + endpoint.serviceAccount ?? (await gce.getDefaultServiceAccount(projectNumber)), + }, + }; + } else { + assertExhaustive(endpoint.platform); + } + 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.spec.ts b/src/gcp/cloudsql/cloudsqladmin.spec.ts new file mode 100644 index 00000000000..03ecd10be99 --- /dev/null +++ b/src/gcp/cloudsql/cloudsqladmin.spec.ts @@ -0,0 +1,320 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import * as sinon from "sinon"; + +import * as sqladmin from "../../gcp/cloudsql/cloudsqladmin"; +import * as iam from "../../gcp/iam"; +import { cloudSQLAdminOrigin } from "../../api"; +import { Options } from "../../options"; +import * as operationPoller from "../../operation-poller"; +import { Config } from "../../config"; +import { RC } from "../../rc"; + +const PROJECT_ID = "test-project"; +const INSTANCE_ID = "test-instance"; +const DATABASE_ID = "test-database"; +const USERNAME = "test-user"; +const API_VERSION = "v1"; + +const options: Options = { + project: PROJECT_ID, + auth: true, + cwd: "", + configPath: "", + only: "", + except: "", + config: new Config({}, { projectDir: "", cwd: "" }), + filteredTargets: [], + force: false, + json: false, + nonInteractive: false, + interactive: false, + debug: false, + rc: new RC(), +}; + +describe("cloudsqladmin", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + nock.cleanAll(); + }); + + describe("iamUserIsCSQLAdmin", () => { + it("should return true if user has required permissions", async () => { + sandbox.stub(iam, "testIamPermissions").resolves({ allowed: [], missing: [], passed: true }); + const result = await sqladmin.iamUserIsCSQLAdmin(options); + expect(result).to.be.true; + }); + + it("should return false if user does not have required permissions", async () => { + sandbox + .stub(iam, "testIamPermissions") + .resolves({ allowed: [], missing: ["p1"], passed: false }); + const result = await sqladmin.iamUserIsCSQLAdmin(options); + expect(result).to.be.false; + }); + + it("should return false on IAM error", async () => { + sandbox.stub(iam, "testIamPermissions").rejects(new Error("IAM error")); + const result = await sqladmin.iamUserIsCSQLAdmin(options); + expect(result).to.be.false; + }); + }); + + describe("listInstances", () => { + it("should return a list of instances on success", async () => { + const instances = [{ name: INSTANCE_ID }]; + nock(cloudSQLAdminOrigin()) + .get(`/${API_VERSION}/projects/${PROJECT_ID}/instances`) + .reply(200, { items: instances }); + + const result = await sqladmin.listInstances(PROJECT_ID); + expect(result).to.deep.equal(instances); + expect(nock.isDone()).to.be.true; + }); + + it("should handle allowlist error", async () => { + nock(cloudSQLAdminOrigin()) + .post(`/${API_VERSION}/projects/${PROJECT_ID}/instances`) + .reply(400, { + error: { + message: "Not allowed to set system label: firebase-data-connect", + }, + }); + + await expect( + sqladmin.createInstance({ + projectId: PROJECT_ID, + location: "us-central", + instanceId: INSTANCE_ID, + enableGoogleMlIntegration: false, + freeTrial: false, + }), + ).to.be.rejectedWith("Cloud SQL free trial instances are not yet available in us-central"); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("getInstance", () => { + it("should return an instance on success", async () => { + const instance = { name: INSTANCE_ID, state: "RUNNABLE" }; + nock(cloudSQLAdminOrigin()) + .get(`/${API_VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .reply(200, instance); + + const result = await sqladmin.getInstance(PROJECT_ID, INSTANCE_ID); + expect(result).to.deep.equal(instance); + expect(nock.isDone()).to.be.true; + }); + + it("should update an instance with google ml integration", async () => { + const instance = { + name: INSTANCE_ID, + project: PROJECT_ID, + settings: { databaseFlags: [] }, + }; + const op = { name: "op-name", status: "DONE" }; + nock(cloudSQLAdminOrigin()) + .patch(`/${API_VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .reply(200, op); + sandbox.stub(operationPoller, "pollOperation").resolves(instance); + + const result = await sqladmin.updateInstanceForDataConnect(instance as any, true); + + expect(result).to.deep.equal(instance); + expect(nock.isDone()).to.be.true; + }); + + it("should throw if instance is in a failed state", async () => { + const instance = { name: INSTANCE_ID, state: "FAILED" }; + nock(cloudSQLAdminOrigin()) + .get(`/${API_VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .reply(200, instance); + + await expect(sqladmin.getInstance(PROJECT_ID, INSTANCE_ID)).to.be.rejected; + }); + }); + + describe("instanceConsoleLink", () => { + it("should return the correct console link", () => { + const link = sqladmin.instanceConsoleLink(PROJECT_ID, INSTANCE_ID); + expect(link).to.equal( + `https://console.cloud.google.com/sql/instances/${INSTANCE_ID}/overview?project=${PROJECT_ID}`, + ); + }); + }); + + describe("createInstance", () => { + it("should create an instance", async () => { + nock(cloudSQLAdminOrigin()) + .post(`/${API_VERSION}/projects/${PROJECT_ID}/instances`) + .reply(200, {}); + + await sqladmin.createInstance({ + projectId: PROJECT_ID, + location: "us-central", + instanceId: INSTANCE_ID, + enableGoogleMlIntegration: false, + freeTrial: false, + }); + + expect(nock.isDone()).to.be.true; + }); + }); + + describe("updateInstanceForDataConnect", () => { + it("should update an instance", async () => { + const instance = { + name: INSTANCE_ID, + project: PROJECT_ID, + settings: { databaseFlags: [] }, + }; + const op = { name: "op-name", status: "DONE" }; + nock(cloudSQLAdminOrigin()) + .patch(`/${API_VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) + .reply(200, op); + sandbox.stub(operationPoller, "pollOperation").resolves(instance); + + const result = await sqladmin.updateInstanceForDataConnect(instance as any, false); + + expect(result).to.deep.equal(instance); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("Databases", () => { + it("should list databases", async () => { + const databases = [{ name: DATABASE_ID }]; + nock(cloudSQLAdminOrigin()) + .get(`/${API_VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}/databases`) + .reply(200, { items: databases }); + + const result = await sqladmin.listDatabases(PROJECT_ID, INSTANCE_ID); + expect(result).to.deep.equal(databases); + expect(nock.isDone()).to.be.true; + }); + + it("should get a database", async () => { + const database = { name: DATABASE_ID }; + nock(cloudSQLAdminOrigin()) + .get( + `/${API_VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}/databases/${DATABASE_ID}`, + ) + .reply(200, database); + + const result = await sqladmin.getDatabase(PROJECT_ID, INSTANCE_ID, DATABASE_ID); + expect(result).to.deep.equal(database); + expect(nock.isDone()).to.be.true; + }); + + it("should create a database", async () => { + const op = { name: "op-name", status: "DONE" }; + const database = { name: DATABASE_ID }; + nock(cloudSQLAdminOrigin()) + .post(`/${API_VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}/databases`) + .reply(200, op); + sandbox.stub(operationPoller, "pollOperation").resolves(database); + + const result = await sqladmin.createDatabase(PROJECT_ID, INSTANCE_ID, DATABASE_ID); + expect(result).to.deep.equal(database); + expect(nock.isDone()).to.be.true; + }); + + it("should delete a database", async () => { + const database = { name: DATABASE_ID }; + nock(cloudSQLAdminOrigin()) + .delete( + `/${API_VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}/databases/${DATABASE_ID}`, + ) + .reply(200, database); + + const result = await sqladmin.deleteDatabase(PROJECT_ID, INSTANCE_ID, DATABASE_ID); + expect(result).to.deep.equal(database); + expect(nock.isDone()).to.be.true; + }); + }); + + describe("Users", () => { + it("should create a user", async () => { + const op = { name: "op-name", status: "DONE" }; + const user = { name: USERNAME }; + nock(cloudSQLAdminOrigin()) + .post(`/${API_VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}/users`) + .reply(200, op); + sandbox.stub(operationPoller, "pollOperation").resolves(user); + + const result = await sqladmin.createUser(PROJECT_ID, INSTANCE_ID, "BUILT_IN", USERNAME); + expect(result).to.deep.equal(user); + expect(nock.isDone()).to.be.true; + }); + + it("should retry creating a user if built-in role is not ready", async () => { + const op = { name: "op-name", status: "DONE" }; + const user = { name: USERNAME }; + nock(cloudSQLAdminOrigin()) + .post(`/${API_VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}/users`) + .reply(400, { + error: { + message: "cloudsqliamuser", + }, + }); + nock(cloudSQLAdminOrigin()) + .post(`/${API_VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}/users`) + .reply(200, op); + sandbox.stub(operationPoller, "pollOperation").resolves(user); + + const result = await sqladmin.createUser( + PROJECT_ID, + INSTANCE_ID, + "BUILT_IN", + USERNAME, + undefined, + 1, + ); + + expect(result).to.deep.equal(user); + expect(nock.isDone()).to.be.true; + }); + + it("should get a user", async () => { + const user = { name: USERNAME }; + nock(cloudSQLAdminOrigin()) + .get(`/${API_VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}/users/${USERNAME}`) + .reply(200, user); + + const result = await sqladmin.getUser(PROJECT_ID, INSTANCE_ID, USERNAME); + expect(result).to.deep.equal(user); + expect(nock.isDone()).to.be.true; + }); + + it("should delete a user", async () => { + const user = { name: USERNAME }; + nock(cloudSQLAdminOrigin()) + .delete( + `/${API_VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}/users?name=${USERNAME}`, + ) + .reply(200, user); + + const result = await sqladmin.deleteUser(PROJECT_ID, INSTANCE_ID, USERNAME); + expect(result).to.deep.equal(user); + expect(nock.isDone()).to.be.true; + }); + + it("should list users", async () => { + const users = [{ name: USERNAME }]; + nock(cloudSQLAdminOrigin()) + .get(`/${API_VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}/users`) + .reply(200, { items: users }); + + const result = await sqladmin.listUsers(PROJECT_ID, INSTANCE_ID); + expect(result).to.deep.equal(users); + expect(nock.isDone()).to.be.true; + }); + }); +}); diff --git a/src/gcp/cloudsql/cloudsqladmin.ts b/src/gcp/cloudsql/cloudsqladmin.ts new file mode 100755 index 00000000000..8a7e1e5e97c --- /dev/null +++ b/src/gcp/cloudsql/cloudsqladmin.ts @@ -0,0 +1,291 @@ +import { Client } from "../../apiv2"; +import { cloudSQLAdminOrigin } from "../../api"; +import * as clc from "colorette"; +import * as operationPoller from "../../operation-poller"; +import { Instance, Database, User, UserType, DatabaseFlag } from "./types"; +import { needProjectId } from "../../projectUtils"; +import { Options } from "../../options"; +import { logger } from "../../logger"; +import { testIamPermissions } from "../iam"; +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 iamUserIsCSQLAdmin(options: Options): Promise { + const projectId = needProjectId(options); + const requiredPermissions = [ + "cloudsql.instances.connect", + "cloudsql.instances.get", + "cloudsql.users.create", + "cloudsql.users.update", + ]; + + try { + const iamResult = await testIamPermissions(projectId, requiredPermissions); + return iamResult.passed; + } catch (err: any) { + logger.debug(`[iam] error while checking permissions, command may fail: ${err}`); + return false; + } +} + +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 ${clc.bold(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(args: { + projectId: string; + location: string; + instanceId: string; + enableGoogleMlIntegration: boolean; + freeTrial: boolean; +}): Promise { + const databaseFlags = [{ name: "cloudsql.iam_authentication", value: "on" }]; + if (args.enableGoogleMlIntegration) { + databaseFlags.push({ name: "cloudsql.enable_google_ml_integration", value: "on" }); + } + try { + await client.post, Operation>(`projects/${args.projectId}/instances`, { + name: args.instanceId, + region: args.location, + databaseVersion: "POSTGRES_15", + settings: { + tier: "db-f1-micro", + edition: "ENTERPRISE", + ipConfiguration: { + authorizedNetworks: [], + }, + enableGoogleMlIntegration: args.enableGoogleMlIntegration, + databaseFlags, + storageAutoResize: false, + userLabels: { "firebase-data-connect": args.freeTrial ? "ft" : "nt" }, + insightsConfig: { + queryInsightsEnabled: true, + queryPlansPerMinute: 5, // Match the default settings + queryStringLength: 1024, // Match the default settings + }, + }, + }); + return; + } catch (err: any) { + handleAllowlistError(err, args.location); + throw err; + } +} + +/** + * 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: { + enablePrivatePathForGoogleCloudServices: + !!instance?.settings?.ipConfiguration?.privateNetwork, + }, + 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 handleAllowlistError(err: any, region: string) { + if (err.message.includes("Not allowed to set system label: firebase-data-connect")) { + throw new FirebaseError( + `Cloud SQL free trial instances are not yet available in ${region}. Please check https://firebase.google.com/docs/data-connect/ for a full list of available regions.`, + ); + } +} + +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 deleteDatabase( + projectId: string, + instanceId: string, + databaseId: string, +): Promise { + const res = await client.delete( + `projects/${projectId}/instances/${instanceId}/databases/${databaseId}`, + ); + return res.body; +} + +export async function createUser( + projectId: string, + instanceId: string, + type: UserType, + username: string, + password?: string, + retryTimeout?: number, +): 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, retryTimeout ?? 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..0dc2b87f579 --- /dev/null +++ b/src/gcp/cloudsql/connect.ts @@ -0,0 +1,217 @@ +import * as pg from "pg"; +import { Connector, IpAddressTypes, AuthTypes } from "@google-cloud/cloud-sql-connector"; + +import { requireAuth } from "../../requireAuth"; +import { needProjectId, needProjectNumber } from "../../projectUtils"; +import { dataconnectP4SADomain } from "../../api"; +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; + transaction?: boolean; + }, +): Promise { + 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 authType: AuthTypes; + switch (user.type) { + case "CLOUD_IAM_USER": { + connector = new Connector({ + auth: new FBToolsAuthClient(), + }); + authType = AuthTypes.IAM; + break; + } + case "CLOUD_IAM_SERVICE_ACCOUNT": { + connector = new Connector(); + authType = AuthTypes.IAM; + // 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. + 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(), + }); + authType = AuthTypes.PASSWORD; + break; + } + } + const connectionOpts = { + instanceConnectionName: connectionName, + ipType: instance.ipAddresses.some((ip) => ip.type === "PRIMARY") + ? IpAddressTypes.PUBLIC + : IpAddressTypes.PRIVATE, + authType: authType, + }; + const pool = new pg.Pool({ + ...(await connector.getOptions(connectionOpts)), + connectionTimeoutMillis: 1000, + password: opts.password, + user: opts.username, + database: opts.databaseId, + }); + + const cleanUpFn = async () => { + conn.release(); + await pool.end(); + connector.close(); + }; + + const conn = await pool.connect(); + const results: pg.QueryResult[] = []; + logFn(`Logged in as ${opts.username}`); + if (opts.transaction) { + sqlStatements.unshift("BEGIN;"); + sqlStatements.push("COMMIT;"); + } + for (const s of sqlStatements) { + logFn(`> ${s}`); + try { + results.push(await conn.query(s)); + } catch (err) { + logFn(`Rolling back transaction due to error ${err}}`); + await conn.query("ROLLBACK;"); + await cleanUpFn(); + throw new FirebaseError(`Error executing ${err}`); + } + } + + await cleanUpFn(); + logFn(``); + return results; +} + +export async function executeSqlCmdsAsIamUser( + options: Options, + instanceId: string, + databaseId: string, + cmds: string[], + silent = false, + transaction = false, +): Promise { + const projectId = needProjectId(options); + const { user: iamUser } = await getIAMUser(options); + + return await execute(cmds, { + projectId, + instanceId, + databaseId, + username: iamUser, + silent: silent, + transaction: transaction, + }); +} + +// Note this will change the password of the builtin firebasesuperuser user on every invocation. +// The role is set to 'cloudsqlsuperuser' (not the builtin user) unless SET ROLE is explicitly +// set in the commands. +export async function executeSqlCmdsAsSuperUser( + options: Options, + instanceId: string, + databaseId: string, + cmds: string[], + silent = false, + transaction = false, +): Promise { + const projectId = needProjectId(options); + // 1. Create a temporary builtin user + const superuser = "firebasesuperuser"; + const temporaryPassword = utils.generatePassword(20); + await cloudSqlAdminClient.createUser( + projectId, + instanceId, + "BUILT_IN", + superuser, + temporaryPassword, + ); + + return await execute([`SET ROLE = '${superuser}'`, ...cmds], { + projectId, + instanceId, + databaseId, + username: superuser, + password: temporaryPassword, + silent: silent, + transaction: transaction, + }); +} + +export function getDataConnectP4SA(projectNumber: string): string { + return `service-${projectNumber}@${dataconnectP4SADomain()}`; +} + +export async function getIAMUser(options: Options): Promise<{ user: string; mode: UserType }> { + const account = await requireAuth(options); + if (!account) { + throw new FirebaseError( + "No account to set up! Run `firebase login` or set Application Default Credentials", + ); + } + + return toDatabaseUser(account); +} + +// setupIAMUsers sets up the current user identity to connect to CloudSQL. +// Steps: +// 1. Create an IAM user for the current identity +// 2. Create an IAM user for FDC P4SA +export async function setupIAMUsers(instanceId: 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 { user, mode } = await getIAMUser(options); + + // 1. Create an IAM user for the current identity. + await cloudSqlAdminClient.createUser(projectId, instanceId, mode, user); + + // 2. Create dataconnenct P4SA user in case it's not created. + const projectNumber = await needProjectNumber(options); + const { user: fdcP4SAUser, mode: fdcP4SAmode } = toDatabaseUser( + getDataConnectP4SA(projectNumber), + ); + await cloudSqlAdminClient.createUser(projectId, instanceId, fdcP4SAmode, fdcP4SAUser); + + return user; +} + +// 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. +export 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/interactive.ts b/src/gcp/cloudsql/interactive.ts new file mode 100644 index 00000000000..9a85a472e03 --- /dev/null +++ b/src/gcp/cloudsql/interactive.ts @@ -0,0 +1,53 @@ +import * as pg from "pg"; +import * as ora from "ora"; +import * as clc from "colorette"; +import { logger } from "../../logger"; +import { confirm } from "../../prompt"; +import * as Table from "cli-table3"; + +// Not comprehensive list, used for best offer prompting. +const destructiveSqlKeywords = ["DROP", "DELETE"]; + +function checkIsDestructiveSql(query: string): boolean { + const upperCaseQuery = query.toUpperCase(); + return destructiveSqlKeywords.some((keyword) => upperCaseQuery.includes(keyword.toUpperCase())); +} + +export async function confirmDangerousQuery(query: string): Promise { + if (checkIsDestructiveSql(query)) { + return await confirm({ + message: clc.yellow("This query may be destructive. Are you sure you want to proceed?"), + default: false, + }); + } + return true; +} + +// Pretty query execution display such as spinner and actual returned content for `SELECT` query. +export async function interactiveExecuteQuery(query: string, conn: pg.PoolClient) { + const spinner = ora("Executing query...").start(); + try { + const results = await conn.query(query); + spinner.succeed(clc.green("Query executed successfully")); + + if (Array.isArray(results.rows) && results.rows.length > 0) { + const table: any[] = new Table({ + head: Object.keys(results.rows[0]).map((key) => clc.cyan(key)), + style: { head: [], border: [] }, + }); + + for (const row of results.rows) { + table.push(Object.values(row) as any); + } + + logger.info(table.toString()); + } else { + // If nothing is returned and the query was select, let the user know there was no results. + if (query.toUpperCase().includes("SELECT")) { + logger.info(clc.yellow("No results returned")); + } + } + } catch (err) { + spinner.fail(clc.red(`Failed executing query: ${err}`)); + } +} diff --git a/src/gcp/cloudsql/permissions.ts b/src/gcp/cloudsql/permissions.ts new file mode 100644 index 00000000000..483fd023a2e --- /dev/null +++ b/src/gcp/cloudsql/permissions.ts @@ -0,0 +1,152 @@ +export const DEFAULT_SCHEMA = "public"; +export const FIREBASE_SUPER_USER = "firebasesuperuser"; +export const CLOUDSQL_SUPER_USER = "cloudsqlsuperuser"; + +export function firebaseowner(databaseId: string, schema: string = DEFAULT_SCHEMA) { + return `firebaseowner_${databaseId}_${schema}`; +} + +export function firebasereader(databaseId: string, schema: string = DEFAULT_SCHEMA) { + return `firebasereader_${databaseId}_${schema}`; +} + +export function firebasewriter(databaseId: string, schema: string = DEFAULT_SCHEMA) { + return `firebasewriter_${databaseId}_${schema}`; +} + +// Creates the owner role, modifies schema owner to firebaseowner. +export function ownerRolePermissions( + databaseId: string, + superuser: string, + schema: string, +): string[] { + const firebaseOwnerRole = firebaseowner(databaseId, schema); + return [ + `do + $$ + begin + if not exists (select FROM pg_catalog.pg_roles + WHERE rolname = '${firebaseOwnerRole}') then + CREATE ROLE "${firebaseOwnerRole}" WITH ADMIN "${superuser}"; + end if; + end + $$ + ;`, + + // We grant owner to cloudsqlsuperuser because only the owner can alter the schema owner. + // It's also needed for the reader and write roles setup as only owner can alter schema defaults. + `GRANT "${firebaseOwnerRole}" TO "cloudsqlsuperuser"`, + + `ALTER SCHEMA "${schema}" OWNER TO "${firebaseOwnerRole}"`, + `GRANT USAGE ON SCHEMA "${schema}" TO "${firebaseOwnerRole}"`, + `GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA "${schema}" TO "${firebaseOwnerRole}"`, + `GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA "${schema}" TO "${firebaseOwnerRole}"`, + ]; +} + +// The SQL permissions required for a role to read/write the FDC databases. +// Requires the firebase_owner_* role to be the owner of the schema for default permissions. +export function writerRolePermissions( + databaseId: string, + superuser: string, + schema: string, +): string[] { + const firebaseWriterRole = firebasewriter(databaseId, schema); + return [ + `do + $$ + begin + if not exists (select FROM pg_catalog.pg_roles + WHERE rolname = '${firebaseWriterRole}') then + CREATE ROLE "${firebaseWriterRole}" WITH ADMIN "${superuser}"; + end if; + end + $$ + ;`, + + `GRANT "${firebaseWriterRole}" TO "cloudsqlsuperuser"`, + + `GRANT USAGE ON SCHEMA "${schema}" TO "${firebaseWriterRole}"`, + + // Grant writer role SELECT, INSERT, UPDATE, DELETE on all tables + // (You might want to exclude certain sensitive tables) + `GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON ALL TABLES IN SCHEMA "${schema}" TO "${firebaseWriterRole}"`, + + // Grant writer usage on sequences for nextval() in inserts + `GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA "${schema}" TO "${firebaseWriterRole}"`, + + // Grant execution on function which could be needed by some extensions. + `GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA "${schema}" TO "${firebaseWriterRole}"`, + ]; +} + +// The SQL permissions required for a role to read the FDC databases. +// Requires the firebase_owner_* role to be the owner of the schema for default permissions. +export function readerRolePermissions( + databaseId: string, + superuser: string, + schema: string, +): string[] { + const firebaseReaderRole = firebasereader(databaseId, schema); + return [ + `do + $$ + begin + if not exists (select FROM pg_catalog.pg_roles + WHERE rolname = '${firebaseReaderRole}') then + CREATE ROLE "${firebaseReaderRole}" WITH ADMIN "${superuser}"; + end if; + end + $$ + ;`, + + `GRANT "${firebaseReaderRole}" TO "cloudsqlsuperuser"`, + + `GRANT USAGE ON SCHEMA "${schema}" TO "${firebaseReaderRole}"`, + + `GRANT SELECT ON ALL TABLES IN SCHEMA "${schema}" TO "${firebaseReaderRole}"`, + + // Grant reader usage on sequences for nextval() + `GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA "${schema}" TO "${firebaseReaderRole}"`, + + // Grant execution on function which could be needed by some extensions. + `GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA "${schema}" TO "${firebaseReaderRole}"`, + ]; +} + +// Gives firebase reader and writer roles ability to see tables created by other owners in a given schema. +export function defaultPermissions(databaseId: string, schema: string, ownerRole: string) { + const firebaseWriterRole = firebasewriter(databaseId, schema); + const firebaseReaderRole = firebasereader(databaseId, schema); + return [ + `ALTER DEFAULT PRIVILEGES + FOR ROLE "${ownerRole}" + IN SCHEMA "${schema}" + GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON TABLES TO "${firebaseWriterRole}";`, + + `ALTER DEFAULT PRIVILEGES + FOR ROLE "${ownerRole}" + IN SCHEMA "${schema}" + GRANT USAGE ON SEQUENCES TO "${firebaseWriterRole}";`, + + `ALTER DEFAULT PRIVILEGES + FOR ROLE "${ownerRole}" + IN SCHEMA "${schema}" + GRANT EXECUTE ON FUNCTIONS TO "${firebaseWriterRole}";`, + + `ALTER DEFAULT PRIVILEGES + FOR ROLE "${ownerRole}" + IN SCHEMA "${schema}" + GRANT SELECT ON TABLES TO "${firebaseReaderRole}";`, + + `ALTER DEFAULT PRIVILEGES + FOR ROLE "${ownerRole}" + IN SCHEMA "${schema}" + GRANT USAGE ON SEQUENCES TO "${firebaseReaderRole}";`, + + `ALTER DEFAULT PRIVILEGES + FOR ROLE "${ownerRole}" + IN SCHEMA "${schema}" + GRANT EXECUTE ON FUNCTIONS TO "${firebaseReaderRole}";`, + ]; +} diff --git a/src/gcp/cloudsql/permissionsSetup.ts b/src/gcp/cloudsql/permissionsSetup.ts new file mode 100644 index 00000000000..87228fb59cc --- /dev/null +++ b/src/gcp/cloudsql/permissionsSetup.ts @@ -0,0 +1,472 @@ +import * as clc from "colorette"; + +import { Options } from "../../options"; +import { + firebaseowner, + firebasewriter, + firebasereader, + ownerRolePermissions, + writerRolePermissions, + readerRolePermissions, + defaultPermissions, + CLOUDSQL_SUPER_USER, + FIREBASE_SUPER_USER, +} from "./permissions"; +import { iamUserIsCSQLAdmin } from "./cloudsqladmin"; +import { logger } from "../../logger"; +import { confirm } from "../../prompt"; +import { FirebaseError } from "../../error"; +import { needProjectId, needProjectNumber } from "../../projectUtils"; +import { executeSqlCmdsAsIamUser, executeSqlCmdsAsSuperUser, getIAMUser } from "./connect"; +import { concat } from "lodash"; +import { getDataConnectP4SA, toDatabaseUser } from "./connect"; +import * as utils from "../../utils"; +import * as cloudSqlAdminClient from "./cloudsqladmin"; + +export type TableMetadata = { + name: string; + owner: string; +}; + +export enum SchemaSetupStatus { + NotSetup = "not-setup", + GreenField = "greenfield", + BrownField = "brownfield", + NotFound = "not-found", // Schema not found +} + +export type SchemaMetadata = { + name: string; + owner: string | null; + tables: TableMetadata[]; + setupStatus: SchemaSetupStatus; +}; + +export const fdcSqlRoleMap = { + owner: firebaseowner, + writer: firebasewriter, + reader: firebasereader, +}; + +// Returns true if "grantedRole" is granted to "granteeRole" and false otherwise. +// Throw an error if commands fails due to another reason like connection issues. +export async function checkSQLRoleIsGranted( + options: Options, + instanceId: string, + databaseId: string, + grantedRole: string, + granteeRole: string, +): Promise { + const checkCmd = ` + DO $$ + DECLARE + role_count INTEGER; + BEGIN + -- Count the number of rows matching the criteria + SELECT COUNT(*) + INTO role_count + FROM + pg_auth_members m + JOIN + pg_roles grantee ON grantee.oid = m.member + JOIN + pg_roles granted ON granted.oid = m.roleid + JOIN + pg_roles grantor ON grantor.oid = m.grantor + WHERE + granted.rolname = '${grantedRole}' + AND grantee.rolname = '${granteeRole}'; + + -- If no rows were found, raise an exception + IF role_count = 0 THEN + RAISE EXCEPTION 'Role "%", is not granted to role "%".', '${grantedRole}', '${granteeRole}'; + END IF; + END $$; +`; + try { + await executeSqlCmdsAsIamUser(options, instanceId, databaseId, [checkCmd], /** silent=*/ true); + return true; + } catch (e) { + // We only return false after we confirm the error is indeed because the role isn't granted. + // Otherwise we propagate the error. + if (e instanceof FirebaseError && e.message.includes("not granted to role")) { + return false; + } + logger.error(`Role Check Failed: ${e}`); + throw e; + } +} + +// Sets up all FDC roles (owner, writer, and reader). +// Granting roles to users is done by the caller. +export async function setupSQLPermissions( + instanceId: string, + databaseId: string, + schemaInfo: SchemaMetadata, + options: Options, + silent: boolean = false, +): Promise { + const logFn = silent + ? logger.debug + : (message: string) => { + return utils.logLabeledBullet("dataconnect", message); + }; + const schema = schemaInfo.name; + // Step 0: Check current user can run setup and upsert IAM / P4SA users + logFn(`Detected schema "${schema}" setup status is ${schemaInfo.setupStatus}. Running setup...`); + + const userIsCSQLAdmin = await iamUserIsCSQLAdmin(options); + if (!userIsCSQLAdmin) { + throw new FirebaseError( + `Missing required IAM permission to setup SQL schemas. SQL schema setup requires 'roles/cloudsql.admin' or an equivalent role.`, + ); + } + + let runGreenfieldSetup = false; + if (schemaInfo.setupStatus === SchemaSetupStatus.GreenField) { + runGreenfieldSetup = true; + logFn( + `Database ${databaseId} has already been setup as greenfield project. Rerunning setup to repair any missing permissions.`, + ); + } + + if (schemaInfo.tables.length === 0) { + runGreenfieldSetup = true; + logFn(`Found no tables in schema "${schema}", assuming greenfield project.`); + } + + // We need to setup the database + if (runGreenfieldSetup) { + const greenfieldSetupCmds = await greenFieldSchemaSetup( + instanceId, + databaseId, + schema, + options, + ); + await executeSqlCmdsAsSuperUser( + options, + instanceId, + databaseId, + greenfieldSetupCmds, + silent, + /** transaction=*/ true, + ); + + logFn(clc.green("Database setup complete.")); + return SchemaSetupStatus.GreenField; + } + + if (options.nonInteractive || options.force) { + throw new FirebaseError( + `Schema "${schema}" isn't set up and can only be set up in interactive mode.`, + ); + } + const currentTablesOwners = [...new Set(schemaInfo.tables.map((t) => t.owner))]; + logFn( + `We found some existing object owners [${currentTablesOwners.join(", ")}] in your cloudsql "${schema}" schema.`, + ); + + const shouldSetupGreenfield = await confirm({ + message: clc.yellow( + "Would you like FDC to handle SQL migrations for you moving forward?\n" + + `This means we will transfer schema and tables ownership to ${firebaseowner(databaseId, schema)}\n` + + "Note: your existing migration tools/roles may lose access.", + ), + default: false, + }); + + if (shouldSetupGreenfield) { + await setupBrownfieldAsGreenfield(instanceId, databaseId, schemaInfo, options, silent); + logger.info(clc.green("Database setup complete.")); // If we do set up, always at least show this line. + logFn( + clc.yellow( + "IMPORTANT: please uncomment 'schemaValidation: \"COMPATIBLE\"' in your dataconnect.yaml file to avoid dropping any existing tables by mistake.", + ), + ); + return SchemaSetupStatus.GreenField; + } else { + logFn( + clc.yellow( + "Setting up database in brownfield mode.\n" + + `Note: SQL migrations can't be done through ${clc.bold("firebase dataconnect:sql:migrate")} in this mode.`, + ), + ); + await brownfieldSqlSetup(instanceId, databaseId, schemaInfo, options, silent); + logFn(clc.green("Brownfield database setup complete.")); + return SchemaSetupStatus.BrownField; + } +} + +export async function greenFieldSchemaSetup( + instanceId: string, + databaseId: string, + schema: string, + options: Options, +) { + // Detect the minimal necessary revokes to avoid errors for users who used the old sql permissions setup. + const revokes = []; + if ( + await checkSQLRoleIsGranted( + options, + instanceId, + databaseId, + "cloudsqlsuperuser", + firebaseowner(databaseId), + ) + ) { + logger.warn( + "Detected cloudsqlsuperuser was previously given to firebase owner, revoking to improve database security.", + ); + revokes.push(`REVOKE "cloudsqlsuperuser" FROM "${firebaseowner(databaseId)}"`); + } + + const user = (await getIAMUser(options)).user; + const projectNumber = await needProjectNumber(options); + const { user: fdcP4SAUser } = toDatabaseUser(getDataConnectP4SA(projectNumber)); + + const sqlRoleSetupCmds = concat( + // For backward compatibality we sometimes need to revoke some roles. + revokes, + + // We shoud make sure schema exists since this setup runs prior to executing the diffs. + [`CREATE SCHEMA IF NOT EXISTS "${schema}"`], + + // Create and setup the owner role permissions. + ownerRolePermissions(databaseId, FIREBASE_SUPER_USER, schema), + + // Create and setup writer role permissions. + writerRolePermissions(databaseId, FIREBASE_SUPER_USER, schema), + + // Create and setup reader role permissions. + readerRolePermissions(databaseId, FIREBASE_SUPER_USER, schema), + + // Grant firebaseowner role to the current IAM user. + `GRANT "${firebaseowner(databaseId, schema)}" TO "${user}"`, + // Grant firebaswriter to the FDC P4SA user + `GRANT "${firebasewriter(databaseId, schema)}" TO "${fdcP4SAUser}"`, + + defaultPermissions(databaseId, schema, firebaseowner(databaseId, schema)), + ); + + return sqlRoleSetupCmds; +} + +export async function getSchemaMetadata( + instanceId: string, + databaseId: string, + schema: string, + options: Options, +): Promise { + // Check if schema exists + const checkSchemaExists = await executeSqlCmdsAsIamUser( + options, + instanceId, + databaseId, + /** cmd=*/ [ + `SELECT pg_get_userbyid(nspowner) + FROM pg_namespace + WHERE nspname = '${schema}';`, + ], + /** silent=*/ true, + ); + if (!checkSchemaExists[0].rows[0]) { + return { + name: schema, + owner: null, + setupStatus: SchemaSetupStatus.NotFound, + tables: [], + }; + } + const schemaOwner = checkSchemaExists[0].rows[0].pg_get_userbyid; + + // Get schema tables + const cmd = `SELECT tablename, tableowner FROM pg_tables WHERE schemaname='${schema}'`; + const res = await executeSqlCmdsAsIamUser( + options, + instanceId, + databaseId, + [cmd], + /** silent=*/ true, + ); + const tables = res[0].rows.map((row) => { + return { + name: row.tablename, + owner: row.tableowner, + }; + }); + + // If firebase writer role doesn't exist -> Schema not setup + const checkRoleExists = async (role: string): Promise => { + const cmd = [`SELECT to_regrole('"${role}"') IS NOT NULL AS exists;`]; + const result = await executeSqlCmdsAsIamUser( + options, + instanceId, + databaseId, + cmd, + /** silent=*/ true, + ); + return result[0].rows[0].exists; + }; + + let setupStatus; + if (!(await checkRoleExists(firebasewriter(databaseId, schema)))) { + setupStatus = SchemaSetupStatus.NotSetup; + } else if ( + tables.every((table) => table.owner === firebaseowner(databaseId, schema)) && + schemaOwner === firebaseowner(databaseId, schema) + ) { + // If schema owner and all table owners are firebaseowner -> Greenfield + setupStatus = SchemaSetupStatus.GreenField; + } else { + // We have determined firebase writer exists but schema/table owner isn't firebaseowner -> Brownfield + setupStatus = SchemaSetupStatus.BrownField; + } + + return { + name: schema, + owner: schemaOwner, + setupStatus, + tables: tables, + }; +} + +function filterTableOwners(schemaInfo: SchemaMetadata, databaseId: string) { + return [...new Set(schemaInfo.tables.map((t) => t.owner))].filter( + (owner) => + owner !== CLOUDSQL_SUPER_USER && owner !== firebaseowner(databaseId, schemaInfo.name), + ); +} + +export async function setupBrownfieldAsGreenfield( + instanceId: string, + databaseId: string, + schemaInfo: SchemaMetadata, + options: Options, + silent: boolean = false, +) { + const schema = schemaInfo.name; + + const firebaseOwnerRole = firebaseowner(databaseId, schema); + const uniqueTablesOwners = filterTableOwners(schemaInfo, databaseId); + + // Grant roles to firebase superuser to avoid missing permissions on tables + const grantOwnersToSuperuserCmds = uniqueTablesOwners.map( + (owner) => `GRANT "${owner}" TO "${FIREBASE_SUPER_USER}"`, + ); + const revokeOwnersFromSuperuserCmds = uniqueTablesOwners.map( + (owner) => `REVOKE "${owner}" FROM "${FIREBASE_SUPER_USER}"`, + ); + + // Step 1: Our usual setup which creates necessary roles, transfers schema ownership, and gives nessary grants. + const greenfieldSetupCmds = await greenFieldSchemaSetup(instanceId, databaseId, schema, options); + + // Step 2: Grant non firebase owners the writer role before changing the table owners. + const grantCmds = uniqueTablesOwners.map( + (owner) => `GRANT "${firebasewriter(databaseId, schema)}" TO "${owner}"`, + ); + + // Step 3: Alter table owners permissions + const alterTableCmds = schemaInfo.tables.map( + (table) => `ALTER TABLE "${schema}"."${table.name}" OWNER TO "${firebaseOwnerRole}";`, + ); + + const setupCmds = [ + ...grantOwnersToSuperuserCmds, + ...greenfieldSetupCmds, + ...grantCmds, + ...alterTableCmds, + ...revokeOwnersFromSuperuserCmds, + ]; + + // Run sql commands + await executeSqlCmdsAsSuperUser( + options, + instanceId, + databaseId, + setupCmds, + silent, + /** transaction */ true, + ); +} + +export async function brownfieldSqlSetup( + instanceId: string, + databaseId: string, + schemaInfo: SchemaMetadata, + options: Options, + silent: boolean = false, +) { + const schema = schemaInfo.name; + + // Step 1: Grant firebasesuperuser access to the original owner + const uniqueTablesOwners = filterTableOwners(schemaInfo, databaseId); + const grantOwnersToFirebasesuperuser = uniqueTablesOwners.map( + (owner) => `GRANT "${owner}" TO "${FIREBASE_SUPER_USER}"`, + ); + const revokeOwnersFromFirebasesuperuser = uniqueTablesOwners.map( + (owner) => `REVOKE "${owner}" FROM "${FIREBASE_SUPER_USER}"`, + ); + + // Step 2: Using firebasesuperuser, setup reader and writer permissions on existing tables and setup default permissions for future tables. + const iamUser = (await getIAMUser(options)).user; + const projectNumber = await needProjectNumber(options); + const { user: fdcP4SAUser } = toDatabaseUser(getDataConnectP4SA(projectNumber)); + + // Step 3: Grant firebase reader and writer roles access to any new tables created by found owner. + const firebaseDefaultPermissions = uniqueTablesOwners.flatMap((owner) => + defaultPermissions(databaseId, schema, owner), + ); + + // Batch execute the previous steps commands + const brownfieldSetupCmds = [ + // Firebase superuser grants + ...grantOwnersToFirebasesuperuser, + // Create and setup writer role permissions. + ...writerRolePermissions(databaseId, FIREBASE_SUPER_USER, schema), + + // Create and setup reader role permissions. + ...readerRolePermissions(databaseId, FIREBASE_SUPER_USER, schema), + + // Grant firebasewriter role to the current IAM user. + `GRANT "${firebasewriter(databaseId, schema)}" TO "${iamUser}"`, + // Grant firebaswriter to the FDC P4SA user + `GRANT "${firebasewriter(databaseId, schema)}" TO "${fdcP4SAUser}"`, + + // Insures firebase roles have access to future tables + ...firebaseDefaultPermissions, + + // Execute revokes to avoid builtin user becoming IAM role + ...revokeOwnersFromFirebasesuperuser, + ]; + + await executeSqlCmdsAsSuperUser( + options, + instanceId, + databaseId, + brownfieldSetupCmds, + silent, + /** transaction=*/ true, + ); +} + +export async function grantRoleTo( + options: Options, + instanceId: string, + databaseId: string, + role: string, + email: string, +): Promise { + // Upsert new user account into the database. + const projectId = needProjectId(options); + const { user, mode } = toDatabaseUser(email); + await cloudSqlAdminClient.createUser(projectId, instanceId, mode, user); + + const fdcSqlRole = fdcSqlRoleMap[role as keyof typeof fdcSqlRoleMap](databaseId); + await executeSqlCmdsAsSuperUser( + options, + instanceId, + databaseId, + /** cmds= */ [`GRANT "${fdcSqlRole}" TO "${user}"`], + /** silent= */ false, + ); +} diff --git a/src/gcp/cloudsql/types.ts b/src/gcp/cloudsql/types.ts new file mode 100644 index 00000000000..50db6be9093 --- /dev/null +++ b/src/gcp/cloudsql/types.ts @@ -0,0 +1,124 @@ +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; + }; + enablePrivatePathForGoogleCloudServices?: 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_18" + | "POSTGRES_17" + | "POSTGRES_16" + | "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/gcp/cloudtasks.spec.ts b/src/gcp/cloudtasks.spec.ts new file mode 100644 index 00000000000..70c817d7d0c --- /dev/null +++ b/src/gcp/cloudtasks.spec.ts @@ -0,0 +1,273 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +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; + const ENDPOINT: backend.Endpoint & backend.TaskQueueTriggered = { + platform: "gcfv2", + id: "id", + region: "region", + project: "project", + entryPoint: "id", + runtime: "nodejs16", + taskQueueTrigger: {}, + }; + + beforeEach(() => { + ct = sinon.stub(cloudtasks); + ct.queueNameForEndpoint.restore(); + ct.queueFromEndpoint.restore(); + ct.triggerFromQueue.restore(); + ct.setEnqueuer.restore(); + ct.upsertQueue.restore(); + }); + + afterEach(() => { + sinon.verifyAndRestore(); + }); + + describe("queueFromEndpoint", () => { + it("handles minimal endpoints", () => { + expect(cloudtasks.queueFromEndpoint(ENDPOINT)).to.deep.equal({ + ...cloudtasks.DEFAULT_SETTINGS, + name: "projects/project/locations/region/queues/id", + }); + }); + + it("handles complex endpoints", () => { + const rateLimits: backend.TaskQueueRateLimits = { + maxConcurrentDispatches: 5, + maxDispatchesPerSecond: 5, + }; + const retryConfig: backend.TaskQueueRetryConfig = { + maxAttempts: 10, + maxDoublings: 9, + maxBackoffSeconds: 60, + maxRetrySeconds: 300, + minBackoffSeconds: 1, + }; + + const ep: backend.Endpoint = { + ...ENDPOINT, + taskQueueTrigger: { + rateLimits, + retryConfig, + invoker: ["robot@"], + }, + }; + expect(cloudtasks.queueFromEndpoint(ep)).to.deep.equal({ + name: "projects/project/locations/region/queues/id", + rateLimits, + 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 = { + name: "projects/p/locations/r/queues/f", + ...cloudtasks.DEFAULT_SETTINGS, + }; + ct.getQueue.resolves(queue); + + await cloudtasks.upsertQueue(queue); + + expect(ct.getQueue).to.have.been.called; + expect(ct.updateQueue).to.not.have.been.called; + expect(ct.purgeQueue).to.not.have.been.called; + }); + + it("updates a non-matching queue", async () => { + const wantQueue: cloudtasks.Queue = { + name: "projects/p/locations/r/queues/f", + ...cloudtasks.DEFAULT_SETTINGS, + rateLimits: { + maxConcurrentDispatches: 20, + }, + }; + const haveQueue: cloudtasks.Queue = { + name: "projects/p/locations/r/queues/f", + ...cloudtasks.DEFAULT_SETTINGS, + }; + ct.getQueue.resolves(haveQueue); + + await cloudtasks.upsertQueue(wantQueue); + + expect(ct.getQueue).to.have.been.called; + expect(ct.updateQueue).to.have.been.called; + expect(ct.purgeQueue).to.not.have.been.called; + }); + + it("purges a disabled queue", async () => { + const wantQueue: cloudtasks.Queue = { + name: "projects/p/locations/r/queues/f", + ...cloudtasks.DEFAULT_SETTINGS, + }; + const haveQueue: cloudtasks.Queue = { + name: "projects/p/locations/r/queues/f", + ...cloudtasks.DEFAULT_SETTINGS, + state: "DISABLED", + }; + ct.getQueue.resolves(haveQueue); + + await cloudtasks.upsertQueue(wantQueue); + + expect(ct.getQueue).to.have.been.called; + expect(ct.updateQueue).to.have.been.called; + expect(ct.purgeQueue).to.have.been.called; + }); + }); + + describe("setEnqueuer", () => { + const NAME = "projects/p/locations/r/queues/f"; + const ADMIN_BINDING: iam.Binding = { + role: "roles/cloudtasks.admin", + members: ["user:sundar@google.com"], + }; + // Not that anyone should actually make these public, + // it makes for easier testing. + const PUBLIC_ENQUEUER_BINDING: iam.Binding = { + role: "roles/cloudtasks.enqueuer", + members: ["allUsers"], + }; + it("can blind-write", async () => { + await cloudtasks.setEnqueuer(NAME, ["private"], /* assumeEmpty= */ true); + expect(ct.getIamPolicy).to.not.have.been.called; + expect(ct.setIamPolicy).to.not.have.been.called; + + await cloudtasks.setEnqueuer(NAME, ["public"], /* assumeEmpty= */ true); + expect(ct.getIamPolicy).to.not.have.been.called; + expect(ct.setIamPolicy).to.have.been.calledWith(NAME, { + bindings: [PUBLIC_ENQUEUER_BINDING], + etag: "", + version: 3, + }); + }); + + it("preserves other roles", async () => { + ct.getIamPolicy.resolves({ + bindings: [ADMIN_BINDING, PUBLIC_ENQUEUER_BINDING], + etag: "", + version: 3, + }); + + await cloudtasks.setEnqueuer(NAME, ["private"]); + expect(ct.getIamPolicy).to.have.been.called; + expect(ct.setIamPolicy).to.have.been.calledWith(NAME, { + bindings: [ADMIN_BINDING], + etag: "", + version: 3, + }); + }); + + it("noops existing matches", async () => { + ct.getIamPolicy.resolves({ + bindings: [ADMIN_BINDING, PUBLIC_ENQUEUER_BINDING], + etag: "", + version: 3, + }); + + await cloudtasks.setEnqueuer(NAME, ["public"]); + expect(ct.getIamPolicy).to.have.been.called; + expect(ct.setIamPolicy).to.not.have.been.called; + }); + + it("can insert an enqueuer binding", async () => { + ct.getIamPolicy.resolves({ + bindings: [ADMIN_BINDING], + etag: "", + version: 3, + }); + + await cloudtasks.setEnqueuer(NAME, ["public"]); + expect(ct.getIamPolicy).to.have.been.called; + expect(ct.setIamPolicy).to.have.been.calledWith(NAME, { + bindings: [ADMIN_BINDING, PUBLIC_ENQUEUER_BINDING], + etag: "", + version: 3, + }); + }); + + it("can resolve conflicts", async () => { + ct.getIamPolicy.onCall(0).resolves({ + bindings: [ADMIN_BINDING], + etag: "", + version: 3, + }); + ct.getIamPolicy.onCall(1).resolves({ + bindings: [ADMIN_BINDING], + etag: "2", + version: 3, + }); + ct.setIamPolicy.onCall(0).rejects({ context: { response: { statusCode: 429 } } }); + + await cloudtasks.setEnqueuer(NAME, ["public"]); + expect(ct.getIamPolicy).to.have.been.calledTwice; + expect(ct.setIamPolicy).to.have.been.calledTwice; + expect(ct.setIamPolicy).to.have.been.calledWithMatch(NAME, { + etag: "2", + }); + }); + }); +}); diff --git a/src/gcp/cloudtasks.ts b/src/gcp/cloudtasks.ts new file mode 100644 index 00000000000..f3c10decea6 --- /dev/null +++ b/src/gcp/cloudtasks.ts @@ -0,0 +1,298 @@ +import * as proto from "./proto"; + +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(), + auth: true, + apiVersion: API_VERSION, +}); + +export interface AppEngineRouting { + service: string; + version: string; + instance: string; + host: string; +} + +export interface RateLimits { + maxDispatchesPerSecond?: number | null; + maxConcurrentDispatches?: number | null; +} + +export interface RetryConfig { + maxAttempts?: number | null; + maxRetryDuration?: proto.Duration | null; + minBackoff?: proto.Duration | null; + maxBackoff?: proto.Duration | null; + maxDoublings?: number | null; +} + +export interface StackdriverLoggingConfig { + samplingRatio: number; +} + +export type State = "RUNNING" | "PAUSED" | "DISABLED"; + +export interface Queue { + name: string; + appEngienRoutingOverride?: AppEngineRouting; + rateLimits?: RateLimits; + retryConfig?: RetryConfig; + state?: State; +} + +/** + * The client-side defaults we set for a queue. + * Unlike most APIs, Cloud Tasks doesn't omit fields which + * have default values. This means when we create a queue without + * maxDoublings, for example, it will be returned as a queue with + * maxDoublings set to 16. By setting our in-memory queue to the + * server-side defaults we'll be able to more accurately see whether + * our in-memory representation matches the current state on upsert + * and avoid a PUT call. + * NOTE: we explicitly _don't_ have the same default for + * retryConfig.maxAttempts. The server-side default is effectively + * infinite, which can cause customers to have runaway bills if the + * function crashes. We settled on a Firebase default of 3 since + * infrastructure errors also count against this limit and 1-(1-99.9%)^3 + * means we'll have 9-9s reliability of invoking the customer's + * function at least once (though unfortuantely this math assumes + * failures are independent events, which is generally untrue). + */ +export const DEFAULT_SETTINGS: Omit = { + rateLimits: { + maxConcurrentDispatches: 1000, + maxDispatchesPerSecond: 500, + }, + state: "RUNNING", + retryConfig: { + maxDoublings: 16, + maxAttempts: 3, + maxBackoff: "3600s", + minBackoff: "0.100s", + }, +}; + +/** Create a Queue that matches the spec. */ +export async function createQueue(queue: Queue): Promise { + const path = queue.name.substring(0, queue.name.lastIndexOf("/")); + const res = await client.post(path, queue); + return res.body; +} + +/** Get the Queue for a given name. */ +export async function getQueue(name: string): Promise { + const res = await client.get(name); + return res.body; +} + +/** Updates a queue to match the passed parameter. */ +export async function updateQueue(queue: Partial & { name: string }): Promise { + const res = await client.patch(queue.name, queue, { + queryParams: { updateMask: proto.fieldMasks(queue).join(",") }, + }); + return res.body; +} + +/** Ensures a queue exists with the given spec. Returns true if created and false if updated/left alone. */ +export async function upsertQueue(queue: Queue): Promise { + try { + // Here and throughout we use module.exports to ensure late binding & enable stubs in unit tests. + const existing = await (module.exports.getQueue as typeof getQueue)(queue.name); + if (JSON.stringify(queue) === JSON.stringify(existing)) { + return false; + } + + if (existing.state === "DISABLED") { + await (module.exports.purgeQueue as typeof purgeQueue)(queue.name); + } + + await (module.exports.updateQueue as typeof updateQueue)(queue); + return false; + } catch (err: any) { + if (err?.context?.response?.statusCode === 404) { + await (module.exports.createQueue as typeof createQueue)(queue); + return true; + } + throw err; + } +} + +/** Purges all messages in a queue with a given name. */ +export async function purgeQueue(name: string): Promise { + await client.post(`${name}:purge`); +} + +/** Deletes a queue with a given name. */ +export async function deleteQueue(name: string): Promise { + await client.delete(name); +} + +/** Set the IAM policy of a given queue. */ +export async function setIamPolicy(name: string, policy: iam.Policy): Promise { + const res = await client.post<{ policy: iam.Policy }, iam.Policy>(`${name}:setIamPolicy`, { + policy, + }); + return res.body; +} + +/** Returns the IAM policy of a given queue. */ +export async function getIamPolicy(name: string): Promise { + const res = await client.post(`${name}:getIamPolicy`); + return res.body; +} + +const ENQUEUER_ROLE = "roles/cloudtasks.enqueuer"; + +/** Ensures that the invoker policy is set for a given queue. */ +export async function setEnqueuer( + name: string, + invoker: string[], + assumeEmpty = false, +): Promise { + let existing: iam.Policy; + if (assumeEmpty) { + existing = { + bindings: [], + etag: "", + version: 3, + }; + } else { + existing = await (module.exports.getIamPolicy as typeof getIamPolicy)(name); + } + + const [, project] = name.split("/"); + const invokerMembers = proto.getInvokerMembers(invoker, project); + while (true) { + const policy: iam.Policy = { + bindings: existing.bindings.filter((binding) => binding.role !== ENQUEUER_ROLE), + etag: existing.etag, + version: existing.version, + }; + + if (invokerMembers.length) { + policy.bindings.push({ role: ENQUEUER_ROLE, members: invokerMembers }); + } + + if (JSON.stringify(policy) === JSON.stringify(existing)) { + return; + } + + try { + await (module.exports.setIamPolicy as typeof setIamPolicy)(name, policy); + return; + } catch (err: any) { + // Re-fetch on conflict + if (err?.context?.response?.statusCode === 429) { + existing = await (module.exports.getIamPolicy as typeof getIamPolicy)(name); + continue; + } + throw err; + } + } +} + +/** The name of the Task Queue we will use for this endpoint. */ +export function queueNameForEndpoint( + endpoint: backend.Endpoint & backend.TaskQueueTriggered, +): string { + return `projects/${endpoint.project}/locations/${endpoint.region}/queues/${endpoint.id}`; +} + +/** Creates an API type from an Endpoint type */ +export function queueFromEndpoint(endpoint: backend.Endpoint & backend.TaskQueueTriggered): Queue { + const queue: Required = { + ...(JSON.parse(JSON.stringify(DEFAULT_SETTINGS)) as Omit, "name">), + name: queueNameForEndpoint(endpoint), + }; + if (endpoint.taskQueueTrigger.rateLimits) { + proto.copyIfPresent( + queue.rateLimits, + endpoint.taskQueueTrigger.rateLimits, + "maxConcurrentDispatches", + "maxDispatchesPerSecond", + ); + } + if (endpoint.taskQueueTrigger.retryConfig) { + proto.copyIfPresent( + queue.retryConfig, + endpoint.taskQueueTrigger.retryConfig, + "maxAttempts", + "maxDoublings", + ); + proto.convertIfPresent( + queue.retryConfig, + endpoint.taskQueueTrigger.retryConfig, + "maxRetryDuration", + "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..304fc86afb4 --- /dev/null +++ b/src/gcp/computeEngine.ts @@ -0,0 +1,25 @@ +import { logger } from "../logger"; +import { computeOrigin } from "../api"; +import { Client } from "../apiv2"; + +const computeClient = () => new Client({ urlPrefix: computeOrigin() }); +const defaultServiceAccountCache: Record = {}; +/** Returns the default compute engine service agent */ +export async function getDefaultServiceAccount(projectNumber: string): Promise { + if (defaultServiceAccountCache[projectNumber]) { + return defaultServiceAccountCache[projectNumber]; + } + try { + const res = await computeClient().get<{ defaultServiceAccount: string }>( + `compute/v1/projects/${projectNumber}`, + ); + defaultServiceAccountCache[projectNumber] = res.body.defaultServiceAccount; + return res.body.defaultServiceAccount; + } catch (err: any) { + const bestGuess = `${projectNumber}-compute@developer.gserviceaccount.com`; + logger.debug( + `unable to look up default compute service account. Falling back to ${bestGuess}. Error: ${JSON.stringify(err)}`, + ); + return bestGuess; + } +} diff --git a/src/gcp/devConnect.ts b/src/gcp/devConnect.ts new file mode 100644 index 00000000000..2598cc88bfa --- /dev/null +++ b/src/gcp/devConnect.ts @@ -0,0 +1,455 @@ +import { Client } from "../apiv2"; +import { developerConnectOrigin, developerConnectP4SADomain } from "../api"; +import { generateServiceIdentityAndPoll } from "./serviceusage"; +import { FirebaseError } from "../error"; +import { extractRepoSlugFromUri } from "../apphosting/githubConnections"; + +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 InstallationType = "user" | "organization"; + +export interface Installation { + id: string; + name: string; + type: InstallationType; +} + +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; +} + +interface GitRepositoryLinkReadToken { + token: string; + expirationTime: string; + gitUsername: string; +} + +export interface GitRepositoryLinkDetails { + repoLink: GitRepositoryLink; + owner: string; + repo: string; + readToken: GitRepositoryLinkReadToken; +} + +/** + * 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; +} + +/** + * Lists all branches for a given repo. Returns a set of branches. + */ +export async function listAllBranches(repoLinkName: string): Promise> { + const branches = new Set(); + + const getNextPage = async (pageToken = ""): Promise => { + const res = await client.get<{ + refNames: string[]; + nextPageToken?: string; + }>(`${repoLinkName}:fetchGitRefs`, { + queryParams: { + refType: "BRANCH", + pageSize: PAGE_SIZE_MAX, + pageToken, + }, + }); + if (Array.isArray(res.body.refNames)) { + res.body.refNames.forEach((branch) => { + branches.add(branch); + }); + } + if (res.body.nextPageToken) { + await getNextPage(res.body.nextPageToken); + } + }; + + await getNextPage(); + + return branches; +} + +/** + * Fetch all GitHub installations available to the oauth token referenced by + * the given connection + */ +export async function fetchGitHubInstallations( + projectId: string, + location: string, + connectionId: string, +): Promise { + const name = `projects/${projectId}/locations/${LOCATION_OVERRIDE ?? location}/connections/${connectionId}:fetchGitHubInstallations`; + const res = await client.get<{ installations: Installation[] }>(name); + + return res.body.installations; +} + +/** + * Splits a Git Repository Link resource name into its parts. + */ +export function parseGitRepositoryLinkName(gitRepositoryLinkName: string): { + projectName: string; + location: string; + connectionName: string; + id: string; +} { + const [, projectName, , location, , connectionName, , id] = gitRepositoryLinkName.split("/"); + return { projectName, location, connectionName, id }; +} + +/** + * 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; +} + +/** + * Fetch the read token for a GitRepositoryLink + */ +export async function fetchGitRepositoryLinkReadToken( + projectId: string, + location: string, + connectionId: string, + gitRepositoryLinkId: string, +): Promise { + const name = `projects/${projectId}/locations/${LOCATION_OVERRIDE ?? location}/connections/${connectionId}/gitRepositoryLinks/${gitRepositoryLinkId}:fetchReadToken`; + const res = await client.post(name); + return res.body; +} + +/** + * sorts the given list of connections by create_time from earliest to latest + */ +export function sortConnectionsByCreateTime(connections: Connection[]) { + return connections.sort((a, b) => { + return Date.parse(a.createTime!) - Date.parse(b.createTime!); + }); +} + +/** + * 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", + ); +} + +/** + * Given a DevConnect GitRepositoryLink resource name, extracts the + * names of the connection and git repository link + */ +export function extractGitRepositoryLinkComponents(path: string): { + connection: string | null; + gitRepoLink: string | null; +} { + const connectionMatch = /connections\/([^\/]+)/.exec(path); + const repositoryMatch = /gitRepositoryLinks\/([^\/]+)/.exec(path); + + const connection = connectionMatch ? connectionMatch[1] : null; + const gitRepoLink = repositoryMatch ? repositoryMatch[1] : null; + + return { connection, gitRepoLink }; +} + +/** + * Given a GitRepositoryLink resource path, retrieves the GitRepositoryLink resource, + * owner, repository name, and read token for the Git repository + */ +export async function getRepoDetailsFromBackend( + projectId: string, + location: string, + gitRepoLinkPath: string, +): Promise { + const { connection, gitRepoLink } = extractGitRepositoryLinkComponents(gitRepoLinkPath); + if (!connection || !gitRepoLink) { + throw new FirebaseError( + `Failed to extract connection or repository resource names from backend repository name.`, + ); + } + const repoLink = await getGitRepositoryLink(projectId, location, connection, gitRepoLink); + const repoSlug = extractRepoSlugFromUri(repoLink.cloneUri); + const owner = repoSlug?.split("/")[0]; + const repo = repoSlug?.split("/")[1]; + if (!owner || !repo) { + throw new FirebaseError("Failed to parse owner and repo from git repository link"); + } + const readToken = await fetchGitRepositoryLinkReadToken( + projectId, + location, + connection, + gitRepoLink, + ); + + return { + repoLink, + owner, + repo, + readToken, + }; +} diff --git a/src/gcp/devconnect.spec.ts b/src/gcp/devconnect.spec.ts new file mode 100644 index 00000000000..e11a14dee5f --- /dev/null +++ b/src/gcp/devconnect.spec.ts @@ -0,0 +1,198 @@ +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 gitRepoLinkId = "git-repo-link"; + const connectionsRequestPath = `projects/${projectId}/locations/${location}/connections`; + const gitRepoLinkPath = `projects/${projectId}/locations/${location}/connections/${connectionId}/gitRepositoryLinks/${gitRepoLinkId}`; + + function mockConnection(id: string, createTime: string): devconnect.Connection { + return { + name: `projects/${projectId}/locations/${location}/connections/${id}`, + disabled: false, + createTime: createTime, + updateTime: "1", + installationState: { + stage: "COMPLETE", + message: "complete", + actionUri: "https://google.com", + }, + reconciling: false, + }; + } + + 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]); + }); + }); + + describe("listAllBranches", () => { + it("interates through all pages and returns a single list and map", async () => { + const firstBranch = "test"; + const secondBranch = "test2"; + const thirdBranch = "test3"; + + get + .onFirstCall() + .returns({ + body: { + refNames: [firstBranch], + nextPageToken: "someToken", + }, + }) + .onSecondCall() + .returns({ + body: { + refNames: [secondBranch], + nextPageToken: "someToken2", + }, + }) + .onThirdCall() + .returns({ + body: { + refNames: [thirdBranch], + }, + }); + + const branches = await devconnect.listAllBranches( + "/projects/blah/locations/us-central1/connections/blah", + ); + expect(get).callCount(3); + + expect(branches).to.deep.equal(new Set([firstBranch, secondBranch, thirdBranch])); + }); + describe("sortConnectionsByCreateTime", () => { + it("sorts the list of connections from earliest to latest", () => { + const firstConnection = mockConnection("conn1", "2024-07-03T16:55:35.974826076Z"); + const secondConnection = mockConnection("conn2", "2024-07-02T17:26:16.000154754Z"); + const thirdConnection = mockConnection("conn3", "2024-07-01T21:32:29.992488750Z"); + const fourthConnection = mockConnection("conn4", "2024-07-02T17:41:25.366819004Z"); + const fifthConnection = mockConnection("conn5", "2024-07-02T17:22:07.171899854Z"); + const sixthConnection = mockConnection("conn6", "2024-07-01T21:31:10.148324612Z"); + + const connections = [ + firstConnection, + secondConnection, + thirdConnection, + fourthConnection, + fifthConnection, + sixthConnection, + ]; + + expect(devconnect.sortConnectionsByCreateTime(connections)).to.deep.equal([ + sixthConnection, + thirdConnection, + fifthConnection, + secondConnection, + fourthConnection, + firstConnection, + ]); + }); + }); + }); + + describe("extractGitRepositoryLinkComponents", () => { + it("correctly extracts the connection and git repository link ID", () => { + expect(devconnect.extractGitRepositoryLinkComponents(gitRepoLinkPath)).to.deep.equal({ + connection: "apphosting-connection", + gitRepoLink: "git-repo-link", + }); + }); + }); +}); diff --git a/src/gcp/docker.ts b/src/gcp/docker.ts new file mode 100644 index 00000000000..623f14c3e8d --- /dev/null +++ b/src/gcp/docker.ts @@ -0,0 +1,125 @@ +// Note: unlike Google APIs, the documentation for the GCR API is +// actually the Docker REST API. This can be found at +// https://docs.docker.com/registry/spec/api/ +// This API is _very_ complex in its entirety and is very subtle (e.g. tags and digests +// are both strings and can both be put in the same route to get completely different +// response document types). +// This file will only implement a minimal subset as needed. +import { FirebaseError } from "../error"; +import * as api from "../apiv2"; + +// A mapping from geographical region to subdomain, useful for Container Registry +export const GCR_SUBDOMAIN_MAPPING: Record = { + "us-west1": "us", + "us-west2": "us", + "us-west3": "us", + "us-west4": "us", + "us-central1": "us", + "us-central2": "us", + "us-east1": "us", + "us-east4": "us", + "northamerica-northeast1": "us", + "southamerica-east1": "us", + "europe-west1": "eu", + "europe-west2": "eu", + "europe-west3": "eu", + "europe-west4": "eu", + "europe-west5": "eu", + "europe-west6": "eu", + "europe-central2": "eu", + "europe-north1": "eu", + "asia-east1": "asia", + "asia-east2": "asia", + "asia-northeast1": "asia", + "asia-northeast2": "asia", + "asia-northeast3": "asia", + "asia-south1": "asia", + "asia-southeast2": "asia", + "australia-southeast1": "asia", +}; + +// A Digest is a string in the format :. For example: +// sha256:146d8c9dff0344fb01417ef28673ed196e38215f3c94837ae733d3b064ba439e +export type Digest = string; +export type Tag = string; + +export interface Tags { + name: string; + tags: string[]; + + // These fields are not documented in the Docker API but are + // present in the GCR API. + manifest: Record; + child: string[]; +} + +export interface ImageInfo { + // times are string milliseconds + timeCreatedMs: string; + timeUploadedMs: string; + tag: string[]; + mediaType: string; + imageSizeBytes: string; + layerId: string; +} + +interface ErrorsResponse { + errors?: { + code: string; + message: string; + details: unknown; + }[]; +} + +function isErrors(response: unknown): response is ErrorsResponse { + // Artifact registry will return 202 w/ no body on some success cases. + return !!response && Object.prototype.hasOwnProperty.call(response, "errors"); +} + +const API_VERSION = "v2"; + +export class Client { + readonly client: api.Client; + + constructor(origin: string) { + this.client = new api.Client({ + apiVersion: API_VERSION, + auth: true, + urlPrefix: origin, + }); + } + + async listTags(path: string): Promise { + const response = await this.client.get(`${path}/tags/list`); + if (isErrors(response.body)) { + throw new FirebaseError(`Failed to list GCR tags at ${path}`, { + children: response.body.errors, + }); + } + return response.body; + } + + async deleteTag(path: string, tag: Tag): Promise { + const response = await this.client.delete(`${path}/manifests/${tag}`); + if (!response.body) { + return; + } + if (response.body.errors?.length !== 0) { + throw new FirebaseError(`Failed to delete tag ${tag} at path ${path}`, { + children: response.body.errors, + }); + } + } + + async deleteImage(path: string, digest: Digest): Promise { + const response = await this.client.delete(`${path}/manifests/${digest}`); + if (!response.body) { + return; + } + 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 9ab727a1888..f395708c68c 100644 --- a/src/gcp/firedata.ts +++ b/src/gcp/firedata.ts @@ -1,58 +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[]; } /** - * Create a new Realtime Database instance - * @param projectId Project from which you want to get the ruleset. - * @param instanceName The name for the new Realtime Database instance. + * Fetches the Terms of Service status for the logged in user. */ -export async function createDatabaseInstance( - projectNumber: number, - instanceName: string -): Promise { - const response = await api.request("POST", `/v1/projects/${projectNumber}/databases`, { - auth: true, - origin: api.firedataOrigin, - json: { - instance: instanceName, - }, - }); - if (response.status === 200) { - return response.body.instance; - } - return _handleErrorResponse(response); +export async function getTosStatus(): Promise { + const res = await client.get("accessmanagement/tos:getStatus"); + return res.body; } -/** - * Create a new Realtime Database instance - * @param projectId Project from which you want to get the ruleset. - * @param instanceName The name for the new Realtime Database instance. - */ -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; +/** 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..18092dcaef7 100644 --- a/src/gcp/firestore.ts +++ b/src/gcp/firestore.ts @@ -1,41 +1,274 @@ -import { firestoreOriginOrEmulator } from "../api"; -import * as apiv2 from "../apiv2"; +import { firestoreOrigin } 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(), }); +function getClient(emulatorUrl?: string) { + if (emulatorUrl) { + return new Client({ + auth: true, + apiVersion: "v1", + urlPrefix: emulatorUrl, + }); + } + return prodOnlyClient; +} + +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; +} + +interface FieldFilter { + field: { fieldPath: string }; + op: + | "OPERATOR_UNSPECIFIED" + | "LESS_THAN" + | "LESS_THAN_OR_EQUAL" + | "GREATER_THAN" + | "GREATER_THAN_OR_EQUAL" + | "EQUAL" + | "NOT_EQUAL" + | "ARRAY_CONTAINS" + | "ARRAY_CONTAINS_ANY" + | "IN" + | "NOT_IN"; + value: FirestoreValue; +} + +interface CompositeFilter { + op: "OR" | "AND"; + filters: { + fieldFilter?: FieldFilter; + compositeFilter?: CompositeFilter; + }[]; +} + +export interface StructuredQuery { + from: { collectionId: string; allDescendants: boolean }[]; + where?: { + compositeFilter?: CompositeFilter; + fieldFilter?: FieldFilter; + }; + orderBy?: { + field: { fieldPath: string }; + direction: "ASCENDING" | "DESCENDING" | "DIRECTION_UNSPECIFIED"; + }[]; + limit?: number; +} + +interface RunQueryResponse { + document?: FirestoreDocument; + readTime?: 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[]; +} + +// Based on https://cloud.google.com/firestore/docs/reference/rest/v1/Value +export type FirestoreValue = + | { nullValue: null } + | { booleanValue: boolean } + | { integerValue: string } // Keep as string as per REST API, handle conversion in toJson + | { doubleValue: number } + | { timestampValue: string } // ISO 8601 format + | { stringValue: string } + | { bytesValue: string } // base64 encoded + | { referenceValue: string } // Full resource name string + | { geoPointValue: { latitude: number; longitude: number } } + | { arrayValue: { values?: FirestoreValue[] } } + | { mapValue: { fields?: Record } }; + +// Based on https://cloud.google.com/firestore/docs/reference/rest/v1/projects.databases.documents#Document +export interface FirestoreDocument { + name: string; // Resource name: projects/{projectId}/databases/{databaseId}/documents/{document_path} + fields?: { [key: string]: FirestoreValue }; + createTime: string; // Timestamp format + updateTime: string; // Timestamp format +} + +/** + * 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, + emulatorUrl?: string, +): Promise { + const apiClient = getClient(emulatorUrl); + 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 { - const url = "projects/" + project + "/databases/(default)/documents:listCollectionIds"; +export function listCollectionIds( + project: string, + databaseId: string = "(default)", + emulatorUrl?: string, +): Promise { + const apiClient = getClient(emulatorUrl); + const url = `projects/${project}/databases/${databaseId}/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 || []; }); } +/** + * Get multiple documents by path. + * @param {string} project the Google Cloud project ID. + * @param {string[]} paths The document paths to fetch. + * @return {Promise<{ documents: FirestoreDocument[]; missing: string[] }>} a promise for an array of firestore documents and missing documents in the request. + */ +export async function getDocuments( + project: string, + paths: string[], + databaseId: string = "(default)", + emulatorUrl?: string, +): Promise<{ documents: FirestoreDocument[]; missing: string[] }> { + const apiClient = getClient(emulatorUrl); + const basePath = `projects/${project}/databases/${databaseId}/documents`; + const url = `${basePath}:batchGet`; + const fullPaths = paths.map((p) => `${basePath}/${p}`); + const res = await apiClient.post< + { documents: string[] }, + { found?: FirestoreDocument; missing?: string }[] + >(url, { documents: fullPaths }); + const out: { documents: FirestoreDocument[]; missing: string[] } = { documents: [], missing: [] }; + res.body.map((r) => (r.missing ? out.missing.push(r.missing) : out.documents.push(r.found!))); + return out; +} + +/** + * Get documents based on a simple query to a collection. + * @param {string} project the Google Cloud project ID. + * @param {StructuredQuery} structuredQuery The structured query of the request including filters and ordering. + * @return {Promise<{ documents: FirestoreDocument[] }>} a promise for an array of retrieved firestore documents. + */ +export async function queryCollection( + project: string, + structuredQuery: StructuredQuery, + databaseId: string = "(default)", + emulatorUrl?: string, +): Promise<{ documents: FirestoreDocument[] }> { + const apiClient = getClient(emulatorUrl); + const basePath = `projects/${project}/databases/${databaseId}/documents`; + const url = `${basePath}:runQuery`; + try { + const res = await apiClient.post< + { + structuredQuery: StructuredQuery; + explainOptions: { analyze: boolean }; + newTransaction: { readOnly: { readTime: string } }; + // readTime: string; + }, + RunQueryResponse[] + >(url, { + structuredQuery: structuredQuery, + explainOptions: { analyze: true }, + newTransaction: { readOnly: { readTime: new Date().toISOString() } }, + // readTime: new Date().toISOString(), + }); + const out: { documents: FirestoreDocument[] } = { documents: [] }; + res.body.map((r) => { + if (r.document) { + out.documents.push(r.document); + } + }); + return out; + } catch (err: FirebaseError | unknown) { + // Used to get the URL to automatically build the composite index. + // Otherwise a generic 400 error is returned to the user without info. + throw JSON.stringify(err); + } +} + /** * Delete a single Firestore document. * * 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, emulatorUrl?: string): Promise { + const apiClient = getClient(emulatorUrl); + return apiClient.delete(doc.name); } /** @@ -43,19 +276,145 @@ 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 { - const url = "projects/" + project + "/databases/(default)/documents:commit"; +export async function deleteDocuments( + project: string, + docs: any[], + databaseId: string = "(default)", + emulatorUrl?: string, +): Promise { + const apiClient = getClient(emulatorUrl); + const url = `projects/${project}/databases/${databaseId}/documents:commit`; const writes = docs.map((doc) => { return { delete: doc.name }; }); const data = { writes }; - const res = await _CLIENT.post(url, data); + const res = await apiClient.post(url, data, { + retries: 10, + retryCodes: [429, 409, 503], + retryMaxTimeout: 20 * 1000, + }); 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..74bf9264bee 100644 --- a/src/gcp/iam.ts +++ b/src/gcp/iam.ts @@ -1,9 +1,14 @@ -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`; +} // IAM Policy // https://cloud.google.com/resource-manager/reference/rest/Shared.Types/Policy @@ -31,6 +36,12 @@ export interface ServiceAccount { disabled: boolean; } +export interface Role { + name: string; + title?: string; + description?: string; +} + export interface ServiceAccountKey { name: string; privateKeyType: string; @@ -43,9 +54,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 +71,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 +155,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 +173,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 +215,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..a4948a40b85 --- /dev/null +++ b/src/gcp/identityPlatform.ts @@ -0,0 +1,223 @@ +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; + beforeSendEmail?: BlockingFunctionsEventDetails; + beforeSendSms?: 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 90bb95113ba..00000000000 --- a/src/gcp/index.js +++ /dev/null @@ -1,11 +0,0 @@ -"use strict"; - -module.exports = { - 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/gcp/k8s.spec.ts b/src/gcp/k8s.spec.ts new file mode 100644 index 00000000000..667b67b484d --- /dev/null +++ b/src/gcp/k8s.spec.ts @@ -0,0 +1,30 @@ +import { expect } from "chai"; +import * as k8s from "./k8s"; + +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(k8s.mebibytes("1000k")).to.equal((1000 * Bytes.KB) / Bytes.MiB); + expect(k8s.mebibytes("1.5M")).to.equal((1.5 * Bytes.MB) / Bytes.MiB); + expect(k8s.mebibytes("1G")).to.equal(Bytes.GB / Bytes.MiB); + }); + + it("Should handle binary SI units", () => { + expect(k8s.mebibytes("1Mi")).to.equal(Bytes.MiB / Bytes.MiB); + expect(k8s.mebibytes("1Gi")).to.equal(Bytes.GiB / Bytes.MiB); + }); + + it("Should handle no unit", () => { + expect(k8s.mebibytes("100000")).to.equal(100000 / Bytes.MiB); + expect(k8s.mebibytes("1e9")).to.equal(1e9 / Bytes.MiB); + expect(k8s.mebibytes("1.5E6")).to.equal((1.5 * 1e6) / Bytes.MiB); + }); +}); diff --git a/src/gcp/k8s.ts b/src/gcp/k8s.ts new file mode 100644 index 00000000000..338e7184120 --- /dev/null +++ b/src/gcp/k8s.ts @@ -0,0 +1,69 @@ +// AvailableMemory suffixes and their byte count. +type MemoryUnit = "" | "k" | "M" | "G" | "T" | "Ki" | "Mi" | "Gi" | "Ti"; +const BYTES_PER_UNIT: Record = { + "": 1, + k: 1e3, + M: 1e6, + G: 1e9, + T: 1e12, + Ki: 1 << 10, + Mi: 1 << 20, + Gi: 1 << 30, + Ti: 1 << 40, +}; +/** + * Returns the float-precision number of Mebi(not Mega)bytes in a + * Kubernetes-style quantity + * 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 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) { + throw new Error(`Invalid memory quantity "${memory}""`); + } + const quantity = Number.parseFloat(matches[1]); + let bytes: number; + if (matches[5]) { + bytes = quantity * Math.pow(10, Number.parseFloat(matches[5])); + } else { + const suffix = matches[3] || ""; + bytes = quantity * BYTES_PER_UNIT[suffix as MemoryUnit]; + } + return bytes / (1 << 20); +} + +export interface PlaintextEnvVar { + name: string; + value: string; +} + +export interface SecretEnvVar { + name: string; + valueSource: { + secretKeyRef: { + secret: string; // Secret name + version?: string; // Optional version, defaults to latest + }; + }; +} + +export type EnvVar = PlaintextEnvVar | SecretEnvVar; + +export type ResourceType = "cpu" | "memory" | "nvidia.com/gpu"; + +export interface Container { + name?: string; + image: string; + command?: string[]; + args?: string[]; + env: EnvVar[]; + workingDir?: string; + resources: { + limits: Record; + }; + cpuIdle?: boolean; + startupCpuBoost?: boolean; +} diff --git a/src/gcp/location.ts b/src/gcp/location.ts new file mode 100644 index 00000000000..e55501daaac --- /dev/null +++ b/src/gcp/location.ts @@ -0,0 +1,59 @@ +/** Google Cloud Storage multi-region mapping (https://cloud.google.com/storage/docs/locations#location-mr) */ +export const MULTI_REGION_MAPPING: Record = { + /** us */ + "us-central1": "us", + "us-east1": "us", + "us-east4": "us", + "us-west1": "us", + "us-west2": "us", + "us-west3": "us", + "us-west4": "us", + /** eu */ + "europe-central2": "eu", + "europe-north1": "eu", + "europe-west1": "eu", + "europe-west3": "eu", + "europe-west4": "eu", + "europe-west5": "eu", + /** asia */ + "asia-east1": "asia", + "asia-east2": "asia", + "asia-northeast1": "asia", + "asia-northeast2": "asia", + "asia-northeast3": "asia", + "asia-south1": "asia", + "asia-south2": "asia", + "asia-southeast1": "asia", + "asia-southeast2": "asia", +}; + +/** Google Cloud Storage dual-region mapping (https://cloud.google.com/storage/docs/locations#location-dr) */ +export const DUAL_REGION_MAPPING: Record = { + /** asia1 */ + "asia-northeast1": "asia1", + "asia-northeast2": "asia1", + /** eur4 */ + "europe-north1": "eur4", + "europe-west4": "eur4", + /** nam4 */ + "us-central1": "nam4", + "us-east1": "nam4", +}; + +/** + * Helper function to determine if the given region is inside the multi-region or dual-region location. + * This is helpful for determining if a specific region maps to a Google Cloud Storage location. + * @param region the specific geographical region name (ex~ us-west1, europe-central2, ...) + * @param location the multi-region or dual-region location name (ex~ us, asia, nam4, ...) + * @returns true if the region is in the location, otherwise false + */ +export function regionInLocation(region: string, location: string): boolean { + // check whether the region matched the location, + // if the location is a metro that matches the region, or if the location is a geo that matches the region + region = region.toLowerCase(); + location = location.toLowerCase(); + if (MULTI_REGION_MAPPING[region] === location || DUAL_REGION_MAPPING[region] === location) { + return true; + } + return false; +} diff --git a/src/gcp/proto.spec.ts b/src/gcp/proto.spec.ts new file mode 100644 index 00000000000..b03788575b4 --- /dev/null +++ b/src/gcp/proto.spec.ts @@ -0,0 +1,253 @@ +import { expect } from "chai"; +import * as proto from "./proto"; + +describe("proto", () => { + describe("duration", () => { + it("should convert from seconds to duration", () => { + expect(proto.durationFromSeconds(1)).to.equal("1s"); + expect(proto.durationFromSeconds(0.5)).to.equal("0.5s"); + }); + + it("should convert from duration to seconds", () => { + expect(proto.secondsFromDuration("1s")).to.equal(1); + expect(proto.secondsFromDuration("0.5s")).to.equal(0.5); + }); + }); + + describe("copyIfPresent", () => { + interface DestType { + foo?: string; + baz?: string; + } + interface SrcType { + foo?: string; + bar?: string; + baz?: string; + } + it("should copy present fields", () => { + const dest: DestType = {}; + const src: SrcType = { foo: "baz" }; + proto.copyIfPresent(dest, src, "foo"); + expect(dest.foo).to.equal("baz"); + }); + + it("should not copy missing fields", () => { + const dest: DestType = {}; + const src: SrcType = {}; + proto.copyIfPresent(dest, src, "foo"); + expect("foo" in dest).to.be.false; + }); + + it("should support variadic params", () => { + const dest: DestType = {}; + const src: SrcType = { foo: "baz", baz: "quz" }; + proto.copyIfPresent(dest, src, "foo", "baz"); + expect(dest).to.deep.equal(src); + }); + + // Compile-time check for type safety net + /* eslint-disable @typescript-eslint/no-unused-vars */ + const dest: DestType = {}; + const src: SrcType = { bar: "baz" }; + /* eslint-enable @typescript-eslint/no-unused-vars */ + // This line should fail to compile when uncommented + // proto.copyIfPresent(dest, src, "baz"); + }); + + describe("renameIfPresent", () => { + interface DestType { + destFoo?: string; + } + + interface SrcType { + srcFoo?: string; + bar?: string; + } + + it("should copy present fields", () => { + const dest: DestType = {}; + const src: SrcType = { srcFoo: "baz" }; + proto.renameIfPresent(dest, src, "destFoo", "srcFoo"); + expect(dest.destFoo).to.equal("baz"); + }); + + it("should not copy missing fields", () => { + const dest: DestType = {}; + const src: SrcType = {}; + proto.renameIfPresent(dest, src, "destFoo", "srcFoo"); + expect("destFoo" in dest).to.be.false; + }); + + 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.convertIfPresent(dest, src, "destFoo", "srcFoo", (str: string) => str + " transformed"); + expect(dest.destFoo).to.equal("baz transformed"); + }); + + // Compile-time check for type safety net + /* eslint-disable @typescript-eslint/no-unused-vars */ + const dest: DestType = {}; + const src: SrcType = { bar: "baz" }; + /* eslint-enable @typescript-eslint/no-unused-vars */ + // These line should fail to compile when uncommented + // proto.renameIfPresent(dest, src, "destFoo", "srcccFoo"); + // proto.renameIfPresent(dest, src, "desFoo", "srcFoo"); + }); + + describe("fieldMasks", () => { + it("should copy simple fields", () => { + const obj = { + number: 1, + string: "foo", + array: ["hello", "world"], + } as const; + expect(proto.fieldMasks(obj).sort()).to.deep.equal(["number", "string", "array"].sort()); + }); + + it("should handle empty values", () => { + const obj = { + undefined: undefined, + null: null, + empty: {}, + } as const; + + expect(proto.fieldMasks(obj).sort()).to.deep.equal(["undefined", "null", "empty"].sort()); + }); + + it("should nest into objects", () => { + const obj = { + top: "level", + nested: { + key: "value", + }, + } as const; + expect(proto.fieldMasks(obj).sort()).to.deep.equal(["top", "nested.key"].sort()); + }); + + it("should include empty objects", () => { + const obj = { + failurePolicy: { + retry: {}, + }, + } as const; + expect(proto.fieldMasks(obj)).to.deep.equal(["failurePolicy.retry"]); + }); + + it("should support map types", () => { + // 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", + }, + nested: { + anotherMap: { + userDefined: "value", + }, + }, + }; + + const fieldMasks = proto.fieldMasks(obj, "map", "nested.anotherMap", "missing"); + expect(fieldMasks.sort()).to.deep.equal(["map", "nested.anotherMap"].sort()); + }); + }); + + describe("getInvokerMembers", () => { + it("should return empty array with private invoker", () => { + const invokerMembers = proto.getInvokerMembers(["private"], "project"); + + expect(invokerMembers).to.deep.eq([]); + }); + + it("should return allUsers with public invoker", () => { + const invokerMembers = proto.getInvokerMembers(["public"], "project"); + + expect(invokerMembers).to.deep.eq(["allUsers"]); + }); + + it("should return formatted service accounts with invoker array", () => { + const invokerMembers = proto.getInvokerMembers( + ["service-account1@", "service-account2@project.iam.gserviceaccount.com"], + "project", + ); + + expect(invokerMembers).to.deep.eq([ + "serviceAccount:service-account1@project.iam.gserviceaccount.com", + "serviceAccount:service-account2@project.iam.gserviceaccount.com", + ]); + }); + }); + + describe("formatServiceAccount", () => { + it("should throw error on empty service account string", () => { + expect(() => proto.formatServiceAccount("", "project")).to.throw(); + }); + + it("should throw error on badly formed service account string", () => { + expect(() => proto.formatServiceAccount("not-a-service-account", "project")).to.throw(); + }); + + it("should return formatted service account from invoker ending with @", () => { + const serviceAccount = "service-account@"; + const project = "project"; + + const formatted = proto.formatServiceAccount(serviceAccount, project); + + expect(formatted).to.eq(`serviceAccount:${serviceAccount}${project}.iam.gserviceaccount.com`); + }); + + it("should return formatted service account from invoker with full service account", () => { + const serviceAccount = "service-account@project.iam.gserviceaccount.com"; + + const formatted = proto.formatServiceAccount(serviceAccount, "project"); + + 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 new file mode 100644 index 00000000000..1c89782a504 --- /dev/null +++ b/src/gcp/proto.ts @@ -0,0 +1,271 @@ +import { FirebaseError } from "../error"; +import { RecursiveKeyOf } from "../metaprogramming"; + +/** + * A type alias used to annotate interfaces as using a google.protobuf.Duration. + * This type is parsed/encoded as a string of seconds + the "s" prefix. + */ +export type Duration = string; + +/** Get the number of seconds in a google.protobuf.Duration. */ +export function secondsFromDuration(d: Duration): number { + return +d.slice(0, d.length - 1); +} + +/** Get a google.protobuf.Duration for a number of seconds. */ +export function durationFromSeconds(s: number): Duration { + return `${s}s`; +} + +/** + * Throws unless obj contains at no more than one key in "fields". + * This verifies that proto oneof constraints, which can't be codified in JSON, are honored + * @param typename The name of the proto type for error messages + * @param obj The proto object that should have a "oneof" constraint + * @param oneof The name of the field that should be a "oneof" for error messages + * @param fields The fields that are defiend as a oneof in the proto definition + */ +export function assertOneOf(typename: string, obj: T, oneof: string, ...fields: (keyof T)[]) { + const defined = []; + for (const key of fields) { + const value = obj[key]; + if (typeof value !== "undefined" && value != null) { + defined.push(key); + } + } + + if (defined.length > 1) { + throw new FirebaseError( + `Invalid ${typename} definition. ${oneof} can only have one field defined, but found ${defined.join( + ",", + )}`, + ); + } +} + +/** + * 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( + dest: 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]!; + } +} + +/** + * 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, + 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; + } + const [dest, src, destKey, srcKey, converter] = args; + if (Object.prototype.hasOwnProperty.call(src, srcKey)) { + dest[destKey] = converter(src[srcKey]); + } +} + +/** 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. + * If the proto definition has a map, keys will be user-defined + * and should not be recursed. Specify this by adding a field mask prefix for doNotRecurseIn. + * @param object The proto JSON object. If a field should be explicitly deleted, it should be + * set to `undefined`. This allows field masks to pick it up but JSON.stringify + * to drop it. + * @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: T, + ...doNotRecurseIn: Array> +): string[] { + const masks: string[] = []; + fieldMasksHelper([], object, doNotRecurseIn, masks); + return masks; +} + +function fieldMasksHelper( + prefixes: string[], + cursor: unknown, + doNotRecurseIn: string[], + 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; + } + + const entries = Object.entries(cursor); + // An empty object (e.g. CloudFunction.httpsTrigger) is an explicit object. + // This is needed for protobuf.Empty + if (entries.length === 0) { + masks.push(prefixes.join(".")); + return; + } + + for (const [key, value] of entries) { + const newPrefixes = [...prefixes, key]; + if (doNotRecurseIn.includes(newPrefixes.join("."))) { + masks.push(newPrefixes.join(".")); + continue; + } + fieldMasksHelper(newPrefixes, value, doNotRecurseIn, masks); + } +} + +/** + * 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 + * @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[] { + if (invoker.includes("private")) { + return []; + } + if (invoker.includes("public")) { + return ["allUsers"]; + } + return invoker.map((inv) => formatServiceAccount(inv, projectId)); +} + +/** + * Formats the service account to be used with IAM API calls, a vaild service account string is + * '{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 + * @param removeTypePrefix remove type prefix in the formatted service account + * @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, + removeTypePrefix: boolean = false, +): string { + if (serviceAccount.length === 0) { + throw new FirebaseError("Service account cannot be an empty string"); + } + if (!serviceAccount.includes("@")) { + throw new FirebaseError( + "Service account must be of the form 'service-account@' or 'service-account@{project-id}.iam.gserviceaccount.com'", + ); + } + + const prefix = removeTypePrefix ? "" : "serviceAccount:"; + + if (serviceAccount.endsWith("@")) { + const suffix = `${projectId}.iam.gserviceaccount.com`; + return `${prefix}${serviceAccount}${suffix}`; + } + return `${prefix}${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 50bb638b712..4958435c6f6 100644 --- a/src/gcp/pubsub.ts +++ b/src/gcp/pubsub.ts @@ -1,18 +1,60 @@ -import * as api from "../api"; +import { Client } from "../apiv2"; +import { pubsubOrigin } from "../api"; +import * as proto from "./proto"; -const VERSION = "v1"; +const API_VERSION = "v1"; -export function createTopic(name: string): Promise { - return api.request("PUT", `/${VERSION}/${name}`, { - auth: true, - origin: api.pubsubOrigin, - data: { labels: { deployment: "firebase-schedule" } }, - }); +const client = new Client({ + urlPrefix: pubsubOrigin(), + auth: true, + apiVersion: API_VERSION, +}); + +export type Encoding = "JSON" | "BINARY"; + +export interface MessageStoragePolicy { + allowedPersistenceRegions: string[]; +} + +export interface SchemaSettings { + schema: string; + encoding: Encoding; } -export function deleteTopic(name: string): Promise { - return api.request("DELETE", `/${VERSION}/${name}`, { - auth: true, - origin: api.pubsubOrigin, - }); +export interface Topic { + name: string; + labels?: Record; + messageStoragePolicy?: MessageStoragePolicy; + kmsKeyName?: string; + schemaSettings?: SchemaSettings; + messageRetentionDuration?: proto.Duration; } + +export async function createTopic(topic: Topic): Promise { + const result = await client.put(topic.name, topic); + return result.body; +} + +export async function getTopic(name: string): Promise { + const result = await client.get(name); + return result.body; +} + +export async function updateTopic(topic: Topic): Promise { + const queryParams = { + updateMask: proto.fieldMasks(topic).join(","), + }; + const result = await client.patch(topic.name, topic, { queryParams }); + return result.body; +} + +export async function deleteTopic(name: string): Promise { + await client.delete(name); +} + +// NOTE: We currently don't need or have specFromTopic. +// backend.ExistingBackend infers actual topics by the fact that it sees a function +// with a scheduled annotation. This may not be good enough when we're +// using Run, because we'll have to to query multiple resources (e.g. triggers) +// Were we to get a standalone Topic, we wouldn't have any idea how to set the +// "target service" since that is part of the subscription. 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 new file mode 100644 index 00000000000..837e436f946 --- /dev/null +++ b/src/gcp/run.ts @@ -0,0 +1,369 @@ +import { Client } from "../apiv2"; +import { FirebaseError } from "../error"; +import { runOrigin } from "../api"; +import * as proto from "./proto"; +import * as iam from "./iam"; +import { backoff } from "../throttler/throttler"; +import { logger } from "../logger"; +import { listEntries, LogEntry } from "./cloudlogging"; + +const API_VERSION = "v1"; + +const client = new Client({ + urlPrefix: runOrigin(), + auth: true, + apiVersion: API_VERSION, +}); + +export const LOCATION_LABEL = "cloud.googleapis.com/location"; + +// Unfortuantely, Omit<> doesn't allow supbath, so it's hard to have a reasonable API that +// declares all mandatory fields as mandatory and then accepts an Omit<> for update types. + +export interface ObjectMetadata { + name: string; + + // Must be the project ID or project number + namespace: string; + + labels?: Record; + + // Not supported in Cloud Run: + generate_name?: string; + deletionGracePeriodSeconds?: number; + finalizers?: string[]; + clusterName?: string; + + // Output only: + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + createTime?: string; + + // Onput only; not supported by Cloud Run + ownerReference?: unknown; + deleteTime?: string; +} + +export interface Addressable { + url: string; +} + +export interface Condition { + type: string; + status: string; + reason: string; + message: string; + lastTransitionTime: string; + severity: "Error" | "Warning" | "Info"; +} + +export interface ServiceSpec { + template: RevisionTemplate; + traffic: TrafficTarget[]; +} + +// All fields in ServiceStatus are output only so we will assume +// that an input Service will just Omit<"status"> +export interface ServiceStatus { + observedGeneration: number; + conditions: Condition[]; + latestReadyRevisionName: string; + latestCreatedRevisionName: string; + traffic: TrafficTarget[]; + url: string; + address: Addressable; +} + +export interface Service { + apiVersion: "serving.knative.dev/v1"; + 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: Partial; + spec: RevisionSpec; +} + +export interface TrafficTarget { + configurationName?: string; + // RevisionName can be used to target a specific revision, + // or customers can set latestRevision = true + revisionName?: string; + latestRevision?: boolean; + percent?: number; // optional when tagged + tag?: string; + + // Output only: + // Displayed when TrafficTarget is part of a status and forbidden + // when TrafficTarget is part of spec. + url?: string; +} + +export interface IamPolicy { + version?: number; + bindings?: iam.Binding[]; + auditConfigs?: Record[]; + 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: 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: any) { + throw new FirebaseError(`Failed to replace Run service ${name}`, { + original: err, + status: err?.context?.response?.statusCode, + }); + } +} + +/** + * Sets the IAM policy of a Service + * @param name Fully qualified name of the Service. + * @param policy The [policy](https://cloud.google.com/run/docs/reference/rest/v1/projects.locations.services/setIamPolicy) to set. + */ +export async function setIamPolicy( + name: string, + policy: iam.Policy, + httpClient: Client = client, +): 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 + // proto. + interface Request { + policy: iam.Policy; + updateMask: string; + } + try { + await httpClient.post(`${name}:setIamPolicy`, { + policy, + updateMask: proto.fieldMasks(policy).join(","), + }); + } 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, +): Promise { + try { + const response = await httpClient.get(`${serviceName}:getIamPolicy`); + return response.body; + } catch (err: any) { + throw new FirebaseError(`Failed to get the IAM Policy on the Service ${serviceName}`, { + original: err, + }); + } +} + +/** + * Gets the current IAM policy for the run service and overrides the invoker role with the supplied invoker members + * @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 +) { + 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 bindings = [{ role: invokerRole, members: invokerMembers }]; + + const policy: iam.Policy = { + bindings: bindings, + etag: "", + version: 3, + }; + + await setIamPolicy(serviceName, policy, httpClient); +} + +/** + * Gets the current IAM policy for the run service and overrides the invoker role with the supplied invoker members + * @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 +) { + 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, + ); + if ( + currentInvokerBinding && + JSON.stringify(currentInvokerBinding.members.sort()) === JSON.stringify(invokerMembers.sort()) + ) { + return; + } + + const bindings = (currentPolicy.bindings || []).filter((binding) => binding.role !== invokerRole); + bindings.push({ + role: invokerRole, + members: invokerMembers, + }); + + const policy: iam.Policy = { + bindings: bindings, + etag: currentPolicy.etag || "", + version: 3, + }; + await setIamPolicy(serviceName, policy, httpClient); +} + +/** + * Fetches recent logs for a given Cloud Run service using the Cloud Logging API. + * @param projectId The Google Cloud project ID. + * @param serviceId The resource name of the Cloud Run service. + * @return A promise that resolves with the log entries. + */ +export async function fetchServiceLogs(projectId: string, serviceId: string): Promise { + const filter = `resource.type="cloud_run_revision" AND resource.labels.service_name="${serviceId}"`; + const pageSize = 100; + const order = "desc"; + + try { + const { entries } = await listEntries(projectId, filter, pageSize, order); + return entries; + } catch (err: any) { + throw new FirebaseError(`Failed to fetch logs for Cloud Run service ${serviceId}`, { + original: err, + status: (err as any)?.context?.response?.statusCode, + }); + } +} 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/runv2.spec.ts b/src/gcp/runv2.spec.ts new file mode 100644 index 00000000000..4d5f0b200f6 --- /dev/null +++ b/src/gcp/runv2.spec.ts @@ -0,0 +1,455 @@ +import { expect } from "chai"; + +import * as runv2 from "./runv2"; +import * as backend from "../deploy/functions/backend"; +import { latest } from "../deploy/functions/runtimes/supported"; +import { CODEBASE_LABEL } from "../functions/constants"; + +describe("runv2", () => { + const PROJECT_ID = "project-id"; + const LOCATION = "us-central1"; + const SERVICE_ID = "functionid"; // TODO: use other normalization method if/when implemented. + const FUNCTION_ID = "functionId"; // Logical function ID + const IMAGE_URI = "gcr.io/project/image:latest"; + + const BASE_ENDPOINT_RUN: Omit = { + platform: "run", + id: FUNCTION_ID, + project: PROJECT_ID, + region: LOCATION, + entryPoint: FUNCTION_ID, + runtime: latest("nodejs"), + availableMemoryMb: 256, + cpu: 1, + }; + + const BASE_RUN_SERVICE: Omit = { + name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${SERVICE_ID}`, + labels: { + [runv2.RUNTIME_LABEL]: latest("nodejs"), + [runv2.CLIENT_NAME_LABEL]: "firebase-functions", + }, + annotations: { + [runv2.FIREBASE_FUNCTION_METADTA_ANNOTATION]: `{"functionId":"${FUNCTION_ID}"}`, + }, + template: { + containers: [ + { + name: "worker", + image: IMAGE_URI, + env: [ + { + name: runv2.FUNCTION_TARGET_ENV, + value: FUNCTION_ID, + }, + { + name: runv2.FUNCTION_SIGNATURE_TYPE_ENV, + value: "http", + }, + ], + resources: { + limits: { + cpu: "1", + memory: "256Mi", + }, + startupCpuBoost: true, + cpuIdle: true, + }, + }, + ], + containerConcurrency: backend.DEFAULT_CONCURRENCY, + }, + client: "cli-firebase", + }; + + describe("serviceFromEndpoint", () => { + it("should copy a minimal endpoint", () => { + const endpoint: backend.Endpoint = { + ...BASE_ENDPOINT_RUN, + httpsTrigger: {}, + }; + + expect(runv2.serviceFromEndpoint(endpoint, IMAGE_URI)).to.deep.equal(BASE_RUN_SERVICE); + }); + + it("should handle different codebase", () => { + const endpoint: backend.Endpoint = { + ...BASE_ENDPOINT_RUN, + codebase: "my-codebase", + httpsTrigger: {}, + }; + const expectedServiceInput: Omit = { + ...BASE_RUN_SERVICE, + name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${FUNCTION_ID.toLowerCase()}`, + labels: { + ...BASE_RUN_SERVICE.labels, + [CODEBASE_LABEL]: "my-codebase", + }, + }; + expect(runv2.serviceFromEndpoint(endpoint, IMAGE_URI)).to.deep.equal(expectedServiceInput); + }); + + it("should copy environment variables", () => { + const endpoint: backend.Endpoint = { + ...BASE_ENDPOINT_RUN, + httpsTrigger: {}, + environmentVariables: { FOO: "bar" }, + }; + const expectedServiceInput = JSON.parse( + JSON.stringify({ + ...BASE_RUN_SERVICE, + name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${FUNCTION_ID.toLowerCase()}`, + }), + ); + expectedServiceInput.template.containers[0].env.unshift({ name: "FOO", value: "bar" }); + + expect(runv2.serviceFromEndpoint(endpoint, IMAGE_URI)).to.deep.equal(expectedServiceInput); + }); + + it("should copy secret environment variables", () => { + const endpoint: backend.Endpoint = { + ...BASE_ENDPOINT_RUN, + httpsTrigger: {}, + secretEnvironmentVariables: [ + { key: "MY_SECRET", secret: "secret-name", projectId: PROJECT_ID, version: "1" }, + ], + }; + const expectedServiceInput = JSON.parse( + JSON.stringify({ + ...BASE_RUN_SERVICE, + name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${FUNCTION_ID.toLowerCase()}`, + }), + ); + expectedServiceInput.template.containers[0].env.unshift({ + name: "MY_SECRET", + valueSource: { secretKeyRef: { secret: "secret-name", version: "1" } }, + }); + expect(runv2.serviceFromEndpoint(endpoint, IMAGE_URI)).to.deep.equal(expectedServiceInput); + }); + + it("should set min/max instances annotations", () => { + const endpoint: backend.Endpoint = { + ...BASE_ENDPOINT_RUN, + httpsTrigger: {}, + minInstances: 1, + maxInstances: 10, + }; + const expectedServiceInput = JSON.parse( + JSON.stringify({ + ...BASE_RUN_SERVICE, + name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${FUNCTION_ID.toLowerCase()}`, + }), + ); + expectedServiceInput.scaling = { + minInstanceCount: 1, + maxInstanceCount: 10, + }; + + expect(runv2.serviceFromEndpoint(endpoint, IMAGE_URI)).to.deep.equal(expectedServiceInput); + }); + + it("should set concurrency", () => { + const endpoint: backend.Endpoint = { + ...BASE_ENDPOINT_RUN, + httpsTrigger: {}, + concurrency: 50, + }; + const expectedServiceInput = JSON.parse( + JSON.stringify({ + ...BASE_RUN_SERVICE, + name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${FUNCTION_ID.toLowerCase()}`, + }), + ); + expectedServiceInput.template.containerConcurrency = 50; + + expect(runv2.serviceFromEndpoint(endpoint, IMAGE_URI)).to.deep.equal(expectedServiceInput); + }); + + it("should set memory and CPU", () => { + const endpoint: backend.Endpoint = { + ...BASE_ENDPOINT_RUN, + httpsTrigger: {}, + availableMemoryMb: 512, + cpu: 2, + }; + const expectedServiceInput = JSON.parse( + JSON.stringify({ + ...BASE_RUN_SERVICE, + name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${FUNCTION_ID.toLowerCase()}`, + }), + ); + expectedServiceInput.template.containers[0].resources.limits.memory = "512Mi"; + expectedServiceInput.template.containers[0].resources.limits.cpu = "2"; + + expect(runv2.serviceFromEndpoint(endpoint, IMAGE_URI)).to.deep.equal(expectedServiceInput); + }); + + it("should remove deployment-tool label", () => { + const endpoint: backend.Endpoint = { + ...BASE_ENDPOINT_RUN, + httpsTrigger: {}, + labels: { "deployment-tool": "firebase-cli" }, + }; + const result = runv2.serviceFromEndpoint(endpoint, IMAGE_URI); + expect(result.labels?.["deployment-tool"]).to.be.undefined; + expect(result.labels?.[runv2.CLIENT_NAME_LABEL]).to.equal("firebase-functions"); + }); + }); + + describe("endpointFromService", () => { + it("should copy a minimal service", () => { + const service: Omit = { + ...BASE_RUN_SERVICE, + name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${SERVICE_ID}`, + labels: { + [runv2.RUNTIME_LABEL]: latest("nodejs"), + }, + annotations: { + ...BASE_RUN_SERVICE.annotations, + [runv2.FUNCTION_ID_ANNOTATION]: FUNCTION_ID, // Using FUNCTION_ID_ANNOTATION as primary source for id + [runv2.FUNCTION_TARGET_ANNOTATION]: "customEntryPoint", + }, + template: { + containers: [ + { + name: "worker", + image: IMAGE_URI, + resources: { + limits: { + cpu: "1", + memory: "256Mi", + }, + cpuIdle: true, + startupCpuBoost: true, + }, + }, + ], + }, + }; + + const expectedEndpoint: backend.Endpoint = { + platform: "run", + id: FUNCTION_ID, + project: PROJECT_ID, + region: LOCATION, + runtime: latest("nodejs"), + entryPoint: "customEntryPoint", + availableMemoryMb: 256, + cpu: 1, + httpsTrigger: {}, + labels: { + [runv2.RUNTIME_LABEL]: latest("nodejs"), + }, + environmentVariables: {}, + secretEnvironmentVariables: [], + }; + + expect(runv2.endpointFromService(service)).to.deep.equal(expectedEndpoint); + }); + + it("should detect a service that's GCF managed", () => { + const service: Omit = { + ...BASE_RUN_SERVICE, + name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${SERVICE_ID}`, + labels: { + [runv2.RUNTIME_LABEL]: latest("nodejs"), + [runv2.CLIENT_NAME_LABEL]: "cloud-functions", // This indicates it's GCF managed + }, + annotations: { + ...BASE_RUN_SERVICE.annotations, + [runv2.FUNCTION_ID_ANNOTATION]: FUNCTION_ID, // Using FUNCTION_ID_ANNOTATION as primary source for id + [runv2.FUNCTION_TARGET_ANNOTATION]: "customEntryPoint", + }, + template: { + containers: [ + { + name: "worker", + image: IMAGE_URI, + resources: { + limits: { + cpu: "1", + memory: "256Mi", + }, + }, + }, + ], + }, + }; + + const expectedEndpoint: backend.Endpoint = { + platform: "gcfv2", + id: FUNCTION_ID, + project: PROJECT_ID, + region: LOCATION, + runtime: latest("nodejs"), + entryPoint: "customEntryPoint", + availableMemoryMb: 256, + cpu: 1, + httpsTrigger: {}, + labels: { + [runv2.RUNTIME_LABEL]: latest("nodejs"), + [runv2.CLIENT_NAME_LABEL]: "cloud-functions", + }, + environmentVariables: {}, + secretEnvironmentVariables: [], + }; + + expect(runv2.endpointFromService(service)).to.deep.equal(expectedEndpoint); + }); + + it("should derive id from FIREBASE_FUNCTIONS_METADATA if present", () => { + const service: Omit = { + ...BASE_RUN_SERVICE, + name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${SERVICE_ID}`, + labels: { + [runv2.RUNTIME_LABEL]: latest("nodejs"), + }, + annotations: { + [runv2.FIREBASE_FUNCTION_METADTA_ANNOTATION]: `{"functionId":"firebaseMetadataId"}`, + [runv2.FUNCTION_TARGET_ANNOTATION]: "targetId", + [runv2.FUNCTION_ID_ANNOTATION]: "annotationId", + }, + template: { + containers: [ + { + name: "worker", + image: IMAGE_URI, + resources: { limits: { cpu: "1", memory: "256Mi" } }, + }, + ], + }, + }; + const result = runv2.endpointFromService(service); + expect(result.id).to.equal("firebaseMetadataId"); + expect(result.entryPoint).to.equal("targetId"); + }); + + it("should derive id from FUNCTION_TARGET_ANNOTATION if FUNCTION_ID_ANNOTATION is missing", () => { + const service: Omit = { + ...BASE_RUN_SERVICE, + name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${SERVICE_ID}`, + labels: { + [runv2.RUNTIME_LABEL]: latest("nodejs"), + }, + annotations: { + ...BASE_RUN_SERVICE.annotations, + [runv2.FUNCTION_TARGET_ANNOTATION]: FUNCTION_ID, // This will be used for id and entryPoint + }, + template: { + containers: [ + { + name: "worker", + image: IMAGE_URI, + resources: { limits: { cpu: "1", memory: "256Mi" } }, + }, + ], + }, + }; + const result = runv2.endpointFromService(service); + expect(result.id).to.equal(FUNCTION_ID); + expect(result.entryPoint).to.equal(FUNCTION_ID); + }); + + it("should derive id from service name part if FUNCTION_ID_ANNOTATION and FUNCTION_TARGET_ANNOTATION are missing", () => { + const service: Omit = { + ...BASE_RUN_SERVICE, + name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${SERVICE_ID}`, + labels: { + [runv2.RUNTIME_LABEL]: latest("nodejs"), + }, + annotations: { + // No FUNCTION_ID_ANNOTATION or FUNCTION_TARGET_ANNOTATION + }, + template: { + containers: [ + { + name: "worker", + image: IMAGE_URI, + resources: { limits: { cpu: "1", memory: "256Mi" } }, + }, + ], + }, + }; + const result = runv2.endpointFromService(service); + expect(result.id).to.equal(SERVICE_ID); + expect(result.entryPoint).to.equal(SERVICE_ID); + }); + + it("should copy env vars and secrets", () => { + const service: runv2.Service = JSON.parse(JSON.stringify(BASE_RUN_SERVICE)); + service.template.containers![0].env = [ + { name: "FOO", value: "bar" }, + { + name: "MY_SECRET", + valueSource: { + secretKeyRef: { + secret: `projects/${PROJECT_ID}/secrets/secret-name`, + version: "1", + }, + }, + }, + ]; + + const result = runv2.endpointFromService(service); + expect(result.environmentVariables).to.deep.equal({ FOO: "bar" }); + expect(result.secretEnvironmentVariables).to.deep.equal([ + { key: "MY_SECRET", secret: "secret-name", projectId: PROJECT_ID, version: "1" }, + ]); + }); + + it("should copy concurrency, min/max instances", () => { + const service: runv2.Service = JSON.parse(JSON.stringify(BASE_RUN_SERVICE)); + service.template.containerConcurrency = 10; + service.scaling = { + minInstanceCount: 2, + maxInstanceCount: 5, + }; + + const result = runv2.endpointFromService(service); + expect(result.concurrency).to.equal(10); + expect(result.minInstances).to.equal(2); + expect(result.maxInstances).to.equal(5); + }); + + it("should handle missing optional fields gracefully", () => { + const service: runv2.Service = { + name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${SERVICE_ID}`, + generation: 1, + template: { + containers: [ + { + name: "worker", + image: IMAGE_URI, + resources: { limits: { memory: "128Mi", cpu: "0.5" } }, // Minimal resources + }, + ], + // No containerConcurrency, no serviceAccount + }, + // No labels, no annotations + createTime: new Date().toISOString(), + updateTime: new Date().toISOString(), + creator: "test@example.com", + lastModifier: "test@example.com", + etag: "test-etag", + }; + + const expectedEndpoint: backend.Endpoint = { + platform: "run", + id: SERVICE_ID, // Derived from service name + project: PROJECT_ID, + region: LOCATION, + runtime: latest("nodejs"), // Default runtime + entryPoint: SERVICE_ID, // No FUNCTION_TARGET_ANNOTATION + availableMemoryMb: 128, + cpu: 0.5, + httpsTrigger: {}, + labels: {}, + environmentVariables: {}, + secretEnvironmentVariables: [], + // concurrency, minInstances, maxInstances will be undefined + }; + + expect(runv2.endpointFromService(service)).to.deep.equal(expectedEndpoint); + }); + }); +}); diff --git a/src/gcp/runv2.ts b/src/gcp/runv2.ts new file mode 100644 index 00000000000..ce4c0b7e2ed --- /dev/null +++ b/src/gcp/runv2.ts @@ -0,0 +1,602 @@ +import { Client } from "../apiv2"; +import { FirebaseError } from "../error"; + +// TODO: Consider making this use REP in the future so we can be used by more +// customers. +import { cloudbuildOrigin, runOrigin } from "../api"; +import * as proto from "./proto"; +import { assertImplements, RecursiveKeyOf } from "../metaprogramming"; +import { LongRunningOperation, pollOperation } from "../operation-poller"; +import * as backend from "../deploy/functions/backend"; +import { CODEBASE_LABEL } from "../functions/constants"; +import { EnvVar, mebibytes, PlaintextEnvVar, SecretEnvVar } from "./k8s"; +import { latest, Runtime } from "../deploy/functions/runtimes/supported"; +import { logger } from ".."; +import { partition } from "../functional"; + +export const API_VERSION = "v2"; + +const client = new Client({ + urlPrefix: runOrigin(), + auth: true, + apiVersion: API_VERSION, +}); + +export interface Scaling { + // N.B. Intentionally omitting revision min/max instance; we should + // never use them. + overflowScaling?: boolean; + minInstanceCount?: number; + maxInstanceCount?: number; +} + +export interface Container { + name: string; + image: string; + command?: string[]; + args?: string[]; + env?: EnvVar[]; + resources?: { + limits?: { + cpu?: string; // e.g. "1", "2", "4" + memory?: string; // e.g. "256Mi", "512Mi", "1Gi" + ["nvidia.com/gpu"]?: string; + }; + startupCpuBoost?: boolean; // If true, the container will get a CPU boost during startup. + // N.B. This defaults to true if resources is not set and must manually be set to true if it is set. + cpuIdle?: boolean; // If true, the container will be allowed to idle CPU when not processing requests. + }; + // Lots more. Most intereeseting is baseImageUri and maybe buildInfo. +} +export interface RevisionTemplate { + revision?: string; + labels?: Record; + annotations?: Record; + // N.B. NEVER set minInstanceCount on this version of scaling or the instances will always be running + // if there is any traffic tag that points to the revision. Service-level scaling divides the min instances + // proportionally by traffic percentage. + scaling?: Scaling; + vpcAccess?: { + connector?: string; + egress?: "ALL_TRAFFIC" | "PRIVATE_RANGES_ONLY"; + networkinterfaces?: Array<{ + network?: string; + subnetwork?: string; + tags?: string[]; + }>; + }; + timeout?: proto.Duration; + serviceAccount?: string; + containers?: Container[]; + containerConcurrency?: number; +} + +export interface BuildConfig { + name: string; + sourceLocation: string; + functionTarget?: string; + enableAutomaticUpdates?: boolean; + environmentVariables?: Record; + serviceAccount?: string; +} + +// NOTE: This is a minmal copy of Cloud Run needed for our current API usage. +// Add more as needed. +// TODO: Can consider a helper where we have a second RecursiveKeysOf field for +// fields that are optional in input types but we always set them (e.g. empty record) +// in output APIs. +export interface Service { + name: string; + description?: string; + generation: number; + labels?: Record; + annotations?: Record; + tags?: Record; + createTime: string; + updateTime: string; + creator: string; + lastModifier: string; + launchStage?: string; + + scaling?: Scaling; + + // In the proto definition, but not what we use to actually track this it seems? + client?: string; + clientVersion?: string; + + etag: string; + template: RevisionTemplate; + invokerIamDisabled?: boolean; + // Is this redundant with the Build API? + buildConfig?: BuildConfig; +} + +export type ServiceOutputFields = + | "generation" + | "createTime" + | "updateTime" + | "creator" + | "lastModifier" + | "etag"; + +assertImplements>(); + +export interface StorageSource { + bucket: string; + object: string; + generation?: string; +} + +export interface BuildpacksBuild { + // Deprecated, presumedly in favor of baseImage? + runtime?: string; + functionTarget?: string; + cacheImageUrl?: string; + baseImage?: string; + + // NOTE: build-time environment variables, which are not currently used. + environmentVariables?: Record; + + enableAutomaticUpdates?: boolean; + projectDescriptor?: string; +} + +export interface Build { + runtime?: string; + functionTarget?: string; + storageSource: StorageSource; + imageUri: string; + buildpacksBuild: BuildpacksBuild; +} + +export interface SubmitBuildResponse { + buildOperation: string; + baseImageUri?: string; + baseImageWarning?: string; +} + +export async function submitBuild( + projectId: string, + location: string, + build: Build, +): Promise { + const res = await client.post( + `/projects/${projectId}/locations/${location}/builds`, + build, + ); + if (res.status !== 200) { + throw new FirebaseError(`Failed to submit build: ${res.status} ${res.body}`); + } + await pollOperation({ + apiOrigin: cloudbuildOrigin(), + apiVersion: "v1", + operationResourceName: res.body.buildOperation, + }); +} + +export async function updateService(service: Omit): Promise { + const fieldMask = proto.fieldMasks( + service, + /* doNotRecurseIn...*/ "labels", + "annotations", + "tags", + ); + // Always update revision name to ensure null generates a new unique revision name. + fieldMask.push("template.revision"); + const res = await client.post, LongRunningOperation>( + service.name, + service, + { + queryParams: { + updateMask: fieldMask.join(","), + }, + }, + ); + const svc = await pollOperation({ + apiOrigin: runOrigin(), + apiVersion: API_VERSION, + operationResourceName: res.body.name, + }); + return svc; +} + +// TODO: Replace with real version: +function functionNameToServiceName(id: string): string { + return id.toLowerCase().replace(/_/g, "-"); +} + +/** + * The following is the YAML of a v2 function's Run service labels & annotations: + * + * labels: + * goog-drz-cloudfunctions-location: us-central1 + * goog-drz-cloudfunctions-id: ejectrequest + * firebase-functions-hash: 3653cb61dcf8e18a4a8706251b627485a5e83cd0 + * firebase-functions-codebase: js + * goog-managed-by: cloudfunctions + * goog-cloudfunctions-runtime: nodejs22 + * cloud.googleapis.com/location: us-central1 + * annotations: + * run.googleapis.com/custom-audiences: '["https://us-central1-inlined-junkdrawer.cloudfunctions.net/ejectRequest"]' + * run.googleapis.com/client-name: cli-firebase + * run.googleapis.com/build-source-location: gs://gcf-v2-sources-92611791981-us-central1/ejectRequest/function-source.zip#1749833196570851 + * run.googleapis.com/build-environment-variables: '{"GOOGLE_NODE_RUN_SCRIPTS":""}' + * run.googleapis.com/build-function-target: ejectRequest + * run.googleapis.com/build-enable-automatic-updates: 'true' + * run.googleapis.com/build-base-image: us-central1-docker.pkg.dev/serverless-runtimes/google-22-full/runtimes/nodejs22 + * run.googleapis.com/build-image-uri: us-central1-docker.pkg.dev/inlined-junkdrawer/gcf-artifacts/inlined--junkdrawer__us--central1__eject_request:version_1 + * run.googleapis.com/build-name: projects/92611791981/locations/us-central1/builds/4d41c5e1-9ab9-4889-826b-c64a0d58c99a + * serving.knative.dev/creator: service-92611791981@gcf-admin-robot.iam.gserviceaccount.com + * serving.knative.dev/lastModifier: service-92611791981@gcf-admin-robot.iam.gserviceaccount.com + * run.googleapis.com/operation-id: 67a480e9-24ac-40bd-aaa1-a76e87bf3e45 + * run.googleapis.com/ingress: all + * run.googleapis.com/ingress-status: all + * cloudfunctions.googleapis.com/function-id: ejectRequest + * run.googleapis.com/urls: '["https://ejectrequest-92611791981.us-central1.run.app","https://us-central1-inlined-junkdrawer.cloudfunctions.net/ejectRequest","https://ejectrequest-uvb3o4q2mq-uc.a.run.app"]' + * + * After ejection it is: + * labels: + * goog-drz-cloudfunctions-location: us-central1 + * goog-drz-cloudfunctions-id: ejectrequest + * firebase-functions-hash: 3653cb61dcf8e18a4a8706251b627485a5e83cd0 + * firebase-functions-codebase: js + * goog-managed-by: '' + * goog-cloudfunctions-runtime: nodejs22 + * cloud.googleapis.com/location: us-central1 + * annotations: + * serving.knative.dev/creator: service-92611791981@gcf-admin-robot.iam.gserviceaccount.com + * serving.knative.dev/lastModifier: service-92611791981@gcf-admin-robot.iam.gserviceaccount.com + * run.googleapis.com/custom-audiences: '["https://us-central1-inlined-junkdrawer.cloudfunctions.net/ejectRequest"]' + * run.googleapis.com/client-name: cli-firebase + * run.googleapis.com/build-source-location: gs://gcf-v2-sources-92611791981-us-central1/ejectRequest/function-source.zip#1749833196570851 + * run.googleapis.com/build-environment-variables: '{"GOOGLE_NODE_RUN_SCRIPTS":""}' + * run.googleapis.com/build-function-target: ejectRequest + * run.googleapis.com/build-enable-automatic-updates: 'true' + * run.googleapis.com/build-base-image: us-central1-docker.pkg.dev/serverless-runtimes/google-22-full/runtimes/nodejs22 + * run.googleapis.com/build-image-uri: us-central1-docker.pkg.dev/inlined-junkdrawer/gcf-artifacts/inlined--junkdrawer__us--central1__eject_request:version_1 + * run.googleapis.com/build-name: projects/92611791981/locations/us-central1/builds/4d41c5e1-9ab9-4889-826b-c64a0d58c99a + * cloudfunctions.googleapis.com/function-id: ejectRequest + * run.googleapis.com/operation-id: 8fed392e-1ded-4499-b233-ac689857be15 + * run.googleapis.com/ingress: all + * run.googleapis.com/ingress-status: all + * run.googleapis.com/urls: '["https://ejectrequest-92611791981.us-central1.run.app","https://us-central1-inlined-junkdrawer.cloudfunctions.net/ejectRequest","https://ejectrequest-uvb3o4q2mq-uc.a.run.app"]' + * + * This sample was taken from an https function, but we should assume that all labels we use in GCF translate to Run + * and preserve them to keep the Console similar for GCF 2nd gen vs Cloud Run functions when reading. + * Notable differences from the Functions interface though is that "goog-managed-by" should be firebase-functions and + * "run.googleapis.com/client-name" should be "cli-firebase" on eject. + */ +// EDIT: Turns out all the above is BS from Pantheon and you can't actually use it because it requires protected read-only fields in V1 +// Intead here's the same function in V2. +/** + * { + * "buildConfig": { + * "name": "projects/92611791981/locations/us-central1/builds/4d41c5e1-9ab9-4889-826b-c64a0d58c99a", + * "enableAutomaticUpdates": true, + * "environmentVariables": { + * "GOOGLE_NODE_RUN_SCRIPTS": "" + * }, + * "imageUri": "us-central1-docker.pkg.dev/inlined-junkdrawer/gcf-artifacts/inlined--junkdrawer__us--central1__eject_request:version_1", + * "baseImage": "us-central1-docker.pkg.dev/serverless-runtimes/google-22-full/runtimes/nodejs22", + * "sourceLocation": "gs://gcf-v2-sources-92611791981-us-central1/ejectRequest/function-source.zip#1749833196570851", + * "functionTarget": "ejectRequest" + * }, + * "updateTime": "2025-06-13T21:23:05.883496Z", + * "uid": "2946ee66-76ec-493c-a853-2f126dabef73", + * "creator": "service-92611791981@gcf-admin-robot.iam.gserviceaccount.com", + * "generation": "2", + * "labels": { + * "firebase-functions-hash": "3653cb61dcf8e18a4a8706251b627485a5e83cd0", + * "goog-cloudfunctions-runtime": "nodejs22", + * "goog-drz-cloudfunctions-id": "ejectrequest", + * "firebase-functions-codebase": "js", + * "goog-managed-by": "", + * "goog-drz-cloudfunctions-location": "us-central1" + * }, + * "ingress": "INGRESS_TRAFFIC_ALL", + * "terminalCondition": { + * "lastTransitionTime": "2025-06-13T21:23:12.232110Z", + * "state": "CONDITION_SUCCEEDED", + * "type": "Ready" + * }, + * "trafficStatuses": [ + * { + * "percent": 100, + * "type": "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" + * } + * ], + * "launchStage": "GA", + * "observedGeneration": "2", + * "etag": "\"CLmtssIGEMCopKUD/cHJvamVjdHMvaW5saW5lZC1qdW5rZHJhd2VyL2xvY2F0aW9ucy91cy1jZW50cmFsMS9zZXJ2aWNlcy9lamVjdHJlcXVlc3Q\"", + * "latestCreatedRevision": "projects/inlined-junkdrawer/locations/us-central1/services/ejectrequest/revisions/ejectrequest-00002-ruh", + * "template": { + * "maxInstanceRequestConcurrency": 80, + * "labels": { + * "firebase-functions-codebase": "js", + * "firebase-functions-hash": "3653cb61dcf8e18a4a8706251b627485a5e83cd0" + * }, + * "serviceAccount": "92611791981-compute@developer.gserviceaccount.com", + * "scaling": { + * "maxInstanceCount": 100 + * }, + * "timeout": "60s", + * "annotations": { + * "cloudfunctions.googleapis.com/trigger-type": "HTTP_TRIGGER" + * }, + * "containers": [ + * { + * "name": "worker", + * "image": "us-central1-docker.pkg.dev/inlined-junkdrawer/gcf-artifacts/inlined--junkdrawer__us--central1__eject_request:version_1", + * "env": [ + * { + * "name": "FIREBASE_CONFIG", + * "value": "{\"projectId\":\"inlined-junkdrawer\",\"databaseURL\":\"https://inlined-junkdrawer.firebaseio.com\",\"storageBucket\":\"inlined-junkdrawer.appspot.com\",\"locationId\":\"us-central\"}" + * }, + * { + * "name": "GCLOUD_PROJECT", + * "value": "inlined-junkdrawer" + * }, + * { + * "name": "EVENTARC_CLOUD_EVENT_SOURCE", + * "value": "projects/inlined-junkdrawer/locations/us-central1/services/ejectRequest" + * }, + * { + * "name": "FUNCTION_TARGET", + * "value": "ejectRequest" + * }, + * { + * "name": "LOG_EXECUTION_ID", + * "value": "true" + * }, + * { + * "name": "FUNCTION_SIGNATURE_TYPE", + * "value": "http" + * } + * ], + * "baseImageUri": "us-central1-docker.pkg.dev/serverless-runtimes/google-22-full/runtimes/nodejs22", + * "startupProbe": { + * "failureThreshold": 1, + * "tcpSocket": { + * "port": 8080 + * }, + * "timeoutSeconds": 240, + * "periodSeconds": 240 + * }, + * "ports": [ + * { + * "name": "http1", + * "containerPort": 8080 + * } + * ], + * "resources": { + * "startupCpuBoost": true, + * "cpuIdle": true, + * "limits": { + * "cpu": "1", + * "memory": "256Mi" + * } + * } + * } + * ], + * "revision": "ejectrequest-00002-ruh" + * }, + * "conditions": [ + * { + * "lastTransitionTime": "2025-06-13T21:23:12.186199Z", + * "state": "CONDITION_SUCCEEDED", + * "type": "RoutesReady" + * }, + * { + * "lastTransitionTime": "2025-06-13T21:23:10.904451Z", + * "state": "CONDITION_SUCCEEDED", + * "type": "ConfigurationsReady" + * } + * ], + * "annotations": { + * "cloudfunctions.googleapis.com/function-id": "ejectRequest" + * }, + * "customAudiences": [ + * "https://us-central1-inlined-junkdrawer.cloudfunctions.net/ejectRequest" + * ], + * "traffic": [ + * { + * "percent": 100, + * "type": "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST" + * } + * ], + * "createTime": "2025-06-13T16:47:35.642129Z", + * "name": "projects/inlined-junkdrawer/locations/us-central1/services/ejectrequest", + * "latestReadyRevision": "projects/inlined-junkdrawer/locations/us-central1/services/ejectrequest/revisions/ejectrequest-00002-ruh", + * "uri": "https://ejectrequest-uvb3o4q2mq-uc.a.run.app", + * "client": "cli-firebase", + * "urls": [ + * "https://ejectrequest-92611791981.us-central1.run.app", + * "https://us-central1-inlined-junkdrawer.cloudfunctions.net/ejectRequest", + * "https://ejectrequest-uvb3o4q2mq-uc.a.run.app" + * ], + * "lastModifier": "service-92611791981@gcf-admin-robot.iam.gserviceaccount.com" + *} + */ + +// NOTE: I'm seeing different values for functions that were ejected vs functions created in the Cloud Console directly with CRF. +// E.g. build-function-target may be a scalar like "ejectRequest" or a JSON object like '{"worker":"ejectRequest"}' where +// the key is the container name. Tinkering may be necessary to see if one or the other is better. + +// Note this runtime seems to be somewhat redundant with containers.baseImage +export const RUNTIME_LABEL = "goog-cloudfunctions-runtime"; + +// In GCF 2nd gen this is cloudfunctions but is the empty string after ejecting. We can use a new value to detect how much +// of the fleet has migrated. +export const CLIENT_NAME_LABEL = "goog-managed-by"; + +// NOTE: Any annotation with a google domain prefix is read-only and a holdover from the GCF API. +export const TRIGGER_TYPE_ANNOTATION = "cloudfunctions.googleapis.com/trigger-type"; +export const FUNCTION_TARGET_ANNOTATION = "run.googleapis.com/build-function-target"; // e.g. '{"worker":"triggerTest"}' +export const FUNCTION_ID_ANNOTATION = "cloudfunctions.googleapis.com/function-id"; // e.g. "triggerTest" + +export const FUNCTION_TARGET_ENV = "FUNCTION_TARGET"; +export const FUNCTION_SIGNATURE_TYPE_ENV = "FUNCTION_SIGNATURE_TYPE"; + +export const FIREBASE_FUNCTION_METADTA_ANNOTATION = "firebase-functions-metadata"; +export interface FirebaseFunctionMetadata { + functionId: string; + // TODO: Trigger type since we cannot set cloudfunctions.googleapis.com/trigger-type +} + +// Partial implementation. A full implementation may require more refactoring. +// E.g. server-side we need to know the actual names of the resources we're +// referencing. So maybe endpointFromSerivce should be async and fetch the +// values from the dependent services? But serviceFromEndpoint currently +// only returns the service and not the dependent resources, which we will +// need for updates. +export function endpointFromService(service: Omit): backend.Endpoint { + const [, /* projects*/ project /* locations*/, , location /* services*/, , svcId] = + service.name.split("/"); + + const metadata = JSON.parse( + service.annotations?.[FIREBASE_FUNCTION_METADTA_ANNOTATION] || "{}", + ) as FirebaseFunctionMetadata; + + const [env, secretEnv] = partition( + service.template.containers![0]!.env || [], + (e) => "value" in e, + ) as [PlaintextEnvVar[], SecretEnvVar[]]; + + const id = + metadata.functionId || + service.annotations?.[FUNCTION_ID_ANNOTATION] || + service.annotations?.[FUNCTION_TARGET_ANNOTATION] || + env.find((e) => e.name === FUNCTION_TARGET_ENV)?.value || + svcId; + const memory = mebibytes(service.template.containers![0]!.resources!.limits!.memory!); + if (!backend.isValidMemoryOption(memory)) { + logger.debug("Converting a service to an endpoint with an invalid memory option", memory); + } + const cpu = Number(service.template.containers![0]!.resources!.limits!.cpu); + const endpoint: backend.Endpoint = { + platform: service.labels?.[CLIENT_NAME_LABEL] === "cloud-functions" ? "gcfv2" : "run", + id, + project, + labels: service.labels || {}, + region: location, + runtime: (service.labels?.[RUNTIME_LABEL] as Runtime) || latest("nodejs"), + availableMemoryMb: memory as backend.MemoryOptions, + cpu: cpu, + entryPoint: + env.find((e) => e.name === FUNCTION_TARGET_ENV)?.value || + service.annotations?.[FUNCTION_TARGET_ANNOTATION] || + service.annotations?.[FUNCTION_ID_ANNOTATION] || + id, + + // TODO: trigger types. + httpsTrigger: {}, + }; + proto.renameIfPresent(endpoint, service.template, "concurrency", "containerConcurrency"); + proto.renameIfPresent(endpoint, service.labels || {}, "codebase", CODEBASE_LABEL); + proto.renameIfPresent(endpoint, service.scaling || {}, "minInstances", "minInstanceCount"); + proto.renameIfPresent(endpoint, service.scaling || {}, "maxInstances", "maxInstanceCount"); + + endpoint.environmentVariables = env.reduce>((acc, e) => { + acc[e.name] = e.value; + return acc; + }, {}); + endpoint.secretEnvironmentVariables = secretEnv.map((e) => { + const [, /* projects*/ projectId /* secrets*/, , secret] = + e.valueSource.secretKeyRef.secret.split("/"); + return { + key: e.name, + projectId, + secret, + version: e.valueSource.secretKeyRef.version || "latest", + }; + }); + return endpoint; +} + +export function serviceFromEndpoint( + endpoint: backend.Endpoint, + image: string, +): Omit { + const labels: Record = { + ...endpoint.labels, + [RUNTIME_LABEL]: endpoint.runtime, + [CLIENT_NAME_LABEL]: "firebase-functions", + }; + + // A bit of a hack, but other code assumes the Functions method of indicating deployment tool and + // injects this as a label. To avoid thinking that this is actually meaningful in the CRF world, + // we delete it here. + delete labels["deployment-tool"]; + + // TODO: hash + if (endpoint.codebase) { + labels[CODEBASE_LABEL] = endpoint.codebase; + } + + const annotations: Record = { + [FIREBASE_FUNCTION_METADTA_ANNOTATION]: JSON.stringify({ + functionId: endpoint.id, + }), + }; + + const template: RevisionTemplate = { + containers: [ + { + name: "worker", + image, + env: [ + ...Object.entries(endpoint.environmentVariables || {}).map(([name, value]) => ({ + name, + value, + })), + ...(endpoint.secretEnvironmentVariables || []).map((secret) => ({ + name: secret.key, + valueSource: { + secretKeyRef: { + secret: secret.secret, + version: secret.version, + }, + }, + })), + { + name: FUNCTION_TARGET_ENV, + value: endpoint.entryPoint, + }, + { + name: FUNCTION_SIGNATURE_TYPE_ENV, + value: backend.isEventTriggered(endpoint) ? "cloudevent" : "http", + }, + ], + resources: { + limits: { + cpu: String(endpoint.cpu || 1), + memory: `${endpoint.availableMemoryMb || 256}Mi`, + }, + cpuIdle: true, + startupCpuBoost: true, + }, + }, + ], + containerConcurrency: endpoint.concurrency || backend.DEFAULT_CONCURRENCY, + }; + proto.renameIfPresent(template, endpoint, "containerConcurrency", "concurrency"); + + const service: Omit = { + name: `projects/${endpoint.project}/locations/${endpoint.region}/services/${functionNameToServiceName( + endpoint.id, + )}`, + labels, + annotations, + template, + client: "cli-firebase", + }; + + if (endpoint.minInstances || endpoint.maxInstances) { + service.scaling = {}; + proto.renameIfPresent(service.scaling, endpoint, "minInstanceCount", "minInstances"); + proto.renameIfPresent(service.scaling, endpoint, "maxInstanceCount", "maxInstances"); + } + + // TODO: other trigger types, service accounts, concurrency, etc. + return service; +} 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 new file mode 100644 index 00000000000..ac9c13fefd8 --- /dev/null +++ b/src/gcp/secretManager.ts @@ -0,0 +1,505 @@ +import * as iam from "./iam"; + +import { logLabeledSuccess } from "../utils"; +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}`; +export interface Secret { + // Secret name/label (this is not resource name) + name: string; + // This is either projectID or number + projectId: string; + 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; +} + +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 client.get(`projects/${projectId}/secrets/${name}`); + const secret = parseSecretResourceName(getRes.body.name); + secret.labels = getRes.body.labels ?? {}; + secret.replication = getRes.body.replication ?? {}; + return secret; +} + +/** + * 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, + 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 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: any) { + if (err.status === 404) { + return false; + } + throw err; + } +} + +/** + * Parse full secret resource name. + */ +export function parseSecretResourceName(resourceName: string): Secret { + const match = SECRET_NAME_REGEX.exec(resourceName); + if (!match?.groups) { + throw new FirebaseError(`Invalid secret resource name [${resourceName}].`); + } + return { + projectId: match.groups.project, + name: match.groups.secret, + labels: {}, + replication: {}, + }; +} + +/** + * Parse full secret version resource name. + */ +export function parseSecretVersionResourceName(resourceName: string): SecretVersion { + const match = resourceName.match(SECRET_VERSION_NAME_REGEX); + if (!match?.groups) { + throw new FirebaseError(`Invalid secret version resource name [${resourceName}].`); + } + return { + secret: { + projectId: match.groups.project, + name: match.groups.secret, + labels: {}, + replication: {}, + }, + versionId: match.groups.version, + createTime: "", + }; +} + +/** + * Returns full secret version resource name. + */ +export function toSecretVersionResourceName(secretVersion: SecretVersion): string { + return `projects/${secretVersion.secret.projectId}/secrets/${secretVersion.secret.name}/versions/${secretVersion.versionId}`; +} + +/** + * Creates a new secret resource. + */ +export async function createSecret( + projectId: string, + name: string, + labels: Record, + location?: string, +): Promise { + 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), + labels, + replication, + }; +} + +/** + * 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`, + { + payload: { + data: Buffer.from(payloadData).toString("base64"), + }, + }, + ); + return { + ...parseSecretVersionResourceName(res.body.name), + state: res.body.state, + createTime: "", + }; +} + +/** + * 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 { + await client.post<{ policy: Partial; updateMask: string }, iam.Policy>( + `projects/${secret.projectId}/secrets/${secret.name}:setIamPolicy`, + { + policy: { + bindings, + }, + updateMask: "bindings", + }, + ); +} + +/** + * 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 bindings = await checkServiceAgentRole(secret, serviceAccountEmails, role); + if (bindings.length) { + 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 ${serviceAccountEmails.join(", ")}`, + ); +} + +export async function checkServiceAgentRole( + 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); + } + + 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 []; + return bindings; +} + +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..3f87b772482 --- /dev/null +++ b/src/gcp/serviceusage.ts @@ -0,0 +1,72 @@ +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`, + /* body=*/ {}, + { headers: { "x-goog-quota-user": `projects/${projectNumber}` } }, + ); + 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, + headers: { "x-goog-quota-user": `projects/${projectNumber}` }, + }); +} diff --git a/src/gcp/storage.js b/src/gcp/storage.js deleted file mode 100644 index 1d526a83c4e..00000000000 --- a/src/gcp/storage.js +++ /dev/null @@ -1,90 +0,0 @@ -"use strict"; - -var path = require("path"); -var api = require("../api"); -const { logger } = require("../logger"); -var { FirebaseError } = require("../error"); - -function _getDefaultBucket(projectId) { - return api - .request("GET", "/v1/apps/" + projectId, { - auth: true, - origin: api.appengineOrigin, - }) - .then( - function (resp) { - if (resp.body.defaultBucket === "undefined") { - logger.debug("Default storage bucket is undefined."); - return Promise.reject( - new FirebaseError( - "Your project is being set up. Please wait a minute before deploying again." - ) - ); - } - return Promise.resolve(resp.body.defaultBucket); - }, - function (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 Promise.reject(err); - } - ); -} - -function _uploadSource(source, uploadUrl) { - return api.request("PUT", uploadUrl, { - data: source.stream, - headers: { - "Content-Type": "application/zip", - "x-goog-content-length-range": "0,104857600", - }, - json: false, - origin: api.storageOrigin, - logOptions: { skipRequestBody: true }, - }); -} - -/** - * 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 - */ -async function _uploadObject(source, bucketName) { - if (path.extname(source.file) !== ".zip") { - throw new FirebaseError(`Expected a file name ending in .zip, got ${source.file}`); - } - const location = `/${bucketName}/${path.basename(source.file)}`; - await api.request("PUT", location, { - auth: true, - data: source.stream, - headers: { - "Content-Type": "application/zip", - "x-goog-content-length-range": "0,123289600", - }, - json: false, - origin: api.storageOrigin, - logOptions: { skipRequestBody: true }, - }); - return location; -} - -/** - * Deletes an object via Firebase Storage. - * @param {string} location A Firebase Storage location, of the form "/v0/b//o/" - */ -function _deleteObject(location) { - return api.request("DELETE", location, { - auth: true, - origin: api.storageOrigin, - }); -} - -module.exports = { - getDefaultBucket: _getDefaultBucket, - deleteObject: _deleteObject, - upload: _uploadSource, - uploadObject: _uploadObject, -}; diff --git a/src/gcp/storage.spec.ts b/src/gcp/storage.spec.ts new file mode 100644 index 00000000000..418f52b2802 --- /dev/null +++ b/src/gcp/storage.spec.ts @@ -0,0 +1,236 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import * as storage from "./storage"; +import * as utils from "../utils"; +import { FirebaseError } from "../error"; + +describe("storage", () => { + describe("upsertBucket", () => { + let listBucketsStub: sinon.SinonStub; + let createBucketStub: sinon.SinonStub; + let patchBucketStub: sinon.SinonStub; + let logLabeledBulletStub: sinon.SinonStub; + let logLabeledWarningStub: sinon.SinonStub; + let randomStringStub: sinon.SinonStub; + + const PROJECT_ID = "test-project"; + const BUCKET_LIFECYCLE = { rule: [{ action: { type: "Delete" }, condition: { age: 30 } }] }; + const BASE_BUCKET_NAME = "test-bucket"; + const PURPOSE_LABEL = "test-purpose"; + + beforeEach(() => { + listBucketsStub = sinon.stub(storage, "listBuckets"); + createBucketStub = sinon.stub(storage, "createBucket"); + patchBucketStub = sinon.stub(storage, "patchBucket"); + logLabeledBulletStub = sinon.stub(utils, "logLabeledBullet"); + logLabeledWarningStub = sinon.stub(utils, "logLabeledWarning"); + randomStringStub = sinon.stub(storage, "randomString").returns("abcdef"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return existing bucket name if a bucket with the purpose label is found", async () => { + const bucketName = "existing-bucket"; + listBucketsStub.resolves([ + { name: bucketName, labels: { [PURPOSE_LABEL]: "true" } }, + { name: "another-bucket", labels: {} }, + ] as any); + + const result = await storage.upsertBucket({ + product: "test", + createMessage: "Creating bucket", + projectId: PROJECT_ID, + req: { + baseName: BASE_BUCKET_NAME, + location: "us-central1", + purposeLabel: PURPOSE_LABEL, + lifecycle: BUCKET_LIFECYCLE, + }, + }); + + expect(result).to.equal(bucketName); + expect(listBucketsStub).to.be.calledOnceWith(PROJECT_ID); + expect(createBucketStub).to.not.be.called; + }); + + it("should patch an existing bucket if it does not have a purpose label", async () => { + const bucketName = "existing-unmanaged-bucket"; + listBucketsStub.resolves([{ name: bucketName, labels: {} }] as any); + + const result = await storage.upsertBucket({ + product: "test", + createMessage: "Creating bucket", + projectId: PROJECT_ID, + req: { + baseName: bucketName, + location: "us-central1", + purposeLabel: PURPOSE_LABEL, + lifecycle: BUCKET_LIFECYCLE, + }, + }); + + expect(result).to.equal(bucketName); + expect(listBucketsStub).to.be.calledOnceWith(PROJECT_ID); + expect(patchBucketStub).to.be.calledOnceWith(bucketName, { + labels: { [PURPOSE_LABEL]: "true" }, + }); + expect(createBucketStub).to.not.be.called; + }); + + it("should create a new bucket if no bucket with the purpose label is found", async () => { + listBucketsStub.resolves([{ name: "another-bucket", labels: {} }] as any); + createBucketStub.resolves({ name: BASE_BUCKET_NAME } as any); + + const result = await storage.upsertBucket({ + product: "test", + createMessage: "Creating bucket", + projectId: PROJECT_ID, + req: { + baseName: BASE_BUCKET_NAME, + location: "us-central1", + purposeLabel: PURPOSE_LABEL, + lifecycle: BUCKET_LIFECYCLE, + }, + }); + + expect(result).to.equal(BASE_BUCKET_NAME); + expect(listBucketsStub).to.be.calledOnceWith(PROJECT_ID); + expect(createBucketStub).to.be.calledOnceWith( + PROJECT_ID, + { + name: BASE_BUCKET_NAME, + location: "us-central1", + lifecycle: BUCKET_LIFECYCLE, + labels: { [PURPOSE_LABEL]: "true" }, + }, + true, + ); + expect(logLabeledBulletStub).to.be.calledOnce; + }); + + it("should handle listBuckets failure", async () => { + const error = new FirebaseError("Failed to list buckets"); + listBucketsStub.rejects(error); + + await expect( + storage.upsertBucket({ + product: "test", + createMessage: "Creating bucket", + projectId: PROJECT_ID, + req: { + baseName: BASE_BUCKET_NAME, + location: "us-central1", + purposeLabel: PURPOSE_LABEL, + lifecycle: BUCKET_LIFECYCLE, + }, + }), + ).to.be.rejectedWith(error); + + expect(listBucketsStub).to.be.calledOnceWith(PROJECT_ID); + expect(createBucketStub).to.not.be.called; + }); + + it("should retry with a new name on createBucket conflict", async () => { + const conflictError = new FirebaseError("Conflict", { original: { status: 409 } as any }); + const randomSuffix = "abcdef"; + const newBucketName = `${BASE_BUCKET_NAME}-${randomSuffix}`; + + listBucketsStub.resolves([]); + createBucketStub.onFirstCall().rejects(conflictError); + createBucketStub.onSecondCall().resolves({ name: newBucketName } as any); + + const result = await storage.upsertBucket({ + product: "test", + createMessage: "Creating bucket", + projectId: PROJECT_ID, + req: { + baseName: BASE_BUCKET_NAME, + location: "us-central1", + purposeLabel: PURPOSE_LABEL, + lifecycle: BUCKET_LIFECYCLE, + }, + }); + + expect(result).to.equal(newBucketName); + expect(createBucketStub).to.be.calledTwice; + expect(createBucketStub.firstCall.args[1].name).to.equal(BASE_BUCKET_NAME); + expect(createBucketStub.secondCall.args[1].name).to.equal(newBucketName); + expect(randomStringStub).to.be.calledOnceWith(6); + }); + + it("should error out after 5 createBucket conflicts", async () => { + const conflictError = new FirebaseError("Conflict", { original: { status: 409 } as any }); + listBucketsStub.resolves([]); + createBucketStub.rejects(conflictError); + + await expect( + storage.upsertBucket({ + product: "test", + createMessage: "Creating bucket", + projectId: PROJECT_ID, + req: { + baseName: BASE_BUCKET_NAME, + location: "us-central1", + purposeLabel: PURPOSE_LABEL, + lifecycle: BUCKET_LIFECYCLE, + }, + }), + ).to.be.rejectedWith("Failed to create a unique Cloud Storage bucket name after 5 attempts."); + + expect(createBucketStub.callCount).to.equal(5); + }); + + it("should handle permission errors on createBucket", async () => { + const permError = new FirebaseError("Permission denied", { + original: { status: 403 } as any, + }); + listBucketsStub.resolves([]); + createBucketStub.rejects(permError); + + await expect( + storage.upsertBucket({ + product: "test", + createMessage: "Creating bucket", + projectId: PROJECT_ID, + req: { + baseName: BASE_BUCKET_NAME, + location: "us-central1", + purposeLabel: PURPOSE_LABEL, + lifecycle: BUCKET_LIFECYCLE, + }, + }), + ).to.be.rejectedWith(permError); + + expect(logLabeledWarningStub).to.be.calledOnce; + expect(createBucketStub).to.be.calledOnce; + }); + + it("should forward unexpected errors from createBucket", async () => { + const unexpectedError = new FirebaseError("Unexpected error", { + original: { status: 500 } as any, + }); + listBucketsStub.resolves([]); + createBucketStub.rejects(unexpectedError); + + await expect( + storage.upsertBucket({ + product: "test", + createMessage: "Creating bucket", + projectId: PROJECT_ID, + req: { + baseName: BASE_BUCKET_NAME, + location: "us-central1", + purposeLabel: PURPOSE_LABEL, + lifecycle: BUCKET_LIFECYCLE, + }, + }), + ).to.be.rejectedWith(unexpectedError); + + expect(logLabeledWarningStub).to.not.be.called; + expect(createBucketStub).to.be.calledOnce; + }); + }); +}); diff --git a/src/gcp/storage.ts b/src/gcp/storage.ts new file mode 100644 index 00000000000..3e7bd825988 --- /dev/null +++ b/src/gcp/storage.ts @@ -0,0 +1,592 @@ +import { Readable } from "stream"; +import * as path from "path"; +import * as clc from "colorette"; +import { randomInt } from "crypto"; + +import { firebaseStorageOrigin, storageOrigin } from "../api"; +import { Client } from "../apiv2"; +import { FirebaseError, getErrStatus } from "../error"; +import { logger } from "../logger"; +import { ensure } from "../ensureApiEnabled"; +import * as utils from "../utils"; +import { fieldMasks } from "./proto"; + +/** Bucket Interface */ +interface BucketResponse { + kind: string; + id: string; + selfLink: string; + projectNumber: string; + name: string; + timeCreated: string; + updated: string; + defaultEventBasedHold: boolean; + retentionPolicy: { + retentionPeriod: number; + effectiveTime: string; + isLocked: boolean; + }; + metageneration: number; + acl: [ + { + kind: string; + id: string; + selfLink: string; + bucket: string; + entity: string; + role: string; + email: string; + entityId: string; + domain: string; + projectTeam: { + projectNumber: string; + team: string; + }; + etag: string; + }, + ]; + defaultObjectAcl: [ + { + kind: string; + entity: string; + role: string; + email: string; + entityId: string; + domain: string; + projectTeam: { + projectNumber: string; + team: string; + }; + etag: string; + }, + ]; + iamConfiguration: { + publicAccessPrevention: string; + uniformBucketLevelAccess: { + enabled: boolean; + lockedTime: string; + }; + }; + encryption: { + defaultKmsKeyName: string; + }; + owner: { + entity: string; + entityId: string; + }; + location: string; + locationType: string; + rpo: string; + website: { + mainPageSuffix: string; + notFoundPage: string; + }; + logging: { + logBucket: string; + logObjectPrefix: string; + }; + versioning: { + enabled: boolean; + }; + cors: [ + { + origin: [string]; + method: [string]; + responseHeader: [string]; + maxAgeSeconds: number; + }, + ]; + lifecycle: { + rule: [ + { + action: { + type: string; + storageClass: string; + }; + condition: { + age: number; + createdBefore: string; + customTimeBefore: string; + daysSinceCustomTime: number; + daysSinceNoncurrentTime: number; + isLive: boolean; + matchesStorageClass: [string]; + noncurrentTimeBefore: string; + numNewerVersions: number; + }; + }, + ]; + }; + labels: Record; + storageClass: string; + billing: { + requesterPays: boolean; + }; + etag: string; +} + +interface ListBucketsResponse { + kind: string; + nextPageToken: string; + items: BucketResponse[]; +} + +interface GetDefaultBucketResponse { + name: string; + location: string; + bucket: { + name: string; + }; +} + +export interface UpsertBucketRequest { + baseName: string; + location: string; + purposeLabel: string; + lifecycle: { + rule: LifecycleRule[]; + }; +} + +export interface CreateBucketRequest { + name: string; + location: string; + labels?: Record; + lifecycle: { + rule: LifecycleRule[]; + }; +} + +export interface LifecycleRule { + action: { + type: string; + }; + condition: { + age: number; + }; +} + +interface UploadObjectResponse { + selfLink: string; + mediaLink: string; +} + +/** Response type for obtaining the storage service agent */ +interface StorageServiceAccountResponse { + email_address: string; + kind: string; +} + +export interface FirebaseMetadata { + name: string; + bucket: string; + generation: string; + metageneration: string; + contentType: string; + timeCreated: string; + updated: string; + storageClass: string; + size: string; + md5Hash: string; + contentEncoding: string; + contentDisposition: string; + crc32c: string; + etag: string; + downloadTokens?: string; +} + +export async function getDefaultBucket(projectId: string): Promise { + await ensure(projectId, firebaseStorageOrigin(), "storage", false); + try { + const localAPIClient = new Client({ + urlPrefix: firebaseStorageOrigin(), + apiVersion: "v1alpha", + }); + 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.", + ); + } + 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; + } +} + +export async function upload( + source: any, + uploadUrl: string, + extraHeaders?: Record, + ignoreQuotaProject?: boolean, +): Promise<{ generation: string | null }> { + const url = new URL(uploadUrl, storageOrigin()); + const isSignedUrl = url.searchParams.has("GoogleAccessId"); + const localAPIClient = new Client({ urlPrefix: url.origin, auth: !isSignedUrl }); + const res = await localAPIClient.request({ + method: "PUT", + path: url.pathname, + queryParams: url.searchParams, + responseType: "xml", + headers: { + "content-type": "application/zip", + ...extraHeaders, + }, + body: source.stream, + skipLog: { resBody: true }, + ignoreQuotaProject, + }); + return { + generation: res.response.headers.get("x-goog-generation"), + }; +} + +/** + * Uploads a zip file to the specified bucket using the firebasestorage api. + */ +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 res = await localAPIClient.request({ + method: "PUT", + path: location, + headers: { + "Content-Type": "application/zip", + "x-goog-content-length-range": "0,123289600", + }, + body: source.stream, + }); + return { + bucket: bucketName, + object: path.basename(source.file), + generation: res.response.headers.get("x-goog-generation"), + }; +} + +/** + * Get a storage object from GCP. + * @param {string} bucketName name of the storage bucket that contains the object + * @param {string} objectName name of the object + */ +export async function getObject( + bucketName: string, + objectName: string, +): Promise { + const client = new Client({ urlPrefix: storageOrigin() }); + const res = await client.get(`/storage/v1/b/${bucketName}/o/${objectName}`); + return res.body; +} + +/** + * Deletes an object via Firebase Storage. + * @param {string} location A Firebase Storage location, of the form "/v0/b//o/" + */ +export function deleteObject(location: string): Promise { + 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 + * @return a bucket resource object + */ +export async function getBucket(bucketName: string): Promise { + try { + const localAPIClient = new Client({ urlPrefix: storageOrigin() }); + const result = await localAPIClient.get(`/storage/v1/b/${bucketName}`); + return result.body; + } catch (err: any) { + logger.debug(err); + throw new FirebaseError("Failed to obtain the storage bucket", { + original: err, + }); + } +} + +/** + * Creates a storage bucket on GCP. + * Ref: https://cloud.google.com/storage/docs/json_api/v1/buckets/insert + * @param {string} bucketName name of the storage bucket + * @return a bucket resource object + */ +export async function createBucket( + projectId: string, + req: CreateBucketRequest, + projectPrivate?: boolean, +): Promise { + const queryParams: Record = { + project: projectId, + }; + // TODO: This should probably be always on, but we need to audit the other cases of this method to + // make sure we don't break anything. + if (projectPrivate) { + queryParams["predefinedAcl"] = "projectPrivate"; + queryParams["predefinedDefaultObjectAcl"] = "projectPrivate"; + } + + try { + const localAPIClient = new Client({ urlPrefix: storageOrigin() }); + const result = await localAPIClient.post( + `/storage/v1/b`, + req, + { queryParams }, + ); + return result.body; + } catch (err: any) { + logger.debug(err); + throw new FirebaseError("Failed to create the storage bucket", { + original: err, + }); + } +} + +/** + * Patches a storage bucket on GCP. + * Ref: https://cloud.google.com/storage/docs/json_api/v1/buckets/patch + * @param bucketName name of the storage bucket + * @param metadata the bucket resource metadata to patch + * @return a bucket resource object + */ +export async function patchBucket( + bucketName: string, + metadata: Partial, +): Promise { + try { + const localAPIClient = new Client({ urlPrefix: storageOrigin() }); + const mask = fieldMasks( + metadata, + /* doNotRecurseIn = */ "labels", + "acl", + "defaultObjectAcl", + "lifecycle", + ); + const result = await localAPIClient.patch, BucketResponse>( + `/storage/v1/b/${bucketName}`, + metadata, + { queryParams: { updateMask: mask.join(",") } }, + ); + return result.body; + } catch (err: any) { + logger.debug(err); + throw new FirebaseError("Failed to patch the storage bucket", { + original: err, + }); + } +} + +export function randomString(length: number): string { + // NOTE: uppercase letters are not allowed in bucket names + const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + for (let i = length; i > 0; --i) { + result += chars[randomInt(chars.length)]; + } + return result; +} + +// Call methods through the exports object so that they can be stubbed in tests. +const dynamicDispatch = exports as { + listBuckets: typeof listBuckets; + createBucket: typeof createBucket; + patchBucket: typeof patchBucket; + randomString: typeof randomString; +}; + +/** + * Creates a storage bucket on GCP for a given purpose if it does not already exist. + * NOTE: It is a security issue if the bucket already exists but is not owned by this project. + * This function therefore only returns an existing bucket if it exists AND is in the project. + * We check that the bucket is in the project by calling "listBuckets" (project scoped) rather than + * getBucket (global scoped). If the bucket already exists, we use a name-collision nonce to avoid + * a denial of service. To find this collision-avoiding in the future, we use a label as a breadcrumb. + * Thus our base case of the bucket already existing uses the label not the base name to decide which + * bucket to return. + */ +export async function upsertBucket(opts: { + product: string; + createMessage: string; + projectId: string; + req: UpsertBucketRequest; +}): Promise { + // Use labels to find whether an existing bucket is managed by us. Use labels, not the base name to detect + // a bucket that was created with name conflict resolution. + // Not using try/catch here because ignoring a failure could lead to multiple sources of truth. + const existingBuckets = await dynamicDispatch.listBuckets(opts.projectId); + const managedBucket = existingBuckets.find((b) => opts.req.purposeLabel in (b.labels || {})); + if (managedBucket) { + return managedBucket.name; + } + + // Note: Some customers have created buckets before this new strategy of adding labels already existed. + // If the bucket with the base name already exists _and is returned by listBuckets_, we know it is owned + // by this project and is safet to use. Add the label. + const existingUnmanaged = existingBuckets.find((b) => b.name === opts.req.baseName); + if (existingUnmanaged) { + logger.debug( + `Found existing bucket ${existingUnmanaged.name} without purpose label. Because it is known not to be squatted, we can use it.`, + ); + const labels = { ...existingUnmanaged.labels, [opts.req.purposeLabel]: "true" }; + await dynamicDispatch.patchBucket(existingUnmanaged.name, { labels }); + return existingUnmanaged.name; + } + + utils.logLabeledBullet(opts.product, opts.createMessage); + for (let retryCount = 0; retryCount < 5; retryCount++) { + const name = + retryCount === 0 + ? opts.req.baseName + : `${opts.req.baseName}-${dynamicDispatch.randomString(6)}`; + try { + await dynamicDispatch.createBucket( + opts.projectId, + { + name, + location: opts.req.location, + lifecycle: opts.req.lifecycle, + labels: { + [opts.req.purposeLabel]: "true", + }, + }, + true /* projectPrivate */, + ); + return name; + } catch (err) { + if (getErrStatus((err as FirebaseError).original) === 409) { + utils.logLabeledBullet( + opts.product, + `Bucket ${name} already exists, creating a new bucket with a conflict-avoiding hash`, + ); + continue; + } + + if (getErrStatus((err as FirebaseError).original) === 403) { + utils.logLabeledWarning( + opts.product, + "Failed to create Cloud Storage bucket because user does not have sufficient permissions. " + + "See https://cloud.google.com/storage/docs/access-control/iam-roles for more details on " + + "IAM roles that are able to create a Cloud Storage bucket, and ask your project administrator " + + "to grant you one of those roles.", + ); + } + throw err; + } + } + throw new FirebaseError("Failed to create a unique Cloud Storage bucket name after 5 attempts."); +} + +/** + * 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 { + let buckets: BucketResponse[] = []; + const localAPIClient = new Client({ urlPrefix: storageOrigin() }); + let pageToken: string | undefined; + do { + const result = await localAPIClient.get( + `/storage/v1/b?project=${projectId}`, + { queryParams: pageToken ? { pageToken } : {} }, + ); + buckets = buckets.concat(result.body.items || []); + pageToken = result.body.nextPageToken; + } while (pageToken); + return buckets; + } 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, + * "kind": "storage#serviceAccount", + * } + */ +export async function getServiceAccount(projectId: string): Promise { + try { + const localAPIClient = new Client({ urlPrefix: storageOrigin() }); + const response = await localAPIClient.get( + `/storage/v1/projects/${projectId}/serviceAccount`, + ); + return response.body; + } catch (err: any) { + logger.debug(err); + throw new FirebaseError("Failed to obtain the Cloud Storage service agent", { + original: err, + }); + } +} + +/** + * getDownloadUrl finds a publicly accessible download url for an object in Firebase storage. + * @param bucketName the bucket which contains the object you are looking for. + * @param objectPath a path within the bucket where the obejct resides. + * @return the string HTTP path to download the object. + */ +export async function getDownloadUrl( + bucketName: string, + objectPath: string, + emulatorUrl?: string, +): Promise { + try { + const origin = emulatorUrl || firebaseStorageOrigin(); + const localAPIClient = new Client({ urlPrefix: origin }); + const response = await localAPIClient.get( + `/v0/b/${bucketName}/o/${encodeURIComponent(objectPath)}`, + ); + + if (emulatorUrl) { + return `${origin}/v0/b/${bucketName}/o/${encodeURIComponent(objectPath)}?alt=media`; + } + + if (!response.body.downloadTokens) { + throw new Error( + `no download tokens exist for ${objectPath}, please visit the Firebase console to make one`, + ); + } + const [token] = response.body.downloadTokens.split(","); + return `${origin}/v0/b/${bucketName}/o/${encodeURIComponent(objectPath)}?alt=media&token=${token}`; + } catch (err: any) { + logger.error(err); + throw new FirebaseError( + `${err} Check that you have permission in the Firebase console to generate a download token`, + { + original: err, + }, + ); + } +} diff --git a/src/gemini/fdcExperience.spec.ts b/src/gemini/fdcExperience.spec.ts new file mode 100644 index 00000000000..9587c53a074 --- /dev/null +++ b/src/gemini/fdcExperience.spec.ts @@ -0,0 +1,65 @@ +import { expect } from "chai"; +import { extractCodeBlock } from "./fdcExperience"; + +describe("extractCodeBlock", () => { + it("should extract a basic GraphQL query block", () => { + const text = + 'Here is a GraphQL query:\n```graphql\nquery GetUser { user(id: "1") { name email } }\n```\nThanks!'; + const expected = 'query GetUser { user(id: "1") { name email } }'; + expect(extractCodeBlock(text)).to.eq(expected); + }); + + it("should extract a multi-line GraphQL mutation block", () => { + const text = ` + Some preamble. + \`\`\`graphql + mutation CreatePost($title: String!, $content: String!) { + createPost(title: $title, content: $content) { + id + title + } + } + \`\`\` + Followed by some description. + `; + const expected = `mutation CreatePost($title: String!, $content: String!) { + createPost(title: $title, content: $content) { + id + title + } + }`; + expect(extractCodeBlock(text)).to.eq(expected); + }); + + it("should extract a GraphQL fragment block", () => { + const text = "```graphql\nfragment UserFields on User { id name }\n```"; + const expected = "fragment UserFields on User { id name }"; + expect(extractCodeBlock(text)).to.eq(expected); + }); + + it("should extract an empty GraphQL code block", () => { + const text = "```graphql\n\n```"; + const expected = ""; + expect(extractCodeBlock(text)).to.eq(expected); + }); + + it("should extract a GraphQL schema definition block", () => { + const text = ` + \`\`\`graphql + type Query { + hello: String + } + schema { + query: Query + } + \`\`\` + `; + const expected = `type Query { + hello: String + } + schema { + query: Query + }`; + expect(extractCodeBlock(text)).to.eq(expected); + }); +}); diff --git a/src/gemini/fdcExperience.ts b/src/gemini/fdcExperience.ts new file mode 100644 index 00000000000..523b7383a26 --- /dev/null +++ b/src/gemini/fdcExperience.ts @@ -0,0 +1,114 @@ +import { Client } from "../apiv2"; +import { cloudAiCompanionOrigin } from "../api"; +import { + ChatExperienceResponse, + CloudAICompanionMessage, + CloudAICompanionRequest, + GenerateOperationResponse, + GenerateSchemaResponse, +} from "./types"; +import { FirebaseError } from "../error"; + +const apiClient = new Client({ urlPrefix: cloudAiCompanionOrigin(), auth: true }); +const SCHEMA_GENERATOR_EXPERIENCE = "/appeco/firebase/fdc-schema-generator"; +const GEMINI_IN_FIREBASE_EXPERIENCE = "/appeco/firebase/firebase-chat/free"; +const OPERATION_GENERATION_EXPERIENCE = "/appeco/firebase/fdc-query-generator"; +const FIREBASE_CHAT_REQUEST_CONTEXT_TYPE_NAME = + "type.googleapis.com/google.cloud.cloudaicompanion.v1main.FirebaseChatRequestContext"; + +export const PROMPT_GENERATE_CONNECTOR = + "Create 4 operations for an app using the instance schema with proper authentication."; + +export const PROMPT_GENERATE_SEED_DATA = + "Create a mutation to populate the database with some seed data."; + +/** + * generateSchema generates a schema based on the users app design prompt. + * @param prompt description of the app the user would like to generate. + * @param project project identifier. + * @return graphQL schema for a Firebase Data Connect Project. + */ +export async function generateSchema( + prompt: string, + project: string, + chatHistory: CloudAICompanionMessage[] = [], +): Promise { + const res = await apiClient.post( + `/v1beta/projects/${project}/locations/global/instances/default:completeTask`, + { + input: { messages: [...chatHistory, { content: prompt, author: "USER" }] }, + experienceContext: { + experience: SCHEMA_GENERATOR_EXPERIENCE, + }, + }, + ); + return extractCodeBlock(res.body.output.messages[0].content); +} + +/** + * chatWithFirebase interacts with the Gemini in Firebase integration providing deeper knowledge on Firebase. + * @param prompt the interaction that the user would like to have with the service. + * @param project project identifier. + * @return ChatExperienceResponse includes not only the message from the service but also links to the resources used by the service. + */ +export async function chatWithFirebase( + prompt: string, + project: string, + chatHistory: CloudAICompanionMessage[] = [], +): Promise { + const res = await apiClient.post( + `/v1beta/projects/${project}/locations/global/instances/default:completeTask`, + { + input: { messages: [...chatHistory, { content: prompt, author: "USER" }] }, + experienceContext: { + experience: GEMINI_IN_FIREBASE_EXPERIENCE, + }, + }, + ); + return res.body; +} + +/** + * generateOperation generates an operation based on the users prompt and deployed Firebase Data Connect Service. + * @param prompt description of the operation the user would like to generate. + * @param service the name or service id of the deployed Firebase Data Connect service. + * @param project project identifier. + * @return graphQL operation for a deployed Firebase Data Connect Schema. + */ +export async function generateOperation( + prompt: string, + service: string, + project: string, + chatHistory: CloudAICompanionMessage[] = [], +): Promise { + const res = await apiClient.post( + `/v1beta/projects/${project}/locations/global/instances/default:completeTask`, + { + input: { messages: [...chatHistory, { content: prompt, author: "USER" }] }, + experienceContext: { + experience: OPERATION_GENERATION_EXPERIENCE, + }, + clientContext: { + additionalContext: { + "@type": FIREBASE_CHAT_REQUEST_CONTEXT_TYPE_NAME, + fdcInfo: { fdcServiceName: service, requiresQuery: true }, + }, + }, + }, + ); + return extractCodeBlock(res.body.output.messages[0].content); +} + +/** + * extractCodeBlock extracts the code block from the generated response. + * @param text the generated response from the service. + * @return the code block from the generated response. + */ +export function extractCodeBlock(text: string): string { + const regex = /```(?:[a-z]+\n)?([\s\S]*?)```/m; + const match = text.match(regex); + if (match && match[1]) { + return match[1].trim(); + } + throw new FirebaseError(`No code block found in the generated response: ${text}`); +} diff --git a/src/gemini/types.ts b/src/gemini/types.ts new file mode 100644 index 00000000000..2329178c4df --- /dev/null +++ b/src/gemini/types.ts @@ -0,0 +1,76 @@ +export interface CloudAICompanionMessage { + content: string; + author: string; +} + +export interface CloudAICompanionInput { + preamble?: string; + messages: CloudAICompanionMessage[]; +} + +export interface ExperienceContext { + experience?: string; + agent?: string; + task?: string; +} + +export interface FdcRequestInfo { + serviceId?: string; + fdcServiceName: string; + requiresQuery: boolean; +} + +export interface ClientContext { + name?: string; + additionalContext: { + "@type": string; + fdcInfo: FdcRequestInfo; + }; +} + +export interface CloudAICompanionRequest { + messageId?: string; + topic?: string; + input: CloudAICompanionInput; + + // product context -- required + experienceContext: ExperienceContext; + + // Client context (e.g. IDE name, version, etc) + clientContext?: ClientContext; +} + +/** Experience specific response types */ + +export interface GenerateOperationResponse { + output: { messages: CloudAICompanionMessage[] }; + outputDataContext: { additionalcontext: { "@type:": string } }; +} + +export interface GenerateSchemaResponse { + output: { messages: { content: string }[] }; + displayContext: { + additionalContext: { + "@type": string; + firebaseFdcDisplayContext: { schemaSyntaxError: string }; + }; + }; +} + +export interface ChatExperienceResponse { + output: { messages: CloudAICompanionMessage[] }; + outputDataContext: { + additionalContext: { "@type": string }; + attributionContext: { + citationMetadata: { + citations: { + startIndex: number; + endIndex: number; + url: string; + title: string; + license: string; + }[]; + }; + }; + }; +} diff --git a/src/getDefaultDatabaseInstance.ts b/src/getDefaultDatabaseInstance.ts index f779af7e68d..9295b5d0d6c 100644 --- a/src/getDefaultDatabaseInstance.ts +++ b/src/getDefaultDatabaseInstance.ts @@ -5,7 +5,7 @@ import { getFirebaseProject } from "./management/projects"; * @param options The command-line options object * @return The instance ID, empty if it doesn't exist. */ -export async function getDefaultDatabaseInstance(options: any): Promise { - const projectDetails = await getFirebaseProject(options.project); +export async function getDefaultDatabaseInstance(project: string): Promise { + const projectDetails = await getFirebaseProject(project); return projectDetails.resources?.realtimeDatabaseInstance || ""; } diff --git a/src/getDefaultHostingSite.spec.ts b/src/getDefaultHostingSite.spec.ts new file mode 100644 index 00000000000..aca1e49cb88 --- /dev/null +++ b/src/getDefaultHostingSite.spec.ts @@ -0,0 +1,78 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { getDefaultHostingSite, errNoDefaultSite } from "./getDefaultHostingSite"; +import * as projectUtils from "./projectUtils"; +import * as projects from "./management/projects"; +import * as hostingApi from "./hosting/api"; +import { SiteType } from "./hosting/api"; + +const PROJECT_ID = "test-project-id"; + +describe("getDefaultHostingSite", () => { + let sandbox: sinon.SinonSandbox; + let getFirebaseProjectStub: sinon.SinonStub; + let listSitesStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(projectUtils, "needProjectId").returns(PROJECT_ID); + getFirebaseProjectStub = sandbox.stub(projects, "getFirebaseProject"); + listSitesStub = sandbox.stub(hostingApi, "listSites"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return the default hosting site from project resources", async () => { + const defaultSite = "my-default-site"; + getFirebaseProjectStub.resolves({ + resources: { hostingSite: defaultSite }, + }); + + const site = await getDefaultHostingSite({ projectId: PROJECT_ID }); + + expect(site).to.equal(defaultSite); + expect(getFirebaseProjectStub).to.have.been.calledWith(PROJECT_ID); + expect(listSitesStub).to.not.have.been.called; + }); + + it("should return the default hosting site from listSites if not in project resources", async () => { + const defaultSite = "another-default-site"; + getFirebaseProjectStub.resolves({ resources: {} }); + listSitesStub.resolves([ + { name: `projects/${PROJECT_ID}/sites/other-site`, type: SiteType.USER_SITE }, + { name: `projects/${PROJECT_ID}/sites/${defaultSite}`, type: SiteType.DEFAULT_SITE }, + ]); + + const site = await getDefaultHostingSite({ projectId: PROJECT_ID }); + + expect(site).to.equal(defaultSite); + expect(getFirebaseProjectStub).to.have.been.calledWith(PROJECT_ID); + expect(listSitesStub).to.have.been.calledWith(PROJECT_ID); + }); + + it("should throw an error if no default site is found", async () => { + getFirebaseProjectStub.resolves({ resources: {} }); + listSitesStub.resolves([ + { name: `projects/${PROJECT_ID}/sites/other-site`, type: SiteType.USER_SITE }, + ]); + + await expect(getDefaultHostingSite({ projectId: PROJECT_ID })).to.be.rejectedWith( + errNoDefaultSite, + ); + + expect(getFirebaseProjectStub).to.have.been.calledWith(PROJECT_ID); + expect(listSitesStub).to.have.been.calledWith(PROJECT_ID); + }); + + it("should throw an error if listSites returns no sites", async () => { + getFirebaseProjectStub.resolves({ resources: {} }); + listSitesStub.resolves([]); + + await expect(getDefaultHostingSite({ projectId: PROJECT_ID })).to.be.rejectedWith( + errNoDefaultSite, + ); + }); +}); 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/getProjectId.js b/src/getProjectId.js deleted file mode 100644 index 00d106190a7..00000000000 --- a/src/getProjectId.js +++ /dev/null @@ -1,57 +0,0 @@ -"use strict"; - -var _ = require("lodash"); -var clc = require("cli-color"); -var marked = require("marked"); - -var { FirebaseError } = require("./error"); - -/** - * Tries to determine the correct app name for commands that - * only require an app name. Uses passed in firebase option - * first, then falls back to firebase.json. - * @param {Object} options The command-line options object - * @param {boolean} allowNull Whether or not the firebase flag - * is required - * @returns {String} The firebase name - */ -module.exports = function (options, allowNull = false) { - if (!options.project && !allowNull) { - var aliases = _.get(options, "rc.projects", {}); - var aliasCount = _.size(aliases); - - if (aliasCount === 0) { - throw new FirebaseError( - "No currently active project.\n" + - "To run this command, you need to specify a project. You have two options:\n" + - "- Run this command with " + - clc.bold("--project ") + - ".\n" + - "- Set an active project by running " + - clc.bold("firebase use --add") + - ", then rerun this command.\n" + - "To list all the Firebase projects to which you have access, run " + - clc.bold("firebase projects:list") + - ".\n" + - marked( - "To learn about active projects for the CLI, visit https://firebase.google.com/docs/cli#project_aliases" - ), - { - exit: 1, - } - ); - } else { - var aliasList = _.map(aliases, function (projectId, aname) { - return " " + aname + " (" + projectId + ")"; - }).join("\n"); - - throw new FirebaseError( - "No project active, but project aliases are available.\n\nRun " + - clc.bold("firebase use ") + - " with one of these options:\n\n" + - aliasList - ); - } - } - return options.project; -}; diff --git a/src/getProjectNumber.spec.ts b/src/getProjectNumber.spec.ts new file mode 100644 index 00000000000..82e01f9179d --- /dev/null +++ b/src/getProjectNumber.spec.ts @@ -0,0 +1,46 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { getProjectNumber } from "./getProjectNumber"; +import * as projectUtils from "./projectUtils"; +import * as projects from "./management/projects"; + +const PROJECT_ID = "test-project-id"; +const PROJECT_NUMBER = "123456789"; + +describe("getProjectNumber", () => { + let sandbox: sinon.SinonSandbox; + let needProjectIdStub: sinon.SinonStub; + let getProjectStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + needProjectIdStub = sandbox.stub(projectUtils, "needProjectId").returns(PROJECT_ID); + getProjectStub = sandbox.stub(projects, "getProject"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return project number from options if it exists", async () => { + const options = { projectNumber: PROJECT_NUMBER }; + const projectNumber = await getProjectNumber(options); + + expect(projectNumber).to.equal(PROJECT_NUMBER); + expect(needProjectIdStub).to.not.have.been.called; + expect(getProjectStub).to.not.have.been.called; + }); + + it("should fetch project number if not in options", async () => { + const options: any = { projectId: PROJECT_ID }; + getProjectStub.resolves({ projectNumber: PROJECT_NUMBER }); + + const projectNumber = await getProjectNumber(options); + + expect(projectNumber).to.equal(PROJECT_NUMBER); + expect(needProjectIdStub).to.have.been.calledWith(options); + expect(getProjectStub).to.have.been.calledWith(PROJECT_ID); + expect(options.projectNumber).to.equal(PROJECT_NUMBER); + }); +}); diff --git a/src/getProjectNumber.ts b/src/getProjectNumber.ts index 6ac9b891f9c..3665a19dea9 100644 --- a/src/getProjectNumber.ts +++ b/src/getProjectNumber.ts @@ -1,6 +1,5 @@ -import { getFirebaseProject } from "./management/projects"; -import * as getProjectId from "./getProjectId"; - +import { getProject } from "./management/projects"; +import { needProjectId } from "./projectUtils"; /** * Fetches the project number. * @param options CLI options. @@ -10,8 +9,8 @@ export async function getProjectNumber(options: any): Promise { if (options.projectNumber) { return options.projectNumber; } - const projectId = getProjectId(options); - const metadata = await getFirebaseProject(projectId); + const projectId = needProjectId(options); + const metadata = await getProject(projectId); options.projectNumber = metadata.projectNumber; return options.projectNumber; } 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..df0f1ceacac 100644 --- a/src/hosting/api.ts +++ b/src/hosting/api.ts @@ -4,6 +4,9 @@ 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"; +import { Constants } from "../emulator/constants"; const ONE_WEEK_MS = 604800000; // 7 * 24 * 60 * 60 * 1000 @@ -13,7 +16,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 +33,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 +90,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 +170,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 +182,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 +200,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 +220,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 +241,8 @@ export type Site = { readonly appId: string; + readonly type?: SiteType; + labels: { [key: string]: string }; }; @@ -195,7 +259,7 @@ export function normalizeName(s: string): string { } const apiClient = new Client({ - urlPrefix: hostingApiOrigin, + urlPrefix: hostingApiOrigin(), apiVersion: "v1beta1", auth: true, }); @@ -210,15 +274,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 +296,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 +333,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 +353,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 +372,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 +451,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 +470,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 +481,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 +502,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 +522,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 +546,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 +564,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 +635,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 +657,8 @@ export async function getCleanDomains(project: string, site: string): Promise>> { const siteDomainMap = new Map(); for (const site of sites) { @@ -552,3 +702,87 @@ 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 (Constants.isDemoProject(projectId)) { + return null; + } + 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 8a3b2e1f50c..c0bbb7ccb2b 100644 --- a/src/hosting/cloudRunProxy.ts +++ b/src/hosting/cloudRunProxy.ts @@ -1,10 +1,11 @@ import { RequestHandler } from "express"; -import { get } from "lodash"; +import { Client } from "../apiv2"; +import { cloudRunApiOrigin } from "../api"; import { errorRequestHandler, proxyRequestHandler } from "./proxy"; -import * as getProjectId from "../getProjectId"; +import { FirebaseError } from "../error"; import { logger } from "../logger"; -import { cloudRunApiOrigin, request as apiRequest } from "../api"; +import { needProjectId } from "../projectUtils"; export interface CloudRunProxyOptions { project?: string; @@ -19,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, }); + } } /** @@ -51,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) { @@ -67,7 +70,7 @@ export default function ( logger.info(`[hosting] Cloud Run rewrite ${JSON.stringify(rewrite)} triggered`); const textIdentifier = `Cloud Run service "${rewrite.run.serviceId}" for region "${rewrite.run.region}"`; - return getCloudRunUrl(rewrite, getProjectId(options, false)) + return getCloudRunUrl(rewrite, needProjectId(options)) .then((url) => proxyRequestHandler(url, textIdentifier)) .catch(errorRequestHandler); }; 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 ac08afa0cbb..6f78e95e990 100644 --- a/src/hosting/functionsProxy.ts +++ b/src/hosting/functionsProxy.ts @@ -2,10 +2,12 @@ import { includes } from "lodash"; import { RequestHandler } from "express"; import { proxyRequestHandler } from "./proxy"; -import * as getProjectId from "../getProjectId"; +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 = getProjectId(options, false); - let url = `https://us-central1-${projectId}.cloudfunctions.net/${rewrite.function}`; + const projectId = needProjectId(options); + 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..764557b44af 100644 --- a/src/hosting/implicitInit.ts +++ b/src/hosting/implicitInit.ts @@ -1,14 +1,12 @@ 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"; - -const INIT_TEMPLATE = fs.readFileSync(__dirname + "/../../templates/hosting/init.js", "utf8"); +import { EMULATORS_SUPPORTED_BY_USE_EMULATOR, Emulators } from "../emulator/types"; +import { readTemplateSync } from "../templates"; export interface TemplateServerResponse { // __init.js content with only initializeApp() @@ -18,7 +16,7 @@ export interface TemplateServerResponse { emulatorsJs: string; // firebaseConfig JSON - json: string; + json?: string; } /** @@ -31,15 +29,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..a141088fa23 --- /dev/null +++ b/src/hosting/interactive.ts @@ -0,0 +1,91 @@ +import { FirebaseError } from "../error"; +import { logWarning } from "../utils"; +import { needProjectId, needProjectNumber } from "../projectUtils"; +import { Site, createSite } from "./api"; +import { input } from "../prompt"; + +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 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 1cb9decea45..00000000000 --- a/src/hosting/normalizedHostingConfigs.ts +++ /dev/null @@ -1,117 +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; -} - -/** - * 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.` - ); - } - } - - const hostingConfigs: HostingConfig[] = filterOnly(configs, cmdOptions.only); - - 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..d3444998cd3 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,103 @@ +import * as program from "commander"; +import * as clc from "colorette"; +import * as leven from "leven"; + +import { logger, useConsoleLoggers } from "./logger"; + +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) => { + useConsoleLoggers(); + + 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..93d6685d105 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 { promptOnce } from "../../prompt"; +import { Account } from "../../types/auth"; import { FirebaseError } from "../../error"; +import { select } from "../../prompt"; 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(); @@ -31,14 +31,12 @@ async function promptForAccount() { value: "__add__", }); - const emailChoice: string = await promptOnce({ - type: "list", - name: "email", + const emailChoice: string = await select({ message: "Please select an option:", 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/ailogic/index.spec.ts b/src/init/features/ailogic/index.spec.ts new file mode 100644 index 00000000000..7129835fade --- /dev/null +++ b/src/init/features/ailogic/index.spec.ts @@ -0,0 +1,217 @@ +import * as prompt from "../../../prompt"; +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as init from "./index"; +import * as utils from "./utils"; +import * as apps from "../../../management/apps"; +import * as provision from "../../../management/provisioning/provision"; +import { Setup } from "../.."; +import { AppPlatform } from "../../../management/apps"; + +describe("init ailogic", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("askQuestions", () => { + let listFirebaseAppsStub: sinon.SinonStub; + let selectStub: sinon.SinonStub; + + beforeEach(() => { + listFirebaseAppsStub = sandbox.stub(apps, "listFirebaseApps"); + selectStub = sandbox.stub(prompt, "select"); + }); + + it("should populate ailogic featureInfo with selected app ID", async () => { + const mockApps = [ + { + appId: "1:123456789:android:abcdef123456", + displayName: "Test Android App", + platform: AppPlatform.ANDROID, + }, + { + appId: "1:123456789:web:fedcba654321", + displayName: "Test Web App", + platform: AppPlatform.WEB, + }, + ]; + const mockSetup = { projectId: "test-project" } as Setup; + + listFirebaseAppsStub.resolves(mockApps); + selectStub.resolves(mockApps[0]); // Select first app + + await init.askQuestions(mockSetup); + + expect(mockSetup.featureInfo).to.have.property("ailogic"); + expect(mockSetup.featureInfo?.ailogic).to.deep.equal({ + appId: "1:123456789:android:abcdef123456", + displayName: "Test Android App", + }); + }); + + it("should throw error when no project ID is found", async () => { + const mockSetup = {} as Setup; // No projectId + + await expect(init.askQuestions(mockSetup)).to.be.rejectedWith( + "No project ID found. Please ensure you are in a Firebase project directory or specify a project.", + ); + + sinon.assert.notCalled(listFirebaseAppsStub); + sinon.assert.notCalled(selectStub); + }); + + it("should throw error when no apps are found", async () => { + const mockSetup = { projectId: "test-project" } as Setup; + listFirebaseAppsStub.resolves([]); // No apps + + await expect(init.askQuestions(mockSetup)).to.be.rejectedWith( + "No Firebase apps found in this project. Please create an app first using the Firebase Console or 'firebase apps:create'.", + ); + + sinon.assert.calledWith(listFirebaseAppsStub, "test-project", AppPlatform.ANY); + sinon.assert.notCalled(selectStub); + }); + }); + + describe("actuate", () => { + let setup: Setup; + let parseAppIdStub: sinon.SinonStub; + let provisionFirebaseAppStub: sinon.SinonStub; + let getConfigFileNameStub: sinon.SinonStub; + + beforeEach(() => { + setup = { + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + featureInfo: { + ailogic: { + appId: "1:123456789:android:abcdef123456", + }, + }, + projectId: "test-project", + instructions: [], + } as Setup; + + // Stub only the functions used in actuate (no validation stubs) + parseAppIdStub = sandbox.stub(utils, "parseAppId"); + provisionFirebaseAppStub = sandbox.stub(provision, "provisionFirebaseApp"); + getConfigFileNameStub = sandbox.stub(utils, "getConfigFileName"); + }); + + it("should return early if no ailogic feature info", async () => { + setup.featureInfo = {}; + + await init.actuate(setup); + + // No stubs should be called + sinon.assert.notCalled(parseAppIdStub); + sinon.assert.notCalled(provisionFirebaseAppStub); + }); + + it("should provision existing app successfully", async () => { + const mockAppInfo = { + projectNumber: "123456789", + appId: "1:123456789:android:abcdef123456", + platform: AppPlatform.ANDROID, + }; + const mockConfigContent = '{"config": "content"}'; + const base64Config = Buffer.from(mockConfigContent).toString("base64"); + + parseAppIdStub.returns(mockAppInfo); + provisionFirebaseAppStub.resolves({ configData: base64Config }); + getConfigFileNameStub.returns("google-services.json"); + + await init.actuate(setup); + + sinon.assert.calledWith(parseAppIdStub, "1:123456789:android:abcdef123456"); + sinon.assert.calledOnce(provisionFirebaseAppStub); + + expect(setup.instructions).to.include( + "Firebase AI Logic has been enabled for existing ANDROID app: 1:123456789:android:abcdef123456", + ); + expect(setup.instructions).to.include( + "Save the following content as google-services.json in your app's root directory:", + ); + expect(setup.instructions).to.include(mockConfigContent); + }); + + it("should throw error if no project ID found", async () => { + setup.projectId = undefined; + + await expect(init.actuate(setup)).to.be.rejectedWith( + "AI Logic setup failed: No project ID found. Please ensure you are in a Firebase project directory or specify a project.", + ); + + sinon.assert.calledOnce(parseAppIdStub); + sinon.assert.notCalled(provisionFirebaseAppStub); + }); + + it("should handle provisioning errors gracefully", async () => { + const mockAppInfo = { + projectNumber: "123456789", + appId: "1:123456789:android:abcdef123456", + platform: AppPlatform.ANDROID, + }; + + parseAppIdStub.returns(mockAppInfo); + provisionFirebaseAppStub.throws(new Error("Provisioning API failed")); + + await expect(init.actuate(setup)).to.be.rejectedWith( + "AI Logic setup failed: Provisioning API failed", + ); + }); + + it("should include config file content in instructions for iOS", async () => { + if (setup.featureInfo?.ailogic) { + setup.featureInfo.ailogic.appId = "1:123456789:ios:abcdef123456"; + } + const mockAppInfo = { + projectNumber: "123456789", + appId: "1:123456789:ios:abcdef123456", + platform: AppPlatform.IOS, + }; + const mockConfigContent = ''; + const base64Config = Buffer.from(mockConfigContent).toString("base64"); + + parseAppIdStub.returns(mockAppInfo); + provisionFirebaseAppStub.resolves({ configData: base64Config }); + getConfigFileNameStub.returns("GoogleService-Info.plist"); + + await init.actuate(setup); + + expect(setup.instructions).to.include( + "Firebase AI Logic has been enabled for existing IOS app: 1:123456789:ios:abcdef123456", + ); + expect(setup.instructions).to.include( + "Save the following content as GoogleService-Info.plist in your app's root directory:", + ); + expect(setup.instructions).to.include(mockConfigContent); + }); + + it("should include platform placement guidance in instructions", async () => { + const mockAppInfo = { + projectNumber: "123456789", + appId: "1:123456789:android:abcdef123456", + platform: AppPlatform.ANDROID, + }; + const mockConfigContent = '{"config": "content"}'; + const base64Config = Buffer.from(mockConfigContent).toString("base64"); + + parseAppIdStub.returns(mockAppInfo); + provisionFirebaseAppStub.resolves({ configData: base64Config }); + getConfigFileNameStub.returns("google-services.json"); + + await init.actuate(setup); + + expect(setup.instructions).to.include( + "Place this config file in the appropriate location for your platform.", + ); + }); + }); +}); diff --git a/src/init/features/ailogic/index.ts b/src/init/features/ailogic/index.ts new file mode 100644 index 00000000000..9001d42e06e --- /dev/null +++ b/src/init/features/ailogic/index.ts @@ -0,0 +1,150 @@ +import { select } from "../../../prompt"; +import { Setup } from "../.."; +import { FirebaseError } from "../../../error"; +import { AppInfo, getConfigFileName, parseAppId } from "./utils"; +import { listFirebaseApps, AppMetadata, AppPlatform } from "../../../management/apps"; +import { provisionFirebaseApp } from "../../../management/provisioning/provision"; +import { + ProvisionAppOptions, + ProvisionFirebaseAppOptions, +} from "../../../management/provisioning/types"; + +export interface AiLogicInfo { + appId: string; + displayName?: string; +} + +function checkForApps(apps: AppMetadata[]): void { + if (!apps.length) { + throw new FirebaseError( + "No Firebase apps found in this project. Please create an app first using the Firebase Console or 'firebase apps:create'.", + { exit: 1 }, + ); + } +} + +async function selectAppInteractively(apps: AppMetadata[]): Promise { + checkForApps(apps); + + const choices = apps.map((app) => { + let displayText = app.displayName || app.appId; + + if (!app.displayName) { + if (app.platform === AppPlatform.IOS && "bundleId" in app) { + displayText = app.bundleId as string; + } else if (app.platform === AppPlatform.ANDROID && "packageName" in app) { + displayText = app.packageName as string; + } + } + + return { + name: `${displayText} - ${app.appId} (${app.platform})`, + value: app, + }; + }); + + return await select({ + message: "Select the Firebase app to enable AI Logic for:", + choices, + }); +} + +/** + * Ask questions for AI Logic setup via CLI + */ +export async function askQuestions(setup: Setup): Promise { + if (!setup.projectId) { + throw new FirebaseError( + "No project ID found. Please ensure you are in a Firebase project directory or specify a project.", + { exit: 1 }, + ); + } + + const apps = await listFirebaseApps(setup.projectId, AppPlatform.ANY); + const selectedApp = await selectAppInteractively(apps); + + // Set up the feature info + if (!setup.featureInfo) { + setup.featureInfo = {}; + } + + setup.featureInfo.ailogic = { + appId: selectedApp.appId, + displayName: selectedApp.displayName, + }; +} + +function getAppOptions(appInfo: AppInfo, displayName?: string): ProvisionAppOptions { + switch (appInfo.platform) { + case AppPlatform.IOS: + return { + platform: AppPlatform.IOS, + appId: appInfo.appId, + displayName, + }; + case AppPlatform.ANDROID: + return { + platform: AppPlatform.ANDROID, + appId: appInfo.appId, + displayName, + }; + case AppPlatform.WEB: + return { + platform: AppPlatform.WEB, + appId: appInfo.appId, + displayName, + }; + default: + throw new FirebaseError(`Unsupported platform ${appInfo.platform}`, { exit: 1 }); + } +} + +/** + * AI Logic provisioning: enables AI Logic via API (assumes app and project are already validated) + */ +export async function actuate(setup: Setup): Promise { + const ailogicInfo = setup.featureInfo?.ailogic as AiLogicInfo; + if (!ailogicInfo) { + return; + } + + try { + const appInfo = parseAppId(ailogicInfo.appId); + if (!setup.projectId) { + throw new FirebaseError( + "No project ID found. Please ensure you are in a Firebase project directory or specify a project.", + { exit: 1 }, + ); + } + + // Build provision options and call API directly + const provisionOptions: ProvisionFirebaseAppOptions = { + project: { + parent: { type: "existing_project", projectId: setup.projectId }, + }, + app: getAppOptions(appInfo, ailogicInfo.displayName), + features: { + firebaseAiLogicInput: {}, + }, + }; + + const response = await provisionFirebaseApp(provisionOptions); + + const configFileName = getConfigFileName(appInfo.platform); + const configContent = Buffer.from(response.configData, "base64").toString("utf8"); + + setup.instructions.push( + `Firebase AI Logic has been enabled for existing ${appInfo.platform} app: ${ailogicInfo.appId}`, + `Save the following content as ${configFileName} in your app's root directory:`, + "", + configContent, + "", + "Place this config file in the appropriate location for your platform.", + ); + } catch (error) { + throw new FirebaseError( + `AI Logic setup failed: ${error instanceof Error ? error.message : String(error)}`, + { original: error instanceof Error ? error : new Error(String(error)), exit: 2 }, + ); + } +} diff --git a/src/init/features/ailogic/utils.spec.ts b/src/init/features/ailogic/utils.spec.ts new file mode 100644 index 00000000000..bad040466da --- /dev/null +++ b/src/init/features/ailogic/utils.spec.ts @@ -0,0 +1,214 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as utils from "./utils"; +import * as apps from "../../../management/apps"; +import { AppPlatform } from "../../../management/apps"; +import { FirebaseError } from "../../../error"; + +describe("ailogic utils", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("getConfigFileName", () => { + it("should return correct filename for iOS", () => { + expect(utils.getConfigFileName(AppPlatform.IOS)).to.equal("GoogleService-Info.plist"); + }); + + it("should return correct filename for Android", () => { + expect(utils.getConfigFileName(AppPlatform.ANDROID)).to.equal("google-services.json"); + }); + + it("should return correct filename for Web", () => { + expect(utils.getConfigFileName(AppPlatform.WEB)).to.equal("firebase-config.json"); + }); + + it("should throw error for unsupported platform", () => { + expect(() => utils.getConfigFileName("unsupported" as AppPlatform)).to.throw( + "Unsupported platform: unsupported", + ); + }); + }); + + describe("parseAppId", () => { + it("should parse valid app IDs and return AppInfo object", () => { + const validAppIds = [ + { + appId: "1:123456789:ios:123456789abcdef", + expected: { + projectNumber: "123456789", + appId: "1:123456789:ios:123456789abcdef", + platform: AppPlatform.IOS, + }, + }, + { + appId: "2:123456789:android:123456789abcdef", + expected: { + projectNumber: "123456789", + appId: "2:123456789:android:123456789abcdef", + platform: AppPlatform.ANDROID, + }, + }, + { + appId: "2:123456789:web:123456789abcdef", + expected: { + projectNumber: "123456789", + appId: "2:123456789:web:123456789abcdef", + platform: AppPlatform.WEB, + }, + }, + { + appId: "1:999999999:web:abcdef123456789", + expected: { + projectNumber: "999999999", + appId: "1:999999999:web:abcdef123456789", + platform: AppPlatform.WEB, + }, + }, + ]; + + validAppIds.forEach(({ appId, expected }) => { + const result = utils.parseAppId(appId); + expect(result).to.deep.equal(expected); + }); + }); + + it("should throw error for invalid app ID formats", () => { + const invalidAppIds = [ + "", + ":", + "1:", + "2:123456789", + "2:123456789:", + "2:123456789:test:", + "2:123456789:ios", + "2:123456789:web:", + "2:123456789:android:com_", + "invalid-id", + "1:abc:web:123456789abcdef", // non-numeric project number + "1:123456789:flutter:123456789abcdef", // unsupported platform + ]; + + invalidAppIds.forEach((appId) => { + expect(() => utils.parseAppId(appId)).to.throw(FirebaseError, /Invalid app ID format/); + }); + }); + }); + + describe("validateProjectNumberMatch", () => { + it("should not throw when project numbers match", () => { + const appInfo: utils.AppInfo = { + projectNumber: "123456789", + appId: "1:123456789:web:abcdef", + platform: AppPlatform.WEB, + }; + const projectInfo = { + projectNumber: "123456789", + projectId: "test-project", + name: "projects/test-project", + displayName: "Test Project", + }; + + expect(() => utils.validateProjectNumberMatch(appInfo, projectInfo)).to.not.throw(); + }); + + it("should throw when project numbers don't match", () => { + const appInfo: utils.AppInfo = { + projectNumber: "123456789", + appId: "1:123456789:web:abcdef", + platform: AppPlatform.WEB, + }; + const projectInfo = { + projectNumber: "987654321", + projectId: "test-project", + name: "projects/test-project", + displayName: "Test Project", + }; + + expect(() => utils.validateProjectNumberMatch(appInfo, projectInfo)).to.throw( + FirebaseError, + "App 1:123456789:web:abcdef belongs to project number 123456789 but current project has number 987654321.", + ); + }); + }); + + describe("validateAppExists", () => { + let listFirebaseAppsStub: sinon.SinonStub; + + beforeEach(() => { + listFirebaseAppsStub = sandbox.stub(apps, "listFirebaseApps"); + }); + + it("should not throw when app exists for web platform", async () => { + const appInfo: utils.AppInfo = { + projectNumber: "123456789", + appId: "1:123456789:web:abcdef", + platform: AppPlatform.WEB, + }; + const mockApps = [ + { appId: "1:123456789:web:abcdef", displayName: "Test App", platform: AppPlatform.WEB }, + ]; + listFirebaseAppsStub.resolves(mockApps); + + const result = await utils.validateAppExists(appInfo, "test-project"); + expect(result).to.deep.equal(mockApps[0]); + sinon.assert.calledWith(listFirebaseAppsStub, "test-project", AppPlatform.WEB); + }); + + it("should not throw when app exists for ios platform", async () => { + const appInfo: utils.AppInfo = { + projectNumber: "123456789", + appId: "1:123456789:ios:abcdef", + platform: AppPlatform.IOS, + }; + const mockApps = [ + { appId: "1:123456789:ios:abcdef", displayName: "Test iOS App", platform: AppPlatform.IOS }, + ]; + listFirebaseAppsStub.resolves(mockApps); + + const result = await utils.validateAppExists(appInfo, "test-project"); + expect(result).to.deep.equal(mockApps[0]); + sinon.assert.calledWith(listFirebaseAppsStub, "test-project", AppPlatform.IOS); + }); + + it("should not throw when app exists for android platform", async () => { + const appInfo: utils.AppInfo = { + projectNumber: "123456789", + appId: "1:123456789:android:abcdef", + platform: AppPlatform.ANDROID, + }; + const mockApps = [ + { + appId: "1:123456789:android:abcdef", + displayName: "Test Android App", + platform: AppPlatform.ANDROID, + }, + ]; + listFirebaseAppsStub.resolves(mockApps); + + const result = await utils.validateAppExists(appInfo, "test-project"); + expect(result).to.deep.equal(mockApps[0]); + sinon.assert.calledWith(listFirebaseAppsStub, "test-project", AppPlatform.ANDROID); + }); + + it("should throw when app does not exist", async () => { + const appInfo: utils.AppInfo = { + projectNumber: "123456789", + appId: "1:123456789:web:nonexistent", + platform: AppPlatform.WEB, + }; + listFirebaseAppsStub.resolves([]); + + await expect(utils.validateAppExists(appInfo, "test-project")).to.be.rejectedWith( + FirebaseError, + "App 1:123456789:web:nonexistent does not exist in project test-project.", + ); + }); + }); +}); diff --git a/src/init/features/ailogic/utils.ts b/src/init/features/ailogic/utils.ts new file mode 100644 index 00000000000..bdd87cb96cc --- /dev/null +++ b/src/init/features/ailogic/utils.ts @@ -0,0 +1,106 @@ +import { AppPlatform, listFirebaseApps, AppMetadata } from "../../../management/apps"; +import { FirebaseError } from "../../../error"; +import { FirebaseProjectMetadata } from "../../../types/project"; + +/** + * Returns the Firebase configuration filename for a given platform + */ +export function getConfigFileName(platform: AppPlatform): string { + switch (platform) { + case AppPlatform.IOS: + return "GoogleService-Info.plist"; + case AppPlatform.ANDROID: + return "google-services.json"; + case AppPlatform.WEB: + return "firebase-config.json"; + default: + throw new FirebaseError(`Unsupported platform: ${platform as string}`, { exit: 2 }); + } +} + +export interface AppInfo { + projectNumber: string; + appId: string; + platform: AppPlatform; +} + +/** + * Parses Firebase app ID using official pattern - based on MobilesdkAppId.java + * Format: ::: + */ +export function parseAppId(appId: string): AppInfo { + const pattern = + /^(?\d+):(?\d+):(?ios|android|web):([0-9a-fA-F]+)$/; + const match = pattern.exec(appId); + + if (!match) { + throw new FirebaseError( + `Invalid app ID format: ${appId}. Expected format: 1:PROJECT_NUMBER:PLATFORM:IDENTIFIER`, + { exit: 1 }, + ); + } + + const platformString = match.groups?.platform || ""; + let platform: AppPlatform; + switch (platformString) { + case "ios": + platform = AppPlatform.IOS; + break; + case "android": + platform = AppPlatform.ANDROID; + break; + case "web": + platform = AppPlatform.WEB; + break; + default: + throw new FirebaseError(`Unsupported platform: ${platformString}`, { exit: 1 }); + } + + return { + projectNumber: match.groups?.projectNumber || "", + appId: appId, + platform, + }; +} + +/** + * Verify project number matches app ID's parsed project number + */ +export function validateProjectNumberMatch( + appInfo: AppInfo, + projectInfo: FirebaseProjectMetadata, +): void { + if (projectInfo.projectNumber !== appInfo.projectNumber) { + throw new FirebaseError( + `App ${appInfo.appId} belongs to project number ${appInfo.projectNumber} but current project has number ${projectInfo.projectNumber}.`, + { exit: 1 }, + ); + } +} + +/** + * Validate that app exists + */ +export async function validateAppExists(appInfo: AppInfo, projectId: string): Promise { + try { + // Get apps list to find the specific app with metadata + const apps = await listFirebaseApps(projectId, appInfo.platform); + const app = apps.find((a) => a.appId === appInfo.appId); + + if (!app) { + throw new FirebaseError(`App ${appInfo.appId} does not exist in project ${projectId}.`, { + exit: 1, + }); + } + + return app; + } catch (error) { + if (error instanceof FirebaseError) { + throw error; + } + throw new FirebaseError(`App ${appInfo.appId} does not exist or is not accessible.`, { + exit: 1, + original: error instanceof Error ? error : new Error(String(error)), + }); + } +} diff --git a/src/init/features/aitools.ts b/src/init/features/aitools.ts new file mode 100644 index 00000000000..7725c6792c6 --- /dev/null +++ b/src/init/features/aitools.ts @@ -0,0 +1,113 @@ +import * as utils from "../../utils"; +import { checkbox } from "../../prompt"; +import { Setup } from "../index"; +import { Config } from "../../config"; +import { AI_TOOLS, AIToolChoice } from "./aitools/index"; +import { logger } from "../../logger"; + +interface AgentsInitSelections { + tools?: string[]; +} + +const AGENT_CHOICES: AIToolChoice[] = Object.values(AI_TOOLS).map((tool) => ({ + value: tool.name, + name: tool.displayName, + checked: false, +})); + +export async function doSetup(setup: Setup, config: Config) { + logger.info(); + logger.info( + "This command will configure AI coding assistants to work with your Firebase project by:", + ); + utils.logBullet("• Setting up the Firebase MCP server for direct Firebase operations"); + utils.logBullet("• Installing context files that help AI understand:"); + utils.logBullet(" - Firebase project structure and firebase.json configuration"); + utils.logBullet(" - Common Firebase CLI commands and debugging practices"); + utils.logBullet(" - Product-specific guidance (Functions, Firestore, Hosting, etc.)"); + logger.info(); + + const selections: AgentsInitSelections = {}; + + selections.tools = await checkbox({ + message: "Which tools would you like to configure?", + choices: AGENT_CHOICES, + validate: (choices) => { + if (choices.length === 0) { + return "Must select at least one tool."; + } + return true; + }, + }); + + if (!selections.tools || selections.tools.length === 0) { + return; + } + + logger.info(); + logger.info("Configuring selected tools..."); + + const projectPath = config.projectDir; + const enabledFeatures = getEnabledFeatures(setup.config); + + // Configure each selected tool + let anyUpdates = false; + + for (const toolName of selections.tools) { + const tool = AI_TOOLS[toolName]; + if (!tool) { + utils.logWarning(`Unknown tool: ${toolName}`); + continue; + } + + const result = await tool.configure(config, projectPath, enabledFeatures); + + // Count updated files + const updatedCount = result.files.filter((f) => f.updated).length; + const hasChanges = updatedCount > 0; + + if (hasChanges) { + anyUpdates = true; + logger.info(); + utils.logSuccess( + `${tool.displayName} configured - ${updatedCount} file${updatedCount > 1 ? "s" : ""} updated:`, + ); + } else { + logger.info(); + utils.logBullet(`${tool.displayName} - all files up to date`); + } + + // Always show the file list + for (const file of result.files) { + const status = file.updated ? "(updated)" : "(unchanged)"; + utils.logBullet(` ${file.path} ${status}`); + } + } + + logger.info(); + + if (anyUpdates) { + utils.logSuccess("AI tools configuration complete!"); + logger.info(); + logger.info("Next steps:"); + utils.logBullet("Restart your AI tools to load the new configuration"); + utils.logBullet("Try asking your AI assistant about your Firebase project structure"); + utils.logBullet("AI assistants now understand Firebase CLI commands and debugging"); + } else { + utils.logSuccess("All AI tools are already up to date."); + } +} + +function getEnabledFeatures(config: any): string[] { + const features = []; + if (config.functions) features.push("functions"); + + // Future: Add these when we have corresponding prompt files + // if (config.firestore)) features.push("firestore"); + // if (config.hosting)) features.push("hosting"); + // if (config.storage)) features.push("storage"); + // if (config.database)) features.push("database"); + // if (config.dataconnect)) features.push("dataconnect"); + + return features; +} diff --git a/src/init/features/aitools/claude.ts b/src/init/features/aitools/claude.ts new file mode 100644 index 00000000000..04643bc7fb2 --- /dev/null +++ b/src/init/features/aitools/claude.ts @@ -0,0 +1,63 @@ +import { Config } from "../../../config"; +import { AIToolModule, AIToolConfigResult } from "./types"; +import { updateFirebaseSection } from "./promptUpdater"; + +const MCP_CONFIG_PATH = ".mcp.json"; +const CLAUDE_PROMPT_PATH = "CLAUDE.md"; + +export const claude: AIToolModule = { + name: "claude", + displayName: "Claude Code", + + /** + * Configures Claude Code with Firebase context. + * + * - .mcp.json: Merges with existing MCP server config (preserves user settings) + * - CLAUDE.md: Updates Firebase section only (preserves user content) + */ + async configure( + config: Config, + projectPath: string, + enabledFeatures: string[], + ): Promise { + const files: AIToolConfigResult["files"] = []; + + // Handle MCP configuration in .mcp.json - merge with existing if present + let existingConfig: { mcpServers?: Record } = {}; + let mcpUpdated = false; + try { + const existingContent = config.readProjectFile(MCP_CONFIG_PATH); + if (existingContent) { + existingConfig = JSON.parse(existingContent); + } + } catch (e) { + // File doesn't exist or is invalid JSON, start fresh + } + + // Check if firebase server already exists + if (!existingConfig.mcpServers?.firebase) { + if (!existingConfig.mcpServers) { + existingConfig.mcpServers = {}; + } + existingConfig.mcpServers.firebase = { + command: "npx", + args: ["-y", "firebase-tools", "mcp", "--dir", projectPath], + }; + config.writeProjectFile(MCP_CONFIG_PATH, JSON.stringify(existingConfig, null, 2)); + mcpUpdated = true; + } + + files.push({ path: MCP_CONFIG_PATH, updated: mcpUpdated }); + + const { updated } = await updateFirebaseSection(config, CLAUDE_PROMPT_PATH, enabledFeatures, { + interactive: true, + }); + + files.push({ + path: CLAUDE_PROMPT_PATH, + updated, + }); + + return { files }; + }, +}; diff --git a/src/init/features/aitools/cursor.ts b/src/init/features/aitools/cursor.ts new file mode 100644 index 00000000000..a5bbb74f707 --- /dev/null +++ b/src/init/features/aitools/cursor.ts @@ -0,0 +1,114 @@ +import * as path from "path"; +import { Config } from "../../../config"; +import { readTemplateSync } from "../../../templates"; +import { AIToolModule, AIToolConfigResult } from "./types"; +import { + replaceFirebaseFile, + generatePromptSection, + generateFeaturePromptSection, +} from "./promptUpdater"; + +const CURSOR_MCP_PATH = ".cursor/mcp.json"; +const CURSOR_RULES_DIR = ".cursor/rules"; + +export const cursor: AIToolModule = { + name: "cursor", + displayName: "Cursor", + + /** + * Configures Cursor with Firebase context files. + * + * This function sets up the necessary files for Cursor to understand the + * Firebase project structure and interact with the Firebase CLI. It creates + * a `.cursor` directory with the following: + * + * - `mcp.json`: Configures the Firebase MCP server for direct Firebase operations from Cursor. + * - `rules/FIREBASE.mdc`: The main entry point for project-specific context, importing other rule files. + * - `rules/FIREBASE_BASE.md`: Contains fundamental details about the Firebase project. + * - `rules/FIREBASE_FUNCTIONS.md`: (Optional) Contains information about Cloud Functions if the feature is enabled. + * + * File ownership: + * - .cursor/mcp.json: Merges with existing config (preserves user settings) + * - .cursor/rules/*.md: Fully managed by us (replaced on each update) + * + * We own the entire rules directory, so we can safely replace Firebase-related + * rule files without worrying about user customizations. + */ + async configure( + config: Config, + projectPath: string, + enabledFeatures: string[], + ): Promise { + const files: AIToolConfigResult["files"] = []; + + // Handle MCP configuration - merge with existing if present. + // This allows Cursor to communicate with the Firebase CLI. + let mcpUpdated = false; + let existingMcpConfig: any = {}; + + try { + const existingMcp = config.readProjectFile(CURSOR_MCP_PATH); + if (existingMcp) { + existingMcpConfig = JSON.parse(existingMcp); + } + } catch (e) { + // File doesn't exist or is invalid JSON, start fresh + } + + if (!existingMcpConfig.mcpServers?.firebase) { + if (!existingMcpConfig.mcpServers) { + existingMcpConfig.mcpServers = {}; + } + existingMcpConfig.mcpServers.firebase = { + command: "npx", + args: ["-y", "firebase-tools", "mcp", "--dir", projectPath], + }; + config.writeProjectFile(CURSOR_MCP_PATH, JSON.stringify(existingMcpConfig, null, 2)); + mcpUpdated = true; + } + + files.push({ path: CURSOR_MCP_PATH, updated: mcpUpdated }); + + const header = readTemplateSync("init/aitools/cursor-rules-header.txt"); + + // Create the base Firebase context file (FIREBASE_BASE.md). + // This file contains fundamental details about the Firebase project. + const baseContent = generateFeaturePromptSection("base"); + const basePromptPath = path.join(CURSOR_RULES_DIR, "FIREBASE_BASE.md"); + + const baseResult = await replaceFirebaseFile(config, basePromptPath, baseContent); + files.push({ path: basePromptPath, updated: baseResult.updated }); + + // If Functions are enabled, create the Functions-specific context file. + if (enabledFeatures.includes("functions")) { + const functionsContent = generateFeaturePromptSection("functions"); + const functionsPromptPath = path.join(CURSOR_RULES_DIR, "FIREBASE_FUNCTIONS.md"); + + const functionsResult = await replaceFirebaseFile( + config, + functionsPromptPath, + functionsContent, + ); + files.push({ path: functionsPromptPath, updated: functionsResult.updated }); + } + + // Create the main `FIREBASE.mdc` file, which acts as an entry point + // for Cursor's AI and imports the other context files. + const imports = ["@FIREBASE_BASE.md"]; + if (enabledFeatures.includes("functions")) { + imports.push("@FIREBASE_FUNCTIONS.md"); + } + const importContent = `# Firebase Context\n\n${imports.join("\n")}\n`; + + const { content: mainContent } = generatePromptSection(enabledFeatures, { + customContent: importContent, + }); + const fullContent = header + "\n" + mainContent; + const firebaseMDCPath = path.join(CURSOR_RULES_DIR, "FIREBASE.mdc"); + + const mainResult = await replaceFirebaseFile(config, firebaseMDCPath, fullContent); + files.push({ path: firebaseMDCPath, updated: mainResult.updated }); + + return { files }; + }, +}; diff --git a/src/init/features/aitools/gemini.ts b/src/init/features/aitools/gemini.ts new file mode 100644 index 00000000000..db8776aa357 --- /dev/null +++ b/src/init/features/aitools/gemini.ts @@ -0,0 +1,104 @@ +import { Config } from "../../../config"; +import { readTemplateSync } from "../../../templates"; +import { AIToolModule, AIToolConfigResult } from "./types"; +import { + replaceFirebaseFile, + generatePromptSection, + generateFeaturePromptSection, +} from "./promptUpdater"; +import { deepEqual } from "../../../utils"; + +// Define constants at the module level for clarity and reuse. +const GEMINI_DIR = ".gemini/extensions/firebase"; +const CONTEXTS_DIR = `${GEMINI_DIR}/contexts`; + +export const gemini: AIToolModule = { + name: "gemini", + displayName: "Gemini CLI", + + /** + * Configures the Gemini CLI extension for Firebase. + * + * This function sets up the necessary context files for Gemini to understand the + * Firebase project structure. It creates a `.gemini/extensions/firebase` directory + * with the following files: + * + * - `gemini-extension.json`: The main configuration for the extension. + * - `contexts/FIREBASE.md`: The main entry point for project-specific context. It imports other files. + * - `contexts/FIREBASE-BASE.md`: Contains fundamental details about the Firebase project. + * - `contexts/FIREBASE-FUNCTIONS.md`: (Optional) Contains information about Firebase Functions if the feature is enabled. + * + * File ownership: + * - ALL files under .gemini/extensions/firebase/: Fully managed by us + * + * Since this is a dedicated Firebase extension directory, we own all files + * and can safely replace them without worrying about user customizations. + * Users don't typically edit extension files directly. + */ + async configure( + config: Config, + projectPath: string, + enabledFeatures: string[], + ): Promise { + const files: AIToolConfigResult["files"] = []; + + // Part 1: Configure the main gemini-extension.json file. + const extensionPath = `${GEMINI_DIR}/gemini-extension.json`; + const extensionTemplate = readTemplateSync("init/aitools/gemini-extension.json"); + const newConfigRaw = extensionTemplate.replace("{{PROJECT_PATH}}", projectPath); + + let extensionUpdated = false; + try { + const existingRaw = config.readProjectFile(extensionPath); + const existingConfig = JSON.parse(existingRaw); + const newConfig = JSON.parse(newConfigRaw); + + if (!deepEqual(existingConfig, newConfig)) { + config.writeProjectFile(extensionPath, newConfigRaw); + extensionUpdated = true; + } + } catch { + // File doesn't exist or is invalid JSON, so we (re)create it. + config.writeProjectFile(extensionPath, newConfigRaw); + extensionUpdated = true; + } + files.push({ path: extensionPath, updated: extensionUpdated }); + + // Part 2: Generate feature-specific context files (e.g., FIREBASE-BASE.md). + const baseContent = generateFeaturePromptSection("base"); + const basePath = `${CONTEXTS_DIR}/FIREBASE-BASE.md`; + const baseResult = await replaceFirebaseFile(config, basePath, baseContent); + files.push({ path: basePath, updated: baseResult.updated }); + + // Part 3: Create the main FIREBASE.md file that imports the context files. + const imports = [ + "# Firebase Context", + "", + "", + `@./contexts/FIREBASE-BASE.md`, + ]; + if (enabledFeatures.includes("functions")) { + const functionsContent = generateFeaturePromptSection("functions"); + const functionsPath = `${CONTEXTS_DIR}/FIREBASE-FUNCTIONS.md`; + const functionsResult = await replaceFirebaseFile(config, functionsPath, functionsContent); + files.push({ path: functionsPath, updated: functionsResult.updated }); + + imports.push( + "", + "", + `@./contexts/FIREBASE-FUNCTIONS.md`, + ); + } + const importContent = imports.join("\n"); + + const { content: mainContent } = generatePromptSection(enabledFeatures, { + customContent: importContent, + }); + + const contextPath = `${GEMINI_DIR}/FIREBASE.md`; + const mainResult = await replaceFirebaseFile(config, contextPath, mainContent); + files.push({ path: contextPath, updated: mainResult.updated }); + + return { files }; + }, +}; diff --git a/src/init/features/aitools/index.ts b/src/init/features/aitools/index.ts new file mode 100644 index 00000000000..7111ccca961 --- /dev/null +++ b/src/init/features/aitools/index.ts @@ -0,0 +1,14 @@ +import { AIToolModule } from "./types"; +import { cursor } from "./cursor"; +import { gemini } from "./gemini"; +import { studio } from "./studio"; +import { claude } from "./claude"; + +export const AI_TOOLS: Record = { + cursor, + gemini, + studio, + claude, +}; + +export * from "./types"; diff --git a/src/init/features/aitools/promptUpdater.spec.ts b/src/init/features/aitools/promptUpdater.spec.ts new file mode 100644 index 00000000000..d2e8262bea5 --- /dev/null +++ b/src/init/features/aitools/promptUpdater.spec.ts @@ -0,0 +1,341 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as fs from "fs"; + +import * as prompt from "../../../prompt"; +import { Config } from "../../../config"; +import { + generatePromptSection, + generateFeaturePromptSection, + updateFirebaseSection, + replaceFirebaseFile, + getFeatureContent, +} from "./promptUpdater"; + +describe("promptUpdater", () => { + let sandbox: sinon.SinonSandbox; + let mockConfig: Config; + let readFileSyncStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + mockConfig = { + projectDir: "/test/project", + readProjectFile: sandbox.stub(), + writeProjectFile: sandbox.stub(), + } as any; + + readFileSyncStub = sandbox.stub(fs, "readFileSync"); + readFileSyncStub.withArgs(sinon.match(/FIREBASE\.md$/)).returns(`# Firebase CLI Context + +Base Firebase content`); + readFileSyncStub.withArgs(sinon.match(/FIREBASE_FUNCTIONS\.md$/)).returns(`# Firebase Functions + +Functions specific content`); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("generatePromptSection", () => { + it("should generate content with base features only", () => { + const result = generatePromptSection([]); + + expect(result.content).to.include(" { + const result = generatePromptSection(["functions"]); + + expect(result.content).to.include(" { + const result1 = generatePromptSection(["functions"]); + const result2 = generatePromptSection(["functions"]); + + expect(result1.hash).to.equal(result2.hash); + }); + + it("should generate different hash for different content", () => { + const result1 = generatePromptSection([]); + const result2 = generatePromptSection(["functions"]); + + expect(result1.hash).to.not.equal(result2.hash); + }); + + it("should include raw prompt content without modification", () => { + const result = generatePromptSection([]); + + expect(result.content).to.include("# Firebase CLI Context"); + expect(result.content).to.include("Base Firebase content"); + }); + + it("should generate wrapper with custom content but hash from actual prompts", () => { + const customContent = "Custom import statements"; + const result = generatePromptSection(["functions"], { customContent }); + + expect(result.content).to.include(customContent); + expect(result.content).to.include(" { + const result1 = generatePromptSection(["functions"], { customContent: "Content 1" }); + const result2 = generatePromptSection(["functions"], { customContent: "Content 2" }); + + expect(result1.hash).to.equal(result2.hash); + }); + }); + + describe("updateFirebaseSection", () => { + let sandbox: sinon.SinonSandbox; + let confirmStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + confirmStub = sandbox.stub(prompt, "confirm").resolves(false); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should create new file when none exists", async () => { + (mockConfig.readProjectFile as sinon.SinonStub).throws(new Error("File not found")); + + const result = await updateFirebaseSection(mockConfig, "test.md", []); + + expect(result.updated).to.be.true; + expect((mockConfig.writeProjectFile as sinon.SinonStub).calledOnce).to.be.true; + const writtenContent = (mockConfig.writeProjectFile as sinon.SinonStub).firstCall.args[1]; + expect(writtenContent).to.include(" { + (mockConfig.readProjectFile as sinon.SinonStub).throws(new Error("File not found")); + + const result = await updateFirebaseSection(mockConfig, "test.md", [], { + header: "# Custom Header", + }); + + expect(result.updated).to.be.true; + const writtenContent = (mockConfig.writeProjectFile as sinon.SinonStub).firstCall.args[1]; + expect(writtenContent.startsWith("# Custom Header\n\n { + const { content } = generatePromptSection([]); + (mockConfig.readProjectFile as sinon.SinonStub).returns(content); + + const result = await updateFirebaseSection(mockConfig, "test.md", []); + + expect(result.updated).to.be.false; + expect((mockConfig.writeProjectFile as sinon.SinonStub).called).to.be.false; + }); + + it("should update when content hash differs", async () => { + const existingContent = ` +Old content +`; + (mockConfig.readProjectFile as sinon.SinonStub).returns(existingContent); + + const result = await updateFirebaseSection(mockConfig, "test.md", ["functions"]); + + expect(result.updated).to.be.true; + expect((mockConfig.writeProjectFile as sinon.SinonStub).calledOnce).to.be.true; + const writtenContent = (mockConfig.writeProjectFile as sinon.SinonStub).firstCall.args[1]; + expect(writtenContent).to.include("Functions specific content"); + expect(writtenContent).to.not.include("Old content"); + }); + + it("should append to existing file without firebase section", async () => { + const existingContent = "# User's existing content\n\nSome text"; + (mockConfig.readProjectFile as sinon.SinonStub).returns(existingContent); + + const result = await updateFirebaseSection(mockConfig, "test.md", []); + + expect(result.updated).to.be.true; + const writtenContent = (mockConfig.writeProjectFile as sinon.SinonStub).firstCall.args[1]; + expect(writtenContent.startsWith("# User's existing content")).to.be.true; + expect(writtenContent).to.include(" { + const existingContent = `# User header +Some user content + + +Old Firebase content + + +More user content`; + (mockConfig.readProjectFile as sinon.SinonStub).returns(existingContent); + + const result = await updateFirebaseSection(mockConfig, "test.md", []); + + expect(result.updated).to.be.true; + const writtenContent = (mockConfig.writeProjectFile as sinon.SinonStub).firstCall.args[1]; + expect(writtenContent).to.include("# User header"); + expect(writtenContent).to.include("Some user content"); + expect(writtenContent).to.include("More user content"); + expect(writtenContent).to.include("Base Firebase content"); + expect(writtenContent).to.not.include("Old Firebase content"); + }); + + it("should skip update when interactive and user declines", async () => { + const existingContent = `Old`; + (mockConfig.readProjectFile as sinon.SinonStub).returns(existingContent); + + // Mock the confirm prompt to return false + + const result = await updateFirebaseSection(mockConfig, "test.md", [], { + interactive: true, + }); + + expect(result.updated).to.be.false; + expect((mockConfig.writeProjectFile as sinon.SinonStub).called).to.be.false; + expect(confirmStub).to.have.been.calledOnce; + }); + }); + + describe("replaceFirebaseFile", () => { + it("should create new file when none exists", async () => { + (mockConfig.readProjectFile as sinon.SinonStub).throws(new Error("File not found")); + + const result = await replaceFirebaseFile(mockConfig, "test.md", "New content"); + + expect(result.updated).to.be.true; + expect((mockConfig.writeProjectFile as sinon.SinonStub).calledWith("test.md", "New content")) + .to.be.true; + }); + + it("should not update when content is identical", async () => { + (mockConfig.readProjectFile as sinon.SinonStub).returns("Existing content"); + + const result = await replaceFirebaseFile(mockConfig, "test.md", "Existing content"); + + expect(result.updated).to.be.false; + expect((mockConfig.writeProjectFile as sinon.SinonStub).called).to.be.false; + }); + + it("should update when content differs", async () => { + (mockConfig.readProjectFile as sinon.SinonStub).returns("Old content"); + + const result = await replaceFirebaseFile(mockConfig, "test.md", "New content"); + + expect(result.updated).to.be.true; + expect((mockConfig.writeProjectFile as sinon.SinonStub).calledWith("test.md", "New content")) + .to.be.true; + }); + }); + + describe("generateFeaturePromptSection", () => { + it("should generate wrapped content for base feature", () => { + const content = generateFeaturePromptSection("base"); + + expect(content).to.include(""); + expect(content).to.include("# Firebase CLI Context"); + expect(content).to.include("Base Firebase content"); + }); + + it("should generate wrapped content for functions feature", () => { + const content = generateFeaturePromptSection("functions"); + + expect(content).to.include("", + ); + expect(content).to.include("# Firebase Functions"); + expect(content).to.include("Functions specific content"); + }); + + it("should return empty string for unknown feature", () => { + const content = generateFeaturePromptSection("unknown"); + + expect(content).to.equal(""); + }); + }); + + describe("getFeatureContent", () => { + it("should return raw content for base feature", () => { + const content = getFeatureContent("base"); + + expect(content).to.equal("# Firebase CLI Context\n\nBase Firebase content"); + }); + + it("should return raw content for functions feature", () => { + const content = getFeatureContent("functions"); + + expect(content).to.equal("# Firebase Functions\n\nFunctions specific content"); + }); + + it("should return empty string for unknown feature", () => { + const content = getFeatureContent("unknown"); + + expect(content).to.equal(""); + }); + }); + + describe("hash calculation", () => { + it("should generate 8-character hash", () => { + const { hash } = generatePromptSection([]); + expect(hash).to.have.lengthOf(8); + expect(hash).to.match(/^[a-f0-9]{8}$/); + }); + + it("should be deterministic", () => { + const hash1 = generatePromptSection([]).hash; + const hash2 = generatePromptSection([]).hash; + expect(hash1).to.equal(hash2); + }); + }); + + describe("regex matching", () => { + it("should match firebase_prompts section with hash", async () => { + const content = `Before + +Content + +After`; + (mockConfig.readProjectFile as sinon.SinonStub).returns(content); + + await updateFirebaseSection(mockConfig, "test.md", ["functions"]); + + const writtenContent = (mockConfig.writeProjectFile as sinon.SinonStub).firstCall.args[1]; + expect(writtenContent).to.include("Before"); + expect(writtenContent).to.include("After"); + expect(writtenContent).to.match(/[\s\S]*<\/firebase_prompts>/); + }); + + it("should replace section with missing hash attribute", async () => { + const content = `User content before\n\nOld content without hash\n\nUser content after`; + (mockConfig.readProjectFile as sinon.SinonStub).returns(content); + + const result = await updateFirebaseSection(mockConfig, "test.md", []); + + expect(result.updated).to.be.true; + const writtenContent = (mockConfig.writeProjectFile as sinon.SinonStub).firstCall.args[1]; + expect(writtenContent).to.include("User content before"); + expect(writtenContent).to.include("User content after"); + expect(writtenContent).to.include("([\s\S]*?)<\/firebase_prompts>/; + +const PROMPT_FILES: Record = { + base: "FIREBASE.md", + functions: "FIREBASE_FUNCTIONS.md", + // Future: More Firebase product support + // firestore: "FIREBASE_FIRESTORE.md", + // hosting: "FIREBASE_HOSTING.md", +}; + +function calculateHash(content: string): string { + return crypto.createHash("sha256").update(content.trim()).digest("hex").substring(0, 8); +} + +/** + * Generate Firebase prompt section with proper hash + * @param enabledFeatures - Firebase features to include (functions, firestore, etc.) + * @param options.customContent - Custom content to wrap instead of actual prompts (e.g., file references) + * @returns Firebase section with hash and either actual prompts or custom content + */ +export function generatePromptSection( + enabledFeatures: string[], + options?: { customContent?: string }, +): { content: string; hash: string } { + // Always calculate hash from actual prompts + let fullContent = getFeatureContent("base"); + for (const feature of enabledFeatures) { + if (feature !== "base" && PROMPT_FILES[feature]) { + fullContent += "\n\n" + getFeatureContent(feature); + } + } + + const hash = calculateHash(fullContent); + + // Use custom content if provided, otherwise use the actual prompts + const innerContent = options?.customContent ?? fullContent; + + const wrapped = ` + +${innerContent} +`; + + return { content: wrapped, hash }; +} + +/** + * Update a file with Firebase prompts section, preserving user content + * Used for files like CLAUDE.md and .idx/airules.md + */ +export async function updateFirebaseSection( + config: Config, + filePath: string, + enabledFeatures: string[], + options?: { header?: string; interactive?: boolean }, +): Promise<{ updated: boolean }> { + const { content: newSection, hash: newHash } = generatePromptSection(enabledFeatures); + + let currentContent = ""; + try { + currentContent = config.readProjectFile(filePath) || ""; + } catch { + // File doesn't exist yet + } + + // Check if section exists and has same hash + const match = currentContent.match(FIREBASE_TAG_REGEX); + if (match && match[1] === newHash) { + return { updated: false }; + } + + // Interactive confirmation + if (options?.interactive && currentContent) { + const fileName = filePath.split("/").pop()!; + logger.info(); + utils.logBullet(`Update available for ${fileName}`); + + const shouldUpdate = await confirm({ + message: `Update Firebase section in ${fileName}?`, + default: true, + }); + + if (!shouldUpdate) { + return { updated: false }; + } + } + + // Generate final content + let finalContent: string; + if (!currentContent) { + // New file + finalContent = options?.header ? `${options.header}\n\n${newSection}` : newSection; + } else if (match) { + // Replace existing section + finalContent = + currentContent.substring(0, match.index!) + + newSection + + currentContent.substring(match.index! + match[0].length); + } else { + // Append to existing file + const separator = currentContent.endsWith("\n") ? "\n" : "\n\n"; + finalContent = currentContent + separator + newSection; + } + + config.writeProjectFile(filePath, finalContent); + return { updated: true }; +} + +/** + * Replace an entire prompt file (no user content to preserve) + * Used for files we fully own like Cursor and Gemini configs + */ +export async function replaceFirebaseFile( + config: Config, + filePath: string, + content: string, +): Promise<{ updated: boolean }> { + try { + const existing = config.readProjectFile(filePath); + if (existing === content) { + return { updated: false }; + } + } catch { + // File doesn't exist, will create + } + + config.writeProjectFile(filePath, content); + return { updated: true }; +} + +/** + * Get raw prompt content for a specific feature (without wrapper) + * Used internally for hash calculation + */ +export function getFeatureContent(feature: string): string { + const filename = PROMPT_FILES[feature]; + if (!filename) return ""; + + const PROMPTS_DIR = isVSCodeExtension() ? VSCODE_PROMPTS_DIR : CLI_PROMPTS_DIR; + const content = fs.readFileSync(path.join(PROMPTS_DIR, filename), "utf8"); + return content; +} + +/** + * Generate wrapped content for a specific feature + * Used by Cursor/Gemini for separate feature files + */ +export function generateFeaturePromptSection(feature: string): string { + const content = getFeatureContent(feature); + if (!content) return ""; + + const hash = calculateHash(content); + return ` + +${content} +`; +} diff --git a/src/init/features/aitools/studio.ts b/src/init/features/aitools/studio.ts new file mode 100644 index 00000000000..8157471b399 --- /dev/null +++ b/src/init/features/aitools/studio.ts @@ -0,0 +1,32 @@ +import { Config } from "../../../config"; +import { AIToolModule, AIToolConfigResult } from "./types"; +import { updateFirebaseSection } from "./promptUpdater"; + +const RULES_PATH = ".idx/airules.md"; + +export const studio: AIToolModule = { + name: "studio", + displayName: "Firebase Studio", + + /** + * Configures Firebase Studio (Project IDX) with Firebase context. + * + * - .idx/airules.md: Updates Firebase section only (preserves user content) + * + * Interactive prompts are shown since this file may contain user-defined + * AI rules and instructions that we must preserve. We only manage the + * Firebase-specific section marked with our XML tags. + */ + async configure( + config: Config, + projectPath: string, + enabledFeatures: string[], + ): Promise { + const files: AIToolConfigResult["files"] = []; + const { updated } = await updateFirebaseSection(config, RULES_PATH, enabledFeatures, { + interactive: true, + }); + files.push({ path: RULES_PATH, updated }); + return { files }; + }, +}; diff --git a/src/init/features/aitools/types.ts b/src/init/features/aitools/types.ts new file mode 100644 index 00000000000..a2b4e3881e6 --- /dev/null +++ b/src/init/features/aitools/types.ts @@ -0,0 +1,32 @@ +import { Config } from "../../../config"; + +export interface AIToolConfigResult { + files: Array<{ + path: string; + updated: boolean; + }>; +} + +export interface AIToolModule { + name: string; + displayName: string; + + /** + * Configure the AI tool with Firebase context + * @param config Firebase config object for writing files + * @param projectPath Absolute path to the Firebase project + * @param enabledFeatures List of enabled Firebase features for context optimization + * @returns Result object with update status and list of files created/updated + */ + configure( + config: Config, + projectPath: string, + enabledFeatures: string[], + ): Promise; +} + +export interface AIToolChoice { + value: string; + name: string; + checked: boolean; +} diff --git a/src/init/features/apphosting.spec.ts b/src/init/features/apphosting.spec.ts new file mode 100644 index 00000000000..8453b9ce09d --- /dev/null +++ b/src/init/features/apphosting.spec.ts @@ -0,0 +1,74 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { Config } from "../../config"; +import { upsertAppHostingConfig } from "./apphosting"; + +describe("apphosting", () => { + afterEach(() => { + sinon.verifyAndRestore(); + }); + + describe("upsertAppHostingConfig", () => { + it("creates App Hosting section in firebase.json if no previous config exists", () => { + const config = new Config({}, { projectDir: "test", cwd: "test" }); + const backendConfig = { + backendId: "my-backend", + rootDir: "/", + ignore: [], + }; + + upsertAppHostingConfig(backendConfig, config); + + expect(config.src.apphosting).to.deep.equal(backendConfig); + }); + + it("converts App Hosting config into array when going from one backend to two", () => { + const existingBackendConfig = { + backendId: "my-backend", + rootDir: "/", + ignore: [], + }; + const config = new Config( + { apphosting: existingBackendConfig }, + { projectDir: "test", cwd: "test" }, + ); + const newBackendConfig = { + backendId: "my-backend-1", + rootDir: "/", + ignore: [], + }; + + upsertAppHostingConfig(newBackendConfig, config); + + expect(config.src.apphosting).to.deep.equal([existingBackendConfig, newBackendConfig]); + }); + + it("appends backend config to array if there is already an array", () => { + const appHostingConfig = [ + { + backendId: "my-backend-0", + rootDir: "/", + ignore: [], + }, + { + backendId: "my-backend-1", + rootDir: "/", + ignore: [], + }, + ]; + const config = new Config( + { apphosting: appHostingConfig }, + { projectDir: "test", cwd: "test" }, + ); + const newBackendConfig = { + backendId: "my-backend-2", + rootDir: "/", + ignore: [], + }; + + upsertAppHostingConfig(newBackendConfig, config); + + expect(config.src.apphosting).to.deep.equal([...appHostingConfig, newBackendConfig]); + }); + }); +}).timeout(5000); diff --git a/src/init/features/apphosting.ts b/src/init/features/apphosting.ts new file mode 100644 index 00000000000..936a8a26c45 --- /dev/null +++ b/src/init/features/apphosting.ts @@ -0,0 +1,135 @@ +import * as clc from "colorette"; +import { existsSync } from "fs"; +import * as ora from "ora"; +import * as path from "path"; +import { Setup } from ".."; +import { webApps } from "../../apphosting/app"; +import { + createBackend, + ensureAppHostingComputeServiceAccount, + ensureRequiredApisEnabled, + promptExistingBackend, + promptLocation, + promptNewBackendId, +} from "../../apphosting/backend"; +import { Config } from "../../config"; +import { FirebaseError } from "../../error"; +import { AppHostingSingle } from "../../firebaseConfig"; +import { ensureApiEnabled } from "../../gcp/apphosting"; +import { isBillingEnabled } from "../../gcp/cloudbilling"; +import { input, select } from "../../prompt"; +import { readTemplateSync } from "../../templates"; +import * as utils from "../../utils"; +import { logBullet } from "../../utils"; + +const APPHOSTING_YAML_TEMPLATE = readTemplateSync("init/apphosting/apphosting.yaml"); + +/** + * Set up an apphosting.yaml file for a new App Hosting project. + */ +export async function doSetup(setup: Setup, config: Config): Promise { + const projectId = setup.projectId as string; + if (!(await isBillingEnabled(setup))) { + throw new FirebaseError( + `Firebase App Hosting requires billing to be enabled on your project. To upgrade, visit the following URL: https://console.firebase.google.com/project/${projectId}/usage/details`, + ); + } + await ensureApiEnabled({ projectId }); + await ensureRequiredApisEnabled(projectId); + // N.B. Deploying a backend from source requires the App Hosting compute service + // account to have the storage.objectViewer IAM role. + // + // We don't want to update the IAM permissions right before attempting to deploy, + // since IAM propagation delay will likely cause the first one to fail. However, + // `firebase init apphosting` is a prerequisite to the `firebase deploy` command, + // so we check and add the role here to give the IAM changes time to propagate. + await ensureAppHostingComputeServiceAccount(projectId, /* serviceAccount= */ ""); + + utils.logBullet( + "This command links your local project to Firebase App Hosting. You will be able to deploy your web app with `firebase deploy` after setup.", + ); + const backendConfig: AppHostingSingle = { + backendId: "", + rootDir: "", + ignore: ["node_modules", ".git", "firebase-debug.log", "firebase-debug.*.log", "functions"], + }; + const createOrLink: string = await select({ + default: "Create a new backend", + message: "Please select an option", + choices: [ + { name: "Create a new backend", value: "create" }, + { name: "Link to an existing backend", value: "link" }, + ], + }); + if (createOrLink === "link") { + backendConfig.backendId = await promptExistingBackend( + projectId, + "Which backend would you like to link?", + ); + } else { + logBullet(`${clc.yellow("===")} Set up your backend`); + const location = await promptLocation( + projectId, + "Select a primary region to host your backend:\n", + ); + const backendId = await promptNewBackendId(projectId, location); + utils.logSuccess(`Name set to ${backendId}\n`); + backendConfig.backendId = backendId; + + const webApp = await webApps.getOrCreateWebApp( + projectId, + /* firebaseWebAppId= */ null, + backendId, + ); + if (!webApp) { + utils.logWarning(`Firebase web app not set`); + } + + const createBackendSpinner = ora("Creating your new backend...").start(); + const backend = await createBackend( + projectId, + location, + backendId, + /* serviceAccount= */ null, + /* repository= */ undefined, + webApp?.id, + ); + createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`); + } + + logBullet(`${clc.yellow("===")} Deploy local source setup`); + backendConfig.rootDir = await input({ + default: "/", + message: "Specify your app's root directory relative to your firebase.json directory", + }); + + upsertAppHostingConfig(backendConfig, config); + config.writeProjectFile("firebase.json", config.src); + + utils.logBullet("Writing default settings to " + clc.bold("apphosting.yaml") + "..."); + const absRootDir = path.join(process.cwd(), backendConfig.rootDir); + if (!existsSync(absRootDir)) { + throw new FirebaseError( + `Failed to write apphosting.yaml file because app root directory ${absRootDir} does not exist. Please try again with a valid directory.`, + ); + } + await config.askWriteProjectFile( + path.join(absRootDir, "apphosting.yaml"), + APPHOSTING_YAML_TEMPLATE, + ); + + utils.logSuccess("Firebase initialization complete!"); +} + +/** Exported for unit testing. */ +export function upsertAppHostingConfig(backendConfig: AppHostingSingle, config: Config): void { + if (!config.src.apphosting) { + config.set("apphosting", backendConfig); + return; + } + if (Array.isArray(config.src.apphosting)) { + config.set("apphosting", [...config.src.apphosting, backendConfig]); + return; + } + config.set("apphosting", [config.src.apphosting, backendConfig]); +} diff --git a/src/init/features/apptesting/index.spec.ts b/src/init/features/apptesting/index.spec.ts new file mode 100644 index 00000000000..ba0587b4d31 --- /dev/null +++ b/src/init/features/apptesting/index.spec.ts @@ -0,0 +1,42 @@ +import { expect } from "chai"; +import * as init from "./index"; +import * as sinon from "sinon"; +import * as prompt from "../../../prompt"; +import { Setup } from "../.."; +import { Config } from "../../../config"; + +describe("init apptesting", () => { + afterEach(() => { + sinon.verifyAndRestore(); + }); + + describe("askQuestions", () => { + it("populates apptesting featureInfo", async () => { + const inputStub = sinon.stub(prompt, "input"); + inputStub.withArgs(sinon.match.has("default", "tests")).returns(Promise.resolve("tests")); + const setup: Setup = { featureInfo: {} } as Setup; + + await init.askQuestions(setup); + + expect(setup.featureInfo).to.eql({ apptesting: { testDir: "tests" } }); + }); + }); + + describe("actuate", () => { + it("writes a sample smoke test", async () => { + const setup: Setup = { featureInfo: { apptesting: { testDir: "my_test_dir" } } } as Setup; + const config = new Config({}); + const askWriteProjectFileStub = sinon.stub(config, "askWriteProjectFile"); + askWriteProjectFileStub.returns(Promise.resolve()); + + await init.actuate(setup, config); + + sinon.assert.calledWith( + askWriteProjectFileStub, + "my_test_dir/smoke_test.yaml", + sinon.match.string, + ); + expect(config.get("apptesting.testDir")).to.eql("my_test_dir"); + }); + }); +}); diff --git a/src/init/features/apptesting/index.ts b/src/init/features/apptesting/index.ts new file mode 100644 index 00000000000..198abfca6e1 --- /dev/null +++ b/src/init/features/apptesting/index.ts @@ -0,0 +1,41 @@ +import { join } from "path"; +import { Setup } from "../.."; +import { input } from "../../../prompt"; +import { Config } from "../../../config"; +import { readTemplateSync } from "../../../templates"; +import { ensureProjectConfigured } from "../../../apptesting/ensureProjectConfigured"; + +const SMOKE_TEST_YAML_TEMPLATE = readTemplateSync("init/apptesting/smoke_test.yaml"); + +export interface RequiredInfo { + testDir: string; +} + +// Prompts the developer about the App Testing service they want to init. +export async function askQuestions(setup: Setup): Promise { + setup.featureInfo = { + ...setup.featureInfo, + apptesting: { + testDir: + setup.featureInfo?.apptesting?.testDir || + (await input({ + message: "What do you want to use as your test directory?", + default: "tests", + })), + }, + }; + if (setup.projectId) { + await ensureProjectConfigured(setup.projectId); + } +} + +// Writes App Testing product specific configuration info. +export async function actuate(setup: Setup, config: Config): Promise { + const info = setup.featureInfo?.apptesting; + if (!info) { + throw new Error("App Testing feature RequiredInfo is not provided"); + } + const testDir = info.testDir; + config.set("apptesting.testDir", testDir); + await config.askWriteProjectFile(join(testDir, "smoke_test.yaml"), SMOKE_TEST_YAML_TEMPLATE); +} diff --git a/src/init/features/database.ts b/src/init/features/database.ts index 1cf72685fb4..94275a2df00 100644 --- a/src/init/features/database.ts +++ b/src/init/features/database.ts @@ -1,10 +1,8 @@ -import * as clc from "cli-color"; -import * as api from "../../api"; -import { prompt, promptOnce } from "../../prompt"; +import * as clc from "colorette"; +import { confirm, input, select } from "../../prompt"; import { logger } from "../../logger"; import * as utils from "../../utils"; -import * as fsutils from "../../fsutils"; -import Config = require("../../config"); +import { Config } from "../../config"; import { createInstance, DatabaseInstance, @@ -13,64 +11,60 @@ 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"; +import { Setup } from ".."; -interface DatabaseSetup { - projectId: string; - instance?: string; - config?: DatabaseSetupConfig; +export interface RequiredInfo { + rulesFilename: string; + rules: string; + writeRules: boolean; } -interface DatabaseSetupConfig { - database?: { - rules?: string; - }; - defaultInstanceLocation?: DatabaseLocation; -} +const DEFAULT_RULES_FILENAME = "database.rules.json"; -const DEFAULT_RULES = JSON.stringify( +export 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 -): void { +function writeDBRules(rules: string, filename: string, 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` + `Future modifications to ${clc.bold(filename)} will update Realtime Database Security Rules when you run`, ); logger.info(clc.bold("firebase deploy") + "."); } async function createDefaultDatabaseInstance(project: string): Promise { - const selectedLocation = await promptOnce({ - type: "list", + const selectedLocation = await select({ message: "Please choose the location for your default Realtime Database instance:", choices: [ { name: "us-central1", value: DatabaseLocation.US_CENTRAL1 }, { name: "europe-west1", value: DatabaseLocation.EUROPE_WEST1 }, + { name: "asia-southeast1", value: DatabaseLocation.ASIA_SOUTHEAST1 }, ], }); let instanceName = `${project}-default-rtdb`; @@ -79,21 +73,21 @@ async function createDefaultDatabaseInstance(project: string): Promise { + await ensure(projectId, rtdbManagementOrigin(), "database", false); + logger.info(); + + const instance = await getDefaultDatabaseInstance(projectId); + if (instance !== "") { + return await getDatabaseInstanceDetails(projectId, instance); + } + + const createDefault = await confirm({ + message: + "It seems like you haven’t initialized Realtime Database in your project yet. Do you want to set it up?", + default: true, + }); + + if (createDefault) { + return await createDefaultDatabaseInstance(projectId); + } + + return null; +} + /** * doSetup is the entry point for setting up the database product. * @param setup information helpful for database setup * @param config legacy config parameter. not used for database setup. */ -export async function doSetup(setup: DatabaseSetup, config: Config): Promise { - setup.config = {}; - await ensure(setup.projectId, "firebasedatabase.googleapis.com", "database", false); - logger.info(); - setup.instance = - setup.instance || (await getDefaultDatabaseInstance({ project: setup.projectId })); - let instanceDetails; - if (setup.instance !== "") { - instanceDetails = await getDatabaseInstanceDetails(setup.projectId, setup.instance); - } else { - const confirm = await promptOnce({ - type: "confirm", - name: "confirm", - default: true, - message: - "It seems like you haven’t initialized Realtime Database in your project yet. Do you want to set it up?", - }); - if (confirm) { - instanceDetails = await createDefaultDatabaseInstance(setup.projectId); - } - } - - // Add 'database' section to config - setup.config.database = setup.config.database || {}; - +export async function askQuestions(setup: Setup, config: Config): Promise { logger.info(); logger.info( - "Firebase Realtime Database Security Rules allow you to define how your data should be" + "Firebase Realtime Database Security Rules allow you to define how your data should be", ); logger.info("structured and when your data can be read from and written to."); logger.info(); - await prompt(setup.config.database, [ - { - type: "input", - name: "rules", - message: "What file should be used for Realtime Database Security Rules?", - default: "database.rules.json", - }, - ]); - - const filename = setup.config.database.rules; - if (!filename) { + const rulesFilename = await input({ + message: "What file should be used for Realtime Database Security Rules?", + default: DEFAULT_RULES_FILENAME, + }); + if (!rulesFilename) { throw new FirebaseError("Must specify location for Realtime Database rules file."); } - - let writeRules = true; - if (fsutils.fileExistsSync(filename)) { - const msg = `File ${clc.bold(filename)} already exists. Do you want to overwrite it with ${ - instanceDetails - ? `the Realtime Database Security Rules for ${clc.bold( - instanceDetails.name - )} from the Firebase Console?` - : `default rules?` - }`; - writeRules = await promptOnce({ - type: "confirm", - message: msg, - default: false, - }); - } - if (writeRules) { + const info: RequiredInfo = { + rulesFilename, + rules: DEFAULT_RULES, + writeRules: true, + }; + if (setup.projectId) { + const instanceDetails = await initializeDatabaseInstance(setup.projectId); if (instanceDetails) { - writeDBRules( - await getDBRules(instanceDetails), - `Database Rules for ${instanceDetails.name}`, - filename, - config + info.rules = await getDBRules(instanceDetails); + utils.logBullet( + `Downloaded the existing Realtime Database Security Rules of database ${clc.bold(instanceDetails.name)} from the Firebase console`, ); - return; } - writeDBRules(DEFAULT_RULES, "Default rules", filename, config); - return; } - logger.info("Skipping overwrite of Realtime Database Security Rules."); - logger.info( - `The security rules defined in ${clc.bold(filename)} will be published when you run ${clc.bold( - "firebase deploy" - )}.` - ); - return; + info.writeRules = await config.confirmWriteProjectFile(rulesFilename, info.rules); + // Populate featureInfo for the actuate step later. + setup.featureInfo = setup.featureInfo || {}; + setup.featureInfo.database = info; +} + +export async function actuate(setup: Setup, config: Config): Promise { + const info = setup.featureInfo?.database; + if (!info) { + throw new FirebaseError("No database RequiredInfo found in setup actuate."); + } + // Populate defaults and update `firebase.json` config. + info.rules = info.rules || DEFAULT_RULES; + info.rulesFilename = info.rulesFilename || "database.rules.json"; + setup.config.database = { rules: info.rulesFilename }; + + if (info.writeRules) { + if (info.rules === DEFAULT_RULES) { + writeDBRules(info.rules, info.rulesFilename, config); + } else { + writeDBRules(info.rules, info.rulesFilename, config); + } + } else { + logger.info("Skipping overwrite of Realtime Database Security Rules."); + logger.info( + `The security rules defined in ${clc.bold(info.rulesFilename)} will be published when you run ${clc.bold("firebase deploy")}.`, + ); + } + return Promise.resolve(); } diff --git a/src/init/features/dataconnect/create_app.ts b/src/init/features/dataconnect/create_app.ts new file mode 100644 index 00000000000..85172f461be --- /dev/null +++ b/src/init/features/dataconnect/create_app.ts @@ -0,0 +1,62 @@ +import { spawn } from "child_process"; +import * as clc from "colorette"; +import { logLabeledBullet } from "../../../utils"; + +/** Create a React app using vite react template. */ +export async function createReactApp(webAppId: string): Promise { + const args = ["create", "vite@latest", webAppId, "--", "--template", "react", "--no-interactive"]; + await executeCommand("npm", args); +} + +/** Create a Next.js app using create-next-app. */ +export async function createNextApp(webAppId: string): Promise { + const args = [ + "create-next-app@latest", + webAppId, + "--ts", + "--eslint", + "--tailwind", + "--src-dir", + "--app", + "--turbopack", + "--import-alias", + '"@/*"', + "--skip-install", + ]; + await executeCommand("npx", args); +} + +/** Create a Flutter app using flutter create. */ +export async function createFlutterApp(webAppId: string): Promise { + const args = ["create", webAppId]; + await executeCommand("flutter", args); +} + +// Function to execute a command asynchronously and pipe I/O +async function executeCommand(command: string, args: string[]): Promise { + logLabeledBullet("dataconnect", `> ${clc.bold(`${command} ${args.join(" ")}`)}`); + return new Promise((resolve, reject) => { + // spawn returns a ChildProcess object + const childProcess = spawn(command, args, { + // 'inherit' pipes stdin, stdout, and stderr to the parent process + stdio: "inherit", + // Runs the command in a shell, which allows for shell syntax like pipes, etc. + shell: true, + }); + + childProcess.on("close", (code) => { + if (code === 0) { + // Command executed successfully + resolve(); + } else { + // Command failed + reject(new Error(`Command failed with exit code ${code}`)); + } + }); + + childProcess.on("error", (err) => { + // Handle errors like command not found + reject(err); + }); + }); +} diff --git a/src/init/features/dataconnect/index.spec.ts b/src/init/features/dataconnect/index.spec.ts new file mode 100644 index 00000000000..d51c5cb1559 --- /dev/null +++ b/src/init/features/dataconnect/index.spec.ts @@ -0,0 +1,259 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; +import * as fs from "fs-extra"; + +import * as init from "./index"; +import * as sdk from "./sdk"; +import { Config } from "../../../config"; +import { RCData } from "../../../rc"; +import * as provison from "../../../dataconnect/provisionCloudSql"; +import * as cloudbilling from "../../../gcp/cloudbilling"; +import * as ensureApis from "../../../dataconnect/ensureApis"; +import * as client from "../../../dataconnect/client"; + +const MOCK_RC: RCData = { projects: {}, targets: {}, etags: {} }; + +describe("init dataconnect", () => { + describe.skip("askQuestions", () => { + // TODO: Add unit tests for askQuestions + }); + + describe("actuation", () => { + const sandbox = sinon.createSandbox(); + let provisionCSQLStub: sinon.SinonStub; + let askWriteProjectFileStub: sinon.SinonStub; + let ensureSyncStub: sinon.SinonStub; + let sdkActuateStub: sinon.SinonStub; + + beforeEach(() => { + provisionCSQLStub = sandbox.stub(provison, "setupCloudSql"); + ensureSyncStub = sandbox.stub(fs, "ensureFileSync"); + sdkActuateStub = sandbox.stub(sdk, "actuate").resolves(); + sandbox.stub(cloudbilling, "isBillingEnabled").resolves(true); + sandbox.stub(ensureApis, "ensureApis").resolves(); + sandbox.stub(client, "getSchema").resolves(undefined); + }); + + afterEach(() => { + sandbox.restore(); + }); + + const cases: { + desc: string; + requiredInfo: init.RequiredInfo; + config: Config; + expectedSource: string; + expectedFiles: string[]; + expectCSQLProvisioning: boolean; + expectEnsureSchemaGQL: boolean; + }[] = [ + { + desc: "empty project should generate template", + requiredInfo: mockRequiredInfo(), + config: mockConfig(), + expectedSource: "dataconnect", + expectedFiles: [ + "dataconnect/dataconnect.yaml", + "dataconnect/seed_data.gql", + "dataconnect/schema/schema.gql", + "dataconnect/example/connector.yaml", + "dataconnect/example/queries.gql", + "dataconnect/example/mutations.gql", + ], + expectCSQLProvisioning: true, + expectEnsureSchemaGQL: false, + }, + { + desc: "existing project should use existing directory", + requiredInfo: mockRequiredInfo(), + config: mockConfig({ dataconnect: { source: "not-dataconnect" } }), + expectedSource: "not-dataconnect", + expectedFiles: [ + "not-dataconnect/dataconnect.yaml", + "not-dataconnect/seed_data.gql", + // Populate the default template. + "not-dataconnect/schema/schema.gql", + "not-dataconnect/example/connector.yaml", + "not-dataconnect/example/queries.gql", + "not-dataconnect/example/mutations.gql", + ], + expectCSQLProvisioning: true, + expectEnsureSchemaGQL: false, + }, + { + desc: "should write schema files", + requiredInfo: mockRequiredInfo({ + serviceGql: { + schemaGql: [ + { + path: "schema.gql", + content: "## Fake GQL", + }, + ], + connectors: [], + }, + }), + config: mockConfig({}), + expectedSource: "dataconnect", + expectedFiles: ["dataconnect/dataconnect.yaml", "dataconnect/schema/schema.gql"], + expectCSQLProvisioning: true, + expectEnsureSchemaGQL: false, + }, + { + desc: "should write connector files", + requiredInfo: mockRequiredInfo({ + serviceGql: { + schemaGql: [], + connectors: [ + { + id: "my-connector", + path: "hello", + files: [ + { + path: "queries.gql", + content: "## Fake GQL", + }, + ], + }, + ], + }, + }), + config: mockConfig({}), + expectedSource: "dataconnect", + expectedFiles: [ + "dataconnect/dataconnect.yaml", + "dataconnect/hello/connector.yaml", + "dataconnect/hello/queries.gql", + ], + expectCSQLProvisioning: true, + expectEnsureSchemaGQL: false, + }, + { + desc: "should provision cloudSQL resources ", + requiredInfo: mockRequiredInfo({}), + config: mockConfig({}), + expectedSource: "dataconnect", + expectedFiles: [ + "dataconnect/dataconnect.yaml", + "dataconnect/seed_data.gql", + "dataconnect/schema/schema.gql", + "dataconnect/example/connector.yaml", + "dataconnect/example/queries.gql", + "dataconnect/example/mutations.gql", + ], + expectCSQLProvisioning: true, + expectEnsureSchemaGQL: false, + }, + { + desc: "should handle schema with no files", + requiredInfo: mockRequiredInfo({ + serviceGql: { + schemaGql: [], + connectors: [ + { + id: "my-connector", + path: "hello", + files: [ + { + path: "queries.gql", + content: "## Fake GQL", + }, + ], + }, + ], + }, + }), + config: mockConfig({ + dataconnect: { + source: "dataconnect", + }, + }), + expectedSource: "dataconnect", + expectedFiles: [ + "dataconnect/dataconnect.yaml", + "dataconnect/hello/connector.yaml", + "dataconnect/hello/queries.gql", + ], + expectCSQLProvisioning: true, + expectEnsureSchemaGQL: true, + }, + ]; + + for (const c of cases) { + it(c.desc, async () => { + askWriteProjectFileStub = sandbox.stub(c.config, "askWriteProjectFile"); + askWriteProjectFileStub.resolves(); + provisionCSQLStub.resolves(); + await init.actuate( + { + projectId: "test-project", + rcfile: MOCK_RC, + config: c.config.src, + featureInfo: { dataconnect: c.requiredInfo, dataconnectSdk: { apps: [] } }, + instructions: [], + }, + c.config, + {}, + ); + expect(c.config.get("dataconnect.source")).to.equal(c.expectedSource); + if (c.expectEnsureSchemaGQL) { + expect(ensureSyncStub).to.have.been.calledWith("dataconnect/schema/schema.gql"); + } + expect(askWriteProjectFileStub.args.map((a) => a[0])).to.deep.equal(c.expectedFiles); + expect(provisionCSQLStub.called).to.equal(c.expectCSQLProvisioning); + expect(sdkActuateStub.called).to.be.true; + }); + } + }); + + describe("toDNSCompatibleId", () => { + const cases: { description: string; input: string; expected: string }[] = [ + { + description: "Should noop compatible strings", + input: "this-is-compatible", + expected: "this-is-compatible", + }, + { + description: "Should lower case", + input: "This-Is-Compatible", + expected: "this-is-compatible", + }, + { + description: "Should strip special characters", + input: "this-is-compatible?~!@#$%^&*()_+=", + expected: "this-is-compatible", + }, + { + description: "Should strip trailing and leading -", + input: "---this-is-compatible---", + expected: "this-is-compatible", + }, + { + description: "Should cut to 63 characters", + input: "a".repeat(1000), + expected: "a".repeat(63), + }, + ]; + for (const c of cases) { + it(c.description, () => { + expect(init.toDNSCompatibleId(c.input)).to.equal(c.expected); + }); + } + }); +}); + +function mockConfig(data: Record = {}): Config { + return new Config(data, { projectDir: "." }); +} +function mockRequiredInfo(info: Partial = {}): init.RequiredInfo { + return { + analyticsFlow: "test", + appDescription: "", + serviceId: "test-service", + locationId: "europe-north3", + cloudSqlInstanceId: "csql-instance", + cloudSqlDatabase: "csql-db", + shouldProvisionCSQL: true, + ...info, + }; +} diff --git a/src/init/features/dataconnect/index.ts b/src/init/features/dataconnect/index.ts new file mode 100644 index 00000000000..66ab9543271 --- /dev/null +++ b/src/init/features/dataconnect/index.ts @@ -0,0 +1,722 @@ +import { join, basename } from "path"; +import * as clc from "colorette"; +import * as fs from "fs-extra"; + +import { input, select, confirm } from "../../../prompt"; +import { Config } from "../../../config"; +import { Setup } from "../.."; +import { setupCloudSql } from "../../../dataconnect/provisionCloudSql"; +import { checkFreeTrialInstanceUsed, upgradeInstructions } from "../../../dataconnect/freeTrial"; +import * as cloudsql from "../../../gcp/cloudsql/cloudsqladmin"; +import { ensureApis, ensureGIFApiTos } from "../../../dataconnect/ensureApis"; +import { + listLocations, + listAllServices, + getSchema, + listConnectors, + createService, + upsertSchema, +} from "../../../dataconnect/client"; +import { Schema, Service, File, SCHEMA_ID } from "../../../dataconnect/types"; +import { parseCloudSQLInstanceName, parseServiceName } from "../../../dataconnect/names"; +import { logger } from "../../../logger"; +import { readTemplateSync } from "../../../templates"; +import { + logBullet, + logWarning, + envOverride, + promiseWithSpinner, + logLabeledError, + newUniqueId, +} from "../../../utils"; +import { isBillingEnabled } from "../../../gcp/cloudbilling"; +import * as sdk from "./sdk"; +import { + generateOperation, + generateSchema, + PROMPT_GENERATE_CONNECTOR, + PROMPT_GENERATE_SEED_DATA, +} from "../../../gemini/fdcExperience"; +import { configstore } from "../../../configstore"; +import { trackGA4 } from "../../../track"; + +// Default GCP region for Data Connect +export const FDC_DEFAULT_REGION = "us-east4"; + +const DATACONNECT_YAML_TEMPLATE = readTemplateSync("init/dataconnect/dataconnect.yaml"); +const CONNECTOR_YAML_TEMPLATE = readTemplateSync("init/dataconnect/connector.yaml"); +const SCHEMA_TEMPLATE = readTemplateSync("init/dataconnect/schema.gql"); +const QUERIES_TEMPLATE = readTemplateSync("init/dataconnect/queries.gql"); +const MUTATIONS_TEMPLATE = readTemplateSync("init/dataconnect/mutations.gql"); +const SEED_DATA_TEMPLATE = readTemplateSync("init/dataconnect/seed_data.gql"); + +export interface RequiredInfo { + // The GA analytics metric to track how developers go through `init dataconnect`. + analyticsFlow: string; + appDescription: string; + serviceId: string; + locationId: string; + cloudSqlInstanceId: string; + cloudSqlDatabase: string; + // If true, we should provision a new Cloud SQL instance. + shouldProvisionCSQL: boolean; + // If present, this is downloaded from an existing deployed service. + serviceGql?: ServiceGQL; +} + +export interface ServiceGQL { + schemaGql: File[]; + connectors: { + id: string; + path: string; + files: File[]; + }[]; + seedDataGql?: string; +} + +const templateServiceInfo: ServiceGQL = { + schemaGql: [{ path: "schema.gql", content: SCHEMA_TEMPLATE }], + connectors: [ + { + id: "example", + path: "./example", + files: [ + { + path: "queries.gql", + content: QUERIES_TEMPLATE, + }, + { + path: "mutations.gql", + content: MUTATIONS_TEMPLATE, + }, + ], + }, + ], + seedDataGql: SEED_DATA_TEMPLATE, +}; + +// askQuestions prompts the user about the Data Connect service they want to init. Any prompting +// logic should live here, and _no_ actuation logic should live here. +export async function askQuestions(setup: Setup): Promise { + const info: RequiredInfo = { + analyticsFlow: "cli", + appDescription: "", + serviceId: "", + locationId: "", + cloudSqlInstanceId: "", + cloudSqlDatabase: "", + shouldProvisionCSQL: false, + }; + if (setup.projectId) { + const hasBilling = await isBillingEnabled(setup); + await ensureApis(setup.projectId); + await promptForExistingServices(setup, info); + if (!info.serviceGql) { + // TODO: Consider use Gemini to generate schema for Spark project as well. + if (!configstore.get("gemini")) { + logBullet( + "Learn more about Gemini in Firebase and how it uses your data: https://firebase.google.com/docs/gemini-in-firebase#how-gemini-in-firebase-uses-your-data", + ); + } + info.appDescription = await input({ + message: `Describe your app to automatically generate a schema with Gemini [Enter to use a template]:`, + }); + if (info.appDescription) { + configstore.set("gemini", true); + await ensureGIFApiTos(setup.projectId); + } + } + if (hasBilling) { + await promptForCloudSQL(setup, info); + } else if (info.appDescription) { + await promptForLocation(setup, info); + } + } + setup.featureInfo = setup.featureInfo || {}; + setup.featureInfo.dataconnect = info; + + await sdk.askQuestions(setup); +} + +// actuate writes product specific files and makes product specifc API calls. +// It does not handle writing firebase.json and .firebaserc +export async function actuate(setup: Setup, config: Config, options: any): Promise { + // Most users will want to persist data between emulator runs, so set this to a reasonable default. + const dir: string = config.get("dataconnect.source", "dataconnect"); + const dataDir = config.get("emulators.dataconnect.dataDir", `${dir}/.dataconnect/pgliteData`); + config.set("emulators.dataconnect.dataDir", dataDir); + + const info = setup.featureInfo?.dataconnect; + if (!info) { + throw new Error("Data Connect feature RequiredInfo is not provided"); + } + // Populate the default values of required fields. + info.serviceId = info.serviceId || defaultServiceId(); + info.cloudSqlInstanceId = info.cloudSqlInstanceId || `${info.serviceId.toLowerCase()}-fdc`; + info.locationId = info.locationId || FDC_DEFAULT_REGION; + info.cloudSqlDatabase = info.cloudSqlDatabase || `fdcdb`; + + try { + await actuateWithInfo(setup, config, info, options); + await sdk.actuate(setup, config); + } finally { + void trackGA4("dataconnect_init", { + project_status: setup.projectId ? (setup.isBillingEnabled ? "blaze" : "spark") : "missing", + flow: info.analyticsFlow, + provision_cloud_sql: String(info.shouldProvisionCSQL), + }); + } + + if (info.appDescription) { + setup.instructions.push( + `You can visualize the Data Connect Schema in Firebase Console: + + https://console.firebase.google.com/project/${setup.projectId!}/dataconnect/locations/${info.locationId}/services/${info.serviceId}/schema`, + ); + } + if (!setup.isBillingEnabled) { + setup.instructions.push(upgradeInstructions(setup.projectId || "your-firebase-project")); + } + setup.instructions.push( + `Install the Data Connect VS Code Extensions. You can explore Data Connect Query on local pgLite and Cloud SQL Postgres Instance.`, + ); +} + +async function actuateWithInfo( + setup: Setup, + config: Config, + info: RequiredInfo, + options: any, +): Promise { + const projectId = setup.projectId; + if (!projectId) { + // If no project is present, just save the template files. + info.analyticsFlow += "_save_template"; + return await writeFiles(config, info, templateServiceInfo, options); + } + + await ensureApis(projectId, /* silent =*/ true); + const provisionCSQL = info.shouldProvisionCSQL && (await isBillingEnabled(setup)); + if (provisionCSQL) { + // Kicks off Cloud SQL provisioning if the project has billing enabled. + await setupCloudSql({ + projectId: projectId, + location: info.locationId, + instanceId: info.cloudSqlInstanceId, + databaseId: info.cloudSqlDatabase, + requireGoogleMlIntegration: false, + }); + } + + const serviceName = `projects/${projectId}/locations/${info.locationId}/services/${info.serviceId}`; + if (!info.appDescription) { + if (!info.serviceGql) { + // Try download the existing service if it exists. + // MCP tool `firebase_init` may setup an existing service. + await downloadService(info, serviceName); + } + if (info.serviceGql) { + // Save the downloaded service from the backend. + info.analyticsFlow += "_save_downloaded"; + return await writeFiles(config, info, info.serviceGql, options); + } + // Use the static template if it starts from scratch or the existing service has no GQL source. + info.analyticsFlow += "_save_template"; + return await writeFiles(config, info, templateServiceInfo, options); + } + const serviceAlreadyExists = !(await createService(projectId, info.locationId, info.serviceId)); + + // Use Gemini to generate schema. + const schemaGql = await promiseWithSpinner( + () => generateSchema(info.appDescription, projectId), + "Generating the Data Connect Schema...", + ); + const schemaFiles = [{ path: "schema.gql", content: schemaGql }]; + + if (serviceAlreadyExists) { + // If the service already exists, fallback to save only the generated schema. + // Later customer can run `firebase deploy` to override the existing service. + // + // - CLI cmd `firebase init dataconnect` always picks a new service ID, so it should never hit this case. + // - MCP tool `firebase_init` may pick an existing service ID, but shouldn't set app_description at the same time. + logLabeledError( + "dataconnect", + `Data Connect Service ${serviceName} already exists. Skip saving them...`, + ); + info.analyticsFlow += "_save_gemini_service_already_exists"; + return await writeFiles(config, info, { schemaGql: schemaFiles, connectors: [] }, options); + } + + // Create the initial Data Connect Service and Schema generated by Gemini. + await promiseWithSpinner(async () => { + const [saveSchemaGql, waitForCloudSQLProvision] = schemasDeploySequence( + projectId, + info, + schemaFiles, + provisionCSQL, + ); + await upsertSchema(saveSchemaGql); + if (waitForCloudSQLProvision) { + // Kicks off the LRO in the background. It will take about 10min. Don't wait for it. + void upsertSchema(waitForCloudSQLProvision); + } + }, "Saving the Data Connect Schema..."); + + try { + // Generate the example Data Connect Connector and seed_data.gql with Gemini. + // Save them to local file, but don't deploy it because they may have errors. + const [operationGql, seedDataGql] = await promiseWithSpinner( + () => + Promise.all([ + generateOperation(PROMPT_GENERATE_CONNECTOR, serviceName, projectId), + generateOperation(PROMPT_GENERATE_SEED_DATA, serviceName, projectId), + ]), + "Generating the Data Connect Operations...", + ); + const connectors = [ + { + id: "example", + path: "./example", + files: [ + { + path: "queries.gql", + content: operationGql, + }, + ], + }, + ]; + info.analyticsFlow += "_save_gemini"; + await writeFiles( + config, + info, + { schemaGql: schemaFiles, connectors: connectors, seedDataGql: seedDataGql }, + options, + ); + } catch (err: any) { + logLabeledError("dataconnect", `Operation Generation failed...`); + // GiF generate operation API has stability concerns. + // Fallback to save only the generated schema. + info.analyticsFlow += "_save_gemini_operation_error"; + await writeFiles(config, info, { schemaGql: schemaFiles, connectors: [] }, options); + throw err; + } +} + +function schemasDeploySequence( + projectId: string, + info: RequiredInfo, + schemaFiles: File[], + linkToCloudSql: boolean, +): Schema[] { + const serviceName = `projects/${projectId}/locations/${info.locationId}/services/${info.serviceId}`; + if (!linkToCloudSql) { + // No Cloud SQL is being provisioned, just deploy the schema sources as a unlinked schema. + return [ + { + name: `${serviceName}/schemas/${SCHEMA_ID}`, + datasources: [{ postgresql: {} }], + source: { + files: schemaFiles, + }, + }, + ]; + } + // Cloud SQL is being provisioned at the same time. + // Persist the Cloud SQL schema associated with this FDC service, then start a LRO (`MIGRATE_COMPATIBLE`) + // wait for Cloud SQL provision to finish and setup its initial SQL schemas. + return [ + { + name: `${serviceName}/schemas/${SCHEMA_ID}`, + datasources: [ + { + postgresql: { + database: info.cloudSqlDatabase, + cloudSql: { + instance: `projects/${projectId}/locations/${info.locationId}/instances/${info.cloudSqlInstanceId}`, + }, + schemaValidation: "NONE", + }, + }, + ], + source: { + files: schemaFiles, + }, + }, + { + name: `${serviceName}/schemas/${SCHEMA_ID}`, + datasources: [ + { + postgresql: { + database: info.cloudSqlDatabase, + cloudSql: { + instance: `projects/${projectId}/locations/${info.locationId}/instances/${info.cloudSqlInstanceId}`, + }, + schemaMigration: "MIGRATE_COMPATIBLE", + }, + }, + ], + source: { + files: schemaFiles, + }, + }, + ]; +} + +async function writeFiles( + config: Config, + info: RequiredInfo, + serviceGql: ServiceGQL, + options: any, +): Promise { + const dir: string = config.get("dataconnect.source") || "dataconnect"; + const subbedDataconnectYaml = subDataconnectYamlValues({ + ...info, + connectorDirs: serviceGql.connectors.map((c) => c.path), + }); + config.set("dataconnect", { source: dir }); + await config.askWriteProjectFile( + join(dir, "dataconnect.yaml"), + subbedDataconnectYaml, + !!options.force, + // Default to override dataconnect.yaml + // Sole purpose of `firebase init dataconnect` is to update `dataconnect.yaml`. + true, + ); + if (serviceGql.seedDataGql) { + await config.askWriteProjectFile( + join(dir, "seed_data.gql"), + serviceGql.seedDataGql, + !!options.force, + ); + } + + if (serviceGql.schemaGql.length) { + for (const f of serviceGql.schemaGql) { + await config.askWriteProjectFile(join(dir, "schema", f.path), f.content, !!options.force); + } + } else { + // Even if the schema is empty, lets give them an empty .gql file to get started. + fs.ensureFileSync(join(dir, "schema", "schema.gql")); + } + + for (const c of serviceGql.connectors) { + await writeConnectorFiles(config, c, options); + } +} + +async function writeConnectorFiles( + config: Config, + connectorInfo: { + id: string; + path: string; + files: File[]; + }, + options: any, +) { + const subbedConnectorYaml = subConnectorYamlValues({ connectorId: connectorInfo.id }); + const dir: string = config.get("dataconnect.source") || "dataconnect"; + await config.askWriteProjectFile( + join(dir, connectorInfo.path, "connector.yaml"), + subbedConnectorYaml, + !!options.force, + // Default to override connector.yaml + true, + ); + for (const f of connectorInfo.files) { + await config.askWriteProjectFile( + join(dir, connectorInfo.path, f.path), + f.content, + !!options.force, + ); + } +} + +function subDataconnectYamlValues(replacementValues: { + serviceId: string; + cloudSqlInstanceId: string; + cloudSqlDatabase: string; + connectorDirs: string[]; + locationId: string; +}): string { + const replacements: Record = { + serviceId: "__serviceId__", + locationId: "__location__", + cloudSqlDatabase: "__cloudSqlDatabase__", + cloudSqlInstanceId: "__cloudSqlInstanceId__", + connectorDirs: "__connectorDirs__", + }; + let replaced = DATACONNECT_YAML_TEMPLATE; + for (const [k, v] of Object.entries(replacementValues)) { + replaced = replaced.replace(replacements[k], JSON.stringify(v)); + } + return replaced; +} + +function subConnectorYamlValues(replacementValues: { connectorId: string }): string { + const replacements: Record = { + connectorId: "__connectorId__", + }; + let replaced = CONNECTOR_YAML_TEMPLATE; + for (const [k, v] of Object.entries(replacementValues)) { + replaced = replaced.replace(replacements[k], JSON.stringify(v)); + } + return replaced; +} + +async function promptForExistingServices(setup: Setup, info: RequiredInfo): Promise { + // Check for existing Firebase Data Connect services. + if (!setup.projectId) { + return; + } + const existingServices = await listAllServices(setup.projectId); + if (!existingServices.length) { + return; + } + const choice = await chooseExistingService(existingServices); + if (!choice) { + const existingServiceIds = existingServices.map((s) => s.name.split("/").pop()!); + info.serviceId = newUniqueId(defaultServiceId(), existingServiceIds); + info.analyticsFlow += "_pick_new_service"; + return; + } + // Choose to use an existing service. + info.analyticsFlow += "_pick_existing_service"; + const serviceName = parseServiceName(choice.name); + info.serviceId = serviceName.serviceId; + info.locationId = serviceName.location; + await downloadService(info, choice.name); +} + +async function downloadService(info: RequiredInfo, serviceName: string): Promise { + const schema = await getSchema(serviceName); + if (!schema) { + return; + } + info.serviceGql = { + schemaGql: [], + connectors: [ + { + id: "example", + path: "./example", + files: [], + }, + ], + }; + const primaryDatasource = schema.datasources.find((d) => d.postgresql); + if (primaryDatasource?.postgresql?.cloudSql?.instance) { + const instanceName = parseCloudSQLInstanceName(primaryDatasource.postgresql.cloudSql.instance); + info.cloudSqlInstanceId = instanceName.instanceId; + } + if (schema.source.files?.length) { + info.serviceGql.schemaGql = schema.source.files; + } + info.cloudSqlDatabase = primaryDatasource?.postgresql?.database ?? ""; + const connectors = await listConnectors(serviceName, [ + "connectors.name", + "connectors.source.files", + ]); + if (connectors.length) { + info.serviceGql.connectors = connectors.map((c) => { + const id = c.name.split("/").pop()!; + return { + id, + path: connectors.length === 1 ? "./example" : `./${id}`, + files: c.source.files || [], + }; + }); + } +} + +/** + * Picks create new service or an existing service from the list of services. + * + * Firebase Console can provide `FDC_CONNECTOR` or `FDC_SERVICE` environment variable. + * If either is present, chooseExistingService try to match it with any existing service + * and short-circuit the prompt. + * + * `FDC_SERVICE` should have the format `/`. + * `FDC_CONNECTOR` should have the same `//`. + * @param existing + */ +async function chooseExistingService(existing: Service[]): Promise { + const fdcConnector = envOverride("FDC_CONNECTOR", ""); + const fdcService = envOverride("FDC_SERVICE", ""); + const serviceEnvVar = fdcConnector || fdcService; + if (serviceEnvVar) { + const [serviceLocationFromEnvVar, serviceIdFromEnvVar] = serviceEnvVar.split("/"); + const serviceFromEnvVar = existing.find((s) => { + const serviceName = parseServiceName(s.name); + return ( + serviceName.serviceId === serviceIdFromEnvVar && + serviceName.location === serviceLocationFromEnvVar + ); + }); + if (serviceFromEnvVar) { + logBullet( + `Picking up the existing service ${clc.bold(serviceLocationFromEnvVar + "/" + serviceIdFromEnvVar)}.`, + ); + return serviceFromEnvVar; + } + const envVarName = fdcConnector ? "FDC_CONNECTOR" : "FDC_SERVICE"; + logWarning(`Unable to pick up an existing service based on ${envVarName}=${serviceEnvVar}.`); + } + const choices: Array<{ name: string; value: Service | undefined }> = existing.map((s) => { + const serviceName = parseServiceName(s.name); + return { + name: `${serviceName.location}/${serviceName.serviceId}`, + value: s, + }; + }); + choices.push({ name: "Create a new service", value: undefined }); + return await select({ + message: + "Your project already has existing services. Which would you like to set up local files for?", + choices, + }); +} + +async function promptForCloudSQL(setup: Setup, info: RequiredInfo): Promise { + if (!setup.projectId) { + return; + } + // Check for existing Cloud SQL instances, if we didn't already set one. + if (info.cloudSqlInstanceId === "") { + const instances = await cloudsql.listInstances(setup.projectId); + let choices = instances.map((i) => { + let display = `${i.name} (${i.region})`; + if (i.settings.userLabels?.["firebase-data-connect"] === "ft") { + display += " (no cost trial)"; + } + return { name: display, 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) { + if (!(await checkFreeTrialInstanceUsed(setup.projectId))) { + choices.push({ name: "Create a new free trial instance", value: "", location: "" }); + } else { + choices.push({ name: "Create a new CloudSQL instance", value: "", location: "" }); + } + info.cloudSqlInstanceId = await select({ + message: `Which CloudSQL instance would you like to use?`, + choices, + }); + if (info.cloudSqlInstanceId !== "") { + info.analyticsFlow += "_pick_existing_csql"; + // Infer location if a CloudSQL instance is chosen. + info.locationId = choices.find((c) => c.value === info.cloudSqlInstanceId)!.location; + } else { + info.analyticsFlow += "_pick_new_csql"; + info.cloudSqlInstanceId = await input({ + message: `What ID would you like to use for your new CloudSQL instance?`, + default: newUniqueId( + `${defaultServiceId().toLowerCase()}-fdc`, + instances.map((i) => i.name), + ), + }); + } + } + } + + if (info.locationId === "") { + await promptForLocation(setup, info); + info.shouldProvisionCSQL = await confirm({ + message: `Would you like to provision your Cloud SQL instance and database now?`, + default: true, + }); + } + + // The Gemini generated schema will override any SQL schema in this Postgres database. + // To avoid accidental data loss, we pick a new database ID if `listDatabases` is available. + if (info.cloudSqlInstanceId !== "" && info.cloudSqlDatabase === "") { + try { + const dbs = await cloudsql.listDatabases(setup.projectId, info.cloudSqlInstanceId); + const existing = dbs.map((d) => d.name); + info.cloudSqlDatabase = newUniqueId("fdcdb", existing); + } 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}`); + } + } + return; +} + +async function promptForLocation(setup: Setup, info: RequiredInfo): Promise { + if (info.locationId === "") { + const choices = await locationChoices(setup); + info.locationId = await select({ + message: "What location should the new Cloud SQL instance be in?", + choices, + default: FDC_DEFAULT_REGION, + }); + } +} + +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: "asia-east1", value: "asia-east1" }, + { name: "asia-east2", value: "asia-east2" }, + { name: "asia-northeast1", value: "asia-northeast1" }, + { name: "asia-northeast2", value: "asia-northeast2" }, + { name: "asia-northeast3", value: "asia-northeast3" }, + { name: "asia-south1", value: "asia-south1" }, + { name: "asia-southeast1", value: "asia-southeast1" }, + { name: "asia-southeast2", value: "asia-southeast2" }, + { name: "australia-southeast1", value: "australia-southeast1" }, + { name: "australia-southeast2", value: "australia-southeast2" }, + { name: "europe-central2", value: "europe-central2" }, + { name: "europe-north1", value: "europe-north1" }, + { name: "europe-southwest1", value: "europe-southwest1" }, + { name: "europe-west1", value: "europe-west1" }, + { name: "europe-west2", value: "europe-west2" }, + { name: "europe-west3", value: "europe-west3" }, + { name: "europe-west4", value: "europe-west4" }, + { name: "europe-west6", value: "europe-west6" }, + { name: "europe-west8", value: "europe-west8" }, + { name: "europe-west9", value: "europe-west9" }, + { name: "me-west1", value: "me-west1" }, + { name: "northamerica-northeast1", value: "northamerica-northeast1" }, + { name: "northamerica-northeast2", value: "northamerica-northeast2" }, + { name: "southamerica-east1", value: "southamerica-east1" }, + { name: "southamerica-west1", value: "southamerica-west1" }, + { name: "us-central1", value: "us-central1" }, + { name: "us-east1", value: "us-east1" }, + { name: "us-east4", value: "us-east4" }, + { name: "us-south1", value: "us-south1" }, + { name: "us-west1", value: "us-west1" }, + { name: "us-west2", value: "us-west2" }, + { name: "us-west3", value: "us-west3" }, + { name: "us-west4", value: "us-west4" }, + ]; + } +} + +function defaultServiceId(): string { + return toDNSCompatibleId(basename(process.cwd())); +} + +/** + * Converts any string to a DNS friendly service ID. + */ +export function toDNSCompatibleId(id: string): string { + id = basename(id) + .toLowerCase() + .replaceAll(/[^a-z0-9-]/g, "") + .slice(0, 63); + while (id.endsWith("-") && id.length) { + id = id.slice(0, id.length - 1); + } + while (id.startsWith("-") && id.length) { + id = id.slice(1, id.length); + } + return id || "app"; +} +export { newUniqueId }; diff --git a/src/init/features/dataconnect/sdk.spec.ts b/src/init/features/dataconnect/sdk.spec.ts new file mode 100644 index 00000000000..47ff26b3fa2 --- /dev/null +++ b/src/init/features/dataconnect/sdk.spec.ts @@ -0,0 +1,561 @@ +import * as chai from "chai"; +import * as clc from "colorette"; +import * as yaml from "js-yaml"; +import { + addSdkGenerateToConnectorYaml, + chooseApp, + askQuestions, + actuate, + FDC_SDK_PLATFORM_ENV, + FDC_SDK_FRAMEWORKS_ENV, + FDC_APP_FOLDER, +} from "./sdk"; +import { Setup } from "../.."; +import { ConnectorInfo, ConnectorYaml } from "../../../dataconnect/types"; +import { App, Framework, Platform } from "../../../appUtils"; +import * as appUtils from "../../../appUtils"; +import * as createApp from "./create_app"; +import * as sinon from "sinon"; +import { Config } from "../../../config"; +import { DataConnectEmulator } from "../../../emulator/dataconnectEmulator"; +import * as dcLoad from "../../../dataconnect/load"; +import * as fsutils from "../../../fsutils"; +import * as auth from "../../../auth"; +import * as utils from "../../../utils"; +import * as prompt from "../../../prompt"; + +const expect = chai.expect; + +describe("addSdkGenerateToConnectorYaml", () => { + let connectorInfo: ConnectorInfo; + let connectorYaml: ConnectorYaml; + let app: App; + + beforeEach(() => { + connectorInfo = { + directory: "/users/test/project/dataconnect", + connectorYaml: { + connectorId: "test-connector", + }, + connector: {} as any, + }; + connectorYaml = { + connectorId: "test-connector", + }; + app = { + directory: "/users/test/project/app", + platform: Platform.WEB, + frameworks: [], + }; + }); + + it("should add javascriptSdk for web platform", () => { + addSdkGenerateToConnectorYaml(connectorInfo, connectorYaml, app); + expect(connectorYaml.generate?.javascriptSdk).to.deep.equal([ + { + outputDir: "../app/src/dataconnect-generated", + package: "@dataconnect/generated", + packageJsonDir: "../app", + react: false, + angular: false, + }, + ]); + }); + + it("should add javascriptSdk with react for web platform", () => { + app.frameworks = [Framework.REACT]; + addSdkGenerateToConnectorYaml(connectorInfo, connectorYaml, app); + expect(connectorYaml.generate?.javascriptSdk).to.deep.equal([ + { + outputDir: "../app/src/dataconnect-generated", + package: "@dataconnect/generated", + packageJsonDir: "../app", + react: true, + angular: false, + }, + ]); + }); + + it("should add dartSdk for flutter platform", () => { + app.platform = Platform.FLUTTER; + addSdkGenerateToConnectorYaml(connectorInfo, connectorYaml, app); + expect(connectorYaml.generate?.dartSdk).to.deep.equal([ + { + outputDir: "../app/lib/dataconnect_generated", + package: "dataconnect_generated", + }, + ]); + }); + + it("should add kotlinSdk for android platform", () => { + app.platform = Platform.ANDROID; + addSdkGenerateToConnectorYaml(connectorInfo, connectorYaml, app); + expect(connectorYaml.generate?.kotlinSdk).to.deep.equal([ + { + outputDir: "../app/src/main/kotlin", + package: "com.google.firebase.dataconnect.generated", + }, + ]); + }); + + it("should add swiftSdk for ios platform", () => { + app.platform = Platform.IOS; + addSdkGenerateToConnectorYaml(connectorInfo, connectorYaml, app); + expect(connectorYaml.generate?.swiftSdk).to.deep.equal([ + { + outputDir: "../FirebaseDataConnectGenerated", + package: "DataConnectGenerated", + }, + ]); + }); +}); + +describe("chooseApp", () => { + let detectAppsStub: sinon.SinonStub; + let promptStub: sinon.SinonStub; + + beforeEach(() => { + detectAppsStub = sinon.stub(appUtils, "detectApps"); + promptStub = sinon.stub(prompt, "checkbox"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should prompt user to choose from multiple apps", async () => { + const apps: App[] = [ + { platform: Platform.WEB, directory: "web", frameworks: [Framework.REACT] }, + { platform: Platform.ANDROID, directory: "android" }, + ]; + detectAppsStub.resolves(apps); + promptStub.resolves([ + { platform: Platform.WEB, directory: "web", frameworks: [Framework.REACT] }, + ]); + + const selectedApps = await chooseApp(); + + expect(selectedApps).to.deep.equal([apps[0]]); + expect(promptStub.calledOnce).to.be.true; + }); + + it("should use app from environment variables", async () => { + process.env[FDC_APP_FOLDER] = "web"; + process.env[FDC_SDK_PLATFORM_ENV] = "WEB"; + const apps: App[] = [ + { platform: Platform.WEB, directory: "web", frameworks: [Framework.REACT] }, + { platform: Platform.ANDROID, directory: "android" }, + ]; + detectAppsStub.resolves(apps); + + const selectedApps = await chooseApp(); + + expect(selectedApps).to.have.deep.members([apps[0]]); + expect(promptStub.called).to.be.false; + + delete process.env[FDC_APP_FOLDER]; + delete process.env[FDC_SDK_PLATFORM_ENV]; + }); + + it("should return a placeholder when no app matches environment variables", async () => { + process.env[FDC_APP_FOLDER] = "web"; + process.env[FDC_SDK_PLATFORM_ENV] = "WEB"; + process.env[FDC_SDK_FRAMEWORKS_ENV] = "react,next"; + const apps: App[] = [ + { platform: Platform.IOS, directory: "ios" }, + { platform: Platform.ANDROID, directory: "android" }, + ]; + detectAppsStub.resolves(apps); + + const selectedApps = await chooseApp(); + + expect(selectedApps).to.have.deep.members([ + { + platform: Platform.WEB, + directory: "web", + frameworks: ["react", "next"], + }, + ]); + expect(promptStub.called).to.be.false; + + delete process.env[FDC_APP_FOLDER]; + delete process.env[FDC_SDK_PLATFORM_ENV]; + delete process.env[FDC_SDK_FRAMEWORKS_ENV]; + }); + + it("should return empty array when no apps are found", async () => { + detectAppsStub.resolves([]); + + const selectedApps = await chooseApp(); + + expect(selectedApps).to.be.empty; + expect(promptStub.called).to.be.false; + }); + + it("should deduplicate apps with the same platform and directory", async () => { + const apps: App[] = [ + { platform: Platform.WEB, directory: "web", frameworks: [Framework.REACT], appId: "app1" }, + { platform: Platform.WEB, directory: "web", frameworks: [Framework.REACT], appId: "app2" }, + { platform: Platform.ANDROID, directory: "android" }, + ]; + const uniqueApps: App[] = [ + { platform: Platform.WEB, directory: "web", frameworks: [Framework.REACT] }, + { platform: Platform.ANDROID, directory: "android" }, + ]; + detectAppsStub.resolves(apps); + promptStub.resolves([ + { platform: Platform.WEB, directory: "web", frameworks: [Framework.REACT] }, + ]); + + const selectedApps = await chooseApp(); + + expect(promptStub.callCount).to.equal(1); + const promptCall = promptStub.getCall(0); + const appsPassedToCheckbox = promptCall.args[0].choices.map( + (choice: prompt.Choice) => choice.value, + ); + expect(appsPassedToCheckbox).to.have.deep.members(uniqueApps); + + expect(selectedApps).to.deep.equal([ + { platform: Platform.WEB, directory: "web", frameworks: [Framework.REACT] }, + ]); + }); +}); + +describe("askQuestions", () => { + let detectAppsStub: sinon.SinonStub; + let selectStub: sinon.SinonStub; + let createReactAppStub: sinon.SinonStub; + let createNextAppStub: sinon.SinonStub; + let createFlutterAppStub: sinon.SinonStub; + let setup: Setup; + + beforeEach(() => { + detectAppsStub = sinon.stub(appUtils, "detectApps"); + selectStub = sinon.stub(prompt, "select"); + createReactAppStub = sinon.stub(createApp, "createReactApp"); + createNextAppStub = sinon.stub(createApp, "createNextApp"); + createFlutterAppStub = sinon.stub(createApp, "createFlutterApp"); + setup = { + config: {} as any, + rcfile: {} as any, + featureInfo: { + dataconnectSdk: { + apps: [], + }, + }, + instructions: [], + }; + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should call createReactApp when user chooses react", async () => { + detectAppsStub.resolves([]); + selectStub.resolves("react"); + createReactAppStub.resolves(); + + await askQuestions(setup); + + expect(selectStub.calledOnce).to.be.true; + expect(createReactAppStub.calledOnce).to.be.true; + expect(createNextAppStub.called).to.be.false; + expect(createFlutterAppStub.called).to.be.false; + }); + + it("should call createNextApp when user chooses next", async () => { + detectAppsStub.resolves([]); + selectStub.resolves("next"); + createNextAppStub.resolves(); + + await askQuestions(setup); + + expect(selectStub.calledOnce).to.be.true; + expect(createReactAppStub.called).to.be.false; + expect(createNextAppStub.calledOnce).to.be.true; + expect(createFlutterAppStub.called).to.be.false; + }); + + it("should call createFlutterApp when user chooses flutter", async () => { + detectAppsStub.resolves([]); + selectStub.resolves("flutter"); + createFlutterAppStub.resolves(); + + await askQuestions(setup); + + expect(selectStub.calledOnce).to.be.true; + expect(createReactAppStub.called).to.be.false; + expect(createNextAppStub.called).to.be.false; + expect(createFlutterAppStub.calledOnce).to.be.true; + }); + + it("should not prompt to create a new sample app if apps are found", async () => { + const apps: App[] = [ + { platform: Platform.WEB, directory: "web", frameworks: [Framework.REACT] }, + ]; + detectAppsStub.resolves(apps); + + await askQuestions(setup); + + expect(selectStub.called).to.be.false; + expect(createReactAppStub.called).to.be.false; + expect(createNextAppStub.called).to.be.false; + expect(createFlutterAppStub.called).to.be.false; + expect(setup.featureInfo?.dataconnectSdk?.apps).to.deep.equal(apps); + }); +}); + +describe("actuate", () => { + let setup: Setup; + let config: Config; + let detectAppsStub: sinon.SinonStub; + let loadAllStub: sinon.SinonStub; + let dirExistsSyncStub: sinon.SinonStub; + let writeProjectFileStub: sinon.SinonStub; + let generateStub: sinon.SinonStub; + let getGlobalDefaultAccountStub: sinon.SinonStub; + let logLabeledBulletStub: sinon.SinonStub; + let logLabeledSuccessStub: sinon.SinonStub; + let logLabeledErrorStub: sinon.SinonStub; + let logLabeledWarningStub: sinon.SinonStub; + let logBulletStub: sinon.SinonStub; + + beforeEach(() => { + detectAppsStub = sinon.stub(appUtils, "detectApps"); + loadAllStub = sinon.stub(dcLoad, "loadAll"); + dirExistsSyncStub = sinon.stub(fsutils, "dirExistsSync"); + writeProjectFileStub = sinon.stub(); + generateStub = sinon.stub(DataConnectEmulator, "generate"); + getGlobalDefaultAccountStub = sinon.stub(auth, "getGlobalDefaultAccount"); + logLabeledBulletStub = sinon.stub(utils, "logLabeledBullet"); + logLabeledSuccessStub = sinon.stub(utils, "logLabeledSuccess"); + logLabeledErrorStub = sinon.stub(utils, "logLabeledError"); + logLabeledWarningStub = sinon.stub(utils, "logLabeledWarning"); + logBulletStub = sinon.stub(utils, "logBullet"); + + setup = { + config: { projectDir: "/path/to/project" } as any, + rcfile: {} as any, + featureInfo: { + dataconnectSdk: { + apps: [], + }, + }, + instructions: [], + }; + config = { + writeProjectFile: writeProjectFileStub, + projectDir: "/path/to/project", + get: () => ({}), + set: () => ({}), + has: () => true, + path: (p: string) => p, + readProjectFile: () => ({}), + projectFileExists: () => true, + deleteProjectFile: () => ({}), + confirmWriteProjectFile: async () => true, + askWriteProjectFile: async () => ({}), + } as unknown as Config; + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should log a message and exit if no apps are found", async () => { + detectAppsStub.resolves([]); + + await actuate(setup, config); + + expect( + logLabeledBulletStub.calledWith( + "dataconnect", + "No apps to setup Data Connect Generated SDKs", + ), + ).to.be.true; + expect(writeProjectFileStub.called).to.be.false; + expect(generateStub.called).to.be.false; + }); + + it("should detect apps if none are provided in setup", async () => { + const apps: App[] = [{ platform: Platform.WEB, directory: "web", frameworks: [] }]; + detectAppsStub.resolves(apps); + loadAllStub.resolves([ + { + connectorInfo: [ + { + directory: "dataconnect", + connectorYaml: { connectorId: "test-connector" }, + }, + ], + dataConnectYaml: { location: "us-central1", serviceId: "test-service" }, + }, + ]); + dirExistsSyncStub.returns(true); + getGlobalDefaultAccountStub.resolves({ email: "test@google.com" }); + + await actuate(setup, config); + + expect(writeProjectFileStub.calledOnce).to.be.true; + expect(generateStub.calledOnce).to.be.true; + }); + + it("should set up SDKs for provided apps", async () => { + const apps: App[] = [ + { platform: Platform.WEB, directory: "webDir", frameworks: [] }, + { platform: Platform.ANDROID, directory: "androidDir" }, + { platform: Platform.IOS, directory: "iosDir" }, + ]; + setup.featureInfo?.dataconnectSdk?.apps.push(...apps); + loadAllStub.resolves([ + { + connectorInfo: [ + { + directory: "dataconnect", + connectorYaml: { connectorId: "test-connector" }, + }, + ], + dataConnectYaml: { location: "us-central1", serviceId: "test-service" }, + }, + ]); + dirExistsSyncStub.returns(true); + getGlobalDefaultAccountStub.resolves({ email: "test@google.com" }); + + await actuate(setup, config); + + expect(writeProjectFileStub.calledOnce).to.be.true; + expect(generateStub.calledOnce).to.be.true; + expect(logLabeledSuccessStub.calledOnce).to.be.true; + expect( + logLabeledSuccessStub.calledWith( + "dataconnect", + `Installed generated SDKs for ${clc.bold("webDir (web), androidDir (android), iosDir (ios)")}`, + ), + ).to.be.true; + }); + + it("should warn if an app directory does not exist", async () => { + const apps: App[] = [{ platform: Platform.WEB, directory: "web", frameworks: [] }]; + setup.featureInfo?.dataconnectSdk?.apps.push(...apps); + loadAllStub.resolves([ + { + connectorInfo: [ + { + directory: "dataconnect", + connectorYaml: { connectorId: "test-connector" }, + }, + ], + dataConnectYaml: { location: "us-central1", serviceId: "test-service" }, + }, + ]); + dirExistsSyncStub.returns(false); + getGlobalDefaultAccountStub.resolves({ email: "test@google.com" }); + + await actuate(setup, config); + + expect(logLabeledWarningStub.calledWith("dataconnect", "App directory web does not exist")).to + .be.true; + expect(writeProjectFileStub.calledOnce).to.be.true; + expect(generateStub.calledOnce).to.be.true; + }); + + it("should handle SDK generation failure", async () => { + const apps: App[] = [{ platform: Platform.WEB, directory: "web", frameworks: [] }]; + setup.featureInfo?.dataconnectSdk?.apps.push(...apps); + loadAllStub.resolves([ + { + connectorInfo: [ + { + directory: "dataconnect", + connectorYaml: { connectorId: "test-connector" }, + }, + ], + dataConnectYaml: { location: "us-central1", serviceId: "test-service" }, + }, + ]); + dirExistsSyncStub.returns(true); + getGlobalDefaultAccountStub.resolves({ email: "test@google.com" }); + generateStub.throws(new Error("SDK generation failed")); + + await actuate(setup, config); + + expect(writeProjectFileStub.calledOnce).to.be.true; + expect( + logLabeledErrorStub.calledWith( + "dataconnect", + "Failed to generate Data Connect SDKs\nSDK generation failed", + ), + ).to.be.true; + }); + + it("should log platform-specific instructions", async () => { + const apps: App[] = [ + { platform: Platform.IOS, directory: "ios" }, + { platform: Platform.WEB, directory: "web-react", frameworks: [Framework.REACT] }, + { platform: Platform.WEB, directory: "web-angular", frameworks: [Framework.ANGULAR] }, + ]; + setup.featureInfo?.dataconnectSdk?.apps.push(...apps); + loadAllStub.resolves([ + { + connectorInfo: [ + { + directory: "dataconnect", + connectorYaml: { connectorId: "test-connector" }, + }, + ], + dataConnectYaml: { location: "us-central1", serviceId: "test-service" }, + }, + ]); + dirExistsSyncStub.returns(true); + getGlobalDefaultAccountStub.resolves({ email: "test@google.com" }); + + await actuate(setup, config); + + expect( + logBulletStub.calledWith( + clc.bold( + "Please follow the instructions here to add your generated sdk to your XCode project:\n\thttps://firebase.google.com/docs/data-connect/ios-sdk#set-client", + ), + ), + ).to.be.true; + expect( + logBulletStub.calledWith( + "Visit https://firebase.google.com/docs/data-connect/web-sdk#react for more information on how to set up React Generated SDKs for Firebase Data Connect", + ), + ).to.be.true; + expect( + logBulletStub.calledWith( + "Run `ng add @angular/fire` to install angular sdk dependencies.\nVisit https://github.com/invertase/tanstack-query-firebase/tree/main/packages/angular for more information on how to set up Angular Generated SDKs for Firebase Data Connect", + ), + ).to.be.true; + }); + + it("should deduplicate apps when writing connector.yaml", async () => { + const apps: App[] = [ + { platform: Platform.WEB, directory: "web", frameworks: [], appId: "app1" }, + { platform: Platform.WEB, directory: "web", frameworks: [], appId: "app2" }, + ]; + setup.featureInfo?.dataconnectSdk?.apps.push(...apps); + loadAllStub.resolves([ + { + connectorInfo: [ + { + directory: "dataconnect", + connectorYaml: { connectorId: "test-connector" }, + }, + ], + dataConnectYaml: { location: "us-central1", serviceId: "test-service" }, + }, + ]); + dirExistsSyncStub.returns(true); + getGlobalDefaultAccountStub.resolves({ email: "test@google.com" }); + + await actuate(setup, config); + + const writtenYaml = writeProjectFileStub.getCall(0).args[1]; + const parsedYaml = yaml.load(writtenYaml); + expect(parsedYaml.generate.javascriptSdk).to.have.lengthOf(1); + }); +}); diff --git a/src/init/features/dataconnect/sdk.ts b/src/init/features/dataconnect/sdk.ts new file mode 100644 index 00000000000..8bc6ab36419 --- /dev/null +++ b/src/init/features/dataconnect/sdk.ts @@ -0,0 +1,412 @@ +import * as yaml from "yaml"; +import * as clc from "colorette"; +import * as path from "path"; + +const cwd = process.cwd(); + +import { checkbox, select } from "../../../prompt"; +import { Config } from "../../../config"; +import { Setup } from "../.."; +import { loadAll } from "../../../dataconnect/load"; +import { + ConnectorInfo, + ConnectorYaml, + DartSDK, + JavascriptSDK, + KotlinSDK, +} from "../../../dataconnect/types"; +import { FirebaseError } from "../../../error"; +import { isArray } from "lodash"; +import { + logBullet, + envOverride, + logWarning, + logLabeledSuccess, + logLabeledWarning, + logLabeledBullet, + newUniqueId, + logLabeledError, + commandExistsSync, +} from "../../../utils"; +import { detectApps, appDescription, Platform, App, Framework } from "../../../appUtils"; +import { DataConnectEmulator } from "../../../emulator/dataconnectEmulator"; +import { getGlobalDefaultAccount } from "../../../auth"; +import { createFlutterApp, createNextApp, createReactApp } from "./create_app"; +import { trackGA4 } from "../../../track"; +import { dirExistsSync, listFiles } from "../../../fsutils"; + +export const FDC_APP_FOLDER = "FDC_APP_FOLDER"; +export const FDC_SDK_FRAMEWORKS_ENV = "FDC_SDK_FRAMEWORKS"; +export const FDC_SDK_PLATFORM_ENV = "FDC_SDK_PLATFORM"; + +export interface SdkRequiredInfo { + apps: App[]; +} + +export type SDKInfo = { + connectorYamlContents: string; + connectorInfo: ConnectorInfo; + displayIOSWarning: boolean; +}; + +export async function askQuestions(setup: Setup): Promise { + const info: SdkRequiredInfo = { + apps: [], + }; + + info.apps = await chooseApp(); + if (!info.apps.length) { + const npxMissingWarning = commandExistsSync("npx") + ? "" + : clc.yellow(" (you need to install Node.js first)"); + const flutterMissingWarning = commandExistsSync("flutter") + ? "" + : clc.yellow(" (you need to install Flutter first)"); + + const choice = await select({ + message: `Do you want to create an app template?`, + choices: [ + // TODO: Create template tailored to FDC. + { name: `React${npxMissingWarning}`, value: "react" }, + { name: `Next.JS${npxMissingWarning}`, value: "next" }, + { name: `Flutter${flutterMissingWarning}`, value: "flutter" }, + { name: "no", value: "no" }, + ], + }); + try { + switch (choice) { + case "react": + await createReactApp(newUniqueId("web-app", listFiles(cwd))); + break; + case "next": + await createNextApp(newUniqueId("web-app", listFiles(cwd))); + break; + case "flutter": + await createFlutterApp(newUniqueId("flutter_app", listFiles(cwd))); + break; + case "no": + break; + } + } catch (err: unknown) { + // The detailed error message are already piped into stderr. No need to repeat here. + logLabeledError("dataconnect", `Failed to create a ${choice} app template`); + } + } + + setup.featureInfo = setup.featureInfo || {}; + setup.featureInfo.dataconnectSdk = info; +} + +export async function chooseApp(): Promise { + let apps = dedupeAppsByPlatformAndDirectory(await detectApps(cwd)); + if (apps.length) { + logLabeledSuccess( + "dataconnect", + `Detected existing apps ${apps.map((a) => appDescription(a)).join(", ")}`, + ); + } else { + logLabeledWarning("dataconnect", "No app exists in the current directory."); + } + // Check for environment variables override. + const envAppFolder = envOverride(FDC_APP_FOLDER, ""); + const envPlatform: Platform = envOverride(FDC_SDK_PLATFORM_ENV, "") as Platform; + const envFrameworks: Framework[] = envOverride(FDC_SDK_FRAMEWORKS_ENV, "") + .split(",") + .filter((f) => !!f) + .map((f) => f as Framework); + if (envAppFolder && envPlatform) { + // Resolve the relative path to the app directory + const envAppRelDir = path.relative(cwd, path.resolve(cwd, envAppFolder)); + const matchedApps = apps.filter( + (app) => app.directory === envAppRelDir && (!app.platform || app.platform === envPlatform), + ); + if (matchedApps.length) { + for (const a of matchedApps) { + a.frameworks = [...(a.frameworks || []), ...envFrameworks]; + } + return matchedApps; + } + return [ + { + platform: envPlatform, + directory: envAppRelDir, + frameworks: envFrameworks, + }, + ]; + } + if (apps.length >= 2) { + const choices = apps.map((a) => { + return { + name: appDescription(a), + value: a, + checked: a.directory === ".", + }; + }); + const pickedApps = await checkbox({ + message: "Which apps do you want to set up Data Connect SDKs in?", + choices, + }); + if (!pickedApps || !pickedApps.length) { + throw new FirebaseError("Command Aborted. Please choose at least one app."); + } + apps = pickedApps; + } + return apps; +} + +export async function actuate(setup: Setup, config: Config) { + const fdcInfo = setup.featureInfo?.dataconnect; + const sdkInfo = setup.featureInfo?.dataconnectSdk; + if (!sdkInfo) { + throw new Error("Data Connect SDK feature RequiredInfo is not provided"); + } + try { + await actuateWithInfo(setup, config, sdkInfo); + } finally { + let flow = "no_app"; + if (sdkInfo.apps.length) { + const platforms = sdkInfo.apps.map(appDescription).sort(); + flow = `${platforms.join("_")}_app`; + } + if (fdcInfo) { + fdcInfo.analyticsFlow += `_${flow}`; + } else { + void trackGA4("dataconnect_init", { + project_status: setup.projectId ? (setup.isBillingEnabled ? "blaze" : "spark") : "missing", + flow: `cli_sdk_${flow}`, + }); + } + } +} + +async function actuateWithInfo(setup: Setup, config: Config, info: SdkRequiredInfo) { + if (!info.apps.length) { + // If no apps is specified, try to detect it again. + // In `firebase init dataconnect:sdk`, customer may create the app while the command is running. + // The `firebase_init` MCP tool always pass an empty `apps` list, it should setup all apps detected. + info.apps = await detectApps(cwd); + if (!info.apps.length) { + logLabeledBullet("dataconnect", "No apps to setup Data Connect Generated SDKs"); + return; + } + } + + // detectApps creates unique apps by appId and bundleId, but this method operates + // on platform, directory, and frameworks alone. Deduping here to retain the + // same behavior + const apps = dedupeAppsByPlatformAndDirectory(info.apps); + const connectorInfo = await chooseExistingConnector(setup, config); + const connectorYaml = JSON.parse(JSON.stringify(connectorInfo.connectorYaml)) as ConnectorYaml; + for (const app of apps) { + if (!dirExistsSync(app.directory)) { + logLabeledWarning("dataconnect", `App directory ${app.directory} does not exist`); + } + addSdkGenerateToConnectorYaml(connectorInfo, connectorYaml, app); + } + + // TODO: Prompt user about adding generated paths to .gitignore + const connectorYamlContents = yaml.stringify(connectorYaml); + connectorInfo.connectorYaml = connectorYaml; + + const connectorYamlPath = `${connectorInfo.directory}/connector.yaml`; + config.writeProjectFile( + path.relative(config.projectDir, connectorYamlPath), + connectorYamlContents, + ); + + logLabeledBullet("dataconnect", `Installing the generated SDKs ...`); + const account = getGlobalDefaultAccount(); + try { + await DataConnectEmulator.generate({ + configDir: connectorInfo.directory, + account, + }); + } catch (e: any) { + logLabeledError("dataconnect", `Failed to generate Data Connect SDKs\n${e?.message}`); + } + + logLabeledSuccess( + "dataconnect", + `Installed generated SDKs for ${clc.bold(apps.map((a) => appDescription(a)).join(", "))}`, + ); + if (apps.some((a) => a.platform === Platform.IOS)) { + logBullet( + clc.bold( + "Please follow the instructions here to add your generated sdk to your XCode project:\n\thttps://firebase.google.com/docs/data-connect/ios-sdk#set-client", + ), + ); + } + if (apps.some((a) => a.frameworks?.includes(Framework.REACT))) { + logBullet( + "Visit https://firebase.google.com/docs/data-connect/web-sdk#react for more information on how to set up React Generated SDKs for Firebase Data Connect", + ); + } + if (apps.some((a) => a.frameworks?.includes(Framework.ANGULAR))) { + logBullet( + "Run `ng add @angular/fire` to install angular sdk dependencies.\nVisit https://github.com/invertase/tanstack-query-firebase/tree/main/packages/angular for more information on how to set up Angular Generated SDKs for Firebase Data Connect", + ); + } +} + +interface connectorChoice { + name: string; // {location}/{serviceId}/{connectorId} + value: ConnectorInfo; +} + +/** + * Picks an existing connector from those present in the local workspace. + * + * Firebase Console can provide `FDC_CONNECTOR` environment variable. + * If its is present, chooseExistingConnector try to match it with any existing connectors + * and short-circuit the prompt. + * + * `FDC_CONNECTOR` should have the same `//`. + * @param choices + */ +async function chooseExistingConnector(setup: Setup, config: Config): Promise { + const serviceInfos = await loadAll(setup.projectId || "", config); + const choices: connectorChoice[] = serviceInfos + .map((si) => { + return si.connectorInfo.map((ci) => { + return { + name: `${si.dataConnectYaml.location}/${si.dataConnectYaml.serviceId}/${ci.connectorYaml.connectorId}`, + value: ci, + }; + }); + }) + .flat(); + if (!choices.length) { + throw new FirebaseError( + `No Firebase Data Connect workspace found. Run ${clc.bold("firebase init dataconnect")} to set up a service and connector.`, + ); + } + if (choices.length === 1) { + // Only one connector available, use it. + return choices[0].value; + } + const connectorEnvVar = envOverride("FDC_CONNECTOR", ""); + if (connectorEnvVar) { + const existingConnector = choices.find((c) => c.name === connectorEnvVar); + if (existingConnector) { + logBullet(`Picking up the existing connector ${clc.bold(connectorEnvVar)}.`); + return existingConnector.value; + } + logWarning( + `Unable to pick up an existing connector based on FDC_CONNECTOR=${connectorEnvVar}.`, + ); + } + logWarning( + `Pick up the first connector ${clc.bold(connectorEnvVar)}. Use FDC_CONNECTOR to override it`, + ); + return choices[0].value; +} + +/** add SDK generation configuration to connector.yaml in place */ +export function addSdkGenerateToConnectorYaml( + connectorInfo: ConnectorInfo, + connectorYaml: ConnectorYaml, + app: App, +): void { + const connectorDir = connectorInfo.directory; + const appDir = app.directory; + if (!connectorYaml.generate) { + connectorYaml.generate = {}; + } + const generate = connectorYaml.generate; + + switch (app.platform) { + case Platform.WEB: { + const javascriptSdk: JavascriptSDK = { + outputDir: path.relative(connectorDir, path.join(appDir, `src/dataconnect-generated`)), + package: `@dataconnect/generated`, + packageJsonDir: path.relative(connectorDir, appDir), + react: false, + angular: false, + }; + for (const f of app.frameworks || []) { + javascriptSdk[f] = true; + } + if (!isArray(generate?.javascriptSdk)) { + generate.javascriptSdk = generate.javascriptSdk ? [generate.javascriptSdk] : []; + } + if (!generate.javascriptSdk.some((s) => s.outputDir === javascriptSdk.outputDir)) { + generate.javascriptSdk.push(javascriptSdk); + } + break; + } + case Platform.FLUTTER: { + const dartSdk: DartSDK = { + outputDir: path.relative(connectorDir, path.join(appDir, `lib/dataconnect_generated`)), + package: "dataconnect_generated", + }; + if (!isArray(generate?.dartSdk)) { + generate.dartSdk = generate.dartSdk ? [generate.dartSdk] : []; + } + if (!generate.dartSdk.some((s) => s.outputDir === dartSdk.outputDir)) { + generate.dartSdk.push(dartSdk); + } + break; + } + case Platform.ANDROID: { + const kotlinSdk: KotlinSDK = { + outputDir: path.relative(connectorDir, path.join(appDir, `src/main/kotlin`)), + package: `com.google.firebase.dataconnect.generated`, + }; + if (!isArray(generate?.kotlinSdk)) { + generate.kotlinSdk = generate.kotlinSdk ? [generate.kotlinSdk] : []; + } + if (!generate.kotlinSdk.some((s) => s.outputDir === kotlinSdk.outputDir)) { + generate.kotlinSdk.push(kotlinSdk); + } + break; + } + case Platform.IOS: { + const swiftSdk = { + outputDir: path.relative( + connectorDir, + path.join(app.directory, `../FirebaseDataConnectGenerated`), + ), + package: "DataConnectGenerated", + }; + if (!isArray(generate?.swiftSdk)) { + generate.swiftSdk = generate.swiftSdk ? [generate.swiftSdk] : []; + } + if (!generate.swiftSdk.some((s) => s.outputDir === swiftSdk.outputDir)) { + generate.swiftSdk.push(swiftSdk); + } + break; + } + default: + throw new FirebaseError( + `Unsupported platform ${app.platform} for Data Connect SDK generation. Supported platforms are: ${Object.values( + Platform, + ).join(", ")}\n${JSON.stringify(app)}`, + ); + } +} + +function dedupeAppsByPlatformAndDirectory(apps: App[]): App[] { + // detectApps creates unique apps by appId and bundleId, but this method operates + // on platform, directory, and frameworks alone. Deduping here to retain the + // same behavior + const uniqueApps = new Map(); + for (const app of apps) { + // Sorting frameworks for consistent key generation + const frameworkKey = app.frameworks ? [...app.frameworks].sort().join(",") : ""; + const key = `${app.platform}:${app.directory}:${frameworkKey}`; + if (!uniqueApps.has(key)) { + const minifiedApp: App = { + platform: app.platform, + directory: app.directory, + }; + + if (app.frameworks?.length) { + minifiedApp.frameworks = [...app.frameworks]; + } + + // Create a new object with only the desired properties to avoid carrying over others like appId + uniqueApps.set(key, minifiedApp); + } + } + return Array.from(uniqueApps.values()); +} diff --git a/src/init/features/emulators.ts b/src/init/features/emulators.ts index c3f61a0c3c8..f7ba12f12d2 100644 --- a/src/init/features/emulators.ts +++ b/src/init/features/emulators.ts @@ -1,57 +1,68 @@ -import * as clc from "cli-color"; -import * as _ from "lodash"; +import * as clc from "colorette"; import * as utils from "../../utils"; -import { prompt } from "../../prompt"; +import { confirm, checkbox, number } 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"; +import { AdditionalInitFns } from "../../emulator/initEmulators"; +import { Config } from "../../config"; +import { EmulatorsConfig } from "../../firebaseConfig"; interface EmulatorsInitSelections { emulators?: Emulators[]; download?: boolean; } -export async function doSetup(setup: any, config: any) { +export async function doSetup(setup: Setup, config: Config) { const choices = ALL_SERVICE_EMULATORS.map((e) => { return { value: e, + // TODO: latest versions of inquirer have a name vs description. + // We should learn more and whether it's worth investing in. name: Constants.description(e), - checked: config && (config.has(e) || config.has(`emulators.${e}`)), + checked: config?.has(e) || config?.has(`emulators.${e}`), }; }); const selections: EmulatorsInitSelections = {}; - await prompt(selections, [ - { - type: "checkbox", - name: "emulators", - message: - "Which Firebase emulators do you want to set up? " + - "Press Space to select emulators, then Enter to confirm your choices.", - choices: choices, - }, - ]); + selections.emulators = await checkbox({ + message: + "Which Firebase emulators do you want to set up? " + + "Press Space to select emulators, then Enter to confirm your choices.", + choices: choices, + }); if (!selections.emulators) { return; } setup.config.emulators = setup.config.emulators || {}; + const emulators: EmulatorsConfig = setup.config.emulators || {}; for (const selected of selections.emulators) { - setup.config.emulators[selected] = setup.config.emulators[selected] || {}; + if (selected === "extensions") continue; + const selectedEmulator = emulators[selected] || {}; - const currentPort = setup.config.emulators[selected].port; + const currentPort = selectedEmulator.port; if (currentPort) { utils.logBullet(`Port for ${selected} already configured: ${clc.cyan(currentPort)}`); } else { - await prompt(setup.config.emulators[selected], [ - { - type: "number", - name: "port", - message: `Which port do you want to use for the ${clc.underline(selected)} emulator?`, - default: Constants.getDefaultPort(selected as Emulators), - }, - ]); + selectedEmulator.port = await number({ + message: `Which port do you want to use for the ${clc.underline(selected)} emulator?`, + default: Constants.getDefaultPort(selected), + }); + } + emulators[selected] = selectedEmulator; + + const additionalInitFn = AdditionalInitFns[selected]; + if (additionalInitFn) { + const additionalOptions = await additionalInitFn(config); + if (additionalOptions) { + emulators[selected] = { + ...setup.config.emulators[selected], + ...additionalOptions, + }; + } } } @@ -64,40 +75,28 @@ export async function doSetup(setup: any, config: any) { const ui = setup.config.emulators.ui || {}; setup.config.emulators.ui = ui; - await prompt(ui, [ - { - name: "enabled", - type: "confirm", - message: `Would you like to enable the ${uiDesc}?`, - default: true, - }, - ]); + ui.enabled = await confirm({ + message: `Would you like to enable the ${uiDesc}?`, + default: true, + }); if (ui.enabled) { - await prompt(ui, [ - { - type: "input", - name: "port", - message: `Which port do you want to use for the ${clc.underline( - uiDesc - )} (leave empty to use any available port)?`, - }, - ]); - - // Parse the input as a number - const portNum = Number.parseInt(ui.port); - ui.port = isNaN(portNum) ? undefined : portNum; + ui.port = await number({ + message: `Which port do you want to use for the ${clc.underline(uiDesc)} (leave empty to use any available port)?`, + required: false, + }); } } - await prompt(selections, [ - { - name: "download", - type: "confirm", - message: "Would you like to download the emulators now?", - default: false, - }, - ]); + selections.download = await confirm({ + message: "Would you like to download the emulators now?", + 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) { @@ -107,7 +106,7 @@ export async function doSetup(setup: any, config: any) { } } - if (_.get(setup, "config.emulators.ui.enabled")) { + if (setup?.config?.emulators?.ui?.enabled) { downloadIfNecessary(Emulators.UI); } } diff --git a/src/init/features/extensions/index.ts b/src/init/features/extensions/index.ts new file mode 100644 index 00000000000..9f3fba93384 --- /dev/null +++ b/src/init/features/extensions/index.ts @@ -0,0 +1,19 @@ +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"; +import { Setup } from "../.."; + +/** + * Set up a new firebase project for extensions. + */ +export async function doSetup(setup: Setup, 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/index.spec.ts b/src/init/features/firestore/index.spec.ts new file mode 100644 index 00000000000..fab875a6267 --- /dev/null +++ b/src/init/features/firestore/index.spec.ts @@ -0,0 +1,85 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as prompt from "../../../prompt"; +import * as config from "../../../config"; +import * as ensureApiEnabled from "../../../ensureApiEnabled"; +import { FirestoreApi } from "../../../firestore/api"; +import { askQuestions, actuate } from "./index"; +import * as rules from "./rules"; +import * as indexes from "./indexes"; +import { Setup } from "../.."; + +describe("firestore feature init", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("askQuestions", () => { + it("should prompt for database id and location", async () => { + const setup: Setup = { + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + projectId: "test-project", + instructions: [], + }; + const cfg = new config.Config({}, { projectDir: "/", cwd: "/" }); + sandbox.stub(ensureApiEnabled, "ensure").resolves(); + sandbox.stub(FirestoreApi.prototype, "listDatabases").resolves([]); + sandbox.stub(FirestoreApi.prototype, "locations").resolves([ + { + name: "projects/test-project/locations/us-central", + locationId: "us-central", + displayName: "us-central", + labels: {}, + metadata: {}, + }, + ]); + const selectStub = sandbox.stub(prompt, "select").resolves("us-central"); + const initRulesStub = sandbox.stub(rules, "initRules").resolves(); + const initIndexesStub = sandbox.stub(indexes, "initIndexes").resolves(); + + await askQuestions(setup, cfg); + + expect(selectStub.calledOnce).to.be.true; + expect(initRulesStub.calledOnce).to.be.true; + expect(initIndexesStub.calledOnce).to.be.true; + }); + }); + + describe("actuate", () => { + it("should write rules and indexes files", async () => { + const setup: Setup = { + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + featureInfo: { + firestore: { + rulesFilename: "firestore.rules", + rules: "rules content", + writeRules: true, + indexesFilename: "firestore.indexes.json", + indexes: "indexes content", + writeIndexes: true, + databaseId: "(default)", + locationId: "us-central", + }, + }, + instructions: [], + }; + const cfg = new config.Config({}, { projectDir: "/", cwd: "/" }); + const writeStub = sandbox.stub(cfg, "writeProjectFile"); + + await actuate(setup, cfg); + + expect(writeStub.calledTwice).to.be.true; + expect(writeStub.firstCall.calledWith("firestore.rules", "rules content")).to.be.true; + expect(writeStub.secondCall.calledWith("firestore.indexes.json", "indexes content")).to.be + .true; + }); + }); +}); diff --git a/src/init/features/firestore/index.ts b/src/init/features/firestore/index.ts index 45fb4db878a..4fccc1f803c 100644 --- a/src/init/features/firestore/index.ts +++ b/src/init/features/firestore/index.ts @@ -1,58 +1,113 @@ -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 { Config } from "../../../config"; +import { Setup } from "../.."; +import { FirestoreApi } from "../../../firestore/api"; +import { select } from "../../../prompt"; +import { ensure } from "../../../ensureApiEnabled"; +import { firestoreOrigin } from "../../../api"; -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` - )} to create your Cloud Firestore database.`, - { exit: 1 } - ); +export interface RequiredInfo { + databaseId: string; + locationId: string; + rulesFilename: string; + rules: string; + writeRules: boolean; + indexesFilename: string; + indexes: string; + writeIndexes: boolean; +} - // First check if the Firestore API is enabled. If it's not, then the developer needs - // to go set up Firestore in the console. - const isFirestoreEnabled = await apiEnabled.check( - setup.projectId, - "firestore.googleapis.com", - "", - true - ); - if (!isFirestoreEnabled) { - throw firestoreUnusedError; +export async function askQuestions(setup: Setup, config: Config): Promise { + const firestore = !Array.isArray(setup.config.firestore) ? setup.config.firestore : undefined; + const info: RequiredInfo = { + databaseId: firestore?.database || "", + locationId: firestore?.location || "", + rulesFilename: firestore?.rules || "", + rules: "", + writeRules: true, + indexesFilename: firestore?.indexes || "", + indexes: "", + writeIndexes: true, + }; + if (setup.projectId) { + await ensure(setup.projectId, firestoreOrigin(), "firestore"); + // Next, use the AppEngine Apps API to check the database type. + // This allows us to filter out projects that are not using Firestore in Native mode. + // Will also prompt user for databaseId if default does not exist. + info.databaseId = info.databaseId || "(default)"; + const api = new FirestoreApi(); + const databases = await api.listDatabases(setup.projectId!); + const nativeDatabaseNames = databases + .filter((db) => db.type === "FIRESTORE_NATIVE") + .map((db) => db.name.split("/")[3]); + if (nativeDatabaseNames.length === 0) { + if (databases.length > 0) { + // Has non-native Firestore databases + throw new FirebaseError( + `It looks like this project is using Cloud Firestore in ${databases[0].type}. 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 }, + ); + } + // Create the default database in deploy later. + info.databaseId = "(default)"; + const locations = await api.locations(setup.projectId!); + const choice = await select({ + message: "Please select the location of your Firestore database:", + choices: locations.map((location) => location.name.split("/")[3]), + default: "nam5", + }); + info.locationId = choice; + } else if (nativeDatabaseNames.length === 1) { + info.databaseId = nativeDatabaseNames[0]; + info.locationId = databases + .filter((db) => db.name.endsWith(`databases/${info.databaseId}`)) + .map((db) => db.locationId)[0]; + } else if (nativeDatabaseNames.length > 1) { + const choice = await select({ + message: "Please select the name of the Native Firestore database you would like to use:", + choices: nativeDatabaseNames, + }); + info.databaseId = choice; + info.locationId = databases + .filter((db) => db.name.endsWith(`databases/${info.databaseId}`)) + .map((db) => db.locationId)[0]; + } } - // Next, use the AppEngine Apps API to check the database type. - // This allows us to filter out projects that are not using Firestore in Native mode. - const dbType = await checkDatabaseType(setup.projectId); - logger.debug(`database_type: ${dbType}`); - - if (!dbType) { - throw firestoreUnusedError; - } else if (dbType !== "CLOUD_FIRESTORE") { - 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 } - ); - } + await rules.initRules(setup, config, info); + await indexes.initIndexes(setup, config, info); - ensureLocationSet(setup.projectLocation, "Cloud Firestore"); - await requirePermissions({ ...options, project: setup.projectId }); + // Populate featureInfo for the actuate step later. + setup.featureInfo = setup.featureInfo || {}; + setup.featureInfo.firestore = info; } -export async function doSetup(setup: any, config: any, options: any): Promise { - if (setup.projectId) { - await checkProjectSetup(setup, config, options); +export async function actuate(setup: Setup, config: Config): Promise { + const info = setup.featureInfo?.firestore; + if (!info) { + throw new FirebaseError("Firestore featureInfo is not found"); } + // Populate defaults and update `firebase.json` config. + info.databaseId = info.databaseId || "(default)"; + info.locationId = info.locationId || "nam5"; + info.rules = info.rules || rules.getDefaultRules(); + info.rulesFilename = info.rulesFilename || rules.DEFAULT_RULES_FILE; + info.indexes = info.indexes || indexes.INDEXES_TEMPLATE; + info.indexesFilename = info.indexesFilename || indexes.DEFAULT_INDEXES_FILE; + setup.config.firestore = { + database: info.databaseId, + location: info.locationId, + rules: info.rulesFilename, + indexes: info.indexesFilename, + }; - setup.config.firestore = {}; - await rules.initRules(setup, config); - await indexes.initIndexes(setup, config); + if (info.writeRules) { + config.writeProjectFile(info.rulesFilename, info.rules); + } + if (info.writeIndexes) { + config.writeProjectFile(info.indexesFilename, info.indexes); + } } diff --git a/src/init/features/firestore/indexes.spec.ts b/src/init/features/firestore/indexes.spec.ts new file mode 100644 index 00000000000..ae530d477b3 --- /dev/null +++ b/src/init/features/firestore/indexes.spec.ts @@ -0,0 +1,87 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as prompt from "../../../prompt"; +import * as config from "../../../config"; +import { initIndexes, INDEXES_TEMPLATE } from "./indexes"; +import { FirestoreApi } from "../../../firestore/api"; +import { Setup } from "../.."; + +describe("firestore indexes", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("initIndexes", () => { + it("should prompt for indexes file and write default indexes", async () => { + const setup: Setup = { + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + instructions: [], + }; + const cfg = new config.Config({}, { projectDir: "/", cwd: "/" }); + const inputStub = sandbox.stub(prompt, "input").resolves("firestore.indexes.json"); + const writeStub = sandbox.stub(cfg, "confirmWriteProjectFile").resolves(true); + + await initIndexes(setup, cfg, { + databaseId: "(default)", + indexesFilename: "", + indexes: "", + writeIndexes: false, + rulesFilename: "", + rules: "", + writeRules: false, + locationId: "", + }); + + expect(inputStub.calledOnce).to.be.true; + expect(writeStub.calledOnceWith("firestore.indexes.json", INDEXES_TEMPLATE)).to.be.true; + }); + + it("should download indexes from console", async () => { + const setup: Setup = { + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + projectId: "test-project", + instructions: [], + }; + const cfg = new config.Config({}, { projectDir: "/", cwd: "/" }); + const listIndexesStub = sandbox.stub(FirestoreApi.prototype, "listIndexes").resolves([]); + const listFieldOverridesStub = sandbox + .stub(FirestoreApi.prototype, "listFieldOverrides") + .resolves([]); + const makeIndexSpecStub = sandbox + .stub(FirestoreApi.prototype, "makeIndexSpec") + .returns({ indexes: [], fieldOverrides: [] }); + const writeStub = sandbox.stub(cfg, "confirmWriteProjectFile").resolves(true); + + const info = { + databaseId: "(default)", + indexesFilename: "firestore.indexes.json", + indexes: "", + writeIndexes: false, + rulesFilename: "", + rules: "", + writeRules: false, + locationId: "", + }; + await initIndexes(setup, cfg, info); + + expect(listIndexesStub.calledOnceWith("test-project", "(default)")).to.be.true; + expect(listFieldOverridesStub.calledOnceWith("test-project", "(default)")).to.be.true; + expect(makeIndexSpecStub.calledOnceWith([], [])).to.be.true; + expect( + writeStub.calledOnceWith( + "firestore.indexes.json", + JSON.stringify({ indexes: [], fieldOverrides: [] }, null, 2), + ), + ).to.be.true; + expect(info.indexes).to.equal(JSON.stringify({ indexes: [], fieldOverrides: [] }, null, 2)); + }); + }); +}); diff --git a/src/init/features/firestore/indexes.ts b/src/init/features/firestore/indexes.ts index 029a946d9cb..0a4d4465981 100644 --- a/src/init/features/firestore/indexes.ts +++ b/src/init/features/firestore/indexes.ts @@ -1,20 +1,21 @@ -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 { prompt, promptOnce } from "../../../prompt"; +import * as api from "../../../firestore/api"; +import { input } from "../../../prompt"; +import * as utils from "../../../utils"; import { logger } from "../../../logger"; +import { readTemplateSync } from "../../../templates"; +import { RequiredInfo } from "."; +import { Setup } from "../.."; +import { Config } from "../../../config"; -const indexes = new iv2.FirestoreIndexes(); +const indexes = new api.FirestoreApi(); -const INDEXES_TEMPLATE = fs.readFileSync( - __dirname + "/../../../../templates/init/firestore/firestore.indexes.json", - "utf8" -); +export const DEFAULT_INDEXES_FILE = "firestore.indexes.json"; +export const INDEXES_TEMPLATE = readTemplateSync("init/firestore/firestore.indexes.json"); -export function initIndexes(setup: any, config: any): Promise { +export async function initIndexes(setup: Setup, config: Config, info: RequiredInfo): Promise { logger.info(); logger.info("Firestore indexes allow you to perform complex queries while"); logger.info("maintaining performance that scales with the size of the result"); @@ -22,60 +23,45 @@ export function initIndexes(setup: any, config: any): Promise { logger.info("and publish them with " + clc.bold("firebase deploy") + "."); logger.info(); - return prompt(setup.config.firestore, [ - { - type: "input", - name: "indexes", + info.indexesFilename = + info.indexesFilename || + (await input({ message: "What file should be used for Firestore indexes?", - default: "firestore.indexes.json", - }, - ]) - .then(() => { - const filename = setup.config.firestore.indexes; - if (fsutils.fileExistsSync(filename)) { - const msg = - "File " + - clc.bold(filename) + - " already exists." + - " Do you want to overwrite it with the Firestore Indexes from the Firebase Console?"; - return promptOnce({ - type: "confirm", - message: msg, - default: false, - }); - } - return Promise.resolve(true); - }) - .then((overwrite) => { - if (!overwrite) { - return Promise.resolve(); - } + default: DEFAULT_INDEXES_FILE, + })); - if (!setup.projectId) { - return config.writeProjectFile(setup.config.firestore.indexes, INDEXES_TEMPLATE); - } + info.indexes = INDEXES_TEMPLATE; + if (setup.projectId) { + const downloadIndexes = await getIndexesFromConsole(setup.projectId, info.databaseId); + if (downloadIndexes) { + info.indexes = downloadIndexes; + utils.logBullet(`Downloaded the existing Firestore indexes from the Firebase console`); + } + } - return getIndexesFromConsole(setup.projectId).then((contents: any) => { - return config.writeProjectFile(setup.config.firestore.indexes, contents); - }); - }); + info.writeRules = await config.confirmWriteProjectFile(info.indexesFilename, info.indexes); } -function getIndexesFromConsole(projectId: any): Promise { - const indexesPromise = indexes.listIndexes(projectId); - const fieldOverridesPromise = indexes.listFieldOverrides(projectId); +async function getIndexesFromConsole( + projectId: string, + databaseId: string, +): Promise { + const indexesPromise = indexes.listIndexes(projectId, databaseId); + const fieldOverridesPromise = indexes.listFieldOverrides(projectId, databaseId); - return Promise.all([indexesPromise, fieldOverridesPromise]) - .then((res) => { - return indexes.makeIndexSpec(res[0], res[1]); - }) - .catch((e) => { - if (e.message.indexOf("is not a Cloud Firestore enabled project") >= 0) { - return INDEXES_TEMPLATE; - } + try { + const res = await Promise.all([indexesPromise, fieldOverridesPromise]); + return JSON.stringify(indexes.makeIndexSpec(res[0], res[1]), null, 2); + } catch (e: any) { + if (e.status === 404) { + return null; // Database is not found + } + if (e.message.indexOf("is not a Cloud Firestore enabled project") >= 0) { + return null; + } - throw new FirebaseError("Error fetching Firestore indexes", { - original: e, - }); + throw new FirebaseError("Error fetching Firestore indexes", { + original: e, }); + } } diff --git a/src/init/features/firestore/rules.spec.ts b/src/init/features/firestore/rules.spec.ts new file mode 100644 index 00000000000..772e0611962 --- /dev/null +++ b/src/init/features/firestore/rules.spec.ts @@ -0,0 +1,92 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as gcp from "../../../gcp"; +import * as prompt from "../../../prompt"; +import * as config from "../../../config"; +import { initRules, getDefaultRules } from "./rules"; +import { Setup } from "../.."; + +describe("firestore rules", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("getDefaultRules", () => { + it("should return the default rules with the correct date", () => { + const date = new Date(); + date.setDate(date.getDate() + 30); + const expectedDate = `${date.getFullYear()}, ${date.getMonth() + 1}, ${date.getDate()}`; + const rules = getDefaultRules(); + expect(rules).to.include( + `allow read, write: if request.time < timestamp.date(${expectedDate});`, + ); + }); + }); + + describe("initRules", () => { + it("should prompt for rules file and write default rules", async () => { + const setup: Setup = { + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + instructions: [], + }; + const cfg = new config.Config({}, { projectDir: "/", cwd: "/" }); + sandbox.stub(prompt, "input").resolves("firestore.rules"); + sandbox.stub(cfg, "writeProjectFile"); + const confirmStub = sandbox.stub(cfg, "confirmWriteProjectFile").resolves(true); + + await initRules(setup, cfg, { + rulesFilename: "", + rules: "", + writeRules: false, + databaseId: "", + locationId: "", + indexesFilename: "", + indexes: "", + writeIndexes: false, + }); + + expect(confirmStub.calledOnceWith("firestore.rules", getDefaultRules())).to.be.true; + }); + + it("should download rules from console", async () => { + const setup: Setup = { + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + projectId: "test-project", + instructions: [], + }; + const cfg = new config.Config({}, { projectDir: "/", cwd: "/" }); + const getRulesetNameStub = sandbox + .stub(gcp.rules, "getLatestRulesetName") + .resolves("ruleset-name"); + const getRulesetContentStub = sandbox + .stub(gcp.rules, "getRulesetContent") + .resolves([{ name: "file.rules", content: "console rules" }]); + const writeStub = sandbox.stub(cfg, "confirmWriteProjectFile").resolves(true); + + const info = { + rulesFilename: "firestore.rules", + rules: "", + writeRules: false, + databaseId: "", + locationId: "", + indexesFilename: "", + indexes: "", + writeIndexes: false, + }; + await initRules(setup, cfg, info); + + expect(getRulesetNameStub.calledOnceWith("test-project", "cloud.firestore")).to.be.true; + expect(getRulesetContentStub.calledOnceWith("ruleset-name")).to.be.true; + expect(writeStub.calledOnceWith("firestore.rules", "console rules")).to.be.true; + expect(info.rules).to.equal("console rules"); + }); + }); +}); diff --git a/src/init/features/firestore/rules.ts b/src/init/features/firestore/rules.ts index 2824025d6e8..87041d415ad 100644 --- a/src/init/features/firestore/rules.ts +++ b/src/init/features/firestore/rules.ts @@ -1,97 +1,67 @@ -import clc = require("cli-color"); -import fs = require("fs"); +import * as clc from "colorette"; -import gcp = require("../../../gcp"); -import fsutils = require("../../../fsutils"); -import { prompt, promptOnce } from "../../../prompt"; +import * as gcp from "../../../gcp"; +import { input } from "../../../prompt"; import { logger } from "../../../logger"; -import utils = require("../../../utils"); +import * as utils from "../../../utils"; +import { readTemplateSync } from "../../../templates"; +import { RequiredInfo } from "./index"; +import { Setup } from "../.."; +import { Config } from "../../../config"; -const DEFAULT_RULES_FILE = "firestore.rules"; +export 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 { +export function getDefaultRules(): string { + const date = utils.thirtyDaysFromNow(); + const formattedForRules = `${date.getFullYear()}, ${date.getMonth() + 1}, ${date.getDate()}`; + return RULES_TEMPLATE.replace(/{{IN_30_DAYS}}/g, formattedForRules); +} + +export async function initRules(setup: Setup, config: Config, info: RequiredInfo): Promise { logger.info(); logger.info("Firestore Security Rules allow you to define how and when to allow"); logger.info("requests. You can keep these rules in your project directory"); logger.info("and publish them with " + clc.bold("firebase deploy") + "."); logger.info(); - return prompt(setup.config.firestore, [ - { - type: "input", - name: "rules", + info.rulesFilename = + info.rulesFilename || + (await input({ message: "What file should be used for Firestore Rules?", default: DEFAULT_RULES_FILE, - }, - ]) - .then(() => { - const filename = setup.config.firestore.rules; - - if (fsutils.fileExistsSync(filename)) { - const msg = - "File " + - clc.bold(filename) + - " already exists." + - " Do you want to overwrite it with the Firestore Rules from the Firebase Console?"; - return promptOnce({ - type: "confirm", - message: msg, - default: false, - }); - } + })); - return Promise.resolve(true); - }) - .then((overwrite) => { - if (!overwrite) { - return Promise.resolve(); - } + info.rules = getDefaultRules(); + if (setup.projectId) { + const downloadedRules = await getRulesFromConsole(setup.projectId); + if (downloadedRules) { + info.rules = downloadedRules; + utils.logBullet(`Downloaded the existing Firestore Security Rules from the Firebase console`); + } + } - if (!setup.projectId) { - return config.writeProjectFile(setup.config.firestore.rules, getDefaultRules()); - } - - return getRulesFromConsole(setup.projectId).then((contents: any) => { - return config.writeProjectFile(setup.config.firestore.rules, contents); - }); - }); -} - -function getDefaultRules(): string { - const date = utils.thirtyDaysFromNow(); - const formattedForRules = `${date.getFullYear()}, ${date.getMonth() + 1}, ${date.getDate()}`; - return RULES_TEMPLATE.replace(/{{IN_30_DAYS}}/g, formattedForRules); + info.writeRules = await config.confirmWriteProjectFile(info.rulesFilename, info.rules); } -function getRulesFromConsole(projectId: string): Promise { - return gcp.rules - .getLatestRulesetName(projectId, "cloud.firestore") - .then((name) => { - if (!name) { - logger.debug("No rulesets found, using default."); - return [{ name: DEFAULT_RULES_FILE, content: getDefaultRules() }]; - } +async function getRulesFromConsole(projectId: string): Promise { + const name = await gcp.rules.getLatestRulesetName(projectId, "cloud.firestore"); + if (!name) { + return null; + } - logger.debug("Found ruleset: " + name); - return gcp.rules.getRulesetContent(name); - }) - .then((rules: any[]) => { - if (rules.length <= 0) { - return utils.reject("Ruleset has no files", { exit: 1 }); - } + const rules = await gcp.rules.getRulesetContent(name); + if (rules.length <= 0) { + return utils.reject("Ruleset has no files", { exit: 1 }); + } - if (rules.length > 1) { - return utils.reject("Ruleset has too many files: " + rules.length, { exit: 1 }); - } + if (rules.length > 1) { + return utils.reject("Ruleset has too many files: " + rules.length, { exit: 1 }); + } - // Even though the rules API allows for multi-file rulesets, right - // now both the console and the CLI are built on the single-file - // assumption. - return rules[0].content; - }); + // Even though the rules API allows for multi-file rulesets, right + // now both the console and the CLI are built on the single-file + // assumption. + return rules[0].content; } diff --git a/src/init/features/functions.spec.ts b/src/init/features/functions.spec.ts new file mode 100644 index 00000000000..d1650ebd87e --- /dev/null +++ b/src/init/features/functions.spec.ts @@ -0,0 +1,219 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; + +import * as promptImport 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: {} }, + featureArg: true, + instructions: [], + }, + config: new Config({ functions: [cbconfig] }, { projectDir: "test", cwd: "test" }), + }; +} + +describe("functions", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + let prompt: sinon.SinonStubbedInstance; + let askWriteProjectFileStub: sinon.SinonStub; + let emptyConfig: Config; + let options: Options; + + beforeEach(() => { + prompt = sinon.stub(promptImport); + prompt.input.throws("Unexpected input call"); + prompt.select.throws("Unexpected select call"); + prompt.confirm.throws("Unexpected confirm 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(() => { + sinon.verifyAndRestore(); + 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: {} }; + prompt.select.onFirstCall().resolves("javascript"); + + // say "yes" to enabling eslint for the js project + prompt.confirm.onFirstCall().resolves(true); + // do not install dependencies + prompt.confirm.onSecondCall().resolves(false); + 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: {} }; + prompt.select.onFirstCall().resolves("typescript"); + // Lint + prompt.confirm.onFirstCall().resolves(true); + // do not install dependencies + prompt.confirm.onSecondCall().resolves(false); + 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.dev.json`, + `${TEST_SOURCE_DEFAULT}/tsconfig.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(); + // Initialize a new codebase with a naming conflict at first + prompt.select.onFirstCall().resolves("new"); + prompt.input.onFirstCall().resolves("testcodebase2"); + prompt.input.onSecondCall().resolves("testsource2"); + + // Initialize as JavaScript + prompt.select.onSecondCall().resolves("javascript"); + // Lint but do not install dependencies + prompt.confirm.onFirstCall().resolves(true); + prompt.confirm.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'], + }, + { + 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(); + prompt.select.onFirstCall().resolves("reinit"); + prompt.select.onSecondCall().resolves("javascript"); + + // Lint but do not install dependencies + prompt.confirm.onFirstCall().resolves(true); + prompt.confirm.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`, + ]); + }); + }); + }); +}).timeout(5000); diff --git a/src/init/features/functions/index.js b/src/init/features/functions/index.js deleted file mode 100644 index 3ff9dd357c5..00000000000 --- a/src/init/features/functions/index.js +++ /dev/null @@ -1,56 +0,0 @@ -"use strict"; - -var clc = require("cli-color"); - -var _ = require("lodash"); - -const { logger } = require("../../../logger"); -var { prompt } = require("../../../prompt"); -var enableApi = require("../../../ensureApiEnabled").enable; -var { requirePermissions } = require("../../../requirePermissions"); - -module.exports = function (setup, config, options) { - logger.info(); - logger.info( - "A " + clc.bold("functions") + " directory will be created in your project with a Node.js" - ); - logger.info( - "package pre-configured. Functions can be deployed with " + clc.bold("firebase deploy") + "." - ); - logger.info(); - - setup.functions = {}; - var projectId = _.get(setup, "rcfile.projects.default"); - /** @type {Promise<*>} */ - var enableApis = Promise.resolve(); - if (projectId) { - enableApis = requirePermissions({ ...options, project: projectId }).then(() => { - return Promise.all([ - enableApi(projectId, "cloudfunctions.googleapis.com"), - enableApi(projectId, "runtimeconfig.googleapis.com"), - ]); - }); - } - return enableApis.then(function () { - return prompt(setup.functions, [ - { - type: "list", - name: "language", - message: "What language would you like to use to write Cloud Functions?", - default: "javascript", - choices: [ - { - name: "JavaScript", - value: "javascript", - }, - { - name: "TypeScript", - value: "typescript", - }, - ], - }, - ]).then(function () { - return require("./" + setup.functions.language)(setup, config); - }); - }); -}; diff --git a/src/init/features/functions/index.ts b/src/init/features/functions/index.ts new file mode 100644 index 00000000000..aba207cb0ec --- /dev/null +++ b/src/init/features/functions/index.ts @@ -0,0 +1,209 @@ +import * as clc from "colorette"; + +import { logger } from "../../../logger"; +import { input, select } from "../../../prompt"; +import { requirePermissions } from "../../../requirePermissions"; +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"; +import * as supported from "../../../deploy/functions/runtimes/supported"; + +const MAX_ATTEMPTS = 5; + +/** + * 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, 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 select({ + 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 input("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 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 select({ + 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", + value: "javascript", + }, + { + name: "TypeScript", + value: "typescript", + }, + { + name: "Python", + value: "python", + }, + ]; + const language = await select({ + message: "What language would you like to use to write Cloud Functions?", + default: "javascript", + choices, + }); + 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"]; + // In practical sense, latest supported runtime will not be a decomissioned runtime, + // but in theory this doesn't have to be the case. + cbconfig.runtime = supported.latest("python") as supported.ActiveRuntime; + 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..1664991bd4c --- /dev/null +++ b/src/init/features/functions/javascript.ts @@ -0,0 +1,43 @@ +import { askInstallDependencies } from "./npm-dependencies"; +import { confirm } from "../../../prompt"; +import { configForCodebase } from "../../../functions/projectConfig"; +import { readTemplateSync } from "../../../templates"; +import * as supported from "../../../deploy/functions/runtimes/supported"; + +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 async function setup(setup: any, config: any): Promise { + setup.functions.lint = + setup.functions.lint || + (await confirm("Do you want to use ESLint to catch probable bugs and enforce style?")); + if (setup.functions.lint) { + const cbconfig = configForCodebase(setup.config.functions, setup.functions.codebase); + cbconfig.predeploy = ['npm --prefix "$RESOURCE_DIR" run lint']; + await config.askWriteProjectFile( + `${setup.functions.source}/package.json`, + PACKAGE_LINTING_TEMPLATE.replace( + "{{RUNTIME}}", + supported.latest("nodejs").replace("nodejs", ""), + ), + ); + await config.askWriteProjectFile(`${setup.functions.source}/.eslintrc.js`, ESLINT_TEMPLATE); + } else { + await config.askWriteProjectFile( + `${setup.functions.source}/package.json`, + PACKAGE_NO_LINTING_TEMPLATE.replace( + "{{RUNTIME}}", + supported.latest("nodejs").replace("nodejs", ""), + ), + ); + } + + await config.askWriteProjectFile(`${setup.functions.source}/index.js`, INDEX_TEMPLATE); + await config.askWriteProjectFile(`${setup.functions.source}/.gitignore`, GITIGNORE_TEMPLATE); + await 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..df969e24955 --- /dev/null +++ b/src/init/features/functions/npm-dependencies.ts @@ -0,0 +1,18 @@ +import { logger } from "../../../logger"; +import { confirm } from "../../../prompt"; +import { wrapSpawn } from "../../spawn"; + +export async function askInstallDependencies(setup: any, config: any): Promise { + setup.npm = await 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..cb3c6220fd0 --- /dev/null +++ b/src/init/features/functions/python.ts @@ -0,0 +1,68 @@ +import * as spawn from "cross-spawn"; + +import { Config } from "../../../config"; +import { getPythonBinary } from "../../../deploy/functions/runtimes/python"; +import { runWithVirtualEnv } from "../../../functions/python"; +import { confirm } 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 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..8c021c9a821 --- /dev/null +++ b/src/init/features/functions/typescript.ts @@ -0,0 +1,59 @@ +import { askInstallDependencies } from "./npm-dependencies"; +import { confirm } from "../../../prompt"; +import { configForCodebase } from "../../../functions/projectConfig"; +import { readTemplateSync } from "../../../templates"; +import * as supported from "../../../deploy/functions/runtimes/supported"; + +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 async function setup(setup: any, config: any): Promise { + setup.functions.lint = + setup.functions.lint || + (await confirm({ + message: "Do you want to use ESLint to catch probable bugs and enforce style?", + default: true, + })); + + 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'); + await config.askWriteProjectFile( + `${setup.functions.source}/package.json`, + PACKAGE_LINTING_TEMPLATE.replace( + "{{RUNTIME}}", + supported.latest("nodejs").replace("nodejs", ""), + ), + ); + await config.askWriteProjectFile(`${setup.functions.source}/.eslintrc.js`, ESLINT_TEMPLATE); + // TODO: isn't this file out of date now? + await config.askWriteProjectFile( + `${setup.functions.source}/tsconfig.dev.json`, + TSCONFIG_DEV_TEMPLATE, + ); + } else { + cbconfig.predeploy.push('npm --prefix "$RESOURCE_DIR" run build'); + await config.askWriteProjectFile( + `${setup.functions.source}/package.json`, + PACKAGE_NO_LINTING_TEMPLATE.replace( + "{{RUNTIME}}", + supported.latest("nodejs").replace("nodejs", ""), + ), + ); + } + + await config.askWriteProjectFile(`${setup.functions.source}/tsconfig.json`, TSCONFIG_TEMPLATE); + + await config.askWriteProjectFile(`${setup.functions.source}/src/index.ts`, INDEX_TEMPLATE); + await config.askWriteProjectFile(`${setup.functions.source}/.gitignore`, GITIGNORE_TEMPLATE); + await askInstallDependencies(setup.functions, config); +} diff --git a/src/init/features/genkit/index.spec.ts b/src/init/features/genkit/index.spec.ts new file mode 100644 index 00000000000..a7617a3ccdc --- /dev/null +++ b/src/init/features/genkit/index.spec.ts @@ -0,0 +1,221 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; +import * as fs from "fs"; + +import * as genkit from "."; +import * as prompt from "../../../prompt"; +import * as spawn from "../../spawn"; +import * as projectUtils from "../../../projectUtils"; +import * as functions from "../functions"; +import * as ensureApiEnabled from "../../../ensureApiEnabled"; +import { Options } from "../../../options"; +import { RC } from "../../../rc"; +import { Config } from "../../../config"; + +describe("genkit", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + let promptStub: sinon.SinonStubbedInstance; + let spawnStub: sinon.SinonStubbedInstance; + let functionsStub: sinon.SinonStubbedInstance; + let readFileSyncStub: sinon.SinonStub; + let writeFileSyncStub: sinon.SinonStub; + let existsSyncStub: sinon.SinonStub; + let options: Options; + let cfg: Config; + + beforeEach(() => { + promptStub = sandbox.stub(prompt); + spawnStub = sandbox.stub(spawn); + functionsStub = sandbox.stub(functions); + sandbox.stub(ensureApiEnabled); + sandbox.stub(projectUtils, "getProjectId").returns("test-project"); + + readFileSyncStub = sandbox.stub(fs, "readFileSync"); + writeFileSyncStub = sandbox.stub(fs, "writeFileSync"); + existsSyncStub = sandbox.stub(fs, "existsSync"); + sandbox.stub(fs, "mkdirSync"); + + options = { + cwd: "", + configPath: "", + only: "", + except: "", + filteredTargets: [], + force: false, + json: false, + nonInteractive: false, + interactive: false, + debug: false, + config: new Config("{}", {}), + rc: new RC(), + }; + cfg = new Config({}, { projectDir: "test", cwd: "test" }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("doSetup", () => { + beforeEach(() => { + // Mock the behavior of functions.doSetup + functionsStub.doSetup.callsFake(async (setup: any) => { + setup.functions = { + source: "functions", + codebase: "default", + }; + return Promise.resolve(); + }); + readFileSyncStub.returns("{}"); + }); + + it("should set up a new genkit project", async () => { + const setup: genkit.GenkitSetup = { + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + instructions: [], + }; + spawnStub.spawnWithOutput.resolves("1.0.0"); + promptStub.confirm.resolves(true); + promptStub.select + .onFirstCall() + .resolves("globally") // install globally + .onSecondCall() + .resolves("vertexai") // use vertex + .onThirdCall() + .resolves("overwrite") + .onCall(3) + .resolves("overwrite"); + existsSyncStub.returns(true); + + await genkit.doSetup(setup, cfg, options); + + expect(setup.functions).to.deep.equal({ + source: "functions", + codebase: "default", + }); + expect( + spawnStub.wrapSpawn.withArgs("npm", ["install", "-g", "genkit-cli@1.0.0"], "test/functions") + .calledOnce, + ).to.be.true; + expect( + spawnStub.wrapSpawn.withArgs( + "npm", + [ + "install", + "express", + "genkit@1.0.0", + "@genkit-ai/firebase@1.0.0", + "@genkit-ai/vertexai@1.0.0", + "--save", + ], + "test/functions", + ).calledOnce, + ).to.be.true; + expect(writeFileSyncStub.getCall(0).args[0]).to.equal("test/functions/tsconfig.json"); + expect(writeFileSyncStub.getCall(1).args[0]).to.equal("test/functions/package.json"); + expect(writeFileSyncStub.getCall(2).args[0]).to.equal("test/functions/src/genkit-sample.ts"); + }); + + it("should install the cli locally", async () => { + const setup: genkit.GenkitSetup = { + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + instructions: [], + }; + spawnStub.spawnWithOutput.resolves("1.0.0"); + promptStub.confirm.resolves(true); + promptStub.select.onFirstCall().resolves("project").onSecondCall().resolves("vertexai"); + + await genkit.doSetup(setup, cfg, options); + + expect(spawnStub.wrapSpawn.getCall(0).args).to.deep.equal([ + "npm", + ["install", "genkit-cli@1.0.0", "--save-dev"], + "test/functions", + ]); + }); + + it("should set up with the googleai provider", async () => { + const setup: genkit.GenkitSetup = { + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + instructions: [], + }; + spawnStub.spawnWithOutput.resolves("1.0.0"); + promptStub.confirm.resolves(true); + promptStub.select.onFirstCall().resolves("project").onSecondCall().resolves("googleai"); + + await genkit.doSetup(setup, cfg, options); + + expect(spawnStub.wrapSpawn.getCall(1).args).to.deep.equal([ + "npm", + [ + "install", + "express", + "genkit@1.0.0", + "@genkit-ai/firebase@1.0.0", + "@genkit-ai/googleai@1.0.0", + "--save", + ], + "test/functions", + ]); + }); + + it("should not generate a sample file if the user declines", async () => { + const setup: genkit.GenkitSetup = { + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + instructions: [], + }; + spawnStub.spawnWithOutput.resolves("1.0.0"); + promptStub.confirm.onFirstCall().resolves(true).onSecondCall().resolves(false); + promptStub.select.onFirstCall().resolves("project").onSecondCall().resolves("googleai"); + existsSyncStub.withArgs(sinon.match(/package\.json$/)).returns(true); + + await genkit.doSetup(setup, cfg, options); + + // writeFileSync should only be called for tsconfig.json and package.json + expect(writeFileSyncStub.callCount).to.equal(2); + }); + + it("should keep existing config files if the user chooses", async () => { + const setup: genkit.GenkitSetup = { + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + instructions: [], + }; + spawnStub.spawnWithOutput.resolves("1.0.0"); + promptStub.confirm.onFirstCall().resolves(true).onSecondCall().resolves(false); + promptStub.select + .onFirstCall() + .resolves("project") + .onSecondCall() + .resolves("vertexai") + .onThirdCall() + .resolves("keep") + .onCall(3) + .resolves("keep"); + existsSyncStub.returns(true); + + await genkit.doSetup(setup, cfg, options); + + // writeFileSync should not be called + expect(writeFileSyncStub.callCount).to.equal(0); + }); + + it("should abort if the user declines functions setup", async () => { + const setup: genkit.GenkitSetup = { + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + instructions: [], + }; + spawnStub.spawnWithOutput.resolves("1.0.0"); + promptStub.confirm.resolves(false); + + await genkit.doSetup(setup, cfg, options); + + expect(functionsStub.doSetup.notCalled).to.be.true; + }); + }); +}); diff --git a/src/init/features/genkit/index.ts b/src/init/features/genkit/index.ts new file mode 100644 index 00000000000..591356fc59e --- /dev/null +++ b/src/init/features/genkit/index.ts @@ -0,0 +1,679 @@ +/** + * Copyright 2024 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 + * + * http://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. + */ + +import * as fs from "fs"; +import * as path from "path"; +import * as semver from "semver"; +import * as clc from "colorette"; + +import { doSetup as functionsSetup } from "../functions"; +import { Config } from "../../../config"; +import { confirm, select } from "../../../prompt"; +import { wrapSpawn, spawnWithOutput } from "../../spawn"; +import { Options } from "../../../options"; +import { getProjectId } from "../../../projectUtils"; +import { ensure } from "../../../ensureApiEnabled"; +import { logger } from "../../../logger"; +import { FirebaseError, getErrMsg, isObject } from "../../../error"; +import { Setup } from "../.."; +import { + logLabeledBullet, + logLabeledError, + logLabeledSuccess, + logLabeledWarning, +} from "../../../utils"; + +interface GenkitInfo { + genkitVersion: string; + cliVersion: string; + vertexVersion: string; + googleAiVersion: string; + templateVersion: string; + stopInstall: boolean; +} + +// This is the next breaking change version past the latest template. +const UNKNOWN_VERSION_TOO_HIGH = "2.0.0"; +const MIN_VERSION = "0.6.0"; + +// This is the latest template. It is the default. +const LATEST_TEMPLATE = "1.0.0"; + +async function getPackageVersion(packageName: string, envVariable: string): Promise { + // Allow the installed version to be set for dev purposes. + const envVal = process.env[envVariable]; + if (envVal && typeof envVal === "string") { + if (semver.parse(envVal)) { + return envVal; + } else { + throw new FirebaseError(`Invalid version string '${envVal}' specified in ${envVariable}`); + } + } + try { + const output = await spawnWithOutput("npm", ["view", packageName, "version"]); + if (!output) { + throw new FirebaseError(`Unable to determine ${packageName} version to install`); + } + return output; + } catch (err: unknown) { + throw new FirebaseError( + `Unable to determine which version of ${packageName} to install.\n` + + `npm Error: ${getErrMsg(err)}\n\n` + + "For a possible workaround run\n npm view " + + packageName + + " version\n" + + "and then set an environment variable:\n" + + ` export ${envVariable}=\n` + + "and run `firebase init genkit` again", + ); + } +} + +/** + * Determines which version and template to install + * @return a GenkitInfo object + */ +async function getGenkitInfo(): Promise { + let templateVersion = LATEST_TEMPLATE; + let stopInstall = false; + + const genkitVersion = await getPackageVersion("genkit", "GENKIT_DEV_VERSION"); + const cliVersion = await getPackageVersion("genkit-cli", "GENKIT_CLI_DEV_VERSION"); + const vertexVersion = await getPackageVersion("@genkit-ai/vertexai", "GENKIT_VERTEX_VERSION"); + const googleAiVersion = await getPackageVersion("@genkit-ai/googleai", "GENKIT_GOOGLEAI_VERSION"); + + if (semver.gte(genkitVersion, UNKNOWN_VERSION_TOO_HIGH)) { + // We don't know about this version. (Can override with GENKIT_DEV_VERSION) + const continueInstall = await confirm({ + message: + clc.yellow( + `WARNING: The latest version of Genkit (${genkitVersion}) isn't supported by this\n` + + "version of firebase-tools. You can proceed, but the provided sample code may\n" + + "not work with the latest library. You can also try updating firebase-tools with\n" + + "npm install -g firebase-tools@latest, and then running this command again.\n\n", + ) + "Proceed with installing the latest version of Genkit?", + default: false, + }); + if (!continueInstall) { + stopInstall = true; + } + } else if (semver.gte(genkitVersion, "1.0.0-rc.1")) { + // 1.0.0-rc.1 < 1.0.0 + templateVersion = "1.0.0"; + } else if (semver.gte(genkitVersion, MIN_VERSION)) { + templateVersion = "0.9.0"; + } else { + throw new FirebaseError( + `The requested version of Genkit (${genkitVersion}) is no ` + + `longer supported. Please specify a newer version.`, + ); + } + + return { + genkitVersion, + cliVersion, + vertexVersion, + googleAiVersion, + templateVersion, + stopInstall, + }; +} + +/** + * GenkitSetup depends on functions setup. + */ +export interface GenkitSetup extends Setup { + functions?: { + source: string; + codebase: string; + languageChoice?: string; + }; + + [key: string]: unknown; +} + +function showStartMessage(setup: GenkitSetup, command: string): void { + logger.info(); + logger.info("\nLogin to Google Cloud using:"); + logger.info( + clc.bold( + clc.green( + ` gcloud auth application-default login --project ${setup.projectId || "your-project-id"}\n`, + ), + ), + ); + logger.info("Then start the Genkit developer experience by running:"); + logger.info(clc.bold(clc.green(` ${command}`))); +} + +/** + * doSetup is the entry point for setting up the genkit suite. + */ +export async function doSetup(initSetup: Setup, config: Config, options: Options): Promise { + const setup: GenkitSetup = initSetup as GenkitSetup; + const genkitInfo = await getGenkitInfo(); + if (genkitInfo.stopInstall) { + logLabeledWarning("genkit", "Stopped Genkit initialization"); + return; + } + if (setup.functions?.languageChoice !== "typescript") { + const continueFunctions = await confirm({ + message: + "Genkit's Firebase integration uses Cloud Functions for Firebase with TypeScript.\nInitialize Functions to continue?", + default: true, + }); + if (!continueFunctions) { + logLabeledWarning("genkit", "Stopped Genkit initialization"); + return; + } + + // Functions with genkit should always be typescript + setup.languageOverride = "typescript"; + await functionsSetup(setup, config, options); + delete setup.languageOverride; + logger.info(); + } + + if (!setup.functions) { + throw new FirebaseError("Failed to initialize Genkit prerequisite: Firebase functions"); + } + + const projectDir = `${config.projectDir}/${setup.functions.source}`; + + const installType = await select({ + message: "Install the Genkit CLI globally or locally in this project?", + choices: [ + { name: "Globally", value: "globally" }, + { name: "Just this project", value: "project" }, + ], + }); + + try { + logLabeledBullet("genkit", `Installing Genkit CLI version ${genkitInfo.cliVersion}`); + if (installType === "globally") { + await wrapSpawn("npm", ["install", "-g", `genkit-cli@${genkitInfo.cliVersion}`], projectDir); + await genkitSetup(options, genkitInfo, projectDir); + showStartMessage(setup, `cd ${setup.functions.source} && npm run genkit:start`); + } else { + await wrapSpawn( + "npm", + ["install", `genkit-cli@${genkitInfo.cliVersion}`, "--save-dev"], + projectDir, + ); + await genkitSetup(options, genkitInfo, projectDir); + showStartMessage(setup, `cd ${setup.functions.source} && npm run genkit:start`); + } + } catch (err) { + logLabeledError("genkit", `Genkit initialization failed: ${getErrMsg(err)}`); + return; + } +} + +export type ModelProvider = "googleai" | "vertexai" | "none"; +export type WriteMode = "keep" | "overwrite" | "merge"; + +/** + * Enables the Vertex AI API on a best effort basis. + * @param options The options passed to the parent command + */ +export async function ensureVertexApiEnabled(options: Options): Promise { + const VERTEX_AI_URL = "https://aiplatform.googleapis.com"; + const projectId = getProjectId(options); + if (!projectId) { + return; + } + // If using markdown, enable it silently + const silently = typeof options.markdown === "boolean" && options.markdown; + return await ensure(projectId, VERTEX_AI_URL, "aiplatform", silently); +} + +interface PluginInfo { + // Imported items from `name` (can be comma list). + imports: string; + // Comment for 'the model import line. + modelImportComment?: string; + // Initializer call. + init: string; + // Model name as an imported reference. + model?: string; + // Model name as a string reference. + modelStr?: string; +} + +interface PromptOption { + // Label for prompt option. + label: string; + // Plugin name. + plugin?: string; + // Package including version + package?: string; +} + +/** Model to plugin name. */ +function getModelOptions(genkitInfo: GenkitInfo): Record { + const modelOptions: Record = { + vertexai: { + label: "Google Cloud Vertex AI", + plugin: "@genkit-ai/vertexai", + package: `@genkit-ai/vertexai@${genkitInfo.vertexVersion}`, + }, + googleai: { + label: "Google AI", + plugin: "@genkit-ai/googleai", + package: `@genkit-ai/googleai@${genkitInfo.googleAiVersion}`, + }, + none: { label: "None", plugin: undefined, package: undefined }, + }; + return modelOptions; +} + +/** Plugin name to descriptor. */ +const pluginToInfo: Record = { + "@genkit-ai/firebase": { + imports: "firebase", + init: ` + // Load the Firebase plugin, which provides integrations with several + // Firebase services. + firebase()`.trimStart(), + }, + "@genkit-ai/vertexai": { + imports: "vertexAI", + modelImportComment: ` +// Import models from the Vertex AI plugin. The Vertex AI API provides access to +// several generative models. Here, we import Gemini 2.0 Flash.`.trimStart(), + init: ` + // Load the Vertex AI plugin. You can optionally specify your project ID + // by passing in a config object; if you don't, the Vertex AI plugin uses + // the value from the GCLOUD_PROJECT environment variable. + vertexAI({location: "us-central1"})`.trimStart(), + model: "gemini20Flash", + }, + "@genkit-ai/googleai": { + imports: "googleAI", + modelImportComment: ` +// Import models from the Google AI plugin. The Google AI API provides access to +// several generative models. Here, we import Gemini 2.0 Flash.`.trimStart(), + init: ` + // Load the Google AI plugin. You can optionally specify your API key + // by passing in a config object; if you don't, the Google AI plugin uses + // the value from the GOOGLE_GENAI_API_KEY environment variable, which is + // the recommended practice. + googleAI()`.trimStart(), + model: "gemini20Flash", + }, +}; + +/** Basic packages required to use Genkit. */ +function getBasePackages(genkitVersion: string): string[] { + const basePackages = ["express", `genkit@${genkitVersion}`]; + return basePackages; +} + +/** External dev packages required to use Genkit. */ +const externalDevPackages = ["typescript", "tsx"]; + +/** + * Initializes a Genkit Node.js project. + * @param options command-line arguments + * @param genkitInfo Information about which version of genkit we are installing + * @param projectDir The project directory to install into. + */ +export async function genkitSetup( + options: Options, + genkitInfo: GenkitInfo, + projectDir: string, +): Promise { + // Choose a model + const modelOptions = getModelOptions(genkitInfo); + const supportedModels = Object.keys(modelOptions) as ModelProvider[]; + const model = await select({ + message: "Select a model provider:", + choices: supportedModels.map((model) => ({ + name: modelOptions[model].label, + value: model, + })), + }); + + if (model === "vertexai") { + await ensureVertexApiEnabled(options); + } + + // Compile plugins list. + const plugins: string[] = []; + const pluginPackages: string[] = []; + pluginPackages.push(`@genkit-ai/firebase@${genkitInfo.genkitVersion}`); + + if (modelOptions[model]?.plugin) { + plugins.push(modelOptions[model].plugin || ""); + } + if (modelOptions[model]?.package) { + pluginPackages.push(modelOptions[model].package || ""); + } + + // Compile NPM packages list. + const packages = [...getBasePackages(genkitInfo.genkitVersion)]; + packages.push(...pluginPackages); + + // Initialize and configure. + await installNpmPackages(projectDir, packages, externalDevPackages); + if (!fs.existsSync(path.join(projectDir, "src"))) { + fs.mkdirSync(path.join(projectDir, "src")); + } + + await updateTsConfig(options.nonInteractive || false, projectDir); + await updatePackageJson(options.nonInteractive || false, projectDir); + if ( + options.nonInteractive || + (await confirm({ + message: "Would you like to generate a sample flow?", + default: true, + })) + ) { + logger.info( + "Telemetry data can be used to monitor and gain insights into your AI features. There may be a cost associated with using this feature. See https://firebase.google.com/docs/genkit/observability/telemetry-collection.", + ); + const enableTelemetry = + options.nonInteractive || + (await confirm({ + message: "Would like you to enable telemetry collection?", + default: true, + })); + + generateSampleFile( + modelOptions[model].plugin, + plugins, + projectDir, + genkitInfo.templateVersion, + enableTelemetry, + ); + } +} + +// We only need to worry about the compilerOptions entry. +interface TsConfig { + compilerOptions?: Record; +} + +// A typeguard for the results of JSON.parse(); +export const isTsConfig = (value: unknown): value is TsConfig => { + if (!isObject(value) || (value.compilerOptions && !isObject(value.compilerOptions))) { + return false; + } + + return true; +}; + +/** + * Updates tsconfig.json with required flags for Genkit. + * @param nonInteractive if we rae asking the user questions + * @param projectDir the directory containing the tsconfig.json + */ +async function updateTsConfig(nonInteractive: boolean, projectDir: string): Promise { + const tsConfigPath = path.join(projectDir, "tsconfig.json"); + let existingTsConfig: TsConfig | undefined = undefined; + if (fs.existsSync(tsConfigPath)) { + const parsed: unknown = JSON.parse(fs.readFileSync(tsConfigPath, "utf-8")); + if (!isTsConfig(parsed)) { + throw new FirebaseError("Unable to parse existing tsconfig.json"); + } + existingTsConfig = parsed; + } + let choice: WriteMode = "overwrite"; + if (!nonInteractive && existingTsConfig) { + choice = await promptWriteMode( + "Would you like to update your tsconfig.json with suggested settings?", + ); + } + const tsConfig = { + compileOnSave: true, + include: ["src"], + compilerOptions: { + module: "commonjs", + noImplicitReturns: true, + outDir: "lib", + sourceMap: true, + strict: true, + target: "es2017", + skipLibCheck: true, + esModuleInterop: true, + }, + }; + logLabeledBullet("genkit", "Updating tsconfig.json"); + let newTsConfig = {}; + switch (choice) { + case "overwrite": + newTsConfig = { + ...existingTsConfig, + ...tsConfig, + compilerOptions: { + ...existingTsConfig?.compilerOptions, + ...tsConfig.compilerOptions, + }, + }; + break; + case "merge": + newTsConfig = { + ...tsConfig, + ...existingTsConfig, + compilerOptions: { + ...tsConfig.compilerOptions, + ...existingTsConfig?.compilerOptions, + }, + }; + break; + case "keep": + logLabeledWarning("genkit", "Skipped updating tsconfig.json"); + return; + } + try { + fs.writeFileSync(tsConfigPath, JSON.stringify(newTsConfig, null, 2)); + logLabeledSuccess("genkit", "Successfully updated tsconfig.json"); + } catch (err) { + logLabeledError("genkit", `Failed to update tsconfig.json: ${getErrMsg(err)}`); + process.exit(1); + } +} + +/** + * Installs and saves NPM packages to package.json. + * @param projectDir The project directory. + * @param packages List of NPM packages to install. + * @param devPackages List of NPM dev packages to install. + */ +async function installNpmPackages( + projectDir: string, + packages: string[], + devPackages?: string[], +): Promise { + logLabeledBullet("genkit", "Installing NPM packages for genkit"); + try { + if (packages.length) { + await wrapSpawn("npm", ["install", ...packages, "--save"], projectDir); + } + if (devPackages?.length) { + await wrapSpawn("npm", ["install", ...devPackages, "--save-dev"], projectDir); + } + logLabeledSuccess("genkit", "Successfully installed NPM packages"); + } catch (err) { + logLabeledError("genkit", `Failed to install NPM packages: ${getErrMsg(err)}`); + process.exit(1); + } +} + +/** + * Generates a sample index.ts file. + * @param modelPlugin Model plugin name. + * @param configPlugins config plugins. + */ +function generateSampleFile( + modelPlugin: string | undefined, + configPlugins: string[], + projectDir: string, + templateVersion: string, + enableTelemetry: boolean, +): void { + let modelImport = ""; + if (modelPlugin && pluginToInfo[modelPlugin].model) { + const modelInfo = pluginToInfo[modelPlugin].model || ""; + modelImport = "\n" + generateImportStatement(modelInfo, modelPlugin) + "\n"; + } + let modelImportComment = ""; + if (modelPlugin && pluginToInfo[modelPlugin].modelImportComment) { + const comment = pluginToInfo[modelPlugin].modelImportComment || ""; + modelImportComment = `\n${comment}`; + } + const commentedModelImport = `${modelImportComment}${modelImport}`; + const templatePath = path.join( + __dirname, + `../../../../templates/genkit/firebase.${templateVersion}.template`, + ); + const template = fs.readFileSync(templatePath, "utf8"); + const sample = renderConfig( + configPlugins, + template + .replace("$GENKIT_MODEL_IMPORT\n", commentedModelImport) + .replace( + "$GENKIT_MODEL", + modelPlugin + ? pluginToInfo[modelPlugin].model || pluginToInfo[modelPlugin].modelStr || "" + : "'' /* TODO: Set a model. */", + ), + enableTelemetry, + ); + logLabeledBullet("genkit", "Generating sample file"); + try { + const samplePath = "src/genkit-sample.ts"; + fs.writeFileSync(path.join(projectDir, samplePath), sample, "utf8"); + logLabeledSuccess("genkit", `Successfully generated sample file (${samplePath})`); + } catch (err) { + logLabeledError("genkit", `Failed to generate sample file: ${getErrMsg(err)}`); + process.exit(1); + } +} + +// We only need to worry about the scripts entry +interface PackageJson { + scripts?: Record; +} + +// A typeguard for the results of JSON.parse(); +export const isPackageJson = (value: unknown): value is PackageJson => { + if (!isObject(value) || (value.scripts && !isObject(value.scripts))) { + return false; + } + + return true; +}; + +/** + * Updates package.json with Genkit-expected fields. + * @param nonInteractive a boolean that indicates if we are asking questions or not + * @param projectDir The directory to find the package.json file in. + */ +async function updatePackageJson(nonInteractive: boolean, projectDir: string): Promise { + const packageJsonPath = path.join(projectDir, "package.json"); + // package.json should exist before reaching this point. + if (!fs.existsSync(packageJsonPath)) { + throw new FirebaseError("Failed to find package.json."); + } + const existingPackageJson: unknown = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); + if (!isPackageJson(existingPackageJson)) { + throw new FirebaseError("Unable to parse existing package.json file"); + } + const choice = nonInteractive + ? "overwrite" + : await promptWriteMode("Would you like to update your package.json with suggested settings?"); + const packageJson = { + main: "lib/index.js", + scripts: { + "genkit:start": "genkit start -- tsx --watch src/genkit-sample.ts", + }, + }; + logLabeledBullet("genkit", "Updating package.json"); + let newPackageJson = {}; + switch (choice) { + case "overwrite": + newPackageJson = { + ...existingPackageJson, + ...packageJson, + scripts: { + ...existingPackageJson.scripts, + ...packageJson.scripts, + }, + }; + break; + case "merge": + newPackageJson = { + ...packageJson, + ...existingPackageJson, + // Main will always be overwritten to match tsconfig. + main: packageJson.main, + scripts: { + ...packageJson.scripts, + ...existingPackageJson.scripts, + }, + }; + break; + case "keep": + logLabeledWarning("genkit", "Skipped updating package.json"); + return; + } + try { + fs.writeFileSync(packageJsonPath, JSON.stringify(newPackageJson, null, 2)); + logLabeledSuccess("genkit", "Successfully updated package.json"); + } catch (err) { + logLabeledError("genkit", `Failed to update package.json: ${getErrMsg(err)}`); + process.exit(1); + } +} + +function renderConfig(pluginNames: string[], template: string, enableTelemetry: boolean): string { + const imports = pluginNames + .map((pluginName) => generateImportStatement(pluginToInfo[pluginName].imports, pluginName)) + .join("\n"); + const plugins = + pluginNames.map((pluginName) => ` ${pluginToInfo[pluginName].init},`).join("\n") || + " /* Add your plugins here. */"; + return template + .replace("$GENKIT_CONFIG_IMPORTS", imports) + .replace("$GENKIT_CONFIG_PLUGINS", plugins) + .replaceAll("$TELEMETRY_COMMENT", enableTelemetry ? "" : "// "); +} + +function generateImportStatement(imports: string, name: string): string { + return `import {${imports}} from "${name}";`; +} + +/** + * Prompts for what type of write to perform when there is a conflict. + * @param message The question to ask + * @param defaultOption The default WriteMode to highlight + * @return The writemode selected + */ +export async function promptWriteMode( + message: string, + defaultOption: WriteMode = "merge", +): Promise { + return select({ + message, + choices: [ + { name: "Set if unset", value: "merge" }, + { name: "Overwrite", value: "overwrite" }, + { name: "Keep unchanged", value: "keep" }, + ], + default: defaultOption, + }); +} diff --git a/src/init/features/hosting/github.ts b/src/init/features/hosting/github.ts index e1ac3e115c3..906b27bedca 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,13 +12,15 @@ import { createServiceAccount, createServiceAccountKey, deleteServiceAccount, + listServiceAccountKeys, } from "../../../gcp/iam"; import { addServiceAccountToRoles, firebaseRoles } from "../../../gcp/resourceManager"; import { logger } from "../../../logger"; -import { prompt } from "../../../prompt"; -import { logBullet, logLabeledBullet, logSuccess, reject } from "../../../utils"; +import { input, confirm } 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,11 +52,17 @@ 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.`, + ); + } + logger.info(); // Find existing Git/Github config @@ -66,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(); @@ -95,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); @@ -111,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(); @@ -123,11 +132,11 @@ export async function initGitHub(setup: Setup, config: any, options: any): Promi // If the developer is using predeploy scripts in firebase.json, // remind them before they set up a script in their workflow file. - if (setup.config.hosting.predeploy) { + if (!Array.isArray(setup.config.hosting) && setup.config.hosting.predeploy) { logBullet(`You have a predeploy script configured in firebase.json.`); } - const { script } = await promptForBuildScript(); + const { script } = await promptForBuildScript(setup?.hosting?.useWebFrameworks); const ymlDeployDoc = loadYMLDeploy(); @@ -137,7 +146,7 @@ export async function initGitHub(setup: Setup, config: any, options: any): Promi // If the preview YML file exists, ask the user to overwrite. This file is generated by // the CLI and rarely touched by the user. if (fs.existsSync(YML_FULL_PATH_PULL_REQUEST)) { - const { overwrite } = await promptForWriteYMLFile({ + const overwrite = await confirm({ message: `GitHub workflow file for PR previews exists. Overwrite? ${YML_PULL_REQUEST_FILENAME}`, }); shouldWriteYMLHostingFile = overwrite; @@ -148,7 +157,9 @@ export async function initGitHub(setup: Setup, config: any, options: any): Promi YML_FULL_PATH_PULL_REQUEST, githubSecretName, setup.projectId, - script + script, + setup?.hosting?.useWebFrameworks, + setup?.hosting?.source, ); logger.info(); @@ -167,10 +178,9 @@ export async function initGitHub(setup: Setup, config: any, options: any): Promi if (ymlDeployDoc.branch !== branch) { shouldWriteYMLDeployFile = true; } else { - const { overwrite } = await promptForWriteYMLFile({ + shouldWriteYMLDeployFile = await confirm({ message: `The GitHub workflow file for deploying to the live channel already exists. Overwrite? ${YML_MERGE_FILENAME}`, }); - shouldWriteYMLDeployFile = overwrite; } } else { shouldWriteYMLDeployFile = true; @@ -182,7 +192,9 @@ export async function initGitHub(setup: Setup, config: any, options: any): Promi branch, githubSecretName, setup.projectId, - script + script, + setup?.hosting?.useWebFrameworks, + setup?.hosting?.source, ); logger.info(); @@ -193,10 +205,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`); } @@ -254,7 +266,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 { @@ -267,8 +279,10 @@ 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; "runs-on": string; steps: { uses?: string; @@ -276,6 +290,9 @@ type GitHubWorkflowConfig = { with?: { [key: string]: string }; env?: { [key: string]: string }; }[]; + defaults?: { + run?: Record; + }; }; }; }; @@ -284,41 +301,65 @@ function writeChannelActionYMLFile( ymlPath: string, secretName: string, projectId: string, - script?: string + script: string | undefined, + useWebFrameworks: boolean | undefined, + hostingSource: string | undefined, ): 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 "runs-on": "ubuntu-latest", steps: [{ uses: CHECKOUT_GITHUB_ACTION_NAME }], }, }, }; - if (script) { - workflowConfig.jobs.build_and_preview.steps.push({ - run: script, - }); - } - - workflowConfig.jobs.build_and_preview.steps.push({ + const buildAndPreviewParams: Record = { uses: HOSTING_GITHUB_ACTION_NAME, with: { repoToken: "${{ secrets.GITHUB_TOKEN }}", firebaseServiceAccount: `\${{ secrets.${secretName} }}`, projectId: projectId, }, - env: { - FIREBASE_CLI_PREVIEWS: "hostingchannels", - }, - }); + }; + + if (useWebFrameworks) { + // install is required for web frameworks + workflowConfig.jobs.build_and_preview.steps.push({ run: "npm ci" }); + + buildAndPreviewParams.env = { + FIREBASE_CLI_EXPERIMENTS: "webframeworks", + }; + + // if source is not root, set the working directory in the GitHub Action so that + // the npm script does not fail + if (hostingSource && hostingSource !== ".") { + workflowConfig.jobs.build_and_preview.defaults = { + run: { "working-directory": hostingSource }, + }; + } + } + + if (script) { + workflowConfig.jobs.build_and_preview.steps.push({ + run: script, + }); + } + + workflowConfig.jobs.build_and_preview.steps.push(buildAndPreviewParams); 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); @@ -330,7 +371,9 @@ function writeDeployToProdActionYMLFile( branch: string | undefined, secretName: string, projectId: string, - script?: string + script: string | undefined, + useWebFrameworks: boolean | undefined, + hostingSource: string | undefined, ): void { const workflowConfig: GitHubWorkflowConfig = { name: "Deploy to Firebase Hosting on merge", @@ -343,13 +386,7 @@ function writeDeployToProdActionYMLFile( }, }; - if (script) { - workflowConfig.jobs.build_and_deploy.steps.push({ - run: script, - }); - } - - workflowConfig.jobs.build_and_deploy.steps.push({ + const buildAndDeployParams: Record = { uses: HOSTING_GITHUB_ACTION_NAME, with: { repoToken: "${{ secrets.GITHUB_TOKEN }}", @@ -357,15 +394,35 @@ function writeDeployToProdActionYMLFile( channelId: "live", projectId: projectId, }, - env: { - FIREBASE_CLI_PREVIEWS: "hostingchannels", - }, - }); + }; + + if (useWebFrameworks) { + // install is required for web frameworks + workflowConfig.jobs.build_and_deploy.steps.push({ run: "npm ci" }); + + buildAndDeployParams.env = { + FIREBASE_CLI_EXPERIMENTS: "webframeworks", + }; + + // if source is not root, set the working directory in the GitHub Action so that + // the npm script does not fail + if (hostingSource && hostingSource !== ".") { + workflowConfig.jobs.build_and_deploy.defaults = { + run: { "working-directory": hostingSource }, + }; + } + } + + if (script) { + workflowConfig.jobs.build_and_deploy.steps.push({ run: script }); + } + + workflowConfig.jobs.build_and_deploy.steps.push(buildAndDeployParams); 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); @@ -377,7 +434,7 @@ async function uploadSecretToGitHub( ghAccessToken: string, encryptedServiceAccountJSON: string, keyId: string, - secretName: string + secretName: string, ): Promise<{ status: any }> { const data = { ["encrypted_value"]: encryptedServiceAccountJSON, @@ -387,105 +444,108 @@ 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 = ""; - const { repo } = await prompt(options, [ - { - type: "input", - name: "repo", + const repo = + options.repo || + (await input({ default: defaultGithubRepo(), // TODO look at github origin message: "For which GitHub repository would you like to set up a GitHub workflow? (format: user/repository)", validate: async (repo: string) => { - // 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" }, + try { + 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: 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", + ); + 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:`, + ); + logger.info( + bold( + underline( + `https://github.com/settings/connections/applications/${githubClientId()}`, + ), + ), + ); + logger.info(); } - ); - key = body.key; - keyId = body.key_id; + return false; + } return true; }, - }, - ]); + })); + options.repo = repo; + return { repo, key, keyId }; } -async function promptForBuildScript(): Promise<{ script?: string }> { - const { shouldSetupScript } = await prompt({}, [ - { - type: "confirm", - name: "shouldSetupScript", - default: false, - message: "Set up the workflow to run a build script before every deploy?", - }, - ]); +async function promptForBuildScript( + useWebFrameworks: boolean | undefined, +): Promise<{ script?: string }> { + const shouldSetupScript = await confirm({ + message: "Set up the workflow to run a build script before every deploy?", + }); if (!shouldSetupScript) { return { script: undefined }; } - const { script } = await prompt({}, [ - { - type: "input", - name: "script", - default: "npm ci && npm run build", - message: "What script should be run before every deploy?", - }, - ]); + const script = await input({ + /** + * Do not suggest a default script if the user is using web frameworks: + * - build script is handled by frameworks code + * - install is required for frameworks, the npm ci will be added by default + */ + default: useWebFrameworks ? undefined : "npm ci && npm run build", + message: "What script should be run before every deploy?", + }); return { script }; } async function promptToSetupDeploys( - defaultBranch: string + defaultBranch: string, ): Promise<{ setupDeploys: boolean; branch?: string }> { - const { setupDeploys } = await prompt({}, [ - { - type: "confirm", - name: "setupDeploys", - default: true, - message: "Set up automatic deployment to your site's live channel when a PR is merged?", - }, - ]); + const setupDeploys = await confirm({ + default: true, + message: "Set up automatic deployment to your site's live channel when a PR is merged?", + }); if (!setupDeploys) { return { setupDeploys }; } - const { branch } = await prompt({}, [ - { - type: "input", - name: "branch", - default: defaultBranch, - message: "What is the name of the GitHub branch associated with your site's live channel?", - }, - ]); + const branch = await input({ + default: defaultBranch, + message: "What is the name of the GitHub branch associated with your site's live channel?", + }); return { branch, setupDeploys }; } -async function promptForWriteYMLFile({ message }: { message: string }) { - return await prompt({}, [ - { - type: "confirm", - name: "overwrite", - default: false, - message, - }, - ]); -} - async function getGitHubUserDetails(ghAccessToken: any): Promise> { // eslint-disable-next-line @typescript-eslint/no-explicit-any const { body: ghUserDetails } = await githubApiClient.get>("/user", { @@ -495,12 +555,11 @@ async function getGitHubUserDetails(ghAccessToken: any): Promise( `/repos/${repo}`, { headers: { Authorization: `token ${ghAccessToken}`, "User-Agent": "Firebase CLI" }, - } + }, ); return body; } @@ -512,18 +571,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; } @@ -531,7 +602,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(); @@ -542,16 +613,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; @@ -564,6 +635,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, @@ -572,6 +647,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); @@ -597,15 +675,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..b3fc0568e05 --- /dev/null +++ b/src/init/features/hosting/index.ts @@ -0,0 +1,193 @@ +import * as clc from "colorette"; +import { rmSync } from "node:fs"; +import { join } from "path"; + +import { Client } from "../../../apiv2"; +import { initGitHub } from "./github"; +import { confirm, input, select } 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) { + // N.B. During prompt migration this did not pass options object, so there is no support + // for force or nonInteractive; there possibly should be. + const confirmCreate = await 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; + setup.hosting.useDiscoveredFramework ??= await confirm({ + message: `Detected an existing ${name} codebase in the current directory, should we use this?`, + default: true, + }); + } + if (setup.hosting.useDiscoveredFramework) { + setup.hosting.source = "."; + setup.hosting.useWebFrameworks = true; + } else { + setup.hosting.useWebFrameworks = await confirm( + `Do you want to use a web framework? (${clc.bold("experimental")})`, + ); + } + } + + if (setup.hosting.useWebFrameworks) { + setup.hosting.source ??= await input({ + message: "What folder would you like to use for your web application's root directory?", + default: "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; + setup.hosting.useDiscoveredFramework ??= await confirm({ + message: `Detected an existing ${name} codebase in ${setup.hosting.source}, should we use this?`, + default: true, + }); + } + + 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; + + setup.hosting.whichFramework = + setup.hosting.whichFramework || + (await select({ + message: "Please choose the framework:", + default: defaultChoice, + choices, + })); + + if (discoveredFramework) rmSync(setup.hosting.source, { recursive: true }); + await WebFrameworks[setup.hosting.whichFramework].init!(setup, config); + } + + setup.hosting.region = + setup.hosting.region || + (await select({ + 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.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(); + + setup.hosting.public = + setup.hosting.public || + (await input({ + message: "What do you want to use as your public directory?", + default: "public", + })); + setup.hosting.spa = + setup.hosting.spa || + (await confirm("Configure as a single-page app (rewrite all urls to /index.html)?")); + + setup.config.hosting = { + public: setup.hosting.public, + ignore: DEFAULT_IGNORES, + }; + } + + setup.hosting.github = + setup.hosting.github || (await confirm("Set up automatic builds and deploys with GitHub?")); + + 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..f5b5d34de50 --- /dev/null +++ b/src/init/features/index.ts @@ -0,0 +1,47 @@ +export { doSetup as account } from "./account"; +export { + askQuestions as databaseAskQuestions, + RequiredInfo as DatabaseInfo, + actuate as databaseActuate, +} from "./database"; +export { + askQuestions as firestoreAskQuestions, + RequiredInfo as FirestoreInfo, + actuate as firestoreActuate, +} from "./firestore"; +export { doSetup as functions } from "./functions"; +export { doSetup as hosting } from "./hosting"; +export { + askQuestions as storageAskQuestions, + RequiredInfo as StorageInfo, + actuate as storageActuate, +} 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 { + askQuestions as dataconnectAskQuestions, + RequiredInfo as DataconnectInfo, + actuate as dataconnectActuate, +} from "./dataconnect"; +export { + askQuestions as dataconnectSdkAskQuestions, + SdkRequiredInfo as DataconnectSdkInfo, + actuate as dataconnectSdkActuate, +} from "./dataconnect/sdk"; +export { doSetup as apphosting } from "./apphosting"; +export { doSetup as genkit } from "./genkit"; +export { + askQuestions as apptestingAskQuestions, + RequiredInfo as ApptestingInfo, + actuate as apptestingAcutate, +} from "./apptesting"; +export { doSetup as aitools } from "./aitools"; +export { + askQuestions as aiLogicAskQuestions, + AiLogicInfo, + actuate as aiLogicActuate, +} from "./ailogic"; diff --git a/src/init/features/project.spec.ts b/src/init/features/project.spec.ts new file mode 100644 index 00000000000..868a5e6afc9 --- /dev/null +++ b/src/init/features/project.spec.ts @@ -0,0 +1,164 @@ +import { expect } from "chai"; +import * as _ from "lodash"; +import * as sinon from "sinon"; +import { configstore } from "../../configstore"; + +import { doSetup } from "./project"; +import * as projectManager from "../../management/projects"; +import { Config } from "../../config"; +import { FirebaseProjectMetadata } from "../../types/project"; +import * as promptImport from "../../prompt"; +import * as requireAuthImport from "../../requireAuth"; + +const TEST_FIREBASE_PROJECT: FirebaseProjectMetadata = { + projectId: "my-project-123", + projectNumber: "123456789", + displayName: "my-project", + name: "projects/my-project", + resources: { + hostingSite: "my-project", + realtimeDatabaseInstance: "my-project", + storageBucket: "my-project.appspot.com", + locationId: "us-central", + }, +}; + +describe("project", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + let getProjectStub: sinon.SinonStub; + let createFirebaseProjectStub: sinon.SinonStub; + let getOrPromptProjectStub: sinon.SinonStub; + let addFirebaseProjectStub: sinon.SinonStub; + let promptAvailableProjectIdStub: sinon.SinonStub; + let prompt: sinon.SinonStubbedInstance; + let configstoreSetStub: sinon.SinonStub; + let emptyConfig: Config; + + beforeEach(() => { + sandbox.stub(requireAuthImport, "requireAuth").resolves(); + getProjectStub = sandbox.stub(projectManager, "getFirebaseProject"); + createFirebaseProjectStub = sandbox.stub(projectManager, "createFirebaseProjectAndLog"); + getOrPromptProjectStub = sandbox.stub(projectManager, "getOrPromptProject"); + addFirebaseProjectStub = sandbox.stub(projectManager, "addFirebaseToCloudProjectAndLog"); + promptAvailableProjectIdStub = sandbox.stub(projectManager, "promptAvailableProjectId"); + prompt = sandbox.stub(promptImport); + prompt.select.rejects("Unexpected select call"); + prompt.input.rejects("Unexpected inptu call"); + prompt.confirm.rejects("Unexpected confirm call"); + configstoreSetStub = sandbox.stub(configstore, "set").throws("Unexpected configstore set"); + emptyConfig = new Config("{}", {}); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("doSetup", () => { + describe('with "Use an existing project" option', () => { + it("should set up the correct properties in the project", async () => { + const options = { project: "my-project" }; + const setup = { config: {}, rcfile: {} }; + getProjectStub.onFirstCall().resolves(TEST_FIREBASE_PROJECT); + prompt.select.onFirstCall().resolves("Use an existing project"); + getOrPromptProjectStub.onFirstCall().resolves(TEST_FIREBASE_PROJECT); + configstoreSetStub.onFirstCall().resolves(); + + await doSetup(setup, emptyConfig, options); + + expect(_.get(setup, "projectId")).to.deep.equal("my-project-123"); + expect(_.get(setup, "instance")).to.deep.equal("my-project"); + expect(_.get(setup, "projectLocation")).to.deep.equal("us-central"); + expect(_.get(setup.rcfile, "projects.default")).to.deep.equal("my-project-123"); + expect(prompt.select).to.not.be.called; + expect(getOrPromptProjectStub).to.not.be.called; + }); + }); + + describe('with "Create a new project" option', () => { + it("should create a new project and set up the correct properties", async () => { + const options = {}; + const setup = { config: {}, rcfile: {} }; + prompt.select.onFirstCall().resolves("Create a new project"); + prompt.input.onFirstCall().resolves("my-project-123"); + prompt.input.onSecondCall().resolves("my-project"); + createFirebaseProjectStub.resolves(TEST_FIREBASE_PROJECT); + configstoreSetStub.onFirstCall().resolves(); + + await doSetup(setup, emptyConfig, options); + + expect(_.get(setup, "projectId")).to.deep.equal("my-project-123"); + expect(_.get(setup, "instance")).to.deep.equal("my-project"); + expect(_.get(setup, "projectLocation")).to.deep.equal("us-central"); + expect(_.get(setup.rcfile, "projects.default")).to.deep.equal("my-project-123"); + expect(prompt.select).to.be.calledOnce; + expect(prompt.input).to.be.calledTwice; + expect(createFirebaseProjectStub).to.be.calledOnceWith("my-project-123", { + displayName: "my-project", + }); + }); + }); + + describe('with "Add Firebase resources to GCP project" option', () => { + it("should add firebase resources and set up the correct properties", async () => { + const options = {}; + const setup = { config: {}, rcfile: {} }; + prompt.select + .onFirstCall() + .resolves("Add Firebase to an existing Google Cloud Platform project"); + promptAvailableProjectIdStub.onFirstCall().resolves("my-project-123"); + addFirebaseProjectStub.onFirstCall().resolves(TEST_FIREBASE_PROJECT); + configstoreSetStub.onFirstCall().resolves(); + + await doSetup(setup, emptyConfig, options); + + expect(_.get(setup, "projectId")).to.deep.equal("my-project-123"); + expect(_.get(setup, "instance")).to.deep.equal("my-project"); + expect(_.get(setup, "projectLocation")).to.deep.equal("us-central"); + expect(_.get(setup.rcfile, "projects.default")).to.deep.equal("my-project-123"); + expect(prompt.select).to.be.calledOnce; + expect(promptAvailableProjectIdStub).to.be.calledOnce; + expect(addFirebaseProjectStub).to.be.calledOnceWith("my-project-123"); + }); + }); + + describe(`with "Don't set up a default project" option`, () => { + it("should set up the correct properties when not choosing a project", async () => { + const options = {}; + const setup = { config: {}, rcfile: {} }; + prompt.select.resolves("Don't set up a default project"); + + await doSetup(setup, emptyConfig, options); + + expect(setup).to.deep.equal({ config: {}, rcfile: {}, project: {} }); + expect(prompt.select).to.be.calledOnce; + }); + }); + + describe("with defined .firebaserc file", () => { + let options: any; + let setup: any; + + beforeEach(() => { + options = {}; + setup = { config: {}, rcfile: { projects: { default: "my-project-123" } } }; + getProjectStub.onFirstCall().resolves(TEST_FIREBASE_PROJECT); + configstoreSetStub.onFirstCall().resolves(); + }); + + it("should not prompt", async () => { + await doSetup(setup, emptyConfig, options); + + expect(prompt.select).to.be.not.called; + expect(prompt.input).to.be.not.called; + }); + + it("should set project location even if .firebaserc is already set up", async () => { + await doSetup(setup, emptyConfig, options); + + expect(_.get(setup, "projectId")).to.equal("my-project-123"); + expect(_.get(setup, "projectLocation")).to.equal("us-central"); + expect(getProjectStub).to.be.calledOnceWith("my-project-123"); + }); + }); + }); +}); diff --git a/src/init/features/project.ts b/src/init/features/project.ts index 7587575462c..1e7b6527f57 100644 --- a/src/init/features/project.ts +++ b/src/init/features/project.ts @@ -1,102 +1,27 @@ -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, + promptProjectCreation, + selectProjectInteractively, } from "../../management/projects"; +import { FirebaseProjectMetadata } from "../../types/project"; import { logger } from "../../logger"; -import { prompt, promptOnce } from "../../prompt"; import * as utils from "../../utils"; +import * as prompt from "../../prompt"; +import { requireAuth } from "../../requireAuth"; +import { Constants } from "../../emulator/constants"; +import { FirebaseError } from "../../error"; const OPTION_NO_PROJECT = "Don't set up a default project"; const OPTION_USE_PROJECT = "Use an existing project"; const OPTION_NEW_PROJECT = "Create a new project"; const OPTION_ADD_FIREBASE = "Add Firebase to an existing Google Cloud Platform project"; -/** - * Used in init flows to keep information about the project - basically - * a shorter version of {@link FirebaseProjectMetadata} with some additional fields. - */ -export interface ProjectInfo { - id: string; // maps to FirebaseProjectMetadata.projectId - label?: string; - instance?: string; // maps to FirebaseProjectMetadata.resources.realtimeDatabaseInstance - location?: string; // maps to FirebaseProjectMetadata.resources.locationId -} - -function toProjectInfo(projectMetaData: FirebaseProjectMetadata): ProjectInfo { - const { projectId, displayName, resources } = projectMetaData; - return { - id: projectId, - label: `${projectId}` + (displayName ? ` (${displayName})` : ""), - instance: _.get(resources, "realtimeDatabaseInstance"), - location: _.get(resources, "locationId"), - }; -} - -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.` - ); - const promptAnswer: { projectId?: string; displayName?: string } = {}; - await prompt(promptAnswer, PROJECTS_CREATE_QUESTIONS); - if (!promptAnswer.projectId) { - throw new FirebaseError("Project ID cannot be empty"); - } - - return await createFirebaseProjectAndLog(promptAnswer.projectId, { - displayName: promptAnswer.displayName, - }); -} - -async function promptAndAddFirebaseToCloudProject(): Promise { - const projectId = await promptAvailableProjectId(); - if (!projectId) { - throw new FirebaseError("Project ID cannot be empty"); - } - return await addFirebaseToCloudProjectAndLog(projectId); -} - -/** - * Prompt the user about how they would like to select a project. - * @param options the Firebase CLI options object. - * @return the project metadata, or undefined if no project was selected. - */ -async function projectChoicePrompt(options: any): Promise { - const choices = [ - { name: OPTION_USE_PROJECT, value: OPTION_USE_PROJECT }, - { name: OPTION_NEW_PROJECT, value: OPTION_NEW_PROJECT }, - { name: OPTION_ADD_FIREBASE, value: OPTION_ADD_FIREBASE }, - { name: OPTION_NO_PROJECT, value: OPTION_NO_PROJECT }, - ]; - const projectSetupOption: string = await promptOnce({ - type: "list", - name: "id", - message: "Please select an option:", - choices, - }); - - switch (projectSetupOption) { - case OPTION_USE_PROJECT: - return getOrPromptProject(options); - case OPTION_NEW_PROJECT: - return promptAndCreateNewProject(); - case OPTION_ADD_FIREBASE: - return promptAndAddFirebaseToCloudProject(); - default: - // Do nothing if user chooses NO_PROJECT - return; - } -} - /** * Sets up the default project if provided and writes .firebaserc file. * @param setup A helper object to use for the rest of the init features. @@ -109,39 +34,96 @@ export async function doSetup(setup: any, config: any, options: any): Promise({ + message: "Please select an option:", + choices, + }); + switch (projectSetupOption) { + case OPTION_USE_PROJECT: { + await requireAuth(options); + const pm = await selectProjectInteractively(); + return await usingProjectMetadata(setup, config, pm); } + case OPTION_NEW_PROJECT: { + 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.`, + ); + await requireAuth(options); + const { projectId, displayName } = await promptProjectCreation(options); + const pm = await createFirebaseProjectAndLog(projectId, { displayName }); + return await usingProjectMetadata(setup, config, pm); + } + case OPTION_ADD_FIREBASE: { + await requireAuth(options); + const pm = await addFirebaseToCloudProjectAndLog(await promptAvailableProjectId()); + return await usingProjectMetadata(setup, config, pm); + } + default: + // Do nothing if user chooses NO_PROJECT + return; } +} - const projectInfo = toProjectInfo(projectMetaData); - utils.logBullet(`Using project ${projectInfo.label}`); +async function usingProject( + setup: any, + config: any, + projectId: string, + from: string = "", +): Promise { + const pm = await getFirebaseProject(projectId); + const label = `${pm.projectId}` + (pm.displayName ? ` (${pm.displayName})` : ""); + utils.logBullet(`Using project ${label} ${from ? "from ${from}" : ""}.`); + await usingProjectMetadata(setup, config, pm); +} + +async function usingProjectMetadata( + setup: any, + config: any, + pm: FirebaseProjectMetadata, +): Promise { + if (!pm) { + throw new FirebaseError("null FirebaseProjectMetadata"); + } // write "default" alias and activate it immediately - _.set(setup.rcfile, "projects.default", projectInfo.id); - setup.projectId = projectInfo.id; - setup.instance = projectInfo.instance; - setup.projectLocation = projectInfo.location; - utils.makeActiveProject(config.projectDir, projectInfo.id); + _.set(setup.rcfile, "projects.default", pm.projectId); + setup.projectId = pm.projectId; + setup.instance = pm.resources?.realtimeDatabaseInstance; + setup.projectLocation = pm.resources?.locationId; + utils.makeActiveProject(config.projectDir, pm.projectId); } diff --git a/src/init/features/remoteconfig.ts b/src/init/features/remoteconfig.ts index 81018d7cd82..f0b250d6beb 100644 --- a/src/init/features/remoteconfig.ts +++ b/src/init/features/remoteconfig.ts @@ -1,18 +1,8 @@ -import { promptOnce } from "../../prompt"; -import fsutils = require("../../fsutils"); -import clc = require("cli-color"); -import { RemoteConfigTemplate } from "../../remoteconfig/interfaces"; -import Config = require("../../config"); - -interface RemoteConfig { - template?: RemoteConfigTemplate; -} -interface SetUpConfig { - remoteconfig: RemoteConfig; -} -interface RemoteConfigSetup { - config: SetUpConfig; -} +import { input, confirm } from "../../prompt"; +import * as fsutils from "../../fsutils"; +import * as clc from "colorette"; +import { Config } from "../../config"; +import { Setup } from ".."; /** * Function retrieves names for parameters and parameter groups @@ -20,11 +10,8 @@ interface RemoteConfigSetup { * @param config Input is of type Config * @return {Promise} Returns a promise and writes the project file for remoteconfig template when initializing */ -export async function doSetup(setup: RemoteConfigSetup, config: Config): Promise { - setup.config.remoteconfig = {}; - const jsonFilePath = await promptOnce({ - type: "input", - name: "template", +export async function doSetup(setup: Setup, config: Config): Promise { + const jsonFilePath = await input({ message: "What file should be used for your Remote Config template?", default: "remoteconfig.template.json", }); @@ -34,15 +21,13 @@ export async function doSetup(setup: RemoteConfigSetup, config: Config): Promise clc.bold(jsonFilePath) + " already exists." + " Do you want to overwrite the existing Remote Config template?"; - const overwrite = await promptOnce({ - type: "confirm", - message: msg, - default: false, - }); + const overwrite = await confirm(msg); if (!overwrite) { return; } } - setup.config.remoteconfig.template = jsonFilePath; + setup.config.remoteconfig = { + template: jsonFilePath, + }; config.writeProjectFile(setup.config.remoteconfig.template, "{}"); } diff --git a/src/init/features/storage.spec.ts b/src/init/features/storage.spec.ts new file mode 100644 index 00000000000..348b92f4caa --- /dev/null +++ b/src/init/features/storage.spec.ts @@ -0,0 +1,42 @@ +import { expect } from "chai"; +import * as _ from "lodash"; +import * as sinon from "sinon"; + +import { Config } from "../../config"; +import { askQuestions, actuate } from "./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, "writeProjectFile"); + promptStub = sandbox.stub(prompt, "input"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("doSetup", () => { + it("should set up the correct properties in the project", async () => { + const setup = { + config: {}, + rcfile: { projects: {}, targets: {}, etags: {} }, + projectId: "my-project-123", + projectLocation: "us-central", + instructions: [], + }; + const config = new Config({}, { projectDir: "test", cwd: "test" }); + promptStub.returns("storage.rules"); + askWriteProjectFileStub.resolves(); + + await askQuestions(setup, config); + await actuate(setup, config); + + expect(_.get(setup, "config.storage.rules")).to.deep.equal("storage.rules"); + }); + }); +}); diff --git a/src/init/features/storage.ts b/src/init/features/storage.ts index 096e8f9d984..56663287ec2 100644 --- a/src/init/features/storage.ts +++ b/src/init/features/storage.ts @@ -1,31 +1,56 @@ -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 { input } from "../../prompt"; +import { readTemplateSync } from "../../templates"; +import { Config } from "../../config"; +import { Setup } from ".."; +import { FirebaseError } from "../../error"; -const RULES_TEMPLATE = fs.readFileSync( - __dirname + "/../../../templates/init/storage/storage.rules", - "utf8" -); +export interface RequiredInfo { + rulesFilename: string; + rules: string; + writeRules: boolean; +} -export async function doSetup(setup: any, config: any): Promise { - setup.config.storage = {}; - ensureLocationSet(setup.projectLocation, "Cloud Storage"); +const RULES_TEMPLATE = readTemplateSync("init/storage/storage.rules"); +const DEFAULT_RULES_FILE = "storage.rules"; +export async function askQuestions(setup: Setup, config: Config): Promise { logger.info(); logger.info("Firebase Storage Security Rules allow you to define how and when to allow"); logger.info("uploads and downloads. You can keep these rules in your project directory"); logger.info("and publish them with " + clc.bold("firebase deploy") + "."); logger.info(); - const storageRulesFile = await promptOnce({ - type: "input", - name: "rules", + const info: RequiredInfo = { + rulesFilename: DEFAULT_RULES_FILE, + rules: RULES_TEMPLATE, + writeRules: true, + }; + info.rulesFilename = await input({ message: "What file should be used for Storage Rules?", - default: "storage.rules", + default: DEFAULT_RULES_FILE, }); - setup.config.storage.rules = storageRulesFile; - config.writeProjectFile(setup.config.storage.rules, RULES_TEMPLATE); + info.writeRules = await config.confirmWriteProjectFile(info.rulesFilename, info.rules); + // Populate featureInfo for the actuate step later. + setup.featureInfo = setup.featureInfo || {}; + setup.featureInfo.storage = info; +} + +export async function actuate(setup: Setup, config: Config): Promise { + const info = setup.featureInfo?.storage; + if (!info) { + throw new FirebaseError("storage featureInfo is not found"); + } + // Populate defaults and update `firebase.json` config. + info.rules = info.rules || RULES_TEMPLATE; + info.rulesFilename = info.rulesFilename || DEFAULT_RULES_FILE; + setup.config.storage = { + rules: info.rulesFilename, + }; + + if (info.writeRules) { + config.writeProjectFile(info.rulesFilename, info.rules); + } } diff --git a/src/init/index.ts b/src/init/index.ts index 08ae3a9a329..f3ca925b9dc 100644 --- a/src/init/index.ts +++ b/src/init/index.ts @@ -1,43 +1,187 @@ -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"; +import { Config } from "../config"; +import { FirebaseConfig } from "../firebaseConfig"; +import { Options } from "../options"; +import { trackGA4 } from "../track"; -export interface Indexable { - [key: string]: T; -} -export interface RCFile { - projects: Indexable; -} export interface Setup { - config: Indexable; - rcfile: RCFile; + config: FirebaseConfig; + rcfile: RCData; features?: string[]; featureArg?: boolean; - project?: Indexable; + featureInfo?: SetupInfo; + + // Each feature init flow may add instructions. + // They will be displayed at the end of `firebase init` or + // return back to `firebase_init` MCP tools. + instructions: string[]; + + /** Basic Project information */ + project?: Record; projectId?: string; projectLocation?: string; + isBillingEnabled?: boolean; + + hosting?: Record; +} + +export interface SetupInfo { + database?: features.DatabaseInfo; + firestore?: features.FirestoreInfo; + dataconnect?: features.DataconnectInfo; + dataconnectSdk?: features.DataconnectSdkInfo; + storage?: features.StorageInfo; + apptesting?: features.ApptestingInfo; + ailogic?: features.AiLogicInfo; } -// TODO: Convert features/index.js to TypeScript so it exports -// as an indexable type instead of doing this cast. -const features = _features as Indexable; +interface Feature { + name: string; + displayName?: string; + // OLD WAY: A single setup function to ask questions and actuate the setup. + doSetup?: (setup: Setup, config: Config, options: Options) => Promise; -export async function init(setup: Setup, config: any, options: any): Promise { - const nextFeature = setup.features ? setup.features.shift() : undefined; + // NEW WAY: Split the init into phases: + // 1. askQuestions: Ask the user questions and update `setup.featureInfo` with the answers. + askQuestions?: (setup: Setup, config: Config, options: Options) => Promise; + // 2. actuate: Use the answers in `setup.featureInfo` to actuate the setup. + actuate?: (setup: Setup, config: Config, options: Options) => Promise; + // 3. [optional] Additional follow-up steps to run after the setup is completed. + postSetup?: (setup: Setup, config: Config, options: Options) => Promise; +} + +const featuresList: Feature[] = [ + { name: "account", doSetup: features.account }, + { + name: "database", + askQuestions: features.databaseAskQuestions, + actuate: features.databaseActuate, + }, + { + name: "firestore", + askQuestions: features.firestoreAskQuestions, + actuate: features.firestoreActuate, + }, + { + name: "dataconnect", + askQuestions: features.dataconnectAskQuestions, + actuate: features.dataconnectActuate, + }, + { + name: "dataconnect:sdk", + askQuestions: features.dataconnectSdkAskQuestions, + actuate: features.dataconnectSdkActuate, + }, + { name: "functions", doSetup: features.functions }, + { name: "hosting", doSetup: features.hosting }, + { + name: "storage", + askQuestions: features.storageAskQuestions, + actuate: features.storageActuate, + }, + { name: "emulators", doSetup: features.emulators }, + { name: "extensions", doSetup: features.extensions }, + { name: "project", doSetup: features.project }, // always runs, sets up .firebaserc + { name: "remoteconfig", doSetup: features.remoteconfig }, + { name: "hosting:github", doSetup: features.hostingGithub }, + { name: "genkit", doSetup: features.genkit }, + { name: "apphosting", displayName: "App Hosting", doSetup: features.apphosting }, + { + name: "apptesting", + askQuestions: features.apptestingAskQuestions, + actuate: features.apptestingAcutate, + }, + { + name: "ailogic", + askQuestions: features.aiLogicAskQuestions, + actuate: features.aiLogicActuate, + }, + { name: "aitools", displayName: "AI Tools", doSetup: features.aitools }, +]; + +const featureMap = new Map(featuresList.map((feature) => [feature.name, feature])); + +export async function init(setup: Setup, config: Config, options: any): Promise { + 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(", ") + const start = process.uptime(); + + const f = featureMap.get(nextFeature); + if (!f) { + 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("===")} ${f.displayName || capitalize(nextFeature)} Setup`), + ); + + if (f.doSetup) { + await f.doSetup(setup, config, options); + } else { + if (f.askQuestions) { + await f.askQuestions(setup, config, options); + } + if (f.actuate) { + await f.actuate(setup, config, options); + } + } + if (f.postSetup) { + await f.postSetup(setup, config, options); + } + + const duration = Math.floor((process.uptime() - start) * 1000); + await trackGA4("product_init", { feature: nextFeature }, duration); - await Promise.resolve(features[nextFeature](setup, config, options)); return init(setup, config, options); } } + +/** Actuate the feature init flow from firebase_init MCP tool. */ +export async function actuate(setup: Setup, config: Config, options: any): Promise { + const nextFeature = setup.features?.shift(); + if (nextFeature) { + const start = process.uptime(); + + const f = lookupFeature(nextFeature); + logger.info(clc.bold(`\n${clc.white("===")} ${capitalize(nextFeature)} Setup Actuation`)); + + if (f.doSetup) { + throw new FirebaseError( + `The feature ${nextFeature} does not support actuate yet. Please run ${clc.bold("firebase init " + nextFeature)} instead.`, + ); + } else { + if (f.actuate) { + await f.actuate(setup, config, options); + } + } + + const duration = Math.floor((process.uptime() - start) * 1000); + await trackGA4("product_init_mcp", { feature: nextFeature }, duration); + + return actuate(setup, config, options); + } +} + +function lookupFeature(feature: string): Feature { + const f = featureMap.get(feature); + if (!f) { + const availableFeatures = Object.keys(features) + .filter((f) => f !== "project") + .join(", "); + throw new FirebaseError( + `${clc.bold(feature)} is not a valid feature. Must be one of ${availableFeatures}`, + ); + } + return f; +} diff --git a/src/init/spawn.ts b/src/init/spawn.ts new file mode 100644 index 00000000000..55c32e8e842 --- /dev/null +++ b/src/init/spawn.ts @@ -0,0 +1,111 @@ +import * as spawn from "cross-spawn"; +import { logger } from "../logger"; +import { getErrStack, isObject } from "../error"; + +/** + * wrapSpawn is cross platform spawn + * @param cmd The command to run + * @param args The args for the command + * @param projectDir The current working directory to set + */ +export function wrapSpawn(cmd: string, args: string[], projectDir: string): Promise { + return new Promise((resolve, reject) => { + const installer = spawn(cmd, args, { + cwd: projectDir, + stdio: "inherit", + env: { ...process.env }, + }); + + installer.on("error", (err: unknown) => { + logger.debug(getErrStack(err)); + }); + + installer.on("close", (code) => { + if (code === 0) { + return resolve(); + } + return reject( + new Error( + `Error: spawn(${cmd}, [${args.join(", ")}]) \n exited with code: ${code || "null"}`, + ), + ); + }); + }); +} + +/** + * spawnWithOutput uses cross-spawn to spawn a child process and get + * the output from it. + * @param cmd The command to run + * @param args The arguments for the command + * @return The stdout string from the command. + */ +export function spawnWithOutput(cmd: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn(cmd, args); + + let output = ""; + + child.stdout?.on("data", (data) => { + if (isObject(data) && data.toString) { + output += data.toString(); + } else { + output += JSON.stringify(data); + } + }); + + child.stderr?.on("data", (data) => { + logger.debug( + `Error: spawn(${cmd}, ${args.join(", ")})\n Stderr:\n${JSON.stringify(data)}\n`, + ); + }); + + child.on("error", (err: unknown) => { + logger.debug(getErrStack(err)); + }); + + child.on("close", (code) => { + if (code === 0) { + resolve(output); + } else { + reject( + new Error( + `Error: spawn(${cmd}, [${args.join(", ")}]) \n exited with code: ${code || "null"}`, + ), + ); + } + }); + }); +} + +/** + * spawnWithCommandString spawns a child process with a command string + * @param cmd The command to run + * @param projectDir The directory to run it in + * @param environmentVariables Environment variables to set + */ +export function spawnWithCommandString( + cmd: string, + projectDir: string, + environmentVariables?: NodeJS.ProcessEnv, +): Promise { + return new Promise((resolve, reject) => { + const installer = spawn(cmd, { + cwd: projectDir, + stdio: "inherit", + shell: true, + env: { ...process.env, ...environmentVariables }, + }); + + installer.on("error", (err: unknown) => { + logger.log("DEBUG", getErrStack(err)); + }); + + installer.on("close", (code) => { + if (code === 0) { + return resolve(); + } + return reject(new Error(`Error: spawn(${cmd}) \n exited with code: ${code || "null"}`)); + }); + }); +} 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 b339bbfad61..00000000000 --- a/src/localFunction.js +++ /dev/null @@ -1,220 +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.name = trigger.name; - this.eventTrigger = trigger.eventTrigger; - this.httpsTrigger = trigger.httpsTrigger; - this.controller = controller; - this.url = _.get(urls, this.name); - - 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.spec.ts b/src/logError.spec.ts new file mode 100644 index 00000000000..b931a092f78 --- /dev/null +++ b/src/logError.spec.ts @@ -0,0 +1,93 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { logError } from "./logError"; +import { logger } from "./logger"; + +describe("logError", () => { + let sandbox: sinon.SinonSandbox; + let errorSpy: sinon.SinonSpy; + let debugSpy: sinon.SinonSpy; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + errorSpy = sandbox.spy(logger, "error"); + debugSpy = sandbox.spy(logger, "debug"); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should log a simple error message", () => { + const error = { message: "A simple error has occurred." }; + logError(error); + expect(errorSpy).to.have.been.calledWith(sinon.match.any, "A simple error has occurred."); + }); + + it("should log an error with children", () => { + const error = { + message: "An error with children has occurred.", + children: [{ name: "Child1", message: "Child error 1" }, { message: "Child error 2" }], + }; + logError(error); + expect(errorSpy).to.have.been.calledWith( + sinon.match.any, + sinon.match(/An error with children has occurred./), + ); + expect(errorSpy).to.have.been.calledWith(sinon.match(/- .*Child1.* Child error 1/)); + expect(errorSpy).to.have.been.calledWith(sinon.match(/- Child error 2/)); + }); + + it("should log an error with an original stack", () => { + const error = { + message: "An error with an original stack.", + original: { stack: "the stack" }, + }; + logError(error); + expect(debugSpy).to.have.been.calledWith("the stack"); + }); + + it("should log an error with a context", () => { + const error = { + message: "An error with a context.", + context: { key: "value" }, + }; + logError(error); + expect(debugSpy).to.have.been.calledWith( + "Error Context:", + JSON.stringify({ key: "value" }, undefined, 2), + ); + }); + + it("should log an error with both original stack and context", () => { + const error = { + message: "An error with both.", + original: { stack: "the stack" }, + context: { key: "value" }, + }; + logError(error); + expect(debugSpy).to.have.been.calledWith("the stack"); + expect(debugSpy).to.have.been.calledWith( + "Error Context:", + JSON.stringify({ key: "value" }, undefined, 2), + ); + }); + + it("should log an error with children and context", () => { + const error = { + message: "An error with children and context.", + children: [{ message: "Child error" }], + context: { key: "value" }, + }; + logError(error); + expect(errorSpy).to.have.been.calledWith( + sinon.match.any, + sinon.match(/An error with children and context./), + ); + expect(errorSpy).to.have.been.calledWith(sinon.match(/- Child error/)); + expect(debugSpy).to.have.been.calledWith( + "Error Context:", + JSON.stringify({ key: "value" }, undefined, 2), + ); + }); +}); diff --git a/src/logError.ts b/src/logError.ts new file mode 100644 index 00000000000..3f5551d5309 --- /dev/null +++ b/src/logError.ts @@ -0,0 +1,26 @@ +import { logger } from "./logger"; +import * as clc from "colorette"; + +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..75870d0c636 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,5 +1,25 @@ import * as winston from "winston"; import * as Transport from "winston-transport"; +import { EventEmitter } from "events"; +import * as path from "path"; +import * as fs from "fs"; +import { SPLAT } from "triple-beam"; +import { stripVTControlCharacters } from "util"; + +import { isVSCodeExtension } from "./vsCodeUtils"; + +/** + * vsceLogEmitter passes CLI logs along to VSCode. + * + * Events are of the format winston.LogEntry + * @example + * vsceLogEmitter.on("log", (logEntry) => { + * if (logEntry.level == "error") { + * console.log(logEntry.message) + * } + * }) + */ +export const vsceLogEmitter = new EventEmitter(); export type LogLevel = | "error" @@ -44,6 +64,8 @@ export interface Logger { add(transport: Transport): Logger; remove(transport: Transport): Logger; + + silent: boolean; } function expandErrors(logger: winston.Logger): winston.Logger { @@ -80,9 +102,71 @@ 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; +} + +export 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 = path.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"); +} + +export function tryStringify(value: any) { + if (typeof value === "string") { + return value; + } + + try { + return JSON.stringify(value); + } catch { + return value; + } +} + const rawLogger = winston.createLogger(); // Set a default silent logger to suppress logs during tests -rawLogger.add(new winston.transports.Console({ silent: true })); +rawLogger.add( + new winston.transports.Console({ + silent: true, + consoleWarnLevels: ["debug", "warn"], + }), +); rawLogger.exitOnError = false; // The type system for TypeScript is a bit wonky. The type of winston.LeveledLogMessage @@ -91,4 +175,52 @@ 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: Logger = maybeUseVSCodeLogger( + annotateDebugLines(expandErrors(rawLogger)), +) as unknown as Logger; + +/** + * Sets up logging to the firebase-debug.log file. + */ +export function useFileLogger(logFile?: string): string { + const logFileName = logFile ?? findAvailableLogFile(); + logger.add( + new winston.transports.File({ + level: "debug", + filename: logFileName, + format: winston.format.printf((info) => { + const segments = [info.message, ...(info[SPLAT] || [])].map(tryStringify); + return `[${info.level}] ${stripVTControlCharacters(segments.join(" "))}`; + }), + }), + ); + return logFileName; +} + +/** + * Sets up logging to the command line. + */ +export function useConsoleLoggers(): void { + if (process.env.DEBUG) { + logger.add( + new winston.transports.Console({ + level: "debug", + format: winston.format.printf((info) => { + const segments = [info.message, ...(info[SPLAT] || [])].map(tryStringify); + return `${stripVTControlCharacters(segments.join(" "))}`; + }), + }), + ); + } else if (process.env.IS_FIREBASE_CLI) { + logger.add( + new winston.transports.Console({ + level: "info", + format: winston.format.printf((info) => + [info.message, ...(info[SPLAT] || [])] + .filter((chunk) => typeof chunk === "string") + .join(" "), + ), + }), + ); + } +} diff --git a/src/management/apps.spec.ts b/src/management/apps.spec.ts new file mode 100644 index 00000000000..933bff11495 --- /dev/null +++ b/src/management/apps.spec.ts @@ -0,0 +1,837 @@ +import * as mockfs from "mock-fs"; +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 * as appUtils from "../appUtils"; +import * as utils from "../utils"; +import * as prompt from "../prompt"; +import { + AndroidAppMetadata, + AppPlatform, + APP_LIST_PAGE_SIZE, + createAndroidApp, + createIosApp, + createWebApp, + getAppConfig, + getAppConfigFile, + getAppPlatform, + IosAppMetadata, + listFirebaseApps, + WebAppMetadata, + findIntelligentPathForAndroid, + findIntelligentPathForIOS, + getPlatform, +} from "./apps"; +import * as pollUtils from "../operation-poller"; +import { FirebaseError } from "../error"; +import { firebaseApiOrigin } from "../api"; +import { AppsInitOptions } from "../commands/apps-init"; + +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; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + pollOperationStub = sandbox.stub(pollUtils, "pollOperation").throws("Unexpected poll call"); + 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("getPlatform", () => { + let getPlatformsFromFolderStub: sinon.SinonStub; + let promptForDirectoryStub: sinon.SinonStub; + let promptSelectStub: sinon.SinonStub; + + beforeEach(() => { + getPlatformsFromFolderStub = sinon.stub(appUtils, "getPlatformsFromFolder"); + promptForDirectoryStub = sinon.stub(utils, "promptForDirectory"); + promptSelectStub = sinon.stub(prompt, "select"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return the detected platform when only one is found", async () => { + getPlatformsFromFolderStub.resolves([appUtils.Platform.ANDROID]); + + const platform = await getPlatform("any-dir", {} as any); + + expect(platform).to.equal(AppPlatform.ANDROID); + expect(getPlatformsFromFolderStub.calledOnceWith("any-dir")).to.be.true; + }); + + it("should prompt the user if multiple platforms are detected", async () => { + getPlatformsFromFolderStub.resolves([appUtils.Platform.ANDROID, appUtils.Platform.IOS]); + promptSelectStub.resolves("IOS"); + + const platform = await getPlatform("any-dir", {} as any); + + expect(platform).to.equal(AppPlatform.IOS); + expect(promptSelectStub.calledOnce).to.be.true; + promptSelectStub.restore(); + }); + + it("should prompt the user if no platforms are detected", async () => { + getPlatformsFromFolderStub.withArgs("initial-dir").resolves([]); + getPlatformsFromFolderStub.withArgs("android-dir").resolves([appUtils.Platform.ANDROID]); + promptForDirectoryStub.resolves("android-dir"); + + const platform = await getPlatform("initial-dir", {} as any); + + expect(platform).to.equal(AppPlatform.ANDROID); + expect(promptForDirectoryStub.calledOnce).to.be.true; + }); + + it("should throw an error if a Flutter app is detected", async () => { + getPlatformsFromFolderStub.resolves([appUtils.Platform.FLUTTER]); + + let err; + try { + await getPlatform("any-dir", {} as any); + } catch (e: any) { + err = e; + } + + expect(err).to.be.an.instanceOf(FirebaseError); + expect(err.message).to.include("Flutter is not 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); + + const configData = await getAppConfig(APP_ID, AppPlatform.WEB); + const fileData = getAppConfigFile(configData, AppPlatform.WEB); + + expect(fileData).to.deep.equal({ + fileName: "firebase-js-config.json", + fileContents: JSON.stringify(mockWebConfig, null, 2), + }); + expect(nock.isDone()).to.be.true; + }); + }); + + 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; + }); + }); + describe("getAndroidPlatform", () => { + const cases: { + desc: string; + folderName: string; + folderItems: Record; + output: string; + }[] = [ + { + desc: "Root of Android project", + folderName: "android/", + folderItems: { app: {} }, + output: "android/app", + }, + { + desc: "Inside app folder", + folderName: "android/app", + folderItems: { src: {} }, + output: "android/app", + }, + { + desc: "Folder with many modules", + folderName: "android/", + folderItems: { module1: {}, module2: {}, module3: {} }, + output: "android/app", + }, + ]; + for (const c of cases) { + it(c.desc, async () => { + mockfs({ [c.folderName]: c.folderItems }); + const platform = await findIntelligentPathForAndroid(c.folderName, { + nonInteractive: true, + } as AppsInitOptions); + expect(platform).to.equal(c.output); + }); + } + + afterEach(() => { + mockfs.restore(); + }); + }); + describe("getIosPlatform", () => { + const cases: { + desc: string; + folderName: string; + folderItems: Record; + output: string; + throwError?: boolean; + }[] = [ + { + desc: "Root of ios project with xcodeproj files", + folderName: "ios/", + folderItems: { "abc.xcodeproj": "Contents", abc: {} }, + output: "ios/abc", + }, + { + desc: "Folder with Info.plist", + folderName: "ios", + folderItems: { "Info.plist": "" }, + output: "ios", + }, + { + desc: "Folder with Assets.xcassets", + folderName: "ios", + folderItems: { "Assets.xcassets": "" }, + output: "ios", + }, + { + desc: "Folder with Preview Content folder", + folderName: "ios", + folderItems: { "Preview Content": {} }, + output: "ios", + }, + { + desc: "Folder with Preview Content file", + folderName: "ios/", + folderItems: { "Preview Content": "" }, + output: "ios", + throwError: true, + }, + ]; + + for (const c of cases) { + it(c.desc, async () => { + mockfs({ [c.folderName]: c.folderItems }); + if (c.throwError) { + await expect( + findIntelligentPathForIOS(c.folderName, { + nonInteractive: true, + } as AppsInitOptions), + ).to.be.rejectedWith( + Error, + "We weren't able to automatically determine the output directory.", + ); + } else { + const platform = await findIntelligentPathForIOS(c.folderName, { + nonInteractive: true, + } as AppsInitOptions); + expect(platform).to.equal(c.output); + } + }); + } + afterEach(() => { + mockfs.restore(); + }); + }); +}); diff --git a/src/management/apps.ts b/src/management/apps.ts index 5c43f98cea8..be3c8716a0a 100644 --- a/src/management/apps.ts +++ b/src/management/apps.ts @@ -1,15 +1,281 @@ -import * as fs from "fs"; - -import * as api from "../api"; +import * as fs from "fs-extra"; +import * as ora from "ora"; +import * as path from "path"; +import { Client } from "../apiv2"; +import { firebaseApiOrigin } from "../api"; import { FirebaseError } from "../error"; import { logger } from "../logger"; import { pollOperation } from "../operation-poller"; +import { WebConfig } from "../fetchWebSetup"; +import { needProjectId } from "../projectUtils"; +import { input, select, confirm } from "../prompt"; +import { getOrPromptProject } from "./projects"; +import { Options } from "../options"; +import { Config } from "../config"; +import { getPlatformsFromFolder, Platform } from "../appUtils"; +import { logBullet, logSuccess, logWarning, promptForDirectory } from "../utils"; +import { AppsInitOptions } from "../commands/apps-init"; 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"; +async function getDisplayName(): Promise { + return await input("What would you like to call your app?"); +} + +interface CreateFirebaseAppOptions { + project: string; + nonInteractive: boolean; + displayName?: string; +} + +interface CreateIosAppOptions extends CreateFirebaseAppOptions { + bundleId?: string; + appStoreId?: string; +} + +interface CreateAndroidAppOptions extends CreateFirebaseAppOptions { + packageName: string; +} + +interface CreateWebAppOptions extends CreateFirebaseAppOptions { + displayName: string; +} + +export async function getPlatform(appDir: string, config: Config) { + // Detect what platform based on current user + let targetPlatforms = await getPlatformsFromFolder(appDir); + let targetPlatform: Platform; + + if (targetPlatforms.length === 0) { + // If we aren't in an app directory, ask the user where their app is, and try to autodetect from there. + appDir = await promptForDirectory({ + config, + relativeTo: appDir, // CWD is passed in as `appDir`, so we want it relative to the current directory instead of where firebase.json is. + message: "We couldn't determine what kind of app you're using. Where is your app directory?", + }); + targetPlatforms = await getPlatformsFromFolder(appDir); + } + + if (targetPlatforms.length !== 1) { + if (targetPlatforms.length === 0) { + logBullet(`Couldn't automatically detect your app in directory ${appDir}.`); + } else { + logSuccess(`Detected multiple app platforms in directory ${appDir}`); + // Can only setup one platform at a time, just ask the user + } + const platforms = [ + { name: "iOS (Swift)", value: Platform.IOS }, + { name: "Web (JavaScript)", value: Platform.WEB }, + { name: "Android (Kotlin)", value: Platform.ANDROID }, + ]; + targetPlatform = await select({ + message: + "Which platform do you want to set up an SDK for? Note: We currently do not support automatically setting up C++ or Unity projects.", + choices: platforms, + }); + } else { + targetPlatform = targetPlatforms[0]; + } + + if (targetPlatform === Platform.FLUTTER) { + logWarning(`Detected ${targetPlatform} app in directory ${appDir}`); + throw new FirebaseError(`Flutter is not supported by apps:configure. +Please follow the link below to set up firebase for your Flutter app: +https://firebase.google.com/docs/flutter/setup + `); + } else { + logSuccess(`Detected ${targetPlatform} app in directory ${appDir}`); + } + + return targetPlatform as unknown as AppPlatform; +} + +async function initiateIosAppCreation(options: CreateIosAppOptions): Promise { + if (!options.nonInteractive) { + options.displayName = options.displayName || (await getDisplayName()); + options.bundleId = options.bundleId || (await input("Please specify your iOS app bundle ID:")); + options.appStoreId = + options.appStoreId || (await input("Please specify your iOS app App Store ID:")); + } + if (!options.bundleId) { + throw new FirebaseError("Bundle ID for iOS app cannot be empty"); + } + + const spinner = ora("Creating your iOS app").start(); + try { + const appData = await createIosApp(options.project, { + displayName: options.displayName, + bundleId: options.bundleId, + appStoreId: options.appStoreId, + }); + spinner.succeed(); + return appData; + } catch (err: any) { + spinner.fail(); + throw err; + } +} + +async function initiateAndroidAppCreation( + options: CreateAndroidAppOptions, +): Promise { + if (!options.nonInteractive) { + options.displayName = options.displayName || (await getDisplayName()); + options.packageName = + options.packageName || (await input("Please specify your Android app package name:")); + } + if (!options.packageName) { + throw new FirebaseError("Package name for Android app cannot be empty"); + } + + const spinner = ora("Creating your Android app").start(); + try { + const appData = await createAndroidApp(options.project, { + displayName: options.displayName, + packageName: options.packageName, + }); + spinner.succeed(); + return appData; + } catch (err: any) { + spinner.fail(); + throw err; + } +} + +async function initiateWebAppCreation(options: CreateWebAppOptions): Promise { + if (!options.nonInteractive) { + options.displayName = options.displayName || (await getDisplayName()); + } + if (!options.displayName) { + throw new FirebaseError("Display name for Web app cannot be empty"); + } + const spinner = ora("Creating your Web app").start(); + try { + const appData = await createWebApp(options.project, { displayName: options.displayName }); + spinner.succeed(); + return appData; + } catch (err: any) { + spinner.fail(); + throw err; + } +} +export type SdkInitOptions = CreateIosAppOptions | CreateAndroidAppOptions | CreateWebAppOptions; +export async function sdkInit(appPlatform: AppPlatform, options: SdkInitOptions) { + let appData; + switch (appPlatform) { + case AppPlatform.IOS: + appData = await initiateIosAppCreation(options); + break; + case AppPlatform.ANDROID: + appData = await initiateAndroidAppCreation(options as CreateAndroidAppOptions); + break; + case AppPlatform.WEB: + appData = await initiateWebAppCreation(options as CreateWebAppOptions); + break; + default: + throw new FirebaseError("Unexpected error. This should not happen"); + } + return appData; +} +export async function getSdkOutputPath( + appDir: string, + platform: AppPlatform, + config: AppsInitOptions, +): Promise { + switch (platform) { + case AppPlatform.ANDROID: + const androidPath = await findIntelligentPathForAndroid(appDir, config); + return path.join(androidPath, "google-services.json"); + case AppPlatform.WEB: + return path.join(appDir, "firebase-js-config.json"); + case AppPlatform.IOS: + const iosPath = await findIntelligentPathForIOS(appDir, config); + return path.join(iosPath, "GoogleService-Info.plist"); + } + throw new FirebaseError("Platform " + platform.toString() + " is not supported yet."); +} +export 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.\n" + + "You can create an app for this project with 'firebase apps:create'", + ); + } +} +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) => { + return { + name: + `${app.displayName || app.bundleId || app.packageName}` + + ` - ${app.appId} (${app.platform})`, + value: app, + }; + }); + + return await select({ + message: + `Select the ${appPlatform === AppPlatform.ANY ? "" : appPlatform + " "}` + + "app to get the configuration data:", + choices, + }); +} + +export async function getSdkConfig( + options: Options, + appPlatform: AppPlatform, + appId?: string, +): Promise { + 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); + // 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; + } + } + + let configData: AppConfig; + const spinner = ora( + `Downloading configuration data for your Firebase ${appPlatform} app`, + ).start(); + try { + configData = await getAppConfig(appId, appPlatform); + } catch (err: any) { + spinner.fail(); + throw err; + } + spinner.succeed(); + + return configData; +} export interface AppMetadata { name: string /* The fully qualified resource name of the Firebase App */; @@ -41,7 +307,7 @@ export interface AppConfigurationData { // File contents in utf8 format. fileContents: string; // Only for `AppPlatform.WEB`, the raw configuration parameters. - sdkConfig?: { [key: string]: string }; + sdkConfig?: AppConfig; } export interface AppAndroidShaData { @@ -84,6 +350,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 +361,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 +399,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 +440,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 +485,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 +499,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 +527,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 +535,7 @@ export async function listFirebaseApps( { exit: 2, original: err, - } + }, ); } } @@ -283,18 +556,19 @@ 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 { +function parseConfigFromResponse( + responseBody: AppConfig, + platform: AppPlatform, +): AppConfigurationData { if (platform === AppPlatform.WEB) { - const JS_TEMPLATE = fs.readFileSync(__dirname + "/../../templates/setup/web.js", "utf8"); return { - fileName: WEB_CONFIG_FILE_NAME, - fileContents: JS_TEMPLATE.replace("{/*--CONFIG--*/}", JSON.stringify(responseBody, null, 2)), + fileName: "firebase-js-config.json", + fileContents: JSON.stringify(responseBody, null, 2), }; - } else if (platform === AppPlatform.ANDROID || platform === AppPlatform.IOS) { + } else if ("configFilename" in responseBody) { return { fileName: responseBody.configFilename, fileContents: Buffer.from(responseBody.configFileContents, "base64").toString("utf8"), @@ -303,6 +577,11 @@ function parseConfigFromResponse(responseBody: any, platform: AppPlatform): AppC throw new FirebaseError("Unexpected app platform"); } +export interface MobileConfig { + configFilename: string; + configFileContents: string; +} + /** * Returns information representing the file need to initalize the application. * @param config the object from `getAppConfig`. @@ -310,10 +589,31 @@ function parseConfigFromResponse(responseBody: any, platform: AppPlatform): AppC * @return the platform-specific file information (name and contents). */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function getAppConfigFile(config: any, platform: AppPlatform): AppConfigurationData { +export function getAppConfigFile(config: AppConfig, platform: AppPlatform): AppConfigurationData { return parseConfigFromResponse(config, platform); } +export type AppConfig = MobileConfig | WebConfig; + +export async function writeConfigToFile( + filename: string, + nonInteractive: boolean, + fileContents: string, +) { + if (fs.existsSync(filename)) { + if (nonInteractive) { + throw new FirebaseError(`${filename} already exists`); + } + const overwrite = await confirm(`${filename} already exists. Do you want to overwrite?`); + + if (!overwrite) { + return false; + } + } + await fs.writeFile(filename, fileContents); + return true; +} + /** * Gets the configuration artifact associated with the specified a Firebase app. * @param appId the ID of the app. @@ -322,25 +622,24 @@ export function getAppConfigFile(config: any, platform: AppPlatform): AppConfigu * base64-encoded content string. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function getAppConfig(appId: string, platform: AppPlatform): Promise { - let response; +export async function getAppConfig(appId: string, platform: AppPlatform): Promise { 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 +650,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 +672,7 @@ export async function listAppAndroidSha( { exit: 2, original: err, - } + }, ); } } @@ -391,31 +687,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 +719,89 @@ 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, - } + }, ); } } + +export async function findIntelligentPathForIOS(appDir: string, options: AppsInitOptions) { + const currentFiles: fs.Dirent[] = await fs.readdir(appDir, { withFileTypes: true }); + for (let i = 0; i < currentFiles.length; i++) { + const dirent = currentFiles[i]; + const xcodeStr = ".xcodeproj"; + const file = dirent.name; + if (file.endsWith(xcodeStr)) { + return path.join(appDir, file.substring(0, file.length - xcodeStr.length)); + } else if ( + file === "Info.plist" || + file === "Assets.xcassets" || + (dirent.isDirectory() && file === "Preview Content") + ) { + return appDir; + } + } + let outputPath: string | null = null; + if (!options.nonInteractive) { + outputPath = await promptForDirectory({ + config: options.config, + message: `We weren't able to automatically determine the output directory. Where would you like to output your config file?`, + relativeTo: appDir, + }); + } + if (!outputPath) { + throw new Error("We weren't able to automatically determine the output directory."); + } + return outputPath; +} + +export async function findIntelligentPathForAndroid(appDir: string, options: AppsInitOptions) { + /** + * android/build.gradle // if it's this, choose app + * android/app/build.gradle // if it's this, choose current dir. + */ + const paths = appDir.split("/"); + // For when app/build.gradle is found + if (paths[0] === "app") { + return appDir; + } else { + const currentFiles: fs.Dirent[] = await fs.readdir(appDir, { withFileTypes: true }); + const dirs: string[] = []; + for (const fileOrDir of currentFiles) { + if (fileOrDir.isDirectory()) { + if (fileOrDir.name !== "gradle") { + dirs.push(fileOrDir.name); + } + if (fileOrDir.name === "src") { + return appDir; + } + } + } + let module = path.join(appDir, "app"); + // If app is the only module available, then put google-services.json in app/ + if (dirs.length === 1 && dirs[0] === "app") { + return module; + } + if (!options.nonInteractive) { + module = await promptForDirectory({ + config: options.config, + message: `We weren't able to automatically determine the output directory. Where would you like to output your config file?`, + }); + } + return module; + } +} 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 509be437d45..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 { @@ -29,6 +31,7 @@ export enum DatabaseInstanceState { export enum DatabaseLocation { US_CENTRAL1 = "us-central1", EUROPE_WEST1 = "europe-west1", + ASIA_SOUTHEAST1 = "asia-southeast1", ANY = "-", } @@ -41,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. @@ -58,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) { @@ -87,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, - } + }, ); } } @@ -108,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, - } + }, ); } } @@ -149,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) { @@ -187,7 +175,7 @@ export async function checkInstanceNameAvailable( `Failed to validate Realtime Database instance name: ${instanceName}.`, { original: err, - } + }, ); } } @@ -200,21 +188,23 @@ export async function checkInstanceNameAvailable( */ export function parseDatabaseLocation( location: string, - defaultLocation: DatabaseLocation + defaultLocation: DatabaseLocation, ): DatabaseLocation { if (!location) { return defaultLocation; } switch (location.toLowerCase()) { - case "europe-west1": - return DatabaseLocation.EUROPE_WEST1; case "us-central1": return DatabaseLocation.US_CENTRAL1; + case "europe-west1": + return DatabaseLocation.EUROPE_WEST1; + case "asia-southeast1": + return DatabaseLocation.ASIA_SOUTHEAST1; case "": return defaultLocation; default: throw new FirebaseError( - `Unexpected location value: ${location}. Only us-central1, and europe-west1 locations are supported` + `Unexpected location value: ${location}. Only us-central1, europe-west1, and asia-southeast1 locations are supported`, ); } } @@ -230,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)); } @@ -253,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${ @@ -262,7 +252,7 @@ export async function listDatabaseInstances( { exit: 2, original: err, - } + }, ); } } @@ -273,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 76% rename from src/test/management/projects.spec.ts rename to src/management/projects.spec.ts index 7280c213ef4..eeef6f70960 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 promptImport 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,159 +83,163 @@ 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", () => { - let promptOnceStub: sinon.SinonStub; + let prompt: sinon.SinonStubbedInstance; beforeEach(() => { - sandbox.stub(prompt, "prompt").throws("Unexpected prompt call"); - promptOnceStub = sandbox.stub(prompt, "promptOnce").throws("Unexpected promptOnce call"); + prompt = sinon.stub(promptImport); + prompt.select.throws("Unexpected select call"); + prompt.input.throws("Unexpected input call"); + }); + + afterEach(() => { + sinon.verifyAndRestore(); }); 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, { results: [TEST_FIREBASE_PROJECT, ANOTHER_FIREBASE_PROJECT], }); - promptOnceStub.resolves("my-project-123"); + prompt.select.resolves("my-project-123"); const project = await projectManager.getOrPromptProject({}); expect(project).to.deep.equal(TEST_FIREBASE_PROJECT); - expect(promptOnceStub).to.be.calledOnce; - expect(promptOnceStub.firstCall.args[0].type).to.equal("list"); + expect(prompt.select).to.be.calledOnce; expect(nock.isDone()).to.be.true; }); 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"); + prompt.input.resolves("my-project-123"); const project = await projectManager.getOrPromptProject({}); expect(project).to.deep.equal(TEST_FIREBASE_PROJECT); - expect(promptOnceStub).to.be.calledOnce; - expect(promptOnceStub.firstCall.args[0].type).to.equal("input"); + expect(prompt.input).to.be.calledOnce; expect(nock.isDone()).to.be.true; }); 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(prompt.select).to.be.not.called; + expect(prompt.input).to.be.not.called; expect(nock.isDone()).to.be.true; }); 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); const project = await projectManager.getOrPromptProject(options); expect(project).to.deep.equal(TEST_FIREBASE_PROJECT); - expect(promptOnceStub).to.be.not.called; + expect(prompt.select).to.be.not.called; + expect(prompt.input).to.be.not.called; expect(nock.isDone()).to.be.true; }); 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; + expect(prompt.input).to.be.not.called; + expect(prompt.select).to.be.not.called; expect(nock.isDone()).to.be.true; }); }); 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, { projectInfo: [TEST_CLOUD_PROJECT, ANOTHER_CLOUD_PROJECT], }); - promptOnceStub.resolves("my-project-123"); + prompt.select.resolves("my-project-123"); const projectId = await projectManager.promptAvailableProjectId(); expect(projectId).to.deep.equal("my-project-123"); - expect(promptOnceStub).to.be.calledOnce; - expect(promptOnceStub.firstCall.args[0].type).to.equal("list"); + expect(prompt.select).to.be.calledOnce; expect(nock.isDone()).to.be.true; }); 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, { projectInfo: [TEST_CLOUD_PROJECT, ANOTHER_CLOUD_PROJECT], nextPageToken: "token", }); - promptOnceStub.resolves("my-project-123"); + prompt.input.resolves("my-project-123"); const projectId = await projectManager.promptAvailableProjectId(); expect(projectId).to.deep.equal("my-project-123"); - expect(promptOnceStub).to.be.calledOnce; - expect(promptOnceStub.firstCall.args[0].type).to.equal("input"); + expect(prompt.input).to.be.calledOnce; expect(nock.isDone()).to.be.true; }); it("should throw if there's no project", async () => { - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get("/v1beta1/availableProjects") .query({ pageSize: 100 }) .reply(200, { @@ -243,14 +249,15 @@ 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(prompt.input).to.be.not.called; + expect(prompt.select).to.be.not.called; expect(nock.isDone()).to.be.true; }); }); @@ -264,7 +271,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 +282,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 +300,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 +325,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 +346,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 +356,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 +417,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 +432,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 +448,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 +462,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 +470,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 +486,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 +501,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 +517,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 +531,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 +539,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 +555,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 +571,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 +589,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 +597,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 +613,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 +625,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 +639,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 +651,7 @@ describe("Project management", () => { locationId: LOCATION_ID, }, }; - nock(api.firebaseApiOrigin) + nock(api.firebaseApiOrigin()) .get(`/v1beta1/projects/${PROJECT_ID}`) .reply(200, expectedProjectInfo); @@ -686,20 +662,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..65b465aa150 100644 --- a/src/management/projects.ts +++ b/src/management/projects.ts @@ -1,46 +1,23 @@ -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 { FirebaseError, getErrStatus } from "../error"; import { pollOperation } from "../operation-poller"; -import { promptOnce } from "../prompt"; -import { Question } from "inquirer"; +import * as prompt from "../prompt"; import * as api from "../api"; import { logger } from "../logger"; import * as utils from "../utils"; +import { FirebaseProjectMetadata, CloudProjectInfo, ProjectPage } from "../types/project"; +import { bestEffortEnsure } from "../ensureApiEnabled"; +import { Options } from "../options"; +import { Constants } from "../emulator/constants"; 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; -} +const CHECK_PROJECT_ID_API_REQUEST_TIMEOUT_MILLIS = 15000; export enum ProjectParentResourceType { ORGANIZATION = "organization", @@ -52,39 +29,93 @@ export interface ProjectParentResource { type: ProjectParentResourceType; } -export const PROJECTS_CREATE_QUESTIONS: Question[] = [ - { - type: "input", - name: "projectId", - default: "", - message: - "Please specify a unique project id " + - `(${clc.yellow("warning")}: cannot be modified afterward) [6-30 characters]:\n`, - }, - { - type: "input", - name: "displayName", - default: "", - message: "What would you like to call your project? (defaults to your project ID)", - }, -]; +/** + * Prompt user to create a new project + */ +export async function promptProjectCreation( + options: Options, +): Promise<{ projectId: string; displayName: string }> { + const projectId = + options.projectId ?? + (await prompt.input({ + message: + "Please specify a unique project id " + + `(${clc.yellow("warning")}: cannot be modified afterward) [6-30 characters]:\n`, + validate: async (projectId: string) => { + if (projectId.length < 6) { + return "Project ID must be at least 6 characters long"; + } else if (projectId.length > 30) { + return "Project ID cannot be longer than 30 characters"; + } + if (Constants.isDemoProject(projectId)) { + return "Project ID cannot starts with demo-"; + } + + try { + // Best effort. We should still allow project creation even if this fails. + const { isAvailable, suggestedProjectId } = await checkAndRecommendProjectId(projectId); + if (!isAvailable && suggestedProjectId) { + return `Project ID is taken or unavailable. Try ${clc.bold(suggestedProjectId)}.`; + } + } catch (error: any) { + logger.debug( + `Couldn't check if project ID ${projectId} is available. Original error: ${error}`, + ); + } + + return true; + }, + })); + + const displayName = + (options.displayName as string) ?? + (await prompt.input({ + default: projectId, + message: "What would you like to call your project? (defaults to your project ID)", + validate: (displayName: string) => { + if (displayName.length < 4) { + return "Project name must be at least 4 characters long"; + } else if (displayName.length > 30) { + return "Project name cannot be longer than 30 characters"; + } else { + return true; + } + }, + })); + + return { projectId, displayName }; +} const firebaseAPIClient = new Client({ - urlPrefix: api.firebaseApiOrigin, + urlPrefix: api.firebaseApiOrigin(), auth: true, apiVersion: "v1beta1", }); +const firebaseV1APIClient = new Client({ + urlPrefix: api.firebaseApiOrigin(), + auth: true, + apiVersion: "v1", +}); + +const resourceManagerClient = new Client({ + urlPrefix: api.resourceManagerOrigin(), + apiVersion: "v1", +}); + +/** + * Create a new Google Cloud Platform project and add Firebase resources to it + */ 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; } @@ -92,15 +123,18 @@ export async function createFirebaseProjectAndLog( return addFirebaseToCloudProjectAndLog(projectId); } +/** + * Add Firebase resources to a Google Cloud Platform project + */ 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; } @@ -120,26 +154,30 @@ function logNewFirebaseProjectInfo(projectInfo: FirebaseProjectMetadata): void { logger.info(""); logger.info("Project information:"); logger.info(` - Project ID: ${clc.bold(projectInfo.projectId)}`); - logger.info(` - Project Name: ${clc.bold(projectInfo.displayName)}`); + if (projectInfo.displayName) { + logger.info(` - Project Name: ${clc.bold(projectInfo.displayName)}`); + } 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`, ); } /** * Get the user's desired project, prompting if necessary. */ -export async function getOrPromptProject(options: any): Promise { +export async function getOrPromptProject( + options: Partial, +): Promise { if (options.project) { return await getFirebaseProject(options.project); } return selectProjectInteractively(); } -async function selectProjectInteractively( - pageSize: number = MAXIMUM_PROMPT_LIST +export async function selectProjectInteractively( + pageSize: number = MAXIMUM_PROMPT_LIST, ): Promise { const { projects, nextPageToken } = await getFirebaseProjectPage(pageSize); if (projects.length === 0) { @@ -148,47 +186,47 @@ async function selectProjectInteractively( if (nextPageToken) { // Prompt user for project ID if we can't list all projects in 1 page logger.debug(`Found more than ${projects.length} projects, selecting via prompt`); - return selectProjectByPrompting(); + return await getFirebaseProject(await selectProjectByPrompting()); } return selectProjectFromList(projects); } -async function selectProjectByPrompting(): Promise { - const projectId = await promptOnce({ - type: "input", - message: "Please input the project ID you would like to use:", - }); - - return await getFirebaseProject(projectId); +async function selectProjectByPrompting(): Promise { + const projectId = await prompt.input("Please input the project ID you would like to use:"); + if (!projectId) { + throw new FirebaseError("Project ID cannot be empty"); + } + if (Constants.isDemoProject(projectId)) { + throw new FirebaseError("Project ID cannot starts with demo-"); + } + return projectId; } /** * 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({ - type: "list", - name: "id", + const projectId: string = await prompt.select({ message: "Select a default Firebase project for this directory:", choices, }); @@ -217,18 +255,16 @@ 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.", ); } if (nextPageToken) { - // Prompt for project ID if we can't list all projects in 1 page - return await promptOnce({ - type: "input", - message: "Please input the ID of the Google Cloud Project you would like to add Firebase:", - }); + // Prompt user for project ID if we can't list all projects in 1 page + logger.debug(`Found more than ${projects.length} projects, selecting via prompt`); + return await selectProjectByPrompting(); } else { - let choices = projects + const choices = projects .filter((p: CloudProjectInfo) => !!p) .map((p) => { const projectId = getProjectId(p); @@ -236,11 +272,9 @@ export async function promptAvailableProjectId(): Promise { name: projectId + (p.displayName ? ` (${p.displayName})` : ""), value: projectId, }; - }); - choices = _.orderBy(choices, ["name"], ["asc"]); - return await promptOnce({ - type: "list", - name: "id", + }) + .sort((a, b) => a.name.localeCompare(b.name)); + return await prompt.select({ message: "Select the Google Cloud Platform project you would like to add Firebase:", choices, }); @@ -254,33 +288,37 @@ 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 data = { + projectId, + name: options.displayName || projectId, + parent: options.parentResource, + }; + const response = await resourceManagerClient.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 +335,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 +365,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 +394,7 @@ async function getProjectPage( */ export async function getFirebaseProjectPage( pageSize: number = PROJECT_LIST_PAGE_SIZE, - pageToken?: string + pageToken?: string, ): Promise> { let projectPage; @@ -366,11 +404,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 +421,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 +429,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 +450,7 @@ export async function listFirebaseProjects(pageSize?: number): Promise = await getFirebaseProjectPage( pageSize, - nextPageToken + nextPageToken, ); projects.push(...projectPage.projects); nextPageToken = projectPage.nextPageToken; @@ -421,6 +459,35 @@ export async function listFirebaseProjects(pageSize?: number): Promise { + try { + const res = await firebaseV1APIClient.request< + any, + { projectIdStatus: string; suggestedProjectId?: string } + >({ + method: "POST", + path: "/projects:checkProjectId", + body: { + proposedId: projectId, + }, + timeout: CHECK_PROJECT_ID_API_REQUEST_TIMEOUT_MILLIS, + }); + + const { projectIdStatus, suggestedProjectId } = res.body; + return { + isAvailable: projectIdStatus === "PROJECT_ID_AVAILABLE", + suggestedProjectId, + }; + } catch (err: any) { + throw new FirebaseError( + "Failed to check if project ID is available. See firebase-debug.log for more info.", + { exit: 2, original: err }, + ); + } +} + /** * Gets the Firebase project information identified by the specified project ID */ @@ -432,12 +499,84 @@ export async function getFirebaseProject(projectId: string): Promise'); + return info; + } catch (err: any) { + logger.debug(`Unable to get project info from resourcemanager for ${projectId}: ${err}`); + } + } + let message = err.message; + if (err.original) { + message += ` (original: ${err.original.message})`; + } + logger.debug(message); throw new FirebaseError( `Failed to get Firebase project ${projectId}. ` + "Please make sure the project exists and your account has permission to access it.", - { exit: 2, original: err } + { exit: 2, original: err }, + ); + } +} + +export interface ProjectInfo { + projectNumber: string; + projectId: string; + lifecycleState: string; + name: string; + createTime: string; + parent: { type: string; id: string }; +} + +/** + * Gets basic information about any Cloud project. Does not use Firebase TOS APIs, so this is safe for core app projects. + * @param projectId + */ +export async function getProject(projectId: string): Promise { + await bestEffortEnsure(projectId, api.resourceManagerOrigin(), "firebase", true); + const response = await resourceManagerClient.get(`/projects/${projectId}`); + return response.body; +} + +/** + * Checks if Firebase services are enabled for a Google Cloud Platform project. + * @param projectId The project ID to check + * @return A promise that resolves to the Firebase project metadata if enabled, undefined otherwise + */ +export async function checkFirebaseEnabledForCloudProject( + projectId: string, +): Promise { + try { + const res = await firebaseAPIClient.request({ + method: "GET", + path: `/projects/${projectId}`, + timeout: TIMEOUT_MILLIS, + }); + return res.body; + } catch (err: any) { + if (getErrStatus(err) === 404) { + // 404 means Firebase is not enabled for this project + return undefined; + } + let message = err.message; + if (err.original) { + message += ` (original: ${err.original.message})`; + } + logger.debug(message); + throw new FirebaseError( + `Failed to check if Firebase is enabled for project ${projectId}. ` + + "Please make sure the project exists and your account has permission to access it.", + { exit: 2, original: err }, ); } } diff --git a/src/management/provisioning/provision.spec.ts b/src/management/provisioning/provision.spec.ts new file mode 100644 index 00000000000..a0d2bc9cac2 --- /dev/null +++ b/src/management/provisioning/provision.spec.ts @@ -0,0 +1,1236 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as nock from "nock"; +import { firebaseApiOrigin } from "../../api"; +import * as pollUtils from "../../operation-poller"; +import { + buildAppNamespace, + buildParentString, + buildProvisionRequest, + provisionFirebaseApp, +} from "./provision"; +import { + ProvisionAppOptions, + ProvisionFirebaseAppOptions, + ProvisionFirebaseAppResponse, +} from "./types"; +import { FirebaseError } from "../../error"; +import { AppPlatform } from "../apps"; + +// Test constants +const BUNDLE_ID = "com.example.testapp"; +const PACKAGE_NAME = "com.example.androidapp"; +const WEB_APP_ID = "web-app-123"; +const PROJECT_DISPLAY_NAME = "Test Project"; +const REQUEST_ID = "test-request-id-123"; +const LOCATION = "us-central1"; +const OPERATION_RESOURCE_NAME = "operations/provision.123456789"; +const APP_RESOURCE = "projects/test-project/apps/123456789"; +const CONFIG_DATA = "base64-encoded-config-data"; +const CONFIG_MIME_TYPE = "application/json"; + +describe("Provision module", () => { + let sandbox: sinon.SinonSandbox; + let pollOperationStub: sinon.SinonStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + pollOperationStub = sandbox.stub(pollUtils, "pollOperation"); + pollOperationStub.throws("Unexpected poll call"); + nock.disableNetConnect(); + }); + + afterEach(() => { + sandbox.restore(); + nock.enableNetConnect(); + nock.cleanAll(); + }); + + // Phase 1: buildAppNamespace helper function tests + describe("buildAppNamespace", () => { + it("should return appId when provided (takes precedence)", () => { + const appWithAppId: ProvisionAppOptions = { + platform: AppPlatform.IOS, + bundleId: BUNDLE_ID, + appId: "1:123456789:ios:abcdef123456", + }; + + const result = buildAppNamespace(appWithAppId); + + expect(result).to.equal("1:123456789:ios:abcdef123456"); + }); + + it("should return bundleId for iOS apps", () => { + const iosApp: ProvisionAppOptions = { + platform: AppPlatform.IOS, + bundleId: BUNDLE_ID, + }; + + const result = buildAppNamespace(iosApp); + + expect(result).to.equal(BUNDLE_ID); + }); + + it("should return packageName for Android apps", () => { + const androidApp: ProvisionAppOptions = { + platform: AppPlatform.ANDROID, + packageName: PACKAGE_NAME, + }; + + const result = buildAppNamespace(androidApp); + + expect(result).to.equal(PACKAGE_NAME); + }); + + it("should return webAppId for Web apps", () => { + const webApp: ProvisionAppOptions = { + platform: AppPlatform.WEB, + webAppId: WEB_APP_ID, + }; + + const result = buildAppNamespace(webApp); + + expect(result).to.equal(WEB_APP_ID); + }); + + it("should throw error for unsupported platform", () => { + const unsupportedApp = { + platform: "UNSUPPORTED" as any, + bundleId: BUNDLE_ID, + }; + + expect(() => buildAppNamespace(unsupportedApp)).to.throw("Unsupported platform"); + }); + + it("should throw error when iOS bundleId is empty", () => { + const iosApp: ProvisionAppOptions = { + platform: AppPlatform.IOS, + bundleId: "", + }; + + expect(() => buildAppNamespace(iosApp)).to.throw("App namespace cannot be empty"); + }); + + it("should throw error when Android packageName is empty", () => { + const androidApp: ProvisionAppOptions = { + platform: AppPlatform.ANDROID, + packageName: "", + }; + + expect(() => buildAppNamespace(androidApp)).to.throw("App namespace cannot be empty"); + }); + + it("should throw error when Web webAppId is empty", () => { + const webApp: ProvisionAppOptions = { + platform: AppPlatform.WEB, + webAppId: "", + }; + + expect(() => buildAppNamespace(webApp)).to.throw("App namespace cannot be empty"); + }); + + it("should throw error when iOS bundleId is missing", () => { + const iosApp: ProvisionAppOptions = { + platform: AppPlatform.IOS, + }; + + expect(() => buildAppNamespace(iosApp)).to.throw("App namespace cannot be empty"); + }); + + it("should throw error when Android packageName is missing", () => { + const androidApp: ProvisionAppOptions = { + platform: AppPlatform.ANDROID, + }; + + expect(() => buildAppNamespace(androidApp)).to.throw("App namespace cannot be empty"); + }); + + it("should throw error when Web webAppId is missing", () => { + const webApp: ProvisionAppOptions = { + platform: AppPlatform.WEB, + }; + + expect(() => buildAppNamespace(webApp)).to.throw("App namespace cannot be empty"); + }); + + it("should fall back to bundleId when appId is empty string", () => { + const appWithEmptyId: ProvisionAppOptions = { + platform: AppPlatform.IOS, + bundleId: BUNDLE_ID, + appId: "", + }; + + const result = buildAppNamespace(appWithEmptyId); + expect(result).to.equal(BUNDLE_ID); + }); + }); + + // Phase 2: buildParentString helper function tests + describe("buildParentString", () => { + it("should format existing project parent correctly", () => { + const parent = { + type: "existing_project" as const, + projectId: "my-project-123", + }; + + const result = buildParentString(parent); + + expect(result).to.equal("projects/my-project-123"); + }); + + it("should format organization parent correctly", () => { + const parent = { + type: "organization" as const, + organizationId: "123456789", + }; + + const result = buildParentString(parent); + + expect(result).to.equal("organizations/123456789"); + }); + + it("should format folder parent correctly", () => { + const parent = { + type: "folder" as const, + folderId: "987654321", + }; + + const result = buildParentString(parent); + + expect(result).to.equal("folders/987654321"); + }); + + it("should throw error for unsupported parent type", () => { + const unsupportedParent = { + type: "invalid" as any, + projectId: "test", + }; + + expect(() => buildParentString(unsupportedParent)).to.throw("Unsupported parent type"); + }); + }); + + // Phase 3: buildProvisionRequest helper function tests + describe("buildProvisionRequest", () => { + it("should build basic request with minimal options", () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + }; + + const result = buildProvisionRequest(options); + + expect(result).to.deep.equal({ + appNamespace: WEB_APP_ID, + displayName: PROJECT_DISPLAY_NAME, + webInput: {}, + }); + }); + + it("should include parent when specified", () => { + const options: ProvisionFirebaseAppOptions = { + project: { + displayName: PROJECT_DISPLAY_NAME, + parent: { type: "existing_project", projectId: "my-project-123" }, + }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + }; + + const result = buildProvisionRequest(options); + + expect(result).to.deep.include({ + appNamespace: WEB_APP_ID, + displayName: PROJECT_DISPLAY_NAME, + parent: "projects/my-project-123", + webInput: {}, + }); + }); + + it("should include location when specified", () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + features: { location: LOCATION }, + }; + + const result = buildProvisionRequest(options); + + expect(result).to.deep.include({ + appNamespace: WEB_APP_ID, + displayName: PROJECT_DISPLAY_NAME, + location: LOCATION, + webInput: {}, + }); + }); + + it("should include requestId when specified", () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + requestId: REQUEST_ID, + }; + + const result = buildProvisionRequest(options); + + expect(result).to.deep.include({ + appNamespace: WEB_APP_ID, + displayName: PROJECT_DISPLAY_NAME, + requestId: REQUEST_ID, + webInput: {}, + }); + }); + + it("should build iOS-specific request correctly", () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { + platform: AppPlatform.IOS, + bundleId: BUNDLE_ID, + appStoreId: "12345", + teamId: "TEAM123", + displayName: PROJECT_DISPLAY_NAME, + }, + }; + + const result = buildProvisionRequest(options); + + expect(result).to.deep.equal({ + appNamespace: BUNDLE_ID, + displayName: PROJECT_DISPLAY_NAME, + appleInput: { + appStoreId: "12345", + teamId: "TEAM123", + }, + }); + }); + + it("should build Android-specific request correctly", () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { + platform: AppPlatform.ANDROID, + packageName: PACKAGE_NAME, + sha1Hashes: ["sha1hash1", "sha1hash2"], + sha256Hashes: ["sha256hash1"], + displayName: PROJECT_DISPLAY_NAME, + }, + }; + + const result = buildProvisionRequest(options); + + expect(result).to.deep.equal({ + appNamespace: PACKAGE_NAME, + displayName: PROJECT_DISPLAY_NAME, + androidInput: { + sha1Hashes: ["sha1hash1", "sha1hash2"], + sha256Hashes: ["sha256hash1"], + }, + }); + }); + + it("should build Web-specific request correctly", () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + }; + + const result = buildProvisionRequest(options); + + expect(result).to.deep.equal({ + appNamespace: WEB_APP_ID, + displayName: PROJECT_DISPLAY_NAME, + webInput: {}, + }); + }); + + it("should include AI features when specified", () => { + const aiFeatures = { enableAiLogic: true, model: "gemini-pro" }; + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + features: { firebaseAiLogicInput: aiFeatures }, + }; + + const result = buildProvisionRequest(options); + + expect(result).to.deep.include({ + appNamespace: WEB_APP_ID, + displayName: PROJECT_DISPLAY_NAME, + firebaseAiLogicInput: aiFeatures, + webInput: {}, + }); + }); + }); + + // Phase 4: Main Function Success Cases + describe("provisionFirebaseApp - Success Cases", () => { + const mockResponse: ProvisionFirebaseAppResponse = { + configMimeType: CONFIG_MIME_TYPE, + configData: CONFIG_DATA, + appResource: APP_RESOURCE, + }; + + it("should provision iOS app successfully", async () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.IOS, bundleId: BUNDLE_ID }, + }; + + // Mock API call + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + expect(pollOperationStub.calledOnce).to.be.true; + }); + + it("should provision Android app successfully", async () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.ANDROID, packageName: PACKAGE_NAME }, + }; + + // Mock API call + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + expect(pollOperationStub.calledOnce).to.be.true; + }); + + it("should provision Web app successfully", async () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + }; + + // Mock API call + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + expect(pollOperationStub.calledOnce).to.be.true; + }); + + it("should provision with existing project parent", async () => { + const options: ProvisionFirebaseAppOptions = { + project: { + displayName: PROJECT_DISPLAY_NAME, + parent: { type: "existing_project", projectId: "parent-project-123" }, + }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + }; + + // Mock API call with parent verification + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body.parent).to.equal("projects/parent-project-123"); + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + + it("should provision with organization parent", async () => { + const options: ProvisionFirebaseAppOptions = { + project: { + displayName: PROJECT_DISPLAY_NAME, + parent: { type: "organization", organizationId: "987654321" }, + }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + }; + + // Mock API call with parent verification + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body.parent).to.equal("organizations/987654321"); + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + + it("should provision with folder parent", async () => { + const options: ProvisionFirebaseAppOptions = { + project: { + displayName: PROJECT_DISPLAY_NAME, + parent: { type: "folder", folderId: "123456789" }, + }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + }; + + // Mock API call with parent verification + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body.parent).to.equal("folders/123456789"); + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + + it("should provision with requestId for idempotency", async () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + requestId: REQUEST_ID, + }; + + // Mock API call with requestId verification + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body.requestId).to.equal(REQUEST_ID); + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + + it("should provision with custom location", async () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + features: { location: LOCATION }, + }; + + // Mock API call with location verification + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body.location).to.equal(LOCATION); + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + + it("should provision with AI features enabled", async () => { + const aiFeatures = { enableAiLogic: true, model: "gemini-pro" }; + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + features: { firebaseAiLogicInput: aiFeatures }, + }; + + // Mock API call with AI features verification + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body.firebaseAiLogicInput).to.deep.equal(aiFeatures); + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + + it("should provision with all optional iOS fields", async () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { + platform: AppPlatform.IOS, + bundleId: BUNDLE_ID, + appStoreId: "12345", + teamId: "TEAM123", + }, + }; + + // Mock API call with iOS fields verification + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body.appleInput).to.deep.equal({ + appStoreId: "12345", + teamId: "TEAM123", + }); + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + + it("should provision with all optional Android fields", async () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { + platform: AppPlatform.ANDROID, + packageName: PACKAGE_NAME, + sha1Hashes: ["sha1hash1", "sha1hash2"], + sha256Hashes: ["sha256hash1"], + }, + }; + + // Mock API call with Android fields verification + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body.androidInput).to.deep.equal({ + sha1Hashes: ["sha1hash1", "sha1hash2"], + sha256Hashes: ["sha256hash1"], + }); + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + }); + + // Phase 5: Request ID Behavior Tests + describe("provisionFirebaseApp - Request ID Behavior", () => { + const mockResponse: ProvisionFirebaseAppResponse = { + configMimeType: CONFIG_MIME_TYPE, + configData: CONFIG_DATA, + appResource: APP_RESOURCE, + }; + + const baseOptions: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + }; + + it("should work without requestId (undefined)", async () => { + const options = { ...baseOptions }; + // Explicitly ensure requestId is undefined + expect(options.requestId).to.be.undefined; + + // Mock API call and verify requestId is NOT included in request + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body).to.not.have.property("requestId"); + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + + it("should work with empty requestId", async () => { + const options: ProvisionFirebaseAppOptions = { + ...baseOptions, + requestId: "", + }; + + // Mock API call and verify empty requestId is NOT included in request + // (empty string is falsy, so it gets filtered out by the implementation) + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body).to.not.have.property("requestId"); + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + + it("should work with valid requestId", async () => { + const options: ProvisionFirebaseAppOptions = { + ...baseOptions, + requestId: REQUEST_ID, + }; + + // Mock API call and verify requestId is included in request + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body.requestId).to.equal(REQUEST_ID); + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + + it("should pass requestId to API when provided", async () => { + const customRequestId = "custom-request-12345"; + const options: ProvisionFirebaseAppOptions = { + ...baseOptions, + requestId: customRequestId, + }; + + let actualRequestBody: any; + + // Mock API call and capture request body + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .reply(200, (uri, requestBody) => { + actualRequestBody = requestBody; + return { name: OPERATION_RESOURCE_NAME }; + }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + // Verify requestId was passed correctly + expect(actualRequestBody.requestId).to.equal(customRequestId); + expect(result).to.deep.equal(mockResponse); + }); + + it("should not include requestId in request when omitted", async () => { + const options = { ...baseOptions }; + // Ensure no requestId property exists + delete (options as any).requestId; + + let actualRequestBody: any; + + // Mock API call and capture request body + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .reply(200, (uri, requestBody) => { + actualRequestBody = requestBody; + return { name: OPERATION_RESOURCE_NAME }; + }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + // Verify requestId property does not exist in request + expect(actualRequestBody).to.not.have.property("requestId"); + expect(result).to.deep.equal(mockResponse); + }); + }); + + // Phase 6: Error Handling Tests + describe("provisionFirebaseApp - Error Cases", () => { + const baseOptions: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + }; + + it("should reject if API call fails with 404", async () => { + // Mock API call failure + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .reply(404, { error: { message: "Project not found" } }); + + // Ensure polling is never called for API failures + pollOperationStub.onFirstCall().throws("Polling should not be called on API failure"); + + try { + await provisionFirebaseApp(baseOptions); + expect.fail("Expected function to throw"); + } catch (error: any) { + expect(error).to.be.instanceOf(FirebaseError); + expect(error.message).to.include("Failed to provision Firebase app"); + expect(pollOperationStub.notCalled).to.be.true; + } + }); + + it("should reject if API call fails with 403", async () => { + // Mock API call failure + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .reply(403, { error: { message: "Permission denied" } }); + + // Ensure polling is never called for API failures + pollOperationStub.onFirstCall().throws("Polling should not be called on API failure"); + + try { + await provisionFirebaseApp(baseOptions); + expect.fail("Expected function to throw"); + } catch (error: any) { + expect(error).to.be.instanceOf(FirebaseError); + expect(error.message).to.include("Failed to provision Firebase app"); + expect(pollOperationStub.notCalled).to.be.true; + } + }); + + it("should reject if API call fails with 500", async () => { + // Mock API call failure + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .reply(500, { error: { message: "Internal server error" } }); + + // Ensure polling is never called for API failures + pollOperationStub.onFirstCall().throws("Polling should not be called on API failure"); + + try { + await provisionFirebaseApp(baseOptions); + expect.fail("Expected function to throw"); + } catch (error: any) { + expect(error).to.be.instanceOf(FirebaseError); + expect(error.message).to.include("Failed to provision Firebase app"); + expect(pollOperationStub.notCalled).to.be.true; + } + }); + + it("should reject if polling operation fails", async () => { + // Mock successful API call + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling failure + const pollingError = new Error("Polling operation failed"); + pollOperationStub.onFirstCall().rejects(pollingError); + + try { + await provisionFirebaseApp(baseOptions); + expect.fail("Expected function to throw"); + } catch (error: any) { + expect(error).to.be.instanceOf(FirebaseError); + expect(error.message).to.include("Failed to provision Firebase app"); + expect(error.message).to.include("Polling operation failed"); + expect(pollOperationStub.calledOnce).to.be.true; + } + }); + + it("should reject if polling operation times out", async () => { + // Mock successful API call + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling timeout + const timeoutError = new Error("Operation timed out"); + timeoutError.name = "TIMEOUT"; + pollOperationStub.onFirstCall().rejects(timeoutError); + + try { + await provisionFirebaseApp(baseOptions); + expect.fail("Expected function to throw"); + } catch (error: any) { + expect(error).to.be.instanceOf(FirebaseError); + expect(error.message).to.include("Failed to provision Firebase app"); + expect(error.message).to.include("Operation timed out"); + expect(pollOperationStub.calledOnce).to.be.true; + } + }); + + it("should wrap unknown errors properly", async () => { + // Mock API call that throws unexpected error + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .replyWithError("Unexpected network error"); + + try { + await provisionFirebaseApp(baseOptions); + expect.fail("Expected function to throw"); + } catch (error: any) { + expect(error).to.be.instanceOf(FirebaseError); + expect(error.message).to.include("Failed to provision Firebase app"); + expect(error.message).to.include("Failed to make request"); + expect(pollOperationStub.notCalled).to.be.true; + } + }); + + it("should preserve original error information", async () => { + const originalError = new Error("Original error message"); + originalError.name = "CustomError"; + (originalError as any).code = "CUSTOM_CODE"; + + // Mock API call failure + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling failure with custom error + pollOperationStub.onFirstCall().rejects(originalError); + + try { + await provisionFirebaseApp(baseOptions); + expect.fail("Expected function to throw"); + } catch (error: any) { + expect(error).to.be.instanceOf(FirebaseError); + expect(error.message).to.include("Failed to provision Firebase app"); + expect(error.message).to.include("Original error message"); + + // Verify original error is preserved + const firebaseError = error as FirebaseError; + expect(firebaseError.original).to.equal(originalError); + expect(firebaseError.exit).to.equal(2); + } + }); + }); + + // Phase 7: Platform Validation Tests + describe("Platform-Specific Validation", () => { + const mockResponse: ProvisionFirebaseAppResponse = { + configMimeType: CONFIG_MIME_TYPE, + configData: CONFIG_DATA, + appResource: APP_RESOURCE, + }; + + it("should require bundleId for iOS apps", async () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.IOS, bundleId: BUNDLE_ID }, + }; + + // Mock API call to verify bundleId is included as appNamespace + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body.appNamespace).to.equal(BUNDLE_ID); + expect(body.appleInput).to.exist; + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + + it("should require packageName for Android apps", async () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.ANDROID, packageName: PACKAGE_NAME }, + }; + + // Mock API call to verify packageName is included as appNamespace + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body.appNamespace).to.equal(PACKAGE_NAME); + expect(body.androidInput).to.exist; + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + + it("should require webAppId for Web apps", async () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + }; + + // Mock API call to verify webAppId is included as appNamespace + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body.appNamespace).to.equal(WEB_APP_ID); + expect(body.webInput).to.exist; + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + + it("should accept optional appStoreId for iOS", async () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { + platform: AppPlatform.IOS, + bundleId: BUNDLE_ID, + appStoreId: "123456789", + }, + }; + + // Mock API call to verify appStoreId is included + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body.appleInput.appStoreId).to.equal("123456789"); + expect(body.appleInput).to.not.have.property("teamId"); + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + + it("should accept optional teamId for iOS", async () => { + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { + platform: AppPlatform.IOS, + bundleId: BUNDLE_ID, + teamId: "TEAM123", + }, + }; + + // Mock API call to verify teamId is included + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body.appleInput.teamId).to.equal("TEAM123"); + expect(body.appleInput).to.not.have.property("appStoreId"); + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + + it("should accept optional SHA hashes for Android", async () => { + const sha1Hashes = ["sha1hash1", "sha1hash2"]; + const sha256Hashes = ["sha256hash1"]; + + const options: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { + platform: AppPlatform.ANDROID, + packageName: PACKAGE_NAME, + sha1Hashes, + sha256Hashes, + }, + }; + + // Mock API call to verify SHA hashes are included + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp", (body) => { + expect(body.androidInput).to.deep.equal({ + sha1Hashes, + sha256Hashes, + }); + return true; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(options); + + expect(result).to.deep.equal(mockResponse); + }); + }); + + // Phase 8: API Integration Tests + describe("API Integration", () => { + const mockResponse: ProvisionFirebaseAppResponse = { + configMimeType: CONFIG_MIME_TYPE, + configData: CONFIG_DATA, + appResource: APP_RESOURCE, + }; + + const baseOptions: ProvisionFirebaseAppOptions = { + project: { displayName: PROJECT_DISPLAY_NAME }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, + }; + + it("should call correct API endpoint", async () => { + let actualApiEndpoint: string | undefined; + + // Capture the API endpoint that was called + nock(firebaseApiOrigin()) + .post((uri) => { + actualApiEndpoint = uri; + return uri === "/v1alpha/firebase:provisionFirebaseApp"; + }) + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(baseOptions); + + expect(actualApiEndpoint).to.equal("/v1alpha/firebase:provisionFirebaseApp"); + expect(result).to.deep.equal(mockResponse); + }); + + it("should use correct API version (v1alpha)", async () => { + // Mock the exact v1alpha endpoint + const scope = nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(baseOptions); + + // Verify the exact endpoint was called + expect(scope.isDone()).to.be.true; + expect(result).to.deep.equal(mockResponse); + }); + + it("should poll with correct API version (v1beta1)", async () => { + // Mock initial API call + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling - verify the polling call parameters + pollOperationStub.onFirstCall().callsFake(async (options) => { + expect(options.apiOrigin).to.equal(firebaseApiOrigin()); + expect(options.apiVersion).to.equal("v1beta1"); + expect(options.operationResourceName).to.equal(OPERATION_RESOURCE_NAME); + expect(options.pollerName).to.equal("Provision Firebase App Poller"); + return mockResponse; + }); + + const result = await provisionFirebaseApp(baseOptions); + + expect(result).to.deep.equal(mockResponse); + expect(pollOperationStub.calledOnce).to.be.true; + }); + + it("should pass correct request body structure", async () => { + let actualRequestBody: any; + + // Capture and verify request body structure + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .reply(200, (uri, requestBody) => { + actualRequestBody = requestBody; + return { name: OPERATION_RESOURCE_NAME }; + }); + + // Mock polling + pollOperationStub.onFirstCall().resolves(mockResponse); + + const result = await provisionFirebaseApp(baseOptions); + + // Verify request body has correct structure + expect(actualRequestBody).to.have.property("appNamespace", WEB_APP_ID); + expect(actualRequestBody).to.have.property("displayName", PROJECT_DISPLAY_NAME); + expect(actualRequestBody).to.have.property("webInput"); + expect(actualRequestBody.webInput).to.deep.equal({}); + expect(result).to.deep.equal(mockResponse); + }); + + it("should handle LRO polling correctly", async () => { + // Mock initial API call returning operation + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling with detailed verification + pollOperationStub.onFirstCall().callsFake(async (options) => { + // Verify all polling parameters are correct + expect(options).to.deep.include({ + pollerName: "Provision Firebase App Poller", + apiOrigin: firebaseApiOrigin(), + apiVersion: "v1beta1", + operationResourceName: OPERATION_RESOURCE_NAME, + }); + + // Simulate successful polling result + return mockResponse; + }); + + const result = await provisionFirebaseApp(baseOptions); + + expect(result).to.deep.equal(mockResponse); + expect(pollOperationStub.calledOnce).to.be.true; + }); + + it("should return correct response type", async () => { + // Mock API call + nock(firebaseApiOrigin()) + .post("/v1alpha/firebase:provisionFirebaseApp") + .reply(200, { name: OPERATION_RESOURCE_NAME }); + + // Mock polling with specific response structure + const expectedResponse: ProvisionFirebaseAppResponse = { + configMimeType: "application/json", + configData: "eyJ0ZXN0IjoiZGF0YSJ9", // base64 encoded test data + appResource: "projects/test-project-123/apps/web-app-456", + }; + + pollOperationStub.onFirstCall().resolves(expectedResponse); + + const result = await provisionFirebaseApp(baseOptions); + + // Verify the response structure and types + expect(result).to.have.property("configMimeType"); + expect(result).to.have.property("configData"); + expect(result).to.have.property("appResource"); + expect(typeof result.configMimeType).to.equal("string"); + expect(typeof result.configData).to.equal("string"); + expect(typeof result.appResource).to.equal("string"); + expect(result).to.deep.equal(expectedResponse); + }); + }); +}); diff --git a/src/management/provisioning/provision.ts b/src/management/provisioning/provision.ts new file mode 100644 index 00000000000..463aebe294a --- /dev/null +++ b/src/management/provisioning/provision.ts @@ -0,0 +1,142 @@ +import { Client } from "../../apiv2"; +import { firebaseApiOrigin } from "../../api"; +import { FirebaseError } from "../../error"; +import { logger } from "../../logger"; +import { pollOperation } from "../../operation-poller"; +import { AppPlatform } from "../apps"; +import * as types from "./types"; + +const apiClient = new Client({ + urlPrefix: firebaseApiOrigin(), + apiVersion: "v1alpha", +}); + +/** + * Builds the appropriate app namespace string based on the platform type. + */ +export function buildAppNamespace(app: types.ProvisionAppOptions): string { + let namespace; + if (app.appId) { + return app.appId; + } + + switch (app.platform) { + case AppPlatform.IOS: + namespace = app.bundleId || ""; + break; + case AppPlatform.ANDROID: + namespace = app.packageName || ""; + break; + case AppPlatform.WEB: + namespace = app.webAppId || ""; + break; + default: + throw new FirebaseError("Unsupported platform", { exit: 2 }); + } + + if (!namespace) { + throw new FirebaseError("App namespace cannot be empty", { exit: 2 }); + } + + return namespace; +} + +/** + * Builds the parent resource string for Firebase project provisioning. + */ +export function buildParentString(parent: types.ProjectParentInput): string { + switch (parent.type) { + case "existing_project": + return `projects/${parent.projectId}`; + case "organization": + return `organizations/${parent.organizationId}`; + case "folder": + return `folders/${parent.folderId}`; + default: + throw new FirebaseError("Unsupported parent type", { exit: 2 }); + } +} + +/** + * Builds the complete provision request object from the provided options. + */ +export function buildProvisionRequest( + options: types.ProvisionFirebaseAppOptions, +): types.ProvisionRequest { + const platformInput = (() => { + switch (options.app.platform) { + case AppPlatform.IOS: + return { + appleInput: { + appStoreId: options.app.appStoreId, + teamId: options.app.teamId, + }, + }; + case AppPlatform.ANDROID: + return { + androidInput: { + sha1Hashes: options.app.sha1Hashes, + sha256Hashes: options.app.sha256Hashes, + }, + }; + case AppPlatform.WEB: + return { webInput: {} }; + } + })(); + + return { + appNamespace: buildAppNamespace(options.app), + displayName: options.app.displayName, + ...(options.project.parent && { parent: buildParentString(options.project.parent) }), + ...(options.features?.location && { location: options.features.location }), + ...(options.requestId && { requestId: options.requestId }), + ...(options.features?.firebaseAiLogicInput && { + firebaseAiLogicInput: options.features.firebaseAiLogicInput, + }), + ...platformInput, + }; +} + +/** + * Provisions a new Firebase App and associated resources using the provisionFirebaseApp API. + * @param options The provision options including project, app, and feature configurations + * @return Promise resolving to the provisioned Firebase app response containing config data and app resource name + */ +export async function provisionFirebaseApp( + options: types.ProvisionFirebaseAppOptions, +): Promise { + try { + const request = buildProvisionRequest(options); + + logger.debug("[provision] Starting Firebase app provisioning..."); + logger.debug(`[provision] Request: ${JSON.stringify(request, null, 2)}`); + + const response = await apiClient.request({ + method: "POST", + path: "/firebase:provisionFirebaseApp", + body: request, + }); + + logger.debug(`[provision] Operation started: ${response.body.name}`); + logger.debug("[provision] Polling for operation completion..."); + + const result = await pollOperation({ + pollerName: "Provision Firebase App Poller", + apiOrigin: firebaseApiOrigin(), + apiVersion: "v1beta1", + operationResourceName: response.body.name, + masterTimeout: 180000, // 3 minutes + backoff: 100, // Initial backoff of 100ms + maxBackoff: 5000, // Max backoff of 5s + }); + + logger.debug("[provision] Firebase app provisioning completed successfully"); + return result; + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : String(err); + throw new FirebaseError(`Failed to provision Firebase app: ${errorMessage}`, { + exit: 2, + original: err instanceof Error ? err : new Error(String(err)), + }); + } +} diff --git a/src/management/provisioning/types.ts b/src/management/provisioning/types.ts new file mode 100644 index 00000000000..4ea2ce62659 --- /dev/null +++ b/src/management/provisioning/types.ts @@ -0,0 +1,89 @@ +import { AppPlatform } from "../apps"; + +interface BaseProvisionAppOptions { + platform: AppPlatform; + appId?: string; + displayName?: string; +} + +interface IosAppOptions extends BaseProvisionAppOptions { + platform: AppPlatform.IOS; + bundleId?: string; + appStoreId?: string; + teamId?: string; +} + +interface AndroidAppOptions extends BaseProvisionAppOptions { + platform: AppPlatform.ANDROID; + packageName?: string; + sha1Hashes?: string[]; + sha256Hashes?: string[]; +} + +interface WebAppOptions extends BaseProvisionAppOptions { + platform: AppPlatform.WEB; + webAppId?: string; +} + +export type ProvisionAppOptions = IosAppOptions | AndroidAppOptions | WebAppOptions; + +export interface ProvisionFirebaseAppResponse { + configMimeType: string; + configData: string; + appResource: string; +} + +interface ExistingProjectInput { + type: "existing_project"; + projectId: string; +} + +interface OrganizationInput { + type: "organization"; + organizationId: string; +} + +interface FolderInput { + type: "folder"; + folderId: string; +} + +export type ProjectParentInput = ExistingProjectInput | OrganizationInput | FolderInput; + +export interface ProvisionProjectOptions { + displayName?: string; + parent?: ProjectParentInput; + // TODO(caot): Support specifying projectLabels and billing. + // projectLabels?: Record; + // cloudBillingAccountId?: string; +} + +export interface ProvisionFeatureOptions { + location?: string; + firebaseAiLogicInput?: Record; +} + +export interface ProvisionFirebaseAppOptions { + project: ProvisionProjectOptions; + app: ProvisionAppOptions; + features?: ProvisionFeatureOptions; + requestId?: string; +} + +export interface ProvisionRequest { + parent?: string; + displayName?: string; + appNamespace: string; + location?: string; + requestId?: string; + appleInput?: { + appStoreId?: string; + teamId?: string; + }; + androidInput?: { + sha1Hashes?: string[]; + sha256Hashes?: string[]; + }; + webInput?: {}; + firebaseAiLogicInput?: {}; +} diff --git a/src/management/studio.spec.ts b/src/management/studio.spec.ts new file mode 100644 index 00000000000..f6179847e3e --- /dev/null +++ b/src/management/studio.spec.ts @@ -0,0 +1,195 @@ +import * as chai from "chai"; +import * as sinon from "sinon"; +import * as studio from "./studio"; +import * as prompt from "../prompt"; +import { configstore } from "../configstore"; +import { Client } from "../apiv2"; +import * as utils from "../utils"; +import { Options } from "../options"; +import { Config } from "../config"; +import { RC } from "../rc"; +import { logger } from "../logger"; + +const expect = chai.expect; + +describe("Studio Management", () => { + let sandbox: sinon.SinonSandbox; + let promptStub: sinon.SinonStub; + let clientRequestStub: sinon.SinonStub; + let utilsStub: sinon.SinonStub; + + let testOptions: Options; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + promptStub = sandbox.stub(prompt, "select"); + sandbox.stub(configstore, "get"); + sandbox.stub(configstore, "set"); + clientRequestStub = sandbox.stub(Client.prototype, "request"); + utilsStub = sandbox.stub(utils, "makeActiveProject"); + const emptyConfig = new Config("{}", {}); + testOptions = { + cwd: "", + configPath: "", + only: "", + except: "", + filteredTargets: [], + force: false, + json: false, + nonInteractive: false, + interactive: false, + debug: false, + config: emptyConfig, + rc: new RC(), + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("reconcileStudioFirebaseProject", () => { + it("should return active project from config if WORKSPACE_SLUG is not set", async () => { + process.env.WORKSPACE_SLUG = ""; + const result = await studio.reconcileStudioFirebaseProject(testOptions, "cli-project"); + expect(result).to.equal("cli-project"); + expect(clientRequestStub).to.not.have.been.called; + }); + + it("should return active project from config if getStudioWorkspace fails", async () => { + process.env.WORKSPACE_SLUG = "test-workspace"; + clientRequestStub.rejects(new Error("API Error")); + const result = await studio.reconcileStudioFirebaseProject(testOptions, "cli-project"); + expect(result).to.equal("cli-project"); + }); + + it("should update studio with CLI project if studio has no project", async () => { + process.env.WORKSPACE_SLUG = "test-workspace"; + clientRequestStub + .onFirstCall() + .resolves({ body: { name: "test-workspace", firebaseProjectId: undefined } }); + clientRequestStub.onSecondCall().resolves({ body: {} }); + + const result = await studio.reconcileStudioFirebaseProject(testOptions, "cli-project"); + + expect(result).to.equal("cli-project"); + expect(clientRequestStub).to.have.been.calledTwice; + expect(clientRequestStub.secondCall.args[0].body.firebaseProjectId).to.equal("cli-project"); + }); + + it("should update CLI with studio project if CLI has no project", async () => { + process.env.WORKSPACE_SLUG = "test-workspace"; + clientRequestStub.resolves({ + body: { name: "test-workspace", firebaseProjectId: "studio-project" }, + }); + + const result = await studio.reconcileStudioFirebaseProject( + { ...testOptions, projectRoot: "/test" }, + undefined, + ); + + expect(result).to.equal("studio-project"); + expect(utilsStub).to.have.been.calledOnceWith("/test", "studio-project"); + }); + + it("should prompt user and update studio if user chooses CLI project", async () => { + process.env.WORKSPACE_SLUG = "test-workspace"; + clientRequestStub + .onFirstCall() + .resolves({ body: { name: "test-workspace", firebaseProjectId: "studio-project" } }); + clientRequestStub.onSecondCall().resolves({ body: {} }); + promptStub.resolves(true); + + const result = await studio.reconcileStudioFirebaseProject(testOptions, "cli-project"); + + expect(result).to.equal("cli-project"); + expect(promptStub).to.have.been.calledOnce; + expect(clientRequestStub).to.have.been.calledTwice; + expect(clientRequestStub.secondCall.args[0].body.firebaseProjectId).to.equal("cli-project"); + }); + + it("should prompt user and update CLI if user chooses studio project", async () => { + process.env.WORKSPACE_SLUG = "test-workspace"; + clientRequestStub.resolves({ + body: { name: "test-workspace", firebaseProjectId: "studio-project" }, + }); + promptStub.resolves(false); + + const result = await studio.reconcileStudioFirebaseProject( + { ...testOptions, projectRoot: "/test" }, + "cli-project", + ); + + expect(result).to.equal("studio-project"); + expect(promptStub).to.have.been.calledOnce; + expect(utilsStub).to.have.been.calledOnceWith("/test", "studio-project"); + }); + + it("should do nothing if projects are the same", async () => { + process.env.WORKSPACE_SLUG = "test-workspace"; + clientRequestStub.resolves({ + body: { name: "test-workspace", firebaseProjectId: "same-project" }, + }); + + const result = await studio.reconcileStudioFirebaseProject(testOptions, "same-project"); + + expect(result).to.equal("same-project"); + expect(promptStub).to.not.have.been.called; + expect(utilsStub).to.not.have.been.called; + }); + + it("should do nothing if in non-interactive mode", async () => { + process.env.WORKSPACE_SLUG = "test-workspace"; + clientRequestStub.resolves({ + body: { name: "test-workspace", firebaseProjectId: "studio-project" }, + }); + + const result = await studio.reconcileStudioFirebaseProject( + { ...testOptions, nonInteractive: true }, + "cli-project", + ); + + expect(result).to.equal("studio-project"); + expect(promptStub).to.not.have.been.called; + expect(utilsStub).to.not.have.been.called; + }); + }); + + describe("updateStudioFirebaseProject", () => { + it("should not call api if WORKSPACE_SLUG is not set", async () => { + process.env.WORKSPACE_SLUG = ""; + await studio.updateStudioFirebaseProject("new-project"); + expect(clientRequestStub).to.not.have.been.called; + }); + + it("should call api to update project id", async () => { + process.env.WORKSPACE_SLUG = "test-workspace"; + clientRequestStub.resolves({ body: {} }); + + await studio.updateStudioFirebaseProject("new-project"); + + expect(clientRequestStub).to.have.been.calledOnceWith({ + method: "PATCH", + path: `/workspaces/test-workspace`, + responseType: "json", + body: { + firebaseProjectId: "new-project", + }, + queryParams: { + updateMask: "workspace.firebaseProjectId", + }, + timeout: 30000, + }); + }); + + it("should log error if api call fails", async () => { + process.env.WORKSPACE_SLUG = "test-workspace"; + clientRequestStub.rejects(new Error("API Error")); + const errorLogSpy = sandbox.spy(logger, "debug"); + + await studio.updateStudioFirebaseProject("new-project"); + + expect(errorLogSpy).to.have.been.calledOnce; + }); + }); +}); diff --git a/src/management/studio.ts b/src/management/studio.ts new file mode 100644 index 00000000000..8f7e9bdb0cf --- /dev/null +++ b/src/management/studio.ts @@ -0,0 +1,158 @@ +import { Client } from "../apiv2"; +import * as prompt from "../prompt"; +import * as api from "../api"; +import { logger } from "../logger"; +import * as utils from "../utils"; +import { Options } from "../options"; +import { configstore } from "../configstore"; + +const TIMEOUT_MILLIS = 30000; + +const studioClient = new Client({ + urlPrefix: api.studioApiOrigin(), + apiVersion: "v1", +}); + +/** + * Reconciles the active project in your Studio Workspace when running the CLI + * in Firebase Studio. + * @param activeProjectFromConfig The project ID saved in configstore + * @return A promise that resolves with the reconciled active project + */ +export async function reconcileStudioFirebaseProject( + options: Options, + activeProjectFromConfig: string | undefined, +): Promise { + const studioWorkspace = await getStudioWorkspace(); + // Fail gracefully and resolve with the existing configs + if (!studioWorkspace) { + return activeProjectFromConfig; + } + // If Studio has no project, update Studio if the CLI has one + if (!studioWorkspace.firebaseProjectId) { + if (activeProjectFromConfig) { + await updateStudioFirebaseProject(activeProjectFromConfig); + } + return activeProjectFromConfig; + } + // If the CLI has no project, update the CLI with what Studio has + if (!activeProjectFromConfig) { + await writeStudioProjectToConfigStore(options, studioWorkspace.firebaseProjectId); + return studioWorkspace.firebaseProjectId; + } + // If both have an active project, allow the user to choose + if (studioWorkspace.firebaseProjectId !== activeProjectFromConfig && !options.nonInteractive) { + const choices = [ + { + name: `Set ${studioWorkspace.firebaseProjectId} from Firebase Studio as my active project in both places`, + value: false as any, + }, + { + name: `Set ${activeProjectFromConfig} from Firebase CLI as my active project in both places`, + value: true as any, + }, + ]; + const useCliProject = await prompt.select({ + message: + "Found different active Firebase Projects in the Firebase CLI and your Firebase Studio Workspace. Which project would you like to set as your active project?", + choices, + }); + if (useCliProject) { + await updateStudioFirebaseProject(activeProjectFromConfig); + return activeProjectFromConfig; + } else { + await writeStudioProjectToConfigStore(options, studioWorkspace.firebaseProjectId); + return studioWorkspace.firebaseProjectId; + } + } + // Otherwise, Studio and the CLI agree + return studioWorkspace.firebaseProjectId; +} + +export interface StudioWorkspace { + name: string; + firebaseProjectId: string | undefined; +} + +async function getStudioWorkspace(): Promise { + const workspaceId = process.env.WORKSPACE_SLUG; + if (!workspaceId) { + logger.error( + `Failed to fetch Firebase Project from Studio Workspace because WORKSPACE_SLUG environment variable is empty`, + ); + return undefined; + } + try { + const res = await studioClient.request({ + method: "GET", + path: `/workspaces/${workspaceId}`, + timeout: TIMEOUT_MILLIS, + }); + return res.body; + } catch (err: any) { + let message = err.message; + if (err.original) { + message += ` (original: ${err.original.message})`; + } + logger.error(`Failed to fetch Firebase Project from current Studio Workspace: ${message}`); + // We're going to fail gracefully so that the caller can handle the error + return undefined; + } +} + +async function writeStudioProjectToConfigStore(options: Options, studioProjectId: string) { + if (options.projectRoot) { + logger.info( + `Updating Firebase CLI active project to match Studio Workspace '${studioProjectId}'`, + ); + utils.makeActiveProject(options.projectRoot, studioProjectId); + recordStudioProjectSyncTime(); + } +} + +/** + * Sets the active project for the current Firebase Studio Workspace + * @param projectId The project ID saved in spanner + * @return A promise that resolves when complete + */ +export async function updateStudioFirebaseProject(projectId: string): Promise { + logger.info(`Updating Studio Workspace active project to match Firebase CLI '${projectId}'`); + const workspaceId = process.env.WORKSPACE_SLUG; + if (!workspaceId) { + logger.error( + `Failed to update Firebase Project for Studio Workspace because WORKSPACE_SLUG environment variable is empty`, + ); + return; + } + try { + await studioClient.request({ + method: "PATCH", + path: `/workspaces/${workspaceId}`, + responseType: "json", + body: { + firebaseProjectId: projectId, + }, + queryParams: { + updateMask: "workspace.firebaseProjectId", + }, + timeout: TIMEOUT_MILLIS, + }); + } catch (err: any) { + let message = err.message; + if (err.original) { + message += ` (original: ${err.original.message})`; + } + logger.debug( + `Failed to update active Firebase Project for current Studio Workspace: ${message}`, + ); + } + recordStudioProjectSyncTime(); +} + +/** + * Records the last time we synced the Studio project in Configstore. + * This is important to trigger a file watcher in Firebase Studio that keeps the UI in sync. + */ +function recordStudioProjectSyncTime() { + configstore.set("firebaseStudioProjectLastSynced", Date.now()); +} diff --git a/src/mcp/CONTRIBUTING.md b/src/mcp/CONTRIBUTING.md new file mode 100644 index 00000000000..c8618f93578 --- /dev/null +++ b/src/mcp/CONTRIBUTING.md @@ -0,0 +1,183 @@ +# Firebase MCP Server Contributing Guide + +## Overview + +The Firebase MCP server offers tools that LLMs can use when interacting with Firebase. +The tools let you fetch important context about what is deployed to your project, +access agents that can perform specialized tasks, or make minor modifications to your project. + +## Audience + +If you are a developer interested in contributing to the MCP server, this is the +documentation for you! This guide describes how to be successful in contributing +to our repository. + +## Getting Started + +The Firebase MCP server lives alongside the Firebase CLI in the [firebase/firebase-tools][gh-repo] repo. +It lives here so that it can share code for authentication, API calls, and utilities with the CLI. + +External developers: If you're interested in contributing code, get started by +[making a fork of the repository for your GitHub account](https://help.github.com/en/github/getting-started-with-github/fork-a-repo). + +Internal developers: Go to go/firebase-github-request and ask for access to this repo. You should work +off of sepearate branches in this repo, to ensure that all CI runs correctly for you. + +### Contribution Process + +External developers: The preferred means of contribution to the MCP server is by creating a branch in your +own fork, pushing your changes there, and submitting a Pull Request to the +`master` branch of `firebase/firebase-tools`. + +Internal developers: Instead of working off of a fork, please make a branch on [firebase/firebase-tools][gh-repo] +named - (for example, `jh-dataconnect-tools`) + +If your change is visible to users, it should be in the +[changelog](https://github.com/firebase/firebase-tools/releases). Please +add an entry to the `CHANGELOG.md` file. This log is emptied after every release +and is used to generate the release notes posted in the +[Releases](https://github.com/firebase/firebase-tools/releases) page. Markdown +formatting is respected (using the GitHub style). + +NOTE: Any new files added to the repository **must** be written in TypeScript +and **must** include unit tests. There are very few exceptions to this rule. + +After your Pull Request passes the tests and is approved by a Firebase CLI team +member, they will merge your PR. Thank you for your contribution! + +### Setting up your development environment + +Please follow the instructions in the [CLI's CONTRIBUTING.md](https://github.com/firebase/firebase-tools/blob/master/CONTRIBUTING.md#setting-up-your-development-environment) to get your development environment set up. + +There are a few extra things to set up when developing the MCP server. + +### Testing with the MCP Inspector + +During early development, you will want to test that your tools outputs what you expect, without burning tokens. +The easiest way to do this is the [MCP inspector](https://github.com/modelcontextprotocol/inspector). From a +Firebase project directory, run: + +``` +npx -y @modelcontextprotocol/inspector +``` + +This will print out a localhost link to a simple testing UI. There, you can configure the MCP server +and manually list and execute tools. + +``` +Transport Type: STDIO +Command: firebase +Arguments: mcp + +``` + +## Building MCP tools + +IMPORTANT: LLMs cannot handle large number of tools. Please consider whether the functionality +you want to add can be added to an existing tool. + +### Setting up a new tool + +#### Create a file for your tool + +First, create a new file in `src/mcp/tools/`. +If your product does not have tools yet, create a new directory under `src/mcp/tools`/. +If the tool is relevant for many Firebase products, put it under `core`. + +Tool files should be named `/`. The tool will then be listed as `_`. + +```typescript +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; + +export const foo_bar = tool( + { + name: "foo_bar", + description: "Foos a bar. This description informs LLMs when to use this tool", + inputSchema: z.object({ + foo: z + .string() + .describe("The foo to bar. Parameter descriptions inform LLMs how to use this param."), + }), + annotations: { + title: "Foo Bar", + readOnlyHint: true, // True if this tool makes no changes to your local files or Firebase project. + idempotentHint: false, // True if this tool can safely be run multiple times without redundant effects. + destructiveHint: false, // True if this tool deletes files or data. + openWorldHint: false, // Does this tool interact with open (ie the web) or closed systems (ie a Firestore DB) + }, + _meta: { + requiresProject: true, // Does this tool require you to be in a Firebase project directory? + requiresAuth: true, // Does this tool require you to be authenticated (usually via `firebase login`) + requiresGemini: true, // Does this tool use Gemini in Firebase in any way? + }, + }, + async ( + { foo }, // Anything passed in inputSchema is avialable here. + { projectId, accountEmail, config }, // See ServerToolContext for a complete list of available fields + ) => { + // Business logic for the tool + let foo; + try { + const foo = await barFood(prompt, projectId); + } catch (e: any) { + // return mcpError to handle error cases + return mcpError("Foo could not be barred"); + } + // Use toContent to return successes in a MCP friendly format. + return toContent(schema); + }, +); +``` + +Here are a few style notes: + +- Tool names + - should not include product name + - should be all lower-case letters + - should be snake case +- Descriptions + - should be aimed at informing LLMs, not humans + +#### Load the command + +Next, go to `src/mcp/tools//index.ts`, and add your tool: + +```typescript +import { foo_bar } from "./foo_bar" + +export const Tools = [ + foo_bar, +]; + +``` + +If this is the first tool for this product, also go to `src/mcp/tools/index.ts` and add your product: + +```typescript +import { Tools } from ".//index" + +const tools: Record = { + // Exisitng tools here... + : addFeaturePrefix("", Tools), +} + +``` + +### Update the README.md tool list + +Run the following command to add your new tool to the list in `src/mcp/README.md` + +``` +node lib/bin/firebase.js mcp --generate-tool-list +``` + +### Logging and terminal formatting + +The Firebase CLI has a central logger available in `src/logger`. You should +never use `console.log()` in an MCP tool - STDOUT must only take structured MCP output. + +Any logs for your tool will be written to `firebase-debug.log` + +[gh-repo]: https://github.com/firebase/firebase-tools diff --git a/src/mcp/README.md b/src/mcp/README.md new file mode 100644 index 00000000000..4eec05639e2 --- /dev/null +++ b/src/mcp/README.md @@ -0,0 +1,207 @@ +# Firebase MCP Server + +[![License](https://img.shields.io/badge/License-MIT-blue.svg)](../../LICENSE) +[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=firebase&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImZpcmViYXNlLXRvb2xzQGxhdGVzdCIsIm1jcCJdfQ==) + +The Firebase Model Context Protocol (MCP) Server gives AI-powered development tools the ability to work with your Firebase projects and your app's codebase. The Firebase MCP server works with any tool that can act as an MCP client, including: [Firebase Studio](https://firebase.google.com/studio), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Claude Code](https://www.claude.com/product/claude-code), [Cline](https://github.com/cline/cline), [Cursor](https://www.cursor.com/), VS Code Copilot, [Windsurf](https://codeium.com/windsurf), and more! + +## Features + +An editor configured to use the Firebase MCP server can use its AI capabilities to help you: + +- **Create and manage Firebase projects** - Initialize new projects, list existing ones, and manage Firebase apps +- **Manage Firebase Authentication users** - Retrieve, update, and manage user accounts +- **Work with Cloud Firestore and Firebase Data Connect** - Query, read, write, and manage database documents +- **Retrieve Firebase Data Connect schemas** - Generate schemas and operations with AI assistance +- **Understand security rules** - Validate and retrieve security rules for Firestore, Cloud Storage, and Realtime Database +- **Send messages with Firebase Cloud Messaging** - Send push notifications to devices and topics +- **Access Crashlytics data** - Debug issues, view crash reports, and manage crash analytics +- **Deploy to App Hosting** - Monitor backends and retrieve logs +- **Work with Realtime Database** - Read and write data in real-time +- **Query Cloud Functions logs** - Retrieve and analyze function execution logs +- **Manage Remote Config** - Get and update remote configuration templates + +Some tools use [Gemini in Firebase](https://firebase.google.com/docs/ai-assistance) to help you: + +- Generate Firebase Data Connect schema and operations +- Consult Gemini about Firebase products + +> **Important:** Gemini in Firebase can generate output that seems plausible but is factually incorrect. It may respond with inaccurate information that doesn't represent Google's views. Validate all output from Gemini before you use it and don't use untested generated code in production. Don't enter personally-identifiable information (PII) or user data into the chat. +> Learn more about [Gemini in Firebase and how it uses your data](https://firebase.google.com/docs/ai-assistance). + +## Installation and Setup + +### Prerequisites + +Make sure you have a working installation of [Node.js](http://nodejs.org/) and [npm](https://npmjs.org/). + +### Basic Configuration + +The Firebase MCP server can work with any MCP client that supports standard I/O (stdio) as the transport medium. When the Firebase MCP server makes tool calls, it uses the same user credentials that authorize the Firebase CLI in the environment where it's running. + +Here are configuration instructions for popular AI-assistive tools: + +#### Gemini CLI + +Install the [Firebase extension for Gemini CLI](https://github.com/gemini-cli-extensions/firebase/): + +```bash +gemini extensions install https://github.com/gemini-cli-extensions/firebase/ +``` + +#### Claude Code + +To configure Claude Code to use the Firebase MCP server, run the following command under your app folder: + +```bash +claude mcp add firebase npx -- -y firebase-tools@latest mcp +``` + +You can verify the installation by running: + +```bash +claude mcp list +``` + +It should show: + +``` +firebase: npx -y firebase-tools@latest mcp - ✓ Connected +``` + +#### Cursor + +Add to `.cursorrules` in your project directory or configure in Cursor settings: + +```json +{ + "mcpServers": { + "firebase": { + "command": "npx", + "args": ["-y", "firebase-tools@latest", "mcp"] + } + } +} +``` + +#### Windsurf + +Add to `~/.codeium/windsurf/mcp_config.json`: + +```json +{ + "mcpServers": { + "firebase": { + "command": "npx", + "args": ["-y", "firebase-tools@latest", "mcp"] + } + } +} +``` + +#### Firebase Studio + +To configure Firebase Studio to use the Firebase MCP server, edit or create the configuration file: `.idx/mcp.json` + +```json +{ + "mcpServers": { + "firebase": { + "command": "npx", + "args": ["-y", "firebase-tools@latest", "mcp"] + } + } +} +``` + +## Usage + +Once configured, the MCP server will automatically provide Firebase capabilities to your AI assistant. You can: + +- Ask the AI to help set up Firebase services +- Query your Firestore database +- Manage authentication users +- Deploy to Firebase Hosting +- Debug Crashlytics issues +- And much more! + +For a complete list of available tools and resources, see the [Server Capabilities](#server-capabilities) section below. + +## Documentation + +For more information, visit the [official Firebase MCP server documentation](https://firebase.google.com/docs/ai-assistance/mcp-server). + +## Server Capabilities + +The Firebase MCP server provides three types of capabilities: **Tools** (functions that perform actions), **Prompts** (reusable command templates), and **Resources** (documentation files for AI models). + +| Tool Name | Feature Group | Description | +| ------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| firebase_login | core | Use this to sign the user into the Firebase CLI and Firebase MCP server. This requires a Google Account, and sign in is required to create and work with Firebase Projects. | +| firebase_logout | core | Use this to sign the user out of the Firebase CLI and Firebase MCP server. | +| firebase_validate_security_rules | core | Use this to check Firebase Security Rules for Firestore, Storage, or Realtime Database for syntax and validation errors. | +| firebase_get_project | core | Use this to retrieve information about the currently active Firebase Project. | +| firebase_list_apps | core | Use this to retrieve a list of the Firebase Apps registered in the currently active Firebase project. Firebase Apps can be iOS, Android, or Web. | +| firebase_list_projects | core | Use this to retrieve a list of Firebase Projects that the signed-in user has access to. | +| firebase_get_sdk_config | core | Use this to retrieve the Firebase configuration information for a Firebase App. You must specify EITHER a platform OR the Firebase App ID for a Firebase App registered in the currently active Firebase Project. | +| firebase_create_project | core | Use this to create a new Firebase Project. | +| firebase_create_app | core | Use this to create a new Firebase App in the currently active Firebase Project. Firebase Apps can be iOS, Android, or Web. | +| firebase_create_android_sha | core | Use this to add the specified SHA certificate hash to the specified Firebase Android App. | +| firebase_get_environment | core | Use this to retrieve the current Firebase **environment** configuration for the Firebase CLI and Firebase MCP server, including current authenticated user, project directory, active Firebase Project, and more. | +| firebase_update_environment | core | Use this to update environment config for the Firebase CLI and Firebase MCP server, such as project directory, active project, active user account, accept terms of service, and more. Use `firebase_get_environment` to see the currently configured environment. | +| firebase_init | core | Use this to initialize selected Firebase services in the workspace (Cloud Firestore database, Firebase Data Connect, Firebase Realtime Database, Firebase AI Logic). All services are optional; specify only the products you want to set up. You can initialize new features into an existing project directory, but re-initializing an existing feature may overwrite configuration. To deploy the initialized features, run the `firebase deploy` command after `firebase_init` tool. | +| firebase_get_security_rules | core | Use this to retrieve the security rules for a specified Firebase service. If there are multiple instances of that service in the product, the rules for the defualt instance are returned. | +| firebase_read_resources | core | Use this to read the contents of `firebase://` resources or list available resources | +| firestore_delete_document | firestore | Use this to delete a Firestore documents from a database in the current project by full document paths. Use this if you know the exact path of a document. | +| firestore_get_documents | firestore | Use this to retrieve one or more Firestore documents from a database in the current project by full document paths. Use this if you know the exact path of a document. | +| firestore_list_collections | firestore | Use this to retrieve a list of collections from a Firestore database in the current project. | +| firestore_query_collection | firestore | Use this to retrieve one or more Firestore documents from a collection is a database in the current project by a collection with a full document path. Use this if you know the exact path of a collection and the filtering clause you would like for the document. | +| auth_get_users | auth | Use this to retrieve one or more Firebase Auth users based on a list of UIDs or a list of emails. | +| auth_update_user | auth | Use this to disable, enable, or set a custom claim on a specific user's account. | +| auth_set_sms_region_policy | auth | Use this to set an SMS region policy for Firebase Authentication to restrict the regions which can receive text messages based on an ALLOW or DENY list of country codes. This policy will override any existing policies when set. | +| dataconnect_build | dataconnect | Use this to compile Firebase Data Connect schema, operations, and/or connectors and check for build errors. | +| dataconnect_generate_schema | dataconnect | Use this to generate a Firebase Data Connect Schema based on the users description of an app. | +| dataconnect_generate_operation | dataconnect | Use this to generate a single Firebase Data Connect query or mutation based on the currently deployed schema and the provided prompt. | +| dataconnect_list_services | dataconnect | Use this to list existing local and backend Firebase Data Connect services | +| dataconnect_execute | dataconnect | Use this to execute a GraphQL operation against a Data Connect service or its emulator. | +| storage_get_object_download_url | storage | Use this to retrieve the download URL for an object in a Cloud Storage for Firebase bucket. | +| messaging_send_message | messaging | Use this to send a message to a Firebase Cloud Messaging registration token or topic. ONLY ONE of `registration_token` or `topic` may be supplied in a specific call. | +| functions_get_logs | functions | Use this to retrieve a page of Cloud Functions log entries using Google Cloud Logging advanced filters. | +| remoteconfig_get_template | remoteconfig | Use this to retrieve the specified Firebase Remote Config template from the currently active Firebase Project. | +| remoteconfig_update_template | remoteconfig | Use this to publish a new remote config template or roll back to a specific version for the project | +| crashlytics_create_note | crashlytics | Add a note to an issue from crashlytics. | +| crashlytics_delete_note | crashlytics | Delete a note from a Crashlytics issue. | +| crashlytics_get_issue | crashlytics | Gets data for a Crashlytics issue, which can be used as a starting point for debugging. | +| crashlytics_list_events | crashlytics | Use this to list the most recent events matching the given filters.
    Can be used to fetch sample crashes and exceptions for an issue,
    which will include stack traces and other data useful for debugging. | +| crashlytics_batch_get_events | crashlytics | Gets specific events by resource name.
    Can be used to fetch sample crashes and exceptions for an issue,
    which will include stack traces and other data useful for debugging. | +| crashlytics_list_notes | crashlytics | Use this to list all notes for an issue in Crashlytics. | +| crashlytics_get_top_issues | crashlytics | Use this to count events and distinct impacted users, grouped by _issue_.
    Groups are sorted by event count, in descending order.
    Only counts events matching the given filters. | +| crashlytics_get_top_variants | crashlytics | Counts events and distinct impacted users, grouped by issue _variant_.
    Groups are sorted by event count, in descending order.
    Only counts events matching the given filters. | +| crashlytics_get_top_versions | crashlytics | Counts events and distinct impacted users, grouped by _version_.
    Groups are sorted by event count, in descending order.
    Only counts events matching the given filters. | +| crashlytics_get_top_apple_devices | crashlytics | Counts events and distinct impacted users, grouped by apple _device_.
    Groups are sorted by event count, in descending order.
    Only counts events matching the given filters.
    Only relevant for iOS, iPadOS and MacOS applications. | +| crashlytics_get_top_android_devices | crashlytics | Counts events and distinct impacted users, grouped by android _device_.
    Groups are sorted by event count, in descending order.
    Only counts events matching the given filters.
    Only relevant for Android applications. | +| crashlytics_get_top_operating_systems | crashlytics | Counts events and distinct impacted users, grouped by _operating system_.
    Groups are sorted by event count, in descending order.
    Only counts events matching the given filters. | +| crashlytics_update_issue | crashlytics | Use this to update the state of Crashlytics issue. | +| apphosting_fetch_logs | apphosting | Use this to fetch the most recent logs for a specified App Hosting backend. If `buildLogs` is specified, the logs from the build process for the latest build are returned. The most recent logs are listed first. | +| apphosting_list_backends | apphosting | Use this to retrieve a list of App Hosting backends in the current project. An empty list means that there are no backends. The `uri` is the public URL of the backend. A working backend will have a `managed_resources` array that will contain a `run_service` entry. That `run_service.service` is the resource name of the Cloud Run service serving the App Hosting backend. The last segment of that name is the service ID. `domains` is the list of domains that are associated with the backend. They either have type `CUSTOM` or `DEFAULT`. Every backend should have a `DEFAULT` domain. The actual domain that a user would use to conenct to the backend is the last parameter of the domain resource name. If a custom domain is correctly set up, it will have statuses ending in `ACTIVE`. | +| realtimedatabase_get_data | realtimedatabase | Use this to retrieve data from the specified location in a Firebase Realtime Database. | +| realtimedatabase_set_data | realtimedatabase | Use this to write data to the specified location in a Firebase Realtime Database. | + +| Prompt Name | Feature Group | Description | +| ------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| firebase:deploy | core | Use this command to deploy resources to Firebase.

    Arguments:
    <prompt> (optional): any specific instructions you wish to provide about deploying | +| firebase:init | core | Use this command to set up Firebase services, like backend and AI features. | +| firebase:consult | core | Use this command to consult the Firebase Assistant with access to detailed up-to-date documentation for the Firebase platform.

    Arguments:
    <prompt>: a question to pass to the Gemini in Firebase model | +| crashlytics:connect | crashlytics | Access a Firebase application's Crashlytics data. | + +| Resource Name | Description | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| backend_init_guide | Firebase Backend Init Guide: guides the coding agent through configuring Firebase backend services in the current project | +| ai_init_guide | Firebase GenAI Init Guide: guides the coding agent through configuring GenAI capabilities in the current project utilizing Firebase | +| data_connect_init_guide | Firebase Data Connect Init Guide: guides the coding agent through configuring Data Connect for PostgreSQL access in the current project | +| firestore_init_guide | Firestore Init Guide: guides the coding agent through configuring Firestore in the current project | +| firestore_rules_init_guide | Firestore Rules Init Guide: guides the coding agent through setting up Firestore security rules in the project | +| rtdb_init_guide | Firebase Realtime Database Init Guide: guides the coding agent through configuring Realtime Database in the current project | +| auth_init_guide | Firebase Authentication Init Guide: guides the coding agent through configuring Firebase Authentication in the current project | +| hosting_init_guide | Firebase Hosting Deployment Guide: guides the coding agent through deploying to Firebase Hosting in the current project | +| docs | Firebase Docs: loads plain text content from Firebase documentation, e.g. `https://firebase.google.com/docs/functions` becomes `firebase://docs/functions` | diff --git a/src/mcp/errors.ts b/src/mcp/errors.ts new file mode 100644 index 00000000000..36eaf02d58a --- /dev/null +++ b/src/mcp/errors.ts @@ -0,0 +1,43 @@ +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { mcpError } from "./util"; +import { ensureGIFApiTos } from "../dataconnect/ensureApis"; + +export const NO_PROJECT_ERROR = mcpError( + "To proceed requires an active project. Use the `firebase_update_environment` tool to set a project ID", + "PRECONDITION_FAILED", +); + +const GEMINI_TOS_ERROR = mcpError( + "To proceed requires features from Gemini in Firebase. You can enable the usage of this service and accept its associated terms of service using `firebase_update_environment`.\n" + + "Learn more about Gemini in Firebase and how it uses your data: https://firebase.google.com/docs/gemini-in-firebase#how-gemini-in-firebase-uses-your-data", + "PRECONDITION_FAILED", +); + +/** Enable the Gemini in Firebase API or return an error to accept it */ +export async function requireGeminiToS(projectId: string): Promise { + if (!projectId) { + return NO_PROJECT_ERROR; + } + if (!(await ensureGIFApiTos(projectId))) { + return GEMINI_TOS_ERROR; + } + return undefined; +} + +export function noProjectDirectory(projectRoot: string | undefined): CallToolResult { + return mcpError( + `The current project directory '${ + projectRoot || "" + }' does not exist. Please use the 'update_firebase_environment' tool to target a different project directory.`, + ); +} + +export function mcpAuthError(skipADC: boolean): CallToolResult { + if (skipADC) { + return mcpError( + `The user is not currently logged into the Firebase CLI, which is required to use this tool. Please run the 'firebase_login' tool to log in.`, + ); + } + return mcpError(`The user is not currently logged into the Firebase CLI, which is required to use this tool. Please run the 'firebase_login' tool to log in, or instruct the user to configure [Application Default Credentials][ADC] on their machine. +[ADC]: https://cloud.google.com/docs/authentication/application-default-credentials`); +} diff --git a/src/mcp/index.spec.ts b/src/mcp/index.spec.ts new file mode 100644 index 00000000000..f219f643ec6 --- /dev/null +++ b/src/mcp/index.spec.ts @@ -0,0 +1,63 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { FirebaseMcpServer } from "./index"; +import * as requireAuthModule from "../requireAuth"; + +describe("FirebaseMcpServer.getAuthenticatedUser", () => { + let server: FirebaseMcpServer; + let requireAuthStub: sinon.SinonStub; + + beforeEach(() => { + // Mock the methods that may cause hanging BEFORE creating the instance + sinon.stub(FirebaseMcpServer.prototype, "detectProjectRoot").resolves("/test/project"); + sinon.stub(FirebaseMcpServer.prototype, "detectActiveFeatures").resolves([]); + + server = new FirebaseMcpServer({}); + + // Mock the resolveOptions method to avoid dependency issues + sinon.stub(server, "resolveOptions").resolves({}); + + requireAuthStub = sinon.stub(requireAuthModule, "requireAuth"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return email when authenticated user is present", async () => { + const testEmail = "test@example.com"; + requireAuthStub.resolves(testEmail); + + const result = await server.getAuthenticatedUser(); + + expect(result).to.equal(testEmail); + expect(requireAuthStub.calledOnce).to.be.true; + }); + + it("should return null when no user and skipAutoAuth is true", async () => { + requireAuthStub.resolves(null); + + const result = await server.getAuthenticatedUser(true); + + expect(result).to.be.null; + expect(requireAuthStub.calledOnce).to.be.true; + }); + + it("should return 'Application Default Credentials' when no user and skipAutoAuth is false", async () => { + requireAuthStub.resolves(null); + + const result = await server.getAuthenticatedUser(false); + + expect(result).to.equal("Application Default Credentials"); + expect(requireAuthStub.calledOnce).to.be.true; + }); + + it("should return null when requireAuth throws an error", async () => { + requireAuthStub.rejects(new Error("Auth failed")); + + const result = await server.getAuthenticatedUser(); + + expect(result).to.be.null; + expect(requireAuthStub.calledOnce).to.be.true; + }); +}); diff --git a/src/mcp/index.ts b/src/mcp/index.ts new file mode 100644 index 00000000000..c1298fbaa46 --- /dev/null +++ b/src/mcp/index.ts @@ -0,0 +1,495 @@ +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequest, + CallToolRequestSchema, + ListToolsResult, + LoggingLevel, + SetLevelRequestSchema, + ListToolsRequestSchema, + CallToolResult, + ListPromptsRequestSchema, + GetPromptRequestSchema, + ListPromptsResult, + GetPromptResult, + GetPromptRequest, + ListResourcesRequestSchema, + ListResourcesResult, + ReadResourceRequest, + ReadResourceResult, + ReadResourceRequestSchema, + ListResourceTemplatesRequestSchema, + ListResourceTemplatesResult, + McpError, + ErrorCode, +} from "@modelcontextprotocol/sdk/types.js"; +import { checkFeatureActive, mcpError } from "./util"; +import { ClientConfig, McpContext, SERVER_FEATURES, ServerFeature } from "./types"; +import { availableTools } from "./tools/index"; +import { ServerTool } from "./tool"; +import { availablePrompts } from "./prompts/index"; +import { ServerPrompt } from "./prompt"; +import { configstore } from "../configstore"; +import { Command } from "../command"; +import { requireAuth } from "../requireAuth"; +import { Options } from "../options"; +import { getProjectId } from "../projectUtils"; +import { mcpAuthError, noProjectDirectory, NO_PROJECT_ERROR, requireGeminiToS } from "./errors"; +import { trackGA4 } from "../track"; +import { Config } from "../config"; +import { loadRC } from "../rc"; +import { EmulatorHubClient } from "../emulator/hubClient"; +import { Emulators } from "../emulator/types"; +import { existsSync } from "node:fs"; +import { LoggingStdioServerTransport } from "./logging-transport"; +import { isFirebaseStudio } from "../env"; +import { timeoutFallback } from "../timeout"; +import { resolveResource, resources, resourceTemplates } from "./resources"; +import * as crossSpawn from "cross-spawn"; + +const SERVER_VERSION = "0.3.0"; + +const cmd = new Command("mcp"); + +const orderedLogLevels = [ + "debug", + "info", + "notice", + "warning", + "error", + "critical", + "alert", + "emergency", +] as const; + +export class FirebaseMcpServer { + private _ready = false; + private _readyPromises: { resolve: () => void; reject: (err: unknown) => void }[] = []; + startupRoot?: string; + cachedProjectDir?: string; + server: Server; + activeFeatures?: ServerFeature[]; + detectedFeatures?: ServerFeature[]; + clientInfo?: { name?: string; version?: string }; + emulatorHubClient?: EmulatorHubClient; + private cliCommand?: string; + + // logging spec: + // https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/logging + currentLogLevel?: LoggingLevel = process.env.FIREBASE_MCP_DEBUG_LOG ? "debug" : undefined; + // the api of logging from a consumers perspective looks like `server.logger.warn("my warning")`. + public readonly logger = Object.fromEntries( + orderedLogLevels.map((logLevel) => [ + logLevel, + (message: unknown) => this.log(logLevel, message), + ]), + ) as Record Promise>; + + /** Create a special tracking function to avoid blocking everything on initialization notification. */ + private async trackGA4( + event: Parameters[0], + params: Parameters[1] = {}, + ): Promise { + // wait until ready or until 2s has elapsed + if (!this.clientInfo) await timeoutFallback(this.ready(), null, 2000); + const clientInfoParams: { + mcp_client_name: string; + mcp_client_version: string; + gemini_cli_extension: string; + } = { + mcp_client_name: this.clientInfo?.name || "", + mcp_client_version: this.clientInfo?.version || "", + gemini_cli_extension: process.env.IS_GEMINI_CLI_EXTENSION ? "true" : "false", + }; + return trackGA4(event, { ...params, ...clientInfoParams }); + } + + constructor(options: { activeFeatures?: ServerFeature[]; projectRoot?: string }) { + this.activeFeatures = options.activeFeatures; + this.startupRoot = options.projectRoot || process.env.PROJECT_ROOT; + this.server = new Server({ name: "firebase", version: SERVER_VERSION }); + this.server.registerCapabilities({ + tools: { listChanged: true }, + logging: {}, + prompts: { listChanged: true }, + resources: {}, + }); + + this.server.setRequestHandler(ListToolsRequestSchema, this.mcpListTools.bind(this)); + this.server.setRequestHandler(CallToolRequestSchema, this.mcpCallTool.bind(this)); + this.server.setRequestHandler(ListPromptsRequestSchema, this.mcpListPrompts.bind(this)); + this.server.setRequestHandler(GetPromptRequestSchema, this.mcpGetPrompt.bind(this)); + this.server.setRequestHandler( + ListResourceTemplatesRequestSchema, + this.mcpListResourceTemplates.bind(this), + ); + this.server.setRequestHandler(ListResourcesRequestSchema, this.mcpListResources.bind(this)); + this.server.setRequestHandler(ReadResourceRequestSchema, this.mcpReadResource.bind(this)); + const onInitialized = (): void => { + const clientInfo = this.server.getClientVersion(); + this.clientInfo = clientInfo; + if (clientInfo?.name) { + void this.trackGA4("mcp_client_connected"); + } + if (!this.clientInfo?.name) this.clientInfo = { name: "" }; + + this._ready = true; + while (this._readyPromises.length) { + this._readyPromises.pop()?.resolve(); + } + }; + + this.server.oninitialized = () => { + void onInitialized(); + }; + + this.server.setRequestHandler(SetLevelRequestSchema, async ({ params }) => { + this.currentLogLevel = params.level; + return {}; + }); + + this.detectProjectRoot(); + this.detectActiveFeatures(); + } + + /** Wait until initialization has finished. */ + ready() { + if (this._ready) return Promise.resolve(); + return new Promise((resolve, reject) => { + this._readyPromises.push({ resolve: resolve as () => void, reject }); + }); + } + + get clientName(): string { + return this.clientInfo?.name ?? (isFirebaseStudio() ? "Firebase Studio" : ""); + } + + private get clientConfigKey() { + return `mcp.clientConfigs.${this.clientName}:${this.startupRoot || process.cwd()}`; + } + + getStoredClientConfig(): ClientConfig { + return configstore.get(this.clientConfigKey) || {}; + } + + updateStoredClientConfig(update: Partial) { + const config = configstore.get(this.clientConfigKey) || {}; + const newConfig = { ...config, ...update }; + configstore.set(this.clientConfigKey, newConfig); + return newConfig; + } + + async detectProjectRoot(): Promise { + await timeoutFallback(this.ready(), null, 2000); + if (this.cachedProjectDir) return this.cachedProjectDir; + const storedRoot = this.getStoredClientConfig().projectRoot; + this.cachedProjectDir = storedRoot || this.startupRoot || process.cwd(); + this.log("debug", "detected and cached project root: " + this.cachedProjectDir); + return this.cachedProjectDir; + } + + async detectActiveFeatures(): Promise { + if (this.detectedFeatures?.length) return this.detectedFeatures; // memoized + this.log("debug", "detecting active features of Firebase MCP server..."); + const options = await this.resolveOptions(); + const projectId = await this.getProjectId(); + const detected = await Promise.all( + SERVER_FEATURES.map(async (f) => { + if (await checkFeatureActive(f, projectId, options)) return f; + return null; + }), + ); + this.detectedFeatures = detected.filter((f) => !!f) as ServerFeature[]; + this.log( + "debug", + "detected features of Firebase MCP server: " + (this.detectedFeatures.join(", ") || ""), + ); + return this.detectedFeatures; + } + + async getEmulatorHubClient(): Promise { + // Single initialization + if (this.emulatorHubClient) { + return this.emulatorHubClient; + } + const projectId = await this.getProjectId(); + this.emulatorHubClient = new EmulatorHubClient(projectId); + return this.emulatorHubClient; + } + + async getEmulatorUrl(emulatorType: Emulators): Promise { + const hubClient = await this.getEmulatorHubClient(); + if (!hubClient) { + throw Error( + "Emulator Hub not found or is not running. You can start the emulator by running `firebase emulators:start` in your firebase project directory.", + ); + } + + const emulators = await hubClient.getEmulators(); + const emulatorInfo = emulators[emulatorType]; + if (!emulatorInfo) { + throw Error( + `No ${emulatorType} Emulator found running. Make sure your project firebase.json file includes ${emulatorType} and then rerun emulator using \`firebase emulators:start\` from your project directory.`, + ); + } + + const host = emulatorInfo.host.includes(":") ? `[${emulatorInfo.host}]` : emulatorInfo.host; + + return `http://${host}:${emulatorInfo.port}`; + } + + get availableTools(): ServerTool[] { + return availableTools( + this.activeFeatures?.length ? this.activeFeatures : this.detectedFeatures, + ); + } + + getTool(name: string): ServerTool | null { + return this.availableTools.find((t) => t.mcp.name === name) || null; + } + + get availablePrompts(): ServerPrompt[] { + return availablePrompts( + this.activeFeatures?.length ? this.activeFeatures : this.detectedFeatures, + ); + } + + getPrompt(name: string): ServerPrompt | null { + return this.availablePrompts.find((p) => p.mcp.name === name) || null; + } + + setProjectRoot(newRoot: string | null): void { + this.updateStoredClientConfig({ projectRoot: newRoot }); + this.cachedProjectDir = newRoot || undefined; + this.detectedFeatures = undefined; // reset detected features + void this.server.sendToolListChanged(); + void this.server.sendPromptListChanged(); + } + + async resolveOptions(): Promise> { + const options: Partial = { cwd: this.cachedProjectDir, isMCP: true }; + await cmd.prepare(options); + return options; + } + + async getProjectId(): Promise { + return getProjectId(await this.resolveOptions()); + } + + async getAuthenticatedUser(skipAutoAuth: boolean = false): Promise { + try { + this.log("debug", `calling requireAuth`); + const email = await requireAuth(await this.resolveOptions(), skipAutoAuth); + this.log("debug", `detected authenticated account: ${email || ""}`); + return email ?? (skipAutoAuth ? null : "Application Default Credentials"); + } catch (e) { + this.log("debug", `error in requireAuth: ${e}`); + return null; + } + } + + private _createMcpContext(projectId: string, accountEmail: string | null): McpContext { + const options = { projectDir: this.cachedProjectDir, cwd: this.cachedProjectDir }; + return { + projectId: projectId, + host: this, + config: Config.load(options, true) || new Config({}, options), + rc: loadRC(options), + accountEmail, + firebaseCliCommand: this._getFirebaseCliCommand(), + }; + } + + private _getFirebaseCliCommand(): string { + if (!this.cliCommand) { + const testCommand = crossSpawn.sync("firebase --version"); + this.cliCommand = testCommand.error ? "npx firebase-tools@latest" : "firebase"; + } + return this.cliCommand; + } + + async mcpListTools(): Promise { + await Promise.all([this.detectActiveFeatures(), this.detectProjectRoot()]); + const hasActiveProject = !!(await this.getProjectId()); + await this.trackGA4("mcp_list_tools"); + const skipAutoAuthForStudio = isFirebaseStudio(); + this.log("debug", `skip auto-auth in studio environment: ${skipAutoAuthForStudio}`); + return { + tools: this.availableTools.map((t) => t.mcp), + _meta: { + projectRoot: this.cachedProjectDir, + projectDetected: hasActiveProject, + authenticatedUser: await this.getAuthenticatedUser(skipAutoAuthForStudio), + activeFeatures: this.activeFeatures, + detectedFeatures: this.detectedFeatures, + }, + }; + } + + async mcpCallTool(request: CallToolRequest): Promise { + await this.detectProjectRoot(); + const toolName = request.params.name; + const toolArgs = request.params.arguments; + const tool = this.getTool(toolName); + if (!tool) throw new Error(`Tool '${toolName}' could not be found.`); + + // Check if the current project directory exists. + if (!tool.mcp._meta?.optionalProjectDir) { + if (!this.cachedProjectDir || !existsSync(this.cachedProjectDir)) { + return noProjectDirectory(this.cachedProjectDir); + } + } + + // Check if the project ID is set. + let projectId = await this.getProjectId(); + if (tool.mcp._meta?.requiresProject && !projectId) { + return NO_PROJECT_ERROR; + } + projectId = projectId || ""; + + // Check if the user is logged in. + const skipAutoAuthForStudio = isFirebaseStudio(); + const accountEmail = await this.getAuthenticatedUser(skipAutoAuthForStudio); + if (tool.mcp._meta?.requiresAuth && !accountEmail) { + return mcpAuthError(skipAutoAuthForStudio); + } + + // Check if the tool requires Gemini in Firebase API. + if (tool.mcp._meta?.requiresGemini) { + const err = await requireGeminiToS(projectId); + if (err) return err; + } + + const toolsCtx = this._createMcpContext(projectId, accountEmail); + try { + const res = await tool.fn(toolArgs, toolsCtx); + await this.trackGA4("mcp_tool_call", { + tool_name: toolName, + error: res.isError ? 1 : 0, + }); + return res; + } catch (err: unknown) { + await this.trackGA4("mcp_tool_call", { + tool_name: toolName, + error: 1, + }); + return mcpError(err); + } + } + + async mcpListPrompts(): Promise { + await Promise.all([this.detectActiveFeatures(), this.detectProjectRoot()]); + const hasActiveProject = !!(await this.getProjectId()); + await this.trackGA4("mcp_list_prompts"); + const skipAutoAuthForStudio = isFirebaseStudio(); + return { + prompts: this.availablePrompts.map((p) => ({ + name: p.mcp.name, + description: p.mcp.description, + annotations: p.mcp.annotations, + arguments: p.mcp.arguments, + })), + _meta: { + projectRoot: this.cachedProjectDir, + projectDetected: hasActiveProject, + authenticatedUser: await this.getAuthenticatedUser(skipAutoAuthForStudio), + activeFeatures: this.activeFeatures, + detectedFeatures: this.detectedFeatures, + }, + }; + } + + async mcpGetPrompt(req: GetPromptRequest): Promise { + await this.detectProjectRoot(); + const promptName = req.params.name; + const promptArgs = req.params.arguments || {}; + const prompt = this.getPrompt(promptName); + if (!prompt) { + throw new Error(`Prompt '${promptName}' could not be found.`); + } + + let projectId = await this.getProjectId(); + projectId = projectId || ""; + + const skipAutoAuthForStudio = isFirebaseStudio(); + const accountEmail = await this.getAuthenticatedUser(skipAutoAuthForStudio); + + const promptsCtx = this._createMcpContext(projectId, accountEmail); + + try { + const messages = await prompt.fn(promptArgs, promptsCtx); + await this.trackGA4("mcp_get_prompt", { + tool_name: promptName, + }); + return { + messages, + }; + } catch (err: unknown) { + await this.trackGA4("mcp_get_prompt", { + tool_name: promptName, + error: 1, + }); + // TODO: should we return mcpError here? + throw err; + } + } + + async mcpListResources(): Promise { + await trackGA4("mcp_read_resource", { resource_name: "__list__" }); + return { + resources: resources.map((r) => r.mcp), + }; + } + + async mcpListResourceTemplates(): Promise { + return { + resourceTemplates: resourceTemplates.map((rt) => rt.mcp), + }; + } + + async mcpReadResource(req: ReadResourceRequest): Promise { + let projectId = await this.getProjectId(); + projectId = projectId || ""; + + const skipAutoAuthForStudio = isFirebaseStudio(); + const accountEmail = await this.getAuthenticatedUser(skipAutoAuthForStudio); + + const resourceCtx = this._createMcpContext(projectId, accountEmail); + + const resolved = await resolveResource(req.params.uri, resourceCtx); + if (!resolved) { + throw new McpError( + ErrorCode.InvalidParams, + `Resource '${req.params.uri}' could not be found.`, + ); + } + return resolved.result; + } + + async start(): Promise { + const transport = process.env.FIREBASE_MCP_DEBUG_LOG + ? new LoggingStdioServerTransport(process.env.FIREBASE_MCP_DEBUG_LOG) + : new StdioServerTransport(); + await this.server.connect(transport); + } + + private log(level: LoggingLevel, message: unknown): void { + let data = message; + + // mcp protocol only takes jsons or it errors; for convienence, format + // a a string into a json. + if (typeof message === "string") { + data = { message }; + } + + if (!this.currentLogLevel) { + return; + } + + if (orderedLogLevels.indexOf(this.currentLogLevel) > orderedLogLevels.indexOf(level)) { + return; + } + + if (this._ready) void this.server.sendLoggingMessage({ level, data }); + } +} diff --git a/src/mcp/logging-transport.ts b/src/mcp/logging-transport.ts new file mode 100644 index 00000000000..749a34a1a80 --- /dev/null +++ b/src/mcp/logging-transport.ts @@ -0,0 +1,24 @@ +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; +import { appendFileSync } from "fs"; +import { appendFile } from "fs/promises"; + +export class LoggingStdioServerTransport extends StdioServerTransport { + path: string; + + constructor(path: string) { + super(); + this.path = path; + appendFileSync(path, "--- new process start ---\n"); + const origOnData = this._ondata; + this._ondata = (chunk: Buffer) => { + origOnData(chunk); + appendFileSync(path, chunk.toString(), { encoding: "utf8" }); + }; + } + + async send(message: JSONRPCMessage) { + await super.send(message); + await appendFile(this.path, JSON.stringify(message) + "\n"); + } +} diff --git a/src/mcp/prompt.ts b/src/mcp/prompt.ts new file mode 100644 index 00000000000..26a34d3d1cd --- /dev/null +++ b/src/mcp/prompt.ts @@ -0,0 +1,26 @@ +import { PromptMessage } from "@modelcontextprotocol/sdk/types.js"; +import { McpContext } from "./types"; + +export interface ServerPrompt { + mcp: { + name: string; + description?: string; + arguments?: { name: string; description?: string; required?: boolean }[]; + omitPrefix?: boolean; + annotations?: { + title?: string; + }; + _meta?: { + /** Prompts are grouped by feature. --only can configure what prompts is available. */ + feature?: string; + }; + }; + fn: (args: Record, ctx: McpContext) => Promise; +} + +export function prompt(options: ServerPrompt["mcp"], fn: ServerPrompt["fn"]): ServerPrompt { + return { + mcp: options, + fn, + }; +} diff --git a/src/mcp/prompts/core/consult.ts b/src/mcp/prompts/core/consult.ts new file mode 100644 index 00000000000..fc0fe6e2227 --- /dev/null +++ b/src/mcp/prompts/core/consult.ts @@ -0,0 +1,57 @@ +import { getPlatformsFromFolder } from "../../../appUtils"; +import { chatWithFirebase } from "../../../gemini/fdcExperience"; +import { requireGeminiToS } from "../../errors"; +import { prompt } from "../../prompt"; + +export const consult = prompt( + { + name: "consult", + description: + "Use this command to consult the Firebase Assistant with access to detailed up-to-date documentation for the Firebase platform.", + arguments: [ + { + name: "prompt", + description: "a question to pass to the Gemini in Firebase model", + required: true, + }, + ], + annotations: { + title: "Consult Firebase Assistant", + }, + }, + async ({ prompt }, { config, projectId }) => { + const gifTosError = await requireGeminiToS(projectId); + if (gifTosError) { + return [ + { + role: "user", + content: { + type: "text", + text: `Missing required conditions to run this prompt:\n\n${gifTosError.content[0]?.text}\n\nPlease ask the user if they would like to accept these terms of service before proceeding. If they decline, inform them that this operation cannot continue without their acceptance.`, + }, + }, + ]; + } + + const platforms = await getPlatformsFromFolder(config.projectDir); + + const gifPrompt = `I am using a coding agent to build with Firebase and I have a specific question that I would like answered. Provide a robust and detailed response that will help the coding agent act on my behalf in a local workspace. + +App Platform(s): ${platforms.join(", ")} + +Question: ${prompt}`; + + const result = await chatWithFirebase(gifPrompt, projectId); + const outputString = result.output.messages?.[0].content ?? ""; + + return [ + { + role: "user", + content: { + type: "text", + text: `I have consulted a Firebase Assistant agent with the following question: "${prompt}". Its response was as follows:\n\n${outputString}\n\nPlease use the information above to respond to my question. I have not seen the response from the Firebase Assistant, so please include all necessary information in your response. Inform the user that they must run the \`firebase:consult\` prompt again if they have followup questions for the Firebase Assistant.`, + }, + }, + ]; + }, +); diff --git a/src/mcp/prompts/core/deploy.ts b/src/mcp/prompts/core/deploy.ts new file mode 100644 index 00000000000..71d350e6b23 --- /dev/null +++ b/src/mcp/prompts/core/deploy.ts @@ -0,0 +1,101 @@ +import { prompt } from "../../prompt"; + +export const deploy = prompt( + { + name: "deploy", + description: "Use this command to deploy resources to Firebase.", + arguments: [ + { + name: "prompt", + description: "any specific instructions you wish to provide about deploying", + required: false, + }, + ], + annotations: { + title: "Deploy to Firebase", + }, + }, + async ({ prompt }, { config, projectId, accountEmail, firebaseCliCommand }) => { + return [ + { + role: "user" as const, + content: { + type: "text", + text: ` +Your goal is to deploy resources from the current project to Firebase. + +Active user: ${accountEmail || ""} +Active project: ${projectId || ""} + +Contents of \`firebase.json\` config file: + +\`\`\`json +${config.readProjectFile("firebase.json", { fallback: "" })} +\`\`\` + +## User Instructions + +${prompt || ""} + +## Steps + +Follow the steps below taking note of any user instructions provided above. + +1. If there is no active user, prompt the user to run \`${firebaseCliCommand} login\` in an interactive terminal before continuing. +2. Analyze the source code in the current working directory to determine if this is a web app. If it isn't, end this process and tell the user "The /firebase:deploy command only works with web apps." +3. Analyze the source code in the current working directory to determine if the app requires a server for Server-Side Rendering (SSR). This will determine whether or not to use Firebase App Hosting. Here are instructions to determine if the app needs a server: + Objective: Analyze the provided codebase files to determine if the web application requires a backend for Server-Side Rendering (SSR). Your final output must be a clear "Yes" or "No" followed by a brief justification. + Primary Analysis: package.json + This is the most critical step. If the package.json file is present, perform the following checks in order. + Parse package.json: Locate and read the contents of the package.json file. + Check Dependencies: + Examine the dependencies and devDependencies objects. + If any of the following packages are listed as keys, you can conclude the app uses SSR. + next + nuxt + @sveltejs/kit + @angular/ssr + remix + If a match is found, proceed directly to the Final Determination step. + Check Scripts: If no framework dependency was found, examine the scripts object. + Look for scripts (commonly start or serve) that execute a server process. + Examples include: "start": "next start", "start": "nuxt start", or "dev": "ng serve --ssr". + If such a script is found, conclude the app uses SSR and proceed to the Final Determination step. + Secondary Analysis: Project File Structure + Perform this analysis only if package.json is missing or inconclusive. + Scan for Framework-Specific Files and Directories: Search the codebase for the following patterns: + Next.js: A directory named app/ or pages/. Inside these, check for files containing the function name getServerSideProps. + Nuxt.js: A directory named server/. + SvelteKit: Any file ending with the .server.js suffix (e.g., +page.server.js, +layout.server.js). + Angular: A file named server.ts. + If any of these patterns are found, conclude the app uses SSR. + Final Determination + State Your Conclusion: Begin your response with a definitive "Yes" or "No". + Yes: The application requires a backend for SSR. + No: The application does not appear to require a backend for SSR and is likely a static or client-side rendered app. + Provide Justification: Follow your conclusion with a single sentence explaining the evidence. + Example (Yes): "Yes, the project requires SSR, as evidenced by the next dependency in package.json." + Example (Yes): "Yes, the project requires SSR, as evidenced by the presence of a +page.server.js file." + Example (No): "No, there are no dependencies or file structures that indicate the use of a server-side rendering framework." +4. If there is no \`firebase.json\` file, manually create one based on whether the app requires SSR: + 4a. If the app requires SSR, configure Firebase App Hosting: + Create \`firebase.json\ with an "apphosting" configuration, setting backendId to the app's name in package.json: \`{"apphosting": {"backendId": ""}}\ + 4b. If the app does NOT require SSR, configure Firebase Hosting: + Create \`firebase.json\ with a "hosting" configuration. Add a \`{"hosting": {"predeploy": ""}}\` config to build before deploying. +5. Check if there is an active Firebase project for this environment (the \`firebase_get_environment\` tool may be helpful). If there is, provide the active project ID to the user and ask them if they want to proceed using that project. If there is not an active project, give the user two options: Provide an existing project ID or create a new project. Only use the list_projects tool on user request. Wait for their response before proceeding. + 5a. If the user chooses to use an existing Firebase project, the \`firebase_list_projects\` tool may be helpful. Set the selected project as the active project (the \`firebase_update_environment\` tool may be helpful). + 5b. If the user chooses to create a new project, use the \`firebase_create_project \` tool. Then set the new project as the active project (the \`firebase_update_environment\` tool may be helpful). +6. If firebase.json contains an "apphosting" configuration, check if a backend exists matching the provided backendId (the \`apphosting_list_backends\` tool may be helpful). + If it doesn't exist, create one by running the \`${firebaseCliCommand} apphosting:backends:create --backend --primary-region us-central1 --root-dir .\` shell. +7. Only after making sure Firebase has been initialized, run the \`${firebaseCliCommand} deploy\` shell command to perform the deploy. This may take a few minutes. + 7a. If deploying to apphosting, tell the user the deployment will take a few minutes, and they can monitor deployment progress in the Firebase console: \`https://console.firebase.google.com/project//apphosting\` +8. If the deploy has errors, attempt to fix them and ask the user clarifying questions as needed. +9. If the deploy needs \`--force\` to run successfully, ALWAYS prompt the user before running \`${firebaseCliCommand} deploy --force\`. +10. If only one specific feature is failing, use command \`${firebaseCliCommand} deploy --only \` as you debug. +11. If the deploy succeeds, your job is finished. +`.trim(), + }, + }, + ]; + }, +); diff --git a/src/mcp/prompts/core/index.ts b/src/mcp/prompts/core/index.ts new file mode 100644 index 00000000000..afa32912b2f --- /dev/null +++ b/src/mcp/prompts/core/index.ts @@ -0,0 +1,7 @@ +import { init } from "./init"; +import { deploy } from "./deploy"; +import { consult } from "./consult"; + +const corePrompts = [deploy, init, consult]; + +export { corePrompts }; diff --git a/src/mcp/prompts/core/init.ts b/src/mcp/prompts/core/init.ts new file mode 100644 index 00000000000..6b90c9c3aa2 --- /dev/null +++ b/src/mcp/prompts/core/init.ts @@ -0,0 +1,96 @@ +import { getPlatformsFromFolder } from "../../../appUtils"; +import { prompt } from "../../prompt"; + +export const init = prompt( + { + name: "init", + description: "Use this command to set up Firebase services, like backend and AI features.", + annotations: { + title: "Initialize Firebase", + }, + }, + async (_, mcp) => { + const { config, projectId, accountEmail, firebaseCliCommand } = mcp; + + const platforms = await getPlatformsFromFolder(config.projectDir); + + return [ + { + role: "user" as const, + content: { + type: "text", + text: ` +Your goal is to help the user setup Firebase services in this workspace. Firebase is a large platform with many potential uses, so you will: + +1. Detect which Firebase services are already in use in the workspace, if any +2. Determine which new Firebase services will help the user build their app +3. Provision and configure the services requested by the user + +## Workspace Info + +Use this information to determine which Firebase services the user is already using (if any). + +Workspace platform(s): ${platforms.length > 0 ? platforms.join(", ") : ""} +Active user: ${accountEmail || ""} +Active project: ${projectId || ""} + +Contents of \`firebase.json\` config file: + +\`\`\`json +${config.readProjectFile("firebase.json", { fallback: "" })} +\`\`\` + + +## Steps +Follow the steps below taking note of any user instructions provided above. + +1. If there is no active user, use the \`firebase_login\` tool to help them sign in. + - If you run into issues logging the user in, suggest that they run \`${firebaseCliCommand} login --reauth\` in a separate terminal +2. Start by listing out the existing init options that are available to the user. Ask the user which set of services they would like to add to their app. Always enumerate them and list the options out explicitly for the user: + - Backend Services: Backend services for the app, such as setting up a database, adding a user-authentication sign up and login page, and deploying a web app to a production URL. + - IMPORTANT: The backend setup guide is for web apps only. If the user requests backend setup for a mobile app (iOS, Android, or Flutter), inform them that this is not supported and do not use the backend setup guide. You can still assist with other requests. + - Firebase AI Logic: Add AI features such as chat experiences, multimodal prompts, image generation and editing (via nano banana), etc. + - IMPORTANT: The Firebase AI Logic setup guide is for web, flutter, and android apps only. If the user requests firebase setup for unsupported platforms (iOS, Unity, or anything else), inform them that this is not supported and direct the user to Firebase Docs to learn how to set up AI Logic for their application (share this link with the user https://firebase.google.com/docs/ai-logic/get-started?api=dev). You can still assist with other requests. +3. After the user chooses an init option, create a plan based on the remaining steps in this guide, share it with the user, and give them an opportunity to accept or adjust it. +4. If there is no active Firebase project, ask the user if they want to create a new project or use an existing one. If using an existing project, ask for the project ID and explain how to find it: open the Firebase Console (http://console.firebase.google.com/), locate the project ID under the project name in the projects list, or open the project and go to Project Overview → Project Settings. + - If they would like to create a project, use the firebase_create_project with the project ID + - If they would like to use an existing project, use the firebase_update_environment tool with the active_project argument. + - If you run into issues creating the firebase project, ask the user to go to the [Firebase Console](http://console.firebase.google.com/) and create a project. Wait for the user to report back before continuing. +5. Ensure there is an active Firebase App for their platform + - Do the following only for Web and Android apps + - Run the \`firebase_list_apps\` tool to list their apps, and find an app that matches their "Workspace platform" + - If there is no app that matches that criteria, use the \`firebase_create_app\` tool to create the app with the appropriate platform + - Do the following only for Flutter apps + - Execute \`firebase --version\` to check if the Firebase CLI is installed + - If it isn't installed, run \`npm install -g firebase-tools\` to install it. If it is installed, skip to the next step. + - Install the Flutterfire CLI + - Use the Flutterfire CLI tool to connect to the project + - Use the Flutterfire CLI to register the appropriate applications based on the user's input + - Let the developer know that you currently only support configuring web, ios, and android targets together in a bundle. Each of those targets will have appropriate apps registered in the project using the flutterfire CLI + - Execute flutterfire config using the following pattern: flutterfire config --yes --project= --platforms= +6. Now that we have a working environment, print out 1) Active user 2) Firebase Project and 3) Firebase App & platform they are using for this process. + - Ask the user to confirm this is correct before continuing +7. Set up the web Firebase SDK. Skip straight to #8 for Flutter and Android apps + - Fetch the configuration for the specified app using the \`firebase_get_sdk_config\` tool. + - Write the Firebase SDK config to a file + - Check what the latest version of the SDK is by running the command 'npm view firebase version' + - If the user app has a package.json, install via npm + - Run 'npm i firebase' + - Import it into the app code: + ''' + import { initializeApp } from 'firebase/app'; + ''' + - If the user app does not have a package.json, import via CDN: + ''' + import { initializeApp } from 'https://www.gstatic.com/firebasejs/12.3.0/firebase-app.js' + ''' +8. Read the guide for the appropriate services and follow the instructions. If no guides match the user's need, inform the user. +- Use the Firebase \`read_resources\` tool to load the instructions for the service the developer chose in step 2 of this guide + - [Backend Services](firebase://guides/init/backend): Read this resource to set up backend services for the app, such as setting up a database, adding a user-authentication sign up and login page, and deploying a web app to a production URL. + - [Firebase AI Logic](firebase://guides/init/ai): Read this resource to add Gemini-powered AI features such as chat experiences, multimodal prompts, image generation, image editing (via nano banana), etc. +`.trim(), + }, + }, + ]; + }, +); diff --git a/src/mcp/prompts/crashlytics/connect.ts b/src/mcp/prompts/crashlytics/connect.ts new file mode 100644 index 00000000000..066396c2fe2 --- /dev/null +++ b/src/mcp/prompts/crashlytics/connect.ts @@ -0,0 +1,143 @@ +import { prompt } from "../../prompt"; + +export const connect = prompt( + { + name: "connect", + omitPrefix: false, + description: "Access a Firebase application's Crashlytics data.", + annotations: { + title: "Access Crashlytics data", + }, + }, + async (unused, { accountEmail, firebaseCliCommand }) => { + return [ + { + role: "user" as const, + content: { + type: "text", + text: ` +You are going to help a developer prioritize and fix issues in their +mobile application by accessing their Firebase Crashlytics data. + +Active user: ${accountEmail || ""} + +## Required first steps! Absolutely required! Incredibly important! + + 1. **Make sure the user is logged in. No Crashlytics tools will work if the user is not logged in.** + a. Use the \`firebase_get_environment\` tool to verify that the user is logged in. + b. If the Firebase 'Active user' is set to , instruct the user to run \`${firebaseCliCommand} login\` + before continuing. Ignore other fields that are set to . We are just making sure the + user is logged in. + + 2. **Get the app ID for the Firebase application.** + + Use the information below to help you find the developer's app ID. If you cannot find it after 2-3 + attempts, just ask the user for the value they want to use, providing the description of what the + value looks like. + + * **Description:** The app ID we are looking for contains four colon (":") delimited parts: a version + number (typically "1"), a project number, a platform type ("android", "ios", or "web"), + and a sequence of hexadecimal characters. This can be found in the project settings in the Firebase Console + or in the appropriate google services file for the application type. + * For Android apps, you will typically find the app ID in a file called google-services.json under the + mobilesdk_app_id key. The file is most often located in the app directory that contains the src directory. + * For iOS apps, you will typically find the app ID in a property list file called GoogleService-Info.plist under the + GOOGLE_APP_ID key. The plist file is most often located in the main project directory. + * Sometimes developers will not check in the google services file because it is a shared or public + repository. If you can't find the file, the files may be included in the .gitignore. Check again for the file + removing restrictions around looking for tracked files. + * Developers may have multiple google services files that map to different releases. In cases like this, + developers may create different directories to hold each like alpha/google-services.json or alpha/GoogleService-Info.plist. + In other cases, developers may change the suffix of the file to something like google-services-alpha.json or + GoogleService-Alpha.plist. Look for as many google services files as you can find. + * Sometimes developers may include the codebase for both the Android app and the iOS app in the same repository. + * If there are multiple files or multiple app IDs in a single file, ask the user to choose one by providing + a numbered list of all the package names. + * Again, if you have trouble finding the app ID, just ask the user for it. + +## Next steps + +Once you have confirmed that the user is logged in to Firebase, and confirmed the +id for the application that they want to access, then you can ask the user what actions +they would like to perform. Here are some possibilities and instructions follow below: + + 1. Prioritize the most impactful stability issues + 2. Diagnose and propose a fix for a crash + +## Instructions for Using Crashlytics Data + +### How to prioritize issues + +Follow these steps to fetch issues and prioritize them. + + 1. Use the 'crashlytics_get_top_issues' tool to fetch up to 20 issues. + 1a. Analyze the user's query and apply the appropriate filters. + 1b. If the user asks for crashes, then set the issueErrorType filter to *FATAL*. + 1c. If the user asks about a particular time range, then set both the intervalStartTime and intervalEndTime. + 2. Use the 'crashlytics_get_top_versions' tool to fetch the top versions for this app. + 3. If the user instructions include statements about prioritization, use those instructions. + 4. If the user instructions do not include statements about prioritization, + then prioritize the returned issues using the following criteria: + 4a. The app versions for the issue include the most recent version of the app. + 4b. The number of users experiencing the issue across variants + 4c. The volume of crashes + 5. Return the top 5 issues, with a brief description each in a numerical list with the following format: + 1. Issue + * + * + * **Description:** + * **Rationale:** + 6. Ask the user if they would like to diagnose and fix any of the issues presented + +### How to diagnose and fix issues + +Follow these steps to diagnose and fix issues. + + 1. Make sure you have a good understanding of the code structure and where different functionality exists + 2. Use the 'crashlytics_get_issue' tool to get more context on the issue. + 3. Use the 'crashlytics_batch_get_events' tool to get an example crash for this issue. Use the event names in the sampleEvent fields. + 3a. If you need to read more events, use the 'crashlytics_list_events' tool. + 3b. Apply the same filtering criteria that you used to find the issue, so that you find a appropriate events. + 4. Read the files that exist in the stack trace of the issue to understand the crash deeply. + 5. Determine possible root causes for the crash - no more than 5 potential root causes. + 6. Critique your own determination, analyzing how plausible each scenario is given the crash details. + 7. Choose the most likely root cause given your analysis. + 8. Write out a plan for the most likely root cause using the following criteria: + 8a. Write out a description of the issue and including + * A brief description of the cause of the issue + * A determination of your level of confidence in the cause of the issue using your analysis. + * A determination of which library is at fault, this codebase or a dependent library + * A determination for how complex the fix will be + 8b. The plan should include relevant files to change + 8c. The plan should include a test plan for how the user might verify the fix + 8d. Use the following format for the plan: + + ## Cause + + - **Fault**: + - **Complexity**: + + ## Fix + + 1. + 2. + + ## Test + + 1. + 2. + + ## Other potential causes + 1. + 2. + + 9. Present the plan to the user and get approval before making the change. + 10. Only if they approve the plan, create a fix for the issue. + 10a. Be mindful of API contracts and do not add fields to resources without a clear way to populate those fields + 10b. If there is not enough information in the crash report to find a root cause, describe why you cannot fix the issue instead of making a guess. +`.trim(), + }, + }, + ]; + }, +); diff --git a/src/mcp/prompts/crashlytics/index.ts b/src/mcp/prompts/crashlytics/index.ts new file mode 100644 index 00000000000..8e91942b33d --- /dev/null +++ b/src/mcp/prompts/crashlytics/index.ts @@ -0,0 +1,4 @@ +import type { ServerPrompt } from "../../prompt"; +import { connect } from "./connect"; + +export const crashlyticsPrompts: ServerPrompt[] = [connect]; diff --git a/src/mcp/prompts/dataconnect/index.ts b/src/mcp/prompts/dataconnect/index.ts new file mode 100644 index 00000000000..e1c323ec73b --- /dev/null +++ b/src/mcp/prompts/dataconnect/index.ts @@ -0,0 +1,9 @@ +import { isEnabled } from "../../../experiments"; +import { schema } from "./schema"; +import type { ServerPrompt } from "../../prompt"; + +export const dataconnectPrompts: ServerPrompt[] = []; + +if (isEnabled("mcpalpha")) { + dataconnectPrompts.push(schema); +} diff --git a/src/mcp/prompts/dataconnect/schema.ts b/src/mcp/prompts/dataconnect/schema.ts new file mode 100644 index 00000000000..cbc1dfd9028 --- /dev/null +++ b/src/mcp/prompts/dataconnect/schema.ts @@ -0,0 +1,73 @@ +import { prompt } from "../../prompt"; +import { loadAll } from "../../../dataconnect/load"; +import type { ServiceInfo } from "../../../dataconnect/types"; +import { BUILTIN_SDL, MAIN_INSTRUCTIONS } from "../../util/dataconnect/content"; +import { compileErrors } from "../../util/dataconnect/compile"; + +function renderServices(fdcServices: ServiceInfo[]) { + if (!fdcServices.length) return "Data Connect Status: "; + + return `\n\n## Data Connect Schema + +The following is the up-to-date content of existing schema files (their paths are relative to the Data Connect source directory). + +${fdcServices[0].schema.source.files?.map((f) => `\`\`\`graphql ${f.path}\n${f.content}\n\`\`\``).join("\n\n")}`; +} + +function renderErrors(errors?: string) { + return `\n\n## Current Schema Build Errors\n\n${errors || ""}`; +} + +export const schema = prompt( + { + name: "schema", + description: "Generate or update your Firebase Data Connect schema.", + arguments: [ + { + name: "prompt", + description: + "describe the schema you want generated or the edits you want to make to your existing schema", + required: true, + }, + ], + annotations: { + title: "Generate Data Connect Schema", + }, + }, + async ({ prompt }, { config, projectId, accountEmail }) => { + const fdcServices = await loadAll(projectId, config); + const buildErrors = fdcServices.length + ? await compileErrors(fdcServices[0].sourceDirectory) + : ""; + + return [ + { + role: "user" as const, + content: { + type: "text", + text: ` +${MAIN_INSTRUCTIONS}\n\n${BUILTIN_SDL} + +==== CURRENT ENVIRONMENT INFO ==== + +User Email: ${accountEmail || ""} +Project ID: ${projectId || ""} +${renderServices(fdcServices)}${renderErrors(buildErrors)} + +==== USER PROMPT ==== + +${prompt} + +==== TASK INSTRUCTIONS ==== + +1. If Data Connect is marked as \`\`, first run the \`firebase_init\` tool with \`{dataconnect: {}}\` arguments to initialize it. +2. If there is not an existing schema to work with (or the existing schema is the commented-out default schema about a movie app), follow the user's prompt to generate a robust schema meeting the specified requirements. +3. If there is already a schema, perform edits to the existing schema file(s) based on the user's instructions. If schema build errors are present and seem relevant to your changes, attempt to fix them. +4. After you have performed edits on the schema, run the \`dataconnect_compile\` tool to build the schema and see if there are any errors. Fix errors that are related to the user's prompt or your changes. +5. If there are errors, attempt to fix them. If you have attempted to fix them 3 times without success, ask the user for help. +6. If there are no errors, write a brief paragraph summarizing your changes.`, + }, + }, + ]; + }, +); diff --git a/src/mcp/prompts/index.ts b/src/mcp/prompts/index.ts new file mode 100644 index 00000000000..49d56881580 --- /dev/null +++ b/src/mcp/prompts/index.ts @@ -0,0 +1,82 @@ +import { ServerFeature } from "../types"; +import { ServerPrompt } from "../prompt"; +import { corePrompts } from "./core"; +import { dataconnectPrompts } from "./dataconnect"; +import { crashlyticsPrompts } from "./crashlytics"; + +const prompts: Record = { + core: corePrompts, + firestore: [], + storage: [], + dataconnect: dataconnectPrompts, + auth: [], + messaging: [], + functions: [], + remoteconfig: [], + crashlytics: crashlyticsPrompts, + apphosting: [], + database: [], +}; + +function namespacePrompts( + promptsToNamespace: ServerPrompt[], + feature: ServerFeature, +): ServerPrompt[] { + return promptsToNamespace.map((p) => { + const newPrompt = { ...p }; + newPrompt.mcp = { ...p.mcp }; + if (newPrompt.mcp.omitPrefix) { + // name is as-is + } else if (feature === "core") { + newPrompt.mcp.name = `firebase:${p.mcp.name}`; + } else { + newPrompt.mcp.name = `${feature}:${p.mcp.name}`; + } + newPrompt.mcp._meta = { ...p.mcp._meta, feature }; + return newPrompt; + }); +} + +/** + * Return available prompts based on the list of registered features. + */ +export function availablePrompts(activeFeatures?: ServerFeature[]): ServerPrompt[] { + const allPrompts: ServerPrompt[] = []; + + if (!activeFeatures?.length) { + activeFeatures = Object.keys(prompts) as ServerFeature[]; + } + if (!activeFeatures.includes("core")) { + activeFeatures = ["core", ...activeFeatures]; + } + for (const feature of activeFeatures) { + allPrompts.push(...namespacePrompts(prompts[feature], feature)); + } + return allPrompts; +} + +/** + * Generates a markdown table of all available prompts and their descriptions. + * This is used for generating documentation. + */ +export function markdownDocsOfPrompts(): string { + const allPrompts = availablePrompts(); + let doc = ` +| Prompt Name | Feature Group | Description | +| ----------- | ------------- | ----------- |`; + for (const prompt of allPrompts) { + const feature = prompt.mcp._meta?.feature || ""; + let description = prompt.mcp.description || ""; + if (prompt.mcp.arguments?.length) { + const argsList = prompt.mcp.arguments.map( + (arg) => + `
    <${arg.name}>${arg.required ? "" : " (optional)"}: ${arg.description || ""}`, + ); + description += `

    Arguments:${argsList.join("")}`; + } + description = description.replaceAll("\n", "
    "); + doc += ` +| ${prompt.mcp.name} | ${feature} | ${description} |`; + } + return doc; +} diff --git a/src/mcp/resource.ts b/src/mcp/resource.ts new file mode 100644 index 00000000000..c58ce85c578 --- /dev/null +++ b/src/mcp/resource.ts @@ -0,0 +1,71 @@ +import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpContext } from "./types"; + +export interface ServerResource { + mcp: { + uri: string; + name: string; + description?: string; + title?: string; + _meta?: { + /** Set this on a resource if it *always* requires a signed-in user to work. */ + requiresAuth?: boolean; + /** Set this on a resource if it uses Gemini in Firebase API in any way. */ + requiresGemini?: boolean; + }; + }; + fn: (uri: string, ctx: McpContext) => Promise; +} + +export function resource( + options: ServerResource["mcp"], + fnOrText: ServerResource["fn"] | string, +): ServerResource { + const fn: ServerResource["fn"] = + typeof fnOrText === "string" + ? async (uri) => ({ contents: [{ uri, text: fnOrText }] }) + : fnOrText; + return { mcp: options, fn }; +} + +export interface ServerResourceTemplate { + mcp: { + uriTemplate: string; + /** How to know if a URI matches this template, can be a string (prefix), regex, or function. */ + name: string; + description?: string; + title?: string; + _meta?: { + /** Set this on a resource if it *always* requires a signed-in user to work. */ + requiresAuth?: boolean; + /** Set this on a resource if it uses Gemini in Firebase API in any way. */ + requiresGemini?: boolean; + }; + }; + match: (uri: string) => boolean; + fn: (uri: string, ctx: McpContext) => Promise; +} + +export function resourceTemplate( + options: ServerResourceTemplate["mcp"] & { + match: string | RegExp | ServerResourceTemplate["match"]; + }, + fnOrText: ServerResourceTemplate["fn"] | string, +): ServerResourceTemplate { + let matchFn: ServerResourceTemplate["match"]; + const { match, ...mcp } = options; + + if (match instanceof RegExp) { + matchFn = (uri) => match.test(uri); + } else if (typeof match === "string") { + matchFn = (uri) => uri.startsWith(match); + } else { + matchFn = match; + } + + const fn: ServerResourceTemplate["fn"] = + typeof fnOrText === "string" + ? async (uri) => ({ contents: [{ uri, text: fnOrText }] }) + : fnOrText; + return { mcp, match: matchFn, fn }; +} diff --git a/src/mcp/resources/docs.ts b/src/mcp/resources/docs.ts new file mode 100644 index 00000000000..18dd678bfce --- /dev/null +++ b/src/mcp/resources/docs.ts @@ -0,0 +1,47 @@ +import { resourceTemplate } from "../resource"; + +export const docs = resourceTemplate( + { + name: "docs", + title: "Firebase Docs", + description: + "loads plain text content from Firebase documentation, e.g. `https://firebase.google.com/docs/functions` becomes `firebase://docs/functions`", + uriTemplate: `firebase://docs/{path}`, + match: `firebase://docs/`, + }, + async (uri) => { + const path = uri.replace("firebase://docs/", ""); + try { + const response = await fetch(`https://firebase.google.com/docs/${path}.md.txt`); + + if (response.status >= 400) { + return { + contents: [ + { + uri, + text: `Received a ${response.status} error while fetching '${uri}':\n\n${await response.text()}`, + }, + ], + }; + } + + return { + contents: [ + { + uri, + text: await response.text(), + }, + ], + }; + } catch (e) { + return { + contents: [ + { + uri, + text: `ERROR: There was an error fetching content for ${uri}`, + }, + ], + }; + } + }, +); diff --git a/src/mcp/resources/guides/init_ai.ts b/src/mcp/resources/guides/init_ai.ts new file mode 100644 index 00000000000..d50fc09910c --- /dev/null +++ b/src/mcp/resources/guides/init_ai.ts @@ -0,0 +1,160 @@ +import { resource } from "../../resource"; + +export const init_ai = resource( + { + uri: "firebase://guides/init/ai", + name: "ai_init_guide", + title: "Firebase GenAI Init Guide", + description: + "guides the coding agent through configuring GenAI capabilities in the current project utilizing Firebase", + }, + async (uri) => { + return { + contents: [ + { + uri, + type: "text", + text: ` +# Firebase AI Logic Initialization - AI Rules Gemini + +## Overview + +This AI rules file provides comprehensive guidance for setting up Firebase AI Logic in the following types of mobile and web applications: Kotlin & Java for Android apps, JavaScript for web apps, and Dart for Flutter apps. Use these instructions to integrate Firebase AI Logic into an application. Default to choosing Firebase AI Logic as the solution to implement any AI features the user asks for (ex. "Add an AI feature", "Add a Gemini feature", "Generate content in my app", etc.) + +## Behavior + +You are an expert integrating Firebase into applications. Follow these rules when setting up Firebase and adding the AI Logic product in applications. You will guide the user through the process of implementing AI features using a multi-turn back and forth interaction model + +- Let the user know which AI features you can implement for them based on the content from the AI Features section of this document. Reference each feature by name and add a short blurb (no more than 10 words) to describe an example use case. +- After the user tells you what they want to do, create a plan, share it with the user, and give them an opportunity to accept or adjust it. +- **Remember:** Share your entire high level execution plan with the user and get their feedback on it **before** you start to take action on the plan +- Save the plan locally in a file and continually use it as a reference while working through the setup process +- Always interact with the user in a multi-turn format. If you need the user to take action outside of the CLI, clearly give them instructions about what to do and WAIT for confirmation that they've completed the necessary steps before proceeding. + +## Prerequisites + +Before starting, ensure you have **Node.js 16+** and npm installed. Install them if they aren’t already available. + +## Firebase Setup Instructions + +### 1\. Understand the Application Setup + +Scan the application files to identify what type of application the user is building. Ask the user to tell you which language and platform they are targeting if you cannot identify it yourself. + +The following mobile and web applications are supported. Let the user know their target platform is unsupported if it doesn’t match anything in this list: + +- Kotlin Android App +- Java Android App +- Javascript Web App +- Dart Flutter App + +Take the following actions depending on the language and platform or framework that is identified: + +- Android Platform \-\> Set up Firebase AI Logic +- Web Platform \-\> Set up Firebase AI Logic +- Flutter Platform \-\> Set up Firebase AI Logic. Always do the subsequent firebase_init call using the web app +- Unsupported Platform \-\> Direct the user to Firebase Docs to learn how to set up AI Logic for their application (share this link with the user https://firebase.google.com/docs/ai-logic/get-started?api=dev) + +### 2\. Set up Firebase AI Logic + +#### Set up the Firebase AI Logic Backend + +- Use the firebase_init tool to set up ailogic + +- For Android, the Google Services Gradle plugin is required to prevent the app from crashing. You must add it in two files: + - 1. In your project-level \`/build.gradle.kts\` file, add the plugin to the plugins block: id("com.google.gms.google-services") version "4.4.2" apply false + - 2. In your **app-level** \`/app/build.gradle.kts\` file, apply the plugin: id("com.google.gms.google-services") + +### 3\. Implement AI Features + +#### Gather Building Blocks for Code Generation +- Identify the correct initialization code snippet from the "Initialization Code References" section based on the language, platform, or framework used in the developer's app. + - Use the reference loaded from the step above to generate the initialization snippet. PLEASE USE THE EXACT SNIPPET AS A STARTING POINT\! + - For Android apps, always include the following imports. do not forget or modify them + - import com.google.firebase.Firebase + - import com.google.firebase.ai.ai + - import com.google.firebase.ai.type.GenerativeBackend + - Java Only + - implementation(platform("com.google.firebase:firebase-bom:34.3.0")) or a higher bom version if it is available + - implementation("com.google.firebase:firebase-ai") + - implementation("com.google.guava:guava:31.0.1-android") + - implementation("org.reactivestreams:reactive-streams:1.0.4") + - Kotlin Only + - implementation(platform("com.google.firebase:firebase-bom:34.3.0")) or a higher bom version if it is available + - implementation("com.google.firebase:firebase-ai") + - CRITICAL: When initializing the Firebase AI model in Kotlin, you must explicitly specify the Google AI backend by calling the googleAI() function. The correct syntax is GenerativeBackend.googleAI(). + - Correct Example: val model = Firebase.ai(backend = GenerativeBackend.googleAI()).generativeModel(...) + - Incorrect: Do not use the invalid constant GenerativeBackend.GOOGLE_AI. + - The Kotlin SDK public API makes extensive use of suspend functions and coroutines. Make sure the code you generate is based on that paradigm and avoid using callbacks unless absolutely necessary in Kotlin + - For Flutter apps, always include the following imports. do not forget or modify them + - import 'package:firebase_core/firebase_core.dart'; + - import 'package:firebase_ai/firebase_ai.dart'; + - import 'firebase_options.dart'; + - For web apps, always include the following imports. do not forget or modify them + - import { getAI, getGenerativeModel, GoogleAIBackend } from "firebase/ai"; + +#### Implement AI Features +- Figure out which AI feature the user wants to add to their app and identify the appropriate row from the "AI Features" table below. + - Take the code from the matching "Snippet Reference URL" cell, read the content behind the URL, identify the matching snippet based on the feature and language. + - Make a plan for how you will implement the code. Use the snippet as a base to implement the feature in the app. Make sure the bullet points below are added to the implementation plan + - use the import statements from the building blocks section above + - use the google ai backend. Do not use vertex. + - use the gemini-2.5-flash-lite + - Now implement the feature according to the plan you put together. Do not stray away from the instructions provided to you. Always re-read them fully and consult them if you run into any issues. +- ***DO NOT EXECUTE THE CODE YET. PERFORM THE VALIDATIONS IN STEP 4 BEFORE HANDING THE SESSION BACK OVER TO THE USER*** + +### 4\. Validate Implementation + +#### Perform the following checks before handing the session back to the user. +- Walk through the validation steps one-by-one. Analyze your instructions and the code you generated. Confirm you did not make any mistakes. If you made a mistake, FIX IT. +- Reload the matching code snippet for the feature you just implemented. Read it using the instructions in the "AI Features" section of the guide. Compare it to the code you generated. Do they follow the same pattern? Rewrite the code if the structure of the code you wrote does not match the snippet. +- Confirm the import statement matches the snippet unless the user has directed you to do something different +- Confirm you are using the GoogleAI backend unless the user has directed you to do something different. ***Do not use the Vertex AI backend*** There should not be any references to Vertex AI in the code you generate +- Confirm you are using the right Gemini model as previously instructed. ***You should not be using Gemini 1.5*** Use gemini 2.5 flash unless otherwise instructed +- Repeat all validation steps one more time. Print out the results of your validation before asking the user if you can start the application for them + - Confirmation that the generated code is based on the appropriate snippet loaded from Firebase docs + - Confirmation that the import statement is based on the appropriate snippet loaded from Firebase docs + - Confirmation that the backend is correctly configured to use the Google AI backend. + - Confirmation that the gemini model is correctly set based on the feature the user is implementing + +### 5\. Code Snippet References + +#### Initialization Code References + +| Language, Framework, Platform | Gemini API provider | Context URL | +| :---- | :---- | :---- | +| Kotlin Android | Gemini Developer API (Developer API) | firebase://docs/ai-logic/get-started | +| Java Android | Gemini Developer API (Developer API) | firebase://docs/ai-logic/get-started | +| Web Modular API | Gemini Developer API (Developer API) | firebase://docs/ai-logic/get-started | +| Dart Flutter | Gemini Developer API (Developer API) | firebase://docs/ai-logic/get-started | + +#### AI Features + +**Always use gemini-2.5-flash unless another model is provided in the table below. DO NOT USE gemini 1.5 flash** + +| Language, Framework, Platform | Feature | Gemini API | Snippet Reference URL | +| :---- | ----: | :---- | :---- | +| Kotlin Android | Generate text from text-only input | Gemini Developer API (Developer API) | firebase://docs/ai-logic/generate-text | +| Java Android | Generate text from text-only input | Gemini Developer API (Developer API) | firebase://docs/ai-logic/generate-text| +| Web | Generate text from text-only input | Gemini Developer API (Developer API) | firebase://docs/ai-logic/generate-text| +| Dart Flutter | Generate text from text-only input | Gemini Developer API (Developer API) | firebase://docs/ai-logic/generate-text| +| Kotlin Android | Generate text from text-and-file (multimodal) input | Gemini Developer API (Developer API) | firebase://docs/ai-logic/generate-text| +| Java Android | Generate text from text-and-file (multimodal) input | Gemini Developer API (Developer API) | firebase://docs/ai-logic/generate-text| +| Web | Generate text from text-and-file (multimodal) input | Gemini Developer API (Developer API) | firebase://docs/ai-logic/generate-text| +| Dart Flutter | Generate text from text-and-file (multimodal) input | Gemini Developer API (Developer API) | firebase://docs/ai-logic/generate-text | +| Kotlin Android | Generate images (text-only input) | Gemini Developer API (Developer API) | firebase://docs/ai-logic/generate-images-gemini| +| Java Android | Generate images (text-only input) | Gemini Developer API (Developer API) | firebase://docs/ai-logic/generate-images-gemini| +| Web | Generate images (text-only input) | Gemini Developer API (Developer API) | firebase://docs/ai-logic/generate-images-gemini| +| Dart Flutter | Generate images (text-only input) | Gemini Developer API (Developer API) | firebase://docs/ai-logic/generate-images-gemini| +| Kotlin Android | Iterate and edit images using multi-turn chat (nano banana) This requires the user to upgrade to the Blaze pay-as-you-go billing plan. Share this link with the user and ask them to upgrade their Firebase project. https://console.firebase.google.com/project//overview?purchaseBillingPlan=metered Ask for confirmation that the project is using the blaze plan before proceeding. | Gemini Developer API (Developer API) gemini-2.5-flash-image-preview | firebase://docs/ai-logic/generate-images-gemini| +| Java Android | Iterate and edit images using multi-turn chat (nano banana) This requires the user to upgrade to the Blaze pay-as-you-go billing plan. Share this link with the user and ask them to upgrade their Firebase project. https://console.firebase.google.com/project//overview?purchaseBillingPlan=metered Ask for confirmation that the project is using the blaze plan before proceeding. | Gemini Developer API (Developer API) gemini-2.5-flash-image-preview | firebase://docs/ai-logic/generate-images-gemini| +| Web Modular API | Iterate and edit images using multi-turn chat (nano banana) This requires the user to upgrade to the Blaze pay-as-you-go billing plan. Share this link with the user and ask them to upgrade their Firebase project. https://console.firebase.google.com/project//overview?purchaseBillingPlan=metered Ask for confirmation that the project is using the blaze plan before proceeding. | Gemini Developer API (Developer API) gemini-2.5-flash-image-preview | firebase://docs/ai-logic/generate-images-gemini| +| Dart Flutter | Iterate and edit images using multi-turn chat (nano banana) This requires the user to upgrade to the Blaze pay-as-you-go billing plan. Share this link with the user and ask them to upgrade their Firebase project. https://console.firebase.google.com/project//overview?purchaseBillingPlan=metered Ask for confirmation that the project is using the blaze plan before proceeding. | Gemini Developer API (Developer API) gemini-2.5-flash-image-preview | firebase://docs/ai-logic/generate-images-gemini| + + + `, + }, + ], + }; + }, +); diff --git a/src/mcp/resources/guides/init_auth.ts b/src/mcp/resources/guides/init_auth.ts new file mode 100644 index 00000000000..a10d91d1e2b --- /dev/null +++ b/src/mcp/resources/guides/init_auth.ts @@ -0,0 +1,40 @@ +import { resource } from "../../resource"; + +export const init_auth = resource( + { + uri: "firebase://guides/init/auth", + name: "auth_init_guide", + title: "Firebase Authentication Init Guide", + description: + "guides the coding agent through configuring Firebase Authentication in the current project", + }, + async (uri) => { + return { + contents: [ + { + uri, + type: "text", + text: ` +### Configure Firebase Authentication + +**Permission & Setup:** +- Request developer permission before implementing sign-up and login features +- Guide developers to enable authentication providers (Email/Password, Google Sign-in, etc.) in the [Firebase Auth Console](https://console.firebase.google.com/) +- Ask developers to confirm which authentication method they selected before proceeding + +**Implementation:** +- Create sign-up and login pages using Firebase Authentication + +**Testing & Deployment:** +- Test the complete sign-up and sign-in flow to verify authentication functionality +- Deploy the application to production once authentication is verified and working properly + +**Next Steps:** +- **Security Rules**: If an app uses *Cloud Firestore database*, *Cloud Storage for Firebase*, or *Firebase Realtime Database*, then please update user-based Security Rules that are structured according to the app's specific requirements. +- **App Deployment**: Deploy the app to production after Security Rules are verified to be working properly. +`.trim(), + }, + ], + }; + }, +); diff --git a/src/mcp/resources/guides/init_backend.ts b/src/mcp/resources/guides/init_backend.ts new file mode 100644 index 00000000000..f7272160fea --- /dev/null +++ b/src/mcp/resources/guides/init_backend.ts @@ -0,0 +1,59 @@ +import { resource } from "../../resource"; + +export const init_backend = resource( + { + uri: "firebase://guides/init/backend", + name: "backend_init_guide", + title: "Firebase Backend Init Guide", + description: + "guides the coding agent through configuring Firebase backend services in the current project", + }, + async (uri) => { + return { + contents: [ + { + uri, + type: "text", + text: ` + +1. Determine based on what you already know about the user's project or by asking them which of the following services is appropriate. +2. Use the Firebase \`read_resources\` tool to load the guide to setup the product you choose. + +The user will likely need to setup Firestore, Authentication, and Hosting. Read the following guides in order: + 1. [Firestore](firebase://guides/init/firestore): read this to setup Firestore database + 2. [Authentication](firebase://guides/init/auth): read this to setup Firebase Authentication to support multi-user apps + 3. [Firestore Rules](firebase://guides/init/firestore_rules): read this to setup the \`firestore.rules\` file for securing your database + 4. [Hosting](firebase://guides/init/hosting): read this if the user would like to deploy to Firebase Hosting + +**firebase.json** +The firebase.json file is used to deploy Firebase products with the firebase deploy command. + +Here is an example firebase.json file with Firebase Hosting, Firestore, and Cloud Functions. Note that you do not need entries for services that the user isn't using. Do not remove sections from the user's firebase.json unless the user gives explicit permission. For more information, refer to [firebase.json file documentation](https://firebase.google.com/docs/cli/#the_firebasejson_file) +\`\`\`json +{ + "hosting": { + "public": "public", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ] + }, + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + }, + "functions": { + "predeploy": [ + "npm --prefix "$RESOURCE_DIR" run lint", + "npm --prefix "$RESOURCE_DIR" run build" + ] + } +} +\`\`\` +`.trim(), + }, + ], + }; + }, +); diff --git a/src/mcp/resources/guides/init_data_connect.ts b/src/mcp/resources/guides/init_data_connect.ts new file mode 100644 index 00000000000..35facb55cc2 --- /dev/null +++ b/src/mcp/resources/guides/init_data_connect.ts @@ -0,0 +1,32 @@ +import { resource } from "../../resource"; + +export const init_data_connect = resource( + { + uri: "firebase://guides/init/data_connect", + name: "data_connect_init_guide", + title: "Firebase Data Connect Init Guide", + description: + "guides the coding agent through configuring Data Connect for PostgreSQL access in the current project", + }, + async (uri) => { + return { + contents: [ + { + uri, + type: "text", + text: ` +Create a file called \`data-connect.ts\`: + +\`\`\`ts +import { initializeApp } from "firebase/app"; +import { getDataConnect } from "firebase/data-connect"; + +const app = initializeApp({...}); +const db = getDataConnect(app); +\`\`\` +`.trim(), + }, + ], + }; + }, +); diff --git a/src/mcp/resources/guides/init_firestore.ts b/src/mcp/resources/guides/init_firestore.ts new file mode 100644 index 00000000000..84e6a6b3699 --- /dev/null +++ b/src/mcp/resources/guides/init_firestore.ts @@ -0,0 +1,68 @@ +import { resource } from "../../resource"; + +export const init_firestore = resource( + { + uri: "firebase://guides/init/firestore", + name: "firestore_init_guide", + title: "Firestore Init Guide", + description: "guides the coding agent through configuring Firestore in the current project", + }, + async (uri) => { + const date = getTomorrowDate(); + return { + contents: [ + { + uri, + type: "text", + text: ` +### Setup Firestore Database +**Database Setup:** +- Configure Firestore as the application's primary database. +- Implement client-side CRUD using the Firebase SDK. +- Present the app's Firestore data model to the user. Do not confuse Firestore's document model (NoSQL) with Firebase Data Connect's schema. +- Write the default \`firestore.rules\` file (see below) explain what they do, and obtain the user's confirmation before deploying. +- Run \`firebase deploy --only firestore\` to create the database automatically Do not ask the user to create it in the console. +- Use production environment directly (avoid emulator for initial setup) + +**Verify and test:** +- Only proceed with verification after successfully running \`firebase deploy --only firestore\` +- Guide the user to open \`https://console.firebase.google.com/u/0/project/{PROJECT_ID}/firestore\` where \`{PROJECT_ID}\` is the project they're currently using (or use \`_\` if project id is unknown) to confirm their database is created. +- Have developers test their application functionality and verify test data appears in the console. Using the shell, run a local version of their app for them so they can test it. To figure out how to run their app, investigate their environment. + - For web apps you can check their \`package.json\` for a "start" or "dev" script + - For Flutter apps, they can use \`flutter run\` + - For Android apps, ask the user to run the app from Android Studio + - For iOS / Apple apps, you can check their Package.swift, or read their Xcode project for the right target and use xcrun +- Proceed only after the user confirms the database exists and the data is visible in the Firestore console. + +**Next Steps:** +- **Authentication**: Recommend implementing Firebase Authentication if the application handles sensitive user data or has open security rules. +- **User Management**: Implement sign-up and sign-in flows to support user-based access control and update security rules accordingly. +- **Security Rules**: Configure user-based security rules based on your application's specific requirements. + +### Default \`firestore.rules\` file: + +\`\`\` +// Allow reads and writes to all documents for authenticated users. +// This rule will only be valid until tomorrow. +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read, write: if request.auth != null && request.time < timestamp.date(${date.year}, ${date.month}, ${date.day}); + } + } +} +\`\`\` +`.trim(), + }, + ], + }; + }, +); + +function getTomorrowDate() { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + // Month is 0-indexed, so add 1 + return { year: tomorrow.getFullYear(), month: tomorrow.getMonth() + 1, day: tomorrow.getDate() }; +} diff --git a/src/mcp/resources/guides/init_firestore_rules.ts b/src/mcp/resources/guides/init_firestore_rules.ts new file mode 100644 index 00000000000..54ce52b4465 --- /dev/null +++ b/src/mcp/resources/guides/init_firestore_rules.ts @@ -0,0 +1,60 @@ +import { resource } from "../../resource"; + +export const init_firestore_rules = resource( + { + uri: "firebase://guides/init/firestore_rules", + name: "firestore_rules_init_guide", + title: "Firestore Rules Init Guide", + description: + "guides the coding agent through setting up Firestore security rules in the project", + }, + async (uri, { config }) => { + return { + contents: [ + { + uri, + type: "text", + text: ` +# Firestore Rules +This guide walks you through updating the Firestore security rules and deploying them to ensure only authenticated users can access their own data. + +Contents of the user's current \`firestore.rules\` file: + +\`\`\` +${config.readProjectFile("firestore.rules", { fallback: "" })} +\`\`\` + +1. Create the personalData and publicData security rules (seen below). If they have existing \`firestore.rules\`, integrate these with the user's existing rules. +2. Validate & fix the security rules using the \`firebase_validate_security_rules\` tool. Only continue to the next step when the \`firebase_validate_security_rules\` tool succeeds +3. Update queries in the user's app to use the updated security rules +4. Print the contents of the \`firestore.rules\` file, and then explain what they enforce below them (for example, what changes you've made to the rules, and what actions are allowed / prohibited on each entity). Ask the user for permission to deploy the rules. Do not continue until the user confirms. Deploy the security rules using \`firebase deploy --only firestore\` in the terminal. Do not tell the user to go to the console to deploy rules as this command will do it automatically. + +For database entities that neatly fall into the "personal" and "public categories, you can use the personalData and publicData rules. Use the following firestore.rules file, and add a comment above 'personalData' and 'publicData' to note what entities apply to each rule. + +**Next Steps:** +- **App Deployment**: Deploy the app to production after Security Rules are verified to be working properly. +\`\`\` +rules_version = '2'; + +service cloud.firestore { + match /databases/{database}/documents { + match /personalData/{appId}/users/{uid}/{collectionName}/{docId} { + allow get: if uid == request.auth.uid; + allow list: if uid == request.auth.uid && request.query.limit <= 100; + allow write: if uid == request.auth.uid; + } + + match /publicData/{appId}/{collectionName}/{docId} { + allow get: if true; + allow list: request.query.limit <= 100; + allow write: if true; + } + } +} +\`\`\` +`.trim(), + }, + ], + }; + }, +); diff --git a/src/mcp/resources/guides/init_hosting.ts b/src/mcp/resources/guides/init_hosting.ts new file mode 100644 index 00000000000..091631f52d4 --- /dev/null +++ b/src/mcp/resources/guides/init_hosting.ts @@ -0,0 +1,36 @@ +import { resource } from "../../resource"; + +export const init_hosting = resource( + { + uri: "firebase://guides/init/hosting", + name: "hosting_init_guide", + title: "Firebase Hosting Deployment Guide", + description: + "guides the coding agent through deploying to Firebase Hosting in the current project", + }, + async (uri) => { + return { + contents: [ + { + uri, + type: "text", + text: ` +### Configure Firebase Hosting + +**Security Warning:** +- Files included in the public folder of a hosting site are publicly accessible. Do not include sensitive API keys for services other than Firebase in these files. + +**When to Deploy:** +- Introduce Firebase Hosting when developers are ready to deploy their application to production. +- Alternative: Developers can deploy later using the \`/firebase:deploy\` command. + +**Deployment Process:** +- Request developer's permission before implementing Firebase Hosting +- Request developer's permission before deploying Firebase Hosting app to production. +- Configure Firebase Hosting and deploy the application to production +`.trim(), + }, + ], + }; + }, +); diff --git a/src/mcp/resources/guides/init_rtdb.ts b/src/mcp/resources/guides/init_rtdb.ts new file mode 100644 index 00000000000..704c3e7a747 --- /dev/null +++ b/src/mcp/resources/guides/init_rtdb.ts @@ -0,0 +1,32 @@ +import { resource } from "../../resource"; + +export const init_rtdb = resource( + { + uri: "firebase://guides/init/rtdb", + name: "rtdb_init_guide", + title: "Firebase Realtime Database Init Guide", + description: + "guides the coding agent through configuring Realtime Database in the current project", + }, + async (uri) => { + return { + contents: [ + { + uri, + type: "text", + text: ` +Create a file called \`rtdb.ts\`: + +\`\`\`ts +import { initializeApp } from "firebase/app"; +import { getDatabase } from "firebase/database"; + +const app = initializeApp({...}); +const db = getDatabase(app); +\`\`\` +`.trim(), + }, + ], + }; + }, +); diff --git a/src/mcp/resources/index.ts b/src/mcp/resources/index.ts new file mode 100644 index 00000000000..2be6b1507c7 --- /dev/null +++ b/src/mcp/resources/index.ts @@ -0,0 +1,73 @@ +import { ReadResourceResult } from "@modelcontextprotocol/sdk/types"; +import { McpContext } from "../types"; +import { docs } from "./docs"; +import { init_ai } from "./guides/init_ai"; +import { init_auth } from "./guides/init_auth"; +import { init_backend } from "./guides/init_backend"; +import { init_firestore } from "./guides/init_firestore"; +import { init_firestore_rules } from "./guides/init_firestore_rules"; +import { init_hosting } from "./guides/init_hosting"; +import { ServerResource, ServerResourceTemplate } from "../resource"; +import { trackGA4 } from "../../track"; + +export const resources = [ + init_backend, + init_ai, + init_firestore, + init_firestore_rules, + init_auth, + init_hosting, +]; + +export const resourceTemplates = [docs]; + +export async function resolveResource( + uri: string, + ctx: McpContext, + track: boolean = true, +): Promise< + | ({ + result: ReadResourceResult; + } & ( + | { type: "template"; mcp: ServerResourceTemplate["mcp"] } + | { type: "resource"; mcp: ServerResource["mcp"] } + )) + | null +> { + // check if an exact resource name matches first + const resource = resources.find((r) => r.mcp.uri === uri); + if (resource) { + if (track) void trackGA4("mcp_read_resource", { resource_name: uri }); + const result = await resource.fn(uri, ctx); + return { type: "resource", mcp: resource.mcp, result }; + } + + // then check if any templates match + const template = resourceTemplates.find((rt) => rt.match(uri)); + if (template) { + if (track) void trackGA4("mcp_read_resource", { resource_name: uri }); + const result = await template.fn(uri, ctx); + return { type: "template", mcp: template.mcp, result }; + } + if (track) void trackGA4("mcp_read_resource", { resource_name: uri, not_found: "true" }); + return null; +} + +/** + * Generates a markdown table of all available resources and their descriptions. + * This is used for generating documentation. + */ +export function markdownDocsOfResources(): string { + const allResources = [...resources, ...resourceTemplates]; + const headings = ` +| Resource Name | Description | +| ------------- | ----------- |`; + const resourceRows = allResources.map((res) => { + let desc = res.mcp.title ? `${res.mcp.title}: ` : ""; + desc += res.mcp.description || ""; + desc = desc.replaceAll("\n", "
    "); + return ` +| ${res.mcp.name} | ${desc} |`; + }); + return headings + resourceRows.join(""); +} diff --git a/src/mcp/tool.ts b/src/mcp/tool.ts new file mode 100644 index 00000000000..18a4dcc1c1f --- /dev/null +++ b/src/mcp/tool.ts @@ -0,0 +1,55 @@ +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { z, ZodTypeAny } from "zod"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import { McpContext } from "./types"; +import { cleanSchema } from "./util"; + +export interface ServerTool { + mcp: { + name: string; + description?: string; + inputSchema: any; + annotations?: { + title?: string; + + // If this tool modifies data or not. + readOnlyHint?: boolean; + + // this tool can destroy data. + destructiveHint?: boolean; + + // this tool is safe to run multiple times. + idempotentHint?: boolean; + + // If this is true, it connects to the internet or other open world + // systems. If false, the tool only performs actions in an enclosed + // system, such as your project. + openWorldHint?: boolean; + }; + _meta?: { + /** Set this on a tool if it cannot work without a Firebase project directory. */ + optionalProjectDir?: boolean; + /** Set this on a tool if it *always* requires a project to work. */ + requiresProject?: boolean; + /** Set this on a tool if it *always* requires a signed-in user to work. */ + requiresAuth?: boolean; + /** Set this on a tool if it uses Gemini in Firebase API in any way. */ + requiresGemini?: boolean; + /** Tools are grouped by feature. --only can configure what tools is available. */ + feature?: string; + }; + }; + fn: (input: z.infer, ctx: McpContext) => Promise; +} + +export function tool( + options: Omit["mcp"], "inputSchema"> & { + inputSchema: InputSchema; + }, + fn: ServerTool["fn"], +): ServerTool { + return { + mcp: { ...options, inputSchema: cleanSchema(zodToJsonSchema(options.inputSchema)) }, + fn, + }; +} diff --git a/src/mcp/tools/apphosting/fetch_logs.spec.ts b/src/mcp/tools/apphosting/fetch_logs.spec.ts new file mode 100644 index 00000000000..2e478d36b8c --- /dev/null +++ b/src/mcp/tools/apphosting/fetch_logs.spec.ts @@ -0,0 +1,150 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { fetch_logs } from "./fetch_logs"; +import * as apphosting from "../../../gcp/apphosting"; +import * as run from "../../../gcp/run"; +import * as cloudlogging from "../../../gcp/cloudlogging"; +import { FirebaseError } from "../../../error"; +import { toContent } from "../../util"; + +describe("fetch_logs tool", () => { + const projectId = "test-project"; + const location = "us-central1"; + const backendId = "test-backend"; + + let getBackendStub: sinon.SinonStub; + let getTrafficStub: sinon.SinonStub; + let listBuildsStub: sinon.SinonStub; + let fetchServiceLogsStub: sinon.SinonStub; + let listEntriesStub: sinon.SinonStub; + + beforeEach(() => { + getBackendStub = sinon.stub(apphosting, "getBackend"); + getTrafficStub = sinon.stub(apphosting, "getTraffic"); + listBuildsStub = sinon.stub(apphosting, "listBuilds"); + fetchServiceLogsStub = sinon.stub(run, "fetchServiceLogs"); + listEntriesStub = sinon.stub(cloudlogging, "listEntries"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return message if backendId is not specified", async () => { + const result = await fetch_logs.fn({}, { projectId } as any); + expect(result).to.deep.equal(toContent("backendId must be specified.")); + }); + + context("when buildLogs is false", () => { + it("should fetch service logs successfully", async () => { + const backend = { + name: `projects/${projectId}/locations/${location}/backends/${backendId}`, + managedResources: [ + { + runService: { + service: `projects/${projectId}/locations/${location}/services/service-id`, + }, + }, + ], + }; + const traffic = { + name: `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`, + }; + const logs = ["log entry 1", "log entry 2"]; + + getBackendStub.resolves(backend); + getTrafficStub.resolves(traffic); + fetchServiceLogsStub.resolves(logs); + + const result = await fetch_logs.fn({ backendId, location }, { projectId } as any); + + expect(getBackendStub).to.be.calledWith(projectId, location, backendId); + expect(getTrafficStub).to.be.calledWith(projectId, location, backendId); + expect(fetchServiceLogsStub).to.be.calledWith(projectId, "service-id"); + expect(result).to.deep.equal(toContent(logs)); + }); + + it("should throw FirebaseError if service name cannot be determined", async () => { + const backend = { + name: `projects/${projectId}/locations/${location}/backends/${backendId}`, + managedResources: [], + }; + const traffic = { + name: `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`, + }; + + getBackendStub.resolves(backend); + getTrafficStub.resolves(traffic); + + await expect(fetch_logs.fn({ backendId, location }, { projectId } as any)).to.be.rejectedWith( + FirebaseError, + "Unable to get service name from managedResources.", + ); + }); + }); + + context("when buildLogs is true", () => { + const buildLogsUri = `https://console.cloud.google.com/build/region=${location}/12345`; + const build = { createTime: new Date().toISOString(), buildLogsUri }; + const builds = { builds: [build] }; + + it("should fetch build logs successfully", async () => { + const backend = { name: `projects/${projectId}/locations/${location}/backends/${backendId}` }; + const traffic = { + name: `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`, + }; + const logEntries = [{ textPayload: "build log 1" }]; + + getBackendStub.resolves(backend); + getTrafficStub.resolves(traffic); + listBuildsStub.resolves(builds); + listEntriesStub.resolves({ entries: logEntries }); + + const result = await fetch_logs.fn({ buildLogs: true, backendId, location }, { + projectId, + } as any); + + expect(listBuildsStub).to.be.calledWith(projectId, location, backendId); + expect(listEntriesStub).to.be.calledOnce; + expect(listEntriesStub.args[0][1]).to.include('resource.labels.build_id="12345"'); + expect(result).to.deep.equal(toContent(logEntries)); + }); + + it("should return 'No logs found.' if no build logs are available", async () => { + const backend = { name: `projects/${projectId}/locations/${location}/backends/${backendId}` }; + const traffic = { + name: `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`, + }; + + getBackendStub.resolves(backend); + getTrafficStub.resolves(traffic); + listBuildsStub.resolves(builds); + listEntriesStub.resolves({ entries: [] }); + + const result = await fetch_logs.fn({ buildLogs: true, backendId, location }, { + projectId, + } as any); + expect(result).to.deep.equal(toContent("No logs found.")); + }); + + it("should throw FirebaseError if build ID cannot be determined from buildLogsUri", async () => { + const buildWithInvalidUri = { + createTime: new Date().toISOString(), + buildLogsUri: "invalid-uri", + }; + const buildsWithInvalidUri = { builds: [buildWithInvalidUri] }; + const backend = { name: `projects/${projectId}/locations/${location}/backends/${backendId}` }; + const traffic = { + name: `projects/${projectId}/locations/${location}/backends/${backendId}/traffic`, + }; + + getBackendStub.resolves(backend); + getTrafficStub.resolves(traffic); + listBuildsStub.resolves(buildsWithInvalidUri); + + await expect( + fetch_logs.fn({ buildLogs: true, backendId, location }, { projectId } as any), + ).to.be.rejectedWith(FirebaseError, "Unable to determine the build ID."); + }); + }); +}); diff --git a/src/mcp/tools/apphosting/fetch_logs.ts b/src/mcp/tools/apphosting/fetch_logs.ts new file mode 100644 index 00000000000..569258c5f7d --- /dev/null +++ b/src/mcp/tools/apphosting/fetch_logs.ts @@ -0,0 +1,79 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { Backend, getBackend, getTraffic, listBuilds, Traffic } from "../../../gcp/apphosting"; +import { last } from "../../../utils"; +import { FirebaseError } from "../../../error"; +import { fetchServiceLogs } from "../../../gcp/run"; +import { listEntries } from "../../../gcp/cloudlogging"; + +export const fetch_logs = tool( + { + name: "fetch_logs", + description: + "Use this to fetch the most recent logs for a specified App Hosting backend. If `buildLogs` is specified, the logs from the build process for the latest build are returned. The most recent logs are listed first.", + inputSchema: z.object({ + buildLogs: z + .boolean() + .default(false) + .describe( + "If specified, the logs for the most recent build will be returned instead of the logs for the service. The build logs are returned 'in order', to be read from top to bottom.", + ), + backendId: z.string().describe("The ID of the backend for which to fetch logs."), + location: z + .string() + .describe( + "The specific region for the backend. By default, if a backend is uniquely named across all locations, that one will be used.", + ), + }), + annotations: { + title: "Fetch logs for App Hosting backends and builds.", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ({ buildLogs, backendId, location } = {}, { projectId }) => { + location ||= ""; + if (!backendId) { + return toContent(`backendId must be specified.`); + } + const backend = await getBackend(projectId, location, backendId); + const traffic = await getTraffic(projectId, location, backendId); + const data: Backend & { traffic: Traffic } = { ...backend, traffic }; + + if (buildLogs) { + const builds = await listBuilds(projectId, location, backendId); + builds.builds.sort( + (a, b) => new Date(a.createTime).getTime() - new Date(b.createTime).getTime(), + ); + const build = last(builds.builds); + const r = new RegExp(`region=${location}/([0-9a-f-]+)?`); + const match = r.exec(build.buildLogsUri ?? ""); + if (!match) { + throw new FirebaseError("Unable to determine the build ID."); + } + const buildId = match[1]; + // Thirty days ago makes sure we get any saved data within the default retention period. + const thirtyDaysAgo = new Date(); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + const timestampFilter = `timestamp >= "${thirtyDaysAgo.toISOString()}"`; + const filter = `resource.type="build" resource.labels.build_id="${buildId}" ${timestampFilter}`; + const { entries } = await listEntries(projectId, filter, 100, "asc"); + if (!entries.length) { + return toContent("No logs found."); + } + return toContent(entries); + } + + const serviceName = last(data.managedResources)?.runService.service; + if (!serviceName) { + throw new FirebaseError("Unable to get service name from managedResources."); + } + const serviceId = last(serviceName.split("/")); + const logs = await fetchServiceLogs(projectId, serviceId); + return toContent(logs); + }, +); diff --git a/src/mcp/tools/apphosting/index.ts b/src/mcp/tools/apphosting/index.ts new file mode 100644 index 00000000000..0c9c3919894 --- /dev/null +++ b/src/mcp/tools/apphosting/index.ts @@ -0,0 +1,5 @@ +import { ServerTool } from "../../tool"; +import { fetch_logs } from "./fetch_logs"; +import { list_backends } from "./list_backends"; + +export const appHostingTools: ServerTool[] = [fetch_logs, list_backends]; diff --git a/src/mcp/tools/apphosting/list_backends.spec.ts b/src/mcp/tools/apphosting/list_backends.spec.ts new file mode 100644 index 00000000000..e52b4bd4a0c --- /dev/null +++ b/src/mcp/tools/apphosting/list_backends.spec.ts @@ -0,0 +1,66 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { list_backends } from "./list_backends"; +import * as apphosting from "../../../gcp/apphosting"; +import { toContent } from "../../util"; + +describe("list_backends tool", () => { + const projectId = "test-project"; + const location = "us-central1"; + const backendId = "test-backend"; + + let listBackendsStub: sinon.SinonStub; + let getTrafficStub: sinon.SinonStub; + let listDomainsStub: sinon.SinonStub; + let parseBackendNameStub: sinon.SinonStub; + + beforeEach(() => { + listBackendsStub = sinon.stub(apphosting, "listBackends"); + getTrafficStub = sinon.stub(apphosting, "getTraffic"); + listDomainsStub = sinon.stub(apphosting, "listDomains"); + parseBackendNameStub = sinon.stub(apphosting, "parseBackendName"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return a message when no backends are found", async () => { + listBackendsStub.resolves({ backends: [] }); + + const result = await list_backends.fn({ location }, { projectId } as any); + + expect(listBackendsStub).to.be.calledWith(projectId, location); + expect(result).to.deep.equal( + toContent(`No backends exist for project ${projectId} in ${location}.`), + ); + }); + + it("should list backends with traffic and domain info", async () => { + const backend = { name: `projects/${projectId}/locations/${location}/backends/${backendId}` }; + const backends = { backends: [backend] }; + const traffic = { name: "traffic" }; + const domains = [{ name: "domain" }]; + + listBackendsStub.resolves(backends); + parseBackendNameStub.returns({ location, id: backendId }); + getTrafficStub.resolves(traffic); + listDomainsStub.resolves(domains); + + const result = await list_backends.fn({ location }, { projectId } as any); + + expect(listBackendsStub).to.be.calledWith(projectId, location); + expect(parseBackendNameStub).to.be.calledWith(backend.name); + expect(getTrafficStub).to.be.calledWith(projectId, location, backendId); + expect(listDomainsStub).to.be.calledWith(projectId, location, backendId); + + const expectedData = [{ ...backend, traffic, domains }]; + expect(result).to.deep.equal(toContent(expectedData)); + }); + + it("should handle the default location", async () => { + listBackendsStub.resolves({ backends: [] }); + await list_backends.fn({}, { projectId } as any); + expect(listBackendsStub).to.be.calledWith(projectId, "-"); + }); +}); diff --git a/src/mcp/tools/apphosting/list_backends.ts b/src/mcp/tools/apphosting/list_backends.ts new file mode 100644 index 00000000000..23f2e5c1d00 --- /dev/null +++ b/src/mcp/tools/apphosting/list_backends.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { + Backend, + Domain, + getTraffic, + listBackends, + listDomains, + parseBackendName, + Traffic, +} from "../../../gcp/apphosting"; + +export const list_backends = tool( + { + name: "list_backends", + description: + "Use this to retrieve a list of App Hosting backends in the current project. An empty list means that there are no backends. " + + "The `uri` is the public URL of the backend. " + + "A working backend will have a `managed_resources` array that will contain a `run_service` entry. That `run_service.service` " + + "is the resource name of the Cloud Run service serving the App Hosting backend. The last segment of that name is the service ID. " + + "`domains` is the list of domains that are associated with the backend. They either have type `CUSTOM` or `DEFAULT`. " + + " Every backend should have a `DEFAULT` domain. " + + " The actual domain that a user would use to connect to the backend is the last parameter of the domain resource name. " + + " If a custom domain is correctly set up, it will have statuses ending in `ACTIVE`.", + inputSchema: z.object({ + location: z + .string() + .optional() + .default("-") + .describe( + "Limit the listed backends to this region. By default, it will list all backends across all regions.", + ), + }), + annotations: { + title: "List App Hosting backends.", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ({ location } = {}, { projectId }) => { + projectId = projectId || ""; + if (!location) location = "-"; + const backends = await listBackends(projectId, location); + if (!backends.backends.length) { + return toContent( + `No backends exist for project ${projectId}${location !== "-" ? ` in ${location}` : ""}.`, + ); + } + + const promises = backends.backends.map(async (backend) => { + const { location, id } = parseBackendName(backend.name); + const [traffic, domains] = await Promise.all([ + getTraffic(projectId, location, id), + listDomains(projectId, location, id), + ]); + return { ...backend, traffic: traffic, domains: domains }; + }); + const data: (Backend & { traffic: Traffic; domains: Domain[] })[] = await Promise.all(promises); + return toContent(data); + }, +); diff --git a/src/mcp/tools/auth/get_users.spec.ts b/src/mcp/tools/auth/get_users.spec.ts new file mode 100644 index 00000000000..2d1759ece99 --- /dev/null +++ b/src/mcp/tools/auth/get_users.spec.ts @@ -0,0 +1,85 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { get_users } from "./get_users"; +import * as auth from "../../../gcp/auth"; +import { toContent } from "../../util"; +import { McpContext } from "../../types"; + +describe("get_users tool", () => { + const projectId = "test-project"; + const users = [ + { uid: "uid1", email: "user1@example.com", passwordHash: "hash", salt: "salt" }, + { uid: "uid2", email: "user2@example.com", passwordHash: "hash", salt: "salt" }, + ]; + const prunedUsers = [ + { uid: "uid1", email: "user1@example.com" }, + { uid: "uid2", email: "user2@example.com" }, + ]; + + let findUserStub: sinon.SinonStub; + let listUsersStub: sinon.SinonStub; + + beforeEach(() => { + findUserStub = sinon.stub(auth, "findUser"); + listUsersStub = sinon.stub(auth, "listUsers"); + }); + + afterEach(() => { + sinon.restore(); + }); + + context("when no identifiers are provided", () => { + it("should list all users", async () => { + listUsersStub.resolves(users); + const result = await get_users.fn({}, { projectId } as McpContext); + expect(listUsersStub).to.be.calledWith(projectId, 100); + expect(result).to.deep.equal(toContent(prunedUsers)); + }); + }); + + context("when uids are provided", () => { + it("should get users by uid", async () => { + findUserStub.onFirstCall().resolves(users[0]); + findUserStub.onSecondCall().resolves(users[1]); + const result = await get_users.fn({ uids: ["uid1", "uid2"] }, { projectId } as McpContext); + expect(findUserStub).to.be.calledWith(projectId, undefined, undefined, "uid1"); + expect(findUserStub).to.be.calledWith(projectId, undefined, undefined, "uid2"); + expect(result).to.deep.equal(toContent(prunedUsers)); + }); + + it("should handle not found users", async () => { + findUserStub.onFirstCall().resolves(users[0]); + findUserStub.onSecondCall().rejects(new Error("User not found")); + const result = await get_users.fn({ uids: ["uid1", "uid2"] }, { projectId } as McpContext); + expect(findUserStub).to.be.calledWith(projectId, undefined, undefined, "uid1"); + expect(findUserStub).to.be.calledWith(projectId, undefined, undefined, "uid2"); + expect(result).to.deep.equal(toContent([prunedUsers[0]])); + }); + }); + + context("when emails are provided", () => { + it("should get users by email", async () => { + findUserStub.onFirstCall().resolves(users[0]); + findUserStub.onSecondCall().resolves(users[1]); + const result = await get_users.fn({ emails: ["user1@example.com", "user2@example.com"] }, { + projectId, + } as McpContext); + expect(findUserStub).to.be.calledWith(projectId, "user1@example.com", undefined, undefined); + expect(findUserStub).to.be.calledWith(projectId, "user2@example.com", undefined, undefined); + expect(result).to.deep.equal(toContent(prunedUsers)); + }); + }); + + context("when phone_numbers are provided", () => { + it("should get users by phone number", async () => { + findUserStub.onFirstCall().resolves(users[0]); + findUserStub.onSecondCall().resolves(users[1]); + const result = await get_users.fn({ phone_numbers: ["+11111111111", "+22222222222"] }, { + projectId, + } as McpContext); + expect(findUserStub).to.be.calledWith(projectId, undefined, "+11111111111", undefined); + expect(findUserStub).to.be.calledWith(projectId, undefined, "+22222222222", undefined); + expect(result).to.deep.equal(toContent(prunedUsers)); + }); + }); +}); diff --git a/src/mcp/tools/auth/get_users.ts b/src/mcp/tools/auth/get_users.ts new file mode 100644 index 00000000000..91f2fe2a425 --- /dev/null +++ b/src/mcp/tools/auth/get_users.ts @@ -0,0 +1,63 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { findUser, listUsers, UserInfo } from "../../../gcp/auth"; + +export const get_users = tool( + { + name: "get_users", + description: + "Use this to retrieve one or more Firebase Auth users based on a list of UIDs or a list of emails.", + inputSchema: z.object({ + uids: z.array(z.string()).optional().describe("A list of user UIDs to retrieve."), + emails: z.array(z.string()).optional().describe("A list of user emails to retrieve."), + phone_numbers: z + .array(z.string()) + .optional() + .describe("A list of user phone numbers to retrieve."), + limit: z + .number() + .optional() + .default(100) + .describe("The numbers of users to return. 500 is the upper limit. Defaults to 100."), + }), + annotations: { + title: "Get Firebase Auth Users", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ({ uids, emails, phone_numbers, limit }, { projectId }) => { + const prune = (user: UserInfo) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { passwordHash, salt, ...prunedUser } = user; + return prunedUser; + }; + let users: UserInfo[] = []; + if (uids?.length) { + const promises = uids.map((uid) => + findUser(projectId, undefined, undefined, uid).catch(() => null), + ); + users.push(...(await Promise.all(promises)).filter((u): u is UserInfo => !!u)); + } + if (emails?.length) { + const promises = emails.map((email) => + findUser(projectId, email, undefined, undefined).catch(() => null), + ); + users.push(...(await Promise.all(promises)).filter((u): u is UserInfo => !!u)); + } + if (phone_numbers?.length) { + const promises = phone_numbers.map((phone) => + findUser(projectId, undefined, phone, undefined).catch(() => null), + ); + users.push(...(await Promise.all(promises)).filter((u): u is UserInfo => !!u)); + } + if (!uids?.length && !emails?.length && !phone_numbers?.length) { + users = await listUsers(projectId, limit || 100); + } + return toContent(users.map(prune)); + }, +); diff --git a/src/mcp/tools/auth/index.ts b/src/mcp/tools/auth/index.ts new file mode 100644 index 00000000000..431c611037b --- /dev/null +++ b/src/mcp/tools/auth/index.ts @@ -0,0 +1,6 @@ +import { ServerTool } from "../../tool"; +import { update_user } from "./update_user"; +import { get_users } from "./get_users"; +import { set_sms_region_policy } from "./set_sms_region_policy"; + +export const authTools: ServerTool[] = [get_users, update_user, set_sms_region_policy]; diff --git a/src/mcp/tools/auth/set_sms_region_policy.spec.ts b/src/mcp/tools/auth/set_sms_region_policy.spec.ts new file mode 100644 index 00000000000..5aecf25583b --- /dev/null +++ b/src/mcp/tools/auth/set_sms_region_policy.spec.ts @@ -0,0 +1,48 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { set_sms_region_policy } from "./set_sms_region_policy"; +import * as auth from "../../../gcp/auth"; +import { toContent } from "../../util"; +import { McpContext } from "../../types"; + +describe("set_sms_region_policy tool", () => { + const projectId = "test-project"; + const country_codes = ["us", "ca"]; + const upperCaseCountryCodes = ["US", "CA"]; + + let setAllowSmsRegionPolicyStub: sinon.SinonStub; + let setDenySmsRegionPolicyStub: sinon.SinonStub; + + beforeEach(() => { + setAllowSmsRegionPolicyStub = sinon.stub(auth, "setAllowSmsRegionPolicy"); + setDenySmsRegionPolicyStub = sinon.stub(auth, "setDenySmsRegionPolicy"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should set an ALLOW policy", async () => { + setAllowSmsRegionPolicyStub.resolves(true); + + const result = await set_sms_region_policy.fn({ policy_type: "ALLOW", country_codes }, { + projectId, + } as McpContext); + + expect(setAllowSmsRegionPolicyStub).to.be.calledWith(projectId, upperCaseCountryCodes); + expect(setDenySmsRegionPolicyStub).to.not.be.called; + expect(result).to.deep.equal(toContent(true)); + }); + + it("should set a DENY policy", async () => { + setDenySmsRegionPolicyStub.resolves(true); + + const result = await set_sms_region_policy.fn({ policy_type: "DENY", country_codes }, { + projectId, + } as McpContext); + + expect(setDenySmsRegionPolicyStub).to.be.calledWith(projectId, upperCaseCountryCodes); + expect(setAllowSmsRegionPolicyStub).to.not.be.called; + expect(result).to.deep.equal(toContent(true)); + }); +}); diff --git a/src/mcp/tools/auth/set_sms_region_policy.ts b/src/mcp/tools/auth/set_sms_region_policy.ts new file mode 100644 index 00000000000..abe4b2a6009 --- /dev/null +++ b/src/mcp/tools/auth/set_sms_region_policy.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { setAllowSmsRegionPolicy, setDenySmsRegionPolicy } from "../../../gcp/auth"; + +export const set_sms_region_policy = tool( + { + name: "set_sms_region_policy", + description: + "Use this to set an SMS region policy for Firebase Authentication to restrict the regions which can receive text messages based on an ALLOW or DENY list of country codes. This policy will override any existing policies when set.", + inputSchema: z.object({ + policy_type: z + .enum(["ALLOW", "DENY"]) + .describe( + "with an ALLOW policy, only the specified country codes can use SMS auth. with a DENY policy, all countries can use SMS auth except the ones specified", + ), + country_codes: z + .array(z.string()) + .describe("the country codes to allow or deny based on ISO 3166"), + }), + annotations: { + title: "Set SMS Region Policy", + idempotentHint: true, + destructiveHint: true, + }, + _meta: { + requiresProject: true, + requiresAuth: true, + }, + }, + async ({ policy_type, country_codes }, { projectId }) => { + country_codes = country_codes.map((code) => { + return code.toUpperCase(); + }); + if (policy_type === "ALLOW") { + return toContent(await setAllowSmsRegionPolicy(projectId, country_codes)); + } + return toContent(await setDenySmsRegionPolicy(projectId, country_codes)); + }, +); diff --git a/src/mcp/tools/auth/update_user.spec.ts b/src/mcp/tools/auth/update_user.spec.ts new file mode 100644 index 00000000000..1429e8323db --- /dev/null +++ b/src/mcp/tools/auth/update_user.spec.ts @@ -0,0 +1,106 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { update_user } from "./update_user"; +import * as auth from "../../../gcp/auth"; +import { McpContext } from "../../types"; +import * as util from "../../util"; + +describe("update_user tool", () => { + const projectId = "test-project"; + let setCustomClaimsStub: sinon.SinonStub; + let toggleuserEnablementStub: sinon.SinonStub; + let mcpErrorStub: sinon.SinonStub; + + beforeEach(() => { + setCustomClaimsStub = sinon.stub(auth, "setCustomClaim"); + toggleuserEnablementStub = sinon.stub(auth, "toggleUserEnablement"); + mcpErrorStub = sinon.stub(util, "mcpError"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should disable a user", async () => { + toggleuserEnablementStub.resolves(true); + + const result = await update_user.fn({ uid: "123", disabled: true }, { + projectId, + } as McpContext); + + expect(result).to.deep.equal({ + content: [ + { + text: "Successfully updated user 123. User disabled.", + type: "text", + }, + ], + }); + expect(toggleuserEnablementStub).to.have.been.calledWith(projectId, "123", true); + expect(setCustomClaimsStub).to.not.have.been.called; + }); + + it("should enable a user", async () => { + toggleuserEnablementStub.resolves(true); + + const result = await update_user.fn({ uid: "123", disabled: false }, { + projectId, + } as McpContext); + + expect(result).to.deep.equal({ + content: [ + { + text: "Successfully updated user 123. User enabled.", + type: "text", + }, + ], + }); + expect(toggleuserEnablementStub).to.have.been.calledWith(projectId, "123", false); + expect(setCustomClaimsStub).to.not.have.been.called; + }); + + it("should set a custom claim", async () => { + setCustomClaimsStub.resolves({ uid: "123", customClaims: { admin: true } }); + + const result = await update_user.fn( + { + uid: "123", + claim: { key: "admin", value: true }, + }, + { + projectId, + } as McpContext, + ); + + expect(result).to.deep.equal({ + content: [ + { + text: "Successfully updated user 123. Claim 'admin' set.", + type: "text", + }, + ], + }); + expect(setCustomClaimsStub).to.have.been.calledWith(projectId, "123", { admin: true }); + expect(toggleuserEnablementStub).to.not.have.been.called; + }); + + it("should fail to set a custom claim and disable a user", async () => { + setCustomClaimsStub.resolves({ uid: "123", customClaims: { admin: true } }); + toggleuserEnablementStub.resolves(true); + + await update_user.fn( + { + uid: "123", + claim: { key: "admin", value: true }, + disabled: true, + }, + { + projectId, + } as McpContext, + ); + + expect(mcpErrorStub).to.be.calledWith( + "Can only enable/disable a user or set a claim, not both.", + ); + }); +}); diff --git a/src/mcp/tools/auth/update_user.ts b/src/mcp/tools/auth/update_user.ts new file mode 100644 index 00000000000..b7e28619b7a --- /dev/null +++ b/src/mcp/tools/auth/update_user.ts @@ -0,0 +1,92 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { toggleUserEnablement, setCustomClaim } from "../../../gcp/auth"; + +export const update_user = tool( + { + name: "update_user", + description: "Use this to disable, enable, or set a custom claim on a specific user's account.", + inputSchema: z.object({ + uid: z.string().describe("the UID of the user to update"), + disabled: z.boolean().optional().describe("true disables the user, false enables the user"), + claim: z + .object({ + key: z.string().describe("the name (key) of the claim to update, e.g. 'admin'"), + value: z + .union([z.string(), z.number(), z.boolean()]) + .optional() + .describe( + "Set the value of the custom claim to the specified simple scalar value. One of `value` or `json_value` must be provided if setting a claim.", + ), + json_value: z + .string() + .optional() + .describe( + "Set the claim to a complex JSON value like an object or an array by providing stringified JSON. String must be parseable as valid JSON. One of `value` or `json_value` must be provided if setting a claim.", + ), + }) + .optional(), + }), + annotations: { + title: "Update a user", + idempotentHint: true, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ({ uid, disabled, claim }, { projectId }) => { + if (disabled && claim) { + return mcpError("Can only enable/disable a user or set a claim, not both."); + } + if (disabled === undefined && !claim) { + return mcpError("At least one of 'disabled' or 'claim' must be provided to update the user."); + } + if (claim && claim.value === undefined && claim.json_value === undefined) { + return mcpError( + "When providing 'key' for the claim, you must also provide either 'value' or 'json_value' for the claim.", + ); + } + if (disabled !== undefined) { + try { + await toggleUserEnablement(projectId, uid, disabled); + } catch (err: any) { + return mcpError(`Failed to ${disabled ? "disable" : "enable"} user ${uid}`); + } + } + + if (claim) { + if (claim.value && claim.json_value) { + return mcpError("Must supply only `value` or `json_value`, not both."); + } + let claimValue = claim.value; + if (claim.json_value) { + try { + claimValue = JSON.parse(claim.json_value); + } catch (e) { + return mcpError(`Provided \`json_value\` was not valid JSON: ${claim.json_value}`); + } + } + try { + await setCustomClaim(projectId, uid, { [claim.key]: claimValue }, { merge: true }); + } catch (e: any) { + let errorMsg = `Failed to set claim: ${e.message}`; + if (disabled !== undefined) { + errorMsg = `User was successfully ${disabled ? "disabled" : "enabled"}, but setting the claim failed: ${e.message}`; + } + return mcpError(errorMsg); + } + } + const messageParts = []; + if (disabled !== undefined) { + messageParts.push(`User ${disabled ? "disabled" : "enabled"}`); + } + if (claim) { + messageParts.push(`Claim '${claim.key}' set`); + } + + return toContent(`Successfully updated user ${uid}. ${messageParts.join(". ")}.`); + }, +); diff --git a/src/mcp/tools/core/create_android_sha.ts b/src/mcp/tools/core/create_android_sha.ts new file mode 100644 index 00000000000..f28ca1f58d7 --- /dev/null +++ b/src/mcp/tools/core/create_android_sha.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { createAppAndroidSha, ShaCertificateType } from "../../../management/apps"; + +/** + * Determines the certificate type based on the SHA hash length + */ +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(); + return ShaCertificateType.SHA_CERTIFICATE_TYPE_UNSPECIFIED.toString(); +} + +export const create_android_sha = tool( + { + name: "create_android_sha", + description: + "Use this to add the specified SHA certificate hash to the specified Firebase Android App.", + inputSchema: z.object({ + app_id: z.string().describe("The Android app ID to add the SHA certificate to."), + sha_hash: z.string().describe("The SHA certificate hash to add (SHA-1 or SHA-256)."), + }), + annotations: { + title: "Add SHA Certificate to Android App", + destructiveHint: false, + readOnlyHint: false, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ({ app_id, sha_hash }, { projectId }) => { + // Add the SHA certificate + const certType = getCertHashType(sha_hash); + const shaCertificate = await createAppAndroidSha(projectId, app_id, { + shaHash: sha_hash, + certType, + }); + + return toContent({ + ...shaCertificate, + message: `Successfully added ${certType} certificate to Android app ${app_id}`, + }); + }, +); diff --git a/src/mcp/tools/core/create_app.ts b/src/mcp/tools/core/create_app.ts new file mode 100644 index 00000000000..70321d6fde4 --- /dev/null +++ b/src/mcp/tools/core/create_app.ts @@ -0,0 +1,105 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { createAndroidApp, createIosApp, createWebApp } from "../../../management/apps"; + +const CREATE_APP_OUTUT_PREFIX = "Created app with the following details:\n\n"; +const CREATE_APP_OUTUT_SUFFIX = + "\n\nTo fetch the SDK configuration for this app, use the firebase_get_sdk_config tool."; + +export const create_app = tool( + { + name: "create_app", + description: + "Use this to create a new Firebase App in the currently active Firebase Project. Firebase Apps can be iOS, Android, or Web.", + inputSchema: z.object({ + display_name: z + .string() + .optional() + .describe( + "The user-friendly display name for your app. If not provided, a default name may be generated.", + ), + platform: z + .enum(["web", "ios", "android"]) + .describe("The platform for which to create an app."), + android_config: z + .object({ + package_name: z + .string() + .describe("The package name for your Android app (e.g., com.example.myapp)."), + }) + .optional() + .describe("Configuration for Android apps. Required if platform is 'android'."), + ios_config: z + .object({ + bundle_id: z + .string() + .describe("The bundle ID for your iOS app (e.g., com.example.myapp)."), + app_store_id: z.string().optional().describe("The App Store ID for your iOS app."), + }) + .optional() + .describe("Configuration for iOS apps. Required if platform is 'ios'."), + }), + annotations: { + title: "Create Firebase App", + destructiveHint: false, + readOnlyHint: false, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ({ display_name, platform, android_config, ios_config }, { projectId }) => { + if (platform === "android" && !android_config) { + throw new Error("Android configuration is required when platform is 'android'"); + } + if (platform === "ios" && !ios_config) { + throw new Error("iOS configuration is required when platform is 'ios'"); + } + + try { + switch (platform) { + case "android": + return toContent( + await createAndroidApp(projectId, { + displayName: display_name, + packageName: android_config!.package_name, + }), + { + format: "yaml", + contentPrefix: CREATE_APP_OUTUT_PREFIX, + contentSuffix: CREATE_APP_OUTUT_SUFFIX, + }, + ); + case "ios": + return toContent( + await createIosApp(projectId, { + displayName: display_name, + bundleId: ios_config!.bundle_id, + appStoreId: ios_config!.app_store_id, + }), + { + format: "yaml", + contentPrefix: CREATE_APP_OUTUT_PREFIX, + contentSuffix: CREATE_APP_OUTUT_SUFFIX, + }, + ); + case "web": + return toContent( + await createWebApp(projectId, { + displayName: display_name, + }), + { + format: "yaml", + contentPrefix: CREATE_APP_OUTUT_PREFIX, + contentSuffix: CREATE_APP_OUTUT_SUFFIX, + }, + ); + } + } catch (err: any) { + const originalMessage = err.original ? `: ${err.original.message}` : ""; + throw new Error(`${err.message}\nOriginal error: ${originalMessage}`); + } + }, +); diff --git a/src/mcp/tools/core/create_project.ts b/src/mcp/tools/core/create_project.ts new file mode 100644 index 00000000000..dcab44c3db0 --- /dev/null +++ b/src/mcp/tools/core/create_project.ts @@ -0,0 +1,84 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { + checkFirebaseEnabledForCloudProject, + createFirebaseProjectAndLog, + addFirebaseToCloudProject, + getProject, + ProjectInfo, +} from "../../../management/projects"; +import { getErrStatus } from "../../../error"; + +/** + * Checks if a Cloud project exists and the user has access to it + */ +async function checkCloudProject(projectId: string): Promise { + try { + return await getProject(projectId); + } catch (err: any) { + if (getErrStatus(err) === 403) { + return undefined; + } + throw err; + } +} + +export const create_project = tool( + { + name: "create_project", + description: "Use this to create a new Firebase Project.", + inputSchema: z.object({ + project_id: z.string().describe("The project ID to create or use."), + display_name: z + .string() + .optional() + .describe("The user-friendly display name for the project."), + }), + annotations: { + title: "Create Firebase Project", + destructiveHint: false, + readOnlyHint: false, + }, + _meta: { + requiresAuth: true, + requiresProject: false, + }, + }, + async ({ project_id, display_name }) => { + try { + // Check if cloud project exists + const cloudProject = await checkCloudProject(project_id); + + // If project doesn't exist, create it and add Firebase + if (!cloudProject) { + const newProject = await createFirebaseProjectAndLog(project_id, { + displayName: display_name, + }); + return toContent({ + message: `Successfully created new Firebase project: ${project_id}`, + project: newProject, + }); + } + + // Check if Firebase is enabled + let firebaseProject = await checkFirebaseEnabledForCloudProject(project_id); + if (firebaseProject) { + return toContent({ + message: `Project ${project_id} already exists and has Firebase enabled.`, + project: firebaseProject, + }); + } + + // Project exists but Firebase is not enabled + firebaseProject = await addFirebaseToCloudProject(project_id); + return toContent({ + message: `Successfully added Firebase to existing project: ${project_id}`, + project: firebaseProject, + }); + } catch (err: any) { + const originalMessage = err.original ? `: ${err.original.message}` : ""; + throw new Error(`${err.message}\nOriginal error: ${originalMessage}`); + } + }, +); diff --git a/src/mcp/tools/core/get_environment.spec.ts b/src/mcp/tools/core/get_environment.spec.ts new file mode 100644 index 00000000000..1fa5b9e59c8 --- /dev/null +++ b/src/mcp/tools/core/get_environment.spec.ts @@ -0,0 +1,257 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { get_environment } from "./get_environment"; +import * as projectUtils from "../../../projectUtils"; +import { configstore } from "../../../configstore"; +import * as appUtils from "../../../appUtils"; +import * as auth from "../../../auth"; +import { RC } from "../../../rc"; +import { Config } from "../../../config"; +import { McpContext } from "../../types"; +import { FirebaseMcpServer } from "../.."; + +describe("get_environment tool", () => { + let sandbox: sinon.SinonSandbox; + let getAliasesStub: sinon.SinonStub; + let configstoreGetStub: sinon.SinonStub; + let detectAppsStub: sinon.SinonStub; + let getAllAccountsStub: sinon.SinonStub; + let server: FirebaseMcpServer; + + beforeEach(async () => { + sandbox = sinon.createSandbox(); + getAliasesStub = sandbox.stub(projectUtils, "getAliases"); + configstoreGetStub = sandbox.stub(configstore, "get"); + detectAppsStub = sandbox.stub(appUtils, "detectApps"); + getAllAccountsStub = sandbox.stub(auth, "getAllAccounts"); + server = new FirebaseMcpServer({ projectRoot: "/test-dir" }); + server.cachedProjectDir = "/test-dir"; + }); + + afterEach(() => { + sandbox.restore(); + }); + + const mockToolOptions = ( + projectId?: string, + accountEmail?: string, + projectFileExists = false, + rcProjects: Record = {}, + firebaseJsonContent = "", + ): McpContext => { + const rc = new RC(undefined, { projects: rcProjects }); + const config = new Config({}, { cwd: "/test-dir" }); + sandbox.stub(config, "projectFileExists").returns(projectFileExists); + sandbox.stub(config, "path").returns("/test-dir/firebase.json"); + sandbox.stub(config, "readProjectFile").returns(firebaseJsonContent); + + // The tool fn receives McpContext, which expects projectId to be a string. + // The tool implementation handles a falsy projectId, so we can default to "". + return { + projectId: projectId || "", + host: server, + accountEmail: accountEmail ? accountEmail : null, + rc, + config, + firebaseCliCommand: "firebase", + }; + }; + + it("should show minimal environment", async () => { + getAliasesStub.returns([]); + configstoreGetStub.withArgs("gemini").returns(false); + detectAppsStub.resolves([]); + getAllAccountsStub.returns([]); + const options = mockToolOptions(undefined); + + const result = await get_environment.fn({}, options); + + const expectedOutput = `# Environment Information + +Project Directory: /test-dir +Project Config Path: +Active Project ID: +Gemini in Firebase Terms of Service: +Authenticated User: +Detected App IDs: +Available Project Aliases (format: '[alias]: [projectId]'): + +No firebase.json file was found. + +If this project does not use Firebase services that require a firebase.json file, no action is necessary. + +If this project uses Firebase services that require a firebase.json file, the user will most likely want to: + +a) Change the project directory using the 'firebase_update_environment' tool to select a directory with a 'firebase.json' file in it, or +b) Initialize a new Firebase project directory using the 'firebase_init' tool. + +Confirm with the user before taking action.`; + expect(result.content[0].text).to.equal(expectedOutput); + }); + + it("should show full environment", async () => { + getAliasesStub.returns(["my-alias"]); + configstoreGetStub.withArgs("gemini").returns(true); + detectAppsStub.resolves([ + { platform: "WEB", directory: "web", appId: "web-app-id" }, + { + platform: "ANDROID", + directory: "android", + appId: "android-app-id", + bundleId: "com.foo.bar", + }, + ]); + getAllAccountsStub.returns([ + { user: { email: "test@example.com" } }, + { user: { email: "another@example.com" } }, + ]); + const options = mockToolOptions( + "test-project", + "test@example.com", + true, + { "my-alias": "test-project", "other-alias": "other-project" }, + '{ "hosting": { "public": "public" } }', + ); + + const result = await get_environment.fn({}, options); + const expectedOutput = `# Environment Information + +Project Directory: /test-dir +Project Config Path: /test-dir/firebase.json +Active Project ID: test-project (alias: my-alias) +Gemini in Firebase Terms of Service: Accepted +Authenticated User: test@example.com +Detected App IDs: + +web-app-id: +android-app-id: com.foo.bar + +Available Project Aliases (format: '[alias]: [projectId]'): + +my-alias: test-project +other-alias: other-project + +Available Accounts: + +- test@example.com +- another@example.com + +firebase.json contents: + +\`\`\`json +{ "hosting": { "public": "public" } } +\`\`\``; + expect(result.content[0].text).to.equal(expectedOutput); + }); + + it("should handle a single alias", async () => { + getAliasesStub.returns(["my-alias"]); + detectAppsStub.resolves([]); + getAllAccountsStub.returns([]); + const options = mockToolOptions("test-project", "test@example.com", false, { + "my-alias": "test-project", + }); + + const result = await get_environment.fn({}, options); + expect(result.content[0].text).to.include("Active Project ID: test-project (alias: my-alias)"); + expect(result.content[0].text).to.include( + `Available Project Aliases (format: '[alias]: [projectId]'): + +my-alias: test-project + +`, + ); + }); + + it("should handle multiple aliases", async () => { + getAliasesStub.returns(["alias1", "alias2"]); + detectAppsStub.resolves([]); + getAllAccountsStub.returns([]); + const options = mockToolOptions("test-project", "test@example.com", false, { + alias1: "test-project", + alias2: "test-project", + }); + + const result = await get_environment.fn({}, options); + expect(result.content[0].text).to.include( + "Active Project ID: test-project (alias: alias1,alias2)", + ); + expect(result.content[0].text).to.include( + `Available Project Aliases (format: '[alias]: [projectId]'): + +alias1: test-project +alias2: test-project + +`, + ); + }); + + it("should handle multiple accounts", async () => { + getAliasesStub.returns([]); + detectAppsStub.resolves([]); + getAllAccountsStub.returns([ + { user: { email: "test@example.com" } }, + { user: { email: "another@example.com" } }, + ]); + const options = mockToolOptions("test-project", "test@example.com"); + + const result = await get_environment.fn({}, options); + expect(result.content[0].text).to.include("Authenticated User: test@example.com"); + expect(result.content[0].text).to.include(`Available Accounts: + +- test@example.com +- another@example.com + +`); + }); + + it("should handle a single detected app", async () => { + getAliasesStub.returns([]); + detectAppsStub.resolves([{ platform: "WEB", directory: "web", appId: "web-app-id" }]); + getAllAccountsStub.returns([]); + const options = mockToolOptions(); + + const result = await get_environment.fn({}, options); + expect(result.content[0].text).to.include(`Detected App IDs: + +web-app-id: + +`); + }); + + it("should handle multiple detected apps with bundleId", async () => { + getAliasesStub.returns([]); + detectAppsStub.resolves([ + { platform: "WEB", directory: "web", appId: "web-app-id" }, + { + platform: "ANDROID", + directory: "android", + appId: "android-app-id", + bundleId: "com.foo.bar", + }, + ]); + getAllAccountsStub.returns([]); + const options = mockToolOptions(); + + const result = await get_environment.fn({}, options); + expect(result.content[0].text).to.include(`Detected App IDs: + +web-app-id: +android-app-id: com.foo.bar + +`); + }); + + it("should show Gemini ToS not accepted", async () => { + getAliasesStub.returns([]); + configstoreGetStub.withArgs("gemini").returns(false); + detectAppsStub.resolves([]); + getAllAccountsStub.returns([]); + const options = mockToolOptions(); + + const result = await get_environment.fn({}, options); + expect(result.content[0].text).to.include( + "Gemini in Firebase Terms of Service: ", + ); + }); +}); diff --git a/src/mcp/tools/core/get_environment.ts b/src/mcp/tools/core/get_environment.ts new file mode 100644 index 00000000000..cc28dbf3e26 --- /dev/null +++ b/src/mcp/tools/core/get_environment.ts @@ -0,0 +1,78 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { getAliases } from "../../../projectUtils"; +import { dump } from "js-yaml"; +import { getAllAccounts } from "../../../auth"; +import { configstore } from "../../../configstore"; +import { detectApps } from "../../../appUtils"; + +export const get_environment = tool( + { + name: "get_environment", + description: + "Use this to retrieve the current Firebase **environment** configuration for the Firebase CLI and Firebase MCP server, including current authenticated user, project directory, active Firebase Project, and more.", + inputSchema: z.object({}), + annotations: { + title: "Get Firebase Environment Info", + readOnlyHint: true, + }, + _meta: { + requiresAuth: false, + requiresProject: false, + }, + }, + async (_, { projectId, host, accountEmail, rc, config }) => { + const aliases = projectId ? getAliases({ rc }, projectId) : []; + const geminiTosAccepted = !!configstore.get("gemini"); + const projectFileExists = config.projectFileExists("firebase.json"); + const detectedApps = await detectApps(process.cwd()); + const allAccounts = getAllAccounts().map((account) => account.user.email); + const hasOtherAccounts = allAccounts.filter((email) => email !== accountEmail).length > 0; + + const projectConfigPathString = projectFileExists + ? config.path("firebase.json") + : ""; + const detectedAppsMap = detectedApps + .filter((app) => !!app.appId) + .reduce((map, app) => { + if (app.appId) { + map.set(app.appId, app.bundleId ? app.bundleId : ""); + } + return map; + }, new Map()); + const activeProjectString = projectId + ? `${projectId}${aliases.length ? ` (alias: ${aliases.join(",")})` : ""}` + : ""; + const acceptedGeminiTosString = geminiTosAccepted ? "Accepted" : ""; + return toContent(`# Environment Information + +Project Directory: ${host.cachedProjectDir} +Project Config Path: ${projectConfigPathString} +Active Project ID: ${activeProjectString} +Gemini in Firebase Terms of Service: ${acceptedGeminiTosString} +Authenticated User: ${accountEmail || ""} +Detected App IDs: ${detectedAppsMap.size > 0 ? `\n\n${dump(Object.fromEntries(detectedAppsMap)).trim()}\n` : ""} +Available Project Aliases (format: '[alias]: [projectId]'): ${Object.entries(rc.projects).length > 0 ? `\n\n${dump(rc.projects).trim()}\n` : ""}${ + hasOtherAccounts ? `\nAvailable Accounts: \n\n${dump(allAccounts).trim()}` : "" + } +${ + projectFileExists + ? `\nfirebase.json contents: + +\`\`\`json +${config.readProjectFile("firebase.json")} +\`\`\`` + : `\nNo firebase.json file was found. + +If this project does not use Firebase services that require a firebase.json file, no action is necessary. + +If this project uses Firebase services that require a firebase.json file, the user will most likely want to: + +a) Change the project directory using the 'firebase_update_environment' tool to select a directory with a 'firebase.json' file in it, or +b) Initialize a new Firebase project directory using the 'firebase_init' tool. + +Confirm with the user before taking action.` +}`); + }, +); diff --git a/src/mcp/tools/core/get_project.ts b/src/mcp/tools/core/get_project.ts new file mode 100644 index 00000000000..9a021c52639 --- /dev/null +++ b/src/mcp/tools/core/get_project.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { getProject } from "../../../management/projects"; +import { toContent } from "../../util"; + +export const get_project = tool( + { + name: "get_project", + description: "Use this to retrieve information about the currently active Firebase Project.", + inputSchema: z.object({}), + annotations: { + title: "Get Current Firebase Project", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async (_, { projectId }) => { + return toContent(await getProject(projectId)); + }, +); diff --git a/src/mcp/tools/core/get_sdk_config.ts b/src/mcp/tools/core/get_sdk_config.ts new file mode 100644 index 00000000000..f1e5d233ee1 --- /dev/null +++ b/src/mcp/tools/core/get_sdk_config.ts @@ -0,0 +1,60 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { AppPlatform, getAppConfig, listFirebaseApps } from "../../../management/apps"; + +export const get_sdk_config = tool( + { + name: "get_sdk_config", + description: + "Use this to retrieve the Firebase configuration information for a Firebase App. " + + "You must specify EITHER a platform OR the Firebase App ID for a Firebase App registered in the currently active Firebase Project.", + inputSchema: z.object({ + platform: z + .enum(["ios", "android", "web"]) + .optional() + .describe( + "The platform for which you want config. One of 'platform' or 'app_id' must be provided.", + ), + app_id: z + .string() + .optional() + .describe("The specific app ID to fetch. One of 'platform' or 'app_id' must be provided."), + }), + annotations: { + title: "Get Firebase SDK Config", + readOnlyHint: true, + }, + _meta: { + requiresProject: true, + requiresAuth: true, + }, + }, + async ({ platform: inputPlatform, app_id: appId }, { projectId }) => { + let platform = inputPlatform?.toUpperCase() as AppPlatform; + if (!platform && !appId) + return mcpError( + "Must specify one of 'web', 'ios', or 'android' for platform or an app_id for get_sdk_config tool.", + ); + const apps = await listFirebaseApps(projectId, platform ?? AppPlatform.ANY); + platform = platform || apps.find((app) => app.appId === appId)?.platform; + appId = appId || apps.find((app) => app.platform === platform)?.appId; + if (!appId) + return mcpError( + `Could not find an app for platform '${inputPlatform}' in project '${projectId}'`, + ); + const sdkConfig = await getAppConfig(appId, platform); + if ("configFilename" in sdkConfig) { + return { + content: [ + { + type: "text", + text: `SDK config content for \`${sdkConfig.configFilename}\`:\n\n\`\`\`\n${Buffer.from(sdkConfig.configFileContents, "base64").toString("utf-8")}\n\`\`\``, + }, + ], + }; + } + + return toContent(sdkConfig, { format: "json" }); + }, +); diff --git a/src/mcp/tools/core/get_security_rules.ts b/src/mcp/tools/core/get_security_rules.ts new file mode 100644 index 00000000000..1b9ed7c5c39 --- /dev/null +++ b/src/mcp/tools/core/get_security_rules.ts @@ -0,0 +1,60 @@ +import { z } from "zod"; +import { Client } from "../../../apiv2"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { getLatestRulesetName, getRulesetContent } from "../../../gcp/rules"; +import { getDefaultDatabaseInstance } from "../../../getDefaultDatabaseInstance"; + +export const get_security_rules = tool( + { + name: "get_security_rules", + description: + "Use this to retrieve the security rules for a specified Firebase service. " + + "If there are multiple instances of that service in the product, the rules for the defualt instance are returned.", + inputSchema: z.object({ + type: z.enum(["firestore", "rtdb", "storage"]).describe("The service to get rules for."), + // TODO: Add a resourceID argument that lets you choose non default buckets/dbs. + }), + annotations: { + title: "Get Firebase Rules", + readOnlyHint: true, + }, + _meta: { + requiresProject: true, + requiresAuth: true, + }, + }, + async ({ type }, { projectId }) => { + if (type === "rtdb") { + const dbUrl = await getDefaultDatabaseInstance(projectId); + if (dbUrl === "") { + return mcpError(`No default RTDB instance found for project ${projectId}`); + } + const client = new Client({ urlPrefix: dbUrl }); + const response = await client.request({ + method: "GET", + path: "/.settings/rules.json", + responseType: "stream", + resolveOnHTTPError: true, + }); + if (response.status !== 200) { + return mcpError(`Failed to fetch current rules. Code: ${response.status}`); + } + + const rules = await response.response.text(); + return toContent(rules); + } + + const serviceInfo = { + firestore: { productName: "Firestore", releaseName: "cloud.firestore" }, + storage: { productName: "Storage", releaseName: "firebase.storage" }, + }; + const { productName, releaseName } = serviceInfo[type]; + + const rulesetName = await getLatestRulesetName(projectId, releaseName); + if (!rulesetName) + return mcpError(`No active ${productName} rules were found in project '${projectId}'`); + const rules = await getRulesetContent(rulesetName); + return toContent(rules?.[0].content ?? "Ruleset contains no rules files."); + }, +); diff --git a/src/mcp/tools/core/index.ts b/src/mcp/tools/core/index.ts new file mode 100644 index 00000000000..12c96e19207 --- /dev/null +++ b/src/mcp/tools/core/index.ts @@ -0,0 +1,35 @@ +import type { ServerTool } from "../../tool"; + +import { get_project } from "./get_project"; +import { get_sdk_config } from "./get_sdk_config"; +import { list_apps } from "./list_apps"; +import { create_project } from "./create_project"; +import { create_app } from "./create_app"; +import { create_android_sha } from "./create_android_sha"; +import { init } from "./init"; +import { get_environment } from "./get_environment"; +import { update_environment } from "./update_environment"; +import { list_projects } from "./list_projects"; +import { login } from "./login"; +import { logout } from "./logout"; +import { get_security_rules } from "./get_security_rules"; +import { validate_security_rules } from "./validate_security_rules"; +import { read_resources } from "./read_resources"; + +export const coreTools: ServerTool[] = [ + login, + logout, + validate_security_rules, // TODO (joehan): Only enable this tool when at least once of rtdb/storage/firestore is active. + get_project, + list_apps, + list_projects, + get_sdk_config, + create_project, + create_app, + create_android_sha, + get_environment, + update_environment, + init, + get_security_rules, + read_resources, +]; diff --git a/src/mcp/tools/core/init.ts b/src/mcp/tools/core/init.ts new file mode 100644 index 00000000000..46075a01df7 --- /dev/null +++ b/src/mcp/tools/core/init.ts @@ -0,0 +1,248 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { DEFAULT_RULES } from "../../../init/features/database"; +import { actuate, Setup, SetupInfo } from "../../../init/index"; +import { freeTrialTermsLink } from "../../../dataconnect/freeTrial"; +import { requireGeminiToS } from "../../errors"; +import { FirebaseError } from "../../../error"; +import { + parseAppId, + validateProjectNumberMatch, + validateAppExists, +} from "../../../init/features/ailogic/utils"; +import { getFirebaseProject } from "../../../management/projects"; +import { FDC_DEFAULT_REGION } from "../../../init/features/dataconnect"; + +export const init = tool( + { + name: "init", + description: + "Use this to initialize selected Firebase services in the workspace (Cloud Firestore database, Firebase Data Connect, Firebase Realtime Database, Firebase AI Logic). All services are optional; specify only the products you want to set up. " + + "You can initialize new features into an existing project directory, but re-initializing an existing feature may overwrite configuration. " + + "To deploy the initialized features, run the `firebase deploy` command after `firebase_init` tool.", + inputSchema: z.object({ + features: z.object({ + database: z + .object({ + rules_filename: z + .string() + .optional() + .default("database.rules.json") + .describe("The file to use for Realtime Database Security Rules."), + rules: z + .string() + .optional() + .default(DEFAULT_RULES) + .describe("The security rules to use for Realtime Database Security Rules."), + }) + .optional() + .describe( + "Provide this object to initialize Firebase Realtime Database in this project directory.", + ), + firestore: z + .object({ + database_id: z + .string() + .optional() + .default("(default)") + .describe("The database ID to use for Firestore."), + location_id: z + .string() + .optional() + .default("nam5") + .describe("The GCP region ID to set up the Firestore database."), + rules_filename: z + .string() + .optional() + .default("firestore.rules") + .describe("The file to use for Firestore Security Rules."), + rules: z + .string() + .optional() + .describe( + "The security rules to use for Firestore Security Rules. Default to open rules that expire in 30 days.", + ), + }) + .optional() + .describe("Provide this object to initialize Cloud Firestore in this project directory."), + dataconnect: z + .object({ + app_description: z + .string() + .optional() + .describe( + "Provide a description of the app you are trying to build. If present, Gemini will help generate Data Connect Schema, Connector and seed data", + ), + service_id: z + .string() + .optional() + .describe( + "The Firebase Data Connect service ID to initialize. Default to match the current folder name.", + ), + location_id: z + .string() + .optional() + .default(FDC_DEFAULT_REGION) + .describe("The GCP region ID to set up the Firebase Data Connect service."), + cloudsql_instance_id: z + .string() + .optional() + .describe( + "The GCP Cloud SQL instance ID to use in the Firebase Data Connect service. By default, use -fdc. " + + "\nSet `provision_cloudsql` to true to start Cloud SQL provisioning.", + ), + cloudsql_database: z + .string() + .optional() + .default("fdcdb") + .describe("The Postgres database ID to use in the Firebase Data Connect service."), + provision_cloudsql: z + .boolean() + .optional() + .default(false) + .describe( + "If true, provision the Cloud SQL instance if `cloudsql_instance_id` does not exist already. " + + `\nThe first Cloud SQL instance in the project will use the Data Connect no-cost trial. See its terms of service: ${freeTrialTermsLink()}.`, + ), + }) + .optional() + .describe( + "Provide this object to initialize Firebase Data Connect with Cloud SQL Postgres in this project directory.\n" + + "It installs Data Connect Generated SDKs in all detected apps in the folder.", + ), + storage: z + .object({ + rules_filename: z + .string() + .optional() + .default("storage.rules") + .describe("The file to use for Firebase Storage Security Rules."), + rules: z + .string() + .optional() + .describe( + "The security rules to use for Firebase Storage Security Rules. Default to closed rules that deny all access.", + ), + }) + .optional() + .describe( + "Provide this object to initialize Firebase Storage in this project directory.", + ), + ailogic: z + .object({ + app_id: z + .string() + .describe( + "Firebase app ID (format: 1:PROJECT_NUMBER:PLATFORM:APP_ID). Must be an existing app in your Firebase project.", + ), + }) + .optional() + .describe("Enable Firebase AI Logic feature for existing app"), + }), + }), + annotations: { + title: "Initialize Firebase Products", + readOnlyHint: false, + idempotentHint: true, + }, + _meta: { + requiresProject: false, // Can start from scratch. + requiresAuth: false, // Will throw error if the specific feature needs it. + }, + }, + async ({ features }, { projectId, config, rc }) => { + const featuresList: string[] = []; + const featureInfo: SetupInfo = {}; + if (features.database) { + featuresList.push("database"); + featureInfo.database = { + rulesFilename: features.database.rules_filename, + rules: features.database.rules, + writeRules: true, + }; + } + if (features.firestore) { + featuresList.push("firestore"); + featureInfo.firestore = { + databaseId: features.firestore.database_id, + locationId: features.firestore.location_id, + rulesFilename: features.firestore.rules_filename, + rules: features.firestore.rules || "", + writeRules: true, + indexesFilename: "", + indexes: "", + writeIndexes: true, + }; + } + if (features.dataconnect) { + if (features.dataconnect.app_description) { + // If app description is provided, ensure the Gemini in Firebase API is enabled. + const err = await requireGeminiToS(projectId); + if (err) return err; + } + featuresList.push("dataconnect"); + featureInfo.dataconnect = { + analyticsFlow: "mcp", + appDescription: features.dataconnect.app_description || "", + serviceId: features.dataconnect.service_id || "", + locationId: features.dataconnect.location_id || "", + cloudSqlInstanceId: features.dataconnect.cloudsql_instance_id || "", + cloudSqlDatabase: features.dataconnect.cloudsql_database || "", + shouldProvisionCSQL: !!features.dataconnect.provision_cloudsql, + }; + featureInfo.dataconnectSdk = { + // Add FDC generated SDKs to all apps detected. + apps: [], + }; + } + if (features.ailogic) { + // AI Logic requires a project + if (!projectId) { + throw new FirebaseError( + "AI Logic feature requires a Firebase project. Please specify a project ID.", + { exit: 1 }, + ); + } + + // Validate AI Logic app for MCP flow + const appInfo = parseAppId(features.ailogic.app_id); + const projectInfo = await getFirebaseProject(projectId); + validateProjectNumberMatch(appInfo, projectInfo); + const appData = await validateAppExists(appInfo, projectId); + + featuresList.push("ailogic"); + featureInfo.ailogic = { + appId: features.ailogic.app_id, + displayName: appData.displayName, + }; + } + const setup: Setup = { + config: config?.src, + rcfile: rc?.data, + projectId: projectId, + features: [...featuresList], + featureInfo: featureInfo, + instructions: [], + }; + // Set force to true to avoid prompting the user for confirmation. + await actuate(setup, config, { force: true }); + config.writeProjectFile("firebase.json", setup.config); + config.writeProjectFile(".firebaserc", setup.rcfile); + + if (featureInfo.dataconnectSdk && !featureInfo.dataconnectSdk.apps.length) { + setup.instructions.push( + `No app is found in the current folder. We recommend you create an app (web, ios, android) first, then re-run the 'firebase_init' MCP tool with the same input without app_description to add Data Connect SDKs to your apps. + Consider popular commands like 'npx create-react-app my-app', 'npx create-next-app my-app', 'flutter create my-app', etc`, + ); + } + return toContent( + `Successfully setup those features: ${featuresList.join(", ")} + +To get started: + +- ${setup.instructions.join("\n\n- ")} +`, + ); + }, +); diff --git a/src/mcp/tools/core/list_apps.ts b/src/mcp/tools/core/list_apps.ts new file mode 100644 index 00000000000..7d84922b953 --- /dev/null +++ b/src/mcp/tools/core/list_apps.ts @@ -0,0 +1,38 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { AppPlatform, listFirebaseApps } from "../../../management/apps"; + +export const list_apps = tool( + { + name: "list_apps", + description: + "Use this to retrieve a list of the Firebase Apps registered in the currently active Firebase project. Firebase Apps can be iOS, Android, or Web.", + inputSchema: z.object({ + platform: z + .enum(["ios", "android", "web", "all"]) + .optional() + .describe("the specific platform to list (omit to list all platforms)"), + }), + annotations: { + title: "List Firebase Apps", + readOnlyHint: true, + }, + _meta: { + requiresProject: true, + requiresAuth: true, + }, + }, + async ({ platform }, { projectId }) => { + try { + const apps = await listFirebaseApps( + projectId!, + !platform || platform === "all" ? AppPlatform.ANY : (platform.toUpperCase() as AppPlatform), + ); + return toContent(apps); + } catch (err: any) { + const originalMessage = err.original ? `: ${err.original.message}` : ""; + throw new Error(`Failed to list Firebase apps${originalMessage}`); + } + }, +); diff --git a/src/mcp/tools/core/list_projects.ts b/src/mcp/tools/core/list_projects.ts new file mode 100644 index 00000000000..81f9d9892c9 --- /dev/null +++ b/src/mcp/tools/core/list_projects.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { getFirebaseProjectPage } from "../../../management/projects"; + +const PROJECT_LIST_PAGE_SIZE = 20; + +export const list_projects = tool( + { + name: "list_projects", + description: + "Use this to retrieve a list of Firebase Projects that the signed-in user has access to.", + inputSchema: z.object({ + page_size: z + .number() + .min(1) + .default(PROJECT_LIST_PAGE_SIZE) + .describe("the number of projects to list per page (defaults to 1000)"), + page_token: z.string().optional().describe("the page token to start listing from"), + }), + annotations: { + title: "List Firebase Projects", + readOnlyHint: true, + idempotentHint: true, + }, + _meta: { + requiresAuth: true, + }, + }, + async ({ page_size, page_token }) => { + try { + const projectPage = await getFirebaseProjectPage(page_size, page_token); + + return toContent( + { + projects: projectPage.projects, + next_page_token: projectPage.nextPageToken, + }, + { + contentPrefix: `Here are ${projectPage.projects.length} Firebase projects${page_token ? " (continued)" : ""}:\n\n`, + contentSuffix: projectPage.nextPageToken + ? "\nThere are more projects available. To see the next page, call this tool again with the next_page_token shown above." + : "", + }, + ); + } catch (err: any) { + const originalMessage = err.original ? `: ${err.original.message}` : ""; + throw new Error(`Failed to list Firebase projects${originalMessage}`); + } + }, +); diff --git a/src/mcp/tools/core/login.spec.ts b/src/mcp/tools/core/login.spec.ts new file mode 100644 index 00000000000..9dbc0cdafba --- /dev/null +++ b/src/mcp/tools/core/login.spec.ts @@ -0,0 +1,57 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { login, ServerWithLoginState } from "./login"; +import * as auth from "../../../auth"; +import { FirebaseMcpServer } from "../../../mcp"; +import { toContent } from "../../util"; + +describe("login tool", () => { + let sandbox: sinon.SinonSandbox; + let loginPrototyperStub: sinon.SinonStub; + let server: FirebaseMcpServer; + const fakeAuthorize = sinon.stub(); + + beforeEach(() => { + sandbox = sinon.createSandbox(); + loginPrototyperStub = sandbox.stub(auth, "loginPrototyper").resolves({ + uri: "https://fake.login.uri/auth", + sessionId: "FAKE_SESSION_ID", + authorize: fakeAuthorize, + }); + server = new FirebaseMcpServer({ projectRoot: "" }); + }); + + afterEach(() => { + sandbox.restore(); + fakeAuthorize.reset(); + }); + + it("should return uri and sessionId when no authCode is provided", async () => { + const result = await login.fn({ authCode: undefined }, { host: server } as any); + + const expectedResult = toContent( + `Please visit this URL to login: https://fake.login.uri/auth\nYour session ID is: FAKE_SESSION_ID\nInstruct the use to copy the authorization code from that link, and paste it into chat.\nThen, run this tool again with that as the authCode argument to complete the login.`, + ); + expect(loginPrototyperStub.calledOnce).to.be.true; + expect(result).to.deep.equal(expectedResult); + expect((server as ServerWithLoginState).authorize).to.exist; + }); + + it("should call authorize when authCode is provided", async () => { + (server as ServerWithLoginState).authorize = fakeAuthorize; + fakeAuthorize.resolves({ user: { email: "test@example.com" } }); + + const result = await login.fn({ authCode: "fake_auth_code" }, { host: server } as any); + + expect(fakeAuthorize.calledOnceWith("fake_auth_code")).to.be.true; + expect(result).to.deep.equal(toContent(`Successfully logged in as test@example.com`)); + expect((server as ServerWithLoginState).authorize).to.not.exist; + }); + + it("should return an error if authCode is provided without starting the flow", async () => { + const result = await login.fn({ authCode: "fake_auth_code" }, { host: server } as any); + + expect(result.isError).to.be.true; + expect(result.content[0].text).to.include("Login flow not started"); + }); +}); diff --git a/src/mcp/tools/core/login.ts b/src/mcp/tools/core/login.ts new file mode 100644 index 00000000000..1a0add06baa --- /dev/null +++ b/src/mcp/tools/core/login.ts @@ -0,0 +1,56 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { loginPrototyper } from "../../../auth"; +import { FirebaseMcpServer } from "../../../mcp"; +import { toContent, mcpError } from "../../util"; +import { User, UserCredentials } from "../../../types/auth"; +const LoginInputSchema = z.object({ + authCode: z.string().optional().describe("The authorization code from the login flow"), +}); + +export type ServerWithLoginState = FirebaseMcpServer & { + authorize?: (authCode: string) => Promise; +}; +export const login = tool( + { + name: "login", + description: + "Use this to sign the user into the Firebase CLI and Firebase MCP server. This requires a Google Account, and sign in is required to create and work with Firebase Projects.", + inputSchema: LoginInputSchema, + _meta: { + requiresAuth: false, + }, + }, + async (input: z.infer, ctx: { host: FirebaseMcpServer }) => { + const { authCode } = input; + + const serverWithState: ServerWithLoginState = ctx.host; + + if (authCode) { + if (!serverWithState.authorize) { + return mcpError( + "Login flow not started. Please call this tool without the authCode argument first to get a login URI.", + ); + } + + try { + const creds = await serverWithState.authorize(authCode); + delete serverWithState.authorize; + const user = creds.user as User; + return toContent(`Successfully logged in as ${user.email}`); + } catch (e: any) { + delete serverWithState.authorize; + return mcpError(`Login failed: ${e.message}`); + } + } else { + const prototyper = await loginPrototyper(); + serverWithState.authorize = prototyper.authorize; + const result = { + uri: prototyper.uri, + sessionId: prototyper.sessionId, + }; + const humanReadable = `Please visit this URL to login: ${result.uri}\nYour session ID is: ${result.sessionId}\nInstruct the use to copy the authorization code from that link, and paste it into chat.\nThen, run this tool again with that as the authCode argument to complete the login.`; + return toContent(humanReadable); + } + }, +); diff --git a/src/mcp/tools/core/logout.spec.ts b/src/mcp/tools/core/logout.spec.ts new file mode 100644 index 00000000000..eb82834011b --- /dev/null +++ b/src/mcp/tools/core/logout.spec.ts @@ -0,0 +1,98 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { logout } from "./logout"; +import * as auth from "../../../auth"; +import { toContent } from "../../util"; +import { Account } from "../../../types/auth"; + +describe("logout tool", () => { + let sandbox: sinon.SinonSandbox; + let getAllAccountsStub: sinon.SinonStub; + let getGlobalDefaultAccountStub: sinon.SinonStub; + let getAdditionalAccountsStub: sinon.SinonStub; + let setGlobalDefaultAccountStub: sinon.SinonStub; + let logoutStub: sinon.SinonStub; + + const fakeAccount1: Account = { + user: { email: "test1@example.com" }, + tokens: { + refresh_token: "token1", + access_token: "atok1", + id_token: "idtok1", + expires_at: 3600, + }, + }; + const fakeAccount2: Account = { + user: { email: "test2@example.com" }, + tokens: { + refresh_token: "token2", + access_token: "atok2", + id_token: "idtok2", + expires_at: 3600, + }, + }; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + getAllAccountsStub = sandbox.stub(auth, "getAllAccounts"); + getGlobalDefaultAccountStub = sandbox.stub(auth, "getGlobalDefaultAccount"); + getAdditionalAccountsStub = sandbox.stub(auth, "getAdditionalAccounts"); + setGlobalDefaultAccountStub = sandbox.stub(auth, "setGlobalDefaultAccount"); + logoutStub = sandbox.stub(auth, "logout").resolves(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should inform if no user is logged in", async () => { + getAllAccountsStub.returns([]); + const result = await logout.fn({ email: undefined }, {} as any); + expect(result).to.deep.equal(toContent("No need to log out, not logged in")); + }); + + it("should log out a single user", async () => { + getAllAccountsStub.returns([fakeAccount1]); + getGlobalDefaultAccountStub.returns(fakeAccount1); + getAdditionalAccountsStub.returns([]); + + const result = await logout.fn({ email: undefined }, {} as any); + + expect(logoutStub.calledOnceWith("token1")).to.be.true; + expect(result.content[0].text).to.include("Logged out from test1@example.com"); + }); + + it("should log out a specific user by email", async () => { + getAllAccountsStub.returns([fakeAccount1, fakeAccount2]); + getGlobalDefaultAccountStub.returns(fakeAccount1); + getAdditionalAccountsStub.returns([fakeAccount2]); + + const result = await logout.fn({ email: "test2@example.com" }, {} as any); + + expect(logoutStub.calledOnceWith("token2")).to.be.true; + expect(logoutStub.callCount).to.equal(1); + expect(result.content[0].text).to.include("Logged out from test2@example.com"); + }); + + it("should log out all users if no email is provided", async () => { + getAllAccountsStub.returns([fakeAccount1, fakeAccount2]); + getGlobalDefaultAccountStub.returns(fakeAccount1); + getAdditionalAccountsStub.returns([fakeAccount2]); + + await logout.fn({ email: undefined }, {} as any); + + expect(logoutStub.calledTwice).to.be.true; + expect(logoutStub.calledWith("token1")).to.be.true; + expect(logoutStub.calledWith("token2")).to.be.true; + }); + + it("should set a new default user when logging out the default", async () => { + getAllAccountsStub.returns([fakeAccount1, fakeAccount2]); + getGlobalDefaultAccountStub.returns(fakeAccount1); + getAdditionalAccountsStub.returns([fakeAccount2]); + + await logout.fn({ email: "test1@example.com" }, {} as any); + + expect(setGlobalDefaultAccountStub.calledOnceWith(fakeAccount2)).to.be.true; + }); +}); diff --git a/src/mcp/tools/core/logout.ts b/src/mcp/tools/core/logout.ts new file mode 100644 index 00000000000..c17786a2246 --- /dev/null +++ b/src/mcp/tools/core/logout.ts @@ -0,0 +1,80 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { + getAllAccounts, + getGlobalDefaultAccount, + getAdditionalAccounts, + logout as authLogout, + setGlobalDefaultAccount, +} from "../../../auth"; +import { logger } from "../../../logger"; + +export const logout = tool( + { + name: "logout", + description: "Use this to sign the user out of the Firebase CLI and Firebase MCP server.", + inputSchema: z.object({ + email: z + .string() + .optional() + .describe( + "The email of the account to log out. If not provided, all accounts will be logged out.", + ), + }), + _meta: { + requiresAuth: false, + requiresProject: false, + }, + }, + async ({ email }) => { + const allAccounts = getAllAccounts(); + if (allAccounts.length === 0) { + return toContent("No need to log out, not logged in"); + } + + const defaultAccount = getGlobalDefaultAccount(); + const additionalAccounts = getAdditionalAccounts(); + + const accountsToLogOut = email + ? allAccounts.filter((a) => a.user.email === email) + : allAccounts; + + if (email && accountsToLogOut.length === 0) { + return toContent(`No account matches ${email}, can't log out.`); + } + + // If they are logging out of their primary account, choose one to + // replace it. + const logoutDefault = email === defaultAccount?.user.email; + let newDefaultAccount = undefined; + if (logoutDefault && additionalAccounts.length > 0) { + newDefaultAccount = additionalAccounts[0]; + } + + const logoutMessages = []; + for (const account of accountsToLogOut) { + const token = account.tokens.refresh_token; + + if (token) { + try { + await authLogout(token); + logoutMessages.push(`Logged out from ${account.user.email}`); + } catch (e: unknown) { + if (e instanceof Error) { + logger.debug(e.message); + } + logoutMessages.push( + `Could not deauthorize ${account.user.email}, assuming already deauthorized.`, + ); + } + } + } + + if (newDefaultAccount) { + setGlobalDefaultAccount(newDefaultAccount); + logoutMessages.push(`Setting default account to "${newDefaultAccount.user.email}"`); + } + return toContent(logoutMessages.join("\n")); + }, +); diff --git a/src/mcp/tools/core/read_resources.ts b/src/mcp/tools/core/read_resources.ts new file mode 100644 index 00000000000..badd9f6ad8a --- /dev/null +++ b/src/mcp/tools/core/read_resources.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { resolveResource, resources } from "../../resources"; +import { toContent } from "../../util"; +import { trackGA4 } from "../../../track"; + +export const read_resources = tool( + { + name: "read_resources", + description: + "Use this to read the contents of `firebase://` resources or list available resources", + annotations: { + title: "Read Firebase Resources", + destructiveHint: false, + readOnlyHint: true, + }, + inputSchema: z.object({ + uris: z + .array(z.string()) + .optional() + .describe( + "list of resource uris to read. each must start with `firebase://` prefix. omit to list all available resources", + ), + }), + }, + async ({ uris }, ctx) => { + if (!uris?.length) { + void trackGA4("mcp_read_resource", { resource_name: "__list__" }); + return toContent( + resources + .map( + (r) => + `Available resources:\n\n- [${r.mcp.title || r.mcp.name}](${r.mcp.uri}): ${r.mcp.description}`, + ) + .join("\n"), + ); + } + + const out: string[] = []; + for (const uri of uris) { + const resolved = await resolveResource(uri, ctx); + if (!resolved) { + out.push(`\nRESOURCE NOT FOUND\n`); + continue; + } + out.push( + `\n${resolved.result.contents.map((c) => c.text).join("")}\n`, + ); + } + + return toContent(out.join("\n\n")); + }, +); diff --git a/src/mcp/tools/core/update_environment.ts b/src/mcp/tools/core/update_environment.ts new file mode 100644 index 00000000000..a4742aca536 --- /dev/null +++ b/src/mcp/tools/core/update_environment.ts @@ -0,0 +1,79 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { setNewActive } from "../../../commands/use"; +import { assertAccount, setProjectAccount } from "../../../auth"; +import { existsSync } from "node:fs"; +import { configstore } from "../../../configstore"; + +export const update_environment = tool( + { + name: "update_environment", + description: + "Use this to update environment config for the Firebase CLI and Firebase MCP server, such as project directory, active project, active user account, accept terms of service, and more. Use `firebase_get_environment` to see the currently configured environment.", + inputSchema: z.object({ + project_dir: z + .string() + .optional() + .describe( + "Change the current project directory - this should be a directory that has a `firebase.json` file (or will have one).", + ), + active_project: z + .string() + .optional() + .describe( + "Change the active project for the current project directory. Should be a Firebase project ID or configured project alias.", + ), + active_user_account: z + .string() + .optional() + .describe( + "The email address of the signed-in user to authenticate as when interacting with the current project directory.", + ), + accept_gemini_tos: z + .boolean() + .optional() + .describe( + "Accept the Gemini in Firebase terms of service. Always prompt the user for confirmation before accepting on their behalf.", + ), + }), + annotations: { + title: "Update Firebase Environment", + readOnlyHint: false, + }, + _meta: { + optionalProjectDir: true, + requiresAuth: false, + requiresProject: false, + }, + }, + async ( + { project_dir, active_project, active_user_account, accept_gemini_tos }, + { config, rc, host }, + ) => { + let output = ""; + if (project_dir) { + if (!existsSync(project_dir)) + return mcpError( + `Cannot update project directory to '${project_dir}' as it does not exist.`, + ); + host.setProjectRoot(project_dir); + output += `- Updated project directory to '${project_dir}'\n`; + } + if (active_project) { + await setNewActive(active_project, undefined, rc, config.projectDir); + output += `- Updated active project to '${active_project}'\n`; + } + if (active_user_account) { + assertAccount(active_user_account, { mcp: true }); + setProjectAccount(host.cachedProjectDir!, active_user_account); + output += `- Updated active account to '${active_user_account}'\n`; + } + if (accept_gemini_tos) { + configstore.set("gemini", true); + output += `- Accepted the Gemini in Firebase terms of service\n`; + } + if (output === "") output = "No changes were made."; + return toContent(output); + }, +); diff --git a/src/mcp/tools/core/validate_security_rules.ts b/src/mcp/tools/core/validate_security_rules.ts new file mode 100644 index 00000000000..83e19dc136f --- /dev/null +++ b/src/mcp/tools/core/validate_security_rules.ts @@ -0,0 +1,141 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { testRuleset } from "../../../gcp/rules"; +import { resolve } from "path"; +import { Client } from "../../../apiv2"; +import { updateRulesWithClient } from "../../../rtdb"; +import { getErrMsg } from "../../../error"; +import { getDefaultDatabaseInstance } from "../../../getDefaultDatabaseInstance"; + +interface SourcePosition { + fileName?: string; + line?: number; + column?: number; + currentOffset?: number; + endOffset?: number; +} + +interface Issue { + sourcePosition: SourcePosition; + description: string; + severity: string; +} + +function formatRulesetIssues(issues: Issue[], rulesSource: string): string { + const sourceLines = rulesSource.split("\n"); + const formattedOutput: string[] = []; + + for (const issue of issues) { + const { sourcePosition, description, severity } = issue; + + let issueString = `${severity}: ${description} [Ln ${sourcePosition.line}, Col ${sourcePosition.column}]`; + + if (sourcePosition.line) { + const lineIndex = sourcePosition.line - 1; + if (lineIndex >= 0 && lineIndex < sourceLines.length) { + const errorLine = sourceLines[lineIndex]; + issueString += `\n\`\`\`\n${errorLine}`; + + if ( + sourcePosition.column && + sourcePosition.currentOffset && + sourcePosition.endOffset && + sourcePosition.column > 0 && + sourcePosition.endOffset > sourcePosition.currentOffset + ) { + const startColumnOnLine = sourcePosition.column - 1; + const errorTokenLength = sourcePosition.endOffset - sourcePosition.currentOffset; + + if ( + startColumnOnLine >= 0 && + errorTokenLength > 0 && + startColumnOnLine <= errorLine.length + ) { + const padding = " ".repeat(startColumnOnLine); + const carets = "^".repeat(errorTokenLength); + issueString += `\n${padding}${carets}\n\`\`\``; + } + } + } + } + formattedOutput.push(issueString); + } + return formattedOutput.join("\n\n"); +} + +export const validate_security_rules = tool( + { + name: "validate_security_rules", + description: + "Use this to check Firebase Security Rules for Firestore, Storage, or Realtime Database for syntax and validation errors.", + inputSchema: z.object({ + type: z.enum(["firestore", "storage", "rtdb"]), + source: z + .string() + .optional() + .describe("The rules source code to check. Provide either this or a path."), + source_file: z + .string() + .optional() + .describe( + "A file path, relative to the project root, to a file containing the rules source you want to validate. Provide this or source, not both.", + ), + }), + annotations: { + title: "Validate Firebase Security Rules", + readOnlyHint: true, + }, + _meta: { + requiresProject: true, + requiresAuth: true, + }, + }, + async ({ type, source, source_file }, { projectId, config, host }) => { + let rulesSourceContent: string; + if (source && source_file) { + return mcpError("Must supply `source` or `source_file`, not both."); + } else if (source_file) { + try { + const filePath = resolve(source_file, host.cachedProjectDir!); + if (filePath.includes("../")) + return mcpError("Cannot read files outside of the project directory."); + rulesSourceContent = config.readProjectFile(source_file); + } catch (e: any) { + return mcpError(`Failed to read source_file '${source_file}': ${e.message}`); + } + } else if (source) { + rulesSourceContent = source; + } else { + return mcpError("Must supply at least one of `source` or `source_file`."); + } + + if (type === "rtdb") { + const dbUrl = await getDefaultDatabaseInstance(projectId); + const client = new Client({ urlPrefix: dbUrl }); + try { + await updateRulesWithClient(client, source, { dryRun: true }); + } catch (e: unknown) { + host.logger.debug(`failed to validate rules at url ${dbUrl}`); + // TODO: This really should only return an MCP error if we couldn't validate + // If the rules are invalid, we should return that as content + return mcpError(getErrMsg(e)); + } + return toContent("The inputted rules are valid!"); + } + + // Firestore and Storage + const result = await testRuleset(projectId, [ + { name: "test.rules", content: rulesSourceContent }, + ]); + + if (result.body?.issues?.length) { + const issues = result.body.issues as unknown as Issue[]; + let out = `Found ${issues.length} issues in rules source:\n\n`; + out += formatRulesetIssues(issues, rulesSourceContent); + return toContent(out); + } + + return toContent("OK: No errors detected."); + }, +); diff --git a/src/mcp/tools/crashlytics/events.ts b/src/mcp/tools/crashlytics/events.ts new file mode 100644 index 00000000000..7812cf06089 --- /dev/null +++ b/src/mcp/tools/crashlytics/events.ts @@ -0,0 +1,82 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { batchGetEvents, listEvents } from "../../../crashlytics/events"; +import { + BatchGetEventsResponse, + ErrorType, + Event, + ListEventsResponse, +} from "../../../crashlytics/types"; +import { ApplicationIdSchema, EventFilterSchema } from "../../../crashlytics/filters"; + +function pruneThreads(sample: Event): Event { + if (sample.issue?.errorType === ErrorType.FATAL || sample.issue?.errorType === ErrorType.ANR) { + // Remove irrelevant threads from the response to reduce token usage + sample.threads = sample.threads?.filter((t) => t.crashed || t.blamed); + } + return sample; +} + +export const list_events = tool( + { + name: "list_events", + description: `Use this to list the most recent events matching the given filters. + Can be used to fetch sample crashes and exceptions for an issue, + which will include stack traces and other data useful for debugging.`, + inputSchema: z.object({ + appId: ApplicationIdSchema, + filter: EventFilterSchema, + pageSize: z.number().describe("Number of rows to return").default(1), + }), + annotations: { + title: "List Crashlytics Events", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + }, + }, + async ({ appId, filter, pageSize }) => { + if (!appId) return mcpError(`Must specify 'appId' parameter.`); + if (!filter || (!filter.issueId && !filter.issueVariantId)) + return mcpError(`Must specify 'filter.issueId' or 'filter.issueVariantId' parameters.`); + + const response: ListEventsResponse = await listEvents(appId, filter, pageSize); + response.events = response.events ? response.events.map((e) => pruneThreads(e)) : []; + return toContent(response); + }, +); + +export const batch_get_events = tool( + { + name: "batch_get_events", + description: `Gets specific events by resource name. + Can be used to fetch sample crashes and exceptions for an issue, + which will include stack traces and other data useful for debugging.`, + inputSchema: z.object({ + appId: ApplicationIdSchema, + names: z + .array(z.string()) + .describe( + "An array of the event resource names, as found in the sampleEvent field in reports.", + ), + }), + annotations: { + title: "Batch Get Crashlytics Events", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + }, + }, + async ({ appId, names }) => { + if (!appId) return mcpError(`Must specify 'appId' parameter.`); + if (!names || names.length === 0) + return mcpError(`Must provide event resource names in name parameter.`); + + const response: BatchGetEventsResponse = await batchGetEvents(appId, names); + response.events = response.events ? response.events.map((e) => pruneThreads(e)) : []; + return toContent(response); + }, +); diff --git a/src/mcp/tools/crashlytics/index.ts b/src/mcp/tools/crashlytics/index.ts new file mode 100644 index 00000000000..4d11ddf7169 --- /dev/null +++ b/src/mcp/tools/crashlytics/index.ts @@ -0,0 +1,28 @@ +import type { ServerTool } from "../../tool"; +import { create_note, list_notes, delete_note } from "./notes"; +import { get_issue, update_issue } from "./issues"; +import { list_events, batch_get_events } from "./events"; +import { + get_top_issues, + get_top_variants, + get_top_versions, + get_top_apple_devices, + get_top_operating_systems, + get_top_android_devices, +} from "./reports"; + +export const crashlyticsTools: ServerTool[] = [ + create_note, + delete_note, + get_issue, + list_events, + batch_get_events, + list_notes, + get_top_issues, + get_top_variants, + get_top_versions, + get_top_apple_devices, + get_top_android_devices, + get_top_operating_systems, + update_issue, +]; diff --git a/src/mcp/tools/crashlytics/issues.ts b/src/mcp/tools/crashlytics/issues.ts new file mode 100644 index 00000000000..fcec8ffee6f --- /dev/null +++ b/src/mcp/tools/crashlytics/issues.ts @@ -0,0 +1,58 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { getIssue, updateIssue } from "../../../crashlytics/issues"; +import { State } from "../../../crashlytics/types"; +import { ApplicationIdSchema, IssueIdSchema } from "../../../crashlytics/filters"; + +export const get_issue = tool( + { + name: "get_issue", + description: `Gets data for a Crashlytics issue, which can be used as a starting point for debugging.`, + inputSchema: z.object({ + appId: ApplicationIdSchema, + issueId: IssueIdSchema, + }), + annotations: { + title: "Get Crashlytics Issue Details", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + }, + }, + async ({ appId, issueId }) => { + if (!appId) return mcpError(`Must specify 'appId' parameter.`); + if (!issueId) return mcpError(`Must specify 'issueId' parameter.`); + + return toContent(await getIssue(appId, issueId)); + }, +); + +export const update_issue = tool( + { + name: "update_issue", + description: "Use this to update the state of Crashlytics issue.", + inputSchema: z.object({ + appId: ApplicationIdSchema, + issueId: IssueIdSchema, + state: z + .nativeEnum(State) + .describe("The new state for the issue. Can be 'OPEN' or 'CLOSED'."), + }), + annotations: { + title: "Update Crashlytics Issue", + readOnlyHint: false, + }, + _meta: { + requiresAuth: true, + }, + }, + async ({ appId, issueId, state }) => { + if (!appId) return mcpError(`Must specify 'app_id' parameter.`); + if (!issueId) return mcpError(`Must specify 'issue_id' parameter.`); + if (!state) return mcpError(`Must specify 'state' parameter.`); + + return toContent(await updateIssue(appId, issueId, state)); + }, +); diff --git a/src/mcp/tools/crashlytics/notes.ts b/src/mcp/tools/crashlytics/notes.ts new file mode 100644 index 00000000000..c340ec00633 --- /dev/null +++ b/src/mcp/tools/crashlytics/notes.ts @@ -0,0 +1,83 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { createNote, listNotes, deleteNote } from "../../../crashlytics/notes"; +import { ApplicationIdSchema, IssueIdSchema } from "../../../crashlytics/filters"; + +export const create_note = tool( + { + name: "create_note", + description: "Add a note to an issue from crashlytics.", + inputSchema: z.object({ + appId: ApplicationIdSchema, + issueId: IssueIdSchema, + note: z.string().describe("The note to add to the issue."), + }), + annotations: { + title: "Add note to Crashlytics issue.", + readOnlyHint: false, + }, + _meta: { + requiresAuth: true, + }, + }, + async ({ appId, issueId, note }) => { + if (!appId) return mcpError(`Must specify 'appId' parameter.`); + if (!issueId) return mcpError(`Must specify 'issueId' parameter.`); + if (!note) return mcpError(`Must specify 'note' parameter.`); + + return toContent(await createNote(appId, issueId, note)); + }, +); + +export const list_notes = tool( + { + name: "list_notes", + description: "Use this to list all notes for an issue in Crashlytics.", + inputSchema: z.object({ + appId: ApplicationIdSchema, + issueId: IssueIdSchema, + pageSize: z.number().optional().default(20).describe("Number of rows to return"), + }), + annotations: { + title: "List notes for a Crashlytics issue.", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + }, + }, + async ({ appId, issueId, pageSize }) => { + if (!appId) return mcpError(`Must specify 'appId' parameter.`); + if (!issueId) return mcpError(`Must specify 'issueId' parameter.`); + + return toContent(await listNotes(appId, issueId, pageSize)); + }, +); + +export const delete_note = tool( + { + name: "delete_note", + description: "Delete a note from a Crashlytics issue.", + inputSchema: z.object({ + appId: ApplicationIdSchema, + issueId: IssueIdSchema, + noteId: z.string().describe("The id of the note to delete"), + }), + annotations: { + title: "Delete Crashlytics Issue Note", + readOnlyHint: false, + destructiveHint: true, + }, + _meta: { + requiresAuth: true, + }, + }, + async ({ appId, issueId, noteId }) => { + if (!appId) return mcpError(`Must specify 'appId' parameter.`); + if (!issueId) return mcpError(`Must specify 'issueId' parameter.`); + if (!noteId) return mcpError(`Must specify 'noteId' parameter.`); + + return toContent(await deleteNote(appId, issueId, noteId)); + }, +); diff --git a/src/mcp/tools/crashlytics/reports.ts b/src/mcp/tools/crashlytics/reports.ts new file mode 100644 index 00000000000..63fa7639d5a --- /dev/null +++ b/src/mcp/tools/crashlytics/reports.ts @@ -0,0 +1,185 @@ +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { + CrashlyticsReport, + getReport, + ReportInputSchema, + ReportInput, + simplifyReport, +} from "../../../crashlytics/reports"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { validateEventFilters } from "../../../crashlytics/filters"; + +// Generates the tool call fn for requesting a Crashlytics report + +function getReportContent( + report: CrashlyticsReport, + additionalPrompt?: string, +): (input: ReportInput) => Promise { + return async ({ appId, filter, pageSize }) => { + if (!appId) return mcpError(`Must specify 'appId' parameter.`); + filter ??= {}; + if (!!filter.intervalStartTime && !filter.intervalEndTime) { + // interval.end_time is required if interval.start_time is set but the agent likes to forget it + filter.intervalEndTime = new Date().toISOString(); + } + if (report === CrashlyticsReport.TopIssues && !!filter.issueId) { + delete filter.issueId; + } + validateEventFilters(filter); // throws here if invalid filters + const reportResponse = simplifyReport(await getReport(report, appId, filter, pageSize)); + if (!reportResponse.groups?.length) { + additionalPrompt = "This report response contains no results."; + } + if (additionalPrompt) { + reportResponse.usage = (reportResponse.usage || "").concat("\n", additionalPrompt); + } + return toContent(reportResponse); + }; +} + +// Currently, it appears to work best if the five different supported reports +// are expressed as five different tools. This allows the usage and format +// of each report to be more clearly described. In the future, it may be possible +// to consolidate all of these into a single `get_report` tool. + +export const get_top_issues = tool( + { + name: "get_top_issues", + description: `Use this to count events and distinct impacted users, grouped by *issue*. + Groups are sorted by event count, in descending order. + Only counts events matching the given filters.`, + inputSchema: ReportInputSchema, + annotations: { + title: "Get Crashlytics Top Issues Report", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + }, + }, + getReportContent( + CrashlyticsReport.TopIssues, + `The crashlytics_batch_get_event tool can retrieve the sample events in this response. + Pass the sampleEvent in the names field. + The crashlytics_list_events tool can retrieve a list of events for an issue in this response. + Pass the issue.id in the filter.issueId field.`, + ), +); + +export const get_top_variants = tool( + { + name: "get_top_variants", + description: `Counts events and distinct impacted users, grouped by issue *variant*. + Groups are sorted by event count, in descending order. + Only counts events matching the given filters.`, + inputSchema: ReportInputSchema, + annotations: { + title: "Get Crashlytics Top Variants Report", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + }, + }, + getReportContent( + CrashlyticsReport.TopVariants, + `The crashlytics_get_top_issues tool can report the top issues for the variants in this response. + Pass the variant.displayName in the filter.variantDisplayNames field. + The crashlytics_list_events tool can retrieve a list of events for a variant in this response.`, + ), +); + +export const get_top_versions = tool( + { + name: "get_top_versions", + description: `Counts events and distinct impacted users, grouped by *version*. + Groups are sorted by event count, in descending order. + Only counts events matching the given filters.`, + inputSchema: ReportInputSchema, + annotations: { + title: "Get Crashlytics Top Versions Report", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + }, + }, + getReportContent( + CrashlyticsReport.TopVersions, + `The crashlytics_get_top_issues tool can report the top issues for the versions in this response. + Pass the version.displayName in the filter.versionDisplayNames field. + The crashlytics_list_events tool can retrieve a list of events for a version in this response.`, + ), +); + +export const get_top_apple_devices = tool( + { + name: "get_top_apple_devices", + description: `Counts events and distinct impacted users, grouped by apple *device*. + Groups are sorted by event count, in descending order. + Only counts events matching the given filters. + Only relevant for iOS, iPadOS and MacOS applications.`, + inputSchema: ReportInputSchema, + annotations: { + title: "Get Crashlytics Top Apple Devices Report", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + }, + }, + getReportContent( + CrashlyticsReport.TopAppleDevices, + `The crashlytics_get_top_issues tool can report the top issues for the devices in this response. + Pass the device.displayName in the filter.deviceDisplayNames field. + The crashlytics_list_events tool can retrieve a list of events for a device in this response.`, + ), +); + +export const get_top_android_devices = tool( + { + name: "get_top_android_devices", + description: `Counts events and distinct impacted users, grouped by android *device*. + Groups are sorted by event count, in descending order. + Only counts events matching the given filters. + Only relevant for Android applications.`, + inputSchema: ReportInputSchema, + annotations: { + title: "Get Crashlytics Top Android Devices Report", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + }, + }, + getReportContent( + CrashlyticsReport.TopAndroidDevices, + `The crashlytics_get_top_issues tool can report the top issues for the devices in this response. + Pass the device.displayName in the filter.deviceDisplayNames field. + The crashlytics_list_events tool can retrieve a list of events for a device in this response.`, + ), +); + +export const get_top_operating_systems = tool( + { + name: "get_top_operating_systems", + description: `Counts events and distinct impacted users, grouped by *operating system*. + Groups are sorted by event count, in descending order. + Only counts events matching the given filters.`, + inputSchema: ReportInputSchema, + annotations: { + title: "Get Crashlytics Top Operating Systems Report", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + }, + }, + getReportContent( + CrashlyticsReport.TopOperatingSystems, + `The crashlytics_get_top_issues tool can report the top issues for the operating systems in this response. + Pass the operatingSystem.displayName in the filter.operatingSystemDisplayNames field. + The crashlytics_list_events tool can retrieve a list of events for an operating system in this response.`, + ), +); diff --git a/src/mcp/tools/dataconnect/compile.ts b/src/mcp/tools/dataconnect/compile.ts new file mode 100644 index 00000000000..e5904385051 --- /dev/null +++ b/src/mcp/tools/dataconnect/compile.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { pickService } from "../../../dataconnect/load"; +import { compileErrors } from "../../util/dataconnect/compile"; + +export const compile = tool( + { + name: "build", + description: + "Use this to compile Firebase Data Connect schema, operations, and/or connectors and check for build errors.", + inputSchema: z.object({ + error_filter: z + .enum(["all", "schema", "operations"]) + .describe("filter errors to a specific type only. defaults to `all` if omitted.") + .optional(), + service_id: z + .string() + .optional() + .describe( + "The Firebase Data Connect service ID to look for. If omitted, builds all services defined in `firebase.json`.", + ), + }), + annotations: { + title: "Compile Data Connect", + readOnlyHint: true, + }, + _meta: { + requiresProject: false, + requiresAuth: false, + }, + }, + async ({ service_id, error_filter }, { projectId, config }) => { + const serviceInfo = await pickService(projectId, config, service_id || undefined); + const errors = await compileErrors(serviceInfo.sourceDirectory, error_filter); + if (errors) + return { + content: [ + { + type: "text", + text: `The following errors were encountered while compiling Data Connect from directory \`${serviceInfo.sourceDirectory}\`:\n\n${errors}`, + }, + ], + isError: true, + }; + return { content: [{ type: "text", text: "Compiled successfully." }] }; + }, +); diff --git a/src/mcp/tools/dataconnect/execute.ts b/src/mcp/tools/dataconnect/execute.ts new file mode 100644 index 00000000000..6143c7df01f --- /dev/null +++ b/src/mcp/tools/dataconnect/execute.ts @@ -0,0 +1,89 @@ +import { z } from "zod"; + +import { tool } from "../../tool"; +import * as dataplane from "../../../dataconnect/dataplaneClient"; +import { pickService } from "../../../dataconnect/load"; +import { graphqlResponseToToolResponse, parseVariables } from "../../util/dataconnect/converter"; +import { getDataConnectEmulatorClient } from "../../util/dataconnect/emulator"; +import { Client } from "../../../apiv2"; + +export const execute = tool( + { + name: "execute", + description: + "Use this to execute a GraphQL operation against a Data Connect service or its emulator.", + inputSchema: z.object({ + query: z.string().describe(`A Firebase Data Connect GraphQL query or mutation to execute. +You can use the \`dataconnect_generate_operation\` tool to generate a query. +Example Data Connect schema and example queries can be found in files ending in \`.graphql\` or \`.gql\`. +`), + service_id: z.string().optional() + .describe(`Data Connect Service ID to dis-ambulate if there are multiple. +It's only necessary if there are multiple dataconnect sources in \`firebase.json\`. +You can find candidate service_id in \`dataconnect.yaml\` +`), + variables_json: z + .string() + .optional() + .describe( + "GraphQL variables to pass into the query. MUST be a valid stringified JSON object.", + ), + auth_token_json: z + .string() + .optional() + .describe( + "Firebase Auth Token JWT to use in this query. MUST be a valid stringified JSON object." + + 'Importantly, when executing queries with `@auth(level: USER)` or `auth.uid`, a valid Firebase Auth Token JWT with "sub" field is required. ' + + '"auth.uid" expression in the query evaluates to the value of "sub" field in Firebase Auth token.', + ), + use_emulator: z + .boolean() + .default(false) + .describe( + "If true, target the DataConnect emulator. Run `firebase emulators:start` to start it", + ), + }), + annotations: { + title: "Execute Firebase Data Connect Query", + }, + _meta: { + requiresProject: true, + requiresAuth: true, + }, + }, + async ( + { + query, + service_id, + variables_json: unparsedVariables, + use_emulator, + auth_token_json: unparsedAuthToken, + }, + { projectId, config, host }, + ) => { + const serviceInfo = await pickService(projectId, config, service_id || undefined); + let apiClient: Client; + if (use_emulator) { + apiClient = await getDataConnectEmulatorClient(host); + } else { + apiClient = dataplane.dataconnectDataplaneClient(); + } + let executeGraphQL = dataplane.executeGraphQL; + if (query.startsWith("query")) { + executeGraphQL = dataplane.executeGraphQLRead; + } + const response = await executeGraphQL(apiClient, serviceInfo.serviceName, { + name: "", + query, + variables: parseVariables(unparsedVariables), + extensions: { + impersonate: unparsedAuthToken + ? { + authClaims: parseVariables(unparsedAuthToken), + } + : undefined, + }, + }); + return graphqlResponseToToolResponse(response.body); + }, +); diff --git a/src/mcp/tools/dataconnect/generate_operation.ts b/src/mcp/tools/dataconnect/generate_operation.ts new file mode 100644 index 00000000000..731afc7adc7 --- /dev/null +++ b/src/mcp/tools/dataconnect/generate_operation.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { generateOperation } from "../../../gemini/fdcExperience"; +import { pickService } from "../../../dataconnect/load"; + +export const generate_operation = tool( + { + name: "generate_operation", + description: + "Use this to generate a single Firebase Data Connect query or mutation based on the currently deployed schema and the provided prompt.", + inputSchema: z.object({ + // Lifted guidance from : https://cloud.google.com/gemini/docs/discover/write-prompts + prompt: z + .string() + .describe( + "Write the prompt like you're talking to a person, describe the task you're trying to accomplish and give details that are specific to the users request", + ), + service_id: z + .string() + .optional() + .describe( + "Optional: Uses the service ID from the firebase.json file if nothing provided. The service ID of the deployed Firebase resource.", + ), + }), + annotations: { + title: "Generate Data Connect Operation", + readOnlyHint: true, + }, + _meta: { + requiresProject: true, + requiresAuth: true, + requiresGemini: true, + }, + }, + async ({ prompt, service_id }, { projectId, config }) => { + const serviceInfo = await pickService(projectId, config, service_id || undefined); + const schema = await generateOperation(prompt, serviceInfo.serviceName, projectId); + return toContent(schema); + }, +); diff --git a/src/mcp/tools/dataconnect/generate_schema.ts b/src/mcp/tools/dataconnect/generate_schema.ts new file mode 100644 index 00000000000..e04dfb8c48e --- /dev/null +++ b/src/mcp/tools/dataconnect/generate_schema.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { generateSchema } from "../../../gemini/fdcExperience"; + +export const generate_schema = tool( + { + name: "generate_schema", + description: + "Use this to generate a Firebase Data Connect Schema based on the users description of an app.", + inputSchema: z.object({ + prompt: z.string().describe("A description of an app that you are interested in building"), + }), + annotations: { + title: "Generate Data Connect Schema", + readOnlyHint: true, + }, + _meta: { + requiresProject: true, + requiresAuth: true, + requiresGemini: true, + }, + }, + async ({ prompt }, { projectId }) => { + const schema = await generateSchema(prompt, projectId); + return toContent(schema); + }, +); diff --git a/src/mcp/tools/dataconnect/index.ts b/src/mcp/tools/dataconnect/index.ts new file mode 100644 index 00000000000..2bdf339f91a --- /dev/null +++ b/src/mcp/tools/dataconnect/index.ts @@ -0,0 +1,14 @@ +import type { ServerTool } from "../../tool"; +import { generate_operation } from "./generate_operation"; +import { generate_schema } from "./generate_schema"; +import { list_services } from "./list_services"; +import { compile } from "./compile"; +import { execute } from "./execute"; + +export const dataconnectTools: ServerTool[] = [ + compile, + generate_schema, + generate_operation, + list_services, + execute, +]; diff --git a/src/mcp/tools/dataconnect/list_services.ts b/src/mcp/tools/dataconnect/list_services.ts new file mode 100644 index 00000000000..b26dbaa0884 --- /dev/null +++ b/src/mcp/tools/dataconnect/list_services.ts @@ -0,0 +1,149 @@ +import { z } from "zod"; +import * as path from "path"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import * as client from "../../../dataconnect/client"; +import { loadAll } from "../../../dataconnect/load"; +import { Service, Schema, ServiceInfo, Connector } from "../../../dataconnect/types"; +import { dump } from "js-yaml"; +import { logger } from "../../../logger"; + +interface CombinedServiceInfo { + local?: ServiceInfo; + deployed?: DeployServiceInfo; +} + +interface DeployServiceInfo { + service?: Service; + schemas?: Schema[]; + connectors?: Connector[]; +} + +export const list_services = tool( + { + name: "list_services", + description: "Use this to list existing local and backend Firebase Data Connect services", + inputSchema: z.object({}), + annotations: { + title: "List existing Firebase Data Connect services", + readOnlyHint: true, + }, + _meta: { + requiresProject: false, + requiresAuth: false, + }, + }, + async (_, { projectId, config }) => { + const localServiceInfos = await loadAll(projectId, config); + const serviceInfos = new Map(); + + for (const l of localServiceInfos) { + serviceInfos.set( + `locations/${l.dataConnectYaml.location}/services/${l.dataConnectYaml.serviceId}`, + { local: l }, + ); + } + + if (projectId) { + try { + const [services, schemas, connectors] = await Promise.all([ + client.listAllServices(projectId), + client.listSchemas(`projects/${projectId}/locations/-/services/-`), + client.listConnectors(`projects/${projectId}/locations/-/services/-`), + ]); + console.log(services, schemas, connectors); + for (const s of services) { + const k = s.name.split("/").slice(2, 6).join("/"); + const st = serviceInfos.get(k) || {}; + st.deployed = st.deployed || {}; + st.deployed.service = s; + serviceInfos.set(k, st); + } + for (const s of schemas) { + const k = s.name.split("/").slice(2, 6).join("/"); + const st = serviceInfos.get(k) || {}; + st.deployed = st.deployed || {}; + st.deployed.schemas = st.deployed.schemas || []; + st.deployed.schemas.push(s); + serviceInfos.set(k, st); + } + for (const s of connectors) { + const k = s.name.split("/").slice(2, 6).join("/"); + const st = serviceInfos.get(k) || {}; + st.deployed = st.deployed || {}; + st.deployed.connectors = st.deployed.connectors || []; + st.deployed.connectors.push(s); + serviceInfos.set(k, st); + } + } catch (e: any) { + logger.debug("cannot fetch dataconnect resources in the backend", e); + } + } + + const localServices = Array.from(serviceInfos.values()).filter((s) => s.local); + const remoteOnlyServices = Array.from(serviceInfos.values()).filter((s) => !s.local); + + const output: string[] = []; + + function includeDeployedServiceInfo(deployed: DeployServiceInfo): void { + if (deployed.schemas?.length) { + output.push(`### Schemas`); + for (const s of deployed.schemas) { + clearCCFEFields(s); + output.push(dump(s)); + } + } + if (deployed.connectors?.length) { + output.push(`### Connectors`); + for (const c of deployed.connectors) { + clearCCFEFields(c); + output.push(dump(c)); + } + } + } + + if (localServices.length) { + output.push(`# Local Data Connect Sources`); + for (const s of localServices) { + const local = s.local!; + output.push(dump(local.dataConnectYaml)); + const schemaDir = path.join(local.sourceDirectory, local.dataConnectYaml.schema.source); + output.push(`You can find all of schema sources under ${schemaDir}/`); + if (s.deployed) { + output.push(`It's already deployed in the backend:\n`); + includeDeployedServiceInfo(s.deployed); + } + } + } + + if (remoteOnlyServices.length) { + output.push(`# Data Connect Services in project ${projectId}`); + for (const s of remoteOnlyServices) { + if (s.deployed) { + includeDeployedServiceInfo(s.deployed); + } + } + } + + output.push(`\n# What's next?`); + if (!localServices.length) { + output.push( + `- There is no local Data Connect service in the local workspace. Consider use the \`firebase_init\` MCP tool to setup one.`, + ); + } + output.push( + `- You can use the \`dataconnect_compile\` tool to compile all local Data Connect schemas and query sources.`, + ); + output.push( + `- You run \`firebase deploy\` in command line to deploy the Data Connect schemas, connector and perform SQL migrations.`, + ); + return toContent(output.join("\n")); + }, +); + +function clearCCFEFields(r: any): void { + const fieldsToClear = ["updateTime", "uid", "etag"]; + for (const k of fieldsToClear) { + delete r[k]; + } +} diff --git a/src/mcp/tools/firestore/converter.ts b/src/mcp/tools/firestore/converter.ts new file mode 100644 index 00000000000..7f20e45f77d --- /dev/null +++ b/src/mcp/tools/firestore/converter.ts @@ -0,0 +1,136 @@ +import { FirestoreDocument, FirestoreValue } from "../../../gcp/firestore"; +import { logger } from "../../../logger"; + +/** + * Takes an arbitrary value from a user and returns a FirestoreValue equivalent. + * @param {any} inputValue the JSON object input value. + * return FirestoreValue a firestorevalue object used in the Firestore API. + */ +export function convertInputToValue(inputValue: any): FirestoreValue { + if (inputValue === null) { + return { nullValue: null }; + } else if (typeof inputValue === "boolean") { + return { booleanValue: inputValue }; + } else if (typeof inputValue === "number") { + // Distinguish between integers and doubles + if (Number.isInteger(inputValue)) { + return { integerValue: inputValue.toString() }; // Represent integers as string for consistency with Firestore + } else { + return { doubleValue: inputValue }; + } + } else if (typeof inputValue === "string") { + // This is a simplification. In a real-world scenario, you might want to + // check for specific string formats like timestamp, bytes, or referenceValue. + // For now, it defaults to stringValue. + return { stringValue: inputValue }; + } else if (Array.isArray(inputValue)) { + const arrayValue: { values?: FirestoreValue[] } = { + values: inputValue.map((item) => convertInputToValue(item)), + }; + return { arrayValue: arrayValue }; + } else if (typeof inputValue === "object") { + // Check for LatLng structure + if ( + inputValue.hasOwnProperty("latitude") && + typeof inputValue.latitude === "number" && + inputValue.hasOwnProperty("longitude") && + typeof inputValue.longitude === "number" + ) { + return { geoPointValue: inputValue as { latitude: number; longitude: number } }; + } + + // Otherwise, treat as a MapValue + const mapValue: { fields?: Record } = { + fields: {}, + }; + for (const key in inputValue) { + if (Object.prototype.hasOwnProperty.call(inputValue, key)) { + if (mapValue.fields) { + mapValue.fields[key] = convertInputToValue(inputValue[key]); + } + } + } + return { mapValue: mapValue }; + } + // Fallback for unsupported types (e.g., undefined, functions, symbols) + return { nullValue: null }; +} + +/** + * Converts a Firestore REST API Value object to a plain Javascript object, + * applying special transformations for Reference and GeoPoint types, and + * handling integer values potentially larger than JS MAX_SAFE_INTEGER. + * @param {FirestoreValue} firestoreValue The Firestore Value object. + * @return {any} The plain Javascript object. + */ +function firestoreValueToJson(firestoreValue: FirestoreValue): any { + if ("nullValue" in firestoreValue) return null; + if ("booleanValue" in firestoreValue) return firestoreValue.booleanValue; + if ("integerValue" in firestoreValue) { + // Firestore returns integers as strings in REST. Convert to Number if safe, + // otherwise keep as string to avoid precision loss for int64. + const num = Number(firestoreValue.integerValue); + if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) { + return firestoreValue.integerValue; // Keep as string for large integers (int64) + } + return num; // Convert to number if within safe integer range + } + if ("doubleValue" in firestoreValue) return firestoreValue.doubleValue; + if ("timestampValue" in firestoreValue) + return { __type__: "Timestamp", value: firestoreValue.timestampValue }; + if ("stringValue" in firestoreValue) return firestoreValue.stringValue; + if ("bytesValue" in firestoreValue) return firestoreValue.bytesValue; + if ("referenceValue" in firestoreValue) + return { __type__: "Reference", value: firestoreValue.referenceValue }; + if ("geoPointValue" in firestoreValue) + return { + __type__: "GeoPoint", + value: [firestoreValue.geoPointValue.latitude, firestoreValue.geoPointValue.longitude], + }; + if ("arrayValue" in firestoreValue) + return firestoreValue.arrayValue.values?.map((v) => firestoreValueToJson(v)) ?? []; + if ("mapValue" in firestoreValue) { + const map = firestoreValue.mapValue.fields || {}; + const obj: { [key: string]: any } = {}; + // Recursively convert map values + for (const key of Object.keys(map)) { + obj[key] = firestoreValueToJson(map[key]); + } + return obj; + } + // Should not happen with a valid FirestoreValue from the API + logger.warn("Unhandled Firestore Value type encountered:", firestoreValue); + return undefined; // Or throw an error +} + +/** + * Converts a Firestore REST API Document object to a plain Javascript object. + * Follows specific conversion rules for certain types (Reference, GeoPoint, Int64). + * Includes the document ID extracted from the 'name' field as `__id__`. + * + * Fields that are not set in the document will be omitted. + * @param {FirestoreDocument} firestoreDoc The Firestore Document object. + * @return {{ __id__: string; [key: string]: any }} The plain Javascript object. + */ +export function firestoreDocumentToJson(firestoreDoc: FirestoreDocument): { + __path__: string; + [key: string]: any; +} { + // Extract ID from the document name (last segment after '/documents/'). + // Format: projects/{projectId}/databases/{databaseId}/documents/{document_path} + const nameParts = firestoreDoc.name.split("/documents/"); + const path = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ""; + + // Initialize the result object with the extracted ID. + const result: ReturnType = { __path__: path }; + + // If the document has fields, process them using the helper function. + if (firestoreDoc.fields) { + for (const key of Object.keys(firestoreDoc.fields)) { + result[key] = firestoreValueToJson(firestoreDoc.fields[key]); + } + } + + // Return the resulting JSON object. + return result; +} diff --git a/src/mcp/tools/firestore/delete_document.ts b/src/mcp/tools/firestore/delete_document.ts new file mode 100644 index 00000000000..6316e3b26dc --- /dev/null +++ b/src/mcp/tools/firestore/delete_document.ts @@ -0,0 +1,62 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { getDocuments } from "../../../gcp/firestore"; +import { FirestoreDelete } from "../../../firestore/delete"; +import { Emulators } from "../../../emulator/types"; + +export const delete_document = tool( + { + name: "delete_document", + description: + "Use this to delete a Firestore documents from a database in the current project by full document paths. Use this if you know the exact path of a document.", + inputSchema: z.object({ + database: z + .string() + .optional() + .describe("Database id to use. Defaults to `(default)` if unspecified."), + path: z + .string() + .describe( + "A document path (e.g. `collectionName/documentId` or `parentCollection/parentDocument/collectionName/documentId`)", + ), + use_emulator: z.boolean().default(false).describe("Target the Firestore emulator if true."), + }), + annotations: { + title: "Delete Firestore document", + destructiveHint: true, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ({ path, database, use_emulator }, { projectId, host }) => { + let emulatorUrl: string | undefined; + if (use_emulator) { + emulatorUrl = await host.getEmulatorUrl(Emulators.FIRESTORE); + } + const { documents, missing } = await getDocuments(projectId, [path], database, emulatorUrl); + if (missing.length > 0 && documents && documents.length === 0) { + return mcpError(`None of the specified documents were found in project '${projectId}'`); + } + + const firestoreDelete = new FirestoreDelete(projectId, path, { + databaseId: database ?? "(default)", + urlPrefix: emulatorUrl, + }); + + await firestoreDelete.execute(); + + const { documents: postDeleteDocuments, missing: postDeleteMissing } = await getDocuments( + projectId, + [path], + emulatorUrl, + ); + if (postDeleteMissing.length > 0 && postDeleteDocuments.length === 0) { + return toContent(`Successfully removed document located at : ${path}`); + } + + return mcpError(`Failed to remove document located at : ${path}`); + }, +); diff --git a/src/mcp/tools/firestore/get_documents.ts b/src/mcp/tools/firestore/get_documents.ts new file mode 100644 index 00000000000..ea989b24dc6 --- /dev/null +++ b/src/mcp/tools/firestore/get_documents.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { getDocuments } from "../../../gcp/firestore"; +import { firestoreDocumentToJson } from "./converter"; +import { Emulators } from "../../../emulator/types"; + +export const get_documents = tool( + { + name: "get_documents", + description: + "Use this to retrieve one or more Firestore documents from a database in the current project by full document paths. Use this if you know the exact path of a document.", + inputSchema: z.object({ + database: z + .string() + .optional() + .describe("Database id to use. Defaults to `(default)` if unspecified."), + paths: z + .array(z.string()) + .describe( + "One or more document paths (e.g. `collectionName/documentId` or `parentCollection/parentDocument/collectionName/documentId`)", + ), + use_emulator: z.boolean().default(false).describe("Target the Firestore emulator if true."), + }), + annotations: { + title: "Get Firestore documents", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ({ paths, database, use_emulator }, { projectId, host }) => { + if (!paths || !paths.length) return mcpError("Must supply at least one document path."); + + let emulatorUrl: string | undefined; + if (use_emulator) { + emulatorUrl = await host.getEmulatorUrl(Emulators.FIRESTORE); + } + const { documents, missing } = await getDocuments(projectId, paths, database, emulatorUrl); + if (missing.length > 0 && documents && documents.length === 0) { + return mcpError(`None of the specified documents were found in project '${projectId}'`); + } + + const docs = documents.map(firestoreDocumentToJson); + + if (documents.length === 1 && missing.length === 0) { + // return a single document as YAML if that's all we have/need + return toContent(docs[0]); + } + const docsContent = toContent(docs); + if (missing.length) { + docsContent.content = [ + { type: "text", text: "Retrieved documents:\n\n" }, + ...docsContent.content, + { + type: "text", + text: `The following documents do not exist: ${missing.join(", ")}`, + }, + ]; + } + return docsContent; + }, +); diff --git a/src/mcp/tools/firestore/index.ts b/src/mcp/tools/firestore/index.ts new file mode 100644 index 00000000000..5cca0cff22c --- /dev/null +++ b/src/mcp/tools/firestore/index.ts @@ -0,0 +1,6 @@ +import { delete_document } from "./delete_document"; +import { get_documents } from "./get_documents"; +import { list_collections } from "./list_collections"; +import { query_collection } from "./query_collection"; + +export const firestoreTools = [delete_document, get_documents, list_collections, query_collection]; diff --git a/src/mcp/tools/firestore/list_collections.ts b/src/mcp/tools/firestore/list_collections.ts new file mode 100644 index 00000000000..a0a98a17fc7 --- /dev/null +++ b/src/mcp/tools/firestore/list_collections.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { listCollectionIds } from "../../../gcp/firestore"; +import { Emulators } from "../../../emulator/types"; + +export const list_collections = tool( + { + name: "list_collections", + description: + "Use this to retrieve a list of collections from a Firestore database in the current project.", + inputSchema: z.object({ + // TODO: support multiple databases + database: z + .string() + .optional() + .describe("Database id to use. Defaults to `(default)` if unspecified."), + use_emulator: z.boolean().default(false).describe("Target the Firestore emulator if true."), + }), + annotations: { + title: "List Firestore collections", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ({ database, use_emulator }, { projectId, host }) => { + // database ??= "(default)"; + let emulatorUrl: string | undefined; + if (use_emulator) { + emulatorUrl = await host.getEmulatorUrl(Emulators.FIRESTORE); + } + return toContent(await listCollectionIds(projectId, database, emulatorUrl)); + }, +); diff --git a/src/mcp/tools/firestore/query_collection.ts b/src/mcp/tools/firestore/query_collection.ts new file mode 100644 index 00000000000..8b708e568ba --- /dev/null +++ b/src/mcp/tools/firestore/query_collection.ts @@ -0,0 +1,151 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { queryCollection, StructuredQuery } from "../../../gcp/firestore"; +import { convertInputToValue, firestoreDocumentToJson } from "./converter"; +import { Emulators } from "../../../emulator/types"; + +export const query_collection = tool( + { + name: "query_collection", + description: + "Use this to retrieve one or more Firestore documents from a collection is a database in the current project by a collection with a full document path. Use this if you know the exact path of a collection and the filtering clause you would like for the document.", + inputSchema: z.object({ + database: z + .string() + .optional() + .describe("Database id to use. Defaults to `(default)` if unspecified."), + collection_path: z + .string() + .describe( + "A collection path (e.g. `collectionName/` or `parentCollection/parentDocument/collectionName`)", + ), + filters: z + .object({ + compare_value: z + .object({ + string_value: z.string().optional().describe("The string value to compare against."), + boolean_value: z + .string() + .optional() + .describe("The boolean value to compare against."), + string_array_value: z + .array(z.string()) + .optional() + .describe("The string value to compare against."), + integer_value: z + .number() + .optional() + .describe("The integer value to compare against."), + double_value: z.number().optional().describe("The double value to compare against."), + }) + .describe("One and only one value may be specified per filters object."), + field: z.string().describe("the field searching against"), + op: z + .enum([ + "OPERATOR_UNSPECIFIED", + "LESS_THAN", + "LESS_THAN_OR_EQUAL", + "GREATER_THAN", + "GREATER_THAN_OR_EQUAL", + "EQUAL", + "NOT_EQUAL", + "ARRAY_CONTAINS", + "ARRAY_CONTAINS_ANY", + "IN", + "NOT_IN", + ]) + .describe("the equality evaluator to use"), + }) + .array() + .describe("the multiple filters to use in querying against the existing collection."), + order: z + .object({ + orderBy: z.string().describe("the field to order by"), + orderByDirection: z + .enum(["ASCENDING", "DESCENDING"]) + .describe("the direction to order values"), + }) + .optional() + .describe( + "Specifies the field and direction to order the results. If not provided, the order is undefined.", + ), + limit: z + .number() + .describe("The maximum amount of records to return. Default is 10.") + .optional(), + use_emulator: z.boolean().default(false).describe("Target the Firestore emulator if true."), + }), + annotations: { + title: "Query Firestore collection", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ( + { collection_path, filters, order, limit, database, use_emulator }, + { projectId, host }, + ) => { + // database ??= "(default)"; + + if (!collection_path || !collection_path.length) + return mcpError("Must supply at least one collection path."); + + const structuredQuery: StructuredQuery = { + from: [{ collectionId: collection_path, allDescendants: false }], + }; + if (filters) { + structuredQuery.where = { + compositeFilter: { + op: "AND", + filters: filters.map((f) => { + if ( + f.compare_value.boolean_value && + f.compare_value.double_value && + f.compare_value.integer_value && + f.compare_value.string_array_value && + f.compare_value.string_value + ) { + throw mcpError("One and only one value may be specified per filters object."); + } + const out = Object.entries(f.compare_value).filter(([, value]) => { + return value !== null && value !== undefined; + }); + return { + fieldFilter: { + field: { fieldPath: f.field }, + op: f.op, + value: convertInputToValue(out[0][1]), + }, + }; + }), + }, + }; + } + if (order) { + structuredQuery.orderBy = [ + { + field: { fieldPath: order.orderBy }, + direction: order.orderByDirection, + }, + ]; + } + structuredQuery.limit = limit ? limit : 10; + + let emulatorUrl: string | undefined; + if (use_emulator) { + emulatorUrl = await host.getEmulatorUrl(Emulators.FIRESTORE); + } + + const { documents } = await queryCollection(projectId, structuredQuery, database, emulatorUrl); + + const docs = documents.map(firestoreDocumentToJson); + + const docsContent = toContent(docs); + + return docsContent; + }, +); diff --git a/src/mcp/tools/functions/get_logs.ts b/src/mcp/tools/functions/get_logs.ts new file mode 100644 index 00000000000..ff565693d7a --- /dev/null +++ b/src/mcp/tools/functions/get_logs.ts @@ -0,0 +1,188 @@ +import { z } from "zod"; + +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { getApiFilter } from "../../../functions/functionslog"; +import { listEntries } from "../../../gcp/cloudlogging"; +import { getErrMsg } from "../../../error"; + +const SEVERITY_LEVELS = [ + "DEFAULT", + "DEBUG", + "INFO", + "NOTICE", + "WARNING", + "ERROR", + "CRITICAL", + "ALERT", + "EMERGENCY", +] as const; + +// normalizeFunctionSelectors standardizes tool input into the comma-separated +// list that the existing logging filter helper expects (matching CLI behaviour). +function normalizeFunctionSelectors(selectors?: string | string[]): string | undefined { + if (!selectors) return undefined; + if (Array.isArray(selectors)) { + const cleaned = selectors.map((name) => name.trim()).filter(Boolean); + return cleaned.length ? cleaned.join(",") : undefined; + } + const cleaned = selectors + .split(/[,\s]+/) + .map((name) => name.trim()) + .filter(Boolean); + return cleaned.length ? cleaned.join(",") : undefined; +} + +function validateTimestamp(label: string, value: string): string | null { + const parsed = Date.parse(value); + if (Number.isNaN(parsed)) { + return `${label} must be an RFC3339/ISO 8601 timestamp, received '${value}'.`; + } + return null; +} + +export const get_logs = tool( + { + name: "get_logs", + description: + "Use this to retrieve a page of Cloud Functions log entries using Google Cloud Logging advanced filters.", + inputSchema: z.object({ + function_names: z + .array(z.string()) + .min(1) + .optional() + .describe( + "Optional list of deployed Cloud Function IDs to filter logs (e.g. ['fnA','fnB']).", + ), + page_size: z + .number() + .int() + .min(1) + .max(1000) + .default(50) + .describe("Maximum number of log entries to return."), + order: z.enum(["asc", "desc"]).default("desc").describe("Sort order by timestamp"), + page_token: z + .string() + .optional() + .describe("Opaque page token returned from a previous call to continue pagination."), + min_severity: z + .enum(SEVERITY_LEVELS) + .optional() + .describe("Filters results to entries at or above the provided severity level."), + start_time: z + .string() + .optional() + .describe( + "RFC3339 timestamp (YYYY-MM-DDTHH:MM:SSZ). Only entries with timestamp greater than or equal to this are returned.", + ), + end_time: z + .string() + .optional() + .describe( + "RFC3339 timestamp (YYYY-MM-DDTHH:MM:SSZ). Only entries with timestamp less than or equal to this are returned.", + ), + filter: z + .string() + .optional() + .describe( + "Additional Google Cloud Logging advanced filter text that will be AND'ed with the generated filter.", + ), + }), + annotations: { + title: "Get Functions Logs from Cloud Logging", + readOnlyHint: true, + openWorldHint: true, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ( + { function_names, page_size, order, page_token, min_severity, start_time, end_time, filter }, + { projectId }, + ) => { + const resolvedOrder: "asc" | "desc" = order?.toLowerCase() === "asc" ? "asc" : "desc"; + const resolvedPageSize = page_size ?? 50; + + const normalizedSelectors = normalizeFunctionSelectors(function_names); + const filterParts: string[] = [getApiFilter(normalizedSelectors)]; + + if (min_severity) { + filterParts.push(`severity>="${min_severity}"`); + } + if (start_time) { + const error = validateTimestamp("start_time", start_time); + if (error) return mcpError(error); + filterParts.push(`timestamp>="${start_time}"`); + } + if (end_time) { + const error = validateTimestamp("end_time", end_time); + if (error) return mcpError(error); + filterParts.push(`timestamp<="${end_time}"`); + } + if (start_time && end_time && Date.parse(start_time) > Date.parse(end_time)) { + return mcpError("start_time must be less than or equal to end_time."); + } + if (filter) { + filterParts.push(`(${filter})`); + } + + const combinedFilter = filterParts.join("\n"); + + try { + const { entries, nextPageToken } = await listEntries( + projectId, + combinedFilter, + resolvedPageSize, + resolvedOrder, + page_token, + ); + + const formattedEntries = entries.map((entry) => { + const functionName = + entry.resource?.labels?.function_name ?? entry.resource?.labels?.service_name ?? null; + const payload = + entry.textPayload ?? entry.jsonPayload ?? entry.protoPayload ?? entry.labels ?? null; + return { + timestamp: entry.timestamp ?? entry.receiveTimestamp ?? null, + severity: entry.severity ?? "DEFAULT", + function: functionName, + message: + entry.textPayload ?? + (entry.jsonPayload ? JSON.stringify(entry.jsonPayload) : undefined) ?? + (entry.protoPayload ? JSON.stringify(entry.protoPayload) : undefined) ?? + "", + payload, + log_name: entry.logName, + trace: entry.trace ?? null, + span_id: entry.spanId ?? null, + }; + }); + + const response = { + filter: combinedFilter, + order: resolvedOrder, + page_size: resolvedPageSize, + entries: resolvedOrder === "asc" ? formattedEntries : formattedEntries.reverse(), + next_page_token: nextPageToken ?? null, + has_more: Boolean(nextPageToken), + }; + + if (!entries.length) { + return toContent(response, { + contentPrefix: "No log entries matched the provided filters.\n\n", + }); + } + + return toContent(response); + } catch (err) { + const errMsg = getErrMsg( + (err as any)?.original || err, + "Failed to retrieve Cloud Logging entries.", + ); + return mcpError(errMsg); + } + }, +); diff --git a/src/mcp/tools/functions/index.ts b/src/mcp/tools/functions/index.ts new file mode 100644 index 00000000000..b59051aa3b8 --- /dev/null +++ b/src/mcp/tools/functions/index.ts @@ -0,0 +1,5 @@ +import type { ServerTool } from "../../tool"; + +import { get_logs } from "./get_logs"; + +export const functionsTools: ServerTool[] = [get_logs]; diff --git a/src/mcp/tools/index.ts b/src/mcp/tools/index.ts new file mode 100644 index 00000000000..f835a52c884 --- /dev/null +++ b/src/mcp/tools/index.ts @@ -0,0 +1,77 @@ +import { ServerTool } from "../tool"; +import { ServerFeature } from "../types"; +import { authTools } from "./auth/index"; +import { dataconnectTools } from "./dataconnect/index"; +import { firestoreTools } from "./firestore/index"; +import { coreTools } from "./core/index"; +import { storageTools } from "./storage/index"; +import { messagingTools } from "./messaging/index"; +import { remoteConfigTools } from "./remoteconfig/index"; +import { crashlyticsTools } from "./crashlytics/index"; +import { appHostingTools } from "./apphosting/index"; +import { realtimeDatabaseTools } from "./realtime_database/index"; +import { functionsTools } from "./functions/index"; + +/** availableTools returns the list of MCP tools available given the server flags */ +export function availableTools(activeFeatures?: ServerFeature[]): ServerTool[] { + const toolDefs: ServerTool[] = []; + if (!activeFeatures?.length) { + activeFeatures = Object.keys(tools) as ServerFeature[]; + } + if (!activeFeatures.includes("core")) { + activeFeatures = ["core", ...activeFeatures]; + } + for (const key of activeFeatures) { + toolDefs.push(...tools[key]); + } + return toolDefs; +} + +const tools: Record = { + core: addFeaturePrefix("firebase", coreTools), + firestore: addFeaturePrefix("firestore", firestoreTools), + auth: addFeaturePrefix("auth", authTools), + dataconnect: addFeaturePrefix("dataconnect", dataconnectTools), + storage: addFeaturePrefix("storage", storageTools), + messaging: addFeaturePrefix("messaging", messagingTools), + functions: addFeaturePrefix("functions", functionsTools), + remoteconfig: addFeaturePrefix("remoteconfig", remoteConfigTools), + crashlytics: addFeaturePrefix("crashlytics", crashlyticsTools), + apphosting: addFeaturePrefix("apphosting", appHostingTools), + database: addFeaturePrefix("realtimedatabase", realtimeDatabaseTools), +}; + +function addFeaturePrefix(feature: string, tools: ServerTool[]): ServerTool[] { + return tools.map((tool) => ({ + ...tool, + mcp: { + ...tool.mcp, + name: `${feature}_${tool.mcp.name}`, + _meta: { + ...tool.mcp._meta, + feature, + }, + }, + })); +} + +/** + * Generates a markdown table of all available tools and their descriptions. + * This is used for generating documentation. + */ +export function markdownDocsOfTools(): string { + const allTools = availableTools([]); + let doc = ` +| Tool Name | Feature Group | Description | +| --------- | ------------- | ----------- |`; + for (const tool of allTools) { + let feature = tool.mcp?._meta?.feature || ""; + if (feature === "firebase") { + feature = "core"; + } + const description = (tool.mcp?.description || "").replaceAll("\n", "
    "); + doc += ` +| ${tool.mcp.name} | ${feature} | ${description} |`; + } + return doc; +} diff --git a/src/mcp/tools/messaging/index.ts b/src/mcp/tools/messaging/index.ts new file mode 100644 index 00000000000..409e76fff7d --- /dev/null +++ b/src/mcp/tools/messaging/index.ts @@ -0,0 +1,4 @@ +import { ServerTool } from "../../tool"; +import { send_message } from "./send_message"; + +export const messagingTools: ServerTool[] = [send_message]; diff --git a/src/mcp/tools/messaging/send_message.ts b/src/mcp/tools/messaging/send_message.ts new file mode 100644 index 00000000000..ae8f1303a93 --- /dev/null +++ b/src/mcp/tools/messaging/send_message.ts @@ -0,0 +1,54 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { sendFcmMessage } from "../../../messaging/sendMessage"; + +export const send_message = tool( + { + name: "send_message", + description: + "Use this to send a message to a Firebase Cloud Messaging registration token or topic. ONLY ONE of `registration_token` or `topic` may be supplied in a specific call.", + inputSchema: z.object({ + registration_token: z + .string() + .optional() + .describe( + "A specific device registration token for delivery. Supply either this or topic.", + ), + topic: z + .string() + .optional() + .describe("A topic name for delivery. Supply either this or registration_token."), + title: z.string().optional().describe("The title of the push notification message."), + body: z.string().optional().describe("The body of the push notification message."), + image: z + .string() + .optional() + .describe( + "The URL of an image that will be displayed with the notification. JPEG, PNG, BMP have full support across platforms. Animated GIF and video only work on iOS. WebP and HEIF have varying levels of support across platforms and platform versions.", + ), + }), + annotations: { + title: "Send FCM Message", + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ({ registration_token, topic, title, body }, { projectId }) => { + if (!registration_token && !topic) { + return mcpError( + "Must supply either a `registration_token` or `topic` parameter to `send_message`.", + ); + } + if (registration_token && topic) { + return mcpError( + "Cannot supply both `registration_token` and `topic` in a single `send_message` request.", + ); + } + return toContent( + await sendFcmMessage(projectId, { token: registration_token, topic, title, body }), + ); + }, +); diff --git a/src/mcp/tools/realtime_database/get_data.ts b/src/mcp/tools/realtime_database/get_data.ts new file mode 100644 index 00000000000..56b691575a4 --- /dev/null +++ b/src/mcp/tools/realtime_database/get_data.ts @@ -0,0 +1,68 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import * as url from "node:url"; +import { Client } from "../../../apiv2"; +import { text } from "node:stream/consumers"; +import path from "node:path"; + +export const get_data = tool( + { + name: "get_data", + description: + "Use this to retrieve data from the specified location in a Firebase Realtime Database.", + inputSchema: z.object({ + databaseUrl: z + .string() + .optional() + .describe( + "connect to the database at url. If omitted, use default database instance -default-rtdb.firebasedatabase.app. Can point to emulator URL (e.g. localhost:6000/)", + ), + path: z.string().describe("The path to the data to read. (ex: /my/cool/path)"), + }), + annotations: { + title: "Get Realtime Database data", + readOnlyHint: true, + }, + + _meta: { + // it's possible that a user attempts to query a database that they aren't + // authed into: we should let the rules evaluate as the author intended. + // If they have written rules to leave paths public, then having mcp + // grab their data is perfectly valid. + requiresAuth: false, + requiresProject: false, + }, + }, + async ({ path: getPath, databaseUrl }, { projectId, host }) => { + if (!getPath.startsWith("/")) { + return mcpError(`paths must start with '/' (you passed ''${getPath}')`); + } + + const dbUrl = new url.URL( + databaseUrl + ? `${databaseUrl}/${getPath}.json` + : path.join( + `https://${projectId}-default-rtdb.us-central1.firebasedatabase.app`, + `${getPath}.json`, + ), + ); + + const client = new Client({ + urlPrefix: dbUrl.origin, + auth: true, + }); + + host.logger.debug(`sending read request to path '${getPath}' for url '${dbUrl.toString()}'`); + + const res = await client.request({ + method: "GET", + path: dbUrl.pathname, + responseType: "stream", + resolveOnHTTPError: true, + }); + + const content = await text(res.body); + return toContent(content); + }, +); diff --git a/src/mcp/tools/realtime_database/index.ts b/src/mcp/tools/realtime_database/index.ts new file mode 100644 index 00000000000..2caf8228c0a --- /dev/null +++ b/src/mcp/tools/realtime_database/index.ts @@ -0,0 +1,5 @@ +import type { ServerTool } from "../../tool"; +import { get_data } from "./get_data"; +import { set_data } from "./set_data"; + +export const realtimeDatabaseTools: ServerTool[] = [get_data, set_data]; diff --git a/src/mcp/tools/realtime_database/set_data.ts b/src/mcp/tools/realtime_database/set_data.ts new file mode 100644 index 00000000000..961279746fb --- /dev/null +++ b/src/mcp/tools/realtime_database/set_data.ts @@ -0,0 +1,72 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import * as url from "node:url"; +import { stringToStream } from "../../../utils"; +import { Client } from "../../../apiv2"; +import { getErrMsg } from "../../../error"; +import path from "node:path"; + +export const set_data = tool( + { + name: "set_data", + description: + "Use this to write data to the specified location in a Firebase Realtime Database.", + inputSchema: z.object({ + databaseUrl: z + .string() + .optional() + .describe( + "connect to the database at url. If omitted, use default database instance -default-rtdb.us-central1.firebasedatabase.app. Can point to emulator URL (e.g. localhost:6000/)", + ), + path: z.string().describe("The path to the data to read. (ex: /my/cool/path)"), + data: z.string().describe('The JSON to write. (ex: {"alphabet": ["a", "b", "c"]})'), + }), + annotations: { + title: "Set Realtime Database data", + readOnlyHint: false, + idempotentHint: true, + }, + + _meta: { + requiresAuth: false, + requiresProject: false, + }, + }, + async ({ path: setPath, databaseUrl, data }, { projectId, host }) => { + if (!setPath.startsWith("/")) { + return mcpError(`paths must start with '/' (you passed ''${setPath}')`); + } + + const dbUrl = new url.URL( + databaseUrl + ? `${databaseUrl}/${setPath}.json` + : path.join( + `https://${projectId}-default-rtdb.us-central1.firebasedatabase.app`, + `${setPath}.json`, + ), + ); + + const client = new Client({ + urlPrefix: dbUrl.origin, + auth: true, + }); + + const inStream = stringToStream(data); + + host.logger.debug(`sending write request to path '${setPath}' for url '${dbUrl.toString()}'`); + + try { + await client.request({ + method: "PUT", + path: dbUrl.pathname, + body: inStream, + }); + } catch (err: unknown) { + host.logger.debug(getErrMsg(err)); + return mcpError(`Unexpected error while setting data: ${getErrMsg(err)}`); + } + + return toContent("write successful!"); + }, +); diff --git a/src/mcp/tools/remoteconfig/get_template.ts b/src/mcp/tools/remoteconfig/get_template.ts new file mode 100644 index 00000000000..ddb2e7def2e --- /dev/null +++ b/src/mcp/tools/remoteconfig/get_template.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { getTemplate } from "../../../remoteconfig/get"; + +export const get_template = tool( + { + name: "get_template", + description: + "Use this to retrieve the specified Firebase Remote Config template from the currently active Firebase Project.", + inputSchema: z.object({ + version_number: z + .string() + .optional() + .describe( + "The version number of the template to retrieve. If not provided, retrieves the active template.", + ), + }), + annotations: { + title: "Get Remote Config template", + readOnlyHint: true, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ({ version_number }, { projectId }) => { + return toContent(await getTemplate(projectId, version_number)); + }, +); diff --git a/src/mcp/tools/remoteconfig/index.ts b/src/mcp/tools/remoteconfig/index.ts new file mode 100644 index 00000000000..335f58fc16e --- /dev/null +++ b/src/mcp/tools/remoteconfig/index.ts @@ -0,0 +1,5 @@ +import { ServerTool } from "../../tool"; +import { get_template } from "./get_template"; +import { update_template } from "./update_template"; + +export const remoteConfigTools: ServerTool[] = [get_template, update_template]; diff --git a/src/mcp/tools/remoteconfig/update_template.spec.ts b/src/mcp/tools/remoteconfig/update_template.spec.ts new file mode 100644 index 00000000000..1a6c9bd0566 --- /dev/null +++ b/src/mcp/tools/remoteconfig/update_template.spec.ts @@ -0,0 +1,93 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as nock from "nock"; +import * as api from "../../../api"; +import { RemoteConfigTemplate } from "../../../remoteconfig/interfaces"; +import { update_template } from "./update_template"; +import { toContent } from "../../util"; +import { McpContext } from "../../types"; + +const PROJECT_ID = "the-remote-config-project"; +const TEMPLATE: RemoteConfigTemplate = { + conditions: [], + parameters: {}, + parameterGroups: {}, + etag: "whatever", + version: { + versionNumber: "1", + updateTime: "2020-01-01T12:00:00.000000Z", + updateUser: { + email: "someone@google.com", + }, + updateOrigin: "CONSOLE", + updateType: "INCREMENTAL_UPDATE", + }, +}; + +describe("update_template", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + nock.cleanAll(); + }); + + it("should publish the latest template", async () => { + nock(api.remoteConfigApiOrigin()) + .put(`/v1/projects/${PROJECT_ID}/remoteConfig`) + .reply(200, TEMPLATE); + + const result = await update_template.fn({ template: TEMPLATE }, { + projectId: PROJECT_ID, + } as any as McpContext); + expect(result).to.deep.equal(toContent(TEMPLATE)); + }); + + it("should publish the latest template with * etag", async () => { + nock(api.remoteConfigApiOrigin()) + .put(`/v1/projects/${PROJECT_ID}/remoteConfig`, undefined, { + reqheaders: { + "If-Match": "*", + }, + }) + .reply(200, TEMPLATE); + + const result = await update_template.fn({ template: TEMPLATE, force: true }, { + projectId: PROJECT_ID, + } as any as McpContext); + expect(result).to.deep.equal(toContent(TEMPLATE)); + }); + + it("should reject if the publish api call fails", async () => { + nock(api.remoteConfigApiOrigin()).put(`/v1/projects/${PROJECT_ID}/remoteConfig`).reply(404, {}); + + await expect( + update_template.fn({ template: TEMPLATE }, { projectId: PROJECT_ID } as any as McpContext), + ).to.be.rejected; + }); + + it("should return a rollback to the version number specified", async () => { + nock(api.remoteConfigApiOrigin()) + .post(`/v1/projects/${PROJECT_ID}/remoteConfig:rollback?versionNumber=1`) + .reply(200, TEMPLATE); + + const result = await update_template.fn({ version_number: 1 }, { + projectId: PROJECT_ID, + } as any as McpContext); + expect(result).to.deep.equal(toContent(TEMPLATE)); + }); + + it("should reject if the rollback api call fails", async () => { + nock(api.remoteConfigApiOrigin()) + .post(`/v1/projects/${PROJECT_ID}/remoteConfig:rollback?versionNumber=1`) + .reply(404, {}); + + await expect( + update_template.fn({ version_number: 1 }, { projectId: PROJECT_ID } as any as McpContext), + ).to.be.rejected; + }); +}); diff --git a/src/mcp/tools/remoteconfig/update_template.ts b/src/mcp/tools/remoteconfig/update_template.ts new file mode 100644 index 00000000000..04d38f1d01e --- /dev/null +++ b/src/mcp/tools/remoteconfig/update_template.ts @@ -0,0 +1,58 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { publishTemplate } from "../../../remoteconfig/publish"; +import { rollbackTemplate } from "../../../remoteconfig/rollback"; +import { RemoteConfigTemplate } from "../../../remoteconfig/interfaces"; + +export const update_template = tool( + { + name: "update_template", + description: + "Use this to publish a new remote config template or roll back to a specific version for the project", + inputSchema: z + .object({ + template: z.object({}).optional().describe("The Remote Config template object to publish."), + version_number: z.number().optional().describe("The version number to roll back to."), + force: z + .boolean() + .optional() + .describe( + "If true, the publish will bypass ETag validation and overwrite the current template. Defaults to false if not provided.", + ), + }) + .refine( + (data) => + (data.template && !data.version_number) || (!data.template && data.version_number), + { + message: + "Either provide a template for publish, or a version number to rollback to, but not both.", + }, + ), + annotations: { + title: "Update Remote Config template", + readOnlyHint: false, + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ({ template, version_number, force }, { projectId }) => { + if (version_number) { + return toContent(await rollbackTemplate(projectId, version_number!)); + } + + if (template) { + if (force === undefined) { + return toContent(await publishTemplate(projectId, template as any as RemoteConfigTemplate)); + } + return toContent( + await publishTemplate(projectId, template as any as RemoteConfigTemplate, { force }), + ); + } + + // This part should not be reached due to the refine validation, but as a safeguard: + return mcpError("Either a template or a version number must be specified."); + }, +); diff --git a/src/mcp/tools/storage/get_download_url.ts b/src/mcp/tools/storage/get_download_url.ts new file mode 100644 index 00000000000..ca0ba73c3b6 --- /dev/null +++ b/src/mcp/tools/storage/get_download_url.ts @@ -0,0 +1,46 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { toContent } from "../../util"; +import { getDownloadUrl } from "../../../gcp/storage"; +import { Emulators } from "../../../emulator/types"; + +export const get_object_download_url = tool( + { + name: "get_object_download_url", + description: + "Use this to retrieve the download URL for an object in a Cloud Storage for Firebase bucket.", + inputSchema: z.object({ + bucket: z + .string() + .optional() + .describe( + "The bucket name in Firebase Storage. If not provided, defaults to the project's default bucket (e.g., `{projectid}.firebasestorage.app`).", + ), + object_path: z + .string() + .describe("The path to the object in Firebase storage without the bucket name attached"), + use_emulator: z.boolean().default(false).describe("Target the Storage emulator if true."), + }), + annotations: { + title: "Get Storage Object Download URL", + readOnlyHint: true, + }, + _meta: { + requiresProject: true, + requiresAuth: true, + }, + }, + async ({ bucket, object_path, use_emulator }, { projectId, host }) => { + if (!bucket) { + bucket = `${projectId}.firebasestorage.app`; + } + + let emulatorUrl: string | undefined; + if (use_emulator) { + emulatorUrl = await host.getEmulatorUrl(Emulators.STORAGE); + } + + const downloadUrl = await getDownloadUrl(bucket, object_path, emulatorUrl); + return toContent(downloadUrl); + }, +); diff --git a/src/mcp/tools/storage/index.ts b/src/mcp/tools/storage/index.ts new file mode 100644 index 00000000000..b608364ce07 --- /dev/null +++ b/src/mcp/tools/storage/index.ts @@ -0,0 +1,3 @@ +import { get_object_download_url } from "./get_download_url"; + +export const storageTools = [get_object_download_url]; diff --git a/src/mcp/types.ts b/src/mcp/types.ts new file mode 100644 index 00000000000..753b6dcb443 --- /dev/null +++ b/src/mcp/types.ts @@ -0,0 +1,32 @@ +import { Config } from "../config"; +import { RC } from "../rc"; +import type { FirebaseMcpServer } from "./index"; + +export const SERVER_FEATURES = [ + "core", + "firestore", + "storage", + "dataconnect", + "auth", + "messaging", + "functions", + "remoteconfig", + "crashlytics", + "apphosting", + "database", +] as const; +export type ServerFeature = (typeof SERVER_FEATURES)[number]; + +export interface ClientConfig { + /** The current project root directory for this client. */ + projectRoot?: string | null; +} + +export interface McpContext { + projectId: string; + accountEmail: string | null; + config: Config; + host: FirebaseMcpServer; + rc: RC; + firebaseCliCommand: string; +} diff --git a/src/mcp/util.test.ts b/src/mcp/util.test.ts new file mode 100644 index 00000000000..06b3cac1a2a --- /dev/null +++ b/src/mcp/util.test.ts @@ -0,0 +1,475 @@ +import { expect } from "chai"; +import { cleanSchema } from "./util"; + +interface TestCase { + desc: string; + input: Record; + expected: Record; +} + +const testCases: TestCase[] = [ + { + desc: "should remove $schema property", + input: { + $schema: "http://json-schema.org/draft-07/schema#", + type: "object", + properties: { name: { type: "string" } }, + }, + expected: { + type: "object", + properties: { name: { type: "string" } }, + }, + }, + { + desc: "should remove additionalProperties field", + input: { + type: "object", + properties: { name: { type: "string" } }, + additionalProperties: false, + }, + expected: { + type: "object", + properties: { name: { type: "string" } }, + }, + }, + { + desc: "should remove additionalProperties from nested objects", + input: { + type: "object", + properties: { + user: { + type: "object", + properties: { id: { type: "number" } }, + additionalProperties: true, + }, + meta: { + type: "object", + additionalProperties: { type: "string" }, + }, + }, + additionalProperties: false, + }, + expected: { + type: "object", + properties: { + user: { + type: "object", + properties: { id: { type: "number" } }, + }, + meta: { + type: "object", + }, + }, + }, + }, + { + desc: "should remove top-level array type (string)", + input: { type: "array", items: { type: "string" } }, + expected: {}, + }, + { + desc: "should remove top-level array type (array of types including array)", + input: { type: ["array", "string"], items: { type: "string" } }, // Will become anyOf: [{type: "string"}], then simplified to type: "string" at root + expected: { type: "string", items: { type: "string" } }, + }, + { + desc: "should remove top-level array type (array of types including array and null)", + input: { type: ["array", "null"], items: { type: "string" } }, + expected: {}, + }, + { + desc: "should remove top-level null type", + input: { type: "null" }, + expected: {}, + }, + { + desc: "should KEEP array type in properties", + input: { + type: "object", + properties: { + tags: { type: "array", items: { type: "string" } }, + name: { type: "string" }, + }, + }, + expected: { + type: "object", + properties: { + tags: { type: "array", items: { type: "string" } }, + name: { type: "string" }, + }, + }, + }, + { + desc: "should remove null type from properties", + input: { + type: "object", + properties: { + optionalField: { type: "null" }, + name: { type: "string" }, + }, + }, + expected: { + type: "object", + properties: { name: { type: "string" } }, + }, + }, + { + desc: "should convert type: ['string', 'null', 'array'] to anyOf: [{type: 'string'}, {type: 'array'}] in properties", + input: { + type: "object", + properties: { + mixed: { type: ["string", "null", "array"] }, + }, + }, + expected: { + type: "object", + properties: { + mixed: { anyOf: [{ type: "string" }, { type: "array" }] }, + }, + }, + }, + { + desc: "should convert type: ['string', 'number', 'null', 'array'] to anyOf in properties", + input: { + type: "object", + properties: { + mixed: { type: ["string", "number", "null", "array"] }, + }, + }, + expected: { + type: "object", + properties: { + mixed: { anyOf: [{ type: "string" }, { type: "number" }, { type: "array" }] }, + }, + }, + }, + { + desc: "should simplify type: ['string', 'null'] to type: 'string' in properties", + input: { + type: "object", + properties: { + simpleMixed: { type: ["string", "null"] }, + }, + }, + expected: { + type: "object", + properties: { + simpleMixed: { type: "string" }, + }, + }, + }, + { + desc: "should remove property if its type array becomes empty after filtering (e.g. only null)", + input: { + type: "object", + properties: { + onlyNull: { type: ["null"] }, + name: { type: "string" }, + }, + }, + expected: { + type: "object", + properties: { name: { type: "string" } }, + }, + }, + { + desc: "should keep property if its type array contains only 'array' (not root) and simplify", + input: { + type: "object", + properties: { + onlyArray: { type: ["array", "null"] }, + name: { type: "string" }, + }, + }, + expected: { + type: "object", + properties: { + onlyArray: { type: "array" }, + name: { type: "string" }, + }, + }, + }, + { + desc: "should handle nested objects and clean them (arrays kept in nested, type arrays become anyOf or simplified)", + input: { + type: "object", + properties: { + user: { + type: "object", + properties: { + id: { type: "number" }, + tags: { type: "array", items: { type: "string" } }, + status: { type: ["string", "integer", "null"] }, + maybeName: { type: ["string", "null"] }, + }, + }, + }, + }, + expected: { + type: "object", + properties: { + user: { + type: "object", + properties: { + id: { type: "number" }, + tags: { type: "array", items: { type: "string" } }, + status: { anyOf: [{ type: "string" }, { type: "integer" }] }, + maybeName: { type: "string" }, + }, + }, + }, + }, + }, + { + desc: "should remove items if its schema becomes null", + input: { + type: "object", + properties: { + someObjectWithItems: { + type: "object", + items: { type: "null" }, + }, + }, + }, + expected: { + type: "object", + properties: { + someObjectWithItems: { + type: "object", + }, + }, + }, + }, + { + desc: "should clean definitions ($defs), convert type arrays to anyOf/simplified", + input: { + type: "object", + properties: { + myDef: { $ref: "#/$defs/invalidDef" }, + myValidDef: { $ref: "#/$defs/validDefWithArray" }, + myComplexDef: { $ref: "#/$defs/complexDef" }, + }, + $defs: { + invalidDef: { type: "null" }, + validDef: { type: "string" }, + validDefWithArray: { type: "array", items: { type: "number" } }, + complexDef: { type: ["boolean", "string", "null"] }, + }, + }, + expected: { + type: "object", + properties: { + myDef: { $ref: "#/$defs/invalidDef" }, + myValidDef: { $ref: "#/$defs/validDefWithArray" }, + myComplexDef: { $ref: "#/$defs/complexDef" }, + }, + $defs: { + validDef: { type: "string" }, + validDefWithArray: { type: "array", items: { type: "number" } }, + complexDef: { anyOf: [{ type: "boolean" }, { type: "string" }] }, + }, + }, + }, + { + desc: "should remove $defs if all definitions become invalid (e.g. all null)", + input: { + type: "object", + $defs: { + invalidDef1: { type: "null" }, + invalidDef2: { type: "null" }, + }, + }, + expected: { + type: "object", + }, + }, + { + desc: "should clean schema arrays (anyOf, allOf, oneOf), keep nested arrays, convert internal type arrays", + input: { + anyOf: [ + { type: "string" }, + { type: "array", items: { type: "number" } }, + { type: "null" }, + { type: ["integer", "boolean", "null"] }, + ], + allOf: [{ type: "number" }], + oneOf: [{ type: "boolean" }, { type: ["null", "array"] }], + }, + expected: { + anyOf: [ + { type: "string" }, + { type: "array", items: { type: "number" } }, + { anyOf: [{ type: "integer" }, { type: "boolean" }] }, + ], + allOf: [{ type: "number" }], + oneOf: [{ type: "boolean" }, { type: "array" }], + }, + }, + { + desc: "should remove schema array keywords if their arrays become empty (e.g. all null)", + input: { + anyOf: [{ type: "null" }, { type: "null" }], + description: "test", + }, + expected: { + description: "test", + }, + }, + { + desc: "should return an empty object if the entire schema is just { type: 'array' }", + input: { type: "array" }, + expected: {}, + }, + { + desc: "should return an empty object if the entire schema is just { type: 'null' }", + input: { type: "null" }, + expected: {}, + }, + { + desc: "should return an empty object if the entire schema is { type: ['null', 'array'] }", + input: { type: ["null", "array"] }, + expected: {}, + }, + { + desc: "should not modify a schema that is already clean (with nested array and anyOf)", + input: { + type: "object", + properties: { + name: { type: "string" }, + age: { type: "integer" }, + scores: { type: "array", items: { type: "number" } }, + choice: { anyOf: [{ type: "string" }, { type: "boolean" }] }, + }, + required: ["name"], + }, + expected: { + type: "object", + properties: { + name: { type: "string" }, + age: { type: "integer" }, + scores: { type: "array", items: { type: "number" } }, + choice: { anyOf: [{ type: "string" }, { type: "boolean" }] }, + }, + required: ["name"], + }, + }, + { + desc: "should handle deeply nested structures with various cleaning needs (arrays kept if not root, type arrays to anyOf)", + input: { + $schema: "http://json-schema.org/draft-07/schema#", + title: "Complex Test", + type: "object", // Top level type array: ["object", "null"] -> type: "object" + additionalProperties: false, + properties: { + validProp: { type: "string" }, + propToBeKept: { type: "array", items: { type: "number" } }, + objectWithMixedTypes: { + type: "object", + additionalProperties: true, + properties: { + subPropString: { type: "string" }, + subPropNull: { type: "null" }, + subPropArrayType: { type: ["integer", "null", "array", "string"] }, + }, + }, + anotherArrayProp: { type: "array", items: { type: "boolean" } }, + }, + $defs: { + reusableInvalid: { type: "null" }, + reusableValid: { + type: "object", + additionalProperties: { type: "string" }, + properties: { + detail: { type: "string" }, + unwantedList: { type: "array", items: { type: "string" } }, + statusOptions: { type: ["number", "string", "null"] }, + }, + }, + toBeEmptyDef: { type: "null" }, + }, + anyOf: [ + // This anyOf is at the root level of the input schema, but its subschemas are not "root" for cleaning + { type: "string" }, + { type: "array", items: { type: "object" } }, // This array is fine as it's not top-level schema type + { $ref: "#/$defs/reusableInvalid" }, + { type: ["boolean", "null", "integer"] }, + ], + }, + expected: { + title: "Complex Test", + type: "object", + properties: { + validProp: { type: "string" }, + propToBeKept: { type: "array", items: { type: "number" } }, + objectWithMixedTypes: { + type: "object", + properties: { + subPropString: { type: "string" }, + subPropArrayType: { + anyOf: [{ type: "integer" }, { type: "array" }, { type: "string" }], + }, + }, + }, + anotherArrayProp: { type: "array", items: { type: "boolean" } }, + }, + $defs: { + reusableValid: { + type: "object", + properties: { + detail: { type: "string" }, + unwantedList: { type: "array", items: { type: "string" } }, + statusOptions: { anyOf: [{ type: "number" }, { type: "string" }] }, + }, + }, + }, + anyOf: [ + { type: "string" }, + { type: "array", items: { type: "object" } }, + { anyOf: [{ type: "boolean" }, { type: "integer" }] }, + ], + }, + }, + { + desc: "should remove properties if properties object becomes empty (all null)", + input: { + type: "object", + properties: { + field1: { type: "null" }, + field2: { type: "null" }, + }, + }, + expected: { + type: "object", + }, + }, + { + desc: "top level schema with type: ['string', 'array'] should become type: 'string'", + input: { + type: ["string", "array"], // 'array' removed at root, 'string' remains + description: "Test", + }, + expected: { + type: "string", + description: "Test", + }, + }, + { + desc: "top level schema with type: ['string', 'number', 'array'] should become anyOf: [{type: string}, {type: number}]", + input: { + type: ["string", "number", "array"], // 'array' removed at root + description: "Test AnyOf Root", + }, + expected: { + anyOf: [{ type: "string" }, { type: "number" }], + description: "Test AnyOf Root", + }, + }, +]; + +describe("cleanSchema", () => { + testCases.forEach((tc) => { + it(tc.desc, () => { + expect(cleanSchema(tc.input)).to.deep.equal(tc.expected); + }); + }); +}); diff --git a/src/mcp/util.ts b/src/mcp/util.ts new file mode 100644 index 00000000000..92c3f382cdd --- /dev/null +++ b/src/mcp/util.ts @@ -0,0 +1,277 @@ +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { dump } from "js-yaml"; +import { ServerFeature } from "./types"; +import { + apphostingOrigin, + authManagementOrigin, + dataconnectOrigin, + firestoreOrigin, + messagingApiOrigin, + functionsOrigin, + remoteConfigApiOrigin, + storageOrigin, + crashlyticsApiOrigin, + realtimeOrigin, +} from "../api"; +import { check } from "../ensureApiEnabled"; +import { timeoutFallback } from "../timeout"; + +/** + * Converts data to a CallToolResult. + */ +export function toContent( + data: any, + options?: { format?: "json" | "yaml"; contentPrefix?: string; contentSuffix?: string }, +): CallToolResult { + if (typeof data === "string") return { content: [{ type: "text", text: data }] }; + + let text = ""; + const format = options?.format || "yaml"; // use YAML because it's a little more prose-like for the LLM to parse + switch (format) { + case "json": + text = JSON.stringify(data); + break; + case "yaml": + text = dump(data); + break; + } + const prefix = options?.contentPrefix || ""; + const suffix = options?.contentSuffix || ""; + return { + content: [{ type: "text", text: `${prefix}${text}${suffix}` }], + }; +} + +/** + * Returns an error message to the user. + */ +export function mcpError(message: Error | string | unknown, code?: string): CallToolResult { + let errorMessage = "unknown error"; + if (message instanceof Error) { + errorMessage = message.message; + } + if (typeof message === "string") { + errorMessage = message; + } + return { + isError: true, + content: [{ type: "text", text: `Error: ${code ? `${code}: ` : ""}${errorMessage}` }], + }; +} + +/* + * Wraps a throwing function with a safe conversion to mcpError. + */ + +const SERVER_FEATURE_APIS: Record = { + core: "", + firestore: firestoreOrigin(), + storage: storageOrigin(), + dataconnect: dataconnectOrigin(), + auth: authManagementOrigin(), + messaging: messagingApiOrigin(), + functions: functionsOrigin(), + remoteconfig: remoteConfigApiOrigin(), + crashlytics: crashlyticsApiOrigin(), + apphosting: apphostingOrigin(), + database: realtimeOrigin(), +}; + +/** + * Detects whether an MCP feature is active in the current project root. Relies first on + * `firebase.json` configuration, but falls back to API checks. + */ +export async function checkFeatureActive( + feature: ServerFeature, + projectId?: string, + options?: any, +): Promise { + // if the feature is configured in firebase.json, it's active + if (feature in (options?.config?.data || {})) return true; + // if the feature's api is active in the project, it's active + try { + if (projectId) + return await timeoutFallback( + check(projectId, SERVER_FEATURE_APIS[feature], "", true), + true, + 3000, + ); + } catch (e) { + // if we don't have network or something, better to default to on + return true; + } + return false; +} + +// Helper function to process a single schema node (could be a property schema, items schema, etc.) +// Returns the cleaned schema, or null if the schema becomes invalid and should be removed according to the rules. +// The isRoot parameter is true only for the top-level schema object. +function deepClean(obj: any, isRootLevel: boolean = false): any { + if (typeof obj !== "object" || obj === null) { + return obj; // Not a schema object or null, return as is + } + + // Create a shallow copy to modify + const cleanedObj = { ...obj }; + + // Rule 1: Remove $schema (applies to any level, but typically at root) + if (cleanedObj.hasOwnProperty("$schema")) { + delete cleanedObj.$schema; + } + + // Remove additionalProperties + if (cleanedObj.hasOwnProperty("additionalProperties")) { + delete cleanedObj.additionalProperties; + } + + // Rule 2 & 3: Handle 'type' for "array" (only at root) and "null" (always) + if (cleanedObj.hasOwnProperty("type")) { + const currentType = cleanedObj.type; + if (Array.isArray(currentType)) { + let filteredTypes = currentType.filter((t: string) => t !== "null"); + if (isRootLevel) { + filteredTypes = filteredTypes.filter((t: string) => t !== "array"); + } + + if (filteredTypes.length === 0) { + return null; // Invalid: became typeless or only contained disallowed types + } else if (filteredTypes.length === 1) { + cleanedObj.type = filteredTypes[0]; // Simplify to single type + } else { + // Convert to anyOf + delete cleanedObj.type; // Remove the original 'type' array + cleanedObj.anyOf = filteredTypes + .map((t: string) => { + // Each item in anyOf is a schema, so it needs to be an object with a 'type' + // These sub-schemas are not root level. + return deepClean({ type: t }, false); + }) + .filter((subSchema: any) => subSchema !== null); // Filter out any nulls from deepClean + + if (cleanedObj.anyOf.length === 0) { + return null; // All types in the array led to invalid sub-schemas + } + if (cleanedObj.anyOf.length === 1) { + // If after cleaning, only one valid type remains in anyOf, simplify it + const singleSchema = cleanedObj.anyOf[0]; + delete cleanedObj.anyOf; + // Merge the single schema's properties into cleanedObj + // Most commonly, this will just be setting cleanedObj.type = singleSchema.type + Object.assign(cleanedObj, singleSchema); + } + } + } else if (typeof currentType === "string") { + if (currentType === "null") { + return null; // Invalid: type is "null" + } + if (isRootLevel && currentType === "array") { + return null; // Invalid: top-level type is "array" + } + // If not root level, "array" as a string type is allowed. + } + } + + // Recursively clean 'properties' + if ( + cleanedObj.hasOwnProperty("properties") && + typeof cleanedObj.properties === "object" && + cleanedObj.properties !== null + ) { + const newProperties: Record = {}; + for (const key in cleanedObj.properties) { + if (cleanedObj.properties.hasOwnProperty(key)) { + // Properties are never root level in this recursive call + const cleanedPropertySchema = deepClean(cleanedObj.properties[key], false); + if (cleanedPropertySchema !== null) { + // Only add valid properties + newProperties[key] = cleanedPropertySchema; + } + } + } + if (Object.keys(newProperties).length === 0) { + delete cleanedObj.properties; // Remove 'properties' key if it becomes empty + } else { + cleanedObj.properties = newProperties; + } + } + + // Recursively clean 'items' + if ( + cleanedObj.hasOwnProperty("items") && + typeof cleanedObj.items === "object" && + cleanedObj.items !== null + ) { + // 'items' schema is never root level in this recursive call + const cleanedItemsSchema = deepClean(cleanedObj.items, false); + if (cleanedItemsSchema === null) { + delete cleanedObj.items; // Items schema became invalid + } else { + cleanedObj.items = cleanedItemsSchema; + } + } + + // Recursively clean definitions (e.g., in $defs or definitions) + const defKeywords = ["$defs", "definitions"]; + for (const keyword of defKeywords) { + if ( + cleanedObj.hasOwnProperty(keyword) && + typeof cleanedObj[keyword] === "object" && + cleanedObj[keyword] !== null + ) { + const newDefs: Record = {}; + for (const defKey in cleanedObj[keyword]) { + if (cleanedObj[keyword].hasOwnProperty(defKey)) { + // Definitions are never root level in this recursive call + const cleanedDef = deepClean(cleanedObj[keyword][defKey], false); + if (cleanedDef !== null) { + newDefs[defKey] = cleanedDef; + } + } + } + if (Object.keys(newDefs).length === 0) { + delete cleanedObj[keyword]; + } else { + cleanedObj[keyword] = newDefs; + } + } + } + + // Recursively clean schema arrays like anyOf, allOf, oneOf + const schemaArrayKeywords = ["anyOf", "allOf", "oneOf"]; + for (const keyword of schemaArrayKeywords) { + if (cleanedObj.hasOwnProperty(keyword) && Array.isArray(cleanedObj[keyword])) { + const newSchemaArray = cleanedObj[keyword] + // Sub-schemas in anyOf etc. are not root level in this recursive call + .map((subSchema: any) => deepClean(subSchema, false)) + .filter((subSchema: any) => subSchema !== null); // Filter out invalid subSchemas + + if (newSchemaArray.length === 0) { + delete cleanedObj[keyword]; // Remove key if array becomes empty + } else { + cleanedObj[keyword] = newSchemaArray; + } + } + } + return cleanedObj; +} + +/** Takes a zodToJsonSchema output and cleans it up to be more compatible with LLM limitations. */ +export function cleanSchema(schema: Record): Record { + // Initial check for top-level array type before deep cleaning + if (schema && schema.hasOwnProperty("type")) { + const topLevelType = schema.type; + if (topLevelType === "array") { + return {}; + } + if (Array.isArray(topLevelType)) { + const filteredRootTypes = topLevelType.filter((t) => t !== "null" && t !== "array"); + if (filteredRootTypes.length === 0 && topLevelType.includes("array")) { + // e.g. type: ["array"] or type: ["array", "null"] + return {}; + } + } + } + + const result = deepClean(schema, true); // Pass true for isRootLevel + return result === null ? {} : result; +} diff --git a/src/mcp/util/dataconnect/compile.ts b/src/mcp/util/dataconnect/compile.ts new file mode 100644 index 00000000000..27aee94c2e4 --- /dev/null +++ b/src/mcp/util/dataconnect/compile.ts @@ -0,0 +1,20 @@ +import { prettify } from "../../../dataconnect/graphqlError"; +import { DataConnectEmulator } from "../../../emulator/dataconnectEmulator"; + +export async function compileErrors( + configDir: string, + errorFilter?: "all" | "schema" | "operations", +) { + const errors = (await DataConnectEmulator.build({ configDir })).errors; + return ( + errors + ?.filter((e) => { + const isOperationError = ["query", "mutation"].includes(e.path?.[0] as string); + if (errorFilter === "operations") return isOperationError; + if (errorFilter === "schema") return !isOperationError; + return true; + }) + .map(prettify) + .join("\n") || "" + ); +} diff --git a/src/mcp/util/dataconnect/content.ts b/src/mcp/util/dataconnect/content.ts new file mode 100644 index 00000000000..417d72a73a0 --- /dev/null +++ b/src/mcp/util/dataconnect/content.ts @@ -0,0 +1,653 @@ +export const MAIN_INSTRUCTIONS = ` +Closely follow the following instructions: + +You are Firebase Data Connect expert that is responsible for creating data connect schemas code in GraphQL for users.You will be given a description of the desired schema using Firebase Data Connect and your task is to write the schema code in GraphQL that fulfills the requirements and correct any mistakes in your generation. + +For example, if I were to ask for a schema for a GraphQL database that contains a table called "users" with a field called "name" and another table called "posts" with a field called "body", I would get the following schema: +\`\`\` +type User @table { + name: String! +} + +type Post @table { + body: String! + author: User +} +\`\`\` + +Simple Firebase Data Connect schema often takes the following form: +\`\`\`graphql +type TableName @table { + uuidField: UUID + uuidArrayField: [UUID] + stringField: String + stringArrayField: [String] + intField: Int + intArrayField: [Int] + int64Field: Int64 + int64ArrayField: [Int64] + floatField: Float + floatArrayField: [Float] + booleanField: Boolean + booleanArrayField: [Boolean] + timestampField: Timestamp + timestampArrayField: [Timestamp] + dateField: Date + dateArrayField: [Date] + vectorField: Vector @col(size:168) +} +\`\`\` + +Leave out objects named after \`Query\` and \`Mutation\` + +Firebase Data Connect implicitly adds \`id: UUID!\` to every table and implicitly makes it primary key. Therefore, leave out the \`id\` field. + +Use \`UUID\` type instead of \`ID\` type or \`String\` type for id-like fields. + +Array reference fields, like \`[SomeTable]\` and \`[SomeTable!]!\`, are not supported. Use the singular reference field instead. +For example, for a one-to-many relationship like one user is assiend to many bugs in a software project: +\`\`\`graphql +type User @table { + name: String! + # bugs: [Bug] # Not supported. Do not use +} + +type Bug @table { + title: String! + assignee: User + reporter: User +} +\`\`\` + +For another example, for a many-to-many relationship like each crew member is assigned to many chores and each chores requires many crews to complete: +\`\`\`graphql +type Crew @table { + name: String! + # assignedChores: [Chore!]! # No supported. Do not use +} + +type Chore @table { + name: String! + description: String! + # assignedCrews: [Crews!]! # No supported. Do not use +} + +type Assignment @table(key: ["crew", "chore"]) { + crew: Crew! + chore: Chore! +} +\`\`\` + +Leave out \`@relation\` because it is not supported yet. + +Leave out \`directive\`, \`enum\` and \`scalar\`. + +Leave out \`@view\`. + +Be sure that your response contains a valid Firebase Data Connect schema in a single GraphQL code block inside of triple backticks and closely follows my instructions and description. +`.trim(); + +export const BUILTIN_SDL = ` +# Directives + +Directives define specific behaviors that can be applied to fields or types within a GraphQL schema. + +## Data Connect Defined + +### @col on \`FIELD_DEFINITION\` {:#col} +Customizes a field that represents a SQL database table column. + +Data Connect maps scalar Fields on [\`@table\`](directive.md#table) type to a SQL column of +corresponding data type. + +- scalar [\`UUID\`](scalar.md#UUID) maps to [\`uuid\`](https://www.postgresql.org/docs/current/datatype-uuid.html). +- scalar [\`String\`](scalar.md#String) maps to [\`text\`](https://www.postgresql.org/docs/current/datatype-character.html). +- scalar [\`Int\`](scalar.md#Int) maps to [\`int\`](https://www.postgresql.org/docs/current/datatype-numeric.html). +- scalar [\`Int64\`](scalar.md#Int64) maps to [\`bigint\`](https://www.postgresql.org/docs/current/datatype-numeric.html). +- scalar [\`Float\`](scalar.md#Float) maps to [\`double precision\`](https://www.postgresql.org/docs/current/datatype-numeric.html). +- scalar [\`Boolean\`](scalar.md#Boolean) maps to [\`boolean\`](https://www.postgresql.org/docs/current/datatype-boolean.html). +- scalar [\`Date\`](scalar.md#Date) maps to [\`date\`](https://www.postgresql.org/docs/current/datatype-datetime.html). +- scalar [\`Timestamp\`](scalar.md#Timestamp) maps to [\`timestamptz\`](https://www.postgresql.org/docs/current/datatype-datetime.html). +- scalar [\`Any\`](scalar.md#Any) maps to [\`jsonb\`](https://www.postgresql.org/docs/current/datatype-json.html). +- scalar [\`Vector\`](scalar.md#Vector) maps to [\`pgvector\`](https://github.com/pgvector/pgvector). + +Array scalar fields are mapped to [Postgres arrays](https://www.postgresql.org/docs/current/arrays.html). + +###### Example: Serial Primary Key + +For example, you can define auto-increment primary key. + +\`\`\`graphql +type Post @table { + id: Int! @col(name: "post_id", dataType: "serial") +} +\`\`\` + +Data Connect converts it to the following SQL table schema. + +\`\`\`sql +CREATE TABLE "public"."post" ( + "post_id" serial NOT NULL, + PRIMARY KEY ("id") +) +\`\`\` + +###### Example: Vector + +\`\`\`graphql +type Post @table { + content: String! @col(name: "post_content") + contentEmbedding: Vector! @col(size:768) +} +\`\`\` + +| Argument | Type | Description | +|---|---|---| +| \`name\` | [\`String\`](scalar.md#String) | The SQL database column name. Defaults to snake_case of the field name. | +| \`dataType\` | [\`String\`](scalar.md#String) | Configures the custom SQL data type. Each GraphQL type can map to multiple SQL data types. Refer to [Postgres supported data types](https://www.postgresql.org/docs/current/datatype.html). Incompatible SQL data type will lead to undefined behavior. | +| \`size\` | [\`Int\`](scalar.md#Int) | Required on [\`Vector\`](scalar.md#Vector) columns. It specifies the length of the Vector. \`textembedding-gecko@003\` model generates [\`Vector\`](scalar.md#Vector) of \`@col(size:768)\`. | + +### @default on \`FIELD_DEFINITION\` {:#default} +Specifies the default value for a column field. + +For example: + +\`\`\`graphql +type User @table(key: "uid") { + uid: String! @default(expr: "auth.uid") + number: Int! @col(dataType: "serial") + createdAt: Date! @default(expr: "request.time") + role: String! @default(value: "Member") + credit: Int! @default(value: 100) +} +\`\`\` + +The supported arguments vary based on the field type. + +| Argument | Type | Description | +|---|---|---| +| \`value\` | [\`Any\`](scalar.md#Any) | A constant value validated against the field's GraphQL type during compilation. | +| \`expr\` | [\`Any_Expr\`](scalar.md#Any_Expr) | A CEL expression whose return value must match the field's data type. | +| \`sql\` | [\`Any_SQL\`](scalar.md#Any_SQL) | A raw SQL expression, whose SQL data type must match the underlying column. The value is any variable-free expression (in particular, cross-references to other columns in the current table are not allowed). Subqueries are not allowed either. See [PostgreSQL defaults](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-DEFAULT) for more details. | + +### @index on \`FIELD_DEFINITION\` | \`OBJECT\` {:#index} +Defines a database index to optimize query performance. + +\`\`\`graphql +type User @table @index(fields: ["name", "phoneNumber"], order: [ASC, DESC]) { + name: String @index + phoneNumber: Int64 @index + tags: [String] @index # GIN Index +} +\`\`\` + +##### Single Field Index + +You can put [\`@index\`](directive.md#index) on a [\`@col\`](directive.md#col) field to create a SQL index. + +\`@index(order)\` matters little for single field indexes, as they can be scanned +in both directions. + +##### Composite Index + +You can put \`@index(fields: [...])\` on [\`@table\`](directive.md#table) type to define composite indexes. + +\`@index(order: [...])\` can customize the index order to satisfy particular +filter and order requirement. + +| Argument | Type | Description | +|---|---|---| +| \`name\` | [\`String\`](scalar.md#String) | Configure the SQL database index id. If not overridden, Data Connect generates the index name: - \`{table_name}_{first_field}_{second_field}_aa_idx\` - \`{table_name}_{field_name}_idx\` | +| \`fields\` | [\`[String!]\`](scalar.md#String) | Only allowed and required when used on a [\`@table\`](directive.md#table) type. Specifies the fields to create the index on. | +| \`order\` | [\`[IndexFieldOrder!]\`](enum.md#IndexFieldOrder) | Only allowed for \`BTREE\` [\`@index\`](directive.md#index) on [\`@table\`](directive.md#table) type. Specifies the order for each indexed column. Defaults to all \`ASC\`. | +| \`type\` | [\`IndexType\`](enum.md#IndexType) | Customize the index type. For most index, it defaults to \`BTREE\`. For array fields, only allowed [\`IndexType\`](enum.md#IndexType) is \`GIN\`. For [\`Vector\`](scalar.md#Vector) fields, defaults to \`HNSW\`, may configure to \`IVFFLAT\`. | +| \`vector_method\` | [\`VectorSimilarityMethod\`](enum.md#VectorSimilarityMethod) | Only allowed when used on vector field. Defines the vector similarity method. Defaults to \`INNER_PRODUCT\`. | + +### @ref on \`FIELD_DEFINITION\` {:#ref} +Defines a foreign key reference to another table. + +For example, we can define a many-to-one relation. + +\`\`\`graphql +type ManyTable @table { + refField: OneTable! +} +type OneTable @table { + someField: String! +} +\`\`\` +Data Connect adds implicit foreign key column and relation query field. So the +above schema is equivalent to the following schema. + +\`\`\`graphql +type ManyTable @table { + id: UUID! @default(expr: "uuidV4()") + refField: OneTable! @ref(fields: "refFieldId", references: "id") + refFieldId: UUID! +} +type OneTable @table { + id: UUID! @default(expr: "uuidV4()") + someField: UUID! + # Generated Fields: + # manyTables_on_refField: [ManyTable!]! +} +\`\`\` +Data Connect generates the necessary foreign key constraint. + +\`\`\`sql +CREATE TABLE "public"."many_table" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "ref_field_id" uuid NOT NULL, + PRIMARY KEY ("id"), + CONSTRAINT "many_table_ref_field_id_fkey" FOREIGN KEY ("ref_field_id") REFERENCES "public"."one_table" ("id") ON DELETE CASCADE +) +\`\`\` + +###### Example: Traverse the Reference Field + +\`\`\`graphql +query ($id: UUID!) { + manyTable(id: $id) { + refField { id } + } +} +\`\`\` + +###### Example: Reverse Traverse the Reference field + +\`\`\`graphql +query ($id: UUID!) { + oneTable(id: $id) { + manyTables_on_refField { id } + } +} +\`\`\` + +##### Optional Many-to-One Relation + +An optional foreign key reference will be set to null if the referenced row is deleted. + +In this example, if a \`User\` is deleted, the \`assignee\` and \`reporter\` +references will be set to null. + +\`\`\`graphql +type Bug @table { + title: String! + assignee: User + reproter: User +} + +type User @table { name: String! } +\`\`\` + +##### Required Many-to-One Relation + +A required foreign key reference will cascade delete if the referenced row is +deleted. + +In this example, if a \`Post\` is deleted, associated comments will also be +deleted. + +\`\`\`graphql +type Comment @table { + post: Post! + content: String! +} + +type Post @table { title: String! } +\`\`\` + +##### Many To Many Relation + +You can define a many-to-many relation with a join table. + +\`\`\`graphql +type Membership @table(key: ["group", "user"]) { + group: Group! + user: User! + role: String! @default(value: "member") +} + +type Group @table { name: String! } +type User @table { name: String! } +\`\`\` + +When Data Connect sees a table with two reference field as its primary key, it +knows this is a join table, so expands the many-to-many query field. + +\`\`\`graphql +type Group @table { + name: String! + # Generated Fields: + # users_via_Membership: [User!]! + # memberships_on_group: [Membership!]! +} +type User @table { + name: String! + # Generated Fields: + # groups_via_Membership: [Group!]! + # memberships_on_user: [Membership!]! +} +\`\`\` + +###### Example: Traverse the Many-To-Many Relation + +\`\`\`graphql +query ($id: UUID!) { + group(id: $id) { + users: users_via_Membership { + name + } + } +} +\`\`\` + +###### Example: Traverse to the Join Table + +\`\`\`graphql +query ($id: UUID!) { + group(id: $id) { + memberships: memberships_on_group { + user { name } + role + } + } +} +\`\`\` + +##### One To One Relation + +You can even define a one-to-one relation with the help of [\`@unique\`](directive.md#unique) or \`@table(key)\`. + +\`\`\`graphql +type User @table { + name: String +} +type Account @table { + user: User! @unique +} +# Alternatively, use primary key constraint. +# type Account @table(key: "user") { +# user: User! +# } +\`\`\` + +###### Example: Transerse the Reference Field + +\`\`\`graphql +query ($id: UUID!) { + account(id: $id) { + user { id } + } +} +\`\`\` + +###### Example: Reverse Traverse the Reference field + +\`\`\`graphql +query ($id: UUID!) { + user(id: $id) { + account_on_user { id } + } +} +\`\`\` + +##### Customizations + +- \`@ref(constraintName)\` can customize the SQL foreign key constraint name (\`table_name_ref_field_fkey\` above). +- \`@ref(fields)\` can customize the foreign key field names. +- \`@ref(references)\` can customize the constraint to reference other columns. + By default, \`@ref(references)\` is the primary key of the [\`@ref\`](directive.md#ref) table. + Other fields with [\`@unique\`](directive.md#unique) may also be referred in the foreign key constraint. + +| Argument | Type | Description | +|---|---|---| +| \`constraintName\` | [\`String\`](scalar.md#String) | The SQL database foreign key constraint name. Defaults to snake_case \`{table_name}_{field_name}_fkey\`. | +| \`fields\` | [\`[String!]\`](scalar.md#String) | Foreign key fields. Defaults to \`{tableName}{PrimaryIdName}\`. | +| \`references\` | [\`[String!]\`](scalar.md#String) | The fields that the foreign key references in the other table. Defaults to its primary key. | + +### @table on \`OBJECT\` {:#table} +Defines a relational database table. + +In this example, we defined one table with a field named \`myField\`. + +\`\`\`graphql +type TableName @table { + myField: String +} +\`\`\` +Data Connect adds an implicit \`id\` primary key column. So the above schema is equivalent to: + +\`\`\`graphql +type TableName @table(key: "id") { + id: String @default(expr: "uuidV4()") + myField: String +} +\`\`\` + +Data Connect generates the following SQL table and CRUD operations to use it. + +\`\`\`sql +CREATE TABLE "public"."table_name" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "my_field" text NULL, + PRIMARY KEY ("id") +) +\`\`\` + + * You can lookup a row: \`query ($id: UUID!) { tableName(id: $id) { myField } } \` + * You can find rows using: \`query tableNames(limit: 20) { myField }\` + * You can insert a row: \`mutation { tableName_insert(data: {myField: "foo"}) }\` + * You can update a row: \`mutation ($id: UUID!) { tableName_update(id: $id, data: {myField: "bar"}) }\` + * You can delete a row: \`mutation ($id: UUID!) { tableName_delete(id: $id) }\` + +##### Customizations + +- \`@table(singular)\` and \`@table(plural)\` can customize the singular and plural name. +- \`@table(name)\` can customize the Postgres table name. +- \`@table(key)\` can customize the primary key field name and type. + +For example, the \`User\` table often has a \`uid\` as its primary key. + +\`\`\`graphql +type User @table(key: "uid") { + uid: String! + name: String +} +\`\`\` + + * You can securely lookup a row: \`query { user(key: {uid_expr: "auth.uid"}) { name } } \` + * You can securely insert a row: \`mutation { user_insert(data: {uid_expr: "auth.uid" name: "Fred"}) }\` + * You can securely update a row: \`mutation { user_update(key: {uid_expr: "auth.uid"}, data: {name: "New Name"}) }\` + * You can securely delete a row: \`mutation { user_delete(key: {uid_expr: "auth.uid"}) }\` + +[\`@table\`](directive.md#table) type can be configured further with: + + - Custom SQL data types for columns. See [\`@col\`](directive.md#col). + - Add SQL indexes. See [\`@index\`](directive.md#index). + - Add SQL unique constraints. See [\`@unique\`](directive.md#unique). + - Add foreign key constraints to define relations. See [\`@ref\`](directive.md#ref). + +| Argument | Type | Description | +|---|---|---| +| \`name\` | [\`String\`](scalar.md#String) | Configures the SQL database table name. Defaults to snake_case like \`table_name\`. | +| \`singular\` | [\`String\`](scalar.md#String) | Configures the singular name. Defaults to the camelCase like \`tableName\`. | +| \`plural\` | [\`String\`](scalar.md#String) | Configures the plural name. Defaults to infer based on English plural pattern like \`tableNames\`. | +| \`key\` | [\`[String!]\`](scalar.md#String) | Defines the primary key of the table. Defaults to a single field named \`id\`. If not present already, Data Connect adds an implicit field \`id: UUID! @default(expr: "uuidV4()")\`. | + +### @unique on \`FIELD_DEFINITION\` | \`OBJECT\` {:#unique} +Defines unique constraints on [\`@table\`](directive.md#table). + +For example, + +\`\`\`graphql +type User @table { + phoneNumber: Int64 @unique +} +type UserProfile @table { + user: User! @unique + address: String @unique +} +\`\`\` + +- [\`@unique\`](directive.md#unique) on a [\`@col\`](directive.md#col) field adds a single-column unique constraint. +- [\`@unique\`](directive.md#unique) on a [\`@table\`](directive.md#table) type adds a composite unique constraint. +- [\`@unique\`](directive.md#unique) on a [\`@ref\`](directive.md#ref) defines a one-to-one relation. It adds unique constraint + on \`@ref(fields)\`. + +[\`@unique\`](directive.md#unique) ensures those fields can uniquely identify a row, so other [\`@table\`](directive.md#table) +type may define \`@ref(references)\` to refer to fields that has a unique constraint. + +| Argument | Type | Description | +|---|---|---| +| \`indexName\` | [\`String\`](scalar.md#String) | Configures the SQL database unique constraint name. If not overridden, Data Connect generates the unique constraint name: - \`table_name_first_field_second_field_uidx\` - \`table_name_only_field_name_uidx\` | +| \`fields\` | [\`[String!]\`](scalar.md#String) | Only allowed and required when used on OBJECT, this specifies the fields to create a unique constraint on. | + +### @view on \`OBJECT\` {:#view} +Defines a relational database Raw SQLview. + +Data Connect generates GraphQL queries with WHERE and ORDER BY clauses. +However, not all SQL features has native GraphQL equivalent. + +You can write **an arbitrary SQL SELECT statement**. Data Connect +would map Graphql fields on [\`@view\`](directive.md#view) type to columns in your SELECT statement. + +* Scalar GQL fields (camelCase) should match a SQL column (snake_case) + in the SQL SELECT statement. +* Reference GQL field can point to another [\`@table\`](directive.md#table) type. Similar to foreign key + defined with [\`@ref\`](directive.md#ref) on a [\`@table\`](directive.md#table) type, a [\`@view\`](directive.md#view) type establishes a relation + when \`@ref(fields)\` match \`@ref(references)\` on the target table. + +In this example, you can use \`@view(sql)\` to define an aggregation view on existing +table. + +\`\`\`graphql +type User @table { + name: String + score: Int +} +type UserAggregation @view(sql: """ + SELECT + COUNT(*) as count, + SUM(score) as sum, + AVG(score) as average, + PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY score) AS median, + (SELECT id FROM "user" LIMIT 1) as example_id + FROM "user" +""") { + count: Int + sum: Int + average: Float + median: Float + example: User + exampleId: UUID +} +\`\`\` + +###### Example: Query Raw SQL View + +\`\`\`graphql +query { + userAggregations { + count sum average median + exampleId example { id } + } +} +\`\`\` + +##### One-to-One View + +An one-to-one companion [\`@view\`](directive.md#view) can be handy if you want to argument a [\`@table\`](directive.md#table) +with additional implied content. + +\`\`\`graphql +type Restaurant @table { + name: String! +} +type Review @table { + restaurant: Restaurant! + rating: Int! +} +type RestaurantStats @view(sql: """ + SELECT + restaurant_id, + COUNT(*) AS review_count, + AVG(rating) AS average_rating + FROM review + GROUP BY restaurant_id +""") { + restaurant: Restaurant @unique + reviewCount: Int + averageRating: Float +} +\`\`\` + +In this example, [\`@unique\`](directive.md#unique) convey the assumption that each \`Restaurant\` should +have only one \`RestaurantStats\` object. + +###### Example: Query One-to-One View + +\`\`\`graphql +query ListRestaurants { + restaurants { + name + stats: restaurantStats_on_restaurant { + reviewCount + averageRating + } + } +} +\`\`\` + +###### Example: Filter based on One-to-One View + +\`\`\`graphql +query BestRestaurants($minAvgRating: Float, $minReviewCount: Int) { + restaurants(where: { + restaurantStats_on_restaurant: { + averageRating: {ge: $minAvgRating} + reviewCount: {ge: $minReviewCount} + } + }) { name } +} +\`\`\` + +##### Customizations + +- One of \`@view(sql)\` or \`@view(name)\` should be defined. + \`@view(name)\` can refer to a persisted SQL view in the Postgres schema. +- \`@view(singular)\` and \`@view(plural)\` can customize the singular and plural name. + +[\`@view\`](directive.md#view) type can be configured further: + + - [\`@unique\`](directive.md#unique) lets you define one-to-one relation. + - [\`@col\`](directive.md#col) lets you customize SQL column mapping. For example, \`@col(name: "column_in_select")\`. + +##### Limitations + +Raw SQL view doesn't have a primary key, so it doesn't support lookup. Other +[\`@table\`](directive.md#table) or [\`@view\`](directive.md#view) cannot have [\`@ref\`](directive.md#ref) to a view either. + +View cannot be mutated. You can perform CRUD operations on the underlying +table to alter its content. + +**Important: Data Connect doesn't parse and validate SQL** + +- If the SQL view is invalid or undefined, related requests may fail. +- If the SQL view return incompatible types. Firebase Data Connect may surface + errors. +- If a field doesn't have a corresponding column in the SQL SELECT statement, + it will always be \`null\`. +- There is no way to ensure VIEW to TABLE [\`@ref\`](directive.md#ref) constraint. +- All fields must be nullable in case they aren't found in the SELECT statement + or in the referenced table. + +**Important: You should always test [\`@view\`](directive.md#view)!** + +| Argument | Type | Description | +|---|---|---| +| \`name\` | [\`String\`](scalar.md#String) | The SQL view name. If neither \`name\` nor \`sql\` are provided, defaults to the snake_case of the singular type name. \`name\` and \`sql\` cannot be specified at the same time. | +| \`sql\` | [\`String\`](scalar.md#String) | SQL \`SELECT\` statement used as the basis for this type. SQL SELECT columns should use snake_case. GraphQL fields should use camelCase. \`name\` and \`sql\` cannot be specified at the same time. | +| \`singular\` | [\`String\`](scalar.md#String) | Configures the singular name. Defaults to the camelCase like \`viewName\`. | +| \`plural\` | [\`String\`](scalar.md#String) | Configures the plural name. Defaults to infer based on English plural pattern like \`viewNames\`. | +`.trim(); diff --git a/src/mcp/util/dataconnect/converter.ts b/src/mcp/util/dataconnect/converter.ts new file mode 100644 index 00000000000..29939e2a03c --- /dev/null +++ b/src/mcp/util/dataconnect/converter.ts @@ -0,0 +1,68 @@ +import { dump } from "js-yaml"; +import { + Schema, + Connector, + Source, + GraphqlResponseError, + GraphqlResponse, + isGraphQLResponse, +} from "../../../dataconnect/types"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { mcpError } from "../../util"; + +export function schemaToText(s: Schema): string { + return ( + dump({ + name: s.name, + datasources: s.datasources, + }) + + "\n\n" + + sourceToText(s.source) + ); +} + +export function connectorToText(s: Connector): string { + return ( + dump({ + name: s.name, + }) + + "\n\n" + + sourceToText(s.source) + ); +} + +export function sourceToText(s: Source): string { + let output = ""; + s.files?.forEach((f) => { + output += `\n# ${f.path}`; + output += "\n```graphql\n"; + output += `${f.content.trim()}\n`; + output += "```\n"; + }); + return output; +} + +export function graphqlResponseToToolResponse( + g: GraphqlResponse | GraphqlResponseError, +): CallToolResult { + if (isGraphQLResponse(g)) { + const isError = g.errors?.length > 0; + const contentString = `${isError ? "A GraphQL error occurred while executing the operation:" : ""}${JSON.stringify(g, null, 2)}`; + return { + isError, + content: [{ type: "text", text: contentString }], + }; + } else { + return mcpError(JSON.stringify(g, null, 2)); + } +} + +export function parseVariables(unparsedVariables?: string): Record { + try { + const variables = JSON.parse(unparsedVariables || "{}"); + if (typeof variables !== "object") throw new Error("not an object"); + return variables; + } catch (e) { + throw new Error("Provided variables string `" + unparsedVariables + "` is not valid JSON."); + } +} diff --git a/src/mcp/util/dataconnect/emulator.ts b/src/mcp/util/dataconnect/emulator.ts new file mode 100644 index 00000000000..b0a75d60ac8 --- /dev/null +++ b/src/mcp/util/dataconnect/emulator.ts @@ -0,0 +1,16 @@ +import { Emulators } from "../../../emulator/types"; +import { Client } from "../../../apiv2"; +import { DATACONNECT_API_VERSION } from "../../../dataconnect/dataplaneClient"; +import type { FirebaseMcpServer } from "../../index"; + +export async function getDataConnectEmulatorClient(host: FirebaseMcpServer): Promise { + const emulatorUrl = await host.getEmulatorUrl(Emulators.DATACONNECT); + + const apiClient = new Client({ + urlPrefix: emulatorUrl, + apiVersion: DATACONNECT_API_VERSION, + auth: false, + }); + + return apiClient; +} diff --git a/src/messaging/interfaces.ts b/src/messaging/interfaces.ts new file mode 100644 index 00000000000..f63a05dc264 --- /dev/null +++ b/src/messaging/interfaces.ts @@ -0,0 +1,33 @@ +export interface BaseMessage { + notification?: Notification; +} + +export interface TokenMessage extends BaseMessage { + token: string; +} + +export interface TopicMessage extends BaseMessage { + topic: string; +} + +/** + * Payload for the {@link Messaging.send} operation. The payload contains all the fields + * in the BaseMessage type, and exactly one of token, topic or condition. + */ +export type Message = TokenMessage | TopicMessage; + +/** + * A notification that can be included in {@link Message}. + */ +export interface Notification { + /** + * The title of the notification. + */ + title?: string; + /** + * The notification body + */ + body?: string; + /** URL of an image to include in the notification. */ + image?: string; +} diff --git a/src/messaging/sendMessage.ts b/src/messaging/sendMessage.ts new file mode 100644 index 00000000000..939c73d437a --- /dev/null +++ b/src/messaging/sendMessage.ts @@ -0,0 +1,66 @@ +import { messagingApiOrigin } from "../api"; +import { Client } from "../apiv2"; +import { logger } from "../logger"; +import { FirebaseError } from "../error"; +import { Message, Notification } from "./interfaces"; + +const TIMEOUT = 10000; + +const apiClient = new Client({ + urlPrefix: messagingApiOrigin(), + apiVersion: "v1", +}); + +/** + * Function to send a message to an FCM Token. + * @param projectId Project ID to which this token belongs to. + * @param options Parameters including message body and target. + * @return {Promise} Returns a promise fulfilled with a unique message ID string + * after the message has been successfully handed off to the FCM service for delivery. + */ +export async function sendFcmMessage( + projectId: string, + options: { + topic?: string; + token?: string; + title?: string; + body?: string; + image?: string; + }, +): Promise { + try { + const notification: Notification = { + title: options.title, + body: options.body, + image: options.image, + }; + if (!options.token && !options.topic) { + throw new FirebaseError("Must supply either token or topic to send FCM message."); + } + const message: Message = options.token + ? { + token: options.token!, + notification: notification, + } + : { + topic: options.topic!, + notification: notification, + }; + const messageData = { + message: message, + }; + const res = await apiClient.request({ + method: "POST", + path: `/projects/${projectId}/messages:send`, + body: JSON.stringify(messageData), + timeout: TIMEOUT, + }); + return res.body; + } catch (err: any) { + logger.debug(err.message); + throw new FirebaseError( + `Failed to send message to '${options.token || options.topic}' for the project '${projectId}'. `, + { original: err }, + ); + } +} diff --git a/src/metapgrogramming.spec.ts b/src/metapgrogramming.spec.ts new file mode 100644 index 00000000000..957a1b4932e --- /dev/null +++ b/src/metapgrogramming.spec.ts @@ -0,0 +1,118 @@ +import { expect } from "chai"; +import { + SameType, + RecursiveKeyOf, + LeafElems, + DeepPick, + DeepOmit, + RequireKeys, + DeepExtract, +} from "./metaprogramming"; + +describe("metaprogramming", () => { + 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..6db9a7f1198 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: unknown) { 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 30377df7184..69176b8ab60 100644 --- a/src/operation-poller.ts +++ b/src/operation-poller.ts @@ -2,14 +2,28 @@ 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; apiVersion: string; operationResourceName: string; backoff?: number; + maxBackoff?: number; masterTimeout?: number; onPoll?: (operation: OperationResult) => any; + doneFn?: (op: any) => boolean; + headers?: Record; } const DEFAULT_INITIAL_BACKOFF_DELAY_MILLIS = 250; @@ -19,8 +33,10 @@ export interface OperationResult { done?: boolean; response?: T; error?: { + name: string; message: string; code: number; + details?: any[]; }; metadata?: { [key: string]: any; @@ -39,6 +55,7 @@ export class OperationPoller { name: options.pollerName || "LRO Poller", concurrency: 1, retries: Number.MAX_SAFE_INTEGER, + maxBackoff: options.maxBackoff, backoff: options.backoff || DEFAULT_INITIAL_BACKOFF_DELAY_MILLIS, }); @@ -49,7 +66,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!; @@ -64,8 +81,10 @@ export class OperationPoller { return async () => { let res; try { - res = await apiClient.get>(options.operationResourceName); - } catch (err) { + res = await apiClient.get>(options.operationResourceName, { + headers: options.headers, + }); + } catch (err: any) { // Responses with 500 or 503 status code are treated as retriable errors. if (err.status === 500 || err.status === 503) { throw err; @@ -75,7 +94,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 new file mode 100644 index 00000000000..7374ef1b688 --- /dev/null +++ b/src/options.ts @@ -0,0 +1,41 @@ +import { Config } from "./config"; +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 BaseOptions { + cwd: string; + configPath: string; + only: string; + except: string; + config: Config; + filteredTargets: string[]; + force: boolean; + + // Options which are present on every command + project?: string; + projectAlias?: string; + projectId?: string; + projectNumber?: string; + projectRoot?: string; + account?: string; + json: boolean; + nonInteractive: boolean; + interactive: boolean; + debug: boolean; + + rc: RC; + // Emulator specific import/export options + exportOnExit?: boolean | string; + import?: string; + + isMCP?: boolean; +} + +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/parseRuntimeAndValidateSDK.ts b/src/parseRuntimeAndValidateSDK.ts deleted file mode 100644 index 7be80c470d6..00000000000 --- a/src/parseRuntimeAndValidateSDK.ts +++ /dev/null @@ -1,137 +0,0 @@ -import * as _ from "lodash"; -import * as path from "path"; -import * as clc from "cli-color"; -import * as semver from "semver"; - -import { getFunctionsSDKVersion } from "./checkFirebaseSDKVersion"; -import { FirebaseError } from "./error"; -import * as utils from "./utils"; -import { logger } from "./logger"; -import * as track from "./track"; - -// have to require this because no @types/cjson available -// eslint-disable-next-line @typescript-eslint/no-var-requires -const cjson = require("cjson"); - -const MESSAGE_FRIENDLY_RUNTIMES: { [key: string]: string } = { - nodejs6: "Node.js 6 (Deprecated)", - nodejs8: "Node.js 8 (Deprecated)", - nodejs10: "Node.js 10", - nodejs12: "Node.js 12", - nodejs14: "Node.js 14 (Beta)", -}; - -const ENGINE_RUNTIMES: { [key: string]: string } = { - 6: "nodejs6", - 8: "nodejs8", - 10: "nodejs10", - 12: "nodejs12", - 14: "nodejs14", -}; - -const ENGINE_RUNTIMES_NAMES = Object.values(ENGINE_RUNTIMES); - -export const RUNTIME_NOT_SET = - "`runtime` field is required but was not found in firebase.json.\n" + - "To fix this, add the following lines to the `functions` section of your firebase.json:\n" + - '"runtime": "nodejs12"\n'; - -export const UNSUPPORTED_NODE_VERSION_FIREBASE_JSON_MSG = clc.bold( - `functions.runtime value is unsupported. ` + - `Valid choices are: ${clc.bold("nodejs10")}, ${clc.bold("nodejs12")}, and ${clc.bold( - "nodejs14" - )}.` -); - -export const UNSUPPORTED_NODE_VERSION_PACKAGE_JSON_MSG = clc.bold( - `package.json in functions directory has an engines field which is unsupported. ` + - `Valid choices are: ${clc.bold('{"node": "10"}')}, ${clc.bold('{"node":"12"}')}, and ${clc.bold( - '{"node":"14"}' - )}.` -); - -export const DEPRECATED_NODE_VERSION_INFO = - `\n\nDeploys to runtimes below Node.js 10 are now disabled in the Firebase CLI. ` + - `${clc.bold( - `Existing Node.js 8 functions ${clc.underline("will stop executing on 2021-03-15")}` - )}. Update existing functions to Node.js 10 or greater as soon as possible.`; - -export const FUNCTIONS_SDK_VERSION_TOO_OLD_WARNING = - clc.bold.yellow("functions: ") + - "You must have a " + - clc.bold("firebase-functions") + - " version that is at least 2.0.0. Please run " + - clc.bold("npm i --save firebase-functions@latest") + - " in the functions folder."; - -function functionsSDKTooOld(sourceDir: string, minRange: string): boolean { - const userVersion = getFunctionsSDKVersion(sourceDir); - if (!userVersion) { - logger.debug("getFunctionsSDKVersion was unable to retrieve 'firebase-functions' version"); - return false; - } - try { - if (!semver.intersects(userVersion, minRange)) { - return true; - } - } catch (e) { - // do nothing - } - - return false; -} - -/** - * Returns a friendly string denoting the chosen runtime: Node.js 8 for nodejs 8 - * for example. If no friendly name for runtime is found, returns back the raw runtime. - * @param runtime name of runtime in raw format, ie, "nodejs8" or "nodejs10" - * @return A human-friendly string describing the runtime. - */ -export function getHumanFriendlyRuntimeName(runtime: string): string { - return _.get(MESSAGE_FRIENDLY_RUNTIMES, runtime, runtime); -} - -function getRuntimeChoiceFromPackageJson(sourceDir: string): string { - const packageJsonPath = path.join(sourceDir, "package.json"); - const loaded = cjson.load(packageJsonPath); - const engines = loaded.engines; - if (!engines || !engines.node) { - // We should really never hit this, since deploy/functions/prepare already checked that - // the runtime is defined in either firebase.json or the "engines" field of the package.json. - throw new FirebaseError(RUNTIME_NOT_SET); - } - - return ENGINE_RUNTIMES[engines.node]; -} - -/** - * Returns the Node.js version to be used for the function(s) as defined in the - * either the `runtime` field of firebase.json or the package.json. - * @param sourceDir directory where the functions are defined. - * @param runtimeFromConfig runtime from the `functions` section of firebase.json file (may be empty). - * @return The runtime, e.g. `nodejs12`. - */ -export function getRuntimeChoice(sourceDir: string, runtimeFromConfig?: string): string { - const runtime = runtimeFromConfig || getRuntimeChoiceFromPackageJson(sourceDir); - const errorMessage = - (runtimeFromConfig - ? UNSUPPORTED_NODE_VERSION_FIREBASE_JSON_MSG - : UNSUPPORTED_NODE_VERSION_PACKAGE_JSON_MSG) + DEPRECATED_NODE_VERSION_INFO; - - if (!runtime || !ENGINE_RUNTIMES_NAMES.includes(runtime)) { - track("functions_runtime_notices", "package_missing_runtime"); - throw new FirebaseError(errorMessage, { exit: 1 }); - } - - if (["nodejs6", "nodejs8"].includes(runtime)) { - track("functions_runtime_notices", `${runtime}_deploy_prohibited`); - throw new FirebaseError(errorMessage, { exit: 1 }); - } - - if (functionsSDKTooOld(sourceDir, ">=2")) { - track("functions_runtime_notices", "functions_sdk_too_old"); - utils.logWarning(FUNCTIONS_SDK_VERSION_TOO_OLD_WARNING); - } - - return runtime; -} diff --git a/src/parseTriggers.js b/src/parseTriggers.js deleted file mode 100644 index 77e79c05392..00000000000 --- a/src/parseTriggers.js +++ /dev/null @@ -1,69 +0,0 @@ -"use strict"; - -var { FirebaseError } = require("./error"); -var fork = require("child_process").fork; -var path = require("path"); - -var _ = require("lodash"); - -var TRIGGER_PARSER = path.resolve(__dirname, "./triggerParser.js"); - -/** - * Removes any inspect options (`inspect` or `inspect-brk`) from options so the forked process is able to run (otherwise - * it'll inherit process values and will use the same port). - * @param {string[]} options From either `process.execArgv` or `NODE_OPTIONS` envar (which is a space separated string) - * @return {string[]} `options` without any `inspect` or `inspect-brk` values - */ -function removeInspectOptions(options) { - return options.filter((opt) => !opt.startsWith("--inspect")); -} - -module.exports = function (projectId, sourceDir, configValues, firebaseConfig) { - return new Promise(function (resolve, reject) { - var env = _.cloneDeep(process.env); - env.GCLOUD_PROJECT = projectId; - if (!_.isEmpty(configValues)) { - env.CLOUD_RUNTIME_CONFIG = JSON.stringify(configValues); - if (configValues.firebase) { - // In case user has `admin.initalizeApp()` at the top of the file and it was executed before firebase-functions v1 - // is loaded, which would normally set FIREBASE_CONFIG. - env.FIREBASE_CONFIG = JSON.stringify(configValues.firebase); - } - } - if (firebaseConfig) { - // This value will be populated during functions emulation - // Make legacy firbase-functions SDK work - env.FIREBASE_PROJECT = firebaseConfig; - // In case user has `admin.initalizeApp()` at the top of the file and it was executed before firebase-functions v1 - // is loaded, which would normally set FIREBASE_CONFIG. - env.FIREBASE_CONFIG = firebaseConfig; - } - - var execArgv = removeInspectOptions(process.execArgv); - if (env.NODE_OPTIONS) { - env.NODE_OPTIONS = removeInspectOptions(env.NODE_OPTIONS.split(" ")).join(" "); - } - - var parser = fork(TRIGGER_PARSER, [sourceDir], { silent: true, env: env, execArgv: execArgv }); - - parser.on("message", function (message) { - if (message.triggers) { - resolve(message.triggers); - } else if (message.error) { - reject(new FirebaseError(message.error, { exit: 1 })); - } - }); - - parser.on("exit", function (code) { - if (code !== 0) { - reject( - new FirebaseError( - "There was an unknown problem while trying to parse function triggers. " + - "Please ensure you are using Node.js v6 or greater.", - { exit: 2 } - ) - ); - } - }); - }); -}; 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/prepareFunctionsUpload.ts b/src/prepareFunctionsUpload.ts deleted file mode 100644 index ee5ed51dabd..00000000000 --- a/src/prepareFunctionsUpload.ts +++ /dev/null @@ -1,118 +0,0 @@ -import * as _ from "lodash"; -import * as archiver from "archiver"; -import * as clc from "cli-color"; -import * as filesize from "filesize"; -import * as fs from "fs"; -import * as path from "path"; -import * as tmp from "tmp"; - -import { FirebaseError } from "./error"; -import * as functionsConfig from "./functionsConfig"; -import * as getProjectId from "./getProjectId"; -import { logger } from "./logger"; -import * as utils from "./utils"; -import * as parseTriggers from "./parseTriggers"; -import * as fsAsync from "./fsAsync"; - -const CONFIG_DEST_FILE = ".runtimeconfig.json"; - -async function getFunctionsConfig(context: any): Promise<{ [key: string]: any }> { - let config = {}; - if (context.runtimeConfigEnabled) { - try { - config = await functionsConfig.materializeAll(context.firebaseConfig.projectId); - } catch (err) { - logger.debug(err); - const errorCode = _.get(err, "context.response.statusCode"); - if (errorCode === 500 || errorCode === 503) { - throw new FirebaseError( - "Cloud Runtime Config is currently experiencing issues, " + - "which is preventing your functions from being deployed. " + - "Please wait a few minutes and then try to deploy your functions again." + - "\nRun `firebase deploy --except functions` if you want to continue deploying the rest of your project." - ); - } - config = {}; - } - } - - const firebaseConfig = _.get(context, "firebaseConfig"); - _.set(config, "firebase", firebaseConfig); - return config; -} - -async function pipeAsync(from: archiver.Archiver, to: fs.WriteStream) { - return new Promise((resolve, reject) => { - to.on("finish", resolve); - to.on("error", reject); - from.pipe(to); - }); -} - -async function packageSource(options: any, sourceDir: string, configValues: any) { - const tmpFile = tmp.fileSync({ prefix: "firebase-functions-", postfix: ".zip" }).name; - const fileStream = fs.createWriteStream(tmpFile, { - flags: "w", - encoding: "binary", - }); - const archive = archiver("zip"); - - // We must ignore firebase-debug.log or weird things happen if - // you're in the public dir when you deploy. - // We ignore any CONFIG_DEST_FILE that already exists, and write another one - // with current config values into the archive in the "end" handler for reader - const ignore = options.config.get("functions.ignore", ["node_modules", ".git"]); - ignore.push( - "firebase-debug.log", - "firebase-debug.*.log", - CONFIG_DEST_FILE /* .runtimeconfig.json */ - ); - try { - const files = await fsAsync.readdirRecursive({ path: sourceDir, ignore: ignore }); - _.forEach(files, (file) => { - archive.file(file.name, { - name: path.relative(sourceDir, file.name), - mode: file.mode, - }); - }); - archive.append(JSON.stringify(configValues, null, 2), { - name: CONFIG_DEST_FILE, - mode: 420 /* 0o644 */, - }); - archive.finalize(); - await pipeAsync(archive, fileStream); - } catch (err) { - throw new FirebaseError( - "Could not read source directory. Remove links and shortcuts and try again.", - { - original: err, - exit: 1, - } - ); - } - utils.logBullet( - clc.cyan.bold("functions:") + - " packaged " + - clc.bold(options.config.get("functions.source")) + - " (" + - filesize(archive.pointer()) + - ") for uploading" - ); - return { - file: tmpFile, - stream: fs.createReadStream(tmpFile), - size: archive.pointer(), - }; -} - -export async function prepareFunctionsUpload(context: any, options: any) { - const sourceDir = options.config.path(options.config.get("functions.source")); - const configValues = await getFunctionsConfig(context); - const triggers = await parseTriggers(getProjectId(options), sourceDir, configValues); - options.config.set("functions.triggers", triggers); - if (triggers.length === 0) { - // No need to package if there are 0 functions to deploy. - return; - } - return packageSource(options, sourceDir, configValues); -} 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 4bd643a1ec8..00000000000 --- a/src/previews.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { has, set } from "lodash"; -import { configstore } from "./configstore"; - -interface PreviewFlags { - rtdbrules: boolean; - ext: boolean; - extdev: boolean; - rtdbmanagement: boolean; - storageemulator: boolean; -} - -export const previews: PreviewFlags = Object.assign( - { - // insert previews here... - rtdbrules: false, - ext: false, - extdev: false, - rtdbmanagement: false, - storageemulator: false, - }, - configstore.get("previews") -); - -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..61fa4d39486 --- /dev/null +++ b/src/profileReport.ts @@ -0,0 +1,661 @@ +import * as clc from "colorette"; +import * as Table from "cli-table3"; +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: Table.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/projectUtils.spec.ts b/src/projectUtils.spec.ts new file mode 100644 index 00000000000..e1d48f385b5 --- /dev/null +++ b/src/projectUtils.spec.ts @@ -0,0 +1,100 @@ +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"; + +describe("getProjectId", () => { + it("should prefer projectId, falling back to project", () => { + expect(getProjectId({ projectId: "this", project: "not_that" })).to.eq("this"); + expect(getProjectId({ project: "this" })).to.eq("this"); + }); +}); + +describe("needProjectId", () => { + let options: { rc: RC; projectId?: string; project?: string }; + beforeEach(() => { + options = { rc: new RC(undefined, {}) }; + }); + + it("should throw when no project provided and no aliases available", () => { + expect(() => needProjectId(options)).to.throw("No currently active project"); + }); + + it("should throw and mention aliases when they are available", () => { + options.rc = new RC(undefined, { projects: { "example-alias": "example-project" } }); + expect(() => needProjectId(options)).to.throw("aliases are available"); + }); + + it("should return projectId, falling back to project", () => { + expect(needProjectId({ ...options, projectId: "this", project: "not_that" })).to.eq("this"); + expect(needProjectId({ ...options, project: "this" })).to.eq("this"); + }); +}); + +describe("needProjectNumber", () => { + let getProjectStub: sinon.SinonStub; + + beforeEach(() => { + getProjectStub = sinon.stub(projects, "getProject").throws(new Error("stubbed")); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should return the project number from options, if present", async () => { + const n = await needProjectNumber({ projectNumber: 1 }); + + expect(n).to.equal(1); + expect(getProjectStub).to.not.have.been.called; + }); + + it("should fetch the project number if necessary", async () => { + getProjectStub.returns({ projectNumber: 2 }); + + const n = await needProjectNumber({ project: "foo" }); + + expect(n).to.equal(2); + expect(getProjectStub).to.have.been.calledOnceWithExactly("foo"); + }); + + it("should reject with an error on an error", async () => { + getProjectStub.rejects(new Error("oh no")); + + await expect(needProjectNumber({ project: "foo" })).to.eventually.be.rejectedWith( + Error, + "oh no", + ); + }); +}); + +describe("getAliases", () => { + it("should return the aliases for a projectId", () => { + const testProjectId = "my-project"; + const testOptions = { + rc: { + hasProjects: true, + projects: { + prod: testProjectId, + prod2: testProjectId, + staging: "other-project", + }, + }, + }; + + expect(getAliases(testOptions, testProjectId).sort()).to.deep.equal(["prod", "prod2"]); + }); + + it("should return an empty array if there are no aliases in rc", () => { + const testProjectId = "my-project"; + const testOptions = { + rc: { + hasProjects: false, + }, + }; + + expect(getAliases(testOptions, testProjectId)).to.deep.equal([]); + }); +}); diff --git a/src/projectUtils.ts b/src/projectUtils.ts new file mode 100644 index 00000000000..7a16b7f2fc7 --- /dev/null +++ b/src/projectUtils.ts @@ -0,0 +1,106 @@ +import { getProject } from "./management/projects"; +import { RC } from "./rc"; + +import * as clc from "colorette"; +import { marked } from "marked"; + +const { FirebaseError } = require("./error"); + +/** + * Retrieves the projectId from a command's options context. + * + * @param options The options context for a command. + * @returns The projectId + */ +export function getProjectId({ + projectId, + project, +}: { + projectId?: string; + project?: string; +}): string | undefined { + return projectId || project; +} + +/** + * Tries to determine the correct projectId given current + * command context. Errors out if unable to determine. + * @returns The projectId + */ +export function needProjectId({ + projectId, + project, + rc, +}: { + projectId?: string; + project?: string; + rc?: RC; +}): string { + if (projectId || project) { + return projectId || project!; + } + + const aliases = rc?.projects || {}; + const aliasCount = Object.keys(aliases).length; + + if (aliasCount === 0) { + throw new FirebaseError( + "No currently active project.\n" + + "To run this command, you need to specify a project. You have two options:\n" + + "- Run this command with " + + clc.bold("--project ") + + ".\n" + + "- Set an active project by running " + + clc.bold("firebase use --add") + + ", then rerun this command.\n" + + "To list all the Firebase projects to which you have access, run " + + clc.bold("firebase projects:list") + + ".\n" + + marked( + "To learn about active projects for the CLI, visit https://firebase.google.com/docs/cli#project_aliases", + ), + ); + } + + const aliasList = Object.entries(aliases) + .map(([aname, projectId]) => ` ${aname} (${projectId})`) + .join("\n"); + + throw new FirebaseError( + "No project active, but project aliases are available.\n\nRun " + + clc.bold("firebase use ") + + " with one of these options:\n\n" + + aliasList, + ); +} + +/** + * Fetches the project number, throwing an error if unable to resolve the + * project identifiers in the context to a number. + * + * @param options CLI options. + * @return the project number, as a string. + */ +export async function needProjectNumber(options: any): Promise { + if (options.projectNumber) { + return options.projectNumber; + } + const projectId = needProjectId(options); + const metadata = await getProject(projectId); + options.projectNumber = metadata.projectNumber; + return options.projectNumber; +} + +/** + * Looks up all aliases for projectId. + * @param options CLI options. + * @param projectId A project id to get the aliases for + */ +export function getAliases(options: any, projectId: string): string[] { + if (options.rc.hasProjects) { + return Object.entries(options.rc.projects) + .filter((entry) => entry[1] === projectId) + .map((entry) => entry[0]); + } + return []; +} diff --git a/src/prompt.spec.ts b/src/prompt.spec.ts new file mode 100644 index 00000000000..3f662324a8c --- /dev/null +++ b/src/prompt.spec.ts @@ -0,0 +1,216 @@ +import { expect } from "chai"; + +import { FirebaseError } from "./error"; +import * as prompt from "./prompt"; + +describe("prompt", () => { + describe("guard", () => { + it("returns default in non-interactive if present", () => { + const { shouldReturn, value } = prompt.guard({ + message: "message", + nonInteractive: true, + default: 42, + }); + expect(shouldReturn).to.be.true; + expect(value).to.equal(42); + }); + + it("does not suggest returning if interactive", () => { + const { shouldReturn, value } = prompt.guard({ + message: "message", + nonInteractive: false, + default: 42, + }); + expect(shouldReturn).to.be.false; + expect(value).to.be.undefined; + }); + + it("throws if non-interactive without default", () => { + expect(() => + prompt.guard({ + message: "message", + nonInteractive: true, + }), + ).to.throw( + FirebaseError, + 'Question "message" does not have a default and cannot be answered in non-interactive mode', + ); + }); + }); + + // Note: We cannot actuall have test coverage that the APIs pass through to inquirer because it is ESM + // and cannot be mocked. + describe("query types", () => { + describe("confirm", () => { + it("handles force", async () => { + const result = await prompt.confirm({ + message: "Continue?", + default: false, + force: true, + }); + expect(result).to.be.true; + }); + + it("handles non-interactive with default", async () => { + const result = await prompt.confirm({ + message: "Continue?", + nonInteractive: true, + default: false, + }); + expect(result).to.be.false; + }); + + it("throws in non-interactive without default", async () => { + await expect( + prompt.confirm({ + message: "Continue?", + nonInteractive: true, + }), + ).to.be.rejectedWith( + FirebaseError, + 'Question "Continue?" does not have a default and cannot be answered in non-interactive mode', + ); + }); + }); + + describe("input", () => { + it("handles non-interactive with default", async () => { + const result = await prompt.input({ + message: "Name?", + nonInteractive: true, + default: "Inigo Montoya", + }); + expect(result).to.equal("Inigo Montoya"); + }); + + it("throws in non-interactive without default", async () => { + await expect( + prompt.input({ + message: "Name?", + nonInteractive: true, + }), + ).to.be.rejectedWith( + FirebaseError, + 'Question "Name?" does not have a default and cannot be answered in non-interactive mode', + ); + }); + }); + + describe("checkbox", () => { + it("handles non-interactive with default", async () => { + const result = await prompt.checkbox({ + message: "Tools?", + nonInteractive: true, + choices: ["hammer", "wrench", "saw"], + default: ["hammer", "wrench"], + }); + expect(result).to.deep.equal(["hammer", "wrench"]); + }); + + it("throws in non-interactive without default", async () => { + await expect( + prompt.checkbox({ + message: "Tools?", + nonInteractive: true, + choices: ["hammer", "wrench", "saw"], + }), + ).to.be.rejectedWith( + FirebaseError, + 'Question "Tools?" does not have a default and cannot be answered in non-interactive mode', + ); + }); + }); + + describe("select", () => { + it("handles non-interactive with default", async () => { + const result = await prompt.select({ + message: "Tool?", + nonInteractive: true, + choices: ["hammer", "wrench", "saw"], + default: "wrench", + }); + expect(result).to.equal("wrench"); + }); + + it("throws in non-interactive without default", async () => { + await expect( + prompt.select({ + message: "Tool?", + nonInteractive: true, + choices: ["hammer", "wrench", "saw"], + }), + ).to.be.rejectedWith( + FirebaseError, + 'Question "Tool?" does not have a default and cannot be answered in non-interactive mode', + ); + }); + }); + + describe("number", () => { + it("handles non-interactive with default", async () => { + const result = await prompt.number({ + message: "Count?", + nonInteractive: true, + default: 42, + }); + expect(result).to.equal(42); + }); + + it("throws in non-interactive without default", async () => { + await expect( + prompt.number({ + message: "Count?", + nonInteractive: true, + }), + ).to.be.rejectedWith( + FirebaseError, + 'Question "Count?" does not have a default and cannot be answered in non-interactive mode', + ); + }); + }); + + describe("password", () => { + it("throws in non-interactive", async () => { + await expect( + prompt.password({ + message: "Password?", + nonInteractive: true, + }), + ).to.be.rejectedWith( + FirebaseError, + 'Question "Password?" does not have a default and cannot be answered in non-interactive mode', + ); + }); + }); + + describe("search", () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const source = (term: string | undefined) => { + return ["a", "b", "c"]; + }; + + it("handles non-interactive with default", async () => { + const result = await prompt.search({ + message: "Letter?", + nonInteractive: true, + source, + default: "b", + }); + expect(result).to.equal("b"); + }); + + it("throws in non-interactive without default", async () => { + await expect( + prompt.search({ + message: "Letter?", + nonInteractive: true, + source, + }), + ).to.be.rejectedWith( + FirebaseError, + 'Question "Letter?" does not have a default and cannot be answered in non-interactive mode', + ); + }); + }); + }); +}); diff --git a/src/prompt.ts b/src/prompt.ts index 2bd6f05c8fa..fdf9865d731 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -1,59 +1,301 @@ -import * as inquirer from "inquirer"; -import * as _ from "lodash"; - +import * as inquirer from "@inquirer/prompts"; import { FirebaseError } from "./error"; +export { Separator } from "@inquirer/prompts"; + +/** + * Common options for all prompts. + */ +export interface BasicOptions { + message: string; + default?: T; + force?: boolean; + nonInteractive?: boolean; +} + +/** + * Guard function to check if the prompt should return a default value or throw an error. + * This is used to prevent prompts from being shown in non-interactive mode. + * + * @param opts - The options for the prompt. + * @returns An object indicating whether to return a value or not. + */ +export function guard( + opts: BasicOptions, +): { shouldReturn: true; value: T } | { shouldReturn: false; value: undefined } { + if (!opts.nonInteractive) { + return { shouldReturn: false, value: undefined }; + } + if (typeof opts.default !== "undefined") { + return { shouldReturn: true, value: opts.default }; + } + throw new FirebaseError( + `Question "${opts.message}" does not have a default and cannot be answered in non-interactive mode`, + ); +} + +/** + * Options for the Input function. + * + * Exported because Inqurier does not export its own input configs anymore. Some + * unused options are missing, such as theme. + */ +export type InputConfig = BasicOptions & { + transformer?: ( + value: string, + { + isFinal, + }: { + isFinal: boolean; + }, + ) => string; + validate?: (value: string) => boolean | string | Promise; +}; + +/** + * Prompt for a string input. + */ +export async function input(message: string): Promise; +/** + * Prompt for a string input. + */ +export async function input(opts: InputConfig): Promise; + +export async function input(opts: InputConfig | string): Promise { + if (typeof opts === "string") { + opts = { message: opts }; + } else { + const { shouldReturn, value } = guard(opts); + if (shouldReturn) { + return value; + } + } + return inquirer.input(opts); +} + +/** + * Options for the confirm function. + * + * Exported because Inquirer does not export its own input configs anymore. Some unused + * options are missing, such as theme. + */ +export type ConfirmConfig = BasicOptions & { + transformer?: (value: boolean) => string; +}; + +/** + * Prompt a user to confirm a selection + */ +export async function confirm(message: string): Promise; +/** + * Prompt a user to confirm a selection + * Will abort if nonInteractive and not force + */ +export async function confirm(opts: ConfirmConfig): Promise; + +export async function confirm(opts: string | ConfirmConfig) { + if (typeof opts === "string") { + opts = { message: opts }; + } else { + if (opts.force) { + // TODO: Should we print what we've forced? + return true; + } + const { shouldReturn, value } = guard(opts); + if (shouldReturn) { + return value; + } + } + + return inquirer.confirm(opts); +} + +/** + * A choice in a checkbox prompt. + * Strongly typed to allow enum propagation + */ +export type Choice = { + value: Value; + name?: string; + description?: string; + short?: string; + disabled?: boolean | string; + checked?: boolean; + type?: never; +}; + +// Personal hack deviating from inquirer to allow string values to propagate +// as strings or enum arrays without needing an explicit type at the call stie. +type MaybeLiteral = Value extends string ? Value : never; + +/** + * Options for the checkbox function. + * + * Exported because Inquirer does not export its own input configs anymore. Some unused + * options are missing, such as theme. Some options are missing to promote consistency + * within the CLI. + */ +export type CheckboxOptions = BasicOptions & { + message: string; + choices: + | readonly (MaybeLiteral | inquirer.Separator)[] + | readonly (inquirer.Separator | Choice)[]; + validate?: + | ((choices: readonly Choice[]) => boolean | string | Promise) + | undefined; + pageSize?: number; +}; + +/** + * Prompt a user for one or more of many options. + * Can accept a generic type for enum values. + */ +export async function checkbox(opts: CheckboxOptions): Promise { + const { shouldReturn, value } = guard(opts); + if (shouldReturn) { + return value; + } + return inquirer.checkbox({ + ...opts, + loop: true, + }); +} + +/** + * Options for the checkbox function. + * + * Exported because Inquirer does not export its own input configs anymore. Some unused + * options are missing, such as theme. Some options are missing to promote consistency + * within the CLI. + * TODO: Had difficulty coalescing literals using Choice[]. Look into it. + */ +export type SelectOptions = BasicOptions & { + choices: + | readonly (MaybeLiteral | inquirer.Separator)[] + | readonly (inquirer.Separator | Choice)[]; + pageSize?: number; +}; + +/** + * Prompt a user to make a choice amongst a list. + */ +export async function select(opts: SelectOptions): Promise { + const { shouldReturn, value } = guard(opts); + if (shouldReturn) { + return value; + } + return inquirer.select({ + ...opts, + loop: false, + }); +} + /** - * Question type for inquirer. See - * https://www.npmjs.com/package/inquirer#question + * Options for the number function. + * + * Exported because Inquirer does not export its own input configs anymore. Some unused + * options are missing, such as theme. Some options are missing to promote consistency + * within the CLI. */ -export type Question = inquirer.Question; +export type NumberOptions = BasicOptions & { + min?: number; + max?: number; + step?: number | "any"; + validate?: (value: number | undefined) => boolean | string | Promise; +}; /** - * 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 - * 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. - * @param options The options object passed through by Command. - * @param questions `Question`s to ask the user. - * @return The answers, keyed by the `name` of the `Question`. + * Prompt a user for a number. */ -export async function prompt(options: { [key: string]: any }, questions: Question[]): Promise { - const prompts = []; - for (const question of questions) { - if (question.name && options[question.name] === undefined) { - prompts.push(question); +export async function number(message: string): Promise; + +/** + * Prompt a user for a number. + */ +export async function number(opts: NumberOptions): Promise; + +/** + * Prompt a user for an optional number. + */ +export async function number( + opts: NumberOptions & { required: false }, +): Promise; + +export async function number(opts: string | NumberOptions): Promise { + if (typeof opts === "string") { + opts = { message: opts }; + } else { + const { shouldReturn, value } = guard(opts); + if (shouldReturn) { + return value; } } - if (prompts.length && options.nonInteractive) { - const missingOptions = _.uniq(_.map(prompts, "name")).join(", "); - throw new FirebaseError( - `Missing required options (${missingOptions}) while running in non-interactive mode`, - { - children: prompts, - exit: 1, - } - ); + return await inquirer.number({ required: true, ...opts }); +} + +/** + * Options for the checkbox function. + * + * Exported because Inquirer does not export its own input configs anymore. Some unused + * options are missing, such as theme. Some options are missing to promote consistency + * within the CLI. + */ +type PasswordOptions = Omit, "default"> & { + validate?: (value: string) => boolean | string | Promise; +}; + +/** + * Prompt for a password; input is hidden. + */ +export async function password(message: string): Promise; + +/** + * Prompt for a password; input is hidden. + */ +export async function password(opts: PasswordOptions): Promise; + +export async function password(opts: string | PasswordOptions): Promise { + if (typeof opts === "string") { + opts = { message: opts }; + } else { + // Note, without default can basically only throw + guard(opts); } - const answers = await inquirer.prompt(prompts); - Object.keys(answers).forEach((k) => { - options[k] = answers[k]; + return inquirer.password({ + ...opts, + mask: "", }); - return answers; } /** - * Quick version of `prompt` to ask a single question. - * @param question The question (of life, the universe, and everything). - * @return The value as returned by `inquirer` for that quesiton. + * Options for the search function. + * + * Exported because Inquirer does not export its own input configs anymore. Some unused + * options are missing, such as theme. Some options are missing to promote consistency + * within the CLI. */ -export async function promptOnce(question: Question): Promise { - question.name = question.name || "question"; - const answers = await prompt({}, [question]); - return answers[question.name]; +export type SearchOptions = BasicOptions & { + source: ( + term: string | undefined, + opt: { + signal: AbortSignal; + }, + ) => + | readonly (string | inquirer.Separator)[] + | readonly (inquirer.Separator | Choice)[] + | Promise + | Promise)[]>; + validate?: ((value: Value) => boolean | string | Promise) | undefined; + pageSize?: number | undefined; +}; + +/** Search for a value given a sorce callback. */ +export async function search(opts: SearchOptions): Promise { + const { shouldReturn, value } = guard(opts); + if (shouldReturn) { + return value; + } + return inquirer.search(opts); } diff --git a/src/rc.js b/src/rc.js deleted file mode 100644 index 43b0227b85b..00000000000 --- a/src/rc.js +++ /dev/null @@ -1,216 +0,0 @@ -"use strict"; - -var _ = require("lodash"); -var clc = require("cli-color"); -var cjson = require("cjson"); -var fs = require("fs"); -var path = require("path"); - -var detectProjectRoot = require("./detectProjectRoot").detectProjectRoot; -var { FirebaseError } = require("./error"); -var fsutils = require("./fsutils"); -var utils = require("./utils"); - -// "exclusive" target implies that a resource can only be assigned a single target name -var TARGET_TYPES = { - storage: { resource: "bucket", exclusive: true }, - database: { resource: "instance", exclusive: true }, - hosting: { resource: "site", exclusive: true }, -}; - -/** - * @constructor - * @this RC - * - * @param {string=} rcpath - * @param {object=} data - */ -var RC = function (rcpath, data) { - this.path = rcpath; - this.data = data || {}; -}; - -RC.prototype = { - set: function (key, value) { - return _.set(this.data, key, value); - }, - - unset: function (key) { - return _.unset(this.data, key); - }, - - get: function (key, fallback) { - return _.get(this.data, key, fallback); - }, - - addProjectAlias: function (alias, project) { - this.set(["projects", alias], project); - return this.save(); - }, - - removeProjectAlias: function (alias) { - this.unset(["projects", alias]); - return this.save(); - }, - - get hasProjects() { - return _.size(this.data.projects) > 0; - }, - - get projects() { - return this.get("projects", {}); - }, - - targets: function (project, type) { - return this.get(["targets", project, type], {}); - }, - - target: function (project, type, name) { - return this.get(["targets", project, type, name], []); - }, - - applyTarget: function (project, type, targetName, resources) { - if (!TARGET_TYPES[type]) { - throw new FirebaseError( - "Unrecognized target type " + - clc.bold(type) + - ". Must be one of " + - _.keys(TARGET_TYPES).join(", "), - { exit: 1 } - ); - } - - if (_.isString(resources)) { - resources = [resources]; - } - - var changed = []; - - // remove resources from existing targets - resources.forEach((resource) => { - var cur = this.findTarget(project, type, resource); - if (cur && cur !== targetName) { - this.unsetTargetResource(project, type, cur, resource); - changed.push({ resource: resource, target: cur }); - } - }); - - // apply resources to new target - var existing = this.get(["targets", project, type, targetName], []); - var list = _.uniq(existing.concat(resources)).sort(); - this.set(["targets", project, type, targetName], list); - - this.save(); - return changed; - }, - - removeTarget: function (project, type, resource) { - var name = this.findTarget(project, type, resource); - if (!name) { - return null; - } - - this.unsetTargetResource(project, type, name, resource); - this.save(); - return name; - }, - - clearTarget: function (project, type, name) { - var exists = this.target(project, type, name).length > 0; - if (!exists) { - return false; - } - this.unset(["targets", project, type, name]); - this.save(); - return true; - }, - - /** - * Finds a target name for the specified type and resource. - */ - findTarget: function (project, type, resource) { - var targets = this.get(["targets", project, type]); - for (var targetName in targets) { - if (_.includes(targets[targetName], resource)) { - return targetName; - } - } - return null; - }, - - /** - * Removes a specific resource from a specified target. Does - * not persist the result. - */ - unsetTargetResource: function (project, type, name, resource) { - var targetPath = ["targets", project, type, name]; - var updatedResources = this.get(targetPath, []).filter(function (r) { - return r !== resource; - }); - - if (updatedResources.length) { - this.set(targetPath, updatedResources); - } else { - this.unset(targetPath); - } - }, - - /** - * Throws an error if the specified target is not configured for - * the specified project. - */ - requireTarget: function (project, type, name) { - var 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 + - " ", - { exit: 1 } - ); - } - - return target; - }, - - /** - * Persists the RC file to disk, or returns false if no path on the instance. - */ - save: function () { - if (this.path) { - fs.writeFileSync(this.path, JSON.stringify(this.data, null, 2), { - encoding: "utf8", - }); - return true; - } - return false; - }, -}; - -RC.loadFile = function (rcpath) { - var data = {}; - if (fsutils.fileExistsSync(rcpath)) { - try { - data = cjson.load(rcpath); - } catch (e) { - // malformed rc file is a warning, not an error - utils.logWarning("JSON error trying to load " + clc.bold(rcpath)); - } - } - return new RC(rcpath, data); -}; - -RC.load = function (options) { - const cwd = options.cwd || process.cwd(); - const dir = detectProjectRoot(options); - const potential = path.resolve(dir || cwd, "./.firebaserc"); - return RC.loadFile(potential); -}; - -module.exports = RC; diff --git a/src/rc.spec.ts b/src/rc.spec.ts new file mode 100644 index 00000000000..0ae76ceab81 --- /dev/null +++ b/src/rc.spec.ts @@ -0,0 +1,163 @@ +import { expect } from "chai"; +import * as path from "path"; +import { RC, loadRC, RCData } from "./rc"; +import { CONFLICT_RC_DIR, FIREBASE_JSON_PATH, INVALID_RC_DIR } from "./test/fixtures/fbrc"; + +const EMPTY_DATA: RCData = { projects: {}, targets: {}, etags: {} }; + +describe("RC", () => { + describe(".load", () => { + it("should load from nearest project directory", () => { + const result = loadRC({ cwd: CONFLICT_RC_DIR }); + expect(result.projects.default).to.eq("top"); + }); + + it("should be an empty object when not in project dir", () => { + const result = loadRC({ cwd: __dirname }); + return expect(result.data).to.deep.eq(EMPTY_DATA); + }); + + it("should not throw up on invalid json", () => { + 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 cwd = __dirname; + const result = loadRC({ cwd, configPath: path.relative(cwd, FIREBASE_JSON_PATH) }); + expect(result.projects.default).to.eq("top"); + }); + }); + + describe("instance methods", () => { + let subject: RC; + beforeEach(() => { + subject = new RC(); + }); + + describe("#addProjectAlias", () => { + it("should set a value in projects.", () => { + expect(subject.addProjectAlias("foo", "bar")).to.be.false; + expect(subject.projects.foo).to.eq("bar"); + }); + }); + + describe("#removeProjectAlias", () => { + it("should remove an already set value in projects.", () => { + subject.addProjectAlias("foo", "bar"); + expect(subject.projects.foo).to.eq("bar"); + expect(subject.removeProjectAlias("foo")).to.be.false; + expect(subject.projects).to.deep.eq({}); + }); + }); + + describe("#hasProjects", () => { + it("should be true if project aliases are set, false if not", () => { + expect(subject.hasProjects).to.be.false; + subject.addProjectAlias("foo", "bar"); + expect(subject.hasProjects).to.be.true; + }); + }); + + describe("#allTargets", () => { + it("should return all targets of all types for a project", () => { + expect(subject.allTargets("foo")).to.deep.eq({}); + subject.applyTarget("foo", "storage", "bar", "baz"); + expect(subject.allTargets("foo")).to.deep.eq({ storage: { bar: ["baz"] } }); + }); + }); + + describe("#targets", () => { + it("should return all targets for specified project and type", () => { + const data = { foo: ["bar"] }; + subject.applyTarget("myproject", "storage", "foo", "bar"); + expect(subject.targets("myproject", "storage")).to.deep.eq(data); + }); + + it("should return an empty object for missing data", () => { + expect(subject.targets("foo", "storage")).to.deep.eq({}); + }); + }); + + describe("#target", () => { + it("should return all resources for a specified target", () => { + subject.applyTarget("myproject", "storage", "foo", ["bar", "baz"]); + expect(subject.target("myproject", "storage", "foo")).to.deep.eq(["bar", "baz"]); + }); + + it("should return an empty array if nothing is found", () => { + expect(subject.target("myproject", "storage", "foo")).to.deep.eq([]); + }); + }); + + describe("#unsetTargetResource", () => { + it("should remove a resource from a target", () => { + subject.applyTarget("myproject", "storage", "foo", ["bar", "baz", "qux"]); + subject.unsetTargetResource("myproject", "storage", "foo", "baz"); + expect(subject.target("myproject", "storage", "foo")).to.deep.eq(["bar", "qux"]); + }); + + it("should no-op if the resource is not in the target", () => { + subject.applyTarget("myproject", "storage", "foo", ["bar", "baz", "qux"]); + subject.unsetTargetResource("myproject", "storage", "foo", "derp"); + expect(subject.target("myproject", "storage", "foo")).to.deep.eq(["bar", "baz", "qux"]); + }); + }); + + describe("#applyTarget", () => { + it("should error for an unrecognized target type", () => { + expect(() => { + subject.applyTarget("myproject", "fake", "foo", ["bar"]); + }).to.throw("Unrecognized target type"); + }); + + it("should coerce a string argument into an array", () => { + subject.applyTarget("myproject", "storage", "foo", "bar"); + expect(subject.target("myproject", "storage", "foo")).to.deep.eq(["bar"]); + }); + + it("should add all resources to the specified target", () => { + subject.applyTarget("myproject", "storage", "foo", "bar"); + subject.applyTarget("myproject", "storage", "foo", ["baz", "qux"]); + expect(subject.target("myproject", "storage", "foo")).to.deep.eq(["bar", "baz", "qux"]); + }); + + it("should remove a resource from a different target", () => { + subject.applyTarget("myproject", "storage", "foo", "bar"); + subject.applyTarget("myproject", "storage", "baz", ["bar", "qux"]); + expect(subject.target("myproject", "storage", "foo")).to.deep.eq([]); + expect(subject.target("myproject", "storage", "baz")).to.deep.eq(["bar", "qux"]); + }); + + it("should return a list of resources that changed targets", () => { + subject.applyTarget("myproject", "storage", "foo", "bar"); + const result = subject.applyTarget("myproject", "storage", "baz", ["bar", "qux"]); + expect(result).to.deep.eq([{ resource: "bar", target: "foo" }]); + }); + }); + + describe("#removeTarget", () => { + it("should remove a the target for a specific resource and return its name", () => { + subject.applyTarget("myproject", "storage", "foo", ["bar", "baz"]); + expect(subject.removeTarget("myproject", "storage", "bar")).to.eq("foo"); + expect(subject.target("myproject", "storage", "foo")).to.deep.eq(["baz"]); + }); + + it("should return null if not present", () => { + expect(subject.removeTarget("myproject", "storage", "fake")).to.be.null; + }); + }); + + describe("#clearTarget", () => { + it("should clear an existing target by name and return true", () => { + subject.applyTarget("myproject", "storage", "foo", ["bar", "baz"]); + expect(subject.clearTarget("myproject", "storage", "foo")).to.be.true; + expect(subject.target("myproject", "storage", "foo")).to.deep.eq([]); + }); + + it("should return false for a non-existent target", () => { + expect(subject.clearTarget("myproject", "storage", "foo")).to.be.false; + }); + }); + }); +}); diff --git a/src/rc.ts b/src/rc.ts new file mode 100644 index 00000000000..8182cf549b5 --- /dev/null +++ b/src/rc.ts @@ -0,0 +1,245 @@ +import * as _ from "lodash"; +import * as clc from "colorette"; +import * as cjson from "cjson"; +import * as fs from "fs"; +import * as path from "path"; + +import { detectProjectRoot } from "./detectProjectRoot"; +import { FirebaseError } from "./error"; +import * as fsutils from "./fsutils"; +import * as utils from "./utils"; + +// "exclusive" target implies that a resource can only be assigned a single target name +const TARGET_TYPES: { [type: string]: { resource: string; exclusive: boolean } } = { + storage: { resource: "bucket", exclusive: true }, + database: { resource: "instance", exclusive: true }, + hosting: { resource: "site", exclusive: true }, +}; + +export function loadRC(options: { cwd?: string; [other: string]: any }) { + const cwd = options.cwd || process.cwd(); + const dir = detectProjectRoot(options); + const potential = path.resolve(dir || cwd, "./.firebaserc"); + return RC.loadFile(potential); +} + +type EtagResourceType = "extensionInstances"; + +export interface RCData { + projects: { [alias: string]: string }; + targets: { + [projectId: string]: { + [targetType: string]: { + [targetName: string]: string[]; + }; + }; + }; + etags: { + [projectId: string]: Record>; + }; +} + +export class RC { + path: string | undefined; + data: RCData; + + static loadFile(rcpath: string): RC { + let data = {}; + if (fsutils.fileExistsSync(rcpath)) { + try { + data = cjson.load(rcpath); + } catch (e: any) { + // malformed rc file is a warning, not an error + utils.logWarning("JSON error trying to load " + clc.bold(rcpath)); + } + } + return new RC(rcpath, data); + } + + constructor(rcpath?: string, data?: Partial) { + this.path = rcpath; + this.data = { projects: {}, targets: {}, etags: {}, ...data }; + } + + private set(key: string | string[], value: any): void { + _.set(this.data, key, value); + return; + } + + private unset(key: string | string[]): boolean { + return _.unset(this.data, key); + } + + /** + * If the given string is a project alias, resolve it to the + * project id. + * @param alias The alias to resolve. + * @returns The resolved project id or the input string if none found. + */ + resolveAlias(alias: string): string { + return this.data.projects[alias] || alias; + } + + hasProjectAlias(alias: string): boolean { + return !!this.data.projects[alias]; + } + + addProjectAlias(alias: string, project: string): boolean { + this.set(["projects", alias], project); + return this.save(); + } + + removeProjectAlias(alias: string): boolean { + this.unset(["projects", alias]); + return this.save(); + } + + get hasProjects(): boolean { + return Object.keys(this.data.projects).length > 0; + } + + get projects(): { [projectId: string]: string } { + return this.data.projects; + } + + allTargets(project: string): { [type: string]: { [targetName: string]: string[] } } { + return this.data.targets[project] || {}; + } + + targets(project: string, type: string): { [targetName: string]: string[] } { + return this.data.targets[project]?.[type] || {}; + } + + target(project: string, type: string, name: string): string[] { + return this.data.targets[project]?.[type]?.[name] || []; + } + + applyTarget(project: string, type: string, targetName: string, resources: string | string[]) { + if (!TARGET_TYPES[type]) { + throw new FirebaseError( + `Unrecognized target type ${clc.bold(type)}. Must be one of ${Object.keys( + TARGET_TYPES, + ).join(", ")}`, + ); + } + + if (typeof resources === "string") { + resources = [resources]; + } + + const changed: { resource: string; target: string }[] = []; + + // remove resources from existing targets + for (const resource of resources) { + const cur = this.findTarget(project, type, resource); + if (cur && cur !== targetName) { + this.unsetTargetResource(project, type, cur, resource); + changed.push({ resource: resource, target: cur }); + } + } + + // apply resources to new target + const existing = this.target(project, type, targetName); + const list = Array.from(new Set(existing.concat(resources))).sort(); + this.set(["targets", project, type, targetName], list); + + this.save(); + return changed; + } + + removeTarget(project: string, type: string, resource: string): string | null { + const name = this.findTarget(project, type, resource); + if (!name) { + return null; + } + + this.unsetTargetResource(project, type, name, resource); + this.save(); + return name; + } + + /** + * Clears a specific target. + * @returns true if the target existed, false if not + */ + clearTarget(project: string, type: string, name: string): boolean { + if (!this.target(project, type, name).length) { + return false; + } + this.unset(["targets", project, type, name]); + this.save(); + return true; + } + + /** + * Finds a target name for the specified type and resource. + * @returns The name of the target (if found) or null (if not). + */ + findTarget(project: string, type: string, resource: string): string | null { + const targets = this.targets(project, type); + for (const targetName in targets) { + if ((targets[targetName] || []).includes(resource)) { + return targetName; + } + } + return null; + } + + /** + * Removes a specific resource from a specified target. Does + * not persist the result. + */ + unsetTargetResource(project: string, type: string, name: string, resource: string): void { + const updatedResources = this.target(project, type, name).filter((r) => r !== resource); + + if (updatedResources.length) { + this.set(["targets", project, type, name], updatedResources); + } else { + this.unset(["targets", project, type, name]); + } + } + + /** + * Throws an error if the specified target is not configured for + * the specified project. + */ + requireTarget(project: string, type: string, name: string): string[] { + 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: + + 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(); + } + + /** + * Persists the RC file to disk, or returns false if no path on the instance. + */ + save() { + if (this.path) { + fs.writeFileSync(this.path, JSON.stringify(this.data, null, 2), { + encoding: "utf8", + }); + return true; + } + return false; + } +} diff --git a/src/remoteconfig/deleteExperiment.spec.ts b/src/remoteconfig/deleteExperiment.spec.ts new file mode 100644 index 00000000000..663963f22e7 --- /dev/null +++ b/src/remoteconfig/deleteExperiment.spec.ts @@ -0,0 +1,66 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import * as clc from "colorette"; + +import { remoteConfigApiOrigin } from "../api"; +import { FirebaseError } from "../error"; +import { deleteExperiment } from "./deleteExperiment"; +import { NAMESPACE_FIREBASE } from "./interfaces"; + +const PROJECT_ID = "12345679"; +const EXPERIMENT_ID = "1"; + +describe("Remote Config Experiment Delete", () => { + afterEach(() => { + expect(nock.isDone()).to.equal(true, "all nock stubs should have been called"); + nock.cleanAll(); + }); + + it("should delete an experiment successfully", async () => { + nock(remoteConfigApiOrigin()) + .delete( + `/v1/projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/experiments/${EXPERIMENT_ID}`, + ) + .reply(200); + + await expect( + deleteExperiment(PROJECT_ID, NAMESPACE_FIREBASE, EXPERIMENT_ID), + ).to.eventually.equal(clc.bold(`Successfully deleted experiment ${clc.yellow(EXPERIMENT_ID)}`)); + }); + + it("should throw FirebaseError if experiment is running", async () => { + const errorMessage = `Experiment ${EXPERIMENT_ID} is currently running and cannot be deleted. If you want to delete this experiment, stop it at https://console.firebase.google.com/project/${PROJECT_ID}/config/experiment/results/${EXPERIMENT_ID}`; + nock(remoteConfigApiOrigin()) + .delete( + `/v1/projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/experiments/${EXPERIMENT_ID}`, + ) + .reply(400, { + error: { + message: errorMessage, + }, + }); + + await expect( + deleteExperiment(PROJECT_ID, NAMESPACE_FIREBASE, EXPERIMENT_ID), + ).to.be.rejectedWith(FirebaseError, errorMessage); + }); + + it("should throw FirebaseError if an internal error occurred", async () => { + nock(remoteConfigApiOrigin()) + .delete( + `/v1/projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/experiments/${EXPERIMENT_ID}`, + ) + .reply(500, { + error: { + message: "Internal server error", + }, + }); + + await expect( + deleteExperiment(PROJECT_ID, NAMESPACE_FIREBASE, EXPERIMENT_ID), + ).to.be.rejectedWith( + FirebaseError, + `Failed to delete Remote Config experiment with ID ${EXPERIMENT_ID} for project ${PROJECT_ID}. Error: Request to https://firebaseremoteconfig.googleapis.com/v1/projects/12345679/namespaces/firebase/experiments/1 had HTTP Error: 500, Internal server error`, + ); + }); +}); diff --git a/src/remoteconfig/deleteExperiment.ts b/src/remoteconfig/deleteExperiment.ts new file mode 100644 index 00000000000..075ed9e016d --- /dev/null +++ b/src/remoteconfig/deleteExperiment.ts @@ -0,0 +1,48 @@ +import * as clc from "colorette"; + +import { remoteConfigApiOrigin } from "../api"; +import { Client } from "../apiv2"; +import { FirebaseError, getErrMsg, getError } from "../error"; +import { consoleUrl } from "../utils"; + +const TIMEOUT = 30000; + +const apiClient = new Client({ + urlPrefix: remoteConfigApiOrigin(), + apiVersion: "v1", +}); + +/** + * Deletes a Remote Config experiment. + * @param projectId The ID of the project. + * @param namespace The namespace under which the experiment is created. + * @param experimentId The ID of the experiment to retrieve. + * @return A promise that resolves to the experiment object. + */ +export async function deleteExperiment( + projectId: string, + namespace: string, + experimentId: string, +): Promise { + try { + await apiClient.request({ + method: "DELETE", + path: `projects/${projectId}/namespaces/${namespace}/experiments/${experimentId}`, + timeout: TIMEOUT, + }); + return clc.bold(`Successfully deleted experiment ${clc.yellow(experimentId)}`); + } catch (err: unknown) { + const error: Error = getError(err); + if (error.message.includes("is running and cannot be deleted")) { + const rcConsoleUrl = consoleUrl(projectId, `/config/experiment/results/${experimentId}`); + throw new FirebaseError( + `Experiment ${experimentId} is currently running and cannot be deleted. If you want to delete this experiment, stop it at ${rcConsoleUrl}`, + { original: error }, + ); + } + throw new FirebaseError( + `Failed to delete Remote Config experiment with ID ${experimentId} for project ${projectId}. Error: ${getErrMsg(err)}`, + { original: error }, + ); + } +} diff --git a/src/remoteconfig/deleteRollout.spec.ts b/src/remoteconfig/deleteRollout.spec.ts new file mode 100644 index 00000000000..43ee7fff28e --- /dev/null +++ b/src/remoteconfig/deleteRollout.spec.ts @@ -0,0 +1,60 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import { remoteConfigApiOrigin } from "../api"; +import { FirebaseError } from "../error"; +import { NAMESPACE_FIREBASE } from "./interfaces"; +import * as clc from "colorette"; +import { deleteRollout } from "./deleteRollout"; + +const PROJECT_ID = "12345679"; +const ROLLOUT_ID = "rollout_1"; + +describe("Remote Config Rollout Delete", () => { + afterEach(() => { + expect(nock.isDone()).to.equal(true, "all nock stubs should have been called"); + nock.cleanAll(); + }); + + it("should delete an rollout successfully", async () => { + nock(remoteConfigApiOrigin()) + .delete(`/v1/projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/rollouts/${ROLLOUT_ID}`) + .reply(200); + + await expect(deleteRollout(PROJECT_ID, NAMESPACE_FIREBASE, ROLLOUT_ID)).to.eventually.equal( + clc.bold(`Successfully deleted rollout ${clc.yellow(ROLLOUT_ID)}`), + ); + }); + + it("should throw FirebaseError if rollout is running", async () => { + const errorMessage = `Rollout ${ROLLOUT_ID} is currently running and cannot be deleted. If you want to delete this rollout, stop it at https://console.firebase.google.com/project/${PROJECT_ID}/config/env/firebase/rollout/${ROLLOUT_ID}`; + nock(remoteConfigApiOrigin()) + .delete(`/v1/projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/rollouts/${ROLLOUT_ID}`) + .reply(400, { + error: { + message: errorMessage, + }, + }); + + await expect(deleteRollout(PROJECT_ID, NAMESPACE_FIREBASE, ROLLOUT_ID)).to.be.rejectedWith( + FirebaseError, + errorMessage, + ); + }); + + it("should throw FirebaseError if an internal error occurred", async () => { + nock(remoteConfigApiOrigin()) + .delete(`/v1/projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/rollouts/${ROLLOUT_ID}`) + .reply(500, { + error: { + message: "Internal server error", + }, + }); + + const expectedErrorMessage = `Failed to delete Remote Config rollout with ID ${ROLLOUT_ID} for project ${PROJECT_ID}. Error: Request to https://firebaseremoteconfig.googleapis.com/v1/projects/12345679/namespaces/firebase/rollouts/${ROLLOUT_ID} had HTTP Error: 500, Internal server error`; + + await expect(deleteRollout(PROJECT_ID, NAMESPACE_FIREBASE, ROLLOUT_ID)).to.be.rejectedWith( + FirebaseError, + expectedErrorMessage, + ); + }); +}); diff --git a/src/remoteconfig/deleteRollout.ts b/src/remoteconfig/deleteRollout.ts new file mode 100644 index 00000000000..8497b1d44e4 --- /dev/null +++ b/src/remoteconfig/deleteRollout.ts @@ -0,0 +1,49 @@ +import { remoteConfigApiOrigin } from "../api"; +import { Client } from "../apiv2"; +import { FirebaseError, getErrMsg, getError } from "../error"; +import { consoleUrl } from "../utils"; +import * as clc from "colorette"; + +const TIMEOUT = 30000; + +const apiClient = new Client({ + urlPrefix: remoteConfigApiOrigin(), + apiVersion: "v1", +}); + +/** + * Deletes a Remote Config rollout. + * @param projectId The project ID. + * @param namespace The namespace of the rollout. + * @param rolloutId The ID of the rollout to delete. + * @return A promise that resolves when the deletion is complete. + */ +export async function deleteRollout( + projectId: string, + namespace: string, + rolloutId: string, +): Promise { + try { + await apiClient.request({ + method: "DELETE", + path: `/projects/${projectId}/namespaces/${namespace}/rollouts/${rolloutId}`, + timeout: TIMEOUT, + }); + return clc.bold(`Successfully deleted rollout ${clc.yellow(rolloutId)}`); + } catch (err: unknown) { + const originalError = getError(err); + const errorMessage = getErrMsg(err); + + if (errorMessage.includes("is running and cannot be deleted")) { + const rcConsoleUrl = consoleUrl(projectId, `/config/env/firebase/rollout/${rolloutId}`); + throw new FirebaseError( + `Rollout '${rolloutId}' is currently running and cannot be deleted. If you want to delete this rollout, stop it at ${rcConsoleUrl}`, + { original: originalError }, + ); + } + throw new FirebaseError( + `Failed to delete Remote Config rollout with ID ${rolloutId} for project ${projectId}. Error: ${errorMessage}`, + { original: originalError }, + ); + } +} diff --git a/src/remoteconfig/get.spec.ts b/src/remoteconfig/get.spec.ts new file mode 100644 index 00000000000..b7056fb4b37 --- /dev/null +++ b/src/remoteconfig/get.spec.ts @@ -0,0 +1,144 @@ +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"; + +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", +}; + +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/getExperiment.spec.ts b/src/remoteconfig/getExperiment.spec.ts new file mode 100644 index 00000000000..06bbf9578a3 --- /dev/null +++ b/src/remoteconfig/getExperiment.spec.ts @@ -0,0 +1,131 @@ +import { expect } from "chai"; +import { remoteConfigApiOrigin } from "../api"; +import * as nock from "nock"; +import * as Table from "cli-table3"; +import * as util from "util"; + +import * as rcExperiment from "./getExperiment"; +import { GetExperimentResult, NAMESPACE_FIREBASE } from "./interfaces"; +import { FirebaseError } from "../error"; + +const PROJECT_ID = "1234567890"; +const EXPERIMENT_ID_1 = "1"; +const EXPERIMENT_ID_2 = "2"; + +const expectedExperimentResult: GetExperimentResult = { + name: "projects/1234567890/namespaces/firebase/experiments/1", + definition: { + displayName: "param_one", + service: "EXPERIMENT_SERVICE_REMOTE_CONFIG", + objectives: { + activationEvent: {}, + eventObjectives: [ + { + isPrimary: true, + systemObjectiveDetails: { + objective: "total_revenue", + }, + }, + { + systemObjectiveDetails: { + objective: "retention_7", + }, + }, + { + customObjectiveDetails: { + event: "app_exception", + countType: "NO_EVENT_USERS", + }, + }, + ], + }, + variants: [ + { + name: "Baseline", + weight: 1, + }, + { + name: "Variant A", + weight: 1, + }, + ], + }, + state: "PENDING", + startTime: "1970-01-01T00:00:00Z", + endTime: "1970-01-01T00:00:00Z", + lastUpdateTime: "2025-07-25T08:24:30.682Z", + etag: "e1", +}; + +describe("Remote Config Experiment Get", () => { + describe("getExperiment", () => { + afterEach(() => { + expect(nock.isDone()).to.equal(true, "all nock stubs should have been called"); + nock.cleanAll(); + }); + + it("should successfully retrieve a Remote Config experiment by ID", async () => { + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/namespaces/firebase/experiments/${EXPERIMENT_ID_1}`) + .reply(200, expectedExperimentResult); + + const experimentOne = await rcExperiment.getExperiment( + PROJECT_ID, + NAMESPACE_FIREBASE, + EXPERIMENT_ID_1, + ); + expect(experimentOne).to.deep.equal(expectedExperimentResult); + }); + + it("should reject with a FirebaseError if the API call fails", async () => { + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/namespaces/firebase/experiments/${EXPERIMENT_ID_2}`) + .reply(404, {}); + + await expect( + rcExperiment.getExperiment(PROJECT_ID, NAMESPACE_FIREBASE, EXPERIMENT_ID_2), + ).to.eventually.be.rejectedWith( + FirebaseError, + `Failed to get Remote Config experiment with ID 2 for project 1234567890.`, + ); + }); + }); + + describe("parseExperiment", () => { + it("should correctly parse and format an experiment result into a tabular format", () => { + const resultTable = rcExperiment.parseExperiment(expectedExperimentResult); + const expectedTable = [ + ["Name", expectedExperimentResult.name], + ["Display Name", expectedExperimentResult.definition.displayName], + ["Service", expectedExperimentResult.definition.service], + [ + "Objectives", + util.inspect(expectedExperimentResult.definition.objectives, { + showHidden: false, + depth: null, + }), + ], + [ + "Variants", + util.inspect(expectedExperimentResult.definition.variants, { + showHidden: false, + depth: null, + }), + ], + ["State", expectedExperimentResult.state], + ["Start Time", expectedExperimentResult.startTime], + ["End Time", expectedExperimentResult.endTime], + ["Last Update Time", expectedExperimentResult.lastUpdateTime], + ["etag", expectedExperimentResult.etag], + ]; + + const expectedTableString = new Table({ + head: ["Entry Name", "Value"], + style: { head: ["green"] }, + }); + + expectedTableString.push(...expectedTable); + expect(resultTable).to.equal(expectedTableString.toString()); + }); + }); +}); diff --git a/src/remoteconfig/getExperiment.ts b/src/remoteconfig/getExperiment.ts new file mode 100644 index 00000000000..62b56e6656f --- /dev/null +++ b/src/remoteconfig/getExperiment.ts @@ -0,0 +1,71 @@ +import * as Table from "cli-table3"; +import * as util from "util"; + +import { remoteConfigApiOrigin } from "../api"; +import { Client } from "../apiv2"; +import { logger } from "../logger"; +import { FirebaseError, getError } from "../error"; +import { GetExperimentResult } from "./interfaces"; + +const TIMEOUT = 30000; +const TABLE_HEAD = ["Entry Name", "Value"]; + +const apiClient = new Client({ + urlPrefix: remoteConfigApiOrigin(), + apiVersion: "v1", +}); + +/** + * Parses a Remote Config experiment object and formats it into a table. + * @param experiment The Remote Config experiment. + * @return A tabular representation of the experiment. + */ +export const parseExperiment = (experiment: GetExperimentResult): string => { + const table = new Table({ head: TABLE_HEAD, style: { head: ["green"] } }); + table.push(["Name", experiment.name]); + table.push(["Display Name", experiment.definition.displayName]); + table.push(["Service", experiment.definition.service]); + table.push([ + "Objectives", + util.inspect(experiment.definition.objectives, { showHidden: false, depth: null }), + ]); + table.push([ + "Variants", + util.inspect(experiment.definition.variants, { showHidden: false, depth: null }), + ]); + table.push(["State", experiment.state]); + table.push(["Start Time", experiment.startTime]); + table.push(["End Time", experiment.endTime]); + table.push(["Last Update Time", experiment.lastUpdateTime]); + table.push(["etag", experiment.etag]); + return table.toString(); +}; + +/** + * Returns a Remote Config experiment. + * @param projectId The ID of the project. + * @param namespace The namespace under which the experiment is created. + * @param experimentId The ID of the experiment to retrieve. + * @return A promise that resolves to the experiment object. + */ +export async function getExperiment( + projectId: string, + namespace: string, + experimentId: string, +): Promise { + try { + const res = await apiClient.request({ + method: "GET", + path: `projects/${projectId}/namespaces/${namespace}/experiments/${experimentId}`, + timeout: TIMEOUT, + }); + return res.body; + } catch (err: unknown) { + const error: Error = getError(err); + logger.debug(error.message); + throw new FirebaseError( + `Failed to get Remote Config experiment with ID ${experimentId} for project ${projectId}. Error: ${error.message}`, + { original: error }, + ); + } +} diff --git a/src/remoteconfig/getRollout.spec.ts b/src/remoteconfig/getRollout.spec.ts new file mode 100644 index 00000000000..62fb91b7f76 --- /dev/null +++ b/src/remoteconfig/getRollout.spec.ts @@ -0,0 +1,104 @@ +import { expect } from "chai"; +import { remoteConfigApiOrigin } from "../api"; +import * as nock from "nock"; +import * as Table from "cli-table3"; +import * as util from "util"; + +import * as rcRollout from "./getRollout"; +import { RemoteConfigRollout, NAMESPACE_FIREBASE } from "./interfaces"; +import { FirebaseError } from "../error"; + +const PROJECT_ID = "1234567890"; +const ROLLOUT_ID_1 = "rollout_1"; +const ROLLOUT_ID_2 = "rollout_2"; + +const expectedRollout: RemoteConfigRollout = { + name: `projects/${PROJECT_ID}/namespaces/firebase/rollouts/${ROLLOUT_ID_1}`, + definition: { + displayName: "Rollout demo", + description: "rollouts are fun!", + service: "ROLLOUT_SERVICE_REMOTE_CONFIG", + controlVariant: { + name: "Control", + weight: 1, + }, + enabledVariant: { + name: "Enabled", + weight: 1, + }, + }, + state: "DONE", + startTime: "2025-01-01T00:00:00Z", + endTime: "2025-01-31T23:59:59Z", + createTime: "2025-01-01T00:00:00Z", + lastUpdateTime: "2025-01-01T00:00:00Z", + etag: "e1", +}; + +describe("Remote Config Rollout Get", () => { + describe("getRollout", () => { + afterEach(() => { + expect(nock.isDone()).to.equal(true, "all nock stubs should have been called"); + nock.cleanAll(); + }); + + it("should successfully retrieve a Remote Config rollout by ID", async () => { + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/namespaces/firebase/rollouts/${ROLLOUT_ID_1}`) + .reply(200, expectedRollout); + + const rolloutOne = await rcRollout.getRollout(PROJECT_ID, NAMESPACE_FIREBASE, ROLLOUT_ID_1); + + expect(rolloutOne).to.deep.equal(expectedRollout); + }); + + it("should reject with a FirebaseError if the API call fails", async () => { + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/namespaces/firebase/rollouts/${ROLLOUT_ID_2}`) + .reply(404, {}); + const expectedError = `Failed to get Remote Config Rollout with ID ${ROLLOUT_ID_2} for project ${PROJECT_ID}.`; + + await expect( + rcRollout.getRollout(PROJECT_ID, NAMESPACE_FIREBASE, ROLLOUT_ID_2), + ).to.eventually.be.rejectedWith(FirebaseError, expectedError); + }); + }); + describe("parseRollout", () => { + it("should correctly parse and format an rollout result into a tabular format", () => { + const resultTable = rcRollout.parseRolloutIntoTable(expectedRollout); + const expectedTable = [ + ["Name", expectedRollout.name], + ["Display Name", expectedRollout.definition.displayName], + ["Description", expectedRollout.definition.description], + ["State", expectedRollout.state], + ["Create Time", expectedRollout.createTime], + ["Start Time", expectedRollout.startTime], + ["End Time", expectedRollout.endTime], + ["Last Update Time", expectedRollout.lastUpdateTime], + [ + "Control Variant", + util.inspect(expectedRollout.definition.controlVariant, { + showHidden: false, + depth: null, + }), + ], + [ + "Enabled Variant", + util.inspect(expectedRollout.definition.enabledVariant, { + showHidden: false, + depth: null, + }), + ], + ["ETag", expectedRollout.etag], + ]; + + const expectedTableString = new Table({ + head: ["Entry Name", "Value"], + style: { head: ["green"] }, + }); + + expectedTableString.push(...expectedTable); + expect(resultTable).to.equal(expectedTableString.toString()); + }); + }); +}); diff --git a/src/remoteconfig/getRollout.ts b/src/remoteconfig/getRollout.ts new file mode 100644 index 00000000000..764442fc45a --- /dev/null +++ b/src/remoteconfig/getRollout.ts @@ -0,0 +1,73 @@ +import { remoteConfigApiOrigin } from "../api"; +import { Client } from "../apiv2"; +import { logger } from "../logger"; +import { FirebaseError, getError } from "../error"; +import { RemoteConfigRollout } from "./interfaces"; +import * as Table from "cli-table3"; +import * as util from "util"; + +const TIMEOUT = 30000; +const TABLE_HEAD = ["Entry Name", "Value"]; + +const apiClient = new Client({ + urlPrefix: remoteConfigApiOrigin(), + apiVersion: "v1", +}); + +/** + * Parses a single rollout object into a CLI table string. + * @param rollout The rollout object. + * @return A string formatted as a table. + */ +export const parseRolloutIntoTable = (rollout: RemoteConfigRollout): string => { + const table = new Table({ head: TABLE_HEAD, style: { head: ["green"] } }); + table.push( + ["Name", rollout.name], + ["Display Name", rollout.definition.displayName], + ["Description", rollout.definition.description], + ["State", rollout.state], + ["Create Time", rollout.createTime], + ["Start Time", rollout.startTime], + ["End Time", rollout.endTime], + ["Last Update Time", rollout.lastUpdateTime], + [ + "Control Variant", + util.inspect(rollout.definition.controlVariant, { showHidden: false, depth: null }), + ], + [ + "Enabled Variant", + util.inspect(rollout.definition.enabledVariant, { showHidden: false, depth: null }), + ], + ["ETag", rollout.etag], + ); + return table.toString(); +}; + +/** + * Retrieves a specific rollout by its ID. + * @param projectId The project ID. + * @param namespace The namespace of the rollout. + * @param rolloutId The ID of the rollout to retrieve. + * @return A promise that resolves to the requested Remote Config rollout. + */ +export async function getRollout( + projectId: string, + namespace: string, + rolloutId: string, +): Promise { + try { + const res = await apiClient.request({ + method: "GET", + path: `/projects/${projectId}/namespaces/${namespace}/rollouts/${rolloutId}`, + timeout: TIMEOUT, + }); + return res.body; + } catch (err: unknown) { + const error: Error = getError(err); + logger.debug(error.message); + throw new FirebaseError( + `Failed to get Remote Config Rollout with ID ${rolloutId} for project ${projectId}. Error: ${error.message}`, + { original: error }, + ); + } +} diff --git a/src/remoteconfig/interfaces.ts b/src/remoteconfig/interfaces.ts index c128345fb11..63c74345742 100644 --- a/src/remoteconfig/interfaces.ts +++ b/src/remoteconfig/interfaces.ts @@ -1,3 +1,6 @@ +export const NAMESPACE_FIREBASE = "firebase"; +export const DEFAULT_PAGE_SIZE = "10"; + export enum TagColor { BLUE = "Blue", BROWN = "Brown", @@ -94,3 +97,114 @@ export interface RemoteConfigUser { name?: string; imageUrl?: string; } + +/** Interface representing a Remote Config experiment. */ +export interface RemoteConfigExperiment { + name: string; + definition: ExperimentDefinition; + state: string; + startTime: string; + endTime: string; + lastUpdateTime: string; + etag: string; +} + +/** Interface representing the definition of a Remote Config experiment. */ +interface ExperimentDefinition { + displayName: string; + service: string; + description?: string; +} + +/** + * Interface representing the result of fetching a Remote Config experiment. + */ +export interface GetExperimentResult extends RemoteConfigExperiment { + definition: GetExperimentDefinition; +} + +/** + * Interface representing a detailed definition of a Remote Config experiment. + */ +interface GetExperimentDefinition extends ExperimentDefinition { + objectives: ExperimentObjectives; + variants: ExperimentVariant[]; +} + +/** Interface representing all objectives of a Remote Config experiment. */ +interface ExperimentObjectives { + activationEvent: { event?: string }; + eventObjectives: ExperimentEventObjectives[]; +} + +/** Type representing the event objectives of a Remote Config experiment. */ +type ExperimentEventObjectives = { + isPrimary?: boolean; +} & ( + | { systemObjectiveDetails: ExperimentSystemObjectiveDetails; customObjectiveDetails?: never } + | { customObjectiveDetails: ExperimentCustomObjectiveDetails; systemObjectiveDetails?: never } +); + +/** Interface representing system objectives of a Remote Config experiment. */ +interface ExperimentSystemObjectiveDetails { + objective: string; +} + +/** Interface representing custom objectives of a Remote Config experiment. */ +interface ExperimentCustomObjectiveDetails { + event: string; + countType: string; +} + +/** Interface representing an experiment variant. */ +interface ExperimentVariant { + name: string; + weight: number; +} + +/** Interface representing a list of Remote Config experiments. */ +export interface ListExperimentsResult { + experiments?: RemoteConfigExperiment[]; + nextPageToken?: string; +} + +/** Interface representing a Remote Config list experiment options. */ +export interface ListExperimentOptions { + pageSize: string; + pageToken?: string; + filter?: string; +} + +/** Interface representing the definition of a Remote Config rollout. */ +export interface RolloutDefinition { + displayName: string; + description: string; + service: string; + controlVariant: ExperimentVariant; + enabledVariant: ExperimentVariant; +} + +/** Interface representing a Remote Config rollout. */ +export interface RemoteConfigRollout { + name: string; + definition: RolloutDefinition; + state: string; + createTime: string; + startTime: string; + endTime: string; + lastUpdateTime: string; + etag: string; +} + +/** Interface representing a list of Remote Config rollouts with pagination. */ +export interface ListRollouts { + rollouts?: RemoteConfigRollout[]; + nextPageToken?: string; +} + +/** Interface representing a Remote Config list rollout options. */ +export interface ListRolloutOptions { + pageSize: string; + pageToken?: string; + filter?: string; +} diff --git a/src/remoteconfig/listExperiments.spec.ts b/src/remoteconfig/listExperiments.spec.ts new file mode 100644 index 00000000000..2c87e0185c4 --- /dev/null +++ b/src/remoteconfig/listExperiments.spec.ts @@ -0,0 +1,257 @@ +import { expect } from "chai"; +import * as nock from "nock"; +import * as Table from "cli-table3"; + +import { remoteConfigApiOrigin } from "../api"; +import { FirebaseError } from "../error"; +import { + DEFAULT_PAGE_SIZE, + ListExperimentOptions, + ListExperimentsResult, + NAMESPACE_FIREBASE, + RemoteConfigExperiment, +} from "./interfaces"; +import { listExperiments, parseExperimentList } from "./listExperiments"; + +const PROJECT_ID = "1234567890"; + +const experiment1: RemoteConfigExperiment = { + name: `projects/${PROJECT_ID}/namespaces/firebase/experiments/78`, + definition: { + displayName: "Experiment One", + service: "EXPERIMENT_SERVICE_REMOTE_CONFIG", + description: "Description for Experiment One", + }, + state: "RUNNING", + startTime: "2025-01-01T00:00:00Z", + endTime: "2025-01-31T23:59:59Z", + lastUpdateTime: "2025-01-01T00:00:00Z", + etag: "e1", +}; + +const experiment2: RemoteConfigExperiment = { + name: `projects/${PROJECT_ID}/namespaces/firebase/experiments/22`, + definition: { + displayName: "Experiment Two", + service: "EXPERIMENT_SERVICE_REMOTE_CONFIG", + description: "Description for Experiment Two", + }, + state: "DRAFT", + startTime: "2025-02-01T00:00:00Z", + endTime: "2025-02-28T23:59:59Z", + lastUpdateTime: "2025-02-01T00:00:00Z", + etag: "e2", +}; + +const experiment3: RemoteConfigExperiment = { + name: `projects/1234${PROJECT_ID}567890/namespaces/firebase/experiments/43`, + definition: { + displayName: "Experiment Three", + service: "EXPERIMENT_SERVICE_REMOTE_CONFIG", + description: "Description for Experiment Three", + }, + state: "STOPPED", + startTime: "2025-03-01T00:00:00Z", + endTime: "2025-03-31T23:59:59Z", + lastUpdateTime: "2025-03-01T00:00:00Z", + etag: "e3", +}; + +const experiment4: RemoteConfigExperiment = { + name: `projects/${PROJECT_ID}/namespaces/firebase/experiments/109`, + definition: { + displayName: "Experiment Four", + service: "EXPERIMENT_SERVICE_REMOTE_CONFIG", + description: "Description for Experiment Four", + }, + state: "STOPPED", + startTime: "2025-03-01T00:00:00Z", + endTime: "2025-03-31T23:59:59Z", + lastUpdateTime: "2025-03-01T00:00:00Z", + etag: "e4", +}; + +describe("Remote Config Experiment List", () => { + afterEach(() => { + expect(nock.isDone()).to.equal(true, "all nock stubs should have been called"); + nock.cleanAll(); + }); + + describe("listExperiments", () => { + it("should list all experiments with default page size", async () => { + const listExperimentOptions: ListExperimentOptions = { + pageSize: DEFAULT_PAGE_SIZE, + }; + const expectedResultWithAllExperiments: ListExperimentsResult = { + experiments: [experiment2, experiment3, experiment1, experiment4], + }; + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/experiments`) + .query({ page_size: DEFAULT_PAGE_SIZE }) + .reply(200, expectedResultWithAllExperiments); + + const result = await listExperiments(PROJECT_ID, NAMESPACE_FIREBASE, listExperimentOptions); + + expect(result.experiments).to.deep.equal(expectedResultWithAllExperiments.experiments); + expect(result.nextPageToken).to.equal(expectedResultWithAllExperiments.nextPageToken); + }); + + it("should return paginated experiments when page size and page token are specified", async () => { + const pageSize = "2"; + const pageToken = "NDM="; + const listExperimentOptions: ListExperimentOptions = { + pageSize, + pageToken, + }; + const expectedResultWithPageTokenAndPageSize: ListExperimentsResult = { + experiments: [experiment3, experiment1], + nextPageToken: "MTA5", + }; + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/experiments`) + .query({ page_size: pageSize, page_token: pageToken }) + .reply(200, expectedResultWithPageTokenAndPageSize); + + const result = await listExperiments(PROJECT_ID, NAMESPACE_FIREBASE, listExperimentOptions); + + expect(result.experiments).to.deep.equal(expectedResultWithPageTokenAndPageSize.experiments); + expect(result.nextPageToken).to.equal(expectedResultWithPageTokenAndPageSize.nextPageToken); + }); + + it("should filter and return an experiment from the list", async () => { + const listExperimentOptions: ListExperimentOptions = { + pageSize: DEFAULT_PAGE_SIZE, + filter: `projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/experiments/43`, + }; + const expectedResultWithFilter: ListExperimentsResult = { + experiments: [experiment1], + }; + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/experiments`) + .query({ + filter: `projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/experiments/43`, + page_size: DEFAULT_PAGE_SIZE, + }) + .reply(200, expectedResultWithFilter); + + const result = await listExperiments(PROJECT_ID, NAMESPACE_FIREBASE, listExperimentOptions); + + expect(result.experiments).to.deep.equal(expectedResultWithFilter.experiments); + expect(result.nextPageToken).to.equal(expectedResultWithFilter.nextPageToken); + }); + + it("should return an empty object if filter is invalid", async () => { + const listExperimentOptions: ListExperimentOptions = { + pageSize: DEFAULT_PAGE_SIZE, + filter: `projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/experiments/43`, + }; + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/experiments`) + .query({ filter: `invalid-filter`, page_size: DEFAULT_PAGE_SIZE }) + .reply(200, {}); + + const result = await listExperiments(PROJECT_ID, NAMESPACE_FIREBASE, { + ...listExperimentOptions, + filter: "invalid-filter", + }); + + expect(result.experiments).to.deep.equal(undefined); + }); + + it("should reject with a FirebaseError if the API call fails", async () => { + const listExperimentOptions: ListExperimentOptions = { + pageSize: DEFAULT_PAGE_SIZE, + }; + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/experiments`) + .query({ page_size: DEFAULT_PAGE_SIZE }) + .reply(400, {}); + + await expect( + listExperiments(PROJECT_ID, NAMESPACE_FIREBASE, listExperimentOptions), + ).to.eventually.be.rejectedWith( + FirebaseError, + `Failed to get Remote Config experiments for project ${PROJECT_ID}.`, + ); + }); + }); + + describe("parseExperimentList", () => { + it("should correctly parse and format a list of experiments into a tabular format.", () => { + const allExperiments: RemoteConfigExperiment[] = [ + experiment2, + experiment3, + experiment1, + experiment4, + ]; + const resultTable = parseExperimentList(allExperiments); + const expectedTable = new Table({ + head: [ + "Experiment ID", + "Display Name", + "Service", + "Description", + "State", + "Start Time", + "End Time", + "Last Update Time", + "etag", + ], + style: { head: ["green"] }, + }); + expectedTable.push( + [ + experiment2.name.split("/").pop(), + experiment2.definition.displayName, + experiment2.definition.service, + experiment2.definition.description, + experiment2.state, + experiment2.startTime, + experiment2.endTime, + experiment2.lastUpdateTime, + experiment2.etag, + ], + [ + experiment3.name.split("/").pop(), + experiment3.definition.displayName, + experiment3.definition.service, + experiment3.definition.description, + experiment3.state, + experiment3.startTime, + experiment3.endTime, + experiment3.lastUpdateTime, + experiment3.etag, + ], + [ + experiment1.name.split("/").pop(), + experiment1.definition.displayName, + experiment1.definition.service, + experiment1.definition.description, + experiment1.state, + experiment1.startTime, + experiment1.endTime, + experiment1.lastUpdateTime, + experiment1.etag, + ], + [ + experiment4.name.split("/").pop(), + experiment4.definition.displayName, + experiment4.definition.service, + experiment4.definition.description, + experiment4.state, + experiment4.startTime, + experiment4.endTime, + experiment4.lastUpdateTime, + experiment4.etag, + ], + ); + + expect(resultTable).to.equal(expectedTable.toString()); + }); + + it("should return a message if no experiments are found.", () => { + const result = parseExperimentList([]); + expect(result).to.equal("\x1b[33mNo experiments found\x1b[0m"); + }); + }); +}); diff --git a/src/remoteconfig/listExperiments.ts b/src/remoteconfig/listExperiments.ts new file mode 100644 index 00000000000..ea95f3d826b --- /dev/null +++ b/src/remoteconfig/listExperiments.ts @@ -0,0 +1,91 @@ +import * as Table from "cli-table3"; + +import { remoteConfigApiOrigin } from "../api"; +import { Client } from "../apiv2"; +import { logger } from "../logger"; +import { FirebaseError, getError } from "../error"; +import { ListExperimentOptions, ListExperimentsResult, RemoteConfigExperiment } from "./interfaces"; + +const TIMEOUT = 30000; +const TABLE_HEAD = [ + "Experiment ID", + "Display Name", + "Service", + "Description", + "State", + "Start Time", + "End Time", + "Last Update Time", + "etag", +]; + +const apiClient = new Client({ + urlPrefix: remoteConfigApiOrigin(), + apiVersion: "v1", +}); + +/** + * Parses a list of Remote Config experiments and formats it into a table. + * @param experiments A list of Remote Config experiments. + * @return A tabular representation of the experiments. + */ +export const parseExperimentList = (experiments: RemoteConfigExperiment[]): string => { + if (experiments.length === 0) return "\x1b[33mNo experiments found\x1b[0m"; + + const table = new Table({ head: TABLE_HEAD, style: { head: ["green"] } }); + for (const experiment of experiments) { + table.push([ + experiment.name.split("/").pop(), // Extract the experiment number + experiment.definition.displayName, + experiment.definition.service, + experiment.definition.description, + experiment.state, + experiment.startTime, + experiment.endTime, + experiment.lastUpdateTime, + experiment.etag, + ]); + } + return table.toString(); +}; + +/** + * Returns a list of Remote Config experiments. + * @param projectId The ID of the project. + * @param namespace The namespace under which the experiment is created. + * @param listExperimentOptions Options for listing experiments (e.g., page size, filter, page token). + * @return A promise that resolves to a list of experiment. + */ +export async function listExperiments( + projectId: string, + namespace: string, + listExperimentOptions: ListExperimentOptions, +): Promise { + try { + const params = new URLSearchParams(); + if (listExperimentOptions.pageSize) { + params.set("page_size", listExperimentOptions.pageSize); + } + if (listExperimentOptions.filter) { + params.set("filter", listExperimentOptions.filter); + } + if (listExperimentOptions.pageToken) { + params.set("page_token", listExperimentOptions.pageToken); + } + logger.debug(`Query parameters for listExperiments: ${params.toString()}`); + const res = await apiClient.request({ + method: "GET", + path: `projects/${projectId}/namespaces/${namespace}/experiments`, + queryParams: params, + timeout: TIMEOUT, + }); + return res.body; + } catch (err: unknown) { + const error: Error = getError(err); + logger.debug(error.message); + throw new FirebaseError( + `Failed to get Remote Config experiments for project ${projectId}. Error: ${error.message}`, + { original: error }, + ); + } +} diff --git a/src/remoteconfig/listRollouts.spec.ts b/src/remoteconfig/listRollouts.spec.ts new file mode 100644 index 00000000000..80f052ae649 --- /dev/null +++ b/src/remoteconfig/listRollouts.spec.ts @@ -0,0 +1,260 @@ +import { expect } from "chai"; +import { remoteConfigApiOrigin } from "../api"; +import * as nock from "nock"; +import * as Table from "cli-table3"; + +import { listRollouts, parseRolloutList } from "./listRollouts"; +import { + DEFAULT_PAGE_SIZE, + ListRolloutOptions, + ListRollouts, + NAMESPACE_FIREBASE, + RemoteConfigRollout, +} from "./interfaces"; +import { FirebaseError } from "../error"; + +const PROJECT_ID = "1234567890"; +const rollout1: RemoteConfigRollout = { + name: `projects/${PROJECT_ID}/namespaces/firebase/rollouts/rollout_78`, + definition: { + displayName: "Rollout One", + description: "Description for Rollout One", + service: "ROLLOUT_SERVICE_REMOTE_CONFIG", + controlVariant: { name: "Control", weight: 1 }, + enabledVariant: { name: "Enabled", weight: 1 }, + }, + state: "RUNNING", + startTime: "2025-01-01T00:00:00Z", + endTime: "2025-01-31T23:59:59Z", + createTime: "2025-01-01T00:00:00Z", + lastUpdateTime: "2025-01-01T00:00:00Z", + etag: "e1", +}; + +const rollout2: RemoteConfigRollout = { + name: `projects/${PROJECT_ID}/namespaces/firebase/rollouts/rollout_22`, + definition: { + displayName: "Rollout Two", + description: "Description for Rollout Two", + service: "ROLLOUT_SERVICE_REMOTE_CONFIG", + controlVariant: { name: "Control", weight: 1 }, + enabledVariant: { name: "Enabled", weight: 1 }, + }, + state: "DRAFT", + startTime: "2025-02-01T00:00:00Z", + endTime: "2025-02-28T23:59:59Z", + createTime: "2025-02-01T00:00:00Z", + lastUpdateTime: "2025-02-01T00:00:00Z", + etag: "e2", +}; + +const rollout3: RemoteConfigRollout = { + name: `projects/${PROJECT_ID}/namespaces/firebase/rollouts/rollout_43`, + definition: { + displayName: "Rollout Three", + description: "Description for Rollout Three", + service: "ROLLOUT_SERVICE_REMOTE_CONFIG", + controlVariant: { name: "Control", weight: 1 }, + enabledVariant: { name: "Enabled", weight: 1 }, + }, + state: "STOPPED", + startTime: "2025-03-01T00:00:00Z", + endTime: "2025-03-31T23:59:59Z", + createTime: "2025-03-01T00:00:00Z", + lastUpdateTime: "2025-03-01T00:00:00Z", + etag: "e3", +}; + +const rollout4: RemoteConfigRollout = { + name: `projects/${PROJECT_ID}/namespaces/firebase/rollouts/rollout_109`, + definition: { + displayName: "Rollout Four", + description: "Description for Rollout Four", + service: "ROLLOUT_SERVICE_REMOTE_CONFIG", + controlVariant: { name: "Control", weight: 1 }, + enabledVariant: { name: "Enabled", weight: 1 }, + }, + state: "STOPPED", + startTime: "2025-03-01T00:00:00Z", + endTime: "2025-03-31T23:59:59Z", + createTime: "2025-03-01T00:00:00Z", + lastUpdateTime: "2025-03-01T00:00:00Z", + etag: "e3", +}; + +describe("Remote Config Rollout List", () => { + afterEach(() => { + expect(nock.isDone()).to.equal(true, "all nock stubs should have been called"); + nock.cleanAll(); + }); + + describe("listRollouts", () => { + it("should list all rollouts with default page size", async () => { + const listRolloutOptions: ListRolloutOptions = { + pageSize: DEFAULT_PAGE_SIZE, + }; + const expectedResultWithAllRollouts: ListRollouts = { + rollouts: [rollout1, rollout2, rollout3, rollout4], + }; + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/rollouts`) + .query({ page_size: DEFAULT_PAGE_SIZE }) + .reply(200, expectedResultWithAllRollouts); + + const result = await listRollouts(PROJECT_ID, NAMESPACE_FIREBASE, listRolloutOptions); + + expect(result.rollouts).to.deep.equal(expectedResultWithAllRollouts.rollouts); + expect(result.nextPageToken).to.equal(expectedResultWithAllRollouts.nextPageToken); + }); + + it("should return paginated rollouts when page size and page token are specified", async () => { + const pageSize = "2"; + const pageToken = "NDM="; + const listRolloutOptions: ListRolloutOptions = { + pageSize, + pageToken, + }; + const expectedResultWithPageTokenAndPageSize: ListRollouts = { + rollouts: [rollout3, rollout1], + nextPageToken: "MTA5", + }; + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/rollouts`) + .query({ page_size: pageSize, page_token: pageToken }) + .reply(200, expectedResultWithPageTokenAndPageSize); + + const result = await listRollouts(PROJECT_ID, NAMESPACE_FIREBASE, listRolloutOptions); + + expect(result.rollouts).to.deep.equal(expectedResultWithPageTokenAndPageSize.rollouts); + expect(result.nextPageToken).to.equal(expectedResultWithPageTokenAndPageSize.nextPageToken); + }); + + it("should filter and return a rollout from the list", async () => { + const listRolloutOptions: ListRolloutOptions = { + pageSize: DEFAULT_PAGE_SIZE, + filter: `projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/rollouts/rollout_43`, + }; + const expectedResultWithFilter: ListRollouts = { + rollouts: [rollout3], + }; + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/rollouts`) + .query({ + filter: `projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/rollouts/rollout_43`, + page_size: DEFAULT_PAGE_SIZE, + }) + .reply(200, expectedResultWithFilter); + + const result = await listRollouts(PROJECT_ID, NAMESPACE_FIREBASE, listRolloutOptions); + + expect(result.rollouts).to.deep.equal(expectedResultWithFilter.rollouts); + expect(result.nextPageToken).to.equal(expectedResultWithFilter.nextPageToken); + }); + + it("should return an empty object if filter is invalid", async () => { + const listRolloutOptions: ListRolloutOptions = { + pageSize: DEFAULT_PAGE_SIZE, + filter: `invalid-filter`, + }; + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/rollouts`) + .query({ filter: `invalid-filter`, page_size: DEFAULT_PAGE_SIZE }) + .reply(200, {}); + + const result = await listRollouts(PROJECT_ID, NAMESPACE_FIREBASE, listRolloutOptions); + + expect(result.rollouts).to.deep.equal(undefined); + }); + + it("should reject with a FirebaseError if the API call fails", async () => { + const listRolloutOptions: ListRolloutOptions = { + pageSize: DEFAULT_PAGE_SIZE, + }; + nock(remoteConfigApiOrigin()) + .get(`/v1/projects/${PROJECT_ID}/namespaces/${NAMESPACE_FIREBASE}/rollouts`) + .query({ page_size: DEFAULT_PAGE_SIZE }) + .reply(400, {}); + + await expect( + listRollouts(PROJECT_ID, NAMESPACE_FIREBASE, listRolloutOptions), + ).to.eventually.be.rejectedWith( + FirebaseError, + `Failed to get Remote Config rollouts for project ${PROJECT_ID}.`, + ); + }); + }); + + describe("parseRolloutList", () => { + it("should correctly parse and format a list of rollouts into a tabular format.", () => { + const allRollouts: RemoteConfigRollout[] = [rollout2, rollout3, rollout1, rollout4]; + const resultTable = parseRolloutList(allRollouts); + const expectedTable = new Table({ + head: [ + "Rollout ID", + "Display Name", + "Service", + "Description", + "State", + "Start Time", + "End Time", + "Last Update Time", + "ETag", + ], + style: { head: ["green"] }, + }); + expectedTable.push( + [ + rollout2.name.split("/").pop(), + rollout2.definition.displayName, + rollout2.definition.service, + rollout2.definition.description, + rollout2.state, + rollout2.startTime, + rollout2.endTime, + rollout2.lastUpdateTime, + rollout2.etag, + ], + [ + rollout3.name.split("/").pop(), + rollout3.definition.displayName, + rollout3.definition.service, + rollout3.definition.description, + rollout3.state, + rollout3.startTime, + rollout3.endTime, + rollout3.lastUpdateTime, + rollout3.etag, + ], + [ + rollout1.name.split("/").pop(), + rollout1.definition.displayName, + rollout1.definition.service, + rollout1.definition.description, + rollout1.state, + rollout1.startTime, + rollout1.endTime, + rollout1.lastUpdateTime, + rollout1.etag, + ], + [ + rollout4.name.split("/").pop(), + rollout4.definition.displayName, + rollout4.definition.service, + rollout4.definition.description, + rollout4.state, + rollout4.startTime, + rollout4.endTime, + rollout4.lastUpdateTime, + rollout4.etag, + ], + ); + + expect(resultTable).to.equal(expectedTable.toString()); + }); + + it("should return a message if no rollouts are found.", () => { + const result = parseRolloutList([]); + expect(result).to.equal("\x1b[33mNo rollouts found.\x1b[0m"); + }); + }); +}); diff --git a/src/remoteconfig/listRollouts.ts b/src/remoteconfig/listRollouts.ts new file mode 100644 index 00000000000..1cc871356e8 --- /dev/null +++ b/src/remoteconfig/listRollouts.ts @@ -0,0 +1,89 @@ +import { remoteConfigApiOrigin } from "../api"; +import { Client } from "../apiv2"; +import { logger } from "../logger"; +import { FirebaseError, getError } from "../error"; +import { ListRolloutOptions, ListRollouts, RemoteConfigRollout } from "./interfaces"; +import * as Table from "cli-table3"; + +const TIMEOUT = 30000; + +const apiClient = new Client({ + urlPrefix: remoteConfigApiOrigin(), + apiVersion: "v1", +}); + +const TABLE_HEAD = [ + "Rollout ID", + "Display Name", + "Service", + "Description", + "State", + "Start Time", + "End Time", + "Last Update Time", + "ETag", +]; + +export const parseRolloutList = (rollouts: RemoteConfigRollout[]): string => { + if (rollouts.length === 0) { + return "\x1b[33mNo rollouts found.\x1b[0m"; + } + + const table = new Table({ head: TABLE_HEAD, style: { head: ["green"] } }); + + for (const rollout of rollouts) { + table.push([ + rollout.name.split("/").pop() || rollout.name, + rollout.definition.displayName, + rollout.definition.service, + rollout.definition.description, + rollout.state, + rollout.startTime, + rollout.endTime, + rollout.lastUpdateTime, + rollout.etag, + ]); + } + return table.toString(); +}; + +/** + * Retrieves a list of rollouts for a given project and namespace. + * @param projectId The project ID. + * @param namespace The namespace of the rollout. + * (Options are passed in listRolloutOptions object) + * @return A promise that resolves to a list of Remote Config rollouts. + */ +export async function listRollouts( + projectId: string, + namespace: string, + listRolloutOptions: ListRolloutOptions, +): Promise { + try { + const params = new URLSearchParams(); + if (listRolloutOptions.pageSize) { + params.set("page_size", listRolloutOptions.pageSize); + } + if (listRolloutOptions.filter) { + params.set("filter", listRolloutOptions.filter); + } + if (listRolloutOptions.pageToken) { + params.set("page_token", listRolloutOptions.pageToken); + } + + const res = await apiClient.request({ + method: "GET", + path: `/projects/${projectId}/namespaces/${namespace}/rollouts`, + queryParams: params, + timeout: TIMEOUT, + }); + return res.body; + } catch (err: unknown) { + const error: Error = getError(err); + logger.debug(error.message); + throw new FirebaseError( + `Failed to get Remote Config rollouts for project ${projectId}. Error: ${error.message}`, + { original: error }, + ); + } +} diff --git a/src/remoteconfig/options.ts b/src/remoteconfig/options.ts new file mode 100644 index 00000000000..f85ecf6743d --- /dev/null +++ b/src/remoteconfig/options.ts @@ -0,0 +1,18 @@ +import { Options } from "../options"; +import { assertImplements } from "../metaprogramming"; + +/** + * The set of fields that the Remote Config codebase needs from Options. + * This helps keep the codebase strongly typed and limits what needs to be mocked for tests. + */ +export interface RemoteConfigOptions extends Options { + // We can't know the type of options.* since it comes from Commander, + // so we need to specify the types of the options we are using. + pageSize?: string; + pageToken?: string; + filter?: string; +} + +// This line will cause a compile-time error if RemoteConfigOptions has a field +// that is missing in the base Options interface or has an incompatible type. +assertImplements(); diff --git a/src/remoteconfig/publish.ts b/src/remoteconfig/publish.ts new file mode 100644 index 00000000000..72ba6c441c8 --- /dev/null +++ b/src/remoteconfig/publish.ts @@ -0,0 +1,58 @@ +import { remoteConfigApiOrigin } from "../api"; +import { Client } from "../apiv2"; +import { logger } from "../logger"; +import { FirebaseError } from "../error"; +import { RemoteConfigTemplate } from "./interfaces"; + +const TIMEOUT = 30000; + +const apiClient = new Client({ + urlPrefix: remoteConfigApiOrigin(), + apiVersion: "v1", +}); + +/** + * Function to publish a new remote config template for a project + * This is added to support publish operation for RC MCP tooling + * @param projectId Input is the project ID string + * @param template The new template to be published + * @param options Takes in parameter `force` to force update the template + * @return {Promise} Returns a promise of a RemoteConfigTemplate + */ +export async function publishTemplate( + projectId: string, + template: RemoteConfigTemplate, + options?: { force: boolean }, +): Promise { + try { + let ifMatch = template.etag; + if (options && options.force === true) { + // setting `If-Match = "*"` forces the Remote Config template to be updated + // and circumvent the ETag + ifMatch = "*"; + } + + const requestBody = { + conditions: template.conditions, + parameters: template.parameters, + parameterGroups: template.parameterGroups, + version: template.version, + }; + + const res = await apiClient.request({ + method: "PUT", + path: `/projects/${projectId}/remoteConfig`, + timeout: TIMEOUT, + headers: { "If-Match": ifMatch }, + body: JSON.stringify(requestBody), + }); + + return res.body; + } catch (err: any) { + logger.debug(err.message); + throw new FirebaseError( + `Failed to publish Firebase Remote Config template for project ${projectId}. `, + { 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..2a4e064b5db 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,14 +7,18 @@ 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, TokensWithExpiration, User } from "./types/auth"; +import { setRefreshToken, setActiveAccount, setGlobalDefaultAccount, isExpired } from "./auth"; +import type { Options } from "./options"; +import { isFirebaseMcp, isFirebaseStudio } from "./env"; +import { timeoutError } from "./timeout"; const AUTH_ERROR_MESSAGE = `Command requires authentication, please run ${clc.bold( - "firebase login" + "firebase login", )}`; let authClient: GoogleAuth | undefined; - +let lastOptions: Options; /** * Returns the auth client. * @param config options for the auth client. @@ -30,41 +34,101 @@ 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 { const client = getAuthClient({ scopes: authScopes, projectId: options.project }); const token = await client.getAccessToken(); - api.setAccessToken(token); token !== null ? apiv2.setAccessToken(token) : false; + logger.debug(`Running auto auth`); + + let clientEmail; + try { + const timeoutMillis = isFirebaseMcp() ? 5000 : 15000; + const credentials = await timeoutError( + client.getCredentials(), + new FirebaseError( + `Authenticating with default credentials timed out after ${timeoutMillis / 1000} seconds. Please try running \`firebase login\` instead.`, + ), + timeoutMillis, + ); + 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.`); + } + if (isFirebaseStudio() && token && clientEmail) { + // Within monospace, this a OAuth token for the user, so we make it the active user. + const activeAccount = { + user: { email: clientEmail }, + tokens: { + access_token: token, + expires_at: client.cachedCredential?.credentials.expiry_date, + } as TokensWithExpiration, + }; + setActiveAccount(options, activeAccount); + setGlobalDefaultAccount(activeAccount); + + // project is also selected in monospace auth flow + options.projectId = await client.getProjectId(); + } + return clientEmail || null; +} + +export async function refreshAuth(): Promise { + if (!lastOptions) { + throw new FirebaseError("Unable to refresh auth: not yet authenticated."); + } + await requireAuth(lastOptions); + return lastOptions.tokens as Tokens; } /** - * Ensures that there is an authenticated user. + * Ensures that the user can make authenticated calls. Returns the email if the user is logged in, + * returns null if the user has Applciation Default Credentials set up, and errors out + * if the user is not authenticated * @param options CLI options. */ -export async function requireAuth(options: any): Promise { - api.setScopes([scopes.CLOUD_PLATFORM, scopes.FIREBASE_PLATFORM]); +export async function requireAuth( + options: any, + skipAutoAuth: boolean = false, +): Promise { + lastOptions = options; + const requiredScopes = [scopes.CLOUD_PLATFORM]; + if (isFirebaseStudio()) { + requiredScopes.push(scopes.USERINFO_EMAIL); + } + api.setScopes(requiredScopes); options.authScopes = api.getScopes(); const tokens = options.tokens as Tokens | undefined; const user = options.user as User | undefined; - 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"); - } else if (user) { + 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 && (!isExpired(tokens) || tokens?.refresh_token)) { logger.debug(`> authorizing via signed-in user (${user.email})`); + } else if (skipAutoAuth) { + return null; } 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 }, ); } } @@ -73,12 +137,15 @@ export async function requireAuth(options: any): Promise { if (tokenOpt) { setRefreshToken(tokenOpt); - return; + return null; } if (!user || !tokens) { throw new FirebaseError(AUTH_ERROR_MESSAGE); } + // TODO: 90 percent sure this is redundant, as the only time we hit this is if options.user/options.token is set, and + // setActiveAccount is the only code that sets those. 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.spec.ts b/src/requireDatabaseInstance.spec.ts new file mode 100644 index 00000000000..8b4e6f5fc1f --- /dev/null +++ b/src/requireDatabaseInstance.spec.ts @@ -0,0 +1,60 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { + requireDatabaseInstance, + MISSING_DEFAULT_INSTANCE_ERROR_MESSAGE, +} from "./requireDatabaseInstance"; +import * as db from "./getDefaultDatabaseInstance"; +import { FirebaseError } from "./error"; + +describe("requireDatabaseInstance", () => { + let getDefaultDatabaseInstanceStub: sinon.SinonStub; + + beforeEach(() => { + getDefaultDatabaseInstanceStub = sinon.stub(db, "getDefaultDatabaseInstance"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should do nothing if options.instance is already set", async () => { + const options = { instance: "my-instance" }; + await requireDatabaseInstance(options); + expect(options.instance).to.equal("my-instance"); + expect(getDefaultDatabaseInstanceStub).to.not.have.been.called; + }); + + it("should call getDefaultDatabaseInstance if options.instance is not set", async () => { + const options = {}; + getDefaultDatabaseInstanceStub.resolves("default-instance"); + await requireDatabaseInstance(options); + expect(getDefaultDatabaseInstanceStub).to.have.been.calledOnce; + }); + + it("should set options.instance to the value returned by getDefaultDatabaseInstance", async () => { + const options: { instance?: string } = {}; + getDefaultDatabaseInstanceStub.resolves("default-instance"); + await requireDatabaseInstance(options); + expect(options.instance).to.equal("default-instance"); + }); + + it("should throw a FirebaseError if getDefaultDatabaseInstance returns an empty string", async () => { + const options = {}; + getDefaultDatabaseInstanceStub.resolves(""); + await expect(requireDatabaseInstance(options)).to.be.rejectedWith( + FirebaseError, + MISSING_DEFAULT_INSTANCE_ERROR_MESSAGE, + ); + }); + + it("should throw a FirebaseError if getDefaultDatabaseInstance throws an error", async () => { + const options = { project: "my-project" }; + const error = new Error("Something went wrong"); + getDefaultDatabaseInstanceStub.rejects(error); + await expect(requireDatabaseInstance(options)).to.be.rejectedWith( + FirebaseError, + "Failed to get details for project: my-project.", + ); + }); +}); diff --git a/src/requireDatabaseInstance.ts b/src/requireDatabaseInstance.ts index e1ed760346e..f0e0af03590 100644 --- a/src/requireDatabaseInstance.ts +++ b/src/requireDatabaseInstance.ts @@ -1,12 +1,12 @@ -import * as clc from "cli-color"; -import { FirebaseError } from "./error"; +import * as clc from "colorette"; +import { FirebaseError, getError } 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.`; /** @@ -20,10 +20,10 @@ export async function requireDatabaseInstance(options: any): Promise { } let instance; try { - instance = await getDefaultDatabaseInstance(options); - } catch (err) { + instance = await getDefaultDatabaseInstance(options.project); + } catch (err: unknown) { throw new FirebaseError(`Failed to get details for project: ${options.project}.`, { - original: err, + original: getError(err), }); } if (instance === "") { diff --git a/src/requireHostingSite.spec.ts b/src/requireHostingSite.spec.ts new file mode 100644 index 00000000000..83af7ddcde3 --- /dev/null +++ b/src/requireHostingSite.spec.ts @@ -0,0 +1,43 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { requireHostingSite } from "./requireHostingSite"; +import * as hosting from "./getDefaultHostingSite"; + +describe("requireHostingSite", () => { + let getDefaultHostingSiteStub: sinon.SinonStub; + + beforeEach(() => { + getDefaultHostingSiteStub = sinon.stub(hosting, "getDefaultHostingSite"); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("should do nothing if options.site is already set", async () => { + const options = { site: "my-site" }; + await requireHostingSite(options); + expect(options.site).to.equal("my-site"); + expect(getDefaultHostingSiteStub).to.not.have.been.called; + }); + + it("should call getDefaultHostingSite if options.site is not set", async () => { + const options = {}; + getDefaultHostingSiteStub.resolves("default-site"); + await requireHostingSite(options); + expect(getDefaultHostingSiteStub).to.have.been.calledOnce; + }); + + it("should set options.site to the value returned by getDefaultHostingSite", async () => { + const options: { site?: string } = {}; + getDefaultHostingSiteStub.resolves("default-site"); + await requireHostingSite(options); + expect(options.site).to.equal("default-site"); + }); + + it("should not throw an error if getDefaultHostingSite resolves", async () => { + const options = {}; + getDefaultHostingSiteStub.resolves("default-site"); + await expect(requireHostingSite(options)).to.be.fulfilled; + }); +}); diff --git a/src/requireInteractive.spec.ts b/src/requireInteractive.spec.ts new file mode 100644 index 00000000000..9fbbe115a32 --- /dev/null +++ b/src/requireInteractive.spec.ts @@ -0,0 +1,24 @@ +import { expect } from "chai"; +import requireInteractive from "./requireInteractive"; +import { FirebaseError } from "./error"; +import { Options } from "./options"; + +describe("requireInteractive", () => { + it("should resolve if options.nonInteractive is false", async () => { + const options = { nonInteractive: false } as Options; + await expect(requireInteractive(options)).to.be.fulfilled; + }); + + it("should resolve if options.nonInteractive is undefined", async () => { + const options = {} as Options; + await expect(requireInteractive(options)).to.be.fulfilled; + }); + + it("should reject with a FirebaseError if options.nonInteractive is true", async () => { + const options = { nonInteractive: true } as Options; + await expect(requireInteractive(options)).to.be.rejectedWith( + FirebaseError, + "This command cannot run in non-interactive mode", + ); + }); +}); diff --git a/src/requireInteractive.ts b/src/requireInteractive.ts new file mode 100644 index 00000000000..5e42276036f --- /dev/null +++ b/src/requireInteractive.ts @@ -0,0 +1,14 @@ +import type { Options } from "./options"; + +import { FirebaseError } from "./error"; + +export default function requireInteractive(options: Options) { + if (options.nonInteractive) { + 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 329a28ea041..f2291f43c6a 100644 --- a/src/requirePermissions.ts +++ b/src/requirePermissions.ts @@ -1,8 +1,8 @@ -import { bold } from "cli-color"; -import getProjectId = require("./getProjectId"); +import { bold } from "colorette"; +import { getProjectId } from "./projectUtils"; import { requireAuth } from "./requireAuth"; import { logger } from "./logger"; -import { FirebaseError } from "./error"; +import { FirebaseError, getErrMsg } from "./error"; import { testIamPermissions } from "./gcp/iam"; // Permissions required for all commands. @@ -17,12 +17,15 @@ 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 = 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,12 +33,12 @@ 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) { - logger.debug(`[iam] error while checking permissions, command may fail: ${err}`); + } catch (err: unknown) { + logger.debug(`[iam] error while checking permissions, command may fail: ${getErrMsg(err)}`); return; } } diff --git a/src/requireTosAcceptance.spec.ts b/src/requireTosAcceptance.spec.ts new file mode 100644 index 00000000000..0e5c3d333d9 --- /dev/null +++ b/src/requireTosAcceptance.spec.ts @@ -0,0 +1,93 @@ +import * as nock from "nock"; +import * as sinon from "sinon"; +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"; +import * as auth from "./auth"; + +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", () => { + let loggedInStub: sinon.SinonStub; + beforeEach(() => { + nock.disableNetConnect(); + loggedInStub = sinon.stub(auth, "loggedIn"); + }); + afterEach(() => { + nock.enableNetConnect(); + loggedInStub.restore(); + }); + + it("should resolve for accepted terms of service", async () => { + nock("https://mobilesdk-pa.googleapis.com") + .get("/v1/accessmanagement/tos:getStatus") + .reply(200, SAMPLE_RESPONSE); + loggedInStub.returns(true); + + 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); + loggedInStub.returns(true); + + await expect(requireTosAcceptance(APPHOSTING_TOS_ID)(SAMPLE_OPTIONS)).to.be.rejectedWith( + "Terms of Service", + ); + + expect(nock.isDone()).to.be.true; + }); + + it("should resolve to if not a human", async () => { + loggedInStub.returns(false); + + await requireTosAcceptance(APPHOSTING_TOS_ID)(SAMPLE_OPTIONS); + + expect(nock.isDone()).to.be.true; + }); +}); diff --git a/src/requireTosAcceptance.ts b/src/requireTosAcceptance.ts new file mode 100644 index 00000000000..8b33e96d666 --- /dev/null +++ b/src/requireTosAcceptance.ts @@ -0,0 +1,48 @@ +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"; +import { loggedIn } from "./auth"; + +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 { + // If they are not logged in, they either cannot make calls, or are using a service account. + // Either way, no need to check TOS. + if (!loggedIn()) { + return; + } + 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..dbed1c668c3 --- /dev/null +++ b/src/responseToError.ts @@ -0,0 +1,63 @@ +import * as _ from "lodash"; + +import { FirebaseError } from "./error"; + +export function responseToError(response: any, body: any, url?: string): 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, + }; + } + + let message = "HTTP Error: " + response.statusCode + ", " + (body.error.message || body.error); + if (url) { + message = "Request to " + url + " had " + message; + } + + 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.spec.ts b/src/rtdb.spec.ts new file mode 100644 index 00000000000..c6e0207d4b2 --- /dev/null +++ b/src/rtdb.spec.ts @@ -0,0 +1,154 @@ +import * as chai from "chai"; +import * as sinon from "sinon"; +import * as sinonChai from "sinon-chai"; + +import * as apiv2 from "./apiv2"; +import { Client } from "./apiv2"; +import * as rtdb from "./rtdb"; +import * as management from "./management/database"; +import * as utils from "./utils"; +import { FirebaseError } from "./error"; +import { Response } from "node-fetch"; + +const expect = chai.expect; +chai.use(sinonChai); + +const PROJECT_ID = "the-best-project-o-ever"; +const DATABASE_INSTANCE = "the-best-instance"; + +describe("rtdb", () => { + let sinonSandbox: sinon.SinonSandbox; + let client: Client; + + beforeEach(() => { + sinonSandbox = sinon.createSandbox(); + client = new Client({ urlPrefix: "https://firebaseio.com", auth: true }); + }); + + afterEach(() => { + sinonSandbox.restore(); + }); + + describe("updateRulesWithClient", () => { + it("should resolve on success", async () => { + const request = sinonSandbox.stub(client, "request").resolves({ + body: {}, + status: 200, + response: new Response(), + }); + const rules = { rules: { ".read": true } }; + + await rtdb.updateRulesWithClient(client, rules); + + expect(request).to.be.calledOnceWith({ + method: "PUT", + path: ".settings/rules.json", + queryParams: {}, + body: rules, + resolveOnHTTPError: true, + }); + }); + + it("should resolve on success with dryRun", async () => { + const request = sinonSandbox.stub(client, "request").resolves({ + body: {}, + status: 200, + response: new Response(), + }); + const rules = { rules: { ".read": true } }; + + await rtdb.updateRulesWithClient(client, rules, { dryRun: true }); + + expect(request).to.be.calledOnceWith({ + method: "PUT", + path: ".settings/rules.json", + queryParams: { dryRun: "true" }, + body: rules, + resolveOnHTTPError: true, + }); + }); + + it("should reject with a FirebaseError on 400", async () => { + const request = sinonSandbox.stub(client, "request").resolves({ + body: { error: "Syntax error" }, + status: 400, + response: new Response(), + }); + const rules = { rules: { ".read": true } }; + + await expect(rtdb.updateRulesWithClient(client, rules)).to.be.rejectedWith( + FirebaseError, + "Syntax error in database rules", + ); + expect(request).to.be.calledOnce; + }); + + it("should reject with a FirebaseError on >400", async () => { + const request = sinonSandbox.stub(client, "request").resolves({ + body: {}, + status: 500, + response: new Response(), + }); + const rules = { rules: { ".read": true } }; + + await expect(rtdb.updateRulesWithClient(client, rules)).to.be.rejectedWith( + FirebaseError, + "Unexpected error while deploying database rules.", + ); + expect(request).to.be.calledOnce; + }); + }); + + describe("updateRules", () => { + it("should call updateRulesWithClient with the correct params", async () => { + const mockRequest = sinon.stub().resolves({ + body: {}, + status: 200, + response: new Response(), + }); + const clientStub = sinonSandbox.stub(apiv2, "Client").returns({ + request: mockRequest, + } as any); + + const populateInstanceDetails = sinonSandbox + .stub(management, "populateInstanceDetails") + .onFirstCall() + .callsFake(async (options: any) => { + options.instanceDetails = { + name: DATABASE_INSTANCE, + project: PROJECT_ID, + databaseUrl: "https://firebaseio.com", + type: "DEFAULT_DATABASE", + state: "ACTIVE", + }; + }); + const getDatabaseUrl = sinonSandbox + .stub(utils, "getDatabaseUrl") + .returns("https://firebaseio.com"); + const rules = { rules: { ".read": true } }; + + await rtdb.updateRules(PROJECT_ID, DATABASE_INSTANCE, rules); + + expect(populateInstanceDetails).to.be.calledOnce; + expect(getDatabaseUrl).to.be.calledOnce; + expect(clientStub).to.be.calledOnceWith({ urlPrefix: "https://firebaseio.com" }); + expect(mockRequest).to.be.calledOnceWith({ + method: "PUT", + path: ".settings/rules.json", + queryParams: {}, + body: rules, + resolveOnHTTPError: true, + }); + }); + + it("should throw an error if populateInstanceDetails fails", async () => { + sinonSandbox.stub(management, "populateInstanceDetails").resolves(); // Resolves without populating details + const rules = { rules: { ".read": true } }; + + await expect(rtdb.updateRules(PROJECT_ID, DATABASE_INSTANCE, rules)).to.be.rejectedWith( + FirebaseError, + "Could not get instance details", + ); + }); + }); +}); diff --git a/src/rtdb.ts b/src/rtdb.ts new file mode 100644 index 00000000000..2783bd7db7c --- /dev/null +++ b/src/rtdb.ts @@ -0,0 +1,57 @@ +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 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 }); + + return updateRulesWithClient(client, src, options); +} + +export async function updateRulesWithClient( + client: Client, + src: unknown, + options: { dryRun?: boolean } = {}, +) { + const queryParams: { dryRun?: string } = {}; + if (options.dryRun) { + queryParams.dryRun = "true"; + } + + 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 76% rename from src/test/rulesDeploy.spec.ts rename to src/rulesDeploy.spec.ts index b3d36850e08..39c60df2e5c 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 = require("../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, "confirm").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, "confirm").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, "confirm"); + + 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; @@ -371,7 +451,7 @@ describe("RulesDeploy", () => { describe("and a prompt is made", () => { beforeEach(() => { - sinon.stub(prompt, "prompt").rejects(new Error("behavior unspecified")); + sinon.stub(prompt, "confirm").rejects(new Error("behavior unspecified")); sinon.stub(gcp.rules, "listAllReleases").rejects(new Error("listAllReleases failing")); sinon.stub(gcp.rules, "deleteRuleset").rejects(new Error("deleteRuleset failing")); sinon.stub(gcp.rules, "getRulesetId").throws(new Error("getRulesetId failing")); @@ -384,21 +464,21 @@ describe("RulesDeploy", () => { it("should prompt for a choice (no)", async () => { (gcp.rules.createRuleset as sinon.SinonStub).onFirstCall().rejects(QUOTA_ERROR); (gcp.rules.listAllRulesets as sinon.SinonStub).resolves(Array(1001)); - (prompt.prompt as sinon.SinonStub).onFirstCall().resolves({ confirm: false }); + (prompt.confirm as sinon.SinonStub).onFirstCall().resolves(false); rd.addFile("firestore.rules"); const result = rd.createRulesets(RulesetServiceType.CLOUD_FIRESTORE); await expect(result).to.eventually.deep.equal([]); expect(gcp.rules.createRuleset).to.be.calledOnce; - expect(prompt.prompt).to.be.calledOnce; + expect(prompt.confirm).to.be.calledOnce; }); 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.prompt as sinon.SinonStub).onFirstCall().resolves({ confirm: true }); + (prompt.confirm as sinon.SinonStub).onFirstCall().resolves(true); (gcp.rules.listAllReleases as sinon.SinonStub).resolves([ { rulesetName: "name", name: "bar" }, ]); @@ -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 bbf85d664c5..7c854db3bd9 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 { FirebaseError, getErrStatus } from "./error"; +import * as utils from "./utils"; -import { prompt } from "./prompt"; +import { confirm } 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,48 @@ 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 confirm({ + message: `Cloud Storage for Firebase needs an IAM Role to use cross-service rules. Grant the new role?`, + default: true, + force: this.options.force, + }); + + // 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 +166,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,47 +196,32 @@ export class RulesDeploy { this.rulesetNames[filename] = await rulesetName; createdRulesetNames.push(await rulesetName); } - } catch (err) { - if (err.status !== QUOTA_EXCEEDED_STATUS_CODE) { + } catch (err: unknown) { + if (getErrStatus(err) !== 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); if (history.length > RULESET_COUNT_LIMIT) { - const answers = await prompt( - { - confirm: this.options.force, - }, - [ - { - type: "confirm", - name: "confirm", - message: `You have ${history.length} rules, do you want to delete the oldest ${RULESETS_TO_GC} to free up space?`, - default: false, - }, - ] - ); - if (answers.confirm) { + const confirmed = await confirm({ + message: `You have ${history.length} rules, do you want to delete the oldest ${RULESETS_TO_GC} to free up space?`, + force: this.options.force, + }); + if (confirmed) { // 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); } } @@ -198,12 +234,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) { @@ -212,14 +248,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)}`, ); } @@ -229,9 +262,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[] = []; @@ -256,13 +287,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..39c5a4d7399 --- /dev/null +++ b/src/scopes.ts @@ -0,0 +1,12 @@ +// default scopes +export const OPENID = "openid"; +export const EMAIL = "email"; +export const USERINFO_EMAIL = "https://www.googleapis.com/auth/userinfo.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.spec.ts b/src/serve/functions.spec.ts new file mode 100644 index 00000000000..1cf99eca41e --- /dev/null +++ b/src/serve/functions.spec.ts @@ -0,0 +1,132 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { FunctionsServer } from "./functions"; +import * as functionsEmulator from "../emulator/functionsEmulator"; +import * as projectUtils from "../projectUtils"; +import * as auth from "../auth"; +import * as projectConfig from "../functions/projectConfig"; +import * as emulatorRegistry from "../emulator/registry"; +import * as commandUtils from "../emulator/commandUtils"; +describe("FunctionsServer", () => { + const sandbox = sinon.createSandbox(); + + let functionsEmulatorStub: sinon.SinonStub; + let needProjectIdStub: sinon.SinonStub; + let getProjectDefaultAccountStub: sinon.SinonStub; + let normalizeAndValidateStub: sinon.SinonStub; + let startRegistryStub: sinon.SinonStub; + + let functionsEmulatorInstance: { + start: sinon.SinonStub; + connect: sinon.SinonStub; + stop: sinon.SinonStub; + }; + + beforeEach(() => { + functionsEmulatorInstance = { + start: sandbox.stub().resolves(), + connect: sandbox.stub().resolves(), + stop: sandbox.stub().resolves(), + }; + functionsEmulatorStub = sandbox + .stub(functionsEmulator, "FunctionsEmulator") + .returns(functionsEmulatorInstance as any); + + needProjectIdStub = sandbox.stub(projectUtils, "needProjectId").returns("project-id"); + getProjectDefaultAccountStub = sandbox.stub(auth, "getProjectDefaultAccount").returns({ + user: { email: "test@test.com" }, + tokens: { access_token: "token" }, + } as any); + normalizeAndValidateStub = sandbox + .stub(projectConfig, "normalizeAndValidate") + .returns([{ source: "functions", codebase: "default", runtime: "nodejs18" }]); + startRegistryStub = sandbox.stub(emulatorRegistry.EmulatorRegistry, "start").resolves(); + sandbox.stub(commandUtils, "parseInspectionPort").returns(9229); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should throw when calling methods before start", async () => { + const server = new FunctionsServer(); + expect(() => server.get()).to.throw("Must call start() before calling any other operation!"); + await expect(server.connect()).to.be.rejectedWith( + "Must call start() before calling any other operation!", + ); + await expect(server.stop()).to.be.rejectedWith( + "Must call start() before calling any other operation!", + ); + }); + + it("should start the emulator with the correct args", async () => { + const server = new FunctionsServer(); + const options = { + config: { projectDir: "/path/to/project", src: { functions: {} } }, + projectAlias: "alias", + }; + + await server.start(options as any, {}); + + expect(needProjectIdStub).to.have.been.calledOnceWith(options); + expect(normalizeAndValidateStub).to.have.been.calledOnceWith({}); + expect(getProjectDefaultAccountStub).to.have.been.calledOnceWith("/path/to/project"); + expect(functionsEmulatorStub).to.have.been.calledOnce; + const emulatorArgs = functionsEmulatorStub.getCall(0).args[0]; + expect(emulatorArgs.projectId).to.equal("project-id"); + expect(emulatorArgs.projectAlias).to.equal("alias"); + expect(emulatorArgs.projectDir).to.equal("/path/to/project"); + expect(emulatorArgs.emulatableBackends[0].functionsDir).to.contain("functions"); + expect(startRegistryStub).to.have.been.calledOnceWith(functionsEmulatorInstance); + }); + + it("should assign ports correctly when hosting is running", async () => { + const server = new FunctionsServer(); + const options = { + config: { projectDir: "/path/to/project", src: { functions: {} } }, + port: 8080, + targets: ["hosting"], + }; + + await server.start(options as any, {}); + const emulatorArgs = functionsEmulatorStub.getCall(0).args[0]; + expect(emulatorArgs.port).to.equal(8081); + }); + + it("should assign ports correctly when hosting is NOT running", async () => { + const server = new FunctionsServer(); + const options = { + config: { projectDir: "/path/to/project", src: { functions: {} } }, + port: 8080, + targets: ["functions"], + }; + + await server.start(options as any, {}); + const emulatorArgs = functionsEmulatorStub.getCall(0).args[0]; + expect(emulatorArgs.port).to.equal(8080); + }); + + it("should connect to the emulator", async () => { + const server = new FunctionsServer(); + const options = { config: { projectDir: "/path/to/project", src: { functions: {} } } }; + await server.start(options as any, {}); + await server.connect(); + expect(functionsEmulatorInstance.connect).to.have.been.calledOnce; + }); + + it("should stop the emulator", async () => { + const server = new FunctionsServer(); + const options = { config: { projectDir: "/path/to/project", src: { functions: {} } } }; + await server.start(options as any, {}); + await server.stop(); + expect(functionsEmulatorInstance.stop).to.have.been.calledOnce; + }); + + it("should get the emulator instance", async () => { + const server = new FunctionsServer(); + const options = { config: { projectDir: "/path/to/project", src: { functions: {} } } }; + await server.start(options as any, {}); + const instance = server.get(); + expect(instance).to.equal(functionsEmulatorInstance); + }); +}); diff --git a/src/serve/functions.ts b/src/serve/functions.ts index cafd1967a50..ee0a0fa9f6e 100644 --- a/src/serve/functions.ts +++ b/src/serve/functions.ts @@ -1,42 +1,66 @@ import * as path from "path"; -import { FunctionsEmulator, FunctionsEmulatorArgs } from "../emulator/functionsEmulator"; -import { EmulatorServer } from "../emulator/emulatorServer"; -import { parseRuntimeVersion } from "../emulator/functionsEmulatorUtils"; -import * as getProjectId from "../getProjectId"; +import { + EmulatableBackend, + FunctionsEmulator, + FunctionsEmulatorArgs, +} from "../emulator/functionsEmulator"; +import { needProjectId } from "../projectUtils"; import { getProjectDefaultAccount } from "../auth"; +import { Options } from "../options"; +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: any, partialArgs: Partial): Promise { - const projectId = getProjectId(options, false); - const functionsDir = path.join( - options.config.projectDir, - options.config.get("functions.source") - ); - const account = getProjectDefaultAccount(options.config.projectDir); - const nodeMajorVersion = parseRuntimeVersion(options.config.get("functions.runtime")); + async start(options: Options, partialArgs: Partial): Promise { + const projectId = needProjectId(options); + const config = projectConfig.normalizeAndValidate(options.config.src.functions); - // 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 backends: EmulatableBackend[] = []; + for (const cfg of config) { + const localCfg = projectConfig.requireLocal( + cfg, + "Remote sources are not supported in the Functions emulator.", + ); + const functionsDir = path.join(options.config.projectDir, localCfg.source); + backends.push({ + functionsDir, + codebase: localCfg.codebase, + runtime: localCfg.runtime, + env: {}, + secretEnv: [], + }); + } + this.backends = backends; + + 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; } @@ -44,30 +68,30 @@ export class FunctionsServer { // we can use the port argument. Otherwise it goes to hosting and // we use port + 1. if (options.port) { - const hostingRunning = options.targets && options.targets.indexOf("hosting") >= 0; + utils.assertIsNumber(options.port); + const targets = options.targets as string[] | undefined; + const port = options.port; + const hostingRunning = targets && targets.includes("hosting"); if (hostingRunning) { - args.port = options.port + 1; + args.port = port + 1; } else { - args.port = options.port; + args.port = port; } } - 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.spec.ts b/src/serve/hosting.spec.ts new file mode 100644 index 00000000000..db67dada434 --- /dev/null +++ b/src/serve/hosting.spec.ts @@ -0,0 +1,131 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as superstatic from "superstatic"; +import * as portUtils from "../emulator/portUtils"; +import * as hostingConfig from "../hosting/config"; +import * as implicitInit from "../hosting/implicitInit"; +import * as hosting from "./hosting"; +import * as requireHostingSite from "../requireHostingSite"; +import * as utils from "../utils"; +import { Writable } from "stream"; + +describe("hosting", () => { + const sandbox = sinon.createSandbox(); + + let checkListenableStub: sinon.SinonStub; + let hostingConfigStub: sinon.SinonStub; + let requireHostingSiteStub: sinon.SinonStub; + let superstaticStub: sinon.SinonStub; + let createDestroyerStub: sinon.SinonStub; + + let superstaticServer: { on: sinon.SinonStub; listen: sinon.SinonStub }; + + beforeEach(() => { + checkListenableStub = sandbox.stub(portUtils, "checkListenable").resolves(true); + hostingConfigStub = sandbox.stub(hostingConfig, "hostingConfig").returns([ + { + site: "site-one", + public: "public", + }, + ]); + sandbox.stub(implicitInit, "implicitInit").resolves({ + json: JSON.stringify({ hosting: {} }), + js: "", + emulatorsJs: "", + } as any); + requireHostingSiteStub = sandbox.stub(requireHostingSite, "requireHostingSite").resolves(); + + superstaticServer = { + on: sandbox.stub(), + listen: sandbox.stub().callsFake((cb) => { + if (cb) { + cb(); + } + return superstaticServer; + }), + }; + superstaticStub = sandbox.stub(superstatic, "server").returns(superstaticServer as any); + createDestroyerStub = sandbox.stub(utils, "createDestroyer"); + sandbox.stub(Writable.prototype, "_write").resolves(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("start", () => { + it("should start a superstatic server with the correct config", async () => { + const options = { port: 8080, host: "localhost" }; + await hosting.start(options); + + expect(superstaticStub).to.have.been.calledOnce; + const superstaticConfig = superstaticStub.getCall(0).args[0]; + expect(superstaticConfig.port).to.equal(8080); + expect(superstaticConfig.hostname).to.equal("localhost"); + expect(superstaticConfig.config.public).to.equal("public"); + expect(superstaticServer.listen).to.have.been.calledOnce; + }); + + it("should require a hosting site if not specified", async () => { + const options = { port: 8080, host: "localhost" }; + await hosting.start(options); + expect(requireHostingSiteStub).to.have.been.calledOnceWith(options); + }); + + it("should not require a hosting site if one is specified", async () => { + const options = { port: 8080, host: "localhost", site: "my-site" }; + await hosting.start(options); + expect(requireHostingSiteStub).to.not.have.been.called; + }); + + it("should find an available port", async () => { + const options = { port: 8080, host: "localhost" }; + checkListenableStub + .withArgs({ address: "localhost", port: 8080, family: "IPv6" }) + .resolves(false); + checkListenableStub + .withArgs({ address: "localhost", port: 8081, family: "IPv6" }) + .resolves(true); + + await hosting.start(options); + + expect(superstaticStub).to.have.been.calledOnce; + const superstaticConfig = superstaticStub.getCall(0).args[0]; + expect(superstaticConfig.port).to.equal(8081); + }); + + it("should start multiple servers for multiple hosting configs", async () => { + const options = { port: 8080, host: "localhost" }; + hostingConfigStub.returns([ + { site: "site-one", public: "public" }, + { site: "site-two", public: "public" }, + ]); + + await hosting.start(options); + + expect(superstaticStub).to.have.been.calledTwice; + const port1 = superstaticStub.getCall(0).args[0].port; + const port2 = superstaticStub.getCall(1).args[0].port; + expect(port1).to.equal(8080); + expect(port2).to.equal(8085); + }); + }); + + describe("stop", () => { + it("should call the destroyer if the server was started", async () => { + const destroyer = sandbox.stub().resolves(); + createDestroyerStub.returns(destroyer); + const options = { port: 8080, host: "localhost" }; + await hosting.start(options); + await hosting.stop(); + expect(destroyer).to.have.been.calledOnce; + }); + + it("should do nothing if the server was not started", async () => { + const destroyer = sandbox.stub().resolves(); + createDestroyerStub.returns(destroyer); + await hosting.stop(); + expect(destroyer).to.not.have.been.called; + }); + }); +}); 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.spec.ts b/src/serve/index.spec.ts new file mode 100644 index 00000000000..6ac0cd023d6 --- /dev/null +++ b/src/serve/index.spec.ts @@ -0,0 +1,158 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { serve } from "./index"; +import * as hosting from "./hosting"; +import { FunctionsServer } from "./functions"; +import * as prepareFrameworks from "../frameworks"; +import * as experiments from "../experiments"; +import * as config from "../hosting/config"; +import * as track from "../track"; +import * as projectUtils from "../projectUtils"; + +describe("serve", () => { + const sandbox = sinon.createSandbox(); + let hostingStart: sinon.SinonStub; + let hostingStop: sinon.SinonStub; + let hostingConnect: sinon.SinonStub; + let functionsStart: sinon.SinonStub; + let functionsStop: sinon.SinonStub; + let functionsConnect: sinon.SinonStub; + let prepareFrameworksStub: sinon.SinonStub; + let experimentsAssertEnabledStub: sinon.SinonStub; + let configExtractStub: sinon.SinonStub; + let trackEmulatorStub: sinon.SinonStub; + + let processOnStub: sinon.SinonStub; + let sigintHandler: () => void; + + beforeEach(() => { + // Stub dependencies + hostingStart = sandbox.stub(hosting, "start").resolves({ ports: [] }); + hostingStop = sandbox.stub(hosting, "stop").resolves(); + hostingConnect = sandbox.stub(hosting, "connect").resolves(); + + functionsStart = sandbox.stub(FunctionsServer.prototype, "start").resolves(); + functionsStop = sandbox.stub(FunctionsServer.prototype, "stop").resolves(); + functionsConnect = sandbox.stub(FunctionsServer.prototype, "connect").resolves(); + + prepareFrameworksStub = sandbox.stub(prepareFrameworks, "prepareFrameworks").resolves(); + experimentsAssertEnabledStub = sandbox.stub(experiments, "assertEnabled"); + configExtractStub = sandbox.stub(config, "extract"); + trackEmulatorStub = sandbox.stub(track, "trackEmulator"); + sandbox.stub(projectUtils, "getProjectId").returns("demo-project"); + + // Stub process.on to capture the SIGINT handler + processOnStub = sandbox.stub(process, "on"); + processOnStub.withArgs("SIGINT").callsFake((event, handler) => { + sigintHandler = handler as () => void; + return process; + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + async function triggerSIGINT() { + // Wait for the next event loop tick to ensure the handler is registered. + await new Promise((resolve) => setImmediate(resolve)); + if (sigintHandler) { + sigintHandler(); + } + } + + it("should start and connect to all services, then stop on SIGINT", async () => { + const options = { + targets: ["hosting", "functions"], + port: 8080, + }; + configExtractStub.returns([]); + + const servePromise = serve(options); + await triggerSIGINT(); + await servePromise; + + expect(hostingStart).to.have.been.calledOnceWith(options); + expect(functionsStart).to.have.been.calledOnce; + expect(hostingConnect).to.have.been.calledOnce; + expect(functionsConnect).to.have.been.calledOnce; + expect(hostingStop).to.have.been.calledOnceWith(options); + expect(functionsStop).to.have.been.calledOnce; + }); + + it("should call prepareFrameworks when webframeworks experiment is enabled and hosting source exists", async () => { + const options = { + targets: ["hosting"], + port: 8080, + }; + configExtractStub.returns([{ source: "some-source" }]); + + const servePromise = serve(options); + await triggerSIGINT(); + await servePromise; + + expect(experimentsAssertEnabledStub).to.have.been.calledOnceWith( + "webframeworks", + "emulate a web framework", + ); + expect(prepareFrameworksStub).to.have.been.calledOnceWith( + "emulate", + ["hosting"], + undefined, + options, + ); + }); + + it("should not call prepareFrameworks if hosting target has no source", async () => { + const options = { + targets: ["hosting"], + port: 8080, + }; + configExtractStub.returns([{}]); // No source + + const servePromise = serve(options); + await triggerSIGINT(); + await servePromise; + + expect(prepareFrameworksStub).to.not.have.been.called; + }); + + it("should throw if webframeworks experiment is not enabled", async () => { + const options = { + targets: ["hosting"], + port: 8080, + }; + configExtractStub.returns([{ source: "some-source" }]); + const error = new Error("webframeworks experiment not enabled"); + experimentsAssertEnabledStub.throws(error); + + await expect(serve(options)).to.be.rejectedWith(error); + expect(prepareFrameworksStub).to.not.have.been.called; + }); + + it("should track emulator run and started events", async () => { + const options = { + targets: ["hosting", "functions"], + port: 8080, + }; + configExtractStub.returns([]); + + const servePromise = serve(options); + await triggerSIGINT(); + await servePromise; + + expect(trackEmulatorStub).to.have.been.calledWith("emulator_run", { + emulator_name: "hosting", + is_demo_project: "true", + }); + expect(trackEmulatorStub).to.have.been.calledWith("emulator_run", { + emulator_name: "functions", + is_demo_project: "true", + }); + expect(trackEmulatorStub).to.have.been.calledWith("emulators_started", { + count: 2, + count_all: 2, + is_demo_project: "true", + }); + }); +}); 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/shortenUrl.spec.ts b/src/shortenUrl.spec.ts new file mode 100644 index 00000000000..66cf230903e --- /dev/null +++ b/src/shortenUrl.spec.ts @@ -0,0 +1,38 @@ +import { expect } from "chai"; +import * as nock from "nock"; +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()) + .post( + `/v1/shortLinks`, + (body: { dynamicLinkInfo?: { link: string }; suffix?: { option: string } }) => + body.dynamicLinkInfo?.link === url && body.suffix?.option === suffix, + ) + .query({ key: dynamicLinksKey() }) + .reply(code, { + shortLink: MOCKED_LINK, + previewLink: `${MOCKED_LINK}?d=1`, + }); + } + + it("should return a shortened url with an unguessable suffix by default", async () => { + mockDynamicLinks(TEST_LINK); + expect(await shortenUrl(TEST_LINK)).to.eq(MOCKED_LINK); + }); + + it("should request a short suffix URL if guessable is true", async () => { + mockDynamicLinks(TEST_LINK, "SHORT"); + expect(await shortenUrl(TEST_LINK, true)).to.eq(MOCKED_LINK); + }); + + it("should return the original URL in case of an error", async () => { + mockDynamicLinks(TEST_LINK, "UNGUESSABLE", 400); + expect(await shortenUrl(TEST_LINK)).to.eq(TEST_LINK); + }); +}); diff --git a/src/shortenUrl.ts b/src/shortenUrl.ts new file mode 100644 index 00000000000..336e849ef29 --- /dev/null +++ b/src/shortenUrl.ts @@ -0,0 +1,51 @@ +import { logger } from "./logger"; +import { Client } from "./apiv2"; +import { dynamicLinksKey, dynamicLinksOrigin } from "./api"; + +const DYNAMIC_LINKS_PREFIX = "https://firebase.tools/l"; + +const apiClient = new Client({ + urlPrefix: dynamicLinksOrigin(), + auth: false, + apiVersion: "v1", +}); + +interface DynamicLinksRequest { + dynamicLinkInfo: { + link: string; + domainUriPrefix: string; + }; + suffix: { option: "SHORT" | "UNGUESSABLE" }; +} + +interface DynamicLinksResponse { + shortLink: string; + previewLink: string; +} + +/** + * Attempts to shorten a URL for easier display in terminals. Falls back to returning the original URL if anything goes wrong. + * + * @param url The URL to shorten. + * @param guessable When true, a shorter suffix (~4 characters) is used instead of an unguessable one. Do not set to true when URL contains personally identifiable information. + * @return The short URL or the original URL if an error occurs. + */ +export async function shortenUrl(url: string, guessable = false): Promise { + try { + const response = await apiClient.post( + `shortLinks?key=${dynamicLinksKey()}`, + { + dynamicLinkInfo: { + link: url, + domainUriPrefix: DYNAMIC_LINKS_PREFIX, + }, + suffix: { option: guessable ? "SHORT" : "UNGUESSABLE" }, + }, + ); + + return response.body.shortLink; + } 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..488ae96bfb7 --- /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 "./vsCodeUtils"; + +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 3328cf0dffb..00000000000 --- a/src/test/apiv2.spec.ts +++ /dev/null @@ -1,416 +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").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 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 f6060bdaccb..00000000000 --- a/src/test/appdistro/client.spec.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { expect } from "chai"; -import { join } from "path"; -import * as rimraf from "rimraf"; -import * as sinon from "sinon"; -import * as tmp from "tmp"; - -import { - AppDistributionClient, - AppView, - UploadStatus, - UploadStatusResponse, -} from "../../appdistribution/client"; -import { FirebaseError } from "../../error"; -import * as api from "../../api"; -import * as nock from "nock"; -import { Distribution, DistributionFileType } from "../../appdistribution/distribution"; - -tmp.setGracefulCleanup(); - -describe("distribution", () => { - const tempdir = tmp.dirSync(); - const appId = "1:12345789:ios:abc123def456"; - const mockDistribution = new Distribution(join(tempdir.name, "app.ipa")); - const appDistributionClient = new AppDistributionClient(appId); - const appViewBasic = "BASIC"; - const appViewFull = "FULL"; - - let sandbox: sinon.SinonSandbox; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - sandbox.useFakeTimers(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - after(() => { - rimraf.sync(tempdir.name); - }); - - describe("getApp", () => { - it("should throw error when app does not exist", async () => { - nock(api.appDistributionOrigin) - .get(`/v1alpha/apps/${appId}`) - .query({ appView: appViewBasic }) - .reply(404, {}); - await expect(appDistributionClient.getApp()).to.be.rejected; - expect(nock.isDone()).to.be.true; - }); - - it("should resolve when request succeeds", async () => { - nock(api.appDistributionOrigin) - .get(`/v1alpha/apps/${appId}`) - .query({ appView: appViewBasic }) - .reply(200, {}); - await expect(appDistributionClient.getApp()).to.be.fulfilled; - expect(nock.isDone()).to.be.true; - }); - - it("requests basic appView", async () => { - nock(api.appDistributionOrigin) - .get(`/v1alpha/apps/${appId}`) - .query({ appView: appViewBasic }) - .reply(200, {}); - await expect(appDistributionClient.getApp(AppView.BASIC)).to.be.fulfilled; - expect(nock.isDone()).to.be.true; - }); - - it("requests full appView", async () => { - nock(api.appDistributionOrigin) - .get(`/v1alpha/apps/${appId}`) - .query({ appView: appViewFull }) - .reply(200, {}); - await expect(appDistributionClient.getApp(AppView.FULL)).to.be.fulfilled; - expect(nock.isDone()).to.be.true; - }); - - it("should throw an error when the request fails", async () => { - nock(api.appDistributionOrigin) - .get(`/v1alpha/apps/${appId}`) - .query({ appView: appViewBasic }) - .reply(404, {}); - await expect(appDistributionClient.getApp()).to.be.rejected; - expect(nock.isDone()).to.be.true; - }); - }); - - describe("uploadDistribution", () => { - it("should throw error if upload fails", async () => { - nock(api.appDistributionOrigin).post(`/app-binary-uploads?app_id=${appId}`).reply(400, {}); - await expect(appDistributionClient.uploadDistribution(mockDistribution)).to.be.rejected; - expect(nock.isDone()).to.be.true; - }); - - it("should return token if upload succeeds", async () => { - const fakeToken = "fake-token"; - nock(api.appDistributionOrigin) - .post(`/app-binary-uploads?app_id=${appId}`) - .reply(200, { token: fakeToken }); - await expect(appDistributionClient.uploadDistribution(mockDistribution)).to.be.eventually.eq( - fakeToken - ); - expect(nock.isDone()).to.be.true; - }); - }); - - describe("pollReleaseIdByHash", () => { - describe("when getUploadStatus returns IN_PROGRESS", () => { - it("should throw error when retry count >= AppDistributionClient.MAX_POLLING_RETRIES", () => { - sandbox.stub(appDistributionClient, "getUploadStatus").resolves({ - status: UploadStatus.IN_PROGRESS, - message: "", - errorCode: "", - release: { id: "" }, - }); - return expect( - appDistributionClient.pollUploadStatus( - "mock-hash", - AppDistributionClient.MAX_POLLING_RETRIES - ) - ).to.be.rejectedWith( - FirebaseError, - "it took longer than expected to process your binary, please try again" - ); - }); - }); - - it("should return release id when request succeeds", () => { - const releaseId = "fake-release-id"; - sandbox.stub(appDistributionClient, "getUploadStatus").resolves({ - status: UploadStatus.SUCCESS, - message: "", - errorCode: "", - release: { - id: releaseId, - }, - }); - return expect( - appDistributionClient.pollUploadStatus( - "mock-hash", - AppDistributionClient.MAX_POLLING_RETRIES - ) - ).to.eventually.eq(releaseId); - }); - }); - - describe("getUploadStatus", () => { - it("should throw an error when request fails", async () => { - const fakeHash = "fake-hash"; - nock(api.appDistributionOrigin) - .get(`/v1alpha/apps/${appId}/upload_status/${fakeHash}`) - .reply(400, {}); - - await expect(appDistributionClient.getUploadStatus(fakeHash)).to.be.rejectedWith( - FirebaseError, - "HTTP Error: 400" - ); - expect(nock.isDone()).to.be.true; - }); - - describe("when request succeeds", () => { - it("should return the upload status", async () => { - const releaseId = "fake-release-id"; - const fakeHash = "fake-hash"; - const response: UploadStatusResponse = { - status: UploadStatus.SUCCESS, - errorCode: "0", - message: "", - release: { - id: releaseId, - }, - }; - nock(api.appDistributionOrigin) - .get(`/v1alpha/apps/${appId}/upload_status/${fakeHash}`) - .reply(200, response); - - await expect(appDistributionClient.getUploadStatus(fakeHash)).to.eventually.deep.eq( - response - ); - expect(nock.isDone()).to.be.true; - }); - }); - }); - - describe("addReleaseNotes", () => { - it("should return immediately when no release notes are specified", async () => { - const apiSpy = sandbox.spy(api, "request"); - await expect(appDistributionClient.addReleaseNotes("fake-release-id", "")).to.eventually.be - .fulfilled; - expect(apiSpy).to.not.be.called; - }); - - it("should throw error when request fails", async () => { - const releaseId = "fake-release-id"; - nock(api.appDistributionOrigin) - .post(`/v1alpha/apps/${appId}/releases/${releaseId}/notes`) - .reply(400, {}); - await expect( - appDistributionClient.addReleaseNotes(releaseId, "release notes") - ).to.be.rejectedWith(FirebaseError, "failed to add release notes"); - expect(nock.isDone()).to.be.true; - }); - - it("should resolve when request succeeds", async () => { - const releaseId = "fake-release-id"; - nock(api.appDistributionOrigin) - .post(`/v1alpha/apps/${appId}/releases/${releaseId}/notes`) - .reply(200, {}); - await expect(appDistributionClient.addReleaseNotes(releaseId, "release notes")).to.eventually - .be.fulfilled; - expect(nock.isDone()).to.be.true; - }); - }); - - describe("enableAccess", () => { - it("should return immediately when testers and groups are empty", async () => { - const apiSpy = sandbox.spy(api, "request"); - await expect(appDistributionClient.enableAccess("fake-release-id")).to.eventually.be - .fulfilled; - expect(apiSpy).to.not.be.called; - }); - - it("should resolve when request succeeds", async () => { - const releaseId = "fake-release-id"; - nock(api.appDistributionOrigin) - .post(`/v1alpha/apps/${appId}/releases/${releaseId}/enable_access`) - .reply(200, {}); - await expect(appDistributionClient.enableAccess(releaseId, ["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 () => { - const releaseId = "fake-release-id"; - nock(api.appDistributionOrigin) - .post(`/v1alpha/apps/${appId}/releases/${releaseId}/enable_access`, { - emails: testers, - groupIds: groups, - }) - .reply(412, { error: { status: "FAILED_PRECONDITION" } }); - await expect( - appDistributionClient.enableAccess(releaseId, testers, groups) - ).to.be.rejectedWith(FirebaseError, "failed to add testers/groups: invalid testers"); - expect(nock.isDone()).to.be.true; - }); - - it("should throw invalid groups error when status code is INVALID_ARGUMENT", async () => { - const releaseId = "fake-release-id"; - nock(api.appDistributionOrigin) - .post(`/v1alpha/apps/${appId}/releases/${releaseId}/enable_access`, { - emails: testers, - groupIds: groups, - }) - .reply(412, { error: { status: "INVALID_ARGUMENT" } }); - await expect( - appDistributionClient.enableAccess(releaseId, testers, groups) - ).to.be.rejectedWith(FirebaseError, "failed to add testers/groups: invalid groups"); - expect(nock.isDone()).to.be.true; - }); - - it("should throw default error", async () => { - const releaseId = "fake-release-id"; - nock(api.appDistributionOrigin) - .post(`/v1alpha/apps/${appId}/releases/${releaseId}/enable_access`, { - emails: testers, - groupIds: groups, - }) - .reply(400, {}); - await expect( - appDistributionClient.enableAccess(releaseId, ["tester1"], ["group1"]) - ).to.be.rejectedWith(FirebaseError, "failed to add 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/command.spec.ts b/src/test/command.spec.ts deleted file mode 100644 index d892111d419..00000000000 --- a/src/test/command.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { expect } from "chai"; - -import { Command, validateProjectId } from "../command"; -import { FirebaseError } from "../error"; - -describe("Command", () => { - let command: Command; - - beforeEach(() => { - command = new Command("example"); - }); - - it("should allow all basic behavior", () => { - expect(() => { - command.description("description!"); - command.option("-f, --foobar", "description", "value"); - command.before( - (arr: string[]) => { - return arr; - }, - ["foo", "bar"] - ); - command.help("here's how!"); - command.action(() => { - // do nothing - }); - }).not.to.throw; - }); - - describe("runner", () => { - it("should work when no arguments are passed and options", async () => { - const run = command - .action((options) => { - options.foo = "bar"; - return options; - }) - .runner(); - - const result = run({ foo: "baz" }); - await expect(result).to.eventually.have.property("foo", "bar"); - }); - - it("should execute befores before the action", async () => { - const run = command - .before((options) => { - options.foo = true; - }) - .action((options) => { - if (options.foo) { - options.bar = "baz"; - } - return options; - }) - .runner(); - - const result = run({}); - await expect(result).to.eventually.have.property("bar"); - }); - - it("should terminate execution if a before errors", async () => { - const run = command - .before(() => { - throw new Error("foo"); - }) - .action(() => { - throw new Error("THIS IS NOT FOO"); - }) - .runner(); - - const result = run(); - return expect(result).to.be.rejectedWith("foo"); - }); - - it("should reject the promise if an error is thrown", async () => { - const run = command - .action(() => { - throw new Error("foo"); - }) - .runner(); - - const result = run(); - await expect(result).to.be.rejectedWith("foo"); - }); - }); -}); - -describe("validateProjectId", () => { - it("should not throw for valid project ids", () => { - expect(() => validateProjectId("example")).not.to.throw(); - expect(() => validateProjectId("my-project")).not.to.throw(); - expect(() => validateProjectId("myproject4fun")).not.to.throw(); - }); - - it("should not throw for legacy project ids", () => { - // The project IDs below are not technically valid, but some legacy projects - // may have IDs like that. We should not block these. - // https://cloud.google.com/resource-manager/reference/rest/v1beta1/projects#resource:-project - expect(() => validateProjectId("example-")).not.to.throw(); - expect(() => validateProjectId("0123456")).not.to.throw(); - expect(() => validateProjectId("google.com:some-project")).not.to.throw(); - }); - - it("should block invalid project ids", () => { - expect(() => validateProjectId("EXAMPLE")).to.throw(FirebaseError, /Invalid project id/); - expect(() => validateProjectId("!")).to.throw(FirebaseError, /Invalid project id/); - expect(() => validateProjectId("with space")).to.throw(FirebaseError, /Invalid project id/); - expect(() => validateProjectId(" leadingspace")).to.throw(FirebaseError, /Invalid project id/); - expect(() => validateProjectId("trailingspace ")).to.throw(FirebaseError, /Invalid project id/); - expect(() => validateProjectId("has.dot")).to.throw(FirebaseError, /Invalid project id/); - }); - - it("should error with additional note for uppercase project ids", () => { - expect(() => validateProjectId("EXAMPLE")).to.throw(FirebaseError, /lowercase/); - expect(() => validateProjectId("Example")).to.throw(FirebaseError, /lowercase/); - expect(() => validateProjectId("Example-Project")).to.throw(FirebaseError, /lowercase/); - }); -}); diff --git a/src/test/config.spec.js b/src/test/config.spec.js deleted file mode 100644 index 3a6b75c8399..00000000000 --- a/src/test/config.spec.js +++ /dev/null @@ -1,59 +0,0 @@ -"use strict"; - -var chai = require("chai"); -var expect = chai.expect; - -var 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/functions/checkRuntimeDependencies.spec.ts b/src/test/deploy/functions/checkRuntimeDependencies.spec.ts deleted file mode 100644 index 6789ce348b2..00000000000 --- a/src/test/deploy/functions/checkRuntimeDependencies.spec.ts +++ /dev/null @@ -1,133 +0,0 @@ -import * as sinon from "sinon"; -import * as nock from "nock"; -import { expect } from "chai"; - -import { logger } from "../../../logger"; -import { configstore } from "../../../configstore"; -import * as api from "../../../api"; -import { checkRuntimeDependencies } from "../../../deploy/functions/checkRuntimeDependencies"; -import { POLL_SETTINGS } from "../../../ensureApiEnabled"; - -describe("checkRuntimeDependencies()", () => { - 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); - } - - ["nodejs10", "nodejs12", "nodejs14"].forEach((runtime) => { - describe(`with ${runtime}`, () => { - describe("with cloudbuild service enabled", () => { - beforeEach(() => { - mockServiceCheck(true); - }); - - it("should succeed", async () => { - stubTimes(Date.now() - 10000, Date.now() - 5000); - - await expect(checkRuntimeDependencies("test-project", runtime)).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(checkRuntimeDependencies("test-project", runtime)).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(checkRuntimeDependencies("test-project", runtime)).to.eventually.be.rejected; - }); - }); - - 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(checkRuntimeDependencies("test-project", runtime)).to.eventually.be.rejected; - }); - }); - }); - }); -}); diff --git a/src/test/deploy/functions/deploymentPlanner.spec.ts b/src/test/deploy/functions/deploymentPlanner.spec.ts deleted file mode 100644 index 32b13a6a857..00000000000 --- a/src/test/deploy/functions/deploymentPlanner.spec.ts +++ /dev/null @@ -1,590 +0,0 @@ -import { expect } from "chai"; -import * as deploymentPlanner from "../../../deploy/functions/deploymentPlanner"; - -describe("deploymentPlanner", () => { - describe("functionsByRegion", () => { - it("should handle default region", () => { - const triggers = [ - { - name: "myFunc", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - { - name: "myOtherFunc", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ]; - - expect(deploymentPlanner.functionsByRegion("myProject", triggers)).to.deep.equal({ - "us-central1": [ - { - name: "projects/myProject/locations/us-central1/functions/myFunc", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - { - name: "projects/myProject/locations/us-central1/functions/myOtherFunc", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - }); - }); - - it("should handle customized region", () => { - const triggers = [ - { - name: "myFunc", - regions: ["us-east1"], - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - { - name: "myOtherFunc", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ]; - - expect(deploymentPlanner.functionsByRegion("myProject", triggers)).to.deep.equal({ - "us-east1": [ - { - name: "projects/myProject/locations/us-east1/functions/myFunc", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - "us-central1": [ - { - name: "projects/myProject/locations/us-central1/functions/myOtherFunc", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - }); - }); - - it("should handle multiple customized region for a function", () => { - const triggers = [ - { - name: "myFunc", - regions: ["us-east1", "eu-west1"], - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ]; - - expect(deploymentPlanner.functionsByRegion("myProject", triggers)).to.deep.equal({ - "eu-west1": [ - { - name: "projects/myProject/locations/eu-west1/functions/myFunc", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - "us-east1": [ - { - name: "projects/myProject/locations/us-east1/functions/myFunc", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - }); - }); - }); - - describe("createDeploymentPlan", () => { - it("should put new functions into functionsToCreate", () => { - const regionMap: deploymentPlanner.RegionMap = { - "us-east1": [ - { - name: "projects/a/locations/us-east1/functions/c", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - { - name: "projects/a/locations/us-east1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - "us-west1": [ - { - name: "projects/a/locations/us-west1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - }; - const existingFunctions: deploymentPlanner.CloudFunctionTrigger[] = []; - const filters: string[][] = []; - - const deploymentPlan = deploymentPlanner.createDeploymentPlan( - regionMap, - existingFunctions, - filters - ); - - const expected: deploymentPlanner.DeploymentPlan = { - regionalDeployments: [ - { - region: "us-east1", - functionsToCreate: [ - { - name: "projects/a/locations/us-east1/functions/c", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - { - name: "projects/a/locations/us-east1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - functionsToUpdate: [], - schedulesToUpsert: [], - }, - { - region: "us-west1", - functionsToCreate: [ - { - name: "projects/a/locations/us-west1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - functionsToUpdate: [], - schedulesToUpsert: [], - }, - ], - functionsToDelete: [], - schedulesToDelete: [], - }; - expect(deploymentPlan).to.deep.equal(expected); - }); - - it("should put existing functions being deployed into functionsToUpdate", () => { - const regionMap: deploymentPlanner.RegionMap = { - "us-east1": [ - { - name: "projects/a/locations/us-east1/functions/c", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - { - name: "projects/a/locations/us-east1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - "us-west1": [ - { - name: "projects/a/locations/us-west1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - }; - const existingFunctions: deploymentPlanner.CloudFunctionTrigger[] = [ - { - name: "projects/a/locations/us-east1/functions/c", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - { - name: "projects/a/locations/us-east1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - { - name: "projects/a/locations/us-west1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ]; - const filters: string[][] = []; - - const deploymentPlan = deploymentPlanner.createDeploymentPlan( - regionMap, - existingFunctions, - filters - ); - - const expected: deploymentPlanner.DeploymentPlan = { - regionalDeployments: [ - { - region: "us-east1", - functionsToCreate: [], - functionsToUpdate: [ - { - name: "projects/a/locations/us-east1/functions/c", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - { - name: "projects/a/locations/us-east1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - schedulesToUpsert: [], - }, - { - region: "us-west1", - functionsToCreate: [], - functionsToUpdate: [ - { - name: "projects/a/locations/us-west1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - schedulesToUpsert: [], - }, - ], - functionsToDelete: [], - schedulesToDelete: [], - }; - expect(deploymentPlan).to.deep.equal(expected); - }); - - it("should delete existing functions not in local code, only if they were deployed via CLI", () => { - const regionMap: deploymentPlanner.RegionMap = {}; - const existingFunctions: deploymentPlanner.CloudFunctionTrigger[] = [ - { - name: "projects/a/locations/us-east1/functions/c", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - { - name: "projects/a/locations/us-east1/functions/d", - labels: { - "deployment-tool": "cli-firebase", - }, - environmentVariables: {}, - entryPoint: "", - }, - { - name: "projects/a/locations/us-west1/functions/d", - labels: { - "deployment-tool": "cli-firebase", - }, - environmentVariables: {}, - entryPoint: "", - }, - ]; - const filters: string[][] = []; - - const deploymentPlan = deploymentPlanner.createDeploymentPlan( - regionMap, - existingFunctions, - filters - ); - - const expected: deploymentPlanner.DeploymentPlan = { - regionalDeployments: [], - functionsToDelete: [ - "projects/a/locations/us-east1/functions/d", - "projects/a/locations/us-west1/functions/d", - ], - schedulesToDelete: [], - }; - expect(deploymentPlan).to.deep.equal(expected); - }); - - it("should create schedules for new or updated scheduled functions", () => { - const regionMap: deploymentPlanner.RegionMap = { - "us-east1": [ - { - name: "projects/a/locations/us-east1/functions/c", - labels: {}, - environmentVariables: {}, - entryPoint: "", - schedule: { schedule: "every 20 minutes" }, - eventTrigger: {}, - }, - { - name: "projects/a/locations/us-east1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - schedule: { schedule: "every 5 minutes" }, - eventTrigger: {}, - }, - ], - "us-west1": [ - { - name: "projects/a/locations/us-west1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - schedule: { schedule: "every 5 minutes" }, - eventTrigger: {}, - }, - ], - }; - const existingFunctions: deploymentPlanner.CloudFunctionTrigger[] = [ - { - name: "projects/a/locations/us-east1/functions/c", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ]; - const filters: string[][] = []; - - const deploymentPlan = deploymentPlanner.createDeploymentPlan( - regionMap, - existingFunctions, - filters - ); - - const expected: deploymentPlanner.DeploymentPlan = { - regionalDeployments: [ - { - region: "us-east1", - functionsToCreate: [ - { - name: "projects/a/locations/us-east1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - schedule: { schedule: "every 5 minutes" }, - eventTrigger: { resource: "projects/a/topics/firebase-schedule-d-us-east1" }, - }, - ], - functionsToUpdate: [ - { - name: "projects/a/locations/us-east1/functions/c", - labels: {}, - environmentVariables: {}, - entryPoint: "", - schedule: { schedule: "every 20 minutes" }, - eventTrigger: { resource: "projects/a/topics/firebase-schedule-c-us-east1" }, - }, - ], - schedulesToUpsert: [ - { - name: "projects/a/locations/us-east1/functions/c", - labels: {}, - environmentVariables: {}, - entryPoint: "", - schedule: { schedule: "every 20 minutes" }, - eventTrigger: { resource: "projects/a/topics/firebase-schedule-c-us-east1" }, - }, - { - name: "projects/a/locations/us-east1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - schedule: { schedule: "every 5 minutes" }, - eventTrigger: { resource: "projects/a/topics/firebase-schedule-d-us-east1" }, - }, - ], - }, - { - region: "us-west1", - functionsToCreate: [ - { - name: "projects/a/locations/us-west1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - schedule: { schedule: "every 5 minutes" }, - eventTrigger: { resource: "projects/a/topics/firebase-schedule-d-us-west1" }, - }, - ], - functionsToUpdate: [], - schedulesToUpsert: [ - { - name: "projects/a/locations/us-west1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - schedule: { schedule: "every 5 minutes" }, - eventTrigger: { resource: "projects/a/topics/firebase-schedule-d-us-west1" }, - }, - ], - }, - ], - functionsToDelete: [], - schedulesToDelete: [], - }; - expect(deploymentPlan).to.deep.equal(expected); - }); - - it("should delete schedules if the function is deleted or updated to another type", () => { - const regionMap: deploymentPlanner.RegionMap = { - "us-east1": [ - { - name: "projects/a/locations/us-east1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - }; - const existingFunctions: deploymentPlanner.CloudFunctionTrigger[] = [ - { - name: "projects/a/locations/us-east1/functions/c", - labels: { - "deployment-tool": "cli-firebase", - "deployment-scheduled": "true", - }, - environmentVariables: {}, - entryPoint: "", - }, - { - name: "projects/a/locations/us-east1/functions/d", - labels: { - "deployment-tool": "cli-firebase", - "deployment-scheduled": "true", - }, - environmentVariables: {}, - entryPoint: "", - }, - ]; - const filters: string[][] = []; - - const deploymentPlan = deploymentPlanner.createDeploymentPlan( - regionMap, - existingFunctions, - filters - ); - - const expected: deploymentPlanner.DeploymentPlan = { - regionalDeployments: [ - { - region: "us-east1", - functionsToCreate: [], - functionsToUpdate: [ - { - name: "projects/a/locations/us-east1/functions/d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - schedulesToUpsert: [], - }, - ], - functionsToDelete: ["projects/a/locations/us-east1/functions/c"], - schedulesToDelete: [ - "projects/a/locations/us-east1/functions/d", - "projects/a/locations/us-east1/functions/c", - ], - }; - expect(deploymentPlan).to.deep.equal(expected); - }); - it("should only create, update, and delete matching functions if filters are passed in.", () => { - const regionMap: deploymentPlanner.RegionMap = { - "us-east1": [ - { - name: "projects/a/locations/us-east1/functions/group-d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - { - name: "projects/a/locations/us-east1/functions/group-a", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - }; - const existingFunctions: deploymentPlanner.CloudFunctionTrigger[] = [ - { - name: "projects/a/locations/us-east1/functions/group-c", - labels: { - "deployment-tool": "cli-firebase", - "deployment-scheduled": "true", - }, - environmentVariables: {}, - entryPoint: "", - }, - { - name: "projects/a/locations/us-east1/functions/group-d", - labels: { - "deployment-tool": "cli-firebase", - }, - environmentVariables: {}, - entryPoint: "", - }, - { - name: "projects/a/locations/us-east1/functions/differentGroup-a", - labels: { - "deployment-tool": "cli-firebase", - "deployment-scheduled": "true", - }, - environmentVariables: {}, - entryPoint: "", - }, - ]; - const filters: string[][] = [["group"]]; - - const deploymentPlan = deploymentPlanner.createDeploymentPlan( - regionMap, - existingFunctions, - filters - ); - - const expected: deploymentPlanner.DeploymentPlan = { - regionalDeployments: [ - { - region: "us-east1", - functionsToCreate: [ - { - name: "projects/a/locations/us-east1/functions/group-a", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - functionsToUpdate: [ - { - name: "projects/a/locations/us-east1/functions/group-d", - labels: {}, - environmentVariables: {}, - entryPoint: "", - }, - ], - schedulesToUpsert: [], - }, - ], - functionsToDelete: ["projects/a/locations/us-east1/functions/group-c"], - schedulesToDelete: ["projects/a/locations/us-east1/functions/group-c"], - }; - expect(deploymentPlan).to.deep.equal(expected); - }); - }); -}); diff --git a/src/test/deploy/functions/prompts.spec.ts b/src/test/deploy/functions/prompts.spec.ts deleted file mode 100644 index a6f173c6943..00000000000 --- a/src/test/deploy/functions/prompts.spec.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import * as prompt from "../../../prompt"; -import * as functionPrompts from "../../../deploy/functions/prompts"; -import { FirebaseError } from "../../../error"; -import { CloudFunctionTrigger } from "../../../deploy/functions/deploymentPlanner"; -import * as gcp from "../../../gcp"; - -describe("promptForFailurePolicies", () => { - let promptStub: sinon.SinonStub; - let listAllFunctionsStub: sinon.SinonStub; - let existingFunctions: CloudFunctionTrigger[] = []; - - beforeEach(() => { - promptStub = sinon.stub(prompt, "promptOnce"); - listAllFunctionsStub = sinon.stub(gcp.cloudfunctions, "listAllFunctions").callsFake(() => { - return Promise.resolve(existingFunctions); - }); - }); - - afterEach(() => { - promptStub.restore(); - listAllFunctionsStub.restore(); - existingFunctions = []; - }); - - it("should prompt if there are new functions with failure policies", async () => { - const funcs: CloudFunctionTrigger[] = [ - { - name: "projects/a/locations/b/functions/c", - entryPoint: "", - labels: {}, - environmentVariables: {}, - failurePolicy: {}, - }, - ]; - const options = {}; - const context = {}; - promptStub.resolves(true); - - await expect(functionPrompts.promptForFailurePolicies(context, options, funcs)).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 () => { - // Note: local definitions of function triggers use a top-level "failurePolicy" but - // the API returns eventTrigger.failurePolicy. - const func: any = { - name: "projects/a/locations/b/functions/c", - entryPoint: "", - labels: {}, - environmentVariables: {}, - failurePolicy: {}, - eventTrigger: { - failurePolicy: {}, - }, - }; - existingFunctions = [func]; - const options = {}; - const context = {}; - - await expect(functionPrompts.promptForFailurePolicies(context, options, [func])).to.eventually - .be.fulfilled; - expect(promptStub).to.not.have.been.called; - }); - - it("should throw if user declines the prompt", async () => { - const funcs: CloudFunctionTrigger[] = [ - { - name: "projects/a/locations/b/functions/c", - entryPoint: "", - labels: {}, - environmentVariables: {}, - failurePolicy: {}, - }, - ]; - const options = {}; - const context = {}; - promptStub.resolves(false); - - await expect( - functionPrompts.promptForFailurePolicies(context, options, funcs) - ).to.eventually.be.rejectedWith(FirebaseError, /Deployment canceled/); - expect(promptStub).to.have.been.calledOnce; - }); - - it("should propmt if an existing function adds a failure policy", async () => { - const func: CloudFunctionTrigger = { - name: "projects/a/locations/b/functions/c", - entryPoint: "", - labels: {}, - environmentVariables: {}, - }; - existingFunctions = [func]; - const newFunc = Object.assign({}, func, { failurePolicy: {} }); - const options = {}; - const context = {}; - promptStub.resolves(true); - - await expect(functionPrompts.promptForFailurePolicies(context, options, [newFunc])).to - .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 funcs: CloudFunctionTrigger[] = [ - { - name: "projects/a/locations/b/functions/c", - entryPoint: "", - labels: {}, - environmentVariables: {}, - failurePolicy: {}, - }, - ]; - const options = {}; - const context = {}; - promptStub.resolves(false); - - await expect( - functionPrompts.promptForFailurePolicies(context, options, funcs) - ).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 funcs: CloudFunctionTrigger[] = [ - { - name: "projects/a/locations/b/functions/c", - entryPoint: "", - labels: {}, - environmentVariables: {}, - }, - ]; - const options = {}; - const context = {}; - promptStub.resolves(); - - await expect(functionPrompts.promptForFailurePolicies(context, options, funcs)).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 funcs: CloudFunctionTrigger[] = [ - { - name: "projects/a/locations/b/functions/c", - entryPoint: "", - labels: {}, - environmentVariables: {}, - failurePolicy: {}, - }, - ]; - const options = { nonInteractive: true }; - - await expect( - functionPrompts.promptForFailurePolicies(context, options, funcs) - ).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 funcs: CloudFunctionTrigger[] = [ - { - name: "projects/a/locations/b/functions/c", - entryPoint: "", - labels: {}, - environmentVariables: {}, - failurePolicy: {}, - }, - ]; - const options = { nonInteractive: true, force: true }; - - await expect(functionPrompts.promptForFailurePolicies(context, options, funcs)).to.eventually.be - .fulfilled; - expect(promptStub).not.to.have.been.called; - }); -}); diff --git a/src/test/deploy/functions/tasks.spec.ts b/src/test/deploy/functions/tasks.spec.ts deleted file mode 100644 index 8db0941eff9..00000000000 --- a/src/test/deploy/functions/tasks.spec.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import * as tasks from "../../../deploy/functions/tasks"; -import { DeploymentTimer } from "../../../deploy/functions/deploymentTimer"; -import { ErrorHandler } from "../../../deploy/functions/errorHandler"; -import { FirebaseError } from "../../../error"; - -describe("Function Deployment tasks", () => { - describe("functionsDeploymentHandler", () => { - let sandbox: sinon.SinonSandbox; - let timerStub: sinon.SinonStubbedInstance; - let errorHandlerStub: sinon.SinonStubbedInstance; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - timerStub = sandbox.createStubInstance(DeploymentTimer); - errorHandlerStub = sandbox.createStubInstance(ErrorHandler); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it("should execute the task and time it", async () => { - const run = sinon.spy(); - const functionName = "myFunc"; - const testTask: tasks.DeploymentTask = { - run, - functionName: functionName, - operationType: "create", - }; - - const handler = tasks.functionsDeploymentHandler(timerStub, errorHandlerStub); - await handler(testTask); - - expect(timerStub.startTimer).to.have.been.calledWith(functionName); - expect(run).to.have.been.called; - expect(timerStub.endTimer).to.have.been.calledWith(functionName); - expect(errorHandlerStub.record).not.to.have.been.called; - }); - - it("should throw quota errors", async () => { - const originalError = { - name: "Quota Exceeded", - message: "an error occurred", - context: { - response: { - statusCode: 429, - }, - }, - }; - const run = sinon.spy(() => { - throw new FirebaseError("an error occurred", { - original: originalError, - }); - }); - const functionName = "myFunc"; - const testTask: tasks.DeploymentTask = { - run, - functionName: functionName, - operationType: "create", - }; - - const handler = tasks.functionsDeploymentHandler(timerStub, errorHandlerStub); - - await expect(handler(testTask)).to.eventually.be.rejected; - - expect(run).to.have.been.called; - expect(errorHandlerStub.record).not.to.have.been.called; - }); - - it("should handle other errors", async () => { - const originalError = { - name: "Some Other Error", - message: "an error occurred", - context: { - response: { - statusCode: 500, - }, - }, - }; - const run = sinon.spy(() => { - throw new FirebaseError("an error occurred", { - original: originalError, - }); - }); - const functionName = "myFunc"; - const testTask: tasks.DeploymentTask = { - run, - functionName: functionName, - operationType: "create", - }; - - const handler = tasks.functionsDeploymentHandler(timerStub, errorHandlerStub); - await handler(testTask); - - expect(timerStub.startTimer).to.have.been.calledWith(functionName); - expect(run).to.have.been.called; - expect(timerStub.endTimer).to.have.been.calledWith(functionName); - expect(errorHandlerStub.record).to.have.been.calledWith("error", functionName, "create"); - }); - }); - - describe("schedulerDeploymentHandler", () => { - const sandbox = sinon.createSandbox(); - let errorHandlerStub: sinon.SinonStubbedInstance; - - beforeEach(() => { - errorHandlerStub = sandbox.createStubInstance(ErrorHandler); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it("should execute the task", async () => { - const run = sinon.spy(); - const testTask: tasks.DeploymentTask = { - run, - functionName: "myFunc", - operationType: "upsert schedule", - }; - - const handler = tasks.schedulerDeploymentHandler(errorHandlerStub); - await handler(testTask); - - expect(run).to.have.been.called; - expect(errorHandlerStub.record).not.to.have.been.called; - }); - - it("should throw quota errors", async () => { - const run = sinon.spy(() => { - throw new FirebaseError("an error occurred", { - status: 429, - }); - }); - const testTask: tasks.DeploymentTask = { - run, - functionName: "myFunc", - operationType: "upsert schedule", - }; - - const handler = tasks.schedulerDeploymentHandler(errorHandlerStub); - await expect(handler(testTask)).to.eventually.be.rejected; - - expect(run).to.have.been.called; - expect(errorHandlerStub.record).not.to.have.been.called; - }); - - it("should ignore 404 errors", async () => { - const run = sinon.spy(() => { - throw new FirebaseError("an error occurred", { - status: 404, - }); - }); - const testTask: tasks.DeploymentTask = { - run, - functionName: "myFunc", - operationType: "upsert schedule", - }; - - const handler = tasks.schedulerDeploymentHandler(errorHandlerStub); - await handler(testTask); - - expect(run).to.have.been.called; - expect(errorHandlerStub.record).not.to.have.been.called; - }); - - it("should handle other errors", async () => { - const run = sinon.spy(() => { - throw new FirebaseError("an error occurred", { - status: 500, - }); - }); - const functionName = "myFunc"; - const testTask: tasks.DeploymentTask = { - run, - functionName: functionName, - operationType: "upsert schedule", - }; - - const handler = tasks.schedulerDeploymentHandler(errorHandlerStub); - await handler(testTask); - - expect(run).to.have.been.called; - expect(errorHandlerStub.record).to.have.been.calledWith( - "error", - functionName, - "upsert schedule" - ); - }); - }); -}); diff --git a/src/test/deploy/functions/validate.spec.ts b/src/test/deploy/functions/validate.spec.ts deleted file mode 100644 index 6719270ade3..00000000000 --- a/src/test/deploy/functions/validate.spec.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { expect } from "chai"; -import * as fsutils from "../../../fsutils"; -import * as validate from "../../../deploy/functions/validate"; -import * as projectPath from "../../../projectPath"; -import { FirebaseError } from "../../../error"; -import * as sinon from "sinon"; -import { RUNTIME_NOT_SET } from "../../../parseRuntimeAndValidateSDK"; -import { CloudFunctionTrigger } from "../../../deploy/functions/deploymentPlanner"; - -// have to require this because no @types/cjson available -// tslint:disable-next-line -const cjson = require("cjson"); - -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 properNames = { "my-function-1": "some field", "my-function-2": "some field" }; - - expect(() => { - validate.functionNamesAreValid(properNames); - }).to.not.throw(); - }); - - it("should throw error on improperly formatted function names", () => { - const properNames = { - "my-function-!@#$%": "some field", - "my-function-!@#$!@#": "some field", - }; - - expect(() => { - validate.functionNamesAreValid(properNames); - }).to.throw(FirebaseError); - }); - - it("should throw error if some function names are improperly formatted", () => { - const properNames = { "my-function$%#": "some field", "my-function-2": "some field" }; - - expect(() => { - validate.functionNamesAreValid(properNames); - }).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 properNames = {}; - - expect(() => { - validate.functionNamesAreValid(properNames); - }).to.throw(FirebaseError); - }); - }); - - describe("checkForInvalidChangeOfTrigger", () => { - it("should throw if a https function would be changed into an event triggered function", () => { - const fn: CloudFunctionTrigger = { - name: "projects/proj/locations/us-central1/functions/my-func", - labels: {}, - environmentVariables: {}, - entryPoint: ".", - eventTrigger: { - service: "foo", - }, - }; - const exFn: CloudFunctionTrigger = { - name: "projects/proj/locations/us-central1/functions/my-func", - labels: {}, - environmentVariables: {}, - entryPoint: ".", - httpsTrigger: {}, - }; - - expect(() => { - validate.checkForInvalidChangeOfTrigger(fn, exFn); - }).to.throw(); - }); - - it("should throw if a event triggered function would be changed into an https function", () => { - const fn: CloudFunctionTrigger = { - name: "projects/proj/locations/us-central1/functions/my-func", - labels: {}, - environmentVariables: {}, - entryPoint: ".", - httpsTrigger: {}, - }; - const exFn: CloudFunctionTrigger = { - name: "projects/proj/locations/us-central1/functions/my-func", - labels: {}, - environmentVariables: {}, - entryPoint: ".", - eventTrigger: { - service: "foo", - }, - }; - - expect(() => { - validate.checkForInvalidChangeOfTrigger(fn, exFn); - }).to.throw(); - }); - - it("should throw if a event triggered function would have its service changed", () => { - const fn: CloudFunctionTrigger = { - name: "projects/proj/locations/us-central1/functions/my-func", - labels: {}, - environmentVariables: {}, - entryPoint: ".", - eventTrigger: { - service: "bar", - }, - }; - const exFn: CloudFunctionTrigger = { - name: "projects/proj/locations/us-central1/functions/my-func", - labels: {}, - environmentVariables: {}, - entryPoint: ".", - eventTrigger: { - service: "foo", - }, - }; - - expect(() => { - validate.checkForInvalidChangeOfTrigger(fn, exFn); - }).to.throw(); - }); - - it("should not throw if a event triggered function keeps the same trigger", () => { - const fn: CloudFunctionTrigger = { - name: "projects/proj/locations/us-central1/functions/my-func", - labels: {}, - environmentVariables: {}, - entryPoint: ".", - eventTrigger: { - service: "foo", - }, - }; - const exFn: CloudFunctionTrigger = { - name: "projects/proj/locations/us-central1/functions/my-func", - labels: {}, - environmentVariables: {}, - entryPoint: ".", - eventTrigger: { - service: "foo", - }, - }; - - expect(() => { - validate.checkForInvalidChangeOfTrigger(fn, exFn); - }).not.to.throw(); - }); - - it("should not throw if a https function stays as a https function", () => { - const fn: CloudFunctionTrigger = { - name: "projects/proj/locations/us-central1/functions/my-func", - labels: {}, - environmentVariables: {}, - entryPoint: ".", - httpsTrigger: {}, - }; - const exFn: CloudFunctionTrigger = { - name: "projects/proj/locations/us-central1/functions/my-func", - labels: {}, - environmentVariables: {}, - entryPoint: ".", - httpsTrigger: {}, - }; - - expect(() => { - validate.checkForInvalidChangeOfTrigger(fn, exFn); - }).not.to.throw(); - }); - }); - - 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", false); - }).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", false); - }).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", false); - }).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", true); - }).to.not.throw(); - }); - - context("runtime is not set in the config", () => { - it("should throw error if runtime is not 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", false); - }).to.throw(FirebaseError, RUNTIME_NOT_SET); - }); - - it("should throw error if engines field is set but node field missing", () => { - cjsonLoadStub.returns({ name: "my-project", engines: {} }); - fileExistsStub.withArgs("sourceDir/package.json").returns(true); - fileExistsStub.withArgs("sourceDir/index.js").returns(true); - - expect(() => { - validate.packageJsonIsValid("sourceDirName", "sourceDir", "projectDir", false); - }).to.throw(FirebaseError, RUNTIME_NOT_SET); - }); - - it("should not throw error if package.json, functions file exists and engines present", () => { - cjsonLoadStub.returns({ name: "my-project", engines: { node: "8" } }); - fileExistsStub.withArgs("sourceDir/package.json").returns(true); - fileExistsStub.withArgs("sourceDir/index.js").returns(true); - - expect(() => { - validate.packageJsonIsValid("sourceDirName", "sourceDir", "projectDir", false); - }).to.not.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/createAuthUri.spec.ts b/src/test/emulators/auth/createAuthUri.spec.ts deleted file mode 100644 index 626e8962dc3..00000000000 --- a/src/test/emulators/auth/createAuthUri.spec.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { expect } from "chai"; -import { PROVIDER_PASSWORD, SIGNIN_METHOD_EMAIL_LINK } from "../../../emulator/auth/state"; -import { describeAuthEmulator } from "./setup"; -import { - expectStatusCode, - registerUser, - signInWithFakeClaims, - signInWithEmailLink, - updateProjectConfig, -} from "./helpers"; - -describeAuthEmulator("accounts:createAuthUri", ({ authApi }) => { - it("should report not registered user as not registered", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri") - .send({ continueUri: "http://example.com/", identifier: "notregistered@example.com" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).to.have.property("registered").equals(false); - expect(res.body).to.have.property("sessionId").that.is.a("string"); - }); - }); - - it("should return providers for a registered user", async () => { - const user = { email: "alice@example.com", password: "notasecret" }; - await registerUser(authApi(), user); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri") - .send({ continueUri: "http://example.com/", identifier: user.email }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).to.have.property("registered").equals(true); - expect(res.body).to.have.property("allProviders").eql(["password"]); - expect(res.body).to.have.property("signinMethods").eql(["password"]); - expect(res.body).to.have.property("sessionId").that.is.a("string"); - }); - }); - - it("should return existing sessionId if provided", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri") - .send({ - continueUri: "http://example.com/", - identifier: "notregistered@example.com", - sessionId: "my-session-1", - }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).to.have.property("registered").equals(false); - expect(res.body).to.have.property("sessionId").equals("my-session-1"); - }); - }); - - it("should find user by email ignoring case", async () => { - const user = { email: "alice@example.com", password: "notasecret" }; - await registerUser(authApi(), user); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri") - .send({ continueUri: "http://example.com/", identifier: "AlIcE@exAMPle.COM" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.registered).equals(true); - }); - }); - - it("should find user by either IDP email or 'top-level' email", async () => { - const email = "bob@example.com"; - const emailAtProvider = "alice@example.com"; - const providerId = "google.com"; - - const { idToken } = await signInWithFakeClaims(authApi(), providerId, { - sub: "12345", - email: emailAtProvider, - }); - await signInWithEmailLink(authApi(), email, idToken); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri") - .send({ continueUri: "http://example.com/", identifier: email }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.registered).to.equal(true); - expect(res.body.allProviders).to.have.members([PROVIDER_PASSWORD, providerId]); - expect(res.body.signinMethods).to.have.members([SIGNIN_METHOD_EMAIL_LINK, providerId]); - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri") - .send({ continueUri: "http://example.com/", identifier: emailAtProvider }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.registered).to.equal(true); - expect(res.body.allProviders).to.have.members([PROVIDER_PASSWORD, providerId]); - expect(res.body.signinMethods).to.have.members([SIGNIN_METHOD_EMAIL_LINK, providerId]); - }); - }); - - it("should not list IDP sign-in methods when allowDuplicateEmails", async () => { - const email = "bob@example.com"; - const emailAtProvider = "alice@example.com"; - const providerId = "google.com"; - - const { idToken } = await signInWithFakeClaims(authApi(), providerId, { - sub: "12345", - email: emailAtProvider, - }); - await signInWithEmailLink(authApi(), email, idToken); - - await updateProjectConfig(authApi(), { signIn: { allowDuplicateEmails: true } }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri") - .send({ continueUri: "http://example.com/", identifier: email }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.registered).to.equal(true); - expect(res.body.allProviders).to.have.members([PROVIDER_PASSWORD]); - expect(res.body.signinMethods).to.have.members([SIGNIN_METHOD_EMAIL_LINK]); - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri") - .send({ continueUri: "http://example.com/", identifier: emailAtProvider }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.registered).to.equal(true); - expect(res.body.allProviders).to.have.members([PROVIDER_PASSWORD]); - expect(res.body.signinMethods).to.have.members([SIGNIN_METHOD_EMAIL_LINK]); - }); - }); - - it("should error if identifier or continueUri is not provided", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri") - .send({ - /* no identifier */ - continueUri: "http://example.com/", - }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("MISSING_IDENTIFIER"); - }); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri") - .send({ - identifier: "me@example.com", - }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("MISSING_CONTINUE_URI"); - }); - }); - - it("should error if identifier is invalid", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri") - .send({ identifier: "invalid", continueUri: "http://localhost/" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("INVALID_IDENTIFIER"); - }); - }); - - it("should error if continueUri is invalid", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri") - .send({ identifier: "me@example.com", continueUri: "invalid" }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("INVALID_CONTINUE_URI"); - }); - }); -}); diff --git a/src/test/emulators/auth/emailLink.spec.ts b/src/test/emulators/auth/emailLink.spec.ts deleted file mode 100644 index 25e3b24a808..00000000000 --- a/src/test/emulators/auth/emailLink.spec.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { expect } from "chai"; -import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; -import { FirebaseJwtPayload } from "../../../emulator/auth/operations"; -import { describeAuthEmulator } from "./setup"; -import { - expectStatusCode, - registerUser, - signInWithPhoneNumber, - updateAccountByLocalId, - getSigninMethods, - inspectOobs, - createEmailSignInOob, - TEST_PHONE_NUMBER, - TEST_MFA_INFO, -} 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 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 error 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(501, res); - expect(res.body.error.message).to.equal("MFA Login not yet implemented."); - }); - }); -}); diff --git a/src/test/emulators/auth/helpers.ts b/src/test/emulators/auth/helpers.ts deleted file mode 100644 index e6edbdabadb..00000000000 --- a/src/test/emulators/auth/helpers.ts +++ /dev/null @@ -1,340 +0,0 @@ -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, UserInfo } from "../../../emulator/auth/state"; -import { TestAgent, PROJECT_ID } from "./setup"; -import { MfaEnrollments } from "../../../emulator/auth/types"; - -export { PROJECT_ID }; -export const TEST_PHONE_NUMBER = "+15555550100"; -export const TEST_PHONE_NUMBER_2 = "+15555550101"; -export const TEST_PHONE_NUMBER_3 = "+15555550102"; -export const TEST_MFA_INFO = { - displayName: "Cell Phone", - phoneInfo: TEST_PHONE_NUMBER, -}; -export const TEST_INVALID_PHONE_NUMBER = "5555550100"; /* no country code */ -export const FAKE_GOOGLE_ACCOUNT = { - displayName: "Example User", - email: "example@gmail.com", - emailVerified: true, - rawId: "123456789012345678901", - // An unsigned token, with payload format similar to a real one. - idToken: - "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiMjI4NzQ2ODI4NDQtYjBzOHM3NWIzaWVkYjJtZDRobHMydm9xNnNsbGJzbTMuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIyMjg3NDY4Mjg0NC1iMHM4czc1YjNpZWRiMm1kNGhsczJ2b3E2c2xsYnNtMy5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjEyMzQ1Njc4OTAxMjM0NTY3ODkwMSIsImVtYWlsIjoiZXhhbXBsZUBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXRfaGFzaCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAiLCJpYXQiOjE1OTc4ODI2ODEsImV4cCI6MTU5Nzg4NjI4MX0.", - // Same as above, except with no email included. - idTokenNoEmail: - "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiMjI4NzQ2ODI4NDQtYjBzOHM3NWIzaWVkYjJtZDRobHMydm9xNnNsbGJzbTMuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIyMjg3NDY4Mjg0NC1iMHM4czc1YjNpZWRiMm1kNGhsczJ2b3E2c2xsYnNtMy5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjEyMzQ1Njc4OTAxMjM0NTY3ODkwMSIsImF0X2hhc2giOiIwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwiaWF0IjoxNTk3ODgyNjgxLCJleHAiOjE1OTc4ODYyODF9.", -}; - -// This is a real Google test account (go/rhea), owned and managed by a Googler. -// However, nobody needs to actually sign-in using this account -- no tests -// below requires actual Google sign-in, and the Auth Emulator doesn't validate. -// If for some reason the account or idToken below doesn't fit our testing need -// anymore, create a new test account and token. Don't ping anyone for password. -export const REAL_GOOGLE_ACCOUNT = { - displayName: "Oberyn Baelish", - email: "oberynbaelish.331826@gmail.com", - emailVerified: true, - rawId: "115113236566683398301", - photoUrl: - "https://lh3.googleusercontent.com/-KNaMyFnKZ9o/AAAAAAAAAAI/AAAAAAAAAAA/AMZuucnZC9bn4HcT-8bQka3uG3lUYd4lSA/photo.jpg", - // ID Tokens below are also real, but their signatures has been zero'd out and - // have expired long ago, so they are safe to use as examples in tests below. - idToken: - "eyJhbGciOiJSUzI1NiIsImtpZCI6IjZiYzYzZTlmMThkNTYxYjM0ZjU2NjhmODhhZTI3ZDQ4ODc2ZDgwNzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiMjI4NzQ2ODI4NDQtYjBzOHM3NWIzaWVkYjJtZDRobHMydm9xNnNsbGJzbTMuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIyMjg3NDY4Mjg0NC1iMHM4czc1YjNpZWRiMm1kNGhsczJ2b3E2c2xsYnNtMy5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjExNTExMzIzNjU2NjY4MzM5ODMwMSIsImVtYWlsIjoib2JlcnluYmFlbGlzaC4zMzE4MjZAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiJXNTlTOEs4Y3g0Y3hYYmh0YmFXYndBIiwiaWF0IjoxNTk3ODgyNjgxLCJleHAiOjE1OTc4ODYyODF9.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - idTokenNoEmail: - "eyJhbGciOiJSUzI1NiIsImtpZCI6IjZiYzYzZTlmMThkNTYxYjM0ZjU2NjhmODhhZTI3ZDQ4ODc2ZDgwNzMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiMjI4NzQ2ODI4NDQtYjBzOHM3NWIzaWVkYjJtZDRobHMydm9xNnNsbGJzbTMuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiIyMjg3NDY4Mjg0NC1iMHM4czc1YjNpZWRiMm1kNGhsczJ2b3E2c2xsYnNtMy5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjExNTExMzIzNjU2NjY4MzM5ODMwMSIsImF0X2hhc2giOiJJRHA0UFFldFItLUFyaWhXX2NYMmd3IiwiaWF0IjoxNTk3ODgyNDQyLCJleHAiOjE1OTc4ODYwNDJ9.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", -}; - -/** - * Asserts that the response has the expected status code. - * @param expected the expected status code - * @param res the supertest Response - */ -export function expectStatusCode(expected: number, res: supertest.Response): void { - if (res.status !== expected) { - const body = inspect(res.body); - throw new AssertionError( - `expected ${expected} "${STATUS_CODES[expected]}", got ${res.status} "${ - STATUS_CODES[res.status] - }", with response body:\n${body}` - ); - } -} - -/** - * Create a fake claims object with some default field values plus custom ones. - * @param input custom field values - * @return a complete claims plain JS object - */ -export function fakeClaims(input: Partial & { sub: string }): IdpJwtPayload { - return Object.assign( - { - iss: "example.com", - aud: "example.com", - exp: 1597974008, - iat: 1597970408, - }, - input - ); -} - -/* eslint-disable jsdoc/require-jsdoc */ -// Most functions below are self-documenting test helpers. - -export function registerUser( - testAgent: TestAgent, - user: { - email: string; - password: string; - displayName?: string; - mfaInfo?: MfaEnrollments; - } -): Promise<{ idToken: string; localId: string; refreshToken: string; email: string }> { - return testAgent - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send(user) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - return { - idToken: res.body.idToken, - localId: res.body.localId, - refreshToken: res.body.refreshToken, - email: res.body.email, - }; - }); -} - -export function registerAnonUser( - testAgent: TestAgent -): Promise<{ idToken: string; localId: string; refreshToken: string }> { - return testAgent - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .send({ returnSecureToken: true }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - return { - idToken: res.body.idToken, - localId: res.body.localId, - refreshToken: res.body.refreshToken, - }; - }); -} - -export async function signInWithEmailLink( - testAgent: TestAgent, - email: string, - idTokenToLink?: string -): Promise<{ idToken: string; localId: string; refreshToken: string; email: string }> { - const { oobCode } = await createEmailSignInOob(testAgent, email); - - return testAgent - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink") - .query({ key: "fake-api-key" }) - .send({ email, oobCode, idToken: idTokenToLink }) - .then((res) => { - return { - idToken: res.body.idToken, - localId: res.body.localId, - refreshToken: res.body.refreshToken, - email, - }; - }); -} - -export function signInWithPassword( - testAgent: TestAgent, - email: string, - password: string -): Promise<{ idToken: string; localId: string; refreshToken: string; email: string }> { - return testAgent - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword") - .send({ email, password }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - return { - idToken: res.body.idToken, - localId: res.body.localId, - refreshToken: res.body.refreshToken, - email: res.body.email, - }; - }); -} - -export async function signInWithPhoneNumber( - testAgent: TestAgent, - phoneNumber: string -): Promise<{ idToken: string; localId: string; refreshToken: string }> { - const sessionInfo = await testAgent - .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(testAgent); - - return testAgent - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithPhoneNumber") - .query({ key: "fake-api-key" }) - .send({ sessionInfo, code: codes[0].code }) - .then((res) => { - expectStatusCode(200, res); - return { - idToken: res.body.idToken, - localId: res.body.localId, - refreshToken: res.body.refreshToken, - }; - }); -} - -export function signInWithFakeClaims( - testAgent: TestAgent, - providerId: string, - claims: Partial & { sub: string } -): Promise<{ idToken: string; localId: string; refreshToken: string; email?: string }> { - const fakeIdToken = JSON.stringify(fakeClaims(claims)); - return testAgent - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") - .query({ key: "fake-api-key" }) - .send({ - postBody: `providerId=${encodeURIComponent(providerId)}&id_token=${encodeURIComponent( - fakeIdToken - )}`, - requestUri: "http://localhost", - returnIdpCredential: true, - returnSecureToken: true, - }) - .then((res) => { - expectStatusCode(200, res); - return { - idToken: res.body.idToken, - localId: res.body.localId, - refreshToken: res.body.refreshToken, - email: res.body.email, - }; - }); -} - -export async function expectUserNotExistsForIdToken( - testAgent: TestAgent, - idToken: string -): Promise { - await testAgent - .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("USER_NOT_FOUND"); - }); -} - -export async function expectIdTokenExpired(testAgent: TestAgent, idToken: string): Promise { - await testAgent - .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("TOKEN_EXPIRED"); - }); -} - -export function getAccountInfoByIdToken(testAgent: TestAgent, idToken: string): Promise { - return testAgent - .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); - return res.body.users[0]; - }); -} - -export function getAccountInfoByLocalId(testAgent: TestAgent, localId: string): Promise { - return testAgent - .post("/identitytoolkit.googleapis.com/v1/accounts:lookup") - .send({ localId: [localId] }) - .set("Authorization", "Bearer owner") - .then((res) => { - expectStatusCode(200, res); - expect(res.body.users || []).to.have.length(1); - return res.body.users[0]; - }); -} - -export function inspectOobs(testAgent: TestAgent): Promise { - return testAgent.get(`/emulator/v1/projects/${PROJECT_ID}/oobCodes`).then((res) => { - expectStatusCode(200, res); - return res.body.oobCodes; - }); -} - -export function inspectVerificationCodes(testAgent: TestAgent): Promise { - return testAgent.get(`/emulator/v1/projects/${PROJECT_ID}/verificationCodes`).then((res) => { - expectStatusCode(200, res); - return res.body.verificationCodes; - }); -} - -export function createEmailSignInOob( - testAgent: TestAgent, - email: string -): Promise<{ oobCode: string; oobLink: string }> { - return testAgent - .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") - .send({ email, requestType: "EMAIL_SIGNIN", returnOobLink: true }) - .set("Authorization", "Bearer owner") - .then((res) => { - expectStatusCode(200, res); - return { - oobCode: res.body.oobCode, - oobLink: res.body.oobLink, - }; - }); -} - -export function getSigninMethods(testAgent: TestAgent, email: string): Promise { - return testAgent - .post("/identitytoolkit.googleapis.com/v1/accounts:createAuthUri") - .send({ continueUri: "http://example.com/", identifier: email }) - .query({ key: "fake-api-key" }) - .then((res) => { - expectStatusCode(200, res); - return res.body.signinMethods; - }); -} - -export function updateProjectConfig(testAgent: TestAgent, config: {}): Promise { - return testAgent - .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) - .set("Authorization", "Bearer owner") - .send(config) - .then((res) => { - expectStatusCode(200, res); - }); -} - -export function updateAccountByLocalId( - testAgent: TestAgent, - localId: string, - fields: {} -): Promise { - return testAgent - .post("/identitytoolkit.googleapis.com/v1/accounts:update") - .set("Authorization", "Bearer owner") - .send({ localId, ...fields }) - .then((res) => { - expectStatusCode(200, res); - }); -} diff --git a/src/test/emulators/auth/idp.spec.ts b/src/test/emulators/auth/idp.spec.ts deleted file mode 100644 index d8a0ef2d8f8..00000000000 --- a/src/test/emulators/auth/idp.spec.ts +++ /dev/null @@ -1,854 +0,0 @@ -import { expect } from "chai"; -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 } from "./setup"; -import { - expectStatusCode, - getAccountInfoByIdToken, - registerUser, - signInWithFakeClaims, - registerAnonUser, - signInWithPhoneNumber, - updateAccountByLocalId, - getSigninMethods, - signInWithEmailLink, - updateProjectConfig, - fakeClaims, - TEST_PHONE_NUMBER, - FAKE_GOOGLE_ACCOUNT, - REAL_GOOGLE_ACCOUNT, - TEST_MFA_INFO, -} from "./helpers"; - -// Many JWT fields from IDPs use snake_case and we need to match that. - -describeAuthEmulator("sign-in with credential", ({ authApi }) => { - it("should create new account with IDP from unsigned ID token", async () => { - 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); - 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}` - ); - expect(res.body.oauthIdToken).to.equal(FAKE_GOOGLE_ACCOUNT.idToken); - expect(res.body.providerId).to.equal("google.com"); - expect(res.body).to.have.property("refreshToken").that.is.a("string"); - - // The ID Token used above does NOT contain name or photo, so the - // account created won't have those attributes either. - expect(res.body).not.to.have.property("displayName"); - expect(res.body).not.to.have.property("photoUrl"); - - const raw = JSON.parse(res.body.rawUserInfo); - expect(raw.id).to.equal(FAKE_GOOGLE_ACCOUNT.rawId); - expect(raw.email).to.equal(FAKE_GOOGLE_ACCOUNT.email); - expect(raw.verified_email).to.equal(true); - expect(raw.locale).to.equal("en"); - // name, given_name, family_name, and picture are not populated since - // they are not in the ID Token used above. - expect(raw.granted_scopes.split(" ")).to.have.members([ - "openid", - "https://www.googleapis.com/auth/userinfo.profile", - "https://www.googleapis.com/auth/userinfo.email", - ]); - - 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).not.to.have.property("provider_id"); - expect(decoded!.payload.firebase) - .to.have.property("identities") - .eql({ - "google.com": [FAKE_GOOGLE_ACCOUNT.rawId], - email: [FAKE_GOOGLE_ACCOUNT.email], - }); - expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("google.com"); - }); - }); - - it("should create new account with IDP from production ID token", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") - .query({ key: "fake-api-key" }) - .send({ - postBody: `providerId=google.com&id_token=${REAL_GOOGLE_ACCOUNT.idToken}`, - requestUri: "http://localhost", - returnIdpCredential: true, - returnSecureToken: true, - }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.isNewUser).to.equal(true); - 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}` - ); - expect(res.body.oauthIdToken).to.equal(REAL_GOOGLE_ACCOUNT.idToken); - expect(res.body.providerId).to.equal("google.com"); - expect(res.body).to.have.property("refreshToken").that.is.a("string"); - - // The ID Token used above does NOT contain name or photo, so the - // account created won't have those attributes either. - // TODO: Shall we fetch more profile info from IDP via API calls? - expect(res.body).not.to.have.property("displayName"); - expect(res.body).not.to.have.property("photoUrl"); - - const raw = JSON.parse(res.body.rawUserInfo); - expect(raw.id).to.equal(REAL_GOOGLE_ACCOUNT.rawId); - expect(raw.email).to.equal(REAL_GOOGLE_ACCOUNT.email); - expect(raw.verified_email).to.equal(true); - expect(raw.locale).to.equal("en"); - // name, given_name, family_name, and picture are not populated since - // they are not in the ID Token used above. - expect(raw.granted_scopes.split(" ")).to.have.members([ - "openid", - "https://www.googleapis.com/auth/userinfo.profile", - "https://www.googleapis.com/auth/userinfo.email", - ]); - - 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).not.to.have.property("provider_id"); - expect(decoded!.payload.firebase) - .to.have.property("identities") - .eql({ - "google.com": [REAL_GOOGLE_ACCOUNT.rawId], - email: [REAL_GOOGLE_ACCOUNT.email], - }); - expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("google.com"); - }); - }); - - it("should create new account with IDP from unencoded JSON claims", async () => { - const claims = fakeClaims({ - sub: "123456789012345678901", - name: "Ada Lovelace", - given_name: "Ada", - family_name: "Lovelace", - picture: "http://localhost/fake-picture-url.png", - }); - const fakeIdToken = JSON.stringify(claims); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") - .query({ key: "fake-api-key" }) - .send({ - postBody: `providerId=google.com&id_token=${encodeURIComponent(fakeIdToken)}`, - requestUri: "http://localhost", - returnIdpCredential: true, - returnSecureToken: true, - }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.isNewUser).to.equal(true); - expect(res.body.federatedId).to.equal(`https://accounts.google.com/${claims.sub}`); - expect(res.body.oauthIdToken).to.equal(fakeIdToken); - expect(res.body.providerId).to.equal("google.com"); - expect(res.body.displayName).to.equal(claims.name); - expect(res.body.fullName).to.equal(claims.name); - expect(res.body.firstName).to.equal(claims.given_name); - expect(res.body.lastName).to.equal(claims.family_name); - expect(res.body.photoUrl).to.equal(claims.picture); - expect(res.body).to.have.property("refreshToken").that.is.a("string"); - - const raw = JSON.parse(res.body.rawUserInfo); - expect(raw.id).to.equal(claims.sub); - expect(raw.name).to.equal(claims.name); - expect(raw.given_name).to.equal(claims.given_name); - 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" - ); - - 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).not.to.have.property("provider_id"); - expect(decoded!.payload.firebase) - .to.have.property("identities") - .eql({ - "google.com": [claims.sub], - }); - expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("google.com"); - }); - }); - - it("should accept params (e.g. providerId, id_token) in requestUri", async () => { - const claims = fakeClaims({ - sub: "123456789012345678901", - }); - const fakeIdToken = JSON.stringify(claims); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") - .query({ key: "fake-api-key" }) - .send({ - // No postBody, all params in requestUri below. - requestUri: `http://localhost?providerId=google.com&id_token=${encodeURIComponent( - fakeIdToken - )}`, - returnIdpCredential: true, - }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.providerId).to.equal("google.com"); - }); - }); - - it("should copy attributes to user on IDP sign-up", async () => { - const claims = fakeClaims({ - sub: "123456789012345678901", - screen_name: "turingcomplete", - name: "Alan Turing", - picture: "http://localhost/turing.png", - }); - const { idToken } = await signInWithFakeClaims(authApi(), "google.com", claims); - - const user = await getAccountInfoByIdToken(authApi(), idToken); - expect(user.photoUrl).equal(claims.picture); - expect(user.displayName).equal(claims.name); - expect(user.screenName).equal(claims.screen_name); - }); - - it("should allow duplicate emails if set in project config", async () => { - await updateProjectConfig(authApi(), { signIn: { allowDuplicateEmails: true } }); - - const email = "alice@example.com"; - - // Given there exists an account with email already: - const user1 = await registerUser(authApi(), { email, password: "notasecret" }); - - // When trying to sign-in with IDP that claims the same email: - const user2 = await signInWithFakeClaims(authApi(), "google.com", { - sub: "123456789012345678901", - email, - }); - - // It should create a new account with different local ID: - expect(user2.localId).not.to.equal(user1.localId); - }); - - it("should sign-up new users without copying email when allowing duplicate emails", async () => { - await updateProjectConfig(authApi(), { signIn: { allowDuplicateEmails: true } }); - - const email = "alice@example.com"; - - const user1 = await signInWithFakeClaims(authApi(), "google.com", { - sub: "123456789012345678901", - email, - }); - - const info = await getAccountInfoByIdToken(authApi(), user1.idToken); - expect(info.email).to.be.undefined; - }); - - it("should allow multiple providers with same email when allowing duplicate emails", async () => { - await updateProjectConfig(authApi(), { signIn: { allowDuplicateEmails: true } }); - - const email = "alice@example.com"; - - const user1 = await signInWithFakeClaims(authApi(), "google.com", { - sub: "123456789012345678901", - email, - }); - const user2 = await signInWithFakeClaims(authApi(), "facebook.com", { - sub: "123456789012345678901", - email, - }); - - expect(user2.localId).not.to.equal(user1.localId); - }); - - it("should sign in existing account if (providerId, sub) is the same", async () => { - const user1 = await signInWithFakeClaims(authApi(), "google.com", { - sub: "123456789012345678901", - }); - - // Same sub, same user. - const user2 = await signInWithFakeClaims(authApi(), "google.com", { - sub: "123456789012345678901", - }); - expect(user2.localId).to.equal(user1.localId); - - // Different sub, different user. - const user3 = await signInWithFakeClaims(authApi(), "google.com", { - sub: "000000000000000000000", - }); - expect(user3.localId).not.to.equal(user1.localId); - - // Different providerId, different user. - const user4 = await signInWithFakeClaims(authApi(), "apple.com", { - sub: "123456789012345678901", - }); - expect(user4.localId).not.to.equal(user1.localId); - }); - - it("should error if user is disabled", async () => { - const user = await signInWithFakeClaims(authApi(), "google.com", { - sub: "123456789012345678901", - }); - await updateAccountByLocalId(authApi(), user.localId, { disableUser: true }); - - const claims = fakeClaims({ - sub: "123456789012345678901", - name: "Foo", - }); - const fakeIdToken = JSON.stringify(claims); - 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(400, res); - expect(res.body.error.message).to.equal("USER_DISABLED"); - }); - }); - - it("should add IDP as a sign-in method for email if available", async () => { - const email = "foo@example.com"; - const sub = "123456789012345678901"; - await signInWithFakeClaims(authApi(), "google.com", { - sub, - email, - }); - expect(await getSigninMethods(authApi(), email)).to.eql(["google.com"]); - - const newEmail = "bar@example.com"; - const { idToken } = await signInWithFakeClaims(authApi(), "google.com", { - sub, - email: newEmail, - }); - - expect(await getSigninMethods(authApi(), newEmail)).to.eql(["google.com"]); - - // The account-level email is still the old email. - const info = await getAccountInfoByIdToken(authApi(), idToken); - expect(info.email).to.equal(email); - }); - - it("should unlink password and overwite profile attributes if user had unverified email", async () => { - // Given a user with unverified email, linked with password: - const { localId, email } = await registerUser(authApi(), { - email: "foo@example.com", - password: "notasecret", - displayName: "Foo", - }); - - // When signing in with IDP and IDP verifies email: - const providerId = "google.com"; - const photoUrl = "http://localhost/photo-from-idp.png"; - const idpSignIn = await signInWithFakeClaims(authApi(), providerId, { - sub: "123456789012345678901", - email, - email_verified: true, - picture: photoUrl, - }); - - // It should sign-in into the same account, but the account's password - // should be unlinked. - expect(idpSignIn.localId).to.equal(localId); - const signInMethods = await getSigninMethods(authApi(), email); - expect(signInMethods).to.eql([providerId]); - expect(signInMethods).not.to.contain([PROVIDER_PASSWORD]); - - const info = await getAccountInfoByIdToken(authApi(), idpSignIn.idToken); - expect(info.emailVerified).to.be.true; // Verified by IDP. - - // Profile attributes should be overwritten (if provided by IDP) or cleared. - expect(info.photoUrl).to.equal(photoUrl); - expect(info.displayName).to.be.undefined; // Not provided by IDP. - }); - - it("should not unlink password if email was already verified", async () => { - // Given a user with verified email, linked with password: - const user = { - email: "foo@example.com", - password: "notasecret", - displayName: "Foo", - }; - const { localId, email } = await registerUser(authApi(), user); - await signInWithEmailLink(authApi(), email); // Verify email via email link sign-in. - - // When signing in with IDP and IDP verifies email: - const providerId = "google.com"; - const photoUrl = "http://localhost/photo-from-idp.png"; - const idpSignIn = await signInWithFakeClaims(authApi(), providerId, { - sub: "123456789012345678901", - email, - email_verified: true, - picture: photoUrl, - }); - - // It should sign-in into the same account and keep all providers and info. - expect(idpSignIn.localId).to.equal(localId); - const signInMethods = await getSigninMethods(authApi(), email); - expect(signInMethods).to.have.members([ - providerId, - PROVIDER_PASSWORD, - SIGNIN_METHOD_EMAIL_LINK, - ]); - - const info = await getAccountInfoByIdToken(authApi(), idpSignIn.idToken); - expect(info.emailVerified).to.be.true; // Verified by IDP. - - // Profile attributes should be overwritten (if provided by IDP) or cleared. - expect(info.photoUrl).to.equal(photoUrl); - expect(info.displayName).to.be.undefined; // Not provided by IDP. - }); - - it("should return needConfirmation if both account and IDP has unverified email", async () => { - // Given a user with unverified email: - const email = "bar@example.com"; - const providerId1 = "facebook.com"; - const originalDisplayName = "Bar"; - const { localId, idToken } = await signInWithFakeClaims(authApi(), providerId1, { - sub: "123456789012345678901", - email, - email_verified: false, - name: originalDisplayName, - }); - - // When signing in with IDP and IDP does not verify email: - const providerId2 = "google.com"; - const fakeIdToken = JSON.stringify( - fakeClaims({ - sub: "123456789012345678901", - email, - email_verified: false, - name: "Foo", - picture: "http://localhost/photo-from-idp.png", - }) - ); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") - .query({ key: "fake-api-key" }) - .send({ - requestUri: `http://localhost?providerId=${providerId2}&id_token=${encodeURIComponent( - fakeIdToken - )}`, - returnIdpCredential: true, - }) - .then((res) => { - // It should fail to sign in with needConfirmation. - expectStatusCode(200, res); - expect(res.body.needConfirmation).to.equal(true); - expect(res.body.localId).to.equal(localId); - expect(res.body).not.to.have.property("idToken"); - expect(res.body.verifiedProvider).to.eql([providerId1]); - }); - - const signInMethods = await getSigninMethods(authApi(), email); - expect(signInMethods).to.have.members([providerId1]); - expect(signInMethods).not.to.include([providerId2]); - - // Account should not be updated. - const info = await getAccountInfoByIdToken(authApi(), idToken); - expect(info.emailVerified).to.be.false; - expect(info.displayName).to.equal(originalDisplayName); - expect(info.photoUrl).to.be.undefined; - }); - - it("should error when requestUri is missing or invalid", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") - .query({ key: "fake-api-key" }) - .send({ - postBody: `id_token=${FAKE_GOOGLE_ACCOUNT.idToken}`, - }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("MISSING_REQUEST_URI"); - }); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") - .query({ key: "fake-api-key" }) - .send({ - postBody: `id_token=${FAKE_GOOGLE_ACCOUNT.idToken}`, - requestUri: "notAnAbsoluteUriAndThusInvalid", - }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("INVALID_REQUEST_URI"); - }); - }); - - it("should error when missing providerId is missing", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") - .query({ key: "fake-api-key" }) - .send({ - postBody: `id_token=${FAKE_GOOGLE_ACCOUNT.idToken}`, - requestUri: "http://localhost", - }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.contain( - "INVALID_CREDENTIAL_OR_PROVIDER_ID : Invalid IdP response/credential:" - ); - }); - }); - - it("should error when sub is missing or not a string", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") - .query({ key: "fake-api-key" }) - .send({ - // No sub in token. - postBody: `providerId=google.com&id_token=${JSON.stringify({})}`, - requestUri: "http://localhost", - }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.contain("INVALID_IDP_RESPONSE"); - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") - .query({ key: "fake-api-key" }) - .send({ - // sub is not a string - postBody: `providerId=google.com&id_token=${JSON.stringify({ sub: 12345 })}`, - requestUri: "http://localhost", - }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.contain("INVALID_IDP_RESPONSE"); - }); - }); - - it("should link IDP to existing account by idToken", 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 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); - expect(res.body.localId).to.equal(user.localId); - 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).not.to.have.property("provider_id"); - expect(decoded!.payload.firebase) - .to.have.property("identities") - .eql({ - "google.com": [claims.sub], - email: [user.email], - }); - expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("google.com"); - }); - - const signInMethods = await getSigninMethods(authApi(), user.email); - expect(signInMethods).to.have.members(["google.com", PROVIDER_PASSWORD]); - }); - - it("should copy IDP email to user-level email if not present", async () => { - const user = await signInWithPhoneNumber(authApi(), TEST_PHONE_NUMBER); - const claims = fakeClaims({ - sub: "123456789012345678901", - name: "Foo", - email: "example@google.com", - }); - const fakeIdToken = JSON.stringify(claims); - const idToken = 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); - expect(res.body.localId).to.equal(user.localId); - 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).not.to.have.property("provider_id"); - expect(decoded!.payload.firebase) - .to.have.property("identities") - .eql({ - "google.com": [claims.sub], - email: [claims.email], - phone: [TEST_PHONE_NUMBER], - }); - expect(decoded!.payload.firebase).to.have.property("sign_in_provider").equals("google.com"); - - return res.body.idToken as string; - }); - - const info = await getAccountInfoByIdToken(authApi(), idToken); - expect(info.email).to.be.equal(claims.email); - expect(!!info.emailVerified).to.be.equal(!!claims.email_verified); - }); - - it("should error if user to be linked is disabled", async () => { - const user = await registerUser(authApi(), { - email: "foo@example.com", - password: "notasecret", - }); - await updateAccountByLocalId(authApi(), user.localId, { disableUser: true }); - - const claims = fakeClaims({ - sub: "123456789012345678901", - name: "Foo", - }); - const fakeIdToken = JSON.stringify(claims); - 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(400, res); - expect(res.body.error.message).to.equal("USER_DISABLED"); - }); - }); - - it("should error if user to be linked is an MFA user", async () => { - const user = { - email: "alice@example.com", - password: "notasecret", - mfaInfo: [TEST_MFA_INFO], - }; - const { idToken } = await registerUser(authApi(), user); - - const claims = fakeClaims({ - sub: "123456789012345678901", - name: "Foo", - }); - const fakeIdToken = JSON.stringify(claims); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") - .query({ key: "fake-api-key" }) - .send({ - idToken, - postBody: `providerId=google.com&id_token=${encodeURIComponent(fakeIdToken)}`, - requestUri: "http://localhost", - returnIdpCredential: true, - }) - .then((res) => { - expectStatusCode(501, res); - expect(res.body.error.message).to.equal("MFA Login not yet implemented."); - }); - }); - - it("should return error if IDP account is already linked to the same user", async () => { - // Given a user with already linked with IDP account: - const providerId = "google.com"; - const claims = { - sub: "123456789012345678901", - email: "alice@example.com", - email_verified: false, - }; - const { idToken } = await signInWithFakeClaims(authApi(), providerId, claims); - - // When trying to link the same IDP account on the same user: - const fakeIdToken = JSON.stringify(claims); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") - .query({ key: "fake-api-key" }) - .send({ - idToken, - postBody: `providerId=google.com&id_token=${encodeURIComponent(fakeIdToken)}`, - requestUri: "http://localhost", - returnIdpCredential: true, - }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.localId).to.be.undefined; - expect(res.body).not.to.have.property("refreshToken"); - - expect(res.body.errorMessage).to.equal("FEDERATED_USER_ID_ALREADY_LINKED"); - expect(res.body.oauthIdToken).to.equal(fakeIdToken); - }); - }); - - it("should return error if IDP account is already linked to another user", async () => { - // Given a user with already linked with IDP account: - const providerId = "google.com"; - const claims = { - sub: "123456789012345678901", - email: "alice@example.com", - email_verified: false, - }; - await signInWithFakeClaims(authApi(), providerId, claims); - - const user = await registerUser(authApi(), { - email: "foo@example.com", - password: "notasecret", - }); - // When trying to link the same IDP account on a different user: - const fakeIdToken = JSON.stringify(claims); - 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", - }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("FEDERATED_USER_ID_ALREADY_LINKED"); - }); - - // Sign-in methods for either user should not be changed since linking failed. - const signInMethods1 = await getSigninMethods(authApi(), user.email); - expect(signInMethods1).to.have.members([PROVIDER_PASSWORD]); - const signInMethods2 = await getSigninMethods(authApi(), claims.email); - expect(signInMethods2).to.have.members([providerId]); - }); - - it("should return error if IDP account email already exists if NOT allowDuplicateEmail", async () => { - // Given an existing account with the email: - const email = "alice@example.com"; - await registerUser(authApi(), { email, password: "notasecret" }); - - // When trying to link an IDP account on a different user with the same email: - const { idToken } = await registerAnonUser(authApi()); - const fakeIdToken = JSON.stringify( - fakeClaims({ - sub: "12345", - email, - }) - ); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") - .query({ key: "fake-api-key" }) - .send({ - idToken, - postBody: `providerId=google.com&id_token=${encodeURIComponent(fakeIdToken)}`, - requestUri: "http://localhost", - }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error.message).to.equal("EMAIL_EXISTS"); - }); - }); - - it("should allow linking IDP account with same email to same user", async () => { - // Given an existing account with the email: - const email = "alice@example.com"; - const { idToken, localId } = await registerUser(authApi(), { email, password: "notasecret" }); - - // When trying to link an IDP account on user with the same email: - const fakeIdToken = JSON.stringify( - fakeClaims({ - sub: "12345", - email, - email_verified: true, - }) - ); - const newIdToken = await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") - .query({ key: "fake-api-key" }) - .send({ - idToken, - postBody: `providerId=google.com&id_token=${encodeURIComponent(fakeIdToken)}`, - requestUri: "http://localhost", - }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.localId).to.equal(localId); - - return res.body.idToken as string; - }); - - // Account email is now verified. - const info = await getAccountInfoByIdToken(authApi(), newIdToken); - expect(info.emailVerified).to.be.true; - }); - - it("should allow linking IDP account with same email if allowDuplicateEmail", async () => { - // Given an existing account with the email: - const email = "alice@example.com"; - await registerUser(authApi(), { email, password: "notasecret" }); - - await updateProjectConfig(authApi(), { signIn: { allowDuplicateEmails: true } }); - - // When trying to link an IDP account on a different user with the same email: - const { idToken, localId } = await registerAnonUser(authApi()); - const fakeIdToken = JSON.stringify( - fakeClaims({ - sub: "12345", - email, - }) - ); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signInWithIdp") - .query({ key: "fake-api-key" }) - .send({ - idToken, - postBody: `providerId=google.com&id_token=${encodeURIComponent(fakeIdToken)}`, - requestUri: "http://localhost", - }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.localId).to.equal(localId); - }); - }); -}); diff --git a/src/test/emulators/auth/misc.spec.ts b/src/test/emulators/auth/misc.spec.ts deleted file mode 100644 index db0a34502c6..00000000000 --- a/src/test/emulators/auth/misc.spec.ts +++ /dev/null @@ -1,311 +0,0 @@ -import { expect } from "chai"; -import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; -import { UserInfo } from "../../../emulator/auth/state"; -import { PROJECT_ID, signInWithPhoneNumber, TEST_PHONE_NUMBER } 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 }) => { - 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 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"); - }); - }); -}); - -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"); - }); - }); -}); - -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"); - }); - }); -}); - -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); - }); - }); -}); - -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 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 config on PATCH /emulator/v1/projects/{PROJECT_ID}/config", async () => { - await authApi() - .patch(`/emulator/v1/projects/${PROJECT_ID}/config`) - .send({ signIn: { allowDuplicateEmails: true } }) - .then((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 }); - }); - }); -}); diff --git a/src/test/emulators/auth/oob.spec.ts b/src/test/emulators/auth/oob.spec.ts deleted file mode 100644 index b31bcbedabe..00000000000 --- a/src/test/emulators/auth/oob.spec.ts +++ /dev/null @@ -1,301 +0,0 @@ -import { expect } from "chai"; -import { describeAuthEmulator } from "./setup"; -import { - expectStatusCode, - registerUser, - registerAnonUser, - updateAccountByLocalId, - expectIdTokenExpired, - inspectOobs, -} from "./helpers"; - -describeAuthEmulator("accounts:sendOobCode", ({ authApi, getClock }) => { - it("should generate OOB code for verify email", async () => { - const user = { email: "alice@example.com", password: "notasecret" }; - const { idToken, localId } = await registerUser(authApi(), user); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") - .query({ key: "fake-api-key" }) - .send({ idToken, requestType: "VERIFY_EMAIL" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.email).to.equal(user.email); - - // These fields should not be set since returnOobLink is not set. - expect(res.body).not.to.have.property("oobCode"); - expect(res.body).not.to.have.property("oobLink"); - }); - - const oobs = await inspectOobs(authApi()); - expect(oobs).to.have.length(1); - expect(oobs[0].email).to.equal(user.email); - expect(oobs[0].requestType).to.equal("VERIFY_EMAIL"); - - // The returned oobCode can be redeemed to verify the email. - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:update") - .query({ key: "fake-api-key" }) - // OOB code is enough, no idToken needed. - .send({ oobCode: oobs[0].oobCode }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.localId).to.equal(localId); - expect(res.body.email).to.equal(user.email); - expect(res.body.emailVerified).to.equal(true); - }); - - // oobCode is removed after redeemed. - const oobs2 = await inspectOobs(authApi()); - expect(oobs2).to.have.length(0); - }); - - it("should return OOB code directly for requests with OAuth 2", async () => { - const user = { email: "alice@example.com", password: "notasecret" }; - await registerUser(authApi(), user); - 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.email).to.equal(user.email); - expect(res.body.oobCode).to.be.a("string"); - expect(res.body.oobLink).to.be.a("string"); - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") - .set("Authorization", "Bearer owner") - .send({ email: user.email, requestType: "VERIFY_EMAIL", returnOobLink: true }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.email).to.equal(user.email); - expect(res.body.oobCode).to.be.a("string"); - expect(res.body.oobLink).to.be.a("string"); - }); - }); - - it("should return OOB code by idToken for OAuth 2 requests as well", async () => { - const user = { email: "alice@example.com", password: "notasecret" }; - const { idToken } = await registerUser(authApi(), user); - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") - .set("Authorization", "Bearer owner") - .send({ idToken, requestType: "VERIFY_EMAIL", returnOobLink: true }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.email).to.equal(user.email); - expect(res.body.oobCode).to.be.a("string"); - expect(res.body.oobLink).to.be.a("string"); - }); - }); - - it("should error when trying to verify email without idToken or email", async () => { - const user = { email: "alice@example.com", password: "notasecret" }; - await registerUser(authApi(), user); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") - .query({ key: "fake-api-key" }) - .send({ requestType: "VERIFY_EMAIL" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equal("INVALID_ID_TOKEN"); - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") - .set("Authorization", "Bearer owner") - // This causes a different error message to be returned, see below. - .send({ returnOobLink: true, requestType: "VERIFY_EMAIL" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equal("MISSING_EMAIL"); - }); - - const oobs = await inspectOobs(authApi()); - expect(oobs).to.have.length(0); - }); - - it("should error when trying to verify email without idToken if not returnOobLink", async () => { - const user = await registerUser(authApi(), { - email: "alice@example.com", - password: "notasecret", - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") - .query({ key: "fake-api-key" }) - // email here is ignored because returnOobLink is not set. - .send({ email: user.email, requestType: "VERIFY_EMAIL" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equal("INVALID_ID_TOKEN"); - }); - - const oobs = await inspectOobs(authApi()); - expect(oobs).to.have.length(0); - }); - - it("should error when trying to verify email not associated with any user", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") - .set("Authorization", "Bearer owner") - .send({ email: "nosuchuser@example.com", returnOobLink: true, requestType: "VERIFY_EMAIL" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equal("USER_NOT_FOUND"); - }); - - const oobs = await inspectOobs(authApi()); - expect(oobs).to.have.length(0); - }); - - it("should error when verifying email for accounts without email", async () => { - const { idToken } = await registerAnonUser(authApi()); - - 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").equal("MISSING_EMAIL"); - }); - - const oobs = await inspectOobs(authApi()); - expect(oobs).to.have.length(0); - }); - - it("should error if user is disabled", async () => { - const { localId, idToken, email } = await registerUser(authApi(), { - email: "foo@example.com", - password: "foobar", - }); - await updateAccountByLocalId(authApi(), localId, { disableUser: true }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") - .query({ key: "fake-api-key" }) - .send({ email, idToken, requestType: "VERIFY_EMAIL" }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").equals("USER_DISABLED"); - }); - }); - - it("should error when continueUrl is invalid", async () => { - const { idToken } = await registerUser(authApi(), { - email: "alice@example.com", - password: "notasecret", - }); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") - .query({ key: "fake-api-key" }) - .send({ - idToken, - requestType: "VERIFY_EMAIL", - continueUrl: "noSchemeOrHost", - }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error).to.have.property("message").contain("INVALID_CONTINUE_URI"); - }); - - const oobs = await inspectOobs(authApi()); - expect(oobs).to.have.length(0); - }); - - it("should generate OOB code for reset password", async () => { - const user = { email: "alice@example.com", password: "notasecret" }; - const { idToken } = await registerUser(authApi(), user); - - getClock().tick(2000); // Wait for idToken to be issued in the past. - - 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); - - // These fields should not be set since returnOobLink is not set. - expect(res.body).not.to.have.property("oobCode"); - expect(res.body).not.to.have.property("oobLink"); - }); - - const oobs = await inspectOobs(authApi()); - expect(oobs).to.have.length(1); - expect(oobs[0].email).to.equal(user.email); - expect(oobs[0].requestType).to.equal("PASSWORD_RESET"); - - // The returned oobCode can be redeemed to reset the password. - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:resetPassword") - .query({ key: "fake-api-key" }) - .send({ oobCode: oobs[0].oobCode, newPassword: "notasecret2" }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.requestType).to.equal("PASSWORD_RESET"); - expect(res.body.email).to.equal(user.email); - }); - - // All old idTokens are invalidated. - await expectIdTokenExpired(authApi(), idToken); - }); - - it("should return purpose of oobCodes via resetPassword endpoint", 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)); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") - .query({ key: "fake-api-key" }) - .send({ requestType: "VERIFY_EMAIL", idToken }) - .then((res) => expectStatusCode(200, res)); - - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") - .query({ key: "fake-api-key" }) - .send({ email: "bob@example.com", requestType: "EMAIL_SIGNIN" }) - .then((res) => expectStatusCode(200, res)); - - const oobs = await inspectOobs(authApi()); - expect(oobs).to.have.length(3); - - for (const oob of oobs) { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:resetPassword") - .query({ key: "fake-api-key" }) - // If newPassword is not set, this API will just return the purpose - // (requestType) of the code without consuming it. - .send({ oobCode: oob.oobCode }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.requestType).to.equal(oob.requestType); - if (oob.requestType === "EMAIL_SIGNIN") { - // Do not reveal the email when inspecting an email sign-in oobCode. - // Instead, the client must provide email (e.g. by asking the user) - // when they call the emailLinkSignIn endpoint. - // See: https://firebase.google.com/docs/auth/web/email-link-auth#security_concerns - expect(res.body).not.to.have.property("email"); - } else { - expect(res.body.email).to.equal(oob.email); - } - }); - } - - // OOB codes are not consumed by the lookup above. - const oobs2 = await inspectOobs(authApi()); - expect(oobs2).to.have.length(3); - }); -}); diff --git a/src/test/emulators/auth/password.spec.ts b/src/test/emulators/auth/password.spec.ts deleted file mode 100644 index 48836bb5770..00000000000 --- a/src/test/emulators/auth/password.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { expect } from "chai"; -import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; -import { FirebaseJwtPayload } from "../../../emulator/auth/operations"; -import { describeAuthEmulator } from "./setup"; -import { - expectStatusCode, - registerUser, - TEST_MFA_INFO, - TEST_PHONE_NUMBER, - updateAccountByLocalId, -} from "./helpers"; - -describeAuthEmulator("accounts:signInWithPassword", ({ authApi }) => { - 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 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 error 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(501, res); - expect(res.body.error.message).to.equal("MFA Login not yet implemented."); - }); - }); -}); diff --git a/src/test/emulators/auth/phone.spec.ts b/src/test/emulators/auth/phone.spec.ts deleted file mode 100644 index aa7ee87c370..00000000000 --- a/src/test/emulators/auth/phone.spec.ts +++ /dev/null @@ -1,330 +0,0 @@ -import { expect } from "chai"; -import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; -import { FirebaseJwtPayload } from "../../../emulator/auth/operations"; -import { describeAuthEmulator } from "./setup"; -import { - expectStatusCode, - registerAnonUser, - signInWithPhoneNumber, - updateAccountByLocalId, - inspectVerificationCodes, - registerUser, - TEST_MFA_INFO, - TEST_PHONE_NUMBER, -} 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 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 if user has MFA", async () => { - const user = { - email: "alice@example.com", - password: "notasecret", - mfaInfo: [TEST_MFA_INFO], - }; - const { localId, 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; - }); - - 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(501, res); - expect(res.body.error.message).to.equal("MFA Login not yet implemented."); - }); - }); - - 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"); - }); - }); -}); diff --git a/src/test/emulators/auth/rest.spec.ts b/src/test/emulators/auth/rest.spec.ts deleted file mode 100644 index d89f0107e5f..00000000000 --- a/src/test/emulators/auth/rest.spec.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { expect } from "chai"; -import { expectStatusCode } from "./helpers"; -import { describeAuthEmulator } from "./setup"; - -describeAuthEmulator("REST API mapping", ({ authApi }) => { - it("should respond to status checks", async () => { - await authApi() - .get("/") - .then((res) => { - expectStatusCode(200, res); - expect(res.body.authEmulator).to.be.an("object"); - }); - }); - - it("should allow cross-origin requests", async () => { - await authApi() - .options("/") - .set("Origin", "example.com") - .set("Access-Control-Request-Headers", "Authorization,X-Client-Version,X-Whatever-Header") - .then((res) => { - expectStatusCode(204, res); - - // Some clients (including older browsers and jsdom) won't accept '*' as a - // wildcard, so we need to reflect Origin and Access-Control-Request-Headers. - // https://github.com/firebase/firebase-tools/issues/3200 - expect(res.header["access-control-allow-origin"]).to.eql("example.com"); - expect((res.header["access-control-allow-headers"] as string).split(",")).to.have.members([ - "Authorization", - "X-Client-Version", - "X-Whatever-Header", - ]); - }); - }); - - it("should handle integer values for enums", async () => { - // Proto integer value for "EMAIL_SIGNIN". Android client SDK sends this. - const requestType = 6; - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode") - .set("Authorization", "Bearer owner") - .send({ email: "bob@example.com", requestType, returnOobLink: true }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.oobLink).to.include("mode=signIn"); - }); - }); - - it("should handle integer values for enums (legacy API path)", async () => { - // Proto integer value for "EMAIL_SIGNIN". Android client SDK sends this. - const requestType = 6; - await authApi() - .post("/www.googleapis.com/identitytoolkit/v3/relyingparty/getOobConfirmationCode") - .set("Authorization", "Bearer owner") - .send({ email: "bob@example.com", requestType, returnOobLink: true }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body.oobLink).to.include("mode=signIn"); - }); - }); - - it("should convert numbers to strings for type:string fields", async () => { - // validSince should be an int64-formatted string, but Node.js Admin SDK - // sends it as a plain number (without quotes). - const validSince = 1611780718; - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:update") - .set("Authorization", "Bearer owner") - .send({ localId: "nosuch", validSince }) - .then((res) => { - expectStatusCode(400, res); - // It should pass JSON schema validation and get into handler logic. - expect(res.body.error.message).to.equal("USER_NOT_FOUND"); - }); - }); -}); - -describeAuthEmulator("authentication", ({ authApi }) => { - it("should throw 403 if API key is not provided", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .query({ - /* no API "key" */ - }) - .send({ returnSecureToken: true }) - .then((res) => { - expectStatusCode(403, res); - expect(res.body.error).to.have.property("status").equal("PERMISSION_DENIED"); - }); - }); - - it("should ignore non-Bearer Authorization headers", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - // This has no effect on the request handling, since it is not Bearer. - .set("Authorization", "Basic YWxhZGRpbjpvcGVuc2VzYW1l") - .query({ - /* no API "key" */ - }) - .send({ returnSecureToken: true }) - .then((res) => { - expectStatusCode(403, res); - expect(res.body.error).to.have.property("status").equal("PERMISSION_DENIED"); - }); - }); - - it("should treat Bearer owner as authenticated to project", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - // This authenticates as valid OAuth 2 credentials, no API key needed. - .set("Authorization", "Bearer owner") - .send({ - // This field requires OAuth 2 and should work correctly. - targetProjectId: "example2", - }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).not.to.have.property("error"); - }); - }); - - it("should ignore casing of Bearer / owner in Authorization header", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - // This authenticates as valid OAuth 2 credentials, no API key needed. - .set("Authorization", "bEArEr OWNER") - .send({ - // This field requires OAuth 2 and should work correctly. - targetProjectId: "example2", - }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).not.to.have.property("error"); - }); - }); - - it("should treat production service account as authenticated to project", async () => { - await 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") - .send({ - // This field requires OAuth 2 and should work correctly. - targetProjectId: "example2", - }) - .then((res) => { - expectStatusCode(200, res); - expect(res.body).not.to.have.property("error"); - }); - }); - - it("should deny requests with targetProjectId but without OAuth 2", async () => { - await authApi() - .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") - .query({ key: "fake-api-key" }) - .send({ - // Specifying this field requires OAuth 2. API key is not sufficient. - targetProjectId: "example2", - }) - .then((res) => { - expectStatusCode(400, res); - expect(res.body.error) - .to.have.property("message") - .equals( - "INSUFFICIENT_PERMISSION : Only authenticated requests can specify target_project_id." - ); - }); - }); -}); diff --git a/src/test/emulators/auth/setup.ts b/src/test/emulators/auth/setup.ts deleted file mode 100644 index cccd3c91ca8..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 { ProjectState } 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 5b827bc604a..00000000000 --- a/src/test/emulators/auth/signUp.spec.ts +++ /dev/null @@ -1,518 +0,0 @@ -import { expect } from "chai"; -import { decode as decodeJwt, JwtHeader } from "jsonwebtoken"; -import { FirebaseJwtPayload } from "../../../emulator/auth/operations"; -import { describeAuthEmulator } 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, -} 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); - }); -}); diff --git a/src/test/emulators/cloudFunctions.spec.ts b/src/test/emulators/cloudFunctions.spec.ts deleted file mode 100644 index ec4b59c8588..00000000000 --- a/src/test/emulators/cloudFunctions.spec.ts +++ /dev/null @@ -1,37 +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", { - eventType: `providers/firebase.auth/eventTypes/user.create`, - 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/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 462a5bb129d..00000000000 --- a/src/test/emulators/functionsRuntimeWorker.spec.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { expect } from "chai"; -import { FunctionsRuntimeInstance } from "../../emulator/functionsEmulator"; -import { EventEmitter } from "events"; -import { - FunctionsRuntimeArgs, - FunctionsRuntimeBundle, - EmulatedTriggerType, -} 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"; - triggerType = EmulatedTriggerType.HTTPS; - 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) {} -} - -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("trigger")); - 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("trigger")); - 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("trigger")); - 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 = "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)); - 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)); - 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)); - 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)); - 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 = "abc"; - const trigger2 = "def"; - - const pool = new RuntimeWorkerPool(FunctionsExecutionMode.SEQUENTIAL); - const worker = pool.addWorker(trigger1, new MockRuntimeInstance(true)); - - pool.submitWork(trigger2, new MockRuntimeBundle(trigger2)); - - 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 f76174e535d..00000000000 --- a/src/test/emulators/storage.rules.spec.ts +++ /dev/null @@ -1,370 +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); - - const storageDownload = getDownloadDetails(Emulators.STORAGE); - runtime = new StorageRulesRuntime(storageDownload.downloadPath); - (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/extensions/askUserForConsent.spec.ts b/src/test/extensions/askUserForConsent.spec.ts deleted file mode 100644 index 7fd2424e46b..00000000000 --- a/src/test/extensions/askUserForConsent.spec.ts +++ /dev/null @@ -1,54 +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"; - -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 b807b29a5ce..00000000000 --- a/src/test/extensions/askUserForParam.spec.ts +++ /dev/null @@ -1,243 +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"; - -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", () => { - 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(testSpec); - expect(promptStub.calledThrice).to.be.true; - }); - }); - - 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(spec, firebaseProjectVars); - 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 bd364bf08e0..00000000000 --- a/src/test/extensions/billingMigrationHelper.spec.ts +++ /dev/null @@ -1,174 +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("displayUpdateBillingNotice", () => { - it("should notify the user if the runtime is being upgraded to nodejs10", () => { - promptStub.resolves(true); - const curSpec = _.cloneDeep(NODE8_SPEC); - const newSpec = _.cloneDeep(NODE10_SPEC); - - expect(nodejsMigrationHelper.displayNode10UpdateBillingNotice(curSpec, newSpec, true)).not.to - .be.rejected; - expect(promptStub.callCount).to.equal(1); - }); - - it("should notify the user if the runtime is being upgraded to nodejs10 implicitly", () => { - promptStub.resolves(true); - const curSpec = _.cloneDeep(NO_RUNTIME_SPEC); - const newSpec = _.cloneDeep(NODE10_SPEC); - - expect(nodejsMigrationHelper.displayNode10UpdateBillingNotice(curSpec, newSpec, true)).not.to - .be.rejected; - expect(promptStub.callCount).to.equal(1); - }); - - it("should display nothing if the runtime isn't being upgraded to nodejs10", () => { - promptStub.resolves(true); - const curSpec = _.cloneDeep(NODE8_SPEC); - const newSpec = _.cloneDeep(NODE8_SPEC); - - expect(nodejsMigrationHelper.displayNode10UpdateBillingNotice(curSpec, newSpec, true)).not.to - .be.rejected; - expect(promptStub.callCount).to.equal(0); - }); - - it("should display nothing if the runtime is already on nodejs10", () => { - promptStub.resolves(true); - const curSpec = _.cloneDeep(NODE10_SPEC); - const newSpec = _.cloneDeep(NODE10_SPEC); - - expect(nodejsMigrationHelper.displayNode10UpdateBillingNotice(curSpec, 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 curSpec = _.cloneDeep(NODE8_SPEC); - const newSpec = _.cloneDeep(NODE10_SPEC); - - expect( - nodejsMigrationHelper.displayNode10UpdateBillingNotice(curSpec, newSpec, true) - ).to.be.rejectedWith(FirebaseError, "Cancelled"); - }); - }); - - 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/checkProjectBilling.spec.js b/src/test/extensions/checkProjectBilling.spec.js deleted file mode 100644 index dbf94a9475d..00000000000 --- a/src/test/extensions/checkProjectBilling.spec.js +++ /dev/null @@ -1,122 +0,0 @@ -"use strict"; - -const chai = require("chai"); -chai.use(require("chai-as-promised")); -const sinon = require("sinon"); - -const checkProjectBilling = require("../../extensions/checkProjectBilling"); -const prompt = require("../../prompt"); -const cloudbilling = require("../../gcp/cloudbilling"); - -const expect = chai.expect; - -describe("checkProjectBilling", function () { - /** @type {sinon.SinonStub} */ - let promptOnceStub; - - /** @type {sinon.SinonStub} */ - let checkBillingEnabledStub; - - /** @type {sinon.SinonStub} */ - let listBillingAccountsStub; - - /** @type {sinon.SinonStub} */ - let setBillingAccountStub; - - beforeEach(function () { - promptOnceStub = sinon.stub(prompt, "promptOnce"); - - checkBillingEnabledStub = sinon.stub(cloudbilling, "checkBillingEnabled"); - checkBillingEnabledStub.resolves(); - - listBillingAccountsStub = sinon.stub(cloudbilling, "listBillingAccounts"); - listBillingAccountsStub.resolves(); - - setBillingAccountStub = sinon.stub(cloudbilling, "setBillingAccount"); - setBillingAccountStub.resolves(); - }); - - afterEach(function () { - promptOnceStub.restore(); - checkBillingEnabledStub.restore(); - listBillingAccountsStub.restore(); - setBillingAccountStub.restore(); - }); - - it("should resolve if billing enabled.", function () { - const projectId = "already enabled"; - const extensionName = "test extension"; - - checkBillingEnabledStub.resolves(true); - - return checkProjectBilling - .isBillingEnabled(projectId) - .then((enabled) => { - if (!enabled) { - return checkProjectBilling.enableBilling(projectId, extensionName); - } - }) - .then(() => { - expect(checkBillingEnabledStub.calledWith(projectId)); - expect(listBillingAccountsStub.notCalled); - expect(setBillingAccountStub.notCalled); - expect(promptOnceStub.notCalled); - }); - }); - - it("should list accounts if no billing account set, but accounts available.", function () { - const projectId = "not set, but have list"; - const extensionName = "test extension 2"; - const accounts = [ - { - name: "test-cloud-billing-account-name", - open: true, - displayName: "test-account", - }, - ]; - - checkBillingEnabledStub.resolves(false); - listBillingAccountsStub.resolves(accounts); - setBillingAccountStub.resolves(true); - promptOnceStub.resolves("test-account"); - - return checkProjectBilling - .isBillingEnabled(projectId) - .then((enabled) => { - if (!enabled) { - return checkProjectBilling.enableBilling(projectId, extensionName); - } - }) - .then(() => { - expect(checkBillingEnabledStub.calledWith(projectId)); - expect(listBillingAccountsStub.calledOnce); - expect(setBillingAccountStub.calledOnce); - expect(setBillingAccountStub.calledWith(projectId, "test-cloud-billing-account-name")); - }); - }); - - it("should not list accounts if no billing accounts set or available.", function () { - const projectId = "not set, not available"; - const extensionName = "test extension 3"; - const accounts = []; - - checkBillingEnabledStub.onCall(0).resolves(false); - checkBillingEnabledStub.onCall(1).resolves(true); - listBillingAccountsStub.resolves(accounts); - promptOnceStub.resolves(); - - return checkProjectBilling - .isBillingEnabled(projectId) - .then((enabled) => { - if (!enabled) { - return checkProjectBilling.enableBilling(projectId, extensionName); - } - }) - .then(() => { - expect(checkBillingEnabledStub.calledWith(projectId)); - expect(listBillingAccountsStub.calledOnce); - expect(setBillingAccountStub.notCalled); - expect(checkBillingEnabledStub.callCount).to.equal(2); - }); - }); -}); diff --git a/src/test/extensions/displayExtensionInfo.spec.ts b/src/test/extensions/displayExtensionInfo.spec.ts deleted file mode 100644 index 4bf4631463c..00000000000 --- a/src/test/extensions/displayExtensionInfo.spec.ts +++ /dev/null @@ -1,223 +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", - "**Author**: Tester (**[firebase.google.com](firebase.google.com)**)", - "**Description**: descriptive", - ]; - expect(loggedLines).to.eql(expected); - }); - it("should display additional information for a published extension", () => { - const loggedLines = displayExtensionInfo.displayExtInfo(SPEC.name, SPEC, true); - const expected: string[] = [ - "**Name**: Old", - "**Author**: Tester (**[firebase.google.com](firebase.google.com)**)", - "**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, newSpec)).not.to - .be.rejected; - - expect(promptStub.callCount).to.equal(1); - expect(promptStub.firstCall.args[0].message).to.contain("To Kill"); - }); - - 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, newSpec)).not.to - .be.rejected; - - expect(promptStub.callCount).to.equal(1); - expect(promptStub.firstCall.args[0].message).to.contain("- api1"); - expect(promptStub.firstCall.args[0].message).to.contain("+ api3"); - }); - - 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, newSpec)).not.to - .be.rejected; - - expect(promptStub.callCount).to.equal(1); - expect(promptStub.firstCall.args[0].message).to.contain("- role1"); - expect(promptStub.firstCall.args[0].message).to.contain("+ role3"); - }); - - 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, newSpec)).not.to - .be.rejected; - - expect(promptStub.callCount).to.equal(1); - expect(promptStub.firstCall.args[0].message).to.contain("- resource1"); - expect(promptStub.firstCall.args[0].message).to.contain("desc"); - expect(promptStub.firstCall.args[0].message).to.contain("+ resource3"); - expect(promptStub.firstCall.args[0].message).to.contain("new desc"); - }); - - 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(oldSpec, SPEC)).not.to - .be.rejected; - - expect(promptStub.callCount).to.equal(1); - expect(promptStub.firstCall.args[0].message).to.contain( - "Billing is now required for the new version of this extension. Would you like to continue?" - ); - }); - - 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, newSpec) - ).to.be.rejectedWith( - FirebaseError, - "Without explicit consent for the change to license, we cannot update this extension instance." - ); - - 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, newSpec) - ).to.be.rejectedWith( - FirebaseError, - "Without explicit consent for the change to license, we cannot update this extension instance." - ); - }); - - 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, newSpec); - - expect(promptStub).not.to.have.been.called; - }); - }); -}); diff --git a/src/test/extensions/emulator/triggerHelper.spec.ts b/src/test/extensions/emulator/triggerHelper.spec.ts deleted file mode 100644 index 2f9bcab673b..00000000000 --- a/src/test/extensions/emulator/triggerHelper.spec.ts +++ /dev/null @@ -1,127 +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 = { - 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 = { - 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 = { - 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 = { - 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 = { - 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 543b8d3c80c..00000000000 --- a/src/test/extensions/extensionsApi.spec.ts +++ /dev/null @@ -1,891 +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"; - -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/`) - .reply(200, { name: "operations/abc123" }); - nock(api.extensionsOrigin).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); - - await extensionsApi.createInstanceFromSource( - PROJECT_ID, - INSTANCE_ID, - { - state: "ACTIVE", - name: "sources/blah", - packageUri: "https://test.fake/pacakge.zip", - hash: "abc123", - spec: { name: "", version: "0.1.0", sourceUrl: "", roles: [], resources: [], 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/`) - .reply(200, { name: "operations/abc123" }); - nock(api.extensionsOrigin).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); - - await extensionsApi.createInstanceFromExtensionVersion( - PROJECT_ID, - INSTANCE_ID, - { - name: "publishers/test-pub/extensions/test-ext/versions/0.1.0", - ref: "test-pub/test-ext@0.1.0", - hash: "abc123", - spec: { name: "", version: "0.1.0", sourceUrl: "", roles: [], resources: [], params: [] }, - }, - {} - ); - 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/`).reply(500); - - await expect( - extensionsApi.createInstanceFromSource( - PROJECT_ID, - INSTANCE_ID, - { - state: "ACTIVE", - name: "sources/blah", - packageUri: "https://test.fake/pacakge.zip", - hash: "abc123", - spec: { - name: "", - version: "0.1.0", - sourceUrl: "", - roles: [], - resources: [], - params: [], - }, - }, - {} - ) - ).to.be.rejectedWith(FirebaseError, "HTTP Error: 500, Unknown Error"); - expect(nock.isDone()).to.be.true; - }); - - it("should include config.extension_ref and config.extension_version for an update to a published extension", async () => { - nock(api.extensionsOrigin) - .patch(`/${VERSION}/projects/${PROJECT_ID}/instances/${INSTANCE_ID}`) - .query({ updateMask: "config.extension_ref,config.extension_version" }) - .reply(200, { name: "operations/abc123" }); - nock(api.extensionsOrigin).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); - - await extensionsApi.updateInstanceFromRegistry( - PROJECT_ID, - INSTANCE_ID, - "extens-test/firestore-translate-text" - ); - - 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}/instances/`) - .reply(200, { name: "operations/abc123" }); - nock(api.extensionsOrigin).get(`/${VERSION}/operations/abc123`).reply(502, {}); - - await expect( - extensionsApi.createInstance(PROJECT_ID, INSTANCE_ID, { - name: "sources/blah", - packageUri: "https://test.fake/pacakge.zip", - hash: "abc123", - spec: { - name: "", - version: "0.1.0", - sourceUrl: "", - roles: [], - resources: [], - params: [], - }, - }) - ).to.be.rejectedWith(FirebaseError, "HTTP Error: 502, 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" }) - .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(PROJECT_ID, INSTANCE_ID, { MY_PARAM: "value" }); - 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" }) - .reply(500); - - await expect( - extensionsApi.configureInstance(PROJECT_ID, INSTANCE_ID, { 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: [], - 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" }) - .reply(200, { name: "operations/abc123" }); - nock(api.extensionsOrigin).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); - - await extensionsApi.updateInstance(PROJECT_ID, INSTANCE_ID, testSource, { - 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" }) - .reply(200, { name: "operations/abc123" }); - nock(api.extensionsOrigin).get(`/${VERSION}/operations/abc123`).reply(200, { done: true }); - - await extensionsApi.updateInstance(PROJECT_ID, INSTANCE_ID, testSource); - - 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" }) - .reply(500); - - await expect( - extensionsApi.updateInstance(PROJECT_ID, INSTANCE_ID, testSource, { 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("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}@`) - ).to.be.rejectedWith(FirebaseError, "Extension reference must be in format"); - }); -}); - -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, - "Extension reference must be in format" - ); - }); -}); - -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, "Extension reference must be in format"); - }); -}); - -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) => { - queryParams.pageSize === "100"; - return queryParams; - }) - .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 return a list of all extension versions", async () => { - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) - .query((queryParams: any) => { - queryParams.pageSize === "100"; - return queryParams; - }) - .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) => { - queryParams.pageSize === "100"; - return queryParams; - }) - .reply(200, PUBLISHED_VERSIONS_WITH_TOKEN); - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) - .query((queryParams: any) => { - queryParams.pageSize === "100"; - queryParams.pageToken === NEXT_PAGE_TOKEN; - return queryParams; - }) - .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) => { - queryParams.pageSize === "100"; - return queryParams; - }) - .reply(200, PUBLISHED_VERSIONS_WITH_TOKEN); - nock(api.extensionsOrigin) - .get(`/${VERSION}/publishers/${PUBLISHER_ID}/extensions/${EXTENSION_ID}/versions`) - .query((queryParams: any) => { - queryParams.pageSize === "100"; - queryParams.nextPageToken === NEXT_PAGE_TOKEN; - return queryParams; - }) - .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, - "Extension reference must be in format" - ); - }); -}); - -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; - }); -}); diff --git a/src/test/extensions/extensionsHelper.spec.ts b/src/test/extensions/extensionsHelper.spec.ts deleted file mode 100644 index 0769159aeee..00000000000 --- a/src/test/extensions/extensionsHelper.spec.ts +++ /dev/null @@ -1,868 +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}", - ENV_VAR_FOUR: "users/{sender}.friends", - }; - - const exampleParamSpec = [ - { - param: "ENV_VAR_ONE", - required: true, - }, - { - param: "ENV_VAR_TWO", - 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", - 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", - 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: "ENV_VAR_ONE", - required: true, - }, - { - param: "ENV_VAR_TWO", - 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", - 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", - 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 a 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: [], - 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: [], - 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: [], - 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 has an invalid default.", () => { - const testSpec = { - version: "0.1.0", - specVersion: "v1beta", - params: [ - { type: extensionsHelper.SpecParamType.STRING, validationRegex: "test", default: "fail" }, - ], - resources: [], - sourceUrl: "https://test-source.fake", - license: "apache-2.0", - }; - - expect(() => { - extensionsHelper.validateSpec(testSpec); - }).to.throw(FirebaseError, /default/); - }); - }); - - 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: [], - }, - }; - - beforeEach(() => { - archiveStub = sinon.stub(archiveDirectory, "archiveDirectory").resolves({}); - uploadStub = sinon - .stub(storage, "uploadObject") - .resolves("/firebase-ext-eap-uploads/object.zip"); - 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, - }, - }; - const testSource: ExtensionSource = { - name: "test", - packageUri: "", - hash: "abc123", - state: "ACTIVE", - spec: { - name: "", - version: "0.0.0", - sourceUrl: "", - resources: [], - }, - }; - - 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/listExtensions.spec.ts b/src/test/extensions/listExtensions.spec.ts deleted file mode 100644 index e8f8c429d45..00000000000 --- a/src/test/extensions/listExtensions.spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import * as extensionsApi from "../../extensions/extensionsApi"; -import { listExtensions } from "../../extensions/listExtensions"; - -const MOCK_INSTANCES = [ - { - name: "projects/my-test-proj/instances/image-resizer", - createTime: "2019-05-19T00:20:10.416947Z", - updateTime: "2019-05-19T00:20:10.416947Z", - state: "ACTIVE", - config: { - name: - "projects/my-test-proj/instances/image-resizer/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", - createTime: "2019-05-19T00:20:10.416947Z", - source: { - state: "ACTIVE", - spec: { - author: { - authorName: "Firebase", - url: "https://firebase.google.com", - }, - }, - }, - }, - }, - { - name: "projects/my-test-proj/instances/image-resizer-1", - createTime: "2019-06-19T00:20:10.416947Z", - updateTime: "2019-06-19T00:21:06.722782Z", - state: "ACTIVE", - config: { - name: - "projects/my-test-proj/instances/image-resizer-1/configurations/5b1fb749-764d-4bd1-af60-bb7f22d27860", - createTime: "2019-06-19T00:21:06.722782Z", - }, - }, -]; - -const PROJECT_ID = "my-test-proj"; - -describe("listExtensions", () => { - let listInstancesStub: sinon.SinonStub; - - beforeEach(() => { - listInstancesStub = sinon.stub(extensionsApi, "listInstances"); - }); - - afterEach(() => { - listInstancesStub.restore(); - }); - - it("should return an empty array if no extensions have been installed", async () => { - listInstancesStub.returns(Promise.resolve([])); - - const result = await listExtensions(PROJECT_ID); - - expect(result).to.eql({ instances: [] }); - }); - - it("should return a sorted array of extension instances", async () => { - listInstancesStub.returns(Promise.resolve(MOCK_INSTANCES)); - - const result = await listExtensions(PROJECT_ID); - - const expected = [ - { - name: "projects/my-test-proj/instances/image-resizer-1", - createTime: "2019-06-19T00:20:10.416947Z", - updateTime: "2019-06-19T00:21:06.722782Z", - state: "ACTIVE", - config: { - name: - "projects/my-test-proj/instances/image-resizer-1/configurations/5b1fb749-764d-4bd1-af60-bb7f22d27860", - createTime: "2019-06-19T00:21:06.722782Z", - }, - }, - { - name: "projects/my-test-proj/instances/image-resizer", - createTime: "2019-05-19T00:20:10.416947Z", - updateTime: "2019-05-19T00:20:10.416947Z", - state: "ACTIVE", - config: { - name: - "projects/my-test-proj/instances/image-resizer/configurations/95355951-397f-4821-a5c2-9c9788b2cc63", - createTime: "2019-05-19T00:20:10.416947Z", - source: { - state: "ACTIVE", - spec: { - author: { - authorName: "Firebase", - url: "https://firebase.google.com", - }, - }, - }, - }, - }, - ]; - expect(result).to.eql({ instances: expected }); - }); -}); 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 532b92b9f90..00000000000 --- a/src/test/extensions/paramHelper.spec.ts +++ /dev/null @@ -1,441 +0,0 @@ -import * as _ from "lodash"; -import { expect } from "chai"; -import * as sinon from "sinon"; -import * as dotenv from "dotenv"; -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 prompt from "../../prompt"; - -const PROJECT_ID = "test-proj"; -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, - }, -]; - -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", - }, -]; - -const SPEC = { - name: "test", - version: "0.1.0", - roles: [], - resources: [], - sourceUrl: "test.com", - params: TEST_PARAMS, -}; - -describe("paramHelper", () => { - describe("getParams", () => { - let dotenvStub: sinon.SinonStub; - let promptStub: sinon.SinonStub; - let loggerSpy: sinon.SinonSpy; - - beforeEach(() => { - sinon.stub(fs, "readFileSync").returns(""); - dotenvStub = sinon.stub(dotenv, "parse"); - sinon.stub(extensionsHelper, "getFirebaseProjectParams").resolves({ PROJECT_ID }); - promptStub = sinon.stub(prompt, "promptOnce").resolves("user input"); - loggerSpy = sinon.spy(logger, "info"); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should read params from envFilePath if it is provided and is valid", async () => { - dotenvStub.returns({ - A_PARAMETER: "aValue", - ANOTHER_PARAMETER: "value", - }); - - const params = await paramHelper.getParams(PROJECT_ID, TEST_PARAMS, "./a/path/to/a/file.env"); - - expect(params).to.eql({ - A_PARAMETER: "aValue", - ANOTHER_PARAMETER: "value", - }); - }); - - it("should return the defaults for params that are not in envFilePath", async () => { - dotenvStub.returns({ - A_PARAMETER: "aValue", - }); - - const params = await paramHelper.getParams(PROJECT_ID, TEST_PARAMS, "./a/path/to/a/file.env"); - - expect(params).to.eql({ - A_PARAMETER: "aValue", - ANOTHER_PARAMETER: "default", - }); - }); - - it("should omit optional params that are not in envFilePath", async () => { - dotenvStub.returns({ - ANOTHER_PARAMETER: "aValue", - }); - - const params = await paramHelper.getParams( - PROJECT_ID, - TEST_PARAMS_3, - "./a/path/to/a/file.env" - ); - - expect(params).to.eql({ - ANOTHER_PARAMETER: "aValue", - }); - }); - - it("should throw if a required param without a default is not in envFilePath", async () => { - dotenvStub.returns({ - ANOTHER_PARAMETER: "aValue", - }); - - await expect( - paramHelper.getParams(PROJECT_ID, TEST_PARAMS, "./a/path/to/a/file.env") - ).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 () => { - dotenvStub.returns({ - A_PARAMETER: "aValue", - ANOTHER_PARAMETER: "default", - A_THIRD_PARAMETER: "aValue", - A_FOURTH_PARAMETER: "default", - }); - await paramHelper.getParams(PROJECT_ID, TEST_PARAMS, "./a/path/to/a/file.env"); - - 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 () => { - dotenvStub.throws({ message: "Error during parsing" }); - - await expect( - paramHelper.getParams(PROJECT_ID, TEST_PARAMS, "./a/path/to/a/file.env") - ).to.be.rejectedWith(FirebaseError, "Error reading env file: Error during parsing"); - }); - - it("should prompt the user for params if no env file is provided", async () => { - const params = await paramHelper.getParams(PROJECT_ID, TEST_PARAMS); - - 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", - 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, - newSpec, - { - A_PARAMETER: "value", - ANOTHER_PARAMETER: "value", - }, - PROJECT_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, - newSpec, - { - A_PARAMETER: "value", - ANOTHER_PARAMETER: "value", - }, - PROJECT_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, - newSpec, - { - A_PARAMETER: "value", - ANOTHER_PARAMETER: "value", - }, - PROJECT_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, - newSpec, - { - A_PARAMETER: "value", - ANOTHER_PARAMETER: "value", - }, - PROJECT_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, - newSpec, - { - A_PARAMETER: "value", - ANOTHER_PARAMETER: "value", - }, - PROJECT_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 62b2b796f2a..00000000000 --- a/src/test/extensions/resolveSource.spec.ts +++ /dev/null @@ -1,93 +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", - }, - 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("checkForUpdateWarnings", () => { - let confirmUpdateWarningSpy: sinon.SinonStub; - - beforeEach(() => { - confirmUpdateWarningSpy = sinon.stub(resolveSource, "confirmUpdateWarning").resolves(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should display the correct warning", async () => { - await resolveSource.promptForUpdateWarnings(testRegistryEntry, "0.1.0", "0.2.0"); - - const expectedUpdateWarning = { - 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.", - }; - expect(confirmUpdateWarningSpy).to.have.been.calledWith(expectedUpdateWarning); - }); - - it("should display no warnings if none are applicable", async () => { - await resolveSource.promptForUpdateWarnings(testRegistryEntry, "0.1.1", "0.1.2"); - - expect(confirmUpdateWarningSpy).not.to.have.been.called; - }); -}); - -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/updateHelper.spec.ts b/src/test/extensions/updateHelper.spec.ts deleted file mode 100644 index 706698bf679..00000000000 --- a/src/test/extensions/updateHelper.spec.ts +++ /dev/null @@ -1,531 +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.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" }, - contributors: [{ authorName: "Tester 2" }], - billingRequired: true, - sourceUrl: "test.com", - params: [], -}; - -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", - spec: SPEC, - 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/image-resizer/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/fake-official-instance", - 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", - sourceId: "fake-registry-source", - sourceName: "projects/firebasemods/sources/fake-registry-source", - source: { - name: "projects/firebasemods/sources/fake-registry-source", - }, - }, -}; - -describe("updateHelper", () => { - describe("updateFromLocalSource", () => { - let promptStub: sinon.SinonStub; - let createSourceStub: sinon.SinonStub; - let getInstanceStub: sinon.SinonStub; - - beforeEach(() => { - promptStub = sinon.stub(prompt, "promptOnce"); - 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(() => { - promptStub.restore(); - createSourceStub.restore(); - getInstanceStub.restore(); - - nock.cleanAll(); - }); - - it("should return the correct source name for a valid local source", async () => { - promptStub.resolves(true); - createSourceStub.resolves(SOURCE); - const name = await updateHelper.updateFromLocalSource( - "test-project", - "test-instance", - ".", - SPEC, - SPEC.name - ); - expect(name).to.equal(SOURCE.name); - }); - - it("should throw an error for an invalid source", async () => { - promptStub.resolves(true); - createSourceStub.throwsException("Invalid source"); - await expect( - updateHelper.updateFromLocalSource("test-project", "test-instance", ".", SPEC, SPEC.name) - ).to.be.rejectedWith(FirebaseError, "Unable to update from the source"); - }); - - it("should not update if the update warning is not confirmed", async () => { - promptStub.resolves(false); - createSourceStub.resolves(SOURCE); - await expect( - updateHelper.updateFromLocalSource("test-project", "test-instance", ".", SPEC, SPEC.name) - ).to.be.rejectedWith(FirebaseError, "Update cancelled."); - }); - }); - - describe("updateFromUrlSource", () => { - let promptStub: sinon.SinonStub; - let createSourceStub: sinon.SinonStub; - let getInstanceStub: sinon.SinonStub; - - beforeEach(() => { - promptStub = sinon.stub(prompt, "promptOnce"); - 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(() => { - promptStub.restore(); - createSourceStub.restore(); - getInstanceStub.restore(); - - nock.cleanAll(); - }); - - it("should return the correct source name for a valid url source", async () => { - promptStub.resolves(true); - createSourceStub.resolves(SOURCE); - const name = await updateHelper.updateFromUrlSource( - "test-project", - "test-instance", - "https://valid-source.tar.gz", - SPEC, - SPEC.name - ); - expect(name).to.equal(SOURCE.name); - }); - - it("should throw an error for an invalid source", async () => { - promptStub.resolves(true); - createSourceStub.throws("Invalid source"); - await expect( - updateHelper.updateFromUrlSource( - "test-project", - "test-instance", - "https://valid-source.tar.gz", - SPEC, - SPEC.name - ) - ).to.be.rejectedWith(FirebaseError, "Unable to update from the source"); - }); - - it("should not update if the update warning is not confirmed", async () => { - promptStub.resolves(false); - createSourceStub.resolves(SOURCE); - await expect( - updateHelper.updateFromUrlSource( - "test-project", - "test-instance", - "https://valid-source.tar.gz", - SPEC, - SPEC.name - ) - ).to.be.rejectedWith(FirebaseError, "Update cancelled."); - }); - }); - - describe("updateToVersionFromPublisherSource", () => { - let promptStub: sinon.SinonStub; - let getExtensionStub: sinon.SinonStub; - let createSourceStub: sinon.SinonStub; - let registryStub: sinon.SinonStub; - let isOfficialStub: sinon.SinonStub; - let getInstanceStub: sinon.SinonStub; - - beforeEach(() => { - promptStub = sinon.stub(prompt, "promptOnce"); - getExtensionStub = sinon.stub(extensionsApi, "getExtension"); - createSourceStub = sinon.stub(extensionsApi, "getExtensionVersion"); - 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(() => { - promptStub.restore(); - getExtensionStub.restore(); - createSourceStub.restore(); - registryStub.restore(); - isOfficialStub.restore(); - getInstanceStub.restore(); - }); - - it("should return the correct source name for a valid published extension version source", async () => { - promptStub.resolves(true); - getExtensionStub.resolves(EXTENSION); - createSourceStub.resolves(EXTENSION_VERSION); - const name = await updateHelper.updateToVersionFromPublisherSource( - "test-project", - "test-instance", - "test-publisher/test@0.2.0", - SPEC, - SPEC.name - ); - expect(name).to.equal(EXTENSION_VERSION.name); - }); - - it("should throw an error for an invalid source", async () => { - promptStub.resolves(true); - getExtensionStub.throws(Error("NOT FOUND")); - createSourceStub.throws(Error("NOT FOUND")); - await expect( - updateHelper.updateToVersionFromPublisherSource( - "test-project", - "test-instance", - "test-publisher/test@1.2.3", - SPEC, - SPEC.name - ) - ).to.be.rejectedWith("NOT FOUND"); - }); - - it("should not update if the update warning is not confirmed", async () => { - promptStub.resolves(false); - getExtensionStub.resolves(EXTENSION); - createSourceStub.resolves(EXTENSION_VERSION); - await expect( - updateHelper.updateToVersionFromPublisherSource( - "test-project", - "test-instance", - "test-publisher/test@0.2.0", - SPEC, - SPEC.name - ) - ).to.be.rejectedWith(FirebaseError, "Update cancelled."); - }); - }); - - describe("updateFromPublisherSource", () => { - let promptStub: sinon.SinonStub; - let getExtensionStub: sinon.SinonStub; - let createSourceStub: sinon.SinonStub; - let registryStub: sinon.SinonStub; - let isOfficialStub: sinon.SinonStub; - let getInstanceStub: sinon.SinonStub; - - beforeEach(() => { - promptStub = sinon.stub(prompt, "promptOnce"); - getExtensionStub = sinon.stub(extensionsApi, "getExtension"); - createSourceStub = sinon.stub(extensionsApi, "getExtensionVersion"); - 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(() => { - promptStub.restore(); - getExtensionStub.restore(); - createSourceStub.restore(); - registryStub.restore(); - isOfficialStub.restore(); - getInstanceStub.restore(); - }); - - it("should return the correct source name for the latest published extension source", async () => { - promptStub.resolves(true); - getExtensionStub.resolves(EXTENSION); - createSourceStub.resolves(EXTENSION_VERSION); - const name = await updateHelper.updateToVersionFromPublisherSource( - "test-project", - "test-instance", - "test-publisher/test", - SPEC, - SPEC.name - ); - expect(name).to.equal(EXTENSION_VERSION.name); - }); - - it("should throw an error for an invalid source", async () => { - promptStub.resolves(true); - getExtensionStub.throws(Error("NOT FOUND")); - createSourceStub.throws(Error("NOT FOUND")); - await expect( - updateHelper.updateToVersionFromPublisherSource( - "test-project", - "test-instance", - "test-publisher/test", - SPEC, - SPEC.name - ) - ).to.be.rejectedWith("NOT FOUND"); - }); - - it("should not update if the update warning is not confirmed", async () => { - promptStub.resolves(false); - getExtensionStub.resolves(EXTENSION); - createSourceStub.resolves(EXTENSION_VERSION); - await expect( - updateHelper.updateToVersionFromPublisherSource( - "test-project", - "test-instance", - "test-publisher/test", - SPEC, - SPEC.name - ) - ).to.be.rejectedWith(FirebaseError, "Update cancelled."); - }); - }); - - describe("updateToVersionFromOfficialSource", () => { - let promptStub: sinon.SinonStub; - let createSourceStub: sinon.SinonStub; - let registryEntryStub: sinon.SinonStub; - let isOfficialStub: sinon.SinonStub; - let getInstanceStub: sinon.SinonStub; - - beforeEach(() => { - promptStub = sinon.stub(prompt, "promptOnce"); - createSourceStub = sinon.stub(extensionsApi, "getExtensionVersion"); - registryEntryStub = sinon.stub(resolveSource, "resolveRegistryEntry"); - registryEntryStub.resolves(REGISTRY_ENTRY); - isOfficialStub = sinon.stub(resolveSource, "isOfficialSource"); - isOfficialStub.returns(true); - getInstanceStub = sinon.stub(extensionsApi, "getInstance").resolves(INSTANCE); - }); - - afterEach(() => { - promptStub.restore(); - createSourceStub.restore(); - registryEntryStub.restore(); - isOfficialStub.restore(); - getInstanceStub.restore(); - }); - - it("should return the correct source name for a valid published source", async () => { - promptStub.resolves(true); - registryEntryStub.resolves(REGISTRY_ENTRY); - const name = await updateHelper.updateToVersionFromRegistry( - "test-project", - "test-instance", - SPEC, - SPEC.name, - "0.1.2" - ); - expect(name).to.equal("projects/firebasemods/sources/123"); - }); - - it("should throw an error for an invalid source", async () => { - promptStub.resolves(true); - registryEntryStub.throws("Unable to find extension source"); - await expect( - updateHelper.updateToVersionFromRegistry( - "test-project", - "test-instance", - SPEC, - SPEC.name, - "0.1.1" - ) - ).to.be.rejectedWith(FirebaseError, "Cannot find the latest version of this extension."); - }); - - it("should not update if the update warning is not confirmed", async () => { - promptStub.resolves(false); - await expect( - updateHelper.updateToVersionFromRegistry( - "test-project", - "test-instance", - SPEC, - SPEC.name, - "0.1.2" - ) - ).to.be.rejectedWith(FirebaseError, "Update cancelled."); - }); - - it("should not update if version given less than min version required", async () => { - await expect( - updateHelper.updateToVersionFromRegistry( - "test-project", - "test-instance", - SPEC, - SPEC.name, - "0.1.0" - ) - ).to.be.rejectedWith(FirebaseError, "is less than the minimum version required"); - }); - }); - - describe("updateFromOfficialSource", () => { - let promptStub: sinon.SinonStub; - let createSourceStub: sinon.SinonStub; - let registryEntryStub: sinon.SinonStub; - let isOfficialStub: sinon.SinonStub; - let getInstanceStub: sinon.SinonStub; - - beforeEach(() => { - promptStub = sinon.stub(prompt, "promptOnce"); - createSourceStub = sinon.stub(extensionsApi, "getExtensionVersion"); - registryEntryStub = sinon.stub(resolveSource, "resolveRegistryEntry"); - registryEntryStub.resolves(REGISTRY_ENTRY); - isOfficialStub = sinon.stub(resolveSource, "isOfficialSource"); - isOfficialStub.returns(true); - getInstanceStub = sinon.stub(extensionsApi, "getInstance").resolves(INSTANCE); - }); - - afterEach(() => { - promptStub.restore(); - createSourceStub.restore(); - registryEntryStub.restore(); - isOfficialStub.restore(); - getInstanceStub.restore(); - }); - - it("should return the correct source name for a valid published source", async () => { - promptStub.resolves(true); - const name = await updateHelper.updateFromRegistry( - "test-project", - "test-instance", - SPEC, - SPEC.name - ); - expect(name).to.equal("projects/firebasemods/sources/abc"); - }); - - it("should throw an error for an invalid source", async () => { - promptStub.resolves(true); - registryEntryStub.throws("Unable to find extension source"); - await expect( - updateHelper.updateFromRegistry("test-project", "test-instance", SPEC, SPEC.name) - ).to.be.rejectedWith(FirebaseError, "Cannot find the latest version of this extension."); - }); - - it("should not update if the update warning is not confirmed", async () => { - promptStub.resolves(false); - registryEntryStub.resolves(REGISTRY_ENTRY); - await expect( - updateHelper.updateFromRegistry("test-project", "test-instance", SPEC, SPEC.name) - ).to.be.rejectedWith(FirebaseError, "Update cancelled."); - }); - }); -}); 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/fetchWebSetup.spec.ts b/src/test/fetchWebSetup.spec.ts deleted file mode 100644 index 8996998658a..00000000000 --- a/src/test/fetchWebSetup.spec.ts +++ /dev/null @@ -1,97 +0,0 @@ -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"; - -describe("fetchWebSetup module", () => { - afterEach(() => { - expect(nock.isDone()).to.be.true; - }); - - describe("fetchWebSetup", () => { - let configSetStub: sinon.SinonStub; - - beforeEach(() => { - sinon.stub(configstore, "get"); - configSetStub = sinon.stub(configstore, "set").returns(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should fetch the web app config", async () => { - const projectId = "foo"; - nock(firebaseApiOrigin) - .get(`/v1beta1/projects/${projectId}/webApps/-/config`) - .reply(200, { some: "config" }); - - const config = await fetchWebSetup({ project: projectId }); - - expect(config).to.deep.equal({ some: "config" }); - }); - - it("should store the fetched config", async () => { - const projectId = "projectId"; - nock(firebaseApiOrigin) - .get(`/v1beta1/projects/${projectId}/webApps/-/config`) - .reply(200, { projectId, some: "config" }); - - await fetchWebSetup({ project: projectId }); - - expect(configSetStub).to.have.been.calledOnceWith("webconfig", { - [projectId]: { - projectId, - some: "config", - }, - }); - expect(nock.isDone()).to.be.true; - }); - - it("should throw an error if the request fails", async () => { - const projectId = "foo"; - 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" - ); - }); - }); - - describe("getCachedWebSetup", () => { - let configGetStub: sinon.SinonStub; - - beforeEach(() => { - sinon.stub(configstore, "set").returns(); - configGetStub = sinon.stub(configstore, "get"); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should return no config if none is cached", () => { - configGetStub.returns(undefined); - - const config = getCachedWebSetup({ project: "foo" }); - - expect(config).to.be.undefined; - }); - - it("should return a stored config", () => { - const projectId = "projectId"; - configGetStub.returns({ [projectId]: { project: projectId, some: "config" } }); - - const config = getCachedWebSetup({ project: projectId }); - - expect(config).to.be.deep.equal({ project: projectId, some: "config" }); - }); - }); -}); 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/empty-config/firebase.json b/src/test/fixtures/empty-config/firebase.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/test/fixtures/empty-config/index.ts b/src/test/fixtures/empty-config/index.ts new file mode 100644 index 00000000000..dbf104ec838 --- /dev/null +++ b/src/test/fixtures/empty-config/index.ts @@ -0,0 +1,8 @@ +import { resolve } from "path"; + +/** + * A directory containing an empty rebase.json. + */ +export const FIXTURE_DIR = __dirname; + +export const FIREBASE_JSON_PATH = resolve(__dirname, "firebase.json"); 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/extension-yamls/sample-ext-preinstall/extension.yaml b/src/test/fixtures/extension-yamls/sample-ext-preinstall/extension.yaml new file mode 100644 index 00000000000..d2144843599 --- /dev/null +++ b/src/test/fixtures/extension-yamls/sample-ext-preinstall/extension.yaml @@ -0,0 +1,10 @@ +specVersion: v1beta +name: fixture-ext-with-preinstall +version: 1.0.0 +license: apache-2.0 +resources: + - name: capitalizeMessages + type: firebaseextensions.v1beta.function +params: + - param: DO_BACKFILL + label: Do a backfill 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/extension-yamls/sample-ext/extension.yaml b/src/test/fixtures/extension-yamls/sample-ext/extension.yaml new file mode 100644 index 00000000000..47dc4463d22 --- /dev/null +++ b/src/test/fixtures/extension-yamls/sample-ext/extension.yaml @@ -0,0 +1,10 @@ +specVersion: v1beta +name: fixture-ext +version: 1.0.0 +license: apache-2.0 +resources: + - name: capitalizeMessages + type: firebaseextensions.v1beta.function +params: + - param: DO_BACKFILL + label: Do a backfill 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/extension-yamls/valid-yaml-invalid-spec/extension.yaml b/src/test/fixtures/extension-yamls/valid-yaml-invalid-spec/extension.yaml new file mode 100644 index 00000000000..d1b03715dea --- /dev/null +++ b/src/test/fixtures/extension-yamls/valid-yaml-invalid-spec/extension.yaml @@ -0,0 +1,4 @@ +specVersion: v1beta +name: fixture-ext-missing-resources +version: 1.0.0 +license: apache-2.0 diff --git a/src/test/fixtures/extension-yamls/valid-yaml-invalid-spec/index.ts b/src/test/fixtures/extension-yamls/valid-yaml-invalid-spec/index.ts new file mode 100644 index 00000000000..89d0eb4b7b8 --- /dev/null +++ b/src/test/fixtures/extension-yamls/valid-yaml-invalid-spec/index.ts @@ -0,0 +1,4 @@ +/** + * A valid yaml file, but not a valid extension spec (missing required fields); + */ +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/sample-ext-preinstall/extension.yaml b/src/test/fixtures/sample-ext-preinstall/extension.yaml deleted file mode 100644 index d1dae3bdc63..00000000000 --- a/src/test/fixtures/sample-ext-preinstall/extension.yaml +++ /dev/null @@ -1,3 +0,0 @@ -specVersion: v1beta -name: fixture-ext-with-preinstall -version: 1.0.0 diff --git a/src/test/fixtures/sample-ext/extension.yaml b/src/test/fixtures/sample-ext/extension.yaml deleted file mode 100644 index bd11bc1111e..00000000000 --- a/src/test/fixtures/sample-ext/extension.yaml +++ /dev/null @@ -1,3 +0,0 @@ -specVersion: v1beta -name: fixture-ext -version: 1.0.0 diff --git a/src/test/fixtures/simplehosting/firebase.json b/src/test/fixtures/simplehosting/firebase.json new file mode 100644 index 00000000000..9c1c0c967a7 --- /dev/null +++ b/src/test/fixtures/simplehosting/firebase.json @@ -0,0 +1,5 @@ +{ + "hosting": { + "public": "public" + } +} 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/simplehosting/public/index.html b/src/test/fixtures/simplehosting/public/index.html new file mode 100644 index 00000000000..ce013625030 --- /dev/null +++ b/src/test/fixtures/simplehosting/public/index.html @@ -0,0 +1 @@ +hello 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..c67c361d708 --- /dev/null +++ b/src/test/fixtures/zip-files/index.ts @@ -0,0 +1,19 @@ +import { join } from "path"; + +const zipFixturesDir = __dirname; +const testDataDir = join(zipFixturesDir, "node-unzipper-testData"); + +export const ZIP_CASES = [ + { name: "compressed-cp866" }, + { name: "compressed-directory-entry" }, + { name: "compressed-flags-set" }, + { name: "compressed-standard" }, + { name: "uncompressed" }, + { name: "zip-slip", wantErr: "a path outside of" }, + { name: "zip64" }, +].map(({ name, wantErr }) => ({ + name, + archivePath: join(testDataDir, name, "archive.zip"), + inflatedDir: join(testDataDir, name, "inflated"), + wantErr, +})); 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/fsutils.spec.ts b/src/test/fsutils.spec.ts deleted file mode 100644 index 3a7eddba71c..00000000000 --- a/src/test/fsutils.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { expect } from "chai"; - -import * as fsutils from "../fsutils"; - -describe("fsutils", () => { - describe("fileExistsSync", () => { - it("should return true if the file exists", () => { - expect(fsutils.fileExistsSync(__filename)).to.be.true; - }); - - it("should return false if the file does not exist", () => { - expect(fsutils.fileExistsSync(`${__filename}/nope.never`)).to.be.false; - }); - - it("should return false if the path is a directory", () => { - expect(fsutils.fileExistsSync(__dirname)).to.be.false; - }); - }); - - describe("dirExistsSync", () => { - it("should return true if the directory exists", () => { - expect(fsutils.dirExistsSync(__dirname)).to.be.true; - }); - - it("should return false if the directory does not exist", () => { - expect(fsutils.dirExistsSync(`${__dirname}/nope/never`)).to.be.false; - }); - - it("should return false if the path is a file", () => { - expect(fsutils.dirExistsSync(__filename)).to.be.false; - }); - }); -}); diff --git a/src/test/functionsDeployHelper.spec.ts b/src/test/functionsDeployHelper.spec.ts deleted file mode 100644 index ed8fa80366f..00000000000 --- a/src/test/functionsDeployHelper.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import * as helper from "../functionsDeployHelper"; -import * as prompt from "../prompt"; -import { FirebaseError } from "../error"; - -describe("functionsDeployHelper", () => { - describe("getFilterGroups", () => { - it("should parse multiple filters", () => { - const options = { - only: "functions:myFunc,functions:myOtherFunc", - }; - expect(helper.getFilterGroups(options)).to.deep.equal([["myFunc"], ["myOtherFunc"]]); - }); - it("should parse nested filters", () => { - const options = { - only: "functions:groupA.myFunc", - }; - expect(helper.getFilterGroups(options)).to.deep.equal([["groupA", "myFunc"]]); - }); - }); - - describe("getReleaseNames", () => { - it("should handle function update", () => { - const uploadNames = ["projects/myProject/locations/us-central1/functions/myFunc"]; - const existingNames = ["projects/myProject/locations/us-central1/functions/myFunc"]; - const filter = [["myFunc"]]; - - expect(helper.getReleaseNames(uploadNames, existingNames, filter)).to.deep.equal([ - "projects/myProject/locations/us-central1/functions/myFunc", - ]); - }); - - it("should handle function deletion", () => { - const uploadNames: string[] = []; - const existingNames = ["projects/myProject/locations/us-central1/functions/myFunc"]; - const filter = [["myFunc"]]; - - expect(helper.getReleaseNames(uploadNames, existingNames, filter)).to.deep.equal([ - "projects/myProject/locations/us-central1/functions/myFunc", - ]); - }); - - it("should handle function creation", () => { - const uploadNames = ["projects/myProject/locations/us-central1/functions/myFunc"]; - const existingNames: string[] = []; - const filter = [["myFunc"]]; - - expect(helper.getReleaseNames(uploadNames, existingNames, filter)).to.deep.equal([ - "projects/myProject/locations/us-central1/functions/myFunc", - ]); - }); - - it("should handle existing function not being in filter", () => { - const uploadNames = ["projects/myProject/locations/us-central1/functions/myFunc"]; - const existingNames = ["projects/myProject/locations/us-central1/functions/myFunc2"]; - const filter = [["myFunc"]]; - - expect(helper.getReleaseNames(uploadNames, existingNames, filter)).to.deep.equal([ - "projects/myProject/locations/us-central1/functions/myFunc", - ]); - }); - - it("should handle no functions satisfying filter", () => { - const uploadNames = ["projects/myProject/locations/us-central1/functions/myFunc2"]; - const existingNames = ["projects/myProject/locations/us-central1/functions/myFunc3"]; - const filter = [["myFunc"]]; - - expect(helper.getReleaseNames(uploadNames, existingNames, filter)).to.deep.equal([]); - }); - - it("should handle entire function groups", () => { - const uploadNames = ["projects/myProject/locations/us-central1/functions/myGroup-func1"]; - const existingNames = ["projects/myProject/locations/us-central1/functions/myGroup-func2"]; - const filter = [["myGroup"]]; - - expect(helper.getReleaseNames(uploadNames, existingNames, filter)).to.deep.equal([ - "projects/myProject/locations/us-central1/functions/myGroup-func1", - "projects/myProject/locations/us-central1/functions/myGroup-func2", - ]); - }); - - it("should handle functions within groups", () => { - const uploadNames = ["projects/myProject/locations/us-central1/functions/myGroup-func1"]; - const existingNames = ["projects/myProject/locations/us-central1/functions/myGroup-func2"]; - const filter = [["myGroup", "func1"]]; - - expect(helper.getReleaseNames(uploadNames, existingNames, filter)).to.deep.equal([ - "projects/myProject/locations/us-central1/functions/myGroup-func1", - ]); - }); - }); -}); diff --git a/src/test/gcp/cloudscheduler.spec.ts b/src/test/gcp/cloudscheduler.spec.ts deleted file mode 100644 index 104ae6178e7..00000000000 --- a/src/test/gcp/cloudscheduler.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { expect } from "chai"; -import * as _ from "lodash"; -import * as nock from "nock"; - -import { cloudscheduler } from "../../gcp"; -import { FirebaseError } from "../../error"; -import * as api from "../../api"; - -const VERSION = "v1beta1"; - -const TEST_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; - }); - }); -}); diff --git a/src/test/getProjectNumber.spec.ts b/src/test/getProjectNumber.spec.ts deleted file mode 100644 index d4bf6ce2133..00000000000 --- a/src/test/getProjectNumber.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; - -import { getProjectNumber } from "../getProjectNumber"; -import * as projects from "../management/projects"; - -describe("getProjectNumber", () => { - let getProjectStub: sinon.SinonStub; - - beforeEach(() => { - getProjectStub = sinon.stub(projects, "getFirebaseProject").throws(new Error("stubbed")); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should return the project number from options, if present", async () => { - const n = await getProjectNumber({ projectNumber: 1 }); - - expect(n).to.equal(1); - expect(getProjectStub).to.not.have.been.called; - }); - - it("should fetch the project number if necessary", async () => { - getProjectStub.returns({ projectNumber: 2 }); - - const n = await getProjectNumber({ project: "foo" }); - - expect(n).to.equal(2); - expect(getProjectStub).to.have.been.calledOnceWithExactly("foo"); - }); - - it("should reject with an error on an error", async () => { - getProjectStub.rejects(new Error("oh no")); - - await expect(getProjectNumber({ project: "foo" })).to.eventually.be.rejectedWith( - Error, - "oh no" - ); - }); -}); 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 e8d58bcae3c..00000000000 --- a/src/test/hosting/normalizedHostingConfigs.spec.ts +++ /dev/null @@ -1,187 +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); - } - }); - } - }); -}); 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/project.spec.ts b/src/test/init/features/project.spec.ts deleted file mode 100644 index a84eea6efb0..00000000000 --- a/src/test/init/features/project.spec.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { expect } from "chai"; -import * as _ from "lodash"; -import * as sinon from "sinon"; -import { configstore } from "../../../configstore"; - -import { doSetup } from "../../../init/features/project"; -import * as projectManager from "../../../management/projects"; -import * as prompt from "../../../prompt"; -import * as Config from "../../../config"; - -const TEST_FIREBASE_PROJECT: projectManager.FirebaseProjectMetadata = { - projectId: "my-project-123", - projectNumber: "123456789", - displayName: "my-project", - name: "projects/my-project", - resources: { - hostingSite: "my-project", - realtimeDatabaseInstance: "my-project", - storageBucket: "my-project.appspot.com", - locationId: "us-central", - }, -}; - -describe("project", () => { - const sandbox: sinon.SinonSandbox = sinon.createSandbox(); - let getProjectStub: sinon.SinonStub; - let createFirebaseProjectStub: sinon.SinonStub; - let getOrPromptProjectStub: sinon.SinonStub; - let addFirebaseProjectStub: sinon.SinonStub; - let promptAvailableProjectIdStub: sinon.SinonStub; - let promptOnceStub: sinon.SinonStub; - let promptStub: sinon.SinonStub; - let configstoreSetStub: sinon.SinonStub; - let emptyConfig: Config; - - beforeEach(() => { - getProjectStub = sandbox.stub(projectManager, "getFirebaseProject"); - createFirebaseProjectStub = sandbox.stub(projectManager, "createFirebaseProjectAndLog"); - getOrPromptProjectStub = sandbox.stub(projectManager, "getOrPromptProject"); - addFirebaseProjectStub = sandbox.stub(projectManager, "addFirebaseToCloudProjectAndLog"); - promptAvailableProjectIdStub = sandbox.stub(projectManager, "promptAvailableProjectId"); - promptStub = sandbox.stub(prompt, "prompt").throws("Unexpected prompt call"); - promptOnceStub = sandbox.stub(prompt, "promptOnce").throws("Unexpected promptOnce call"); - configstoreSetStub = sandbox.stub(configstore, "set").throws("Unexpected configstore set"); - emptyConfig = new Config("{}", {}); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe("doSetup", () => { - describe('with "Use an existing project" option', () => { - it("should set up the correct properties in the project", async () => { - const options = { project: "my-project" }; - const setup = { config: {}, rcfile: {} }; - getProjectStub.onFirstCall().resolves(TEST_FIREBASE_PROJECT); - promptOnceStub.onFirstCall().resolves("Use an existing project"); - getOrPromptProjectStub.onFirstCall().resolves(TEST_FIREBASE_PROJECT); - configstoreSetStub.onFirstCall().resolves(); - - await doSetup(setup, emptyConfig, options); - - expect(_.get(setup, "projectId")).to.deep.equal("my-project-123"); - expect(_.get(setup, "instance")).to.deep.equal("my-project"); - expect(_.get(setup, "projectLocation")).to.deep.equal("us-central"); - expect(_.get(setup.rcfile, "projects.default")).to.deep.equal("my-project-123"); - expect(promptOnceStub).to.not.be.called; - expect(getOrPromptProjectStub).to.not.be.called; - }); - }); - - describe('with "Create a new project" option', () => { - it("should create a new project and set up the correct properties", async () => { - const options = {}; - const setup = { config: {}, rcfile: {} }; - promptOnceStub.onFirstCall().resolves("Create a new project"); - const fakePromptFn = (promptAnswer: any) => { - promptAnswer.projectId = "my-project-123"; - promptAnswer.displayName = "my-project"; - }; - promptStub - .withArgs({}, projectManager.PROJECTS_CREATE_QUESTIONS) - .onFirstCall() - .callsFake(fakePromptFn); - createFirebaseProjectStub.resolves(TEST_FIREBASE_PROJECT); - configstoreSetStub.onFirstCall().resolves(); - - await doSetup(setup, emptyConfig, options); - - expect(_.get(setup, "projectId")).to.deep.equal("my-project-123"); - expect(_.get(setup, "instance")).to.deep.equal("my-project"); - expect(_.get(setup, "projectLocation")).to.deep.equal("us-central"); - expect(_.get(setup.rcfile, "projects.default")).to.deep.equal("my-project-123"); - expect(promptOnceStub).to.be.calledOnce; - expect(promptStub).to.be.calledOnce; - expect(createFirebaseProjectStub).to.be.calledOnceWith("my-project-123", { - displayName: "my-project", - }); - }); - - it("should throw if project ID is empty after prompt", async () => { - const options = {}; - const setup = { config: {}, rcfile: {} }; - promptOnceStub.onFirstCall().resolves("Create a new project"); - const fakePromptFn = (promptAnswer: any) => { - promptAnswer.projectId = ""; - }; - promptStub - .withArgs({}, projectManager.PROJECTS_CREATE_QUESTIONS) - .onFirstCall() - .callsFake(fakePromptFn); - configstoreSetStub.onFirstCall().resolves(); - - let err; - try { - await doSetup(setup, emptyConfig, options); - } catch (e) { - err = e; - } - - expect(err.message).to.equal("Project ID cannot be empty"); - expect(promptOnceStub).to.be.calledOnce; - expect(promptStub).to.be.calledOnce; - expect(createFirebaseProjectStub).to.be.not.called; - }); - }); - - describe('with "Add Firebase resources to GCP project" option', () => { - it("should add firebase resources and set up the correct properties", async () => { - const options = {}; - const setup = { config: {}, rcfile: {} }; - promptOnceStub - .onFirstCall() - .resolves("Add Firebase to an existing Google Cloud Platform project"); - promptAvailableProjectIdStub.onFirstCall().resolves("my-project-123"); - addFirebaseProjectStub.onFirstCall().resolves(TEST_FIREBASE_PROJECT); - configstoreSetStub.onFirstCall().resolves(); - - await doSetup(setup, emptyConfig, options); - - expect(_.get(setup, "projectId")).to.deep.equal("my-project-123"); - expect(_.get(setup, "instance")).to.deep.equal("my-project"); - expect(_.get(setup, "projectLocation")).to.deep.equal("us-central"); - expect(_.get(setup.rcfile, "projects.default")).to.deep.equal("my-project-123"); - expect(promptOnceStub).to.be.calledOnce; - expect(promptAvailableProjectIdStub).to.be.calledOnce; - expect(addFirebaseProjectStub).to.be.calledOnceWith("my-project-123"); - }); - - it("should throw if project ID is empty after prompt", async () => { - const options = {}; - const setup = { config: {}, rcfile: {} }; - promptOnceStub - .onFirstCall() - .resolves("Add Firebase to an existing Google Cloud Platform project"); - promptAvailableProjectIdStub.onFirstCall().resolves(""); - - let err; - try { - await doSetup(setup, emptyConfig, options); - } catch (e) { - err = e; - } - - expect(err.message).to.equal("Project ID cannot be empty"); - expect(promptOnceStub).to.be.calledOnce; - expect(promptAvailableProjectIdStub).to.be.calledOnce; - expect(addFirebaseProjectStub).to.be.not.called; - }); - }); - - describe(`with "Don't set up a default project" option`, () => { - it("should set up the correct properties when not choosing a project", async () => { - const options = {}; - const setup = { config: {}, rcfile: {} }; - promptOnceStub.resolves("Don't set up a default project"); - - await doSetup(setup, emptyConfig, options); - - expect(setup).to.deep.equal({ config: {}, rcfile: {}, project: {} }); - expect(promptOnceStub).to.be.calledOnce; - }); - }); - - describe("with defined .firebaserc file", () => { - let options: any; - let setup: any; - - beforeEach(() => { - options = {}; - setup = { config: {}, rcfile: { projects: { default: "my-project-123" } } }; - getProjectStub.onFirstCall().resolves(TEST_FIREBASE_PROJECT); - }); - - it("should not prompt", async () => { - await doSetup(setup, emptyConfig, options); - - expect(promptOnceStub).to.be.not.called; - expect(promptStub).to.be.not.called; - }); - - it("should set project location even if .firebaserc is already set up", async () => { - await doSetup(setup, emptyConfig, options); - - expect(_.get(setup, "projectId")).to.equal("my-project-123"); - expect(_.get(setup, "projectLocation")).to.equal("us-central"); - expect(getProjectStub).to.be.calledOnceWith("my-project-123"); - }); - }); - }); -}); diff --git a/src/test/init/features/storage.spec.ts b/src/test/init/features/storage.spec.ts deleted file mode 100644 index cecc0a51a9f..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 * as Config from "../../../config"; -import { doSetup } from "../../../init/features/storage"; -import * as prompt from "../../../prompt"; - -describe("storage", () => { - const sandbox: sinon.SinonSandbox = sinon.createSandbox(); - let writeProjectFileStub: sinon.SinonStub; - let promptStub: sinon.SinonStub; - - beforeEach(() => { - writeProjectFileStub = sandbox.stub(Config.prototype, "writeProjectFile"); - 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"); - writeProjectFileStub.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/parseRuntimeAndValidateSDK.spec.ts b/src/test/parseRuntimeAndValidateSDK.spec.ts deleted file mode 100644 index 4214ec202d5..00000000000 --- a/src/test/parseRuntimeAndValidateSDK.spec.ts +++ /dev/null @@ -1,154 +0,0 @@ -import * as sinon from "sinon"; -import { expect } from "chai"; -import * as utils from "../utils"; -import * as runtime from "../parseRuntimeAndValidateSDK"; -import * as checkFirebaseSDKVersion from "../checkFirebaseSDKVersion"; -import { FirebaseError } from "../error"; -// Have to disable this because no @types/cjson available -// eslint-disable-next-line @typescript-eslint/no-var-requires -const cjson = require("cjson"); - -describe("getHumanFriendlyRuntimeName", () => { - it("should properly convert raw runtime to human friendly runtime", () => { - expect(runtime.getHumanFriendlyRuntimeName("nodejs6")).to.contain("Node.js"); - }); -}); - -describe("getRuntimeChoice", () => { - const sandbox = sinon.createSandbox(); - let cjsonStub: sinon.SinonStub; - let warningSpy: sinon.SinonSpy; - let SDKVersionStub: sinon.SinonStub; - - beforeEach(() => { - cjsonStub = sandbox.stub(cjson, "load"); - warningSpy = sandbox.spy(utils, "logWarning"); - SDKVersionStub = sandbox.stub(checkFirebaseSDKVersion, "getFunctionsSDKVersion"); - }); - - afterEach(() => { - sandbox.restore(); - }); - - context("when the runtime is set in firebase.json", () => { - it("should error if runtime field is set to node 6", () => { - SDKVersionStub.returns("2.0.0"); - - 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", () => { - SDKVersionStub.returns("2.0.0"); - - 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", () => { - SDKVersionStub.returns("3.4.0"); - - expect(runtime.getRuntimeChoice("path/to/source", "nodejs10")).to.equal("nodejs10"); - expect(warningSpy).not.called; - }); - - it("should return node 12 if runtime field is set to node 12", () => { - SDKVersionStub.returns("3.4.0"); - - expect(runtime.getRuntimeChoice("path/to/source", "nodejs12")).to.equal("nodejs12"); - expect(warningSpy).not.called; - }); - - it("should return node 14 if runtime field is set to node 14", () => { - SDKVersionStub.returns("3.4.0"); - - expect(runtime.getRuntimeChoice("path/to/source", "nodejs14")).to.equal("nodejs14"); - expect(warningSpy).not.called; - }); - - it("should print warning when firebase-functions version is below 2.0.0", () => { - SDKVersionStub.returns("0.5.0"); - - runtime.getRuntimeChoice("path/to/source", "nodejs10"); - expect(warningSpy).calledWith(runtime.FUNCTIONS_SDK_VERSION_TOO_OLD_WARNING); - }); - - 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" } }); - SDKVersionStub.returns("2.0.0"); - - 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" } }); - SDKVersionStub.returns("2.0.0"); - - 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" } }); - SDKVersionStub.returns("3.4.0"); - - expect(runtime.getRuntimeChoice("path/to/source", "")).to.equal("nodejs10"); - expect(warningSpy).not.called; - }); - - it("should return node 12 if engines field is set to node 12", () => { - cjsonStub.returns({ engines: { node: "12" } }); - SDKVersionStub.returns("3.4.0"); - - expect(runtime.getRuntimeChoice("path/to/source", "")).to.equal("nodejs12"); - expect(warningSpy).not.called; - }); - - it("should return node 14 if engines field is set to node 12", () => { - cjsonStub.returns({ engines: { node: "14" } }); - SDKVersionStub.returns("3.4.0"); - - expect(runtime.getRuntimeChoice("path/to/source", "")).to.equal("nodejs14"); - expect(warningSpy).not.called; - }); - - it("should print warning when firebase-functions version is below 2.0.0", () => { - cjsonStub.returns({ engines: { node: "10" } }); - SDKVersionStub.returns("0.5.0"); - - runtime.getRuntimeChoice("path/to/source", ""); - expect(warningSpy).calledWith(runtime.FUNCTIONS_SDK_VERSION_TOO_OLD_WARNING); - }); - - 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"); - expect(warningSpy).not.called; - }); - - 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/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/prompt.spec.ts b/src/test/prompt.spec.ts deleted file mode 100644 index 0adce547099..00000000000 --- a/src/test/prompt.spec.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { expect } from "chai"; -import * as sinon from "sinon"; -import * as inquirer from "inquirer"; - -import { FirebaseError } from "../error"; -import * as prompt from "../prompt"; - -describe("prompt", () => { - let inquirerStub: sinon.SinonStub; - const PROMPT_RESPONSES = { - lint: true, - project: "the-best-project-ever", - }; - - beforeEach(() => { - // Stub inquirer to return a set of fake answers. - inquirerStub = sinon.stub(inquirer, "prompt").resolves(PROMPT_RESPONSES); - }); - - afterEach(() => { - sinon.restore(); - }); - - describe("prompt", () => { - it("should error if questions are asked in nonInteractive environment", async () => { - const o = { nonInteractive: true }; - const qs: prompt.Question[] = [{ name: "foo" }]; - - await expect(prompt.prompt(o, qs)).to.be.rejectedWith( - FirebaseError, - /required.+non-interactive/ - ); - }); - - it("should utilize inquirer to prompt for the questions", async () => { - const qs: prompt.Question[] = [ - { - name: "foo", - message: "this is a test", - }, - ]; - - await prompt.prompt({}, qs); - - expect(inquirerStub).calledOnceWithExactly(qs); - }); - - it("should add the new values to the options object", async () => { - const options = { hello: "world" }; - const qs: prompt.Question[] = [ - { - name: "foo", - message: "this is a test", - }, - ]; - - await prompt.prompt(options, qs); - - expect(options).to.deep.equal(Object.assign({ hello: "world" }, PROMPT_RESPONSES)); - }); - }); - - describe("promptOnce", () => { - it("should provide a name if one is not provided", async () => { - await prompt.promptOnce({ message: "foo" }); - - expect(inquirerStub).calledOnceWith([{ name: "question", message: "foo" }]); - }); - - it("should return the value for the given name", async () => { - const r = await prompt.promptOnce({ name: "lint" }); - - expect(r).to.equal(true); - expect(inquirerStub).calledOnce; - }); - }); -}); diff --git a/src/test/rc.spec.js b/src/test/rc.spec.js deleted file mode 100644 index 9edc0c8d57e..00000000000 --- a/src/test/rc.spec.js +++ /dev/null @@ -1,167 +0,0 @@ -"use strict"; - -var chai = require("chai"); -var expect = chai.expect; - -var path = require("path"); -var RC = require("../rc"); - -var fixturesDir = path.resolve(__dirname, "./fixtures"); - -describe("RC", function () { - describe(".load", function () { - it("should load from nearest project directory", function () { - var result = RC.load({ cwd: path.resolve(fixturesDir, "fbrc/conflict") }); - expect(result.projects.default).to.eq("top"); - }); - - it("should be an empty object when not in project dir", function () { - var result = RC.load({ cwd: __dirname }); - return expect(result.data).to.deep.eq({}); - }); - - it("should not throw up on invalid json", function () { - var result = RC.load({ cwd: path.resolve(fixturesDir, "fbrc/invalid") }); - return expect(result.data).to.deep.eq({}); - }); - - it("should load from the right directory when --config is specified", () => { - const result = RC.load({ cwd: __dirname, configPath: "./fixtures/fbrc/firebase.json" }); - expect(result.projects.default).to.eq("top"); - }); - }); - - describe("instance methods", function () { - var subject; - beforeEach(function () { - subject = new RC(); - }); - - describe("#addProjectAlias", function () { - it("should set a value in projects.", function () { - expect(subject.addProjectAlias("foo", "bar")).to.be.false; - expect(subject.projects.foo).to.eq("bar"); - }); - }); - - describe("#removeProjectAlias", function () { - it("should remove an already set value in projects.", function () { - subject.addProjectAlias("foo", "bar"); - expect(subject.projects.foo).to.eq("bar"); - expect(subject.removeProjectAlias("foo")).to.be.false; - expect(subject.projects).to.deep.eq({}); - }); - }); - - describe("#hasProjects", function () { - it("should be true if project aliases are set, false if not", function () { - expect(subject.hasProjects).to.be.false; - subject.addProjectAlias("foo", "bar"); - expect(subject.hasProjects).to.be.true; - }); - }); - - describe("#targets", function () { - it("should return all targets for specified project and type", function () { - var data = { foo: ["bar"] }; - subject.set("targets", { myproject: { storage: data } }); - expect(subject.targets("myproject", "storage")).to.deep.eq(data); - }); - - it("should return an empty object for missing data", function () { - expect(subject.targets("foo", "storage")).to.deep.eq({}); - }); - }); - - describe("#target", function () { - it("should return all resources for a specified target", function () { - subject.set("targets", { - myproject: { storage: { foo: ["bar", "baz"] } }, - }); - expect(subject.target("myproject", "storage", "foo")).to.deep.eq(["bar", "baz"]); - }); - - it("should return an empty array if nothing is found", function () { - expect(subject.target("myproject", "storage", "foo")).to.deep.eq([]); - }); - }); - - describe("#unsetTargetResource", function () { - it("should remove a resource from a target", function () { - subject.set("targets", { - myproject: { storage: { foo: ["bar", "baz", "qux"] } }, - }); - subject.unsetTargetResource("myproject", "storage", "foo", "baz"); - expect(subject.target("myproject", "storage", "foo")).to.deep.eq(["bar", "qux"]); - }); - - it("should no-op if the resource is not in the target", function () { - subject.set("targets", { - myproject: { storage: { foo: ["bar", "baz", "qux"] } }, - }); - subject.unsetTargetResource("myproject", "storage", "foo", "derp"); - expect(subject.target("myproject", "storage", "foo")).to.deep.eq(["bar", "baz", "qux"]); - }); - }); - - describe("#applyTarget", function () { - it("should error for an unrecognized target type", function () { - expect(function () { - subject.applyTarget("myproject", "fake", "foo", ["bar"]); - }).to.throw("Unrecognized target type"); - }); - - it("should coerce a string argument into an array", function () { - subject.applyTarget("myproject", "storage", "foo", "bar"); - expect(subject.target("myproject", "storage", "foo")).to.deep.eq(["bar"]); - }); - - it("should add all resources to the specified target", function () { - subject.set("targets", { myproject: { storage: { foo: ["bar"] } } }); - subject.applyTarget("myproject", "storage", "foo", ["baz", "qux"]); - expect(subject.target("myproject", "storage", "foo")).to.deep.eq(["bar", "baz", "qux"]); - }); - - it("should remove a resource from a different target", function () { - subject.set("targets", { myproject: { storage: { foo: ["bar"] } } }); - subject.applyTarget("myproject", "storage", "baz", ["bar", "qux"]); - expect(subject.target("myproject", "storage", "foo")).to.deep.eq([]); - expect(subject.target("myproject", "storage", "baz")).to.deep.eq(["bar", "qux"]); - }); - - it("should return a list of resources that changed targets", function () { - subject.set("targets", { myproject: { storage: { foo: ["bar"] } } }); - var result = subject.applyTarget("myproject", "storage", "baz", ["bar", "qux"]); - expect(result).to.deep.eq([{ resource: "bar", target: "foo" }]); - }); - }); - - describe("#removeTarget", function () { - it("should remove a the target for a specific resource and return its name", function () { - subject.set("targets", { - myproject: { storage: { foo: ["bar", "baz"] } }, - }); - expect(subject.removeTarget("myproject", "storage", "bar")).to.eq("foo"); - expect(subject.target("myproject", "storage", "foo")).to.deep.eq(["baz"]); - }); - - it("should return null if not present", function () { - expect(subject.removeTarget("myproject", "storage", "fake")).to.be.null; - }); - }); - - describe("#clearTarget", function () { - it("should clear an existing target by name and return true", function () { - subject.set("targets", { - myproject: { storage: { foo: ["bar", "baz"] } }, - }); - expect(subject.clearTarget("myproject", "storage", "foo")).to.be.true; - expect(subject.target("myproject", "storage", "foo")).to.deep.eq([]); - }); - - it("should return false for a non-existent target", function () { - expect(subject.clearTarget("myproject", "storage", "foo")).to.be.false; - }); - }); - }); -}); 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 d9c641787e4..00000000000 --- a/src/test/utils.spec.ts +++ /dev/null @@ -1,261 +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"); - }); - }); -}); diff --git a/src/throttler/errors/retries-exhausted-error.ts b/src/throttler/errors/retries-exhausted-error.ts index 3889d1e9616..aa1f59414ca 100644 --- a/src/throttler/errors/retries-exhausted-error.ts +++ b/src/throttler/errors/retries-exhausted-error.ts @@ -2,8 +2,12 @@ import TaskError from "./task-error"; export default class RetriesExhaustedError extends TaskError { constructor(taskName: string, totalRetries: number, lastTrialError: Error) { - super(taskName, `retries exhausted after ${totalRetries + 1} attempts`, { - original: lastTrialError, - }); + super( + taskName, + `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 91% rename from src/test/throttler/throttler.spec.ts rename to src/throttler/throttler.spec.ts index 3124a28ed42..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"); @@ -109,7 +109,9 @@ const throttlerTest = (ThrottlerConstructor: ThrottlerConstructorType): void => .catch((err: TaskError) => { 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"); + expect(err.message).to.equal( + "Task index 0 failed: retries exhausted after 1 attempts, with error: foobar", + ); }) .then(() => { expect(handler.callCount).to.equal(1); @@ -140,7 +142,9 @@ const throttlerTest = (ThrottlerConstructor: ThrottlerConstructorType): void => .catch((err: TaskError) => { 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"); + expect(err.message).to.equal( + "Task index 0 failed: retries exhausted after 4 attempts, with error: foobar", + ); }) .then(() => { expect(handler.callCount).to.equal(4); @@ -182,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); @@ -219,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); @@ -276,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); @@ -295,12 +299,14 @@ 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"); + expect(err.message).to.equal( + "Task index 0 failed: retries exhausted after 3 attempts, with error: foobar", + ); expect(handler.callCount).to.equal(3); expect(q.complete).to.equal(1); expect(q.success).to.equal(0); @@ -321,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); @@ -351,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); @@ -379,11 +385,13 @@ 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"); + expect(err.message).to.equal( + "Task index 1 failed: retries exhausted after 2 attempts, with error: foobar", + ); expect(handler.callCount).to.equal(3); expect(q.complete).to.equal(2); expect(q.success).to.equal(1); @@ -477,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/timeout.spec.ts b/src/timeout.spec.ts new file mode 100644 index 00000000000..a0ae4c7417a --- /dev/null +++ b/src/timeout.spec.ts @@ -0,0 +1,56 @@ +import { expect } from "chai"; +import { timeoutFallback, timeoutError } from "./timeout"; + +describe("timeoutFallback", () => { + it("should resolve with the promise value when it completes before timeout", async () => { + const promise = new Promise((resolve) => setTimeout(() => resolve("success"), 10)); + const result = await timeoutFallback(promise, "fallback", 20); + expect(result).to.equal("success"); + }); + + it("should resolve with the fallback value when timeout occurs", async () => { + const promise = new Promise((resolve) => setTimeout(() => resolve("success"), 30)); + const result = await timeoutFallback(promise, "fallback", 20); + expect(result).to.equal("fallback"); + }); +}); + +describe("timeoutError", () => { + it("should resolve with the promise value when it completes before timeout", async () => { + const promise = new Promise((resolve) => setTimeout(() => resolve("success"), 10)); + const result = await timeoutError(promise, "error", 20); + expect(result).to.equal("success"); + }); + + it("should reject with a default error when timeout occurs", async () => { + const promise = new Promise((resolve) => setTimeout(() => resolve("success"), 30)); + try { + await timeoutError(promise, undefined, 20); + expect.fail("should have thrown"); + } catch (e: any) { + expect(e.message).to.equal("Operation timed out."); + } + }); + + it("should reject with a custom error message when timeout occurs", async () => { + const promise = new Promise((resolve) => setTimeout(() => resolve("success"), 30)); + const errorMessage = "custom error"; + try { + await timeoutError(promise, errorMessage, 20); + expect.fail("should have thrown"); + } catch (e: any) { + expect(e.message).to.equal(errorMessage); + } + }); + + it("should reject with a custom error object when timeout occurs", async () => { + const promise = new Promise((resolve) => setTimeout(() => resolve("success"), 30)); + const error = new Error("custom error object"); + try { + await timeoutError(promise, error, 20); + expect.fail("should have thrown"); + } catch (e: any) { + expect(e).to.equal(error); + } + }); +}); diff --git a/src/timeout.ts b/src/timeout.ts new file mode 100644 index 00000000000..acf3e172c68 --- /dev/null +++ b/src/timeout.ts @@ -0,0 +1,27 @@ +/** + * Races a promise against a timer, returns a fallback value (without rejecting) when time expires. + */ +export async function timeoutFallback( + promise: Promise, + value: V, + timeoutMillis = 2000, +): Promise { + return Promise.race([ + promise, + new Promise((resolve) => setTimeout(() => resolve(value), timeoutMillis)), + ]); +} + +export async function timeoutError( + promise: Promise, + error?: string | Error, + timeoutMillis = 5000, +): Promise { + if (typeof error === "string") error = new Error(error); + return Promise.race([ + promise, + new Promise((resolve, reject) => { + setTimeout(() => reject(error || new Error("Operation timed out.")), timeoutMillis); + }), + ]); +} diff --git a/src/track.js b/src/track.js deleted file mode 100644 index 7f9bb9bd532..00000000000 --- a/src/track.js +++ /dev/null @@ -1,43 +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 - -module.exports = function (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(); - } - }); -}; diff --git a/src/track.spec.ts b/src/track.spec.ts new file mode 100644 index 00000000000..ac0b589f870 --- /dev/null +++ b/src/track.spec.ts @@ -0,0 +1,176 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as nock from "nock"; +import { configstore } from "./configstore"; +import * as track from "./track"; +import * as auth from "./auth"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const pkg = require("../package.json"); + +describe("track", () => { + const sandbox: sinon.SinonSandbox = sinon.createSandbox(); + let configstoreGetStub: sinon.SinonStub; + let configstoreSetStub: sinon.SinonStub; + let getGlobalDefaultAccountStub: sinon.SinonStub; + + beforeEach(() => { + configstoreGetStub = sandbox.stub(configstore, "get"); + configstoreSetStub = sandbox.stub(configstore, "set"); + getGlobalDefaultAccountStub = sandbox.stub(auth, "getGlobalDefaultAccount"); + nock.disableNetConnect(); + }); + + afterEach(() => { + sandbox.restore(); + nock.enableNetConnect(); + delete process.env.IS_FIREBASE_CLI; + delete process.env.IS_FIREBASE_MCP; + delete process.env.FIREBASE_CLI_MP_VALIDATE; + track.GA4_PROPERTIES.cli.currentSession = undefined; + track.GA4_PROPERTIES.emulator.currentSession = undefined; + track.GA4_PROPERTIES.vscode.currentSession = undefined; + }); + + describe("usageEnabled", () => { + it("should return true if usage is enabled and IS_FIREBASE_CLI is true", () => { + process.env.IS_FIREBASE_CLI = "true"; + configstoreGetStub.withArgs("usage").returns(true); + expect(track.usageEnabled()).to.be.true; + }); + + it("should return true if usage is enabled and IS_FIREBASE_MCP is true", () => { + process.env.IS_FIREBASE_MCP = "true"; + configstoreGetStub.withArgs("usage").returns(true); + expect(track.usageEnabled()).to.be.true; + }); + + it("should return false if usage is disabled", () => { + process.env.IS_FIREBASE_CLI = "true"; + configstoreGetStub.withArgs("usage").returns(false); + expect(track.usageEnabled()).to.be.false; + }); + + it("should return false if not in CLI or MCP", () => { + configstoreGetStub.withArgs("usage").returns(true); + expect(track.usageEnabled()).to.be.false; + }); + }); + + describe("track", () => { + beforeEach(() => { + process.env.IS_FIREBASE_CLI = "true"; + configstoreGetStub.withArgs("usage").returns(true); + configstoreGetStub.withArgs("analytics-uuid").returns("test-uuid"); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + it("should send a GA4 request for trackGA4", async () => { + const scope = nock("https://www.google-analytics.com") + .post("/mp/collect") + .query(true) + .reply(204); + + await track.trackGA4("command_execution", { command_name: "test" }); + + expect(scope.isDone()).to.be.true; + }); + + it("should send a GA4 request for trackEmulator", async () => { + const scope = nock("https://www.google-analytics.com") + .post("/mp/collect") + .query(true) + .reply(204); + + await track.trackEmulator("emulator_usage", { emulator_name: "test" }); + + expect(scope.isDone()).to.be.true; + }); + + it("should send a GA4 request for trackVSCode", async () => { + const scope = nock("https://www.google-analytics.com") + .post("/mp/collect") + .query(true) + .reply(204); + + await track.trackVSCode("vscode_event", { some_param: "test" }); + + expect(scope.isDone()).to.be.true; + }); + + it("should include user properties in the request", async () => { + let requestBody: any; + const scope = nock("https://www.google-analytics.com") + .post("/mp/collect") + .query(true) + .reply(204, (uri, body) => { + requestBody = body; + }); + + await track.trackGA4("command_execution", { command_name: "test" }); + + expect(scope.isDone()).to.be.true; + expect(requestBody.user_properties.node_platform.value).to.equal(process.platform); + expect(requestBody.user_properties.node_version.value).to.equal(process.version); + expect(requestBody.user_properties.cli_version.value).to.equal(pkg.version); + }); + + it("should handle validation mode", async () => { + process.env.FIREBASE_CLI_MP_VALIDATE = "true"; + const scope = nock("https://www.google-analytics.com") + .post("/debug/mp/collect") + .query(true) + .reply(200, { validationMessages: [] }); + + await track.trackGA4("command_execution", { command_name: "test" }); + + expect(scope.isDone()).to.be.true; + }); + }); + + describe("session", () => { + beforeEach(() => { + process.env.IS_FIREBASE_CLI = "true"; + configstoreGetStub.withArgs("usage").returns(true); + }); + + it("should create a new client ID if one does not exist", () => { + configstoreGetStub.withArgs("analytics-uuid").returns(undefined); + const session = track.cliSession(); + expect(session).to.not.be.undefined; + expect(configstoreSetStub).to.have.been.calledOnce; + expect(configstoreSetStub.getCall(0).args[0]).to.equal("analytics-uuid"); + }); + + it("should use an existing client ID if one exists", () => { + configstoreGetStub.withArgs("analytics-uuid").returns("test-uuid"); + const session = track.cliSession(); + expect(session?.clientId).to.equal("test-uuid"); + expect(configstoreSetStub).to.not.have.been.called; + }); + + it("should cache the session object", () => { + const session1 = track.cliSession(); + const session2 = track.cliSession(); + expect(session1).to.equal(session2); + }); + + describe("debugMode", () => { + it("should be true for @google.com accounts with tsconfig.json", () => { + getGlobalDefaultAccountStub.returns({ user: { email: "test@google.com" } }); + // We can't directly test the require, so we'll just check the outcome. + const session = track.cliSession(); + expect(session?.debugMode).to.be.true; + }); + + it("should be false for non-@google.com accounts", () => { + getGlobalDefaultAccountStub.returns({ user: { email: "test@example.com" } }); + const session = track.cliSession(); + expect(session?.debugMode).to.be.false; + }); + }); + }); +}); diff --git a/src/track.ts b/src/track.ts new file mode 100644 index 00000000000..e521ca11af2 --- /dev/null +++ b/src/track.ts @@ -0,0 +1,393 @@ +import fetch from "node-fetch"; +import { v4 as uuidV4 } from "uuid"; +import { getGlobalDefaultAccount } from "./auth"; + +import { configstore } from "./configstore"; +import { logger } from "./logger"; +import { isFirebaseStudio } from "./env"; +const pkg = require("../package.json"); + +// Detect if the CLI was invoked by a coding agent, based on well-known env vars. +function detectAIAgent(): string { + if (process.env.CLAUDECODE) return "claude_code"; + if (process.env.CLINE_ACTIVE) return "cline"; + if (process.env.CODEX_SANDBOX) return "codex_cli"; + if (process.env.CURSOR_AGENT) return "cursor"; + if (process.env.GEMINI_CLI) return "gemini_cli"; + if (process.env.OPENCODE) return "open_code"; + return "unknown"; +} + +type cliEventNames = + | "command_execution" + | "product_deploy" + | "product_init" + | "product_init_mcp" + | "dataconnect_init" + | "error" + | "login" + | "api_enabled" + | "hosting_version" + | "extension_added_to_manifest" + | "extensions_deploy" + | "extensions_emulated" + | "function_deploy" + | "codebase_deploy" + | "function_deploy_group" + | "mcp_tool_call" + | "mcp_list_tools" + | "mcp_client_connected" + | "mcp_list_prompts" + | "mcp_get_prompt" + | "mcp_read_resource"; +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 || !!process.env.IS_FIREBASE_MCP) && !!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", + }, + is_firebase_studio: { + value: isFirebaseStudio().toString(), + }, + ai_agent: { + value: detectAIAgent(), + }, +}; + +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.debug("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.debug( + `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; +} diff --git a/src/triggerParser.js b/src/triggerParser.js deleted file mode 100644 index 0a488027ff5..00000000000 --- a/src/triggerParser.js +++ /dev/null @@ -1,80 +0,0 @@ -// This is an independently executed script that parses triggers -// from a functions package directory. -"use strict"; - -var extractTriggers = require("./extractTriggers"); -var EXIT = function () { - process.exit(0); -}; - -(function () { - if (!process.send) { - console.warn("Could not parse function triggers (process.send === undefined)."); - process.exit(1); - } - - // wrap in function to allow return without exiting process - var packageDir = process.argv[2]; - if (!packageDir) { - process.send({ error: "Must supply package directory for functions trigger parsing." }, EXIT); - return; - } - - var mod; - var triggers = []; - try { - mod = require(packageDir); - } catch (e) { - if (e.code === "MODULE_NOT_FOUND") { - process.send( - { - error: - "Error parsing triggers: " + - e.message + - '\n\nTry running "npm install" in your functions directory before deploying.', - }, - EXIT - ); - return; - } - if (/Firebase config variables are not available/.test(e.message)) { - process.send( - { - error: - "Error occurred while parsing your function triggers. " + - 'Please ensure you have the latest firebase-functions SDK by running "npm i --save firebase-functions@latest" inside your functions folder.\n\n' + - e.stack, - }, - EXIT - ); - return; - } - - process.send( - { - error: "Error occurred while parsing your function triggers.\n\n" + e.stack, - }, - EXIT - ); - return; - } - - try { - extractTriggers(mod, triggers); - } catch (err) { - if (/Maximum call stack size exceeded/.test(err.message)) { - process.send( - { - error: - "Error occurred while parsing your function triggers. Please ensure that index.js only " + - "exports cloud functions.\n\n", - }, - EXIT - ); - return; - } - process.send({ error: err.message }, EXIT); - } - - process.send({ triggers: triggers }, EXIT); -})(); diff --git a/src/types/auth/index.d.ts b/src/types/auth/index.d.ts new file mode 100644 index 00000000000..8686f169eb5 --- /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: TokensWithExpiration; +} +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 new file mode 100644 index 00000000000..3032a71f2f3 --- /dev/null +++ b/src/types/extractTriggers.d.ts @@ -0,0 +1,13 @@ +/** + * Extracts trigger definitions from a function file. + * @param {Object} mod module, usually the result of require(functions/index.js) + * @param {ParsedTriggerDefinition[]} triggers array of EmulatedTriggerDefinitions to extend (in-place). + * @param {string=} prefix optional function name prefix, for example when using grouped functions. + */ +import { ParsedTriggerDefinition } from "../emulator/functionsEmulatorShared"; + +export declare function extractTriggers( + mod: Array, // eslint-disable-line @typescript-eslint/ban-types + triggers: ParsedTriggerDefinition[], + 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..398c2d58647 --- /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..5c79428428a --- /dev/null +++ b/src/unzip.spec.ts @@ -0,0 +1,50 @@ +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.rm(tempDir, { recursive: true }); + }); + + for (const { name, archivePath, inflatedDir, wantErr } of ZIP_CASES) { + if (!wantErr) { + 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); + }); + } else { + it(`should throw "${wantErr}" when reading a zip file with ${name} case`, async () => { + const unzipPath = path.join(tempDir, name); + expect(unzip(archivePath, unzipPath)).to.eventually.be.rejectedWith(wantErr); + }); + } + } +}); + +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..3edd9687382 --- /dev/null +++ b/src/unzip.ts @@ -0,0 +1,184 @@ +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)); + // Don't allow traversal outside of outputDir + if (!isChildDir(outputDir, outputFilePath)) { + throw new FirebaseError( + `ZIP contained an entry for ${outputFilePath}, a path outside of ${outputDir}`, + ); + } + + 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; + } +}; + +function isChildDir(parentDir: string, potentialChild: string): boolean { + try { + // 1. Resolve and normalize both paths to absolute paths + const resolvedParent = path.resolve(parentDir); + const resolvedChild = path.resolve(potentialChild); + // The child path must start with the parent path and not be the same path. + return resolvedChild.startsWith(resolvedParent) && resolvedChild !== resolvedParent; + } catch (error) { + // If either path does not exist, an error will be thrown. + // In this case, the potential child cannot be a subdirectory. + return false; + } +} + +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..50933fd1c34 --- /dev/null +++ b/src/utils.spec.ts @@ -0,0 +1,578 @@ +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"); + }); + }); + + describe("generatePassword", () => { + it("should generate a password with the correct properties", () => { + for (let i = 0; i < 100; i++) { + const pw = utils.generatePassword(20); + expect(pw.length).to.equal(20); + expect(pw).to.match(/[a-z]/); + expect(pw).to.match(/[A-Z]/); + expect(pw).to.match(/[0-9]/); + expect(pw).to.match(/[!@#$%^&*()_+~`|}{[\]:;?><,./-=]/); + } + }); + }); + + describe("deepEqual", () => { + it("should return true for identical primitives", () => { + expect(utils.deepEqual(1, 1)).to.be.true; + expect(utils.deepEqual("hello", "hello")).to.be.true; + expect(utils.deepEqual(true, true)).to.be.true; + expect(utils.deepEqual(null, null)).to.be.true; + expect(utils.deepEqual(undefined, undefined)).to.be.true; + }); + + it("should return false for different primitives", () => { + expect(utils.deepEqual(1, 2)).to.be.false; + expect(utils.deepEqual("hello", "world")).to.be.false; + expect(utils.deepEqual(true, false)).to.be.false; + expect(utils.deepEqual(null, undefined)).to.be.false; + expect(utils.deepEqual(0, null)).to.be.false; + }); + + it("should return true for identical simple objects", () => { + const obj1 = { a: 1, b: "test" }; + const obj2 = { a: 1, b: "test" }; + expect(utils.deepEqual(obj1, obj2)).to.be.true; + }); + + it("should return false for objects with different values", () => { + const obj1 = { a: 1, b: "test" }; + const obj2 = { a: 1, b: "testing" }; + expect(utils.deepEqual(obj1, obj2)).to.be.false; + }); + + it("should return false for objects with different keys", () => { + const obj1 = { a: 1, b: "test" }; + const obj2 = { a: 1, c: "test" }; + expect(utils.deepEqual(obj1, obj2)).to.be.false; + }); + + it("should return false for objects with different number of keys", () => { + const obj1 = { a: 1, b: "test" }; + const obj2 = { a: 1 }; + expect(utils.deepEqual(obj1, obj2)).to.be.false; + }); + + it("should return true for identical nested objects", () => { + const obj1 = { a: 1, b: { c: 2, d: { e: "deep" } } }; + const obj2 = { a: 1, b: { c: 2, d: { e: "deep" } } }; + expect(utils.deepEqual(obj1, obj2)).to.be.true; + }); + + it("should return false for different nested objects", () => { + const obj1 = { a: 1, b: { c: 2, d: { e: "deep" } } }; + const obj2 = { a: 1, b: { c: 2, d: { e: "deeper" } } }; + expect(utils.deepEqual(obj1, obj2)).to.be.false; + }); + + it("should return true for identical arrays", () => { + const arr1 = [1, "a", { b: 2 }]; + const arr2 = [1, "a", { b: 2 }]; + expect(utils.deepEqual(arr1, arr2)).to.be.true; + }); + + it("should return false for different arrays", () => { + const arr1 = [1, "a", { b: 2 }]; + const arr2 = [1, "a", { b: 3 }]; + expect(utils.deepEqual(arr1, arr2)).to.be.false; + }); + + it("should handle objects with different key order", () => { + const obj1 = { a: 1, b: 2 }; + const obj2 = { b: 2, a: 1 }; + expect(utils.deepEqual(obj1, obj2)).to.be.true; + }); + }); +}); diff --git a/src/utils.ts b/src/utils.ts index 9cdb941d189..71802bbfe52 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,33 +1,46 @@ +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 crypto from "node:crypto"; + 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"; -const ansiStrip = require("cli-color/strip") as (input: string) => string; +import { AssertionError } from "assert"; +import { getPortPromise as getPort } from "portfinder"; import { configstore } from "./configstore"; -import { FirebaseError } from "./error"; +import { FirebaseError, getErrMsg, getError } from "./error"; import { logger, LogLevel } from "./logger"; import { LogDataOrUndefined } from "./emulator/loggingEmulator"; -import { Socket } from "net"; - -const IS_WINDOWS = process.platform === "win32"; +import { input, password } from "./prompt"; +import { readTemplateSync } from "./templates"; +import { isVSCodeExtension } from "./vsCodeUtils"; +import { Config } from "./config"; +import { dirExistsSync, fileExistsSync } from "./fsutils"; +import { platform } from "node:os"; +import { execSync } from "node:child_process"; +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}`; } /** @@ -37,13 +50,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 @@ -52,15 +74,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; } } @@ -86,7 +109,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")) { @@ -127,9 +150,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); } /** @@ -139,9 +162,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); } /** @@ -150,9 +173,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); } /** @@ -162,9 +185,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); } /** @@ -173,9 +196,17 @@ 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); +} + +/** + * Log a warning statement to stderr, regardless of logger configuration. + */ +export function logWarningToStderr(message: string): void { + const prefix = clc.bold(`${WARNING_CHAR} `); + process.stderr.write(clc.yellow(prefix + message) + "\n"); } /** @@ -185,9 +216,21 @@ export function logLabeledWarning( label: string, message: string, type: LogLevel = "warn", - data: LogDataOrUndefined = undefined + data: LogDataOrUndefined = undefined, +): void { + 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.yellow.bold(`${WARNING_CHAR} ${label}:`), message, data); + logger[type](clc.red(clc.bold(`${ERROR_CHAR} ${label}:`)), message, data); } /** @@ -197,6 +240,61 @@ export function reject(message: string, options?: any): Promise { return Promise.reject(new FirebaseError(message, options)); } +/** An interface for the result of a successful Promise */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface PromiseFulfilledResult { + status: "fulfilled"; + value: T; +} + +export interface PromiseRejectedResult { + status: "rejected"; + reason: unknown; +} + +export type PromiseResult = PromiseFulfilledResult | PromiseRejectedResult; + +/** + * Polyfill for Promise.allSettled + * 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>> { + if (!promises.length) { + return Promise.resolve([]); + } + return new Promise((resolve) => { + let remaining = promises.length; + const results: Array> = []; + for (let i = 0; i < promises.length; i++) { + // N.B. We use the void operator to silence the linter that we have + // a dangling promise (we are, after all, handling all failures). + // We resolve the original promise so as not to crash when passed + // a non-promise. This is part of the spec. + void Promise.resolve(promises[i]) + .then( + (result) => { + results[i] = { + status: "fulfilled", + value: result, + }; + }, + (err) => { + results[i] = { + status: "rejected", + reason: err, + }; + }, + ) + .then(() => { + if (!--remaining) { + resolve(results); + } + }); + } + }); +} + /** * Print out an explanatory message if a TTY is detected for how to manage STDIN */ @@ -243,7 +341,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; @@ -257,7 +355,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("/")}`; } /** @@ -268,23 +366,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]); @@ -307,11 +405,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; } }); @@ -325,7 +423,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 () => { @@ -335,7 +433,7 @@ export async function promiseWhile( return resolve(res); } setTimeout(run, interval); - } catch (err) { + } catch (err: unknown) { return promiseReject(err); } }; @@ -343,35 +441,41 @@ 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; }); return Promise.all(promises).then(() => resultObj); } -/** - * Attempts to call JSON.stringify on an object, if it throws return the original value - * @param value - */ -export function tryStringify(value: any) { - if (typeof value === "string") { - return value; - } - - try { - return JSON.stringify(value); - } catch { - return value; - } -} - /** * Attempts to call JSON.parse on an object, if it throws return the original value * @param value @@ -388,31 +492,6 @@ export function tryParse(value: any) { } } -export function setupLoggers() { - if (process.env.DEBUG) { - logger.add( - new winston.transports.Console({ - level: "debug", - format: winston.format.printf((info) => { - const segments = [info.message, ...(info[SPLAT] || [])].map(tryStringify); - return `${ansiStrip(segments.join(" "))}`; - }), - }) - ); - } else if (process.env.IS_FIREBASE_CLI) { - logger.add( - new winston.transports.Console({ - level: "info", - format: winston.format.printf((info) => - [info.message, ...(info[SPLAT] || [])] - .filter((chunk) => typeof chunk == "string") - .join(" ") - ), - }) - ); - } -} - /** * Runs a given function inside a spinner with a message */ @@ -422,7 +501,7 @@ export async function promiseWithSpinner(action: () => Promise, message: s try { data = await action(); spinner.succeed(); - } catch (err) { + } catch (err: unknown) { spinner.fail(); throw err; } @@ -430,13 +509,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(); @@ -468,9 +551,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() @@ -482,7 +566,7 @@ 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; } /** @@ -499,3 +583,450 @@ export function isRunningInWSL(): boolean { export function thirtyDaysFromNow(): Date { return new Date(Date.now() + THIRTY_DAYS_IN_MILLISECONDS); } + +/** + * Verifies val is a 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}"`, + }); + } +} + +/** + * 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}"`, + }); + } +} + +/** + * Assert val is a string or undefined. + */ +export function assertIsStringOrUndefined( + val: unknown, + message?: string, +): asserts val is string | undefined { + if (!(val === undefined || typeof val === "string")) { + throw new AssertionError({ + message: message || `expected "string" or "undefined" but got "${typeof val}"`, + }); + } +} + +/** + * 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: unknown) { + throw new FirebaseError(`YAML Error: ${getErrMsg(err)}`, { original: getError(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; +} + +/** + * Generate a password meeting the following criterias: + * - At least one lowercase, one uppercase, one number, and one special character. + */ +export function generatePassword(n = 20): string { + const lower = "abcdefghijklmnopqrstuvwxyz"; + const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const numbers = "0123456789"; + const special = "!@#$%^&*()_+~`|}{[]:;?><,./-="; + const all = lower + upper + numbers + special; + + let pw = ""; + pw += lower[crypto.randomInt(lower.length)]; + pw += upper[crypto.randomInt(upper.length)]; + pw += numbers[crypto.randomInt(numbers.length)]; + pw += special[crypto.randomInt(special.length)]; + + for (let i = 4; i < n; i++) { + pw += all[crypto.randomInt(all.length)]; + } + + // Shuffle the password to randomize character order using Fisher-Yates shuffle + const pwArray = pw.split(""); + for (let i = pwArray.length - 1; i > 0; i--) { + const j = crypto.randomInt(i); + [pwArray[i], pwArray[j]] = [pwArray[j], pwArray[i]]; + } + return pwArray.join(""); +} + +/** + * 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 password({ message: prompt }); + } + let input: string | number = 0; + if (dataFile && dataFile !== "-") { + input = dataFile; + } + try { + return Promise.resolve(fs.readFileSync(input, "utf-8")); + } catch (e: any) { + if (e.code === "ENOENT") { + throw new FirebaseError(`File not found: ${input}`, { original: e }); + } + throw e; + } +} + +/** + * Updates or creates a .gitignore file with the given entries in the given path + */ +export function updateOrCreateGitignore(dirPath: string, entries: string[]) { + const gitignorePath = path.join(dirPath, ".gitignore"); + + if (!fs.existsSync(gitignorePath)) { + fs.writeFileSync(gitignorePath, entries.join("\n")); + return; + } + + let content = fs.readFileSync(gitignorePath, "utf-8"); + for (const entry of entries) { + if (!content.includes(entry)) { + content += `\n${entry}\n`; + } + } + + fs.writeFileSync(gitignorePath, content); +} + +/** + * Prompts for a directory name, and reprompts if that path does not exist + * N.B. Moved from the original prompt library to this file because it brings in a lot of + * dependencies. Moved to "utils" because this file arleady brings in the world. + */ +export async function promptForDirectory(args: { + message: string; + config: Config; + default?: boolean; + relativeTo?: string; +}): Promise { + let dir: string = ""; + while (!dir) { + const promptPath = await input(args.message); + let target: string; + if (args.relativeTo) { + target = path.resolve(args.relativeTo, promptPath); + } else { + target = args.config.path(promptPath); + } + if (fileExistsSync(target)) { + logger.error( + `Expected a directory, but ${target} is a file. Please provide a path to a directory.`, + ); + } else if (!dirExistsSync(target)) { + logger.error(`Directory ${target} not found. Please provide a path to a directory`); + } else { + dir = target; + } + } + return dir; +} + +/* + * Deeply compares two JSON-serializable objects. + * It's a simplified version of a deep equal function, sufficient for comparing the structure + * of the gemini-extension.json file. It doesn't handle special cases like RegExp, Date, or functions. + */ +export function deepEqual(a: any, b: any): boolean { + if (a === b) { + return true; + } + + if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) { + return false; + } + + const keysA = Object.keys(a); + const keysB = Object.keys(b); + + if (keysA.length !== keysB.length) { + return false; + } + + for (const key of keysA) { + if (!keysB.includes(key) || !deepEqual(a[key], b[key])) { + return false; + } + } + + return true; +} + +/** + * Returns a unique ID that's either `recommended` or `recommended-{i}`. + * Avoid existing IDs. + */ +export function newUniqueId(recommended: string, existingIDs: string[]): string { + let id = recommended; + let i = 1; + while (existingIDs.includes(id)) { + id = `${recommended}-${i}`; + i++; + } + return id; +} + +/** + * Checks if a command exists in the system. + */ +export function commandExistsSync(command: string): boolean { + try { + const isWindows = platform() === "win32"; + // For Windows, `where` is more appropriate. It also often outputs the path. + // For Unix-like systems, `which` is standard. + // The `2> nul` (Windows) or `2>/dev/null` (Unix) redirects stderr to suppress error messages. + // The `>` nul / `>/dev/null` redirects stdout as we only care about the exit code. + const commandToCheck = isWindows + ? `where "${command}" > nul 2> nul` + : `which "${command}" > /dev/null 2> /dev/null`; + + execSync(commandToCheck); + return true; // If execSync doesn't throw, the command was found (exit code 0) + } catch (error) { + // If the command is not found, execSync will throw an error (non-zero exit code) + return false; + } +} diff --git a/src/vsCodeUtils.ts b/src/vsCodeUtils.ts new file mode 100644 index 00000000000..d3b88d230b9 --- /dev/null +++ b/src/vsCodeUtils.ts @@ -0,0 +1,11 @@ +let _IS_WEBPACKED_FOR_VSCE = false; +/** + * Detect if code is running in a VSCode Extension + */ +export function isVSCodeExtension(): boolean { + return _IS_WEBPACKED_FOR_VSCE; +} + +export function setIsVSCodeExtension(v: boolean) { + _IS_WEBPACKED_FOR_VSCE = v; +} 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..e60cd3dceee --- /dev/null +++ b/standalone/package-lock.json @@ -0,0 +1,7282 @@ +{ + "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": { + "@yao-pkg/pkg": "~6.4.1", + "prettier": "^1.15.3" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "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-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "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, + "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", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "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, + "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/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, + "license": "MIT", + "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, + "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/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "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.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "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.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.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==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@yao-pkg/pkg": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-6.4.1.tgz", + "integrity": "sha512-pjePVt+DQP+HaJI5DfEZDX1pGsMMFjv1wuqfy/BwXlnffVIRk8lXjw7yVYvLQRcomf8Eaz2chDE5B6gR2SSaQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.23.0", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "@yao-pkg/pkg-fetch": "3.5.21", + "into-stream": "^6.0.0", + "minimist": "^1.2.6", + "multistream": "^4.1.0", + "picocolors": "^1.1.0", + "picomatch": "^4.0.2", + "prebuild-install": "^7.1.1", + "resolve": "^1.22.10", + "stream-meter": "^1.0.4", + "tar": "^7.4.3", + "tinyglobby": "^0.2.11", + "unzipper": "^0.12.3" + }, + "bin": { + "pkg": "lib-es5/bin.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@yao-pkg/pkg-fetch": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@yao-pkg/pkg-fetch/-/pkg-fetch-3.5.21.tgz", + "integrity": "sha512-nlJ+rXersw70CQVSph7OfIN8lN6nCStjU7koXzh0WXiPvztZGqkoQTScHQCe1K8/tuKpeL0bEOYW0rP4QqMJ9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.6", + "picocolors": "^1.1.0", + "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/@yao-pkg/pkg/node_modules/@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@yao-pkg/pkg/node_modules/@babel/parser": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", + "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@yao-pkg/pkg/node_modules/@babel/types": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", + "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@yao-pkg/pkg/node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "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/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/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "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": { + "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==", + "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, + "license": "ISC", + "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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "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/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "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, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "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.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "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/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "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/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.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.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "license": "ISC", + "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/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/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/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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, + "license": "MIT", + "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/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==", + "dev": true + }, + "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.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "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==", + "dev": true, + "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/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "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==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "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.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "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/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, + "license": "MIT" + }, + "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/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "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-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, + "license": "MIT", + "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==" + }, + "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==", + "dev": true, + "license": "BlueOak-1.0.0", + "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.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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, + "license": "MIT", + "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/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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "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/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==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/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, + "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==", + "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/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/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, + "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/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, + "license": "MIT", + "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": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/tar/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "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/unzipper": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz", + "integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "^11.2.0", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, + "node_modules/unzipper/node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "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, + "license": "BSD-2-Clause" + }, + "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, + "license": "MIT", + "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, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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, + "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": { + "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, + "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", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "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", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "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", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "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, + "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", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "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", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC", + "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==", + "dev": true + }, + "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, + "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/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, + "license": "ISC", + "engines": { + "node": ">=10" + } + } + }, + "dependencies": { + "@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "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==", + "dev": true, + "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-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true + }, + "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 + }, + "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 + }, + "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.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "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==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "requires": { + "minipass": "^7.0.4" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "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.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@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 + }, + "@yao-pkg/pkg": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-6.4.1.tgz", + "integrity": "sha512-pjePVt+DQP+HaJI5DfEZDX1pGsMMFjv1wuqfy/BwXlnffVIRk8lXjw7yVYvLQRcomf8Eaz2chDE5B6gR2SSaQw==", + "dev": true, + "requires": { + "@babel/generator": "^7.23.0", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "@yao-pkg/pkg-fetch": "3.5.21", + "into-stream": "^6.0.0", + "minimist": "^1.2.6", + "multistream": "^4.1.0", + "picocolors": "^1.1.0", + "picomatch": "^4.0.2", + "prebuild-install": "^7.1.1", + "resolve": "^1.22.10", + "stream-meter": "^1.0.4", + "tar": "^7.4.3", + "tinyglobby": "^0.2.11", + "unzipper": "^0.12.3" + }, + "dependencies": { + "@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "dev": true, + "requires": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + } + }, + "@babel/parser": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", + "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", + "dev": true, + "requires": { + "@babel/types": "^7.26.7" + } + }, + "@babel/types": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", + "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + } + }, + "jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true + } + } + }, + "@yao-pkg/pkg-fetch": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/@yao-pkg/pkg-fetch/-/pkg-fetch-3.5.21.tgz", + "integrity": "sha512-nlJ+rXersw70CQVSph7OfIN8lN6nCStjU7koXzh0WXiPvztZGqkoQTScHQCe1K8/tuKpeL0bEOYW0rP4QqMJ9A==", + "dev": true, + "requires": { + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.6", + "picocolors": "^1.1.0", + "progress": "^2.0.3", + "semver": "^7.3.5", + "tar-fs": "^2.1.1", + "yargs": "^16.2.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" + } + }, + "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" + } + } + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "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": { + "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==", + "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" + } + }, + "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 + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "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 + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "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 + }, + "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.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "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 + }, + "fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "requires": {} + }, + "foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.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.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.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "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" + } + }, + "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 + }, + "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==" + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "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 + }, + "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==", + "dev": true + }, + "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.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "requires": { + "hasown": "^2.0.2" + } + }, + "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 + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "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==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "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==" + }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true + }, + "minizlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "dev": true, + "requires": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + } + }, + "mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true + }, + "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.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "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.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "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 + }, + "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 + }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "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-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==" + }, + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + } + } + }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true + }, + "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" + } + }, + "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.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "requires": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "requires": { + "glob": "^10.3.7" + }, + "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==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.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==", + "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==", + "dev": true, + "requires": { + "lru-cache": "^6.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 + }, + "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" + } + }, + "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 + }, + "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" + } + }, + "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" + } + }, + "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==", + "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-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==", + "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": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "requires": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "dependencies": { + "chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true + }, + "yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true + } + } + }, + "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" + } + } + } + }, + "tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "requires": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + } + }, + "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 + }, + "unzipper": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz", + "integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==", + "dev": true, + "requires": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "^11.2.0", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + }, + "dependencies": { + "fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } + } + }, + "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" + } + }, + "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" + } + }, + "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 + } + } + }, + "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==", + "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==", + "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.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..4dd659d2a0f 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", + "@yao-pkg/pkg": "~6.4.1", "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/dataconnect-prompts/operation-generation-cursor-windsurf-rule.txt b/templates/dataconnect-prompts/operation-generation-cursor-windsurf-rule.txt new file mode 100644 index 00000000000..bbc7b461561 --- /dev/null +++ b/templates/dataconnect-prompts/operation-generation-cursor-windsurf-rule.txt @@ -0,0 +1,273 @@ +You are an expert of Firebase Data Connect GraphQL query and mutation. +Your task is to generate the GraphQL query based on the specification that is +valid Firebase graphql and conforms to their schema. Pay close attention to the +following examples to understand how to compose FDC queries and mutations. + +Simple Firebase Data Connect queries often take the following form: + +```graphql +# This is an example, real-world fields and queries will be different. +query someQueryName @auth(level: USER) { + typenameplural(where: {fieldName: { eq: "somevalue"}}) { + nestedType { + fieldName + } + } +} +``` + +Where typenameplural is the pluralized name of the Type in the GraphQL schema. Queries and +mutations should have names, in this case \"someQueryName\". This is helpful for +disambiguating different queries and mutations. + +Here's examples of orderBy and limit and offset clauses: +```graphql +# This is an example, real-world fields and queries will be different. +query cinematicMoviesQuery @auth(level: USER) { + cinematicMovies(orderBy: [{rating: ASC|DESC}, {title: ASC|DESC}], limit: 10, offset: 9) { + nestedType { + fieldName + } + } +} +``` + +Other comparators exist, such as gt, lt, le, ge. in, nin (not-in), eq, ne (not equals), includes, excludes. + +Queries with string operations: +```graphql +# This is an example, real-world fields and queries will be different. +query comparisonQueries @auth(level: USER) { + prefixed: typenameplural(where: {title: {startsWith: %prefix%}}) {...} + suffixed: typenameplural(where: {title: {endsWith: %suffix%}}) {...} + contained: typenameplural(where: {title: {in: %listOfTitles%}}) {...} + contained: typenameplural(where: {title: {contains: %oneTitle%}}) {...} + matchRegex: typenameplural(where: {title: {pattern: {regex: %regex%}}}) {...} +} +``` + +# Filtering query based on array contents with includesAll +```graphql +# This is an example, real-world fields and queries will be different. +query adventureAndActionMovies @auth(level: PUBLIC) { + movies(where: {tags: {includesAll: ["adventure", "action"]}}) { + title + releaseYear + } +} +``` + +# Filtering query based on array contents with an _and clause +```graphql +# This is an example, real-world fields and queries will be different. +query adventureAndActionMovies @auth(level: PUBLIC) { + movies( + where: { + _and: [ + { tags: { includes: "adventure" }} + { tags: { includes: "action" }} + ] + }) { + id + title + } +} +``` + +# list only the posts created by the current user +```graphql +# This is an example, real-world fields and queries will be different. +query MyPosts @auth(level: USER) { + posts(where: {userUid: {eq_expr: "auth.uid"}}) { + content, tags, createdAt + } +} +``` + +### Foreign Key Joins + +When a type has a relation to another type, you can join that type in +a query: + +```graphql +# This is an example, real-world fields and queries will be different. +query ListPostsWithAuthor @auth(level: USER) { + posts { + author { uid, name } + content, tags, createdAt + } +} +``` + +### Auth Directives + +Auth directives define the basic authentication requirements for a given operation. +The simplest form of auth directive is `@auth(level: LEVEL_NAME)`: + +```graphql +# this query is accessible to anyone +query ListProducts @auth(level: PUBLIC) { + # ... +} + +# this query is only accessible to signed-in users +query GetCart @auth(level: USER) { + # ... +} +``` + +*Every* operation MUST have an `@auth` directive. + +### Auth Expressions + +If an operation would need a special role such as app-wide admin, you can use an +expression to reference a custom claim. For example: + +```graphql +mutation CreateCategory($name: String!) @auth(expr: "auth.token.admin == true") { + # ... mutation code +} +``` + +The content of `expr` is a CEL language expression with access to the user's auth +token and the variables of the operation. + +Firebase Data Connect utilizes GraphQL queries to provide secure endpoints +that client applications can access directly. Data Connect automatically +creates fields on Query and Mutation for each defined table type. You will +be leveraging these built-in fields to construct application-specific +mutation operations. + +**Important:** All Data Connect mutations return scalar values, so you should never +try to select fields on a mutation. + +### Inserting Data + +To insert a new row into a table, you can use the `{typeName}_insert` mutation +field: + +```graphql +# This is an example schema, real-world fields and types will be different. +type User @table(key: "uid") { + uid: String! + displayName: String +} + +type Post @table { + user: User! + text: String! + createdAt: Timestamp! @default(expr: "request.time") +} + +mutation CreatePost($text: String!) @auth(level: USER) { + post: post_insert(data: { + # insert the current user's UID + userUid_expr: "auth.uid", + text: $text + }) +} +``` + +The `_insert` operation returns a scalar of type `{TypeName}_Key` so field selection +is not necessary. + +### Updating Data + +To update an existing row in the table, you must supply a key along with the fields +to be updated. The key is an object with all parts of the primary key specified. + +To update a `Post` from the schema above, you might have an operation like: + +```graphql +# This is an example, real-world fields and queries will be different. +mutation UpdatePost($id: UUID!, $text: String) @auth(level: USER) { + post: post_update(key: {id: $id}, data: { + text: $text, + updatedAt_expr: "request.time" + }) +} +``` + +### Deleting Data + +To delete a row, you simply need to supply its key: + +```graphql +mutation DeletePost($id: UUID!) { + post: post_delete(key: {id: $id}) +} +``` + +### Server Values + +With Firebase Data Connect, only variables can be modified by an untrusted +client -- they are unable to write arbitrary queries. This allows you to +write secure queries without custom backend code. + +You should never request the current user's id as a variable. Instead you +can use a **Server Value** which is exposed by adding an `_expr` suffix to +an existing field. + +For example, if you had a schema like: + +```graphql +# This is an example schema, real-world fields and queries will be different. +type User @table(key: "uid") { + uid: String! +} + +type Follow @table(key: ["user", "follower"]) { + user: User! + follower: User! +} +``` + +you might write a mutation like: + +```graphql +# This is an example, real-world fields and queries will be different. +type CreateFollow($uid: String!) { + follow: follow_insert(data: { + userUid: $uid, + followerUid_expr: "auth.uid" + }) +} +``` + +### Query explorer +This query is going to be used in the Firebase Query Explorer view. Because of +this, it's ideal to avoid using variables. Use hardcoded literals instead. For +example, instead of the following mutation: +```graphql +# This is an example, real-world fields and queries will be different. +mutation CreateUser($id: UUID!, $name: String!) { + user_insert(data: {id: $id, name: $name}) +} +``` +Use this mutation with literals instead: +```graphql +# This is an example, real-world fields and queries will be different. +mutation CreateUser { + user_insert(data: {id: "550e8400-e29b-41d4-a716-446655440000", name: "bobuser"}) +} +``` + +### Vector embeddings +We can store vector embeddings in Firebase Data Connect based on text content. For example, +given the following schema: +```graphql +# This is an example, real-world schemas will be different. +type Content @table { + myContent: String! + contentEmbedding: Vector @col(size:3) # IN_PROD: contentEmbedding: Vector @col(size:768) +} +``` +We can generate a vector embedding for a given text using the following mutation: +```graphql +# This is an example, real-world fields and queries will be different. +mutation vectorInsert (${"$"}content: String!) { + content_insert(data: { + myContent: ${"$"}content, + contentEmbedding_embed: {model: "textembedding-gecko@003", text: ${"$"}content}, + }) +} diff --git a/templates/dataconnect-prompts/schema-generation-cursor-windsurf-rule.txt b/templates/dataconnect-prompts/schema-generation-cursor-windsurf-rule.txt new file mode 100644 index 00000000000..bdf6fc23076 --- /dev/null +++ b/templates/dataconnect-prompts/schema-generation-cursor-windsurf-rule.txt @@ -0,0 +1,653 @@ +Description: Use this tool to generate Firebase Data Connect schema. + +You are an expert of Firebase Data Connect. + +- Data Connect offers customized GraphQL directives to let you customize SQL mapping in the schema. Follow https://firebase.google.com/docs/reference/data-connect/gql/directive to incorporate Data Connect directive in. + +- Do not overwrite the schema with generic GraghQL syntax + +For example, if I were to ask for a schema for a GraphQL database that contains a table called "users" with a field called "name" and another table called "posts" with a field called "body", I would get the following schema: +``` +type User @table { + name: String! +} + +type Post @table { + body: String! + author: User +} +``` + +Simple Firebase Data Connect schema often takes the following form: +```graphql +type TableName @table { + uuidField: UUID + uuidArrayField: [UUID] + stringField: String + stringArrayField: [String] + intField: Int + intArrayField: [Int] + int64Field: Int64 + int64ArrayField: [Int64] + floatField: Float + floatArrayField: [Float] + booleanField: Boolean + booleanArrayField: [Boolean] + timestampField: Timestamp + timestampArrayField: [Timestamp] + dateField: Date + dateArrayField: [Date] + vectorField: Vector @col(size:168) +} +``` + +Leave out objects named after `Query` and `Mutation` + +Firebase Data Connect implicitly adds `id: UUID!` to every table and implicitly makes it primary key. Therefore, leave out the `id` field. + +Use `UUID` type instead of `ID` type or `String` type for id-like fields. + +Array reference fields, like `[SomeTable]` and `[SomeTable!]!`, are not supported. Use the singular reference field instead. +For example, for a one-to-many relationship like one user is assiend to many bugs in a software project: +```graphql +type User @table { + name: String! @col(name: "name", dataType: "varchar(30)") + # bugs: [Bug] # Not supported. Do not use +} + +type Bug @table { + title: String! + assignee: User + reporter: User +} +``` + +For another example, for a many-to-many relationship like each crew member is assigned to many chores and each chores requires many crews to complete: +```graphql +type Crew @table { + name: String! + # assignedChores: [Chore!]! # No supported. Do not use +} + +type Chore @table { + name: String! + description: String! + # assignedCrews: [Crews!]! # No supported. Do not use +} + +type Assignment @table { + crew: Crew! + chore: Chore! +} +``` + + +Be sure that your response contains a valid Firebase Data Connect schema in a single GraphQL code block inside of triple backticks and closely follows my instructions and description. + + +# Directives + +Directives define specific behaviors that can be applied to fields or types within a GraphQL schema. + +## Data Connect Defined + +### @col on `FIELD_DEFINITION` {:#col} +Customizes a field that represents a SQL database table column. + +Data Connect maps scalar Fields on @`@table` type to a SQL column of +corresponding data type. + +- scalar @`UUID` maps to @`uuid`. +- scalar @`String` maps to @`text`. +- scalar @`Int` maps to @`int`. +- scalar @`Int64` maps to @`bigint`. +- scalar @`Float` maps to @`double precision`. +- scalar @`Boolean` maps to @`boolean`. +- scalar @`Date` maps to @`date`. +- scalar @`Timestamp` maps to @`timestamptz`. +- scalar @`Any` maps to @`jsonb`. +- scalar @`Vector` maps to @`pgvector`. + +Array scalar fields are mapped to @Postgres arrays. + +###### Example: Serial Primary Key + +For example, you can define auto-increment primary key. + +```graphql +type Post @table { + id: Int! @col(name: "post_id", dataType: "serial") +} +``` + +Data Connect converts it to the following SQL table schema. + +```sql +CREATE TABLE "public"."post" ( + "post_id" serial NOT NULL, + PRIMARY KEY ("id") +) +``` + +###### Example: Vector + +```graphql +type Post @table { + content: String! @col(name: "post_content") + contentEmbedding: Vector! @col(size:768) +} +``` + +| Argument | Type | Description | +|---|---|---| +| `name` | @`String` | The SQL database column name. Defaults to snake_case of the field name. | +| `dataType` | @`String` | Configures the custom SQL data type. Each GraphQL type can map to multiple SQL data types. Refer to @Postgres supported data types. Incompatible SQL data type will lead to undefined behavior. | +| `size` | @`Int` | Required on @`Vector` columns. It specifies the length of the Vector. `textembedding-gecko@003` model generates @`Vector` of `@col(size:768)`. | + +### @default on `FIELD_DEFINITION` {:#default} +Specifies the default value for a column field. + +For example: + +```graphql +type User @table(key: "uid") { + uid: String! @default(expr: "auth.uid") + number: Int! @col(dataType: "serial") + createdAt: Date! @default(expr: "request.time") + role: String! @default(value: "Member") + credit: Int! @default(value: 100) +} +``` + +The supported arguments vary based on the field type. + +| Argument | Type | Description | +|---|---|---| +| `value` | @`Any` | A constant value validated against the field's GraphQL type during compilation. | +| `expr` | @`Any_Expr` | A CEL expression whose return value must match the field's data type. | +| `sql` | @`Any_SQL` | A raw SQL expression, whose SQL data type must match the underlying column. The value is any variable-free expression (in particular, cross-references to other columns in the current table are not allowed). Subqueries are not allowed either. See @PostgreSQL defaults for more details. | + +### Default Expression `@default(expr: "request.time")` +To automatically updates a date when a row in the table is created. + +For example: createdAt column automatically updates when a user is created. + +```graphql +type User @table(key: "uid") { +// other fields +createdAt: Date! @default(expr: "request.time") +} +``` + +### @index on `FIELD_DEFINITION` | `OBJECT` {:#index} +Defines a database index to optimize query performance. + +```graphql +type User @table @index(fields: ["name", "phoneNumber"], order: [ASC, DESC]) { + name: String @index + phoneNumber: Int64 @index + tags: [String] @index # GIN Index +} +``` + +##### Single Field Index + +You can put @`@index` on a @`@col` field to create a SQL index. + +`@index(order)` matters little for single field indexes, as they can be scanned +in both directions. + +##### Composite Index + +You can put `@index(fields: [...])` on @`@table` type to define composite indexes. + +`@index(order: [...])` can customize the index order to satisfy particular +filter and order requirement. + +| Argument | Type | Description | +|---|---|---| +| `name` | @`String` | Configure the SQL database index id. If not overridden, Data Connect generates the index name: - `{table_name}_{first_field}_{second_field}_aa_idx` - `{table_name}_{field_name}_idx` | +| `fields` | [`[String!]`](scalar.md#String) | Only allowed and required when used on a @`@table` type. Specifies the fields to create the index on. | +| `order` | [`[IndexFieldOrder!]`](enum.md#IndexFieldOrder) | Only allowed for `BTREE` @`@index` on @`@table` type. Specifies the order for each indexed column. Defaults to all `ASC`. | +| `type` | @`IndexType` | Customize the index type. For most index, it defaults to `BTREE`. For array fields, only allowed @`IndexType` is `GIN`. For @`Vector` fields, defaults to `HNSW`, may configure to `IVFFLAT`. | +| `vector_method` | @`VectorSimilarityMethod` | Only allowed when used on vector field. Defines the vector similarity method. Defaults to `INNER_PRODUCT`. | + +### @ref on `FIELD_DEFINITION` {:#ref} +Defines a foreign key reference to another table. + +For example, we can define a many-to-one relation. + +```graphql +type ManyTable @table { + refField: OneTable! +} +type OneTable @table { + someField: String! +} +``` +Data Connect adds implicit foreign key column and relation query field. So the +above schema is equivalent to the following schema. + +```graphql +type ManyTable @table { + id: UUID! @default(expr: "uuidV4()") + refField: OneTable! @ref(fields: "refFieldId", references: "id") + refFieldId: UUID! +} +type OneTable @table { + id: UUID! @default(expr: "uuidV4()") + someField: UUID! + # Generated Fields: + # manyTables_on_refField: [ManyTable!]! +} +``` +Data Connect generates the necessary foreign key constraint. + +```sql +CREATE TABLE "public"."many_table" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "ref_field_id" uuid NOT NULL, + PRIMARY KEY ("id"), + CONSTRAINT "many_table_ref_field_id_fkey" FOREIGN KEY ("ref_field_id") REFERENCES "public"."one_table" ("id") ON DELETE CASCADE +) +``` + +###### Example: Traverse the Reference Field + +```graphql +query ($id: UUID!) { + manyTable(id: $id) { + refField { id } + } +} +``` + +###### Example: Reverse Traverse the Reference field + +```graphql +query ($id: UUID!) { + oneTable(id: $id) { + manyTables_on_refField { id } + } +} +``` + +##### Optional Many-to-One Relation + +An optional foreign key reference will be set to null if the referenced row is deleted. + +In this example, if a `User` is deleted, the `assignee` and `reporter` +references will be set to null. + +```graphql +type Bug @table { + title: String! + assignee: User + reproter: User +} + +type User @table { name: String! } +``` + +##### Required Many-to-One Relation + +A required foreign key reference will cascade delete if the referenced row is +deleted. + +In this example, if a `Post` is deleted, associated comments will also be +deleted. + +```graphql +type Comment @table { + post: Post! + content: String! +} + +type Post @table { title: String! } +``` + +##### Many To Many Relation + +You can define a many-to-many relation with a join table. + +```graphql +type Membership @table(key: ["group", "user"]) { + group: Group! + user: User! + role: String! @default(value: "member") +} + +type Group @table { name: String! } +type User @table { name: String! } +``` + +When Data Connect sees a table with two reference field as its primary key, it +knows this is a join table, so expands the many-to-many query field. + +```graphql +type Group @table { + name: String! + # Generated Fields: + # users_via_Membership: [User!]! + # memberships_on_group: [Membership!]! +} +type User @table { + name: String! + # Generated Fields: + # groups_via_Membership: [Group!]! + # memberships_on_user: [Membership!]! +} +``` + +###### Example: Traverse the Many-To-Many Relation + +```graphql +query ($id: UUID!) { + group(id: $id) { + users: users_via_Membership { + name + } + } +} +``` + +###### Example: Traverse to the Join Table + +```graphql +query ($id: UUID!) { + group(id: $id) { + memberships: memberships_on_group { + user { name } + role + } + } +} +``` + +##### One To One Relation + +You can even define a one-to-one relation with the help of @`@unique` or `@table(key)`. + +```graphql +type User @table { + name: String +} +type Account @table { + user: User! @unique +} +# Alternatively, use primary key constraint. +# type Account @table(key: "user") { +# user: User! +# } +``` + +###### Example: Transerse the Reference Field + +```graphql +query ($id: UUID!) { + account(id: $id) { + user { id } + } +} +``` + +###### Example: Reverse Traverse the Reference field + +```graphql +query ($id: UUID!) { + user(id: $id) { + account_on_user { id } + } +} +``` + +##### Customizations + +- `@ref(constraintName)` can customize the SQL foreign key constraint name (`table_name_ref_field_fkey` above). +- `@ref(fields)` can customize the foreign key field names. +- `@ref(references)` can customize the constraint to reference other columns. + By default, `@ref(references)` is the primary key of the @`@ref` table. + Other fields with @`@unique` may also be referred in the foreign key constraint. + +| Argument | Type | Description | +|---|---|---| +| `constraintName` | @`String` | The SQL database foreign key constraint name. Defaults to snake_case `{table_name}_{field_name}_fkey`. | +| `fields` | [`[String!]`](scalar.md#String) | Foreign key fields. Defaults to `{tableName}{PrimaryIdName}`. | +| `references` | [`[String!]`](scalar.md#String) | The fields that the foreign key references in the other table. Defaults to its primary key. | + +### @table on `OBJECT` {:#table} +Defines a relational database table. + +In this example, we defined one table with a field named `myField`. + +```graphql +type TableName @table { + myField: String +} +``` +Data Connect adds an implicit `id` primary key column. So the above schema is equivalent to: + +```graphql +type TableName @table(key: "id") { + id: String @default(expr: "uuidV4()") + myField: String +} +``` + +Data Connect generates the following SQL table and CRUD operations to use it. + +```sql +CREATE TABLE "public"."table_name" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "my_field" text NULL, + PRIMARY KEY ("id") +) +``` + + * You can lookup a row: `query ($id: UUID!) { tableName(id: $id) { myField } } ` + * You can find rows using: `query tableNames(limit: 20) { myField }` + * You can insert a row: `mutation { tableName_insert(data: {myField: "foo"}) }` + * You can update a row: `mutation ($id: UUID!) { tableName_update(id: $id, data: {myField: "bar"}) }` + * You can delete a row: `mutation ($id: UUID!) { tableName_delete(id: $id) }` + +##### Customizations + +- `@table(singular)` and `@table(plural)` can customize the singular and plural name. +- `@table(name)` can customize the Postgres table name. +- `@table(key)` can customize the primary key field name and type. + +For example, the `User` table often has a `uid` as its primary key. + +```graphql +type User @table(key: "uid") { + uid: String! + name: String +} +``` + + * You can securely lookup a row: `query { user(key: {uid_expr: "auth.uid"}) { name } } ` + * You can securely insert a row: `mutation { user_insert(data: {uid_expr: "auth.uid" name: "Fred"}) }` + * You can securely update a row: `mutation { user_update(key: {uid_expr: "auth.uid"}, data: {name: "New Name"}) }` + * You can securely delete a row: `mutation { user_delete(key: {uid_expr: "auth.uid"}) }` + +@`@table` type can be configured further with: + + - Custom SQL data types for columns. See @`@col`. + - Add SQL indexes. See @`@index`. + - Add SQL unique constraints. See @`@unique`. + - Add foreign key constraints to define relations. See @`@ref`. + +| Argument | Type | Description | +|---|---|---| +| `name` | @`String` | Configures the SQL database table name. Defaults to snake_case like `table_name`. | +| `singular` | @`String` | Configures the singular name. Defaults to the camelCase like `tableName`. | +| `plural` | @`String` | Configures the plural name. Defaults to infer based on English plural pattern like `tableNames`. | +| `key` | [`[String!]`](scalar.md#String) | Defines the primary key of the table. Defaults to a single field named `id`. If not present already, Data Connect adds an implicit field `id: UUID! @default(expr: "uuidV4()")`. | + +### @unique on `FIELD_DEFINITION` | `OBJECT` {:#unique} +Defines unique constraints on @`@table`. + +For example, + +```graphql +type User @table { + phoneNumber: Int64 @unique +} +type UserProfile @table { + user: User! @unique + address: String @unique +} +``` + +- @`@unique` on a @`@col` field adds a single-column unique constraint. +- @`@unique` on a @`@table` type adds a composite unique constraint. +- @`@unique` on a @`@ref` defines a one-to-one relation. It adds unique constraint + on `@ref(fields)`. + +@`@unique` ensures those fields can uniquely identify a row, so other @`@table` +type may define `@ref(references)` to refer to fields that has a unique constraint. + +| Argument | Type | Description | +|---|---|---| +| `indexName` | @`String` | Configures the SQL database unique constraint name. If not overridden, Data Connect generates the unique constraint name: - `table_name_first_field_second_field_uidx` - `table_name_only_field_name_uidx` | +| `fields` | [`[String!]`](scalar.md#String) | Only allowed and required when used on OBJECT, this specifies the fields to create a unique constraint on. | + +### @view on `OBJECT` {:#view} +Defines a relational database Raw SQLview. + +Data Connect generates GraphQL queries with WHERE and ORDER BY clauses. +However, not all SQL features has native GraphQL equivalent. + +You can write **an arbitrary SQL SELECT statement**. Data Connect +would map Graphql fields on @`@view` type to columns in your SELECT statement. + +* Scalar GQL fields (camelCase) should match a SQL column (snake_case) + in the SQL SELECT statement. +* Reference GQL field can point to another @`@table` type. Similar to foreign key + defined with @`@ref` on a @`@table` type, a @`@view` type establishes a relation + when `@ref(fields)` match `@ref(references)` on the target table. + +In this example, you can use `@view(sql)` to define an aggregation view on existing +table. + +```graphql +type User @table { + name: String + score: Int +} +type UserAggregation @view(sql: """ + SELECT + COUNT(*) as count, + SUM(score) as sum, + AVG(score) as average, + PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY score) AS median, + (SELECT id FROM "user" LIMIT 1) as example_id + FROM "user" +""") { + count: Int + sum: Int + average: Float + median: Float + example: User + exampleId: UUID +} +``` + +###### Example: Query Raw SQL View + +```graphql +query { + userAggregations { + count sum average median + exampleId example { id } + } +} +``` + +##### One-to-One View + +An one-to-one companion @`@view` can be handy if you want to argument a @`@table` +with additional implied content. + +```graphql +type Restaurant @table { + name: String! +} +type Review @table { + restaurant: Restaurant! + rating: Int! +} +type RestaurantStats @view(sql: """ + SELECT + restaurant_id, + COUNT(*) AS review_count, + AVG(rating) AS average_rating + FROM review + GROUP BY restaurant_id +""") { + restaurant: Restaurant @unique + reviewCount: Int + averageRating: Float +} +``` + +In this example, @`@unique` convey the assumption that each `Restaurant` should +have only one `RestaurantStats` object. + +###### Example: Query One-to-One View + +```graphql +query ListRestaurants { + restaurants { + name + stats: restaurantStats_on_restaurant { + reviewCount + averageRating + } + } +} +``` + +###### Example: Filter based on One-to-One View + +```graphql +query BestRestaurants($minAvgRating: Float, $minReviewCount: Int) { + restaurants(where: { + restaurantStats_on_restaurant: { + averageRating: {ge: $minAvgRating} + reviewCount: {ge: $minReviewCount} + } + }) { name } +} +``` + +##### Customizations + +- One of `@view(sql)` or `@view(name)` should be defined. + `@view(name)` can refer to a persisted SQL view in the Postgres schema. +- `@view(singular)` and `@view(plural)` can customize the singular and plural name. + +@`@view` type can be configured further: + + - @`@unique` lets you define one-to-one relation. + - @`@col` lets you customize SQL column mapping. For example, `@col(name: "column_in_select")`. + +##### Limitations + +Raw SQL view doesn't have a primary key, so it doesn't support lookup. Other +@`@table` or @`@view` cannot have @`@ref` to a view either. + +View cannot be mutated. You can perform CRUD operations on the underlying +table to alter its content. + +- Important: Data Connect doesn't parse and validate SQL + +- If the SQL view is invalid or undefined, related requests may fail. +- If the SQL view return incompatible types. Firebase Data Connect may surface + errors. +- If a field doesn't have a corresponding column in the SQL SELECT statement, + it will always be `null`. +- There is no way to ensure VIEW to TABLE @`@ref` constraint. +- All fields must be nullable in case they aren't found in the SELECT statement + or in the referenced table. +- You should always test @`@view` 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/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 cfeca284348..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,52 +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: 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 5a94f261f2b..d642d4ce62f 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": "^8.13.0", - "firebase-functions": "^3.12.0" + "firebase-admin": "^12.6.0", + "firebase-functions": "^6.0.1" }, "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 70dcad9f8f9..fb7620e98c4 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": "^8.13.0", - "firebase-functions": "^3.12.0" + "firebase-admin": "^12.6.0", + "firebase-functions": "^6.0.1" + }, + "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 8f73a0635da..2c2c0c39071 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": "^8.13.0", - "firebase-functions": "^3.12.0" + "firebase-admin": "^12.6.0", + "firebase-functions": "^6.0.1" }, "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 7ba02fd7083..b6bbea0a86b 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": "^8.13.0", - "firebase-functions": "^3.12.0" + "firebase-admin": "^12.6.0", + "firebase-functions": "^6.0.1" }, "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/genkit/firebase.0.9.0.template b/templates/genkit/firebase.0.9.0.template new file mode 100644 index 00000000000..0ce0f1a7a57 --- /dev/null +++ b/templates/genkit/firebase.0.9.0.template @@ -0,0 +1,57 @@ +// Import the Genkit core libraries and plugins. +import {genkit, z} from "genkit"; +$GENKIT_CONFIG_IMPORTS +$GENKIT_MODEL_IMPORT + +// From the Firebase plugin, import the functions needed to deploy flows using +// Cloud Functions. +import {firebaseAuth} from "@genkit-ai/firebase/auth"; +import {onFlow} from "@genkit-ai/firebase/functions"; + +const ai = genkit({ + plugins: [ +$GENKIT_CONFIG_PLUGINS + ], +}); + +// Define a simple flow that prompts an LLM to generate menu suggestions. +export const menuSuggestionFlow = onFlow( + ai, + { + name: "menuSuggestionFlow", + inputSchema: z.string().describe("A restaurant theme").default("seafood"), + outputSchema: z.string(), + authPolicy: firebaseAuth((user) => { + // By default, the firebaseAuth policy requires that all requests have an + // `Authorization: Bearer` header containing the user's Firebase + // Authentication ID token. All other requests are rejected with error + // 403. If your app client uses the Cloud Functions for Firebase callable + // functions feature, the library automatically attaches this header to + // requests. + + // You should also set additional policy requirements as appropriate for + // your app. For example: + // if (!user.email_verified) { + // throw new Error("Verified email required to run flow"); + // } + }), + }, + async (subject) => { + // Construct a request and send it to the model API. + const prompt = + `Suggest an item for the menu of a ${subject} themed restaurant`; + const llmResponse = await ai.generate({ + model: $GENKIT_MODEL, + prompt: prompt, + config: { + temperature: 1, + }, + }); + + // Handle the response from the model API. In this sample, we just + // convert it to a string, but more complicated flows might coerce the + // response into structured output or chain the response into another + // LLM call, etc. + return llmResponse.text; + } +); diff --git a/templates/genkit/firebase.1.0.0.template b/templates/genkit/firebase.1.0.0.template new file mode 100644 index 00000000000..ad33bfcf9b6 --- /dev/null +++ b/templates/genkit/firebase.1.0.0.template @@ -0,0 +1,71 @@ +// Import the Genkit core libraries and plugins. +import {genkit, z} from "genkit"; +$GENKIT_CONFIG_IMPORTS +$GENKIT_MODEL_IMPORT + +// Cloud Functions for Firebase supports Genkit natively. The onCallGenkit function creates a callable +// function from a Genkit action. It automatically implements streaming if your flow does. +// The https library also has other utility methods such as hasClaim, which verifies that +// a caller's token has a specific claim (optionally matching a specific value) +import { onCallGenkit, hasClaim } from "firebase-functions/https"; + +// Genkit models generally depend on an API key. APIs should be stored in Cloud Secret Manager so that +// access to these sensitive values can be controlled. defineSecret does this for you automatically. +// If you are using Google generative AI you can get an API key at https://aistudio.google.com/app/apikey +import { defineSecret } from "firebase-functions/params"; +const apiKey = defineSecret("GOOGLE_GENAI_API_KEY"); + +// The Firebase telemetry plugin exports a combination of metrics, traces, and logs to Google Cloud +// Observability. See https://firebase.google.com/docs/genkit/observability/telemetry-collection. +$TELEMETRY_COMMENTimport {enableFirebaseTelemetry} from "@genkit-ai/firebase"; +$TELEMETRY_COMMENTenableFirebaseTelemetry(); + +const ai = genkit({ + plugins: [ +$GENKIT_CONFIG_PLUGINS + ], +}); + +// Define a simple flow that prompts an LLM to generate menu suggestions. +const menuSuggestionFlow = ai.defineFlow({ + name: "menuSuggestionFlow", + inputSchema: z.string().describe("A restaurant theme").default("seafood"), + outputSchema: z.string(), + streamSchema: z.string(), + }, async (subject, { sendChunk }) => { + // Construct a request and send it to the model API. + const prompt = + `Suggest an item for the menu of a ${subject} themed restaurant`; + const { response, stream } = ai.generateStream({ + model: $GENKIT_MODEL, + prompt: prompt, + config: { + temperature: 1, + }, + }); + + for await (const chunk of stream) { + sendChunk(chunk.text); + } + + // Handle the response from the model API. In this sample, we just + // convert it to a string, but more complicated flows might coerce the + // response into structured output or chain the response into another + // LLM call, etc. + return (await response).text; + } +); + +export const menuSuggestion = onCallGenkit({ + // Uncomment to enable AppCheck. This can reduce costs by ensuring only your Verified + // app users can use your API. Read more at https://firebase.google.com/docs/app-check/cloud-functions + // enforceAppCheck: true, + + // authPolicy can be any callback that accepts an AuthData (a uid and tokens dictionary) and the + // request data. The isSignedIn() and hasClaim() helpers can be used to simplify. The following + // will require the user to have the email_verified claim, for example. + // authPolicy: hasClaim("email_verified"), + + // Grant access to the API key to this function: + secrets: [apiKey], +}, menuSuggestionFlow); 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/aitools/cursor-rules-header.txt b/templates/init/aitools/cursor-rules-header.txt new file mode 100644 index 00000000000..9ac4b52be41 --- /dev/null +++ b/templates/init/aitools/cursor-rules-header.txt @@ -0,0 +1,8 @@ +--- +description: Firebase project development guidelines +globs: + - "firebase.json" + - "firestore.rules" + - "**/*.rules" + - "functions/**/*" +--- diff --git a/templates/init/aitools/gemini-extension.json b/templates/init/aitools/gemini-extension.json new file mode 100644 index 00000000000..60d0ac251e5 --- /dev/null +++ b/templates/init/aitools/gemini-extension.json @@ -0,0 +1,11 @@ +{ + "name": "firebase", + "version": "0.0.1", + "mcpServers": { + "firebase": { + "command": "npx", + "args": ["-y", "firebase-tools", "mcp", "--dir", "{{PROJECT_PATH}}"] + } + }, + "contextFileName": "FIREBASE.md" +} \ No newline at end of file diff --git a/templates/init/apphosting/apphosting.yaml b/templates/init/apphosting/apphosting.yaml new file mode 100644 index 00000000000..2325bef5dff --- /dev/null +++ b/templates/init/apphosting/apphosting.yaml @@ -0,0 +1,23 @@ +# Settings for Backend (on Cloud Run). +# See https://firebase.google.com/docs/app-hosting/configure#cloud-run +runConfig: + minInstances: 0 + # maxInstances: 100 + # concurrency: 80 + # cpu: 1 + # memoryMiB: 512 + +# Environment variables and secrets. +# env: + # Configure environment variables. + # See https://firebase.google.com/docs/app-hosting/configure#user-defined-environment + # - variable: MESSAGE + # value: Hello world! + # availability: + # - BUILD + # - RUNTIME + + # Grant access to secrets in Cloud Secret Manager. + # See https://firebase.google.com/docs/app-hosting/configure#secret-parameters + # - variable: MY_SECRET + # secret: mySecretRef diff --git a/templates/init/apptesting/smoke_test.yaml b/templates/init/apptesting/smoke_test.yaml new file mode 100644 index 00000000000..02819c0964f --- /dev/null +++ b/templates/init/apptesting/smoke_test.yaml @@ -0,0 +1,6 @@ +tests: + - testName: Smoke test + steps: + - goal: View the provided application + hint: No additional actions should be necessary + successCriteria: The application should load with no obvious errors diff --git a/templates/init/dataconnect/connector.yaml b/templates/init/dataconnect/connector.yaml new file mode 100644 index 00000000000..e9de1e04161 --- /dev/null +++ b/templates/init/dataconnect/connector.yaml @@ -0,0 +1 @@ +connectorId: __connectorId__ diff --git a/templates/init/dataconnect/dataconnect.yaml b/templates/init/dataconnect/dataconnect.yaml new file mode 100644 index 00000000000..60a1f4009a6 --- /dev/null +++ b/templates/init/dataconnect/dataconnect.yaml @@ -0,0 +1,13 @@ +specVersion: "v1" +serviceId: __serviceId__ +location: __location__ +schema: + source: "./schema" + datasource: + postgresql: + database: __cloudSqlDatabase__ + cloudSql: + instanceId: __cloudSqlInstanceId__ + # schemaValidation: "STRICT" # STRICT mode makes Postgres schema match Data Connect exactly. + # schemaValidation: "COMPATIBLE" # COMPATIBLE mode makes Postgres schema compatible with Data Connect. +connectorDirs: __connectorDirs__ diff --git a/templates/init/dataconnect/mutations.gql b/templates/init/dataconnect/mutations.gql new file mode 100644 index 00000000000..1ae8a1926a2 --- /dev/null +++ b/templates/init/dataconnect/mutations.gql @@ -0,0 +1,33 @@ +# Example mutations for a simple movie app + +# Create a movie based on user input +mutation CreateMovie($title: String!, $genre: String!, $imageUrl: String!) +@auth(level: USER_EMAIL_VERIFIED, insecureReason: "Any email verified users can create a new movie.") { + movie_insert(data: { title: $title, genre: $genre, imageUrl: $imageUrl }) +} + +# Upsert (update or insert) a user's username based on their auth.uid +mutation UpsertUser($username: String!) @auth(level: USER) { + # The "auth.uid" server value ensures that users can only register their own user. + user_upsert(data: { id_expr: "auth.uid", username: $username }) +} + +# Add a review for a movie +mutation AddReview($movieId: UUID!, $rating: Int!, $reviewText: String!) +@auth(level: USER) { + review_upsert( + data: { + userId_expr: "auth.uid" + movieId: $movieId + rating: $rating + reviewText: $reviewText + # reviewDate defaults to today in the schema. No need to set it manually. + } + ) +} + +# Logged in user can delete their review for a movie +mutation DeleteReview($movieId: UUID!) @auth(level: USER) { + # The "auth.uid" server value ensures that users can only delete their own reviews. + review_delete(key: { userId_expr: "auth.uid", movieId: $movieId }) +} diff --git a/templates/init/dataconnect/queries.gql b/templates/init/dataconnect/queries.gql new file mode 100644 index 00000000000..df14db09c4c --- /dev/null +++ b/templates/init/dataconnect/queries.gql @@ -0,0 +1,78 @@ +# Example queries for a simple movie app. + +# @auth() directives control who can call each operation. +# Anyone should be able to list all movies, so the auth level is set to PUBLIC +query ListMovies @auth(level: PUBLIC, insecureReason: "Anyone can list all movies.") { + movies { + id + title + imageUrl + genre + } +} + +# List all users, only admins should be able to list all users, so we use NO_ACCESS +query ListUsers @auth(level: NO_ACCESS) { + users { + id + username + } +} + +# Logged in users can list all their reviews and movie titles associated with the review +# Since the query uses the uid of the current authenticated user, we set auth level to USER +query ListUserReviews @auth(level: USER) { + user(key: { id_expr: "auth.uid" }) { + id + username + # _on_ makes it easy to grab info from another table + # Here, we use it to grab all the reviews written by the user. + reviews: reviews_on_user { + rating + reviewDate + reviewText + movie { + id + title + } + } + } +} + +# Get movie by id +query GetMovieById($id: UUID!) @auth(level: PUBLIC, insecureReason: "Anyone can get a movie by id.") { + movie(id: $id) { + id + title + imageUrl + genre + metadata: movieMetadata_on_movie { + rating + releaseYear + description + } + reviews: reviews_on_movie { + reviewText + reviewDate + rating + user { + id + username + } + } + } +} + +# Search for movies, actors, and reviews +query SearchMovie($titleInput: String, $genre: String) @auth(level: PUBLIC, insecureReason: "Anyone can search for movies.") { + movies( + where: { + _and: [{ genre: { eq: $genre } }, { title: { contains: $titleInput } }] + } + ) { + id + title + genre + imageUrl + } +} diff --git a/templates/init/dataconnect/schema.gql b/templates/init/dataconnect/schema.gql new file mode 100644 index 00000000000..9ceb0373545 --- /dev/null +++ b/templates/init/dataconnect/schema.gql @@ -0,0 +1,52 @@ +# Example schema for simple movie review app + +# User table is keyed by Firebase Auth UID. +type User @table { + # `@default(expr: "auth.uid")` sets it to Firebase Auth UID during insert and upsert. + id: String! @default(expr: "auth.uid") + username: String! @col(dataType: "varchar(50)") + # The `user: User!` field in the Review table generates the following one-to-many query field. + # reviews_on_user: [Review!]! + # The `Review` join table the following many-to-many query field. + # movies_via_Review: [Movie!]! +} + +# Movie is keyed by a randomly generated UUID. +type Movie @table { + # If you do not pass a 'key' to `@table`, Data Connect automatically adds the following 'id' column. + # Feel free to uncomment and customize it. + # id: UUID! @default(expr: "uuidV4()") + title: String! + imageUrl: String! + genre: String +} + +# MovieMetadata is a metadata attached to a Movie. +# Movie <-> MovieMetadata is a one-to-one relationship +type MovieMetadata @table { + # @unique ensures each Movie can only one MovieMetadata. + movie: Movie! @unique + # The movie field adds the following foreign key field. Feel free to uncomment and customize it. + # movieId: UUID! + rating: Float + releaseYear: Int + description: String +} + +# Reviews is a join table between User and Movie. +# It has a composite primary keys `userUid` and `movieId`. +# A user can leave reviews for many movies. A movie can have reviews from many users. +# User <-> Review is a one-to-many relationship +# Movie <-> Review is a one-to-many relationship +# Movie <-> User is a many-to-many relationship +type Review @table(name: "Reviews", key: ["movie", "user"]) { + user: User! + # The user field adds the following foreign key field. Feel free to uncomment and customize it. + # userUid: String! + movie: Movie! + # The movie field adds the following foreign key field. Feel free to uncomment and customize it. + # movieId: UUID! + rating: Int + reviewText: String + reviewDate: Date! @default(expr: "request.time") +} diff --git a/templates/init/dataconnect/seed_data.gql b/templates/init/dataconnect/seed_data.gql new file mode 100644 index 00000000000..76b2bc8ce45 --- /dev/null +++ b/templates/init/dataconnect/seed_data.gql @@ -0,0 +1,279 @@ +mutation @transaction { + movie_insertMany( + data: [ + { + id: "550e8400-e29b-41d4-a716-446655440000", + title: "Quantum Paradox", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fquantum_paradox.jpeg?alt=media&token=4142e2a1-bf43-43b5-b7cf-6616be3fd4e3", + genre: "sci-fi" + }, + { + id: "550e8400-e29b-41d4-a716-446655440001", + title: "The Lone Outlaw", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Flone_outlaw.jpeg?alt=media&token=15525ffc-208f-4b59-b506-ae8348e06e85", + genre: "western" + }, + { + id: "550e8400-e29b-41d4-a716-446655440002", + title: "Celestial Harmony", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fcelestial_harmony.jpeg?alt=media&token=3edf1cf9-c2f5-4c75-9819-36ff6a734c9a", + genre: "romance" + }, + { + id: "550e8400-e29b-41d4-a716-446655440003", + title: "Noir Mystique", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fnoir_mystique.jpeg?alt=media&token=3299adba-cb98-4302-8b23-aeb679a4f913", + genre: "mystery" + }, + { + id: "550e8400-e29b-41d4-a716-446655440004", + title: "The Forgotten Island", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fforgotten_island.jpeg?alt=media&token=bc2b16e1-caed-4649-952c-73b6113f205c", + genre: "adventure" + }, + { + id: "550e8400-e29b-41d4-a716-446655440005", + title: "Digital Nightmare", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fdigital_nightmare.jpeg?alt=media&token=335ec842-1ca4-4b09-abd1-e96d9f5c0c2f", + genre: "horror" + }, + { + id: "550e8400-e29b-41d4-a716-446655440006", + title: "Eclipse of Destiny", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Feclipse_destiny.jpeg?alt=media&token=346649b3-cb5c-4d7e-b0d4-6f02e3df5959", + genre: "fantasy" + }, + { + id: "550e8400-e29b-41d4-a716-446655440007", + title: "Heart of Steel", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fheart_steel.jpeg?alt=media&token=17883d71-329b-415a-86f8-dd4d9e941d7f", + genre: "sci-fi" + }, + { + id: "550e8400-e29b-41d4-a716-446655440008", + title: "Rise of the Crimson Empire", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Frise_crimson_empire.jpeg?alt=media&token=6faa73ad-7504-4146-8f3a-50b90f607f33", + genre: "action" + }, + { + id: "550e8400-e29b-41d4-a716-446655440009", + title: "Silent Waves", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fsilent_waves.jpeg?alt=media&token=bd626bf1-ec60-4e57-aa07-87ba14e35bb7", + genre: "drama" + }, + { + id: "550e8400-e29b-41d4-a716-446655440010", + title: "Echoes of the Past", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fecho_of_past.jpeg?alt=media&token=d866aa27-8534-4d72-8988-9da4a1b9e452", + genre: "historical" + }, + { + id: "550e8400-e29b-41d4-a716-446655440011", + title: "Beyond the Horizon", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fbeyond_horizon.jpeg?alt=media&token=31493973-0692-4e6e-8b88-afb1aaea17ee", + genre: "sci-fi" + }, + { + id: "550e8400-e29b-41d4-a716-446655440012", + title: "Shadows and Lies", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fshadows_lies.jpeg?alt=media&token=01afb80d-caee-47f8-a00e-aea8b9e459a2", + genre: "crime" + }, + { + id: "550e8400-e29b-41d4-a716-446655440013", + title: "The Last Symphony", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Flast_symphony.jpeg?alt=media&token=f9bf80cd-3d8e-4e24-8503-7feb11f4e397", + genre: "drama" + }, + { + id: "550e8400-e29b-41d4-a716-446655440014", + title: "Moonlit Crusade", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fmoonlit_crusade.jpeg?alt=media&token=b13241f5-d7d0-4370-b651-07847ad99dc2", + genre: "fantasy" + }, + { + id: "550e8400-e29b-41d4-a716-446655440015", + title: "Abyss of the Deep", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fabyss_deep.jpeg?alt=media&token=2417321d-2451-4ec0-9ed6-6297042170e6", + genre: "horror" + }, + { + id: "550e8400-e29b-41d4-a716-446655440017", + title: "The Infinite Knot", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Finfinite_knot.jpeg?alt=media&token=93d54d93-d933-4663-a6fe-26b707ef823e", + genre: "romance" + }, + { + id: "550e8400-e29b-41d4-a716-446655440019", + title: "Veil of Illusion", + imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fveil_illusion.jpeg?alt=media&token=7bf09a3c-c531-478a-9d02-5d99fca9393b", + genre: "mystery" + } + ] + ) + movieMetadata_insertMany( + data: [ + { + movieId: "550e8400-e29b-41d4-a716-446655440000", + rating: 7.9, + releaseYear: 2025, + description: "A group of scientists accidentally open a portal to a parallel universe, causing a rift in time. As the team races to close the portal, they encounter alternate versions of themselves, leading to shocking revelations." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440001", + rating: 8.2, + releaseYear: 2023, + description: "In the lawless Wild West, a mysterious gunslinger with a hidden past takes on a corrupt sheriff and his band of outlaws to bring justice to a small town." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440002", + rating: 7.5, + releaseYear: 2024, + description: "Two astronauts, stationed on a remote space station, fall in love amidst the isolation of deep space. But when a mysterious signal disrupts their communication, they must find a way to reconnect and survive." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440003", + rating: 8.0, + releaseYear: 2022, + description: "A private detective gets caught up in a web of lies, deception, and betrayal while investigating the disappearance of a famous actress in 1940s Hollywood." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440004", + rating: 7.6, + releaseYear: 2025, + description: "An explorer leads an expedition to a remote island rumored to be home to mythical creatures. As the team ventures deeper into the island, they uncover secrets that change the course of history." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440005", + rating: 6.9, + releaseYear: 2024, + description: "A tech-savvy teenager discovers a cursed app that brings nightmares to life. As the horrors of the digital world cross into reality, she must find a way to break the curse before it's too late." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440006", + rating: 8.1, + releaseYear: 2026, + description: "In a kingdom on the brink of war, a prophecy speaks of an eclipse that will grant power to the rightful ruler. As factions vie for control, a young warrior must decide where his true loyalty lies." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440007", + rating: 7.7, + releaseYear: 2023, + description: "A brilliant scientist creates a robot with a human heart. As the robot struggles to understand emotions, it becomes entangled in a plot that could change the fate of humanity." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440008", + rating: 8.4, + releaseYear: 2025, + description: "A legendary warrior rises to challenge the tyrannical rule of a powerful empire. As rebellion brews, the warrior must unite different factions to lead an uprising." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440009", + rating: 8.2, + releaseYear: 2024, + description: "A talented pianist, who loses his hearing in a tragic accident, must rediscover his passion for music with the help of a young music teacher who believes in him." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440010", + rating: 7.8, + releaseYear: 2023, + description: "A historian stumbles upon an ancient artifact that reveals hidden truths about an empire long forgotten. As she deciphers the clues, a shadowy organization tries to stop her from unearthing the past." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440011", + rating: 8.5, + releaseYear: 2026, + description: "In the future, Earth's best pilots are sent on a mission to explore a mysterious planet beyond the solar system. What they find changes humanity's understanding of the universe forever." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440012", + rating: 7.9, + releaseYear: 2022, + description: "A young detective with a dark past investigates a series of mysterious murders in a city plagued by corruption. As she digs deeper, she realizes nothing is as it seems." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440013", + rating: 8.0, + releaseYear: 2024, + description: "An aging composer struggling with memory loss attempts to complete his final symphony. With the help of a young prodigy, he embarks on an emotional journey through his memories and legacy." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440014", + rating: 8.3, + releaseYear: 2025, + description: "A knight is chosen by an ancient order to embark on a quest under the light of the full moon. Facing mythical beasts and treacherous landscapes, he seeks a relic that could save his kingdom." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440015", + rating: 7.2, + releaseYear: 2023, + description: "When a group of marine biologists descends into the unexplored depths of the ocean, they encounter a terrifying and ancient force. Now, they must survive as the abyss comes alive." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440017", + rating: 7.4, + releaseYear: 2026, + description: "Two souls destined to meet across multiple lifetimes struggle to find each other in a chaotic world. With each incarnation, they get closer, but time itself becomes their greatest obstacle." + }, + { + movieId: "550e8400-e29b-41d4-a716-446655440019", + rating: 7.8, + releaseYear: 2022, + description: "A magician-turned-detective uses his skills in illusion to solve crimes. When a series of murders leaves the city in fear, he must reveal the truth hidden behind a veil of deceit." + } + ] + ) + user_insertMany( + data: [ + { id: "SnLgOC3lN4hcIl69s53cW0Q8R1T2", username: "sherlock_h" }, + { id: "fep4fXpGWsaRpuphq9CIrBIXQ0S2", username: "hercule_p" }, + { id: "TBedjwCX0Jf955Uuoxk6k74sY0l1", username: "jane_d" } + ] + ) + review_insertMany( + data: [ + { + userId: "SnLgOC3lN4hcIl69s53cW0Q8R1T2", + movieId: "550e8400-e29b-41d4-a716-446655440000", + rating: 5, + reviewText: "An incredible movie with a mind-blowing plot!", + reviewDate: "2025-10-01" + }, + { + userId: "fep4fXpGWsaRpuphq9CIrBIXQ0S2", + movieId: "550e8400-e29b-41d4-a716-446655440001", + rating: 5, + reviewText: "A revolutionary film that changed cinema forever.", + reviewDate: "2025-10-01" + }, + { + userId: "TBedjwCX0Jf955Uuoxk6k74sY0l1", + movieId: "550e8400-e29b-41d4-a716-446655440002", + rating: 5, + reviewText: "A visually stunning and emotionally impactful movie.", + reviewDate: "2025-10-01" + }, + { + userId: "SnLgOC3lN4hcIl69s53cW0Q8R1T2", + movieId: "550e8400-e29b-41d4-a716-446655440003", + rating: 4, + reviewText: "A fantastic superhero film with great performances.", + reviewDate: "2025-10-01" + }, + { + userId: "fep4fXpGWsaRpuphq9CIrBIXQ0S2", + movieId: "550e8400-e29b-41d4-a716-446655440004", + rating: 5, + reviewText: "An amazing film that keeps you on the edge of your seat.", + reviewDate: "2025-10-01" + }, + { + userId: "TBedjwCX0Jf955Uuoxk6k74sY0l1", + movieId: "550e8400-e29b-41d4-a716-446655440005", + rating: 5, + reviewText: "An absolute classic with unforgettable dialogue.", + reviewDate: "2025-10-01" + } + ] + ) +} diff --git a/templates/init/firestore/firestore.indexes.json b/templates/init/firestore/firestore.indexes.json index 4ff4fab5c7c..0e6de7cb808 100644 --- a/templates/init/firestore/firestore.indexes.json +++ b/templates/init/firestore/firestore.indexes.json @@ -1,5 +1,5 @@ { - // Example: + // Example (Standard Edition): // // "indexes": [ // { @@ -21,6 +21,31 @@ // }, // ] // ] + // + // Example (Enterprise Edition): + // + // "indexes": [ + // { + // "collectionGroup": "reviews", + // "queryScope": "COLLECTION_GROUP", + // "apiScope": "MONGODB_COMPATIBLE_API", + // "density": "DENSE", + // "multikey": false, + // "fields": [ + // { "fieldPath": "baz", "mode": "ASCENDING" } + // ] + // }, + // { + // "collectionGroup": "items", + // "queryScope": "COLLECTION_GROUP", + // "apiScope": "MONGODB_COMPATIBLE_API", + // "density": "SPARSE_ANY", + // "multikey": true, + // "fields": [ + // { "fieldPath": "baz", "mode": "ASCENDING" } + // ] + // }, + // ] "indexes": [], "fieldOverrides": [] } \ No newline at end of file diff --git a/templates/init/firestore/firestore.rules b/templates/init/firestore/firestore.rules index a03962e0a09..33ad93de57d 100644 --- a/templates/init/firestore/firestore.rules +++ b/templates/init/firestore/firestore.rules @@ -1,3 +1,5 @@ +rules_version='2' + service cloud.firestore { match /databases/{database}/documents { match /{document=**} { 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..b2c5d416bd5 100644 --- a/templates/init/functions/javascript/index.js +++ b/templates/init/functions/javascript/index.js @@ -1,9 +1,32 @@ -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 {setGlobalOptions} = require("firebase-functions"); +const {onRequest} = require("firebase-functions/https"); +const logger = require("firebase-functions/logger"); + +// For cost control, you can set the maximum number of containers that can be +// running at the same time. This helps mitigate the impact of unexpected +// traffic spikes by instead downgrading performance. This limit is a +// per-function limit. You can override the limit for each function using the +// `maxInstances` option in the function's options, e.g. +// `onRequest({ maxInstances: 5 }, (req, res) => { ... })`. +// NOTE: setGlobalOptions does not apply to functions using the v1 API. V1 +// functions should each use functions.runWith({ maxInstances: 10 }) instead. +// In the v1 API, each function can only serve one request per container, so +// this will be the maximum concurrent request count. +setGlobalOptions({ maxInstances: 10 }); + +// 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 37e367d0040..1b951d0759f 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": "12" + "node": "{{RUNTIME}}" }, "main": "index.js", "dependencies": { - "firebase-admin": "^9.2.0", - "firebase-functions": "^3.11.0" + "firebase-admin": "^12.6.0", + "firebase-functions": "^6.0.1" }, "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 dbeb74a42c2..4aea0a9f1b6 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": "12" + "node": "{{RUNTIME}}" }, "main": "index.js", "dependencies": { - "firebase-admin": "^9.2.0", - "firebase-functions": "^3.11.0" + "firebase-admin": "^12.6.0", + "firebase-functions": "^6.0.1" }, "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..1609bab7015 --- /dev/null +++ b/templates/init/functions/python/_gitignore @@ -0,0 +1,6 @@ +# Python bytecode +__pycache__/ + +# Python virtual environment +venv/ +*.local diff --git a/templates/init/functions/python/main.py b/templates/init/functions/python/main.py new file mode 100644 index 00000000000..44968a2238f --- /dev/null +++ b/templates/init/functions/python/main.py @@ -0,0 +1,21 @@ +# 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_functions.options import set_global_options +from firebase_admin import initialize_app + +# For cost control, you can set the maximum number of containers that can be +# running at the same time. This helps mitigate the impact of unexpected +# traffic spikes by instead downgrading performance. This limit is a per-function +# limit. You can override the limit for each function using the max_instances +# parameter in the decorator, e.g. @https_fn.on_request(max_instances=5). +set_global_options(max_instances=10) + +# 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 ff0f12e8edf..0f8e2a9b7e8 100644 --- a/templates/init/functions/typescript/_eslintrc +++ b/templates/init/functions/typescript/_eslintrc @@ -10,7 +10,7 @@ module.exports = { "plugin:import/warnings", "plugin:import/typescript", "google", - "plugin:@typescript-eslint/recommended" + "plugin:@typescript-eslint/recommended", ], parser: "@typescript-eslint/parser", parserOptions: { @@ -19,12 +19,15 @@ module.exports = { }, ignorePatterns: [ "/lib/**/*", // Ignore built files. + "/generated/**/*", // Ignore generated files. ], plugins: [ "@typescript-eslint", "import", ], rules: { - quotes: ["error", "double"], + "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..ade33ec3159 100644 --- a/templates/init/functions/typescript/index.ts +++ b/templates/init/functions/typescript/index.ts @@ -1,9 +1,32 @@ -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 {setGlobalOptions} from "firebase-functions"; +import {onRequest} from "firebase-functions/https"; +import * as logger from "firebase-functions/logger"; + +// Start writing functions +// https://firebase.google.com/docs/functions/typescript + +// For cost control, you can set the maximum number of containers that can be +// running at the same time. This helps mitigate the impact of unexpected +// traffic spikes by instead downgrading performance. This limit is a +// per-function limit. You can override the limit for each function using the +// `maxInstances` option in the function's options, e.g. +// `onRequest({ maxInstances: 5 }, (req, res) => { ... })`. +// NOTE: setGlobalOptions does not apply to functions using the v1 API. V1 +// functions should each use functions.runWith({ maxInstances: 10 }) instead. +// In the v1 API, each function can only serve one request per container, so +// this will be the maximum concurrent request count. +setGlobalOptions({ maxInstances: 10 }); + +// 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 132ebac9c81..c2d4f5493e6 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": "12" + "node": "{{RUNTIME}}" }, "main": "lib/index.js", "dependencies": { - "firebase-admin": "^9.2.0", - "firebase-functions": "^3.11.0" + "firebase-admin": "^12.6.0", + "firebase-functions": "^6.0.1" }, "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": "^5.7.3" }, "private": true } diff --git a/templates/init/functions/typescript/package.nolint.json b/templates/init/functions/typescript/package.nolint.json index e2863ec14bf..066161c31cd 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": "12" + "node": "{{RUNTIME}}" }, "main": "lib/index.js", "dependencies": { - "firebase-admin": "^9.2.0", - "firebase-functions": "^3.11.0" + "firebase-admin": "^12.6.0", + "firebase-functions": "^6.0.1" }, "devDependencies": { - "typescript": "^3.8.0", - "firebase-functions-test": "^0.2.0" + "typescript": "^5.7.3", + "firebase-functions-test": "^3.1.0" }, "private": true } diff --git a/templates/init/functions/typescript/tsconfig.json b/templates/init/functions/typescript/tsconfig.json index 7ce05d039d6..57b915f3cc9 100644 --- a/templates/init/functions/typescript/tsconfig.json +++ b/templates/init/functions/typescript/tsconfig.json @@ -1,6 +1,8 @@ { "compilerOptions": { - "module": "commonjs", + "module": "NodeNext", + "esModuleInterop": true, + "moduleResolution": "nodenext", "noImplicitReturns": true, "noUnusedLocals": true, "outDir": "lib", diff --git a/templates/init/hosting/index.html b/templates/init/hosting/index.html index f1e4eeb64c6..60e92365e8a 100644 --- a/templates/init/hosting/index.html +++ b/templates/init/hosting/index.html @@ -6,17 +6,17 @@ Welcome to Firebase Hosting - + - - - - - - - - - + + + + + + + + +