diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 968c0320f..000000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - extends: [__dirname+'/config/defaultEslintConfig.cjs'], - parserOptions: { - project: './tsconfig.eslint.json', - tsconfigRootDir: __dirname, - }, - rules: { - '@typescript-eslint/naming-convention': [ - 'error', - { - 'selector': 'variable', - 'types': ['boolean'], - 'format': ['PascalCase'], - 'prefix': ['is', 'with', 'should', 'has', 'can', 'did', 'will'] - } - ] - } - } \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..3df101eae --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,101 @@ +# GitHub Copilot – Instructions for this project + +## 🧰 Tech stack + +This project is a **pnpm monorepo** made up of the following packages: + +- `packages/oidc-client` — vanilla OIDC client +- `packages/react-oidc` — React bindings +- `packages/oidc-client-service-worker` — OIDC service worker + +Main language: **TypeScript**. Package manager: **pnpm**. + +--- + +## ✅ Mandatory steps before stopping + +> **You must never stop before all the following steps have completed successfully.** + +### 1. Auto-fix lint issues + +```bash +pnpm lint-fix +``` + +### 2. Verify no lint errors remain + +```bash +pnpm lint +``` + +> If errors remain after lint-fix, **you must fix them manually** in the code, then re-run `pnpm lint` until zero errors are reported. + +### 3. Run all unit tests + +```bash +pnpm test:ci +``` + +> If tests fail, **you must fix the code or the tests** depending on the root cause, then re-run until all tests are green. + +### 4. Final build (optional but recommended) + +```bash +pnpm build +``` + +--- + +## 🧼 Clean Code principles to follow + +### Naming +- Variable, function, class and file names must be **clear, descriptive and in English**. +- Avoid obscure abbreviations (`tmp`, `cb`, `d`, `val`…) except established conventions (`e` for event, `i` for index). +- Booleans must start with `is`, `has`, `can`, `should`. + +### Functions +- One function = **one single responsibility**. +- No more than **3–4 parameters** per function. Use an object if needed. +- Prefer **pure functions** and **immutability**. +- Avoid undocumented side effects. + +### TypeScript +- **Explicit typing** is mandatory for function parameters and return values. +- No `any` unless absolutely necessary (and with an explanatory comment). +- Use TypeScript **utility types** (`Partial`, `Readonly`, `Pick`, `Omit`…) where appropriate. +- Prefer `interface` for public contracts and `type` for unions/intersections. + +### Code structure +- Respect **separation of concerns**: business logic separated from display logic. +- No dead code left commented out in files. +- `TODO` comments must include clear context (e.g. `// TODO: [#123] Refactor once API is stable`). + +### Tests +- Every new function or behaviour must be **covered by a unit test**. +- Tests must be **readable and expressive**: `describe` / `it` with natural English sentences. +- Do not duplicate tested logic inside assertions. +- Use mocks/stubs sparingly and always clean them up (`afterEach`, `vi.restoreAllMocks()`). + +### Imports +- Imports must be **ordered**: third-party libraries → internal packages → local files. +- Use **named imports** rather than default imports whenever possible. +- No unused imports. + +### Comments +- Code must be **self-documenting**: if you need a comment to explain **what** the code does, refactor the code instead. +- Comments explain the **why**, not the **what**. +- Public functions and types must be documented with **JSDoc**. + +--- + +## 🔁 Expected workflow + +``` +1. Write or modify the code +2. pnpm lint-fix → automatic fixes +3. pnpm lint → verification (fix manually if errors remain) +4. pnpm test:ci → all tests must pass (fix if failures) +5. pnpm build → verify the build does not break +``` + +> 🚫 **Never submit or stop if any step fails.** diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3c6510cc1..334e5b309 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,6 +6,6 @@ updates: directory: '/' # Check the npm registry for updates every day (weekdays) schedule: - interval: 'daily' + interval: 'monthly' commit-message: prefix: 'build(npm): ' diff --git a/.github/workflows/azure-static-web-apps-black-rock-0dc6b0d03.yml b/.github/workflows/azure-static-web-apps-black-rock-0dc6b0d03.yml index 1d00995a0..a425f39d8 100644 --- a/.github/workflows/azure-static-web-apps-black-rock-0dc6b0d03.yml +++ b/.github/workflows/azure-static-web-apps-black-rock-0dc6b0d03.yml @@ -9,6 +9,8 @@ on: branches: - main +env: + NODE_VERSION: 22 jobs: skip_ci: runs-on: ubuntu-latest @@ -22,18 +24,24 @@ jobs: runs-on: ubuntu-latest name: Build and Deploy Job steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: submodules: true - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: ${{ env.NODE_VERSION }} + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + - name: pnpm install + run: pnpm i --frozen-lockfile - name: pnpm install run: pnpm i --frozen-lockfile - working-directory: ./packages/react-oidc + working-directory: ./examples/react-oidc-demo - name: pnpm run build run: pnpm run build - working-directory: ./packages/react-oidc + working-directory: ./examples/react-oidc-demo - name: Build And Deploy id: builddeploy uses: Azure/static-web-apps-deploy@v1 @@ -44,9 +52,9 @@ jobs: skip_app_build: true ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig - app_location: "/packages/react-oidc/build" # App source code path + app_location: "/examples/react-oidc-demo/dist" # App source code path api_location: "" # Api source code path - optional - output_location: "build" # Built app content directory - optional + #output_location: "dist" # Built app content directory - optional ###### End of Repository/Build Configurations ###### close_pull_request_job: diff --git a/.github/workflows/azure-static-web-apps-icy-glacier-004ab4303.yml b/.github/workflows/azure-static-web-apps-icy-glacier-004ab4303.yml index 33e3657df..7c50e77d9 100644 --- a/.github/workflows/azure-static-web-apps-icy-glacier-004ab4303.yml +++ b/.github/workflows/azure-static-web-apps-icy-glacier-004ab4303.yml @@ -9,6 +9,8 @@ on: branches: - main +env: + NODE_VERSION: 22 jobs: skip_ci: runs-on: ubuntu-latest @@ -22,15 +24,24 @@ jobs: runs-on: ubuntu-latest name: Build and Deploy Job steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: submodules: true - - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: ${{ env.NODE_VERSION }} + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + - name: pnpm install root + run: pnpm i --frozen-lockfile - name: pnpm install run: pnpm i --frozen-lockfile - working-directory: ./examples/react-oidc-demo + working-directory: ./examples/oidc-client-demo + - name: pnpm run build + run: pnpm run build + working-directory: ./examples/oidc-client-demo - name: Build And Deploy id: builddeploy uses: Azure/static-web-apps-deploy@v1 @@ -38,11 +49,12 @@ jobs: azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ICY_GLACIER_004AB4303 }} repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) action: "upload" + skip_app_build: true ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig - app_location: "/examples/react-oidc-demo" # App source code path + app_location: "/examples/oidc-client-demo/dist" # App source code path api_location: "" # Api source code path - optional - output_location: "build" # Built app content directory - optional + #output_location: "dist" # Built app content directory - optional ###### End of Repository/Build Configurations ###### close_pull_request_job: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e939c8e61..f5eba74b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,25 +9,23 @@ on: # branches: [ main ] env: - PNPM_VERSION: 8.5.1 - NODE_VERSION: 18 + NODE_VERSION: 22 jobs: tests: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 name: Install pnpm with: - version: ${{ env.PNPM_VERSION }} run_install: false # oidc-client diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..dc5979f25 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,48 @@ +name: Lint + +on: + # Trigger the workflow on push or pull request, + # but only for the main branch + push: + # branches: + # - main + pull_request: + # branches: + # - main + +env: + NODE_VERSION: 22 + +jobs: + run-linters: + name: Run linters + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@v4 + + - name: Install node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - uses: pnpm/action-setup@v4 + name: Install pnpm + with: + run_install: false + + # ESLint and Prettier must be in `package.json` + - name: Install dependencies + run: pnpm i + + - name: Run lint + run: pnpm run lint + + # Currently incompatible with ESlint 9 (https://github.com/wearerequired/lint-action/pull/799) + # - name: Run linters + # uses: wearerequired/lint-action@v2 + # with: + # eslint: true + # prettier: false #runs part of eslint + # autofix: false #does not work well with pull requests https://github.com/wearerequired/lint-action?tab=readme-ov-file#limitations \ No newline at end of file diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index bce42b6f2..85f222029 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -10,8 +10,7 @@ on: branches: [ main ] env: - PNPM_VERSION: 8.5.1 - NODE_VERSION: 18 + NODE_VERSION: 22 jobs: skip_ci: runs-on: ubuntu-latest @@ -21,19 +20,19 @@ jobs: - id: check uses: Legorooj/skip-ci@main tests: + needs: skip_ci runs-on: ubuntu-latest if: needs.skip_ci.outputs.canSkip != 'true' && github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 name: Install pnpm with: - version: ${{ env.PNPM_VERSION }} - run_install: false + run_install: false - name: pnpm i run: pnpm i --frozen-lockfile @@ -56,19 +55,29 @@ jobs: working-directory: ./packages/oidc-client build: + needs: skip_ci + permissions: + contents: write + id-token: write environment: react-oidc runs-on: ubuntu-latest if: needs.skip_ci.outputs.canSkip != 'true' && !github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: token: ${{ secrets.GIT_TOKEN }} - + fetch-depth: 0 + + - name: Configure git identity for versioning and release commits + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + - name: Determine Alpha, Beta or Release id: which_tag run: | if [[ ${{ github.ref }} == refs/pull* ]]; then - last_commit_message=$(curl -s "https://api.github.com/repos/AxaFrance/react-oidc/pulls/${{ github.event.number }}/commits" | jq -r '.[-1].commit.message') + last_commit_message=$(curl -s "https://api.github.com/repos/${GITHUB_REPOSITORY}/pulls/${{ github.event.number }}/commits" | jq -r '.[-1].commit.message') else last_commit_message=$(git log --format=%B -n 1) fi @@ -84,78 +93,128 @@ jobs: echo "tag=release" >> $GITHUB_OUTPUT fi - - name: Bump version and push tag + - name: Compute release version id: tag_release if: github.ref == 'refs/heads/main' && steps.which_tag.outputs.tag == 'release' uses: mathieudutour/github-tag-action@v6.0 with: - github_token: ${{ secrets.GITHUB_TOKEN }} - - name: Bump version and push tag + github_token: ${{ secrets.GIT_TOKEN }} + dry_run: true + - name: Compute prerelease version id: tag_version if: steps.which_tag.outputs.tag == 'alpha' || steps.which_tag.outputs.tag == 'beta' uses: mathieudutour/github-tag-action@v6.0 with: - github_token: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ secrets.GIT_TOKEN }} dry_run: true - name: Compute new version number to publish id: tag if: (github.ref == 'refs/heads/main' && steps.which_tag.outputs.tag == 'release') || steps.which_tag.outputs.tag == 'alpha' || steps.which_tag.outputs.tag == 'beta' + shell: bash run: | + set -euo pipefail + if [[ '${{ steps.which_tag.outputs.tag }}' == 'release' ]]; then - version=${{ steps.tag_release.outputs.new_version }} + version="${{ steps.tag_release.outputs.new_version }}" else - version=${{ steps.tag_version.outputs.new_version }} + version="${{ steps.tag_version.outputs.new_version }}" + fi + + if [[ -z "$version" ]]; then + echo "::error::Unable to compute the package version to publish." + exit 1 fi - + if [[ '${{ steps.which_tag.outputs.tag }}' = 'release' ]]; then echo "new_version=$version" >> $GITHUB_OUTPUT fi if [[ '${{ steps.which_tag.outputs.tag }}' = 'alpha' ]]; then - echo "new_version=$version-alpha${{ github.run_number }}" >> $GITHUB_OUTPUT + echo "new_version=$version-alpha.${{ github.run_number }}" >> $GITHUB_OUTPUT fi if [[ '${{ steps.which_tag.outputs.tag }}' = 'beta' ]]; then - echo "new_version=$version-beta${{ github.run_number }}" >> $GITHUB_OUTPUT + echo "new_version=$version-beta.${{ github.run_number }}" >> $GITHUB_OUTPUT fi - - - uses: actions/setup-node@v2 + + - uses: actions/setup-node@v7 with: node-version: ${{ env.NODE_VERSION }} + package-manager-cache: false + + - name: Install npm with trusted publishing support + run: npm install --global "npm@^11.5.1" + + - name: Verify npm trusted publishing prerequisites + shell: bash + run: | + set -euo pipefail + + npm --version + + if [[ -n "${NODE_AUTH_TOKEN:-}" ]]; then + echo "::error::NODE_AUTH_TOKEN must be unset for npm trusted publishing." + exit 1 + fi + + if [[ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" || -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ]]; then + echo "::error::GitHub Actions did not expose the OIDC token request environment." + exit 1 + fi - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 name: Install pnpm with: - version: ${{ env.PNPM_VERSION }} - run_install: false + run_install: false - - name: npm version ${{ steps.tag.outputs.new_version }} + - name: Update package versions to ${{ steps.tag.outputs.new_version }} if: (github.ref == 'refs/heads/main' && steps.which_tag.outputs.tag == 'release') || steps.which_tag.outputs.tag == 'alpha' || steps.which_tag.outputs.tag == 'beta' - run: npm version ${{ steps.tag.outputs.new_version }} - working-directory: ./packages/react-oidc - - - name: pnpm i + shell: bash + run: | + set -euo pipefail + + version="${{ steps.tag.outputs.new_version }}" + if [[ -z "$version" ]]; then + echo "::error::No version has been computed for publication." + exit 1 + fi + + EXPECTED_VERSION="$version" node <<'NODE' + const fs = require('node:fs'); + + const expectedVersion = process.env.EXPECTED_VERSION; + const packageDirs = [ + 'packages/oidc-client-service-worker', + 'packages/oidc-client', + 'packages/react-oidc', + ]; + + for (const packageDir of packageDirs) { + console.log(`Updating ${packageDir} to ${expectedVersion}`); + + const packageJsonPath = `${packageDir}/package.json`; + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + packageJson.version = expectedVersion; + fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`); + } + NODE + + printf "export default '%s';\n" "$version" > packages/oidc-client-service-worker/src/version.ts + printf "export default '%s';\n" "$version" > packages/oidc-client/src/version.ts + + + # oidc-client-service-worker + - name: pnpm ci run: pnpm i --frozen-lockfile - working-directory: ./packages/react-oidc + working-directory: ./packages/oidc-client-service-worker - name: pnpm prepare run: pnpm run prepare - working-directory: ./packages/react-oidc - + working-directory: ./packages/oidc-client-service-worker + - name: pnpm test run: pnpm test -- --run - working-directory: ./packages/react-oidc - - - id: publish-react - uses: JS-DevTools/npm-publish@v1 - if: (github.ref == 'refs/heads/main' && steps.which_tag.outputs.tag == 'release') || steps.which_tag.outputs.tag == 'alpha' || steps.which_tag.outputs.tag == 'beta' - with: - token: ${{ secrets.NPM_TOKEN }} - package: ./packages/react-oidc/package.json - - - name: pnpm version ${{ steps.tag.outputs.new_version }} - if: (github.ref == 'refs/heads/main' && steps.which_tag.outputs.tag == 'release') || steps.which_tag.outputs.tag == 'alpha' || steps.which_tag.outputs.tag == 'beta' - run: pnpm version ${{ steps.tag.outputs.new_version }} - working-directory: ./packages/oidc-client + working-directory: ./packages/oidc-client-service-worker + # oidc-client - name: pnpm ci run: pnpm i --frozen-lockfile working-directory: ./packages/oidc-client @@ -164,40 +223,356 @@ jobs: run: pnpm run prepare working-directory: ./packages/oidc-client - - id: publish-oidc-client - uses: JS-DevTools/npm-publish@v1 + - name: pnpm test + run: pnpm test -- --run + working-directory: ./packages/oidc-client + + # React-oidc + - name: pnpm i + run: pnpm i --frozen-lockfile + working-directory: ./packages/react-oidc + + - name: pnpm prepare + run: pnpm run prepare + working-directory: ./packages/react-oidc + + - name: pnpm test + run: pnpm test -- --run + working-directory: ./packages/react-oidc + + - name: Prepare package manifests for npm publish + id: prepare_publish_manifests if: (github.ref == 'refs/heads/main' && steps.which_tag.outputs.tag == 'release') || steps.which_tag.outputs.tag == 'alpha' || steps.which_tag.outputs.tag == 'beta' - with: - token: ${{ secrets.NPM_TOKEN }} - package: ./packages/oidc-client/package.json + shell: bash + env: + EXPECTED_VERSION: ${{ steps.tag.outputs.new_version }} + run: | + set -euo pipefail + + node <<'NODE' + const fs = require('node:fs'); + const path = require('node:path'); + + const expectedVersion = process.env.EXPECTED_VERSION; + const lifecycleScriptBackupPath = path.join( + process.env.RUNNER_TEMP ?? process.cwd(), + 'npm-publish-lifecycle-scripts.json', + ); + const packagePaths = [ + 'packages/oidc-client-service-worker/package.json', + 'packages/oidc-client/package.json', + 'packages/react-oidc/package.json', + ]; + const dependencyUpdates = { + 'packages/oidc-client/package.json': ['@axa-fr/oidc-client-service-worker'], + 'packages/react-oidc/package.json': [ + '@axa-fr/oidc-client', + '@axa-fr/oidc-client-service-worker', + ], + }; + const publishLifecycleScripts = [ + 'prepublishOnly', + 'prepack', + 'prepare', + 'postpack', + 'publish', + 'postpublish', + ]; + const lifecycleScriptBackup = {}; + + for (const packagePath of packagePaths) { + const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + + if (!packageJson.scripts?.prepare) { + console.error(`${packageJson.name} is missing the required prepare script.`); + process.exit(1); + } + + lifecycleScriptBackup[packagePath] = { ...packageJson.scripts }; + + for (const dependencyName of dependencyUpdates[packagePath] ?? []) { + if (!packageJson.dependencies?.[dependencyName]) { + console.error(`${packageJson.name} is missing dependency ${dependencyName}.`); + process.exit(1); + } - - name: SonarCloud Scan - uses: sonarsource/sonarcloud-github-action@master - if: github.event.pull_request.head.repo.full_name == github.repository && !github.event.pull_request.head.repo.fork + packageJson.dependencies[dependencyName] = expectedVersion; + } + + for (const scriptName of publishLifecycleScripts) { + delete packageJson.scripts?.[scriptName]; + } + + fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`); + } + + fs.mkdirSync(path.dirname(lifecycleScriptBackupPath), { recursive: true }); + fs.writeFileSync( + lifecycleScriptBackupPath, + `${JSON.stringify(lifecycleScriptBackup, null, 2)}\n`, + ); + NODE + + - name: Verify package versions before publish + if: (github.ref == 'refs/heads/main' && steps.which_tag.outputs.tag == 'release') || steps.which_tag.outputs.tag == 'alpha' || steps.which_tag.outputs.tag == 'beta' + shell: bash env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - with: - args: > - -Dsonar.organization=axaguildev - -Dsonar.projectKey=AxaGuilDEv_react-oidc - -Dsonar.exclusions=**/*.spec.js,**/*.stories.js,Scripts/**,**/*.scss,**/__snapshots__/**,**/*[Tt]ests.cs,**/node_modules/**,**/ClientApp/build/**,**/ClientApp/.storybook/**,**/ClientApp/storybook-static/**,**/obj/**,**/__mocks__/**,**/ClientApp/src/serviceWorker.ts - -Dsonar.javascript.lcov.reportPaths=**/coverage/lcov.info - - - name: Commit updates package.json - uses: stefanzweifel/git-auto-commit-action@v4 - if: github.ref == 'refs/heads/main' - with: - commit_message: "[skip ci] Update version package.json" - commit_user_name: GitHub - commit_user_email: github-action@bot.com - commit_author: GitHub - push_options: '--force' + EXPECTED_VERSION: ${{ steps.tag.outputs.new_version }} + run: | + set -euo pipefail + + node <<'NODE' + const fs = require('node:fs'); + + const expectedVersion = process.env.EXPECTED_VERSION; + const packagePaths = [ + 'packages/oidc-client-service-worker/package.json', + 'packages/oidc-client/package.json', + 'packages/react-oidc/package.json', + ]; + + let hasError = false; + + for (const packagePath of packagePaths) { + const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + + if (packageJson.version !== expectedVersion) { + console.error(`${packageJson.name} has version ${packageJson.version}, expected ${expectedVersion}.`); + hasError = true; + } + + const workspaceDependencies = Object.entries(packageJson.dependencies ?? {}) + .filter(([, version]) => version === 'workspace:*') + .map(([dependencyName]) => dependencyName); + const publishLifecycleScripts = [ + 'prepublishOnly', + 'prepack', + 'prepare', + 'postpack', + 'publish', + 'postpublish', + ]; + const lifecycleScripts = publishLifecycleScripts.filter( + scriptName => packageJson.scripts?.[scriptName], + ); + + if (workspaceDependencies.length > 0) { + console.error(`${packageJson.name} still contains workspace dependencies: ${workspaceDependencies.join(', ')}.`); + hasError = true; + } + + if (lifecycleScripts.length > 0) { + console.error(`${packageJson.name} still contains publish lifecycle scripts: ${lifecycleScripts.join(', ')}.`); + hasError = true; + } + } + + if (hasError) { + process.exit(1); + } + NODE + + - name: Publish packages to npm + id: npm_publish + if: (github.ref == 'refs/heads/main' && steps.which_tag.outputs.tag == 'release') || steps.which_tag.outputs.tag == 'alpha' || steps.which_tag.outputs.tag == 'beta' + continue-on-error: true + shell: bash + env: + EXPECTED_VERSION: ${{ steps.tag.outputs.new_version }} + run: | + set -euo pipefail + + package_dirs=( + "packages/oidc-client-service-worker" + "packages/oidc-client" + "packages/react-oidc" + ) + package_names=( + "@axa-fr/oidc-client-service-worker" + "@axa-fr/oidc-client" + "@axa-fr/react-oidc" + ) + + publish_tag="${{ steps.which_tag.outputs.tag }}" + if [[ "$publish_tag" == "release" ]]; then + publish_tag="latest" + fi + + published_count=0 + + for index in "${!package_dirs[@]}"; do + package_dir="${package_dirs[$index]}" + package_path="./${package_dir}" + package_name="${package_names[$index]}" + + if [[ ! -d "$package_path" ]]; then + echo "::error::Package directory $package_path does not exist." + exit 1 + fi + + if npm view "${package_name}@${EXPECTED_VERSION}" version --registry=https://registry.npmjs.org/ >/dev/null 2>&1; then + echo "${package_name}@${EXPECTED_VERSION} already exists on npm; skipping publish." + continue + fi + + npm publish "$package_path" --access public --tag "$publish_tag" --registry=https://registry.npmjs.org/ --ignore-scripts + published_count=$((published_count + 1)) + done + + if [[ "$published_count" -eq 0 ]]; then + echo "All expected package versions already exist on npm; skipping publish." + fi + + echo "published_count=$published_count" >> "$GITHUB_OUTPUT" + - name: Verify npm packages after publish + if: always() && ((github.ref == 'refs/heads/main' && steps.which_tag.outputs.tag == 'release') || steps.which_tag.outputs.tag == 'alpha' || steps.which_tag.outputs.tag == 'beta') + shell: bash + env: + EXPECTED_VERSION: ${{ steps.tag.outputs.new_version }} + run: | + set -euo pipefail + + package_names=( + "@axa-fr/oidc-client-service-worker" + "@axa-fr/oidc-client" + "@axa-fr/react-oidc" + ) + + for package_name in "${package_names[@]}"; do + published_version="" + + for attempt in {1..6}; do + if published_version=$(npm view "${package_name}@${EXPECTED_VERSION}" version --registry=https://registry.npmjs.org/ 2>/dev/null) && [[ "$published_version" == "$EXPECTED_VERSION" ]]; then + break + fi + + echo "Waiting for ${package_name}@${EXPECTED_VERSION} to be available on npm (attempt ${attempt}/6)." + sleep 10 + done + + if [[ "$published_version" != "$EXPECTED_VERSION" ]]; then + echo "::error::${package_name}@${EXPECTED_VERSION} was not found on npm after publication." + exit 1 + fi + done + + - name: Restore package manifests after publish + if: always() && steps.prepare_publish_manifests.outcome == 'success' + shell: bash + run: | + set -euo pipefail + + node <<'NODE' + const fs = require('node:fs'); + const path = require('node:path'); + + const lifecycleScriptBackupPath = path.join( + process.env.RUNNER_TEMP ?? process.cwd(), + 'npm-publish-lifecycle-scripts.json', + ); + const packagePaths = [ + 'packages/oidc-client-service-worker/package.json', + 'packages/oidc-client/package.json', + 'packages/react-oidc/package.json', + ]; + const dependencyUpdates = { + 'packages/oidc-client/package.json': ['@axa-fr/oidc-client-service-worker'], + 'packages/react-oidc/package.json': [ + '@axa-fr/oidc-client', + '@axa-fr/oidc-client-service-worker', + ], + }; + + if (!fs.existsSync(lifecycleScriptBackupPath)) { + console.error(`Lifecycle script backup does not exist: ${lifecycleScriptBackupPath}.`); + process.exit(1); + } + + const lifecycleScriptBackup = JSON.parse( + fs.readFileSync(lifecycleScriptBackupPath, 'utf8'), + ); + + for (const packagePath of packagePaths) { + const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + const originalScripts = lifecycleScriptBackup[packagePath]; + + if (!originalScripts?.prepare) { + console.error(`${packageJson.name} has no prepare script in the lifecycle script backup.`); + process.exit(1); + } + + for (const dependencyName of dependencyUpdates[packagePath] ?? []) { + if (!packageJson.dependencies?.[dependencyName]) { + console.error(`${packageJson.name} is missing dependency ${dependencyName}.`); + process.exit(1); + } + + packageJson.dependencies[dependencyName] = 'workspace:*'; + } + + packageJson.scripts = originalScripts; + + const unRestoredDependencies = (dependencyUpdates[packagePath] ?? []).filter( + dependencyName => packageJson.dependencies?.[dependencyName] !== 'workspace:*', + ); + + if (!packageJson.scripts.prepare || unRestoredDependencies.length > 0) { + console.error(`${packageJson.name} was not restored to its workspace state.`); + process.exit(1); + } + + fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`); + } + + fs.rmSync(lifecycleScriptBackupPath, { force: true }); + NODE + + - name: Commit, tag and push release + if: github.ref == 'refs/heads/main' && steps.which_tag.outputs.tag == 'release' + shell: bash + run: | + set -euo pipefail + + release_tag="${{ steps.tag_release.outputs.new_tag }}" + if [[ -z "$release_tag" ]]; then + echo "::error::No release tag has been computed." + exit 1 + fi + + git add \ + packages/oidc-client-service-worker/package.json \ + packages/oidc-client-service-worker/src/version.ts \ + packages/oidc-client/package.json \ + packages/oidc-client/src/version.ts \ + packages/react-oidc/package.json + git commit -m "[skip ci] Update to version ${{ steps.tag.outputs.new_version }} in package.json" + git tag "$release_tag" + git push --set-upstream origin "HEAD:main" --follow-tags -f + + chmod +x ./bin/generate-changelog.sh + ./bin/generate-changelog.sh + git add CHANGELOG.md + git commit -m "[skip ci] Generate changelog to version ${{ steps.tag.outputs.new_version }}" + git push --set-upstream origin "HEAD:main" --follow-tags -f + + # - name: SonarCloud Scan + # uses: sonarsource/sonarcloud-github-action@master + # if: github.event.pull_request.head.repo.full_name == github.repository && !github.event.pull_request.head.repo.fork + # env: + # GITHUB_TOKEN: ${{ secrets.GIT_TOKEN }} + # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + # with: + # args: > + # -Dsonar.organization=axaguildev + # -Dsonar.projectKey=AxaGuilDEv_react-oidc + # -Dsonar.exclusions=**/*.spec.js,**/*.stories.js,Scripts/**,**/*.scss,**/__snapshots__/**,**/*[Tt]ests.cs,**/node_modules/**,**/ClientApp/build/**,**/ClientApp/.storybook/**,**/ClientApp/storybook-static/**,**/obj/**,**/__mocks__/**,**/ClientApp/src/serviceWorker.ts + # -Dsonar.javascript.lcov.reportPaths=**/coverage/lcov.info + # - name: Create a GitHub release uses: ncipollo/release-action@v1 - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main' && steps.which_tag.outputs.tag == 'release' with: - tag: ${{ steps.tag_version.outputs.new_tag }} - name: Release ${{ steps.tag_version.outputs.new_tag }} - body: ${{ steps.tag_version.outputs.changelog }} + tag: ${{ steps.tag_release.outputs.new_tag }} + name: Release ${{ steps.tag_release.outputs.new_tag }} + body: ${{ steps.tag_release.outputs.changelog }} diff --git a/.gitignore b/.gitignore index 10d5cede9..204bffd0b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ examples/**/build/ **/public/*.d.ts /public/* +/Folder.DotSettings.user diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..88b151d4f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +# deep dirs +**/dist +**/node_modules +**/fixtures + +# directories +.github +.changeset + +# files +pnpm-lock.yaml \ No newline at end of file diff --git a/.prettierrc.cjs b/.prettierrc.cjs new file mode 100644 index 000000000..6fb26d080 --- /dev/null +++ b/.prettierrc.cjs @@ -0,0 +1,20 @@ +module.exports = { + printWidth: 100, + tabWidth: 2, + trailingComma: 'all', + arrowParens: 'avoid', + endOfLine: 'auto', + bracketSameLine: false, + bracketSpacing: true, + singleQuote: true, + useTabs: false, + semi: true, + overrides: [ + { + files: ['.*', '*.json', '*.md', '*.toml', '*.yml'], + options: { + useTabs: false, + }, + }, + ], +}; diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 8de7f847e..000000000 --- a/.prettierrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "jsxSingleQuote": true, - "singleQuote": true -} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..0c57ba501 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,10 @@ +# Agent instructions + +- Always write code, comments, documentation, and responses in English. +- Keep changes focused and follow the existing TypeScript and pnpm conventions. +- Always keep the root and relevant package `README.md` files and `FAQ.md` up to + date when changing behavior, configuration, APIs, or setup instructions. + Document only verified behavior and clearly state limitations. +- Add or update tests for behavior changes. +- Before submitting, run `pnpm lint-fix`, `pnpm lint`, and `pnpm test:ci`. + Run `pnpm build` when changing code or build configuration. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1374bc538..4bc6dcfd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,489 +1,338 @@ -### Changelog -All notable changes to this project will be documented in this file. - -#### [v3.1.7](https://github.com/AxaGuilDEv/react-oidc/compare/v3.1.6...v3.1.7) -> 17 June 2021 -- feat: add signinSilent to useReactOidc and AuthenticationContext [`#578`](https://github.com/AxaGuilDEv/react-oidc/pull/578) by Olivier YOUF -- ci: add dependabot config file and change prefix to `build(npm)` [`#561`](https://github.com/AxaGuilDEv/react-oidc/pull/561) by Olivier YOUF -- feat:unload userManager when unnmount oidc provider [`#547`](https://github.com/AxaGuilDEv/react-oidc/pull/547) by Olivier YOUF -- feat(context-fetch/fetch-core): removing recompose [`#530`](https://github.com/AxaGuilDEv/react-oidc/pull/530) by Olivier YOUF -- chore(deps): bump example dependencies [`80ccc10`](https://github.com/AxaGuilDEv/react-oidc/commit/80ccc10c5cb33df891365c896b81b8eb1d5e46c5) by Julien Foratier -- fix: create usermanager before rendering + some refacto [`16cb26d`](https://github.com/AxaGuilDEv/react-oidc/commit/16cb26d057e50037b7ee04f32319f02a92ae72cd) by Amine JELTI -- chore(release): publish v3.1.7 ***NO_CI*** [`8b5cb40`](https://github.com/AxaGuilDEv/react-oidc/commit/8b5cb405d5735bacd24318c9fe0de1736b98712e) by Build-CI -- chore(deps): [security] bump handlebars from 4.4.2 to 4.7.6 [`c785f2d`](https://github.com/AxaGuilDEv/react-oidc/commit/c785f2d6cd92d9380c0c3ec99e01bb61f7ec2fcd) by dependabot-preview[bot] -- chore(deps): [security] bump ini from 1.3.5 to 1.3.8 [`c978517`](https://github.com/AxaGuilDEv/react-oidc/commit/c9785176ccd4c1cd3777f8e6e12e3535a37e7aa6) by dependabot-preview[bot] -- fix(examples): replace https://demo.identityserver.io deprecated configuration [`ebe9e79`](https://github.com/AxaGuilDEv/react-oidc/commit/ebe9e79465f52323626b2ac94b333e8d51987999) by Julien Foratier -- docs(changelog) update to new 3.1.6 [`8cfbc7c`](https://github.com/AxaGuilDEv/react-oidc/commit/8cfbc7c6111444a156c5bddcad253c0366e6f38c) by Build-CI -- chore(deps): [security] bump lodash from 4.17.15 to 4.17.20 [`124a9ca`](https://github.com/AxaGuilDEv/react-oidc/commit/124a9ca777135382695f2873253f076bf4f7434a) by dependabot-preview[bot] - -#### [v3.1.6](https://github.com/AxaGuilDEv/react-oidc/compare/v3.1.5...v3.1.6) -> 13 October 2020 -- feat(context): Allowing hooking into the oidc-client events in the AuthenticationProvider [`#415`](https://github.com/AxaGuilDEv/react-oidc/issues/415) by Michael Wolfenden -- chore(release): publish v3.1.6 ***NO_CI*** [`9c1a618`](https://github.com/AxaGuilDEv/react-oidc/commit/9c1a6186d4f804d6ddcf50819602653d50f05df3) by Build-CI -- docs(changelog) update to new 3.1.5 [`02ad255`](https://github.com/AxaGuilDEv/react-oidc/commit/02ad25574978464cb67123d4b695a62a15704960) by Build-CI -- feat(logger): remove debug overriddenConfiguration [`f23bebd`](https://github.com/AxaGuilDEv/react-oidc/commit/f23bebdd64de6082d183bead17553174919d515a) by Arnaud Foraison -- fix(example): add hook dependency [`b491c02`](https://github.com/AxaGuilDEv/react-oidc/commit/b491c02757775052d1605b2922fa13e25acf7128) by Arnaud Foraison - -#### [v3.1.5](https://github.com/AxaGuilDEv/react-oidc/compare/v3.1.4...v3.1.5) -> 8 September 2020 -- Add prop-types as dependency [`#507`](https://github.com/AxaGuilDEv/react-oidc/pull/507) by Olivier YOUF -- fix(core): keep location.hash in the url used to redirect after login [`#515`](https://github.com/AxaGuilDEv/react-oidc/pull/515) by jreimbold-t1 -- fix: move prop-types from peer to dependencies [`a53a189`](https://github.com/AxaGuilDEv/react-oidc/commit/a53a189b08b70ee705bb06fd0d8eee4184a2558b) by Arnaud Foraison -- chore(release): publish v3.1.5 ***NO_CI*** [`0a24a04`](https://github.com/AxaGuilDEv/react-oidc/commit/0a24a04741b5d6ac28d0cd4d6346beeda01e79a8) by Build-CI -- docs(changelog) update to new 3.1.4 [`b43b45d`](https://github.com/AxaGuilDEv/react-oidc/commit/b43b45dab5b2ae6f46cce62e3061e23ab388b61e) by Build-CI - -#### [v3.1.4](https://github.com/AxaGuilDEv/react-oidc/compare/v3.1.3...v3.1.4) -> 14 July 2020 -- fix(context): made type for AuthenticationProviderProps partial [`#473`](https://github.com/AxaGuilDEv/react-oidc/pull/473) by Olivier YOUF -- chore(release): publish v3.1.4 ***NO_CI*** [`aea9407`](https://github.com/AxaGuilDEv/react-oidc/commit/aea940793fbb9ecf4cc87a0d870e4b9e7d2e5156) by Build-CI -- docs(changelog) update to new 3.1.3 [`c3043c4`](https://github.com/AxaGuilDEv/react-oidc/commit/c3043c4078b24906e35b4a15062a1995e79683af) by Build-CI - -#### [v3.1.3](https://github.com/AxaGuilDEv/react-oidc/compare/v3.1.2...v3.1.3) -> 16 June 2020 -- style: remove overwritten property jsx-filename-extension [`#468`](https://github.com/AxaGuilDEv/react-oidc/pull/468) by Olivier YOUF -- fix(Vanilla): return type callback with signinSilent [`#469`](https://github.com/AxaGuilDEv/react-oidc/pull/469) by Olivier YOUF -- feat(react-oidc-redux): Adding Redux Authenticating component [`#463`](https://github.com/AxaGuilDEv/react-oidc/pull/463) by Olivier YOUF -- Fix types for AuthenticationProvider and withOidcUser [`#457`](https://github.com/AxaGuilDEv/react-oidc/pull/457) by Olivier YOUF -- chore(release): publish v3.1.3 ***NO_CI*** [`5b4db53`](https://github.com/AxaGuilDEv/react-oidc/commit/5b4db533b3edaa60b273dd05dd822b6687793165) by Build-CI -- Replace Pick with Omit [`7f160b7`](https://github.com/AxaGuilDEv/react-oidc/commit/7f160b708bb51dd43389571fd6f0d8e66b5cd2f8) by Vincent Ricard -- docs(changelog) update to new 3.1.2 [`faa73a7`](https://github.com/AxaGuilDEv/react-oidc/commit/faa73a7c9cc015ac4dd10289819d06d071ef7ca7) by Build-CI - -#### [v3.1.2](https://github.com/AxaGuilDEv/react-oidc/compare/v3.1.1...v3.1.2) -> 11 June 2020 -- fix(context): Renrender children aftersilent signin [`#464`](https://github.com/AxaGuilDEv/react-oidc/pull/464) by Olivier YOUF -- chore(release): publish v3.1.2 ***NO_CI*** [`6dbcbc8`](https://github.com/AxaGuilDEv/react-oidc/commit/6dbcbc860df7ee6697983d1024950312a2c8eea4) by Build-CI -- Add usage example to demo [`e3bb8b0`](https://github.com/AxaGuilDEv/react-oidc/commit/e3bb8b03e13048f99672050946bb2379dde417a4) by Viet VO -- Fix types for AuthenticationProvider and withOidcUser [`5004289`](https://github.com/AxaGuilDEv/react-oidc/commit/50042890bc070bd87a99c0c730acfc47678e0d8a) by Vincent Ricard -- Add readme [`be2bb57`](https://github.com/AxaGuilDEv/react-oidc/commit/be2bb5783ae702d65dbc90b38cf41bcf55002415) by Viet VO -- Add config option to OidcSecure [`7881742`](https://github.com/AxaGuilDEv/react-oidc/commit/7881742d81fda72571d88642da13a5fd6d188139) by Viet VO -- PR: change to authenticating [`bf06f9e`](https://github.com/AxaGuilDEv/react-oidc/commit/bf06f9e5f25e8d6343849f176ed5e1ecfb54b158) by Viet VO -- fix(Vanilla): return type callback with signinSilent [`3d4f8af`](https://github.com/AxaGuilDEv/react-oidc/commit/3d4f8af68901a39d6a9e4c958cf6c6e24c5c5ab5) by Hamza HAMIDI -- style: remove overwritten property jsx-filename-extension [`5dee604`](https://github.com/AxaGuilDEv/react-oidc/commit/5dee604e9bb16edad8a0c87c3c29d2fa98fc870f) by Hamza Hamidi -- docs(changelog) update to new 3.1.1 [`dd8b685`](https://github.com/AxaGuilDEv/react-oidc/commit/dd8b685650d08c2d2a480e7406b5d2f09c0ddc50) by Build-CI -- remove useMemo [`4cbf8f2`](https://github.com/AxaGuilDEv/react-oidc/commit/4cbf8f2df93271544f09f28008b199bfea07b391) by Viet VO - -#### [v3.1.1](https://github.com/AxaGuilDEv/react-oidc/compare/v3.1.1-alpha.0...v3.1.1) -> 25 May 2020 -- chore(release): publish v3.1.1 ***NO_CI*** [`1500b7e`](https://github.com/AxaGuilDEv/react-oidc/commit/1500b7e28914afb913ea4fc3e6b87ee707999fc3) by Build-CI -- docs(changelog) update to new 3.1.1-alpha.0 [`90ea0c9`](https://github.com/AxaGuilDEv/react-oidc/commit/90ea0c954531093f6f7a5cd221cf4d986468fd7c) by Build-CI - -#### [v3.1.1-alpha.0](https://github.com/AxaGuilDEv/react-oidc/compare/v3.1.0-alpha.0...v3.1.1-alpha.0) -> 20 May 2020 -- fix(context): Memoize the child component [`#451`](https://github.com/AxaGuilDEv/react-oidc/pull/451) by Olivier YOUF -- chore(release): publish v3.1.1-alpha.0 ***NO_CI*** [`dc8ee89`](https://github.com/AxaGuilDEv/react-oidc/commit/dc8ee8944258e33430be309cb09d5c55f71fc13b) by Build-CI -- docs(changelog) update to new 3.1.0-alpha.0 [`6a33c46`](https://github.com/AxaGuilDEv/react-oidc/commit/6a33c46beaa3f32c56c516811d6961362e761b20) by Build-CI - -#### [v3.1.0-alpha.0](https://github.com/AxaGuilDEv/react-oidc/compare/v3.0.8...v3.1.0-alpha.0) -> 12 May 2020 -- fix: add logout action [`#439`](https://github.com/AxaGuilDEv/react-oidc/pull/439) by Olivier YOUF -- refactor(context): Refactor/containers providers [`#432`](https://github.com/AxaGuilDEv/react-oidc/pull/432) by Olivier YOUF -- Typescript migration [`#410`](https://github.com/AxaGuilDEv/react-oidc/pull/410) by Olivier YOUF -- Remove all occurences of 'triggerAuthFlow' [`#425`](https://github.com/AxaGuilDEv/react-oidc/pull/425) by Olivier YOUF -- Overrides the react-redux dependency [`#424`](https://github.com/AxaGuilDEv/react-oidc/pull/424) by Olivier YOUF -- Fix minor typos in docs [`#414`](https://github.com/AxaGuilDEv/react-oidc/pull/414) by Tommi Laukkanen -- Remove all occurences of 'triggerAuthFlow' [`#392`](https://github.com/AxaGuilDEv/react-oidc/issues/392) by Vincent Ricard -- Upgrade Jest [`7281ee1`](https://github.com/AxaGuilDEv/react-oidc/commit/7281ee1e5a7d3f19f6381a427ce5bfa4332829ef) by Vincent Ricard -- Migration of packages/core [`9f518a5`](https://github.com/AxaGuilDEv/react-oidc/commit/9f518a50f7867a702d4174e764d169de9b21a5c6) by Vincent Ricard -- Format code with prettier and fix lint errors [`52ae362`](https://github.com/AxaGuilDEv/react-oidc/commit/52ae362769572e29155403695d8bb5de26ea4c26) by Vincent Ricard -- WIP [`b123a42`](https://github.com/AxaGuilDEv/react-oidc/commit/b123a4206ec4b7b134714d8f9e70434682a01c99) by Vincent Ricard -- WIP context package [`0fa40e9`](https://github.com/AxaGuilDEv/react-oidc/commit/0fa40e99a3d42ebdcf472bbd6d0ba7cca55c25c7) by Vincent Ricard -- WIP redux package [`b251bcc`](https://github.com/AxaGuilDEv/react-oidc/commit/b251bcc452a1362e54463d3f4c86b9f883c973f2) by Vincent Ricard -- Rename OidcHistory into ReactOidcHistory [`760c77f`](https://github.com/AxaGuilDEv/react-oidc/commit/760c77f511536eaf49f7e1e3b702a741c0723d36) by Vincent Ricard -- chore(release): publish v3.1.0-alpha.0 ***NO_CI*** [`892f3cb`](https://github.com/AxaGuilDEv/react-oidc/commit/892f3cb98ab452f11823af8ed44fb16897f14ecb) by Build-CI -- chore(release): publish v3.0.9-alpha.0 ***NO_CI*** [`61c8884`](https://github.com/AxaGuilDEv/react-oidc/commit/61c8884393c0df5ae37b0c8b113cb92a942602aa) by Build-CI -- WIP [`0282c3c`](https://github.com/AxaGuilDEv/react-oidc/commit/0282c3c81cfb1ecc4be57408f3dd6e7a2bc72fe7) by Vincent Ricard -- Fix some tests [`6a7b09c`](https://github.com/AxaGuilDEv/react-oidc/commit/6a7b09c90d47203eb5cfe5bafb71e3371def176f) by Vincent Ricard -- docs(changelog) update to new 3.0.9-alpha.0 [`08a6938`](https://github.com/AxaGuilDEv/react-oidc/commit/08a69388c8177bc87335cb741330811db6d2aca2) by Build-CI -- Add specific type ofr UserStore [`1b44db5`](https://github.com/AxaGuilDEv/react-oidc/commit/1b44db5d9a9c433b27571e73335bfc18fec4f97f) by Vincent Ricard -- WIP redux-fetch package [`a8bed40`](https://github.com/AxaGuilDEv/react-oidc/commit/a8bed40cead711a707ae1dfdb5ca36e77a43935d) by Vincent Ricard -- WIP context-fetch package [`fd6f8f2`](https://github.com/AxaGuilDEv/react-oidc/commit/fd6f8f2872c12f6d4abdec36f0915524b64cab17) by Vincent Ricard -- docs(changelog) update to new 3.0.8 [`f28c0ed`](https://github.com/AxaGuilDEv/react-oidc/commit/f28c0ede0d3bba884085b1269b94eee3a223e50e) by Build-CI -- Revert a useless modification in fetch-core/package.json [`fa101b8`](https://github.com/AxaGuilDEv/react-oidc/commit/fa101b865ba386cdef4ebf1479ab357ce8e0288c) by Vincent Ricard - -#### [v3.0.8](https://github.com/AxaGuilDEv/react-oidc/compare/v3.0.7...v3.0.8) -> 14 February 2020 -- fix(options): options can be overrided [`#399`](https://github.com/AxaGuilDEv/react-oidc/pull/399) by Guillaume Chervet -- fix(fetchToken): options override [`#398`](https://github.com/AxaGuilDEv/react-oidc/pull/398) by Guillaume Chervet -- chore(release): publish v3.0.8 ***NO_CI*** [`ecccc47`](https://github.com/AxaGuilDEv/react-oidc/commit/ecccc47cb9ee65cccbb23aac53e215ced26d4914) by Build-CI -- docs(changelog) update to new 3.0.7 [`1c2e599`](https://github.com/AxaGuilDEv/react-oidc/commit/1c2e599cc9eeffc287b05b6c2671b270c28ad91e) by Build-CI - -#### [v3.0.7](https://github.com/AxaGuilDEv/react-oidc/compare/v3.0.6...v3.0.7) -> 7 February 2020 -- chore(deps-dev): bump concurrently from 4.1.2 to 5.1.0 [`#393`](https://github.com/AxaGuilDEv/react-oidc/pull/393) by dependabot-preview[bot] -- refactor(log): Replace console.log by oidcLog [`#386`](https://github.com/AxaGuilDEv/react-oidc/pull/386) by Cockedey Sébastien -- chore(release): publish v3.0.7 ***NO_CI*** [`9bdbb89`](https://github.com/AxaGuilDEv/react-oidc/commit/9bdbb8976b173d1fbc8da12e1a623aca61c39d6c) by Build-CI -- docs(changelog) update to new 3.0.6 [`46055a6`](https://github.com/AxaGuilDEv/react-oidc/commit/46055a654246bb678e745abb5b57717049850d82) by Build-CI - -#### [v3.0.6](https://github.com/AxaGuilDEv/react-oidc/compare/v3.0.6-alpha.0...v3.0.6) -> 7 January 2020 -- feat(components): add bem to default components [`#384`](https://github.com/AxaGuilDEv/react-oidc/pull/384) by Guillaume Chervet -- chore(release): publish v3.0.6 ***NO_CI*** [`db79cce`](https://github.com/AxaGuilDEv/react-oidc/commit/db79ccec24ddca89f7eb55a84a9684e409c8e548) by Build-CI -- docs(changelog) update to new 3.0.6-alpha.0 [`c55f2ed`](https://github.com/AxaGuilDEv/react-oidc/commit/c55f2ed5538e1dd4bd9f9f0ae0d25fdeb1421984) by Build-CI - -#### [v3.0.6-alpha.0](https://github.com/AxaGuilDEv/react-oidc/compare/v3.0.5-alpha.0...v3.0.6-alpha.0) -> 5 January 2020 -- fix(SessionLost): complete re-auth automaticaly first time display [`#373`](https://github.com/AxaGuilDEv/react-oidc/pull/373) by Guillaume Chervet -- chore(deps-dev): bump eslint-plugin-react-hooks from 1.7.0 to 2.3.0 [`#363`](https://github.com/AxaGuilDEv/react-oidc/pull/363) by dependabot-preview[bot] -- chore(deps-dev): bump redux from 4.0.4 to 4.0.5 [`#372`](https://github.com/AxaGuilDEv/react-oidc/pull/372) by dependabot-preview[bot] -- chore(deps-dev): bump @babel/cli from 7.7.4 to 7.7.7 [`#369`](https://github.com/AxaGuilDEv/react-oidc/pull/369) by dependabot-preview[bot] -- chore(deps-dev): bump oidc-client from 1.9.1 to 1.10.1 [`#366`](https://github.com/AxaGuilDEv/react-oidc/pull/366) by dependabot-preview[bot] -- chore(deps-dev): bump auto-changelog from 1.16.1 to 1.16.2 [`#367`](https://github.com/AxaGuilDEv/react-oidc/pull/367) by dependabot-preview[bot] -- chore(deps-dev): bump eslint-plugin-import from 2.18.2 to 2.19.1 [`#364`](https://github.com/AxaGuilDEv/react-oidc/pull/364) by dependabot-preview[bot] -- chore(deps-dev): bump @testing-library/jest-dom from 4.1.0 to 4.2.4 [`#368`](https://github.com/AxaGuilDEv/react-oidc/pull/368) by dependabot-preview[bot] -- chore(deps): bump @typescript-eslint/parser from 2.3.2 to 2.14.0 [`#371`](https://github.com/AxaGuilDEv/react-oidc/pull/371) by dependabot-preview[bot] -- chore(deps-dev): bump inquirer from 6.5.2 to 7.0.1 [`#365`](https://github.com/AxaGuilDEv/react-oidc/pull/365) by dependabot-preview[bot] -- chore(release): publish v3.0.6-alpha.0 ***NO_CI*** [`c871a60`](https://github.com/AxaGuilDEv/react-oidc/commit/c871a60adc3a6a8bbe816e5298e6056563e5b531) by Build-CI -- docs(changelog) update to new 3.0.5-alpha.0 [`96a24a9`](https://github.com/AxaGuilDEv/react-oidc/commit/96a24a95d62b1b829958c5769ac0617ccab18cca) by Build-CI - -#### [v3.0.5-alpha.0](https://github.com/AxaGuilDEv/react-oidc/compare/v3.0.4-alpha.0...v3.0.5-alpha.0) -> 3 January 2020 -- chore(deps-dev): bump @testing-library/react from 8.0.9 to 9.4.0 [`#358`](https://github.com/AxaGuilDEv/react-oidc/pull/358) by dependabot-preview[bot] -- chore(deps): [security] bump lodash.template from 4.4.0 to 4.5.0 [`#230`](https://github.com/AxaGuilDEv/react-oidc/pull/230) by dependabot-preview[bot] -- Can set the JWT in memory [`#354`](https://github.com/AxaGuilDEv/react-oidc/pull/354) by Olivier YOUF -- fix(all): F5 always re-auth and manage session lost [`#319`](https://github.com/AxaGuilDEv/react-oidc/pull/319) by Olivier YOUF -- chore(deps): bump enzyme-adapter-react-16 from 1.14.0 to 1.15.2 [`#359`](https://github.com/AxaGuilDEv/react-oidc/pull/359) by dependabot-preview[bot] -- chore(deps-dev): bump eslint-plugin-prettier from 3.1.1 to 3.1.2 [`#357`](https://github.com/AxaGuilDEv/react-oidc/pull/357) by dependabot-preview[bot] -- chore(deps-dev): bump redux-oidc from 3.1.5 to 3.1.7 [`#336`](https://github.com/AxaGuilDEv/react-oidc/pull/336) by dependabot-preview[bot] -- chore(deps-dev): bump typescript from 3.6.3 to 3.7.4 [`#361`](https://github.com/AxaGuilDEv/react-oidc/pull/361) by dependabot-preview[bot] -- chore(deps-dev): bump @babel/preset-react from 7.0.0 to 7.7.4 [`#347`](https://github.com/AxaGuilDEv/react-oidc/pull/347) by dependabot-preview[bot] -- chore(deps): [security] bump https-proxy-agent from 2.2.2 to 2.2.4 [`#345`](https://github.com/AxaGuilDEv/react-oidc/pull/345) by dependabot-preview[bot] -- chore(deps-dev): bump lint-staged from 8.2.1 to 9.5.0 [`#348`](https://github.com/AxaGuilDEv/react-oidc/pull/348) by dependabot-preview[bot] -- chore(deps-dev): bump react-redux from 5.1.1 to 7.1.3 [`#334`](https://github.com/AxaGuilDEv/react-oidc/pull/334) by dependabot-preview[bot] -- chore(deps-dev): bump eslint-plugin-jest from 22.17.0 to 23.2.0 [`#362`](https://github.com/AxaGuilDEv/react-oidc/pull/362) by dependabot-preview[bot] -- feat(context): Add events in useReactOidc hook [`#343`](https://github.com/AxaGuilDEv/react-oidc/pull/343) by Olivier YOUF -- chore(release): publish v3.0.5-alpha.0 ***NO_CI*** [`6fa041a`](https://github.com/AxaGuilDEv/react-oidc/commit/6fa041af0e1f1fcb9216fd6ed7a287e7fdc73ed6) by Build-CI -- fix(sessionlost) typo [`dd69fe6`](https://github.com/AxaGuilDEv/react-oidc/commit/dd69fe67acda1d6f2b7b1933cafa67f3c6133839) by guillaume chervet -- fix(sessionlost) typo [`2d8427f`](https://github.com/AxaGuilDEv/react-oidc/commit/2d8427ffd81f39dc65f9ecf26bc65b4244013607) by guillaume chervet -- refactor(auth) refresh branch and clean things [`59dedc1`](https://github.com/AxaGuilDEv/react-oidc/commit/59dedc11c7fc10a246ff8c915fa0583be981ab52) by guillaume chervet -- docs(changelog) update to new 3.0.4-alpha.0 [`b201e35`](https://github.com/AxaGuilDEv/react-oidc/commit/b201e3563ff1caf9bf940dde2210ef2bfc39ace0) by Build-CI - -#### [v3.0.4-alpha.0](https://github.com/AxaGuilDEv/react-oidc/compare/v3.0.3-alpha.0...v3.0.4-alpha.0) -> 18 November 2019 -- fix(context): avoid errors on console with custom callback [`#341`](https://github.com/AxaGuilDEv/react-oidc/pull/341) by Olivier YOUF -- fix(context): Fix silent crash [`#340`](https://github.com/AxaGuilDEv/react-oidc/pull/340) by Olivier YOUF -- doc(oidc-metadata): add document about metadata [`#330`](https://github.com/AxaGuilDEv/react-oidc/pull/330) by Guillaume Chervet -- chore(release): publish v3.0.4-alpha.0 ***NO_CI*** [`f8d7557`](https://github.com/AxaGuilDEv/react-oidc/commit/f8d755783d2b4cfbee0cdaabe6f27178d3200080) by Build-CI -- docs(changelog) update to new 3.0.3-alpha.0 [`6b0fc09`](https://github.com/AxaGuilDEv/react-oidc/commit/6b0fc0961c90ec3200e3995482fe748d1fbe5e1d) by Build-CI - -#### [v3.0.3-alpha.0](https://github.com/AxaGuilDEv/react-oidc/compare/v3.0.2-alpha.0...v3.0.3-alpha.0) -> 5 November 2019 -- fix(redux) oidcSecure hoc was not working [`#331`](https://github.com/AxaGuilDEv/react-oidc/pull/331) by Guillaume Chervet -- wip [`e5d41f8`](https://github.com/AxaGuilDEv/react-oidc/commit/e5d41f82a1b7d7e1608e84aad4482a5f4ac5fefc) by guillaume chervet -- wip [`d03101d`](https://github.com/AxaGuilDEv/react-oidc/commit/d03101d730d6cbecc3a4ee191bfde97137060cdf) by guillaume chervet -- chore(release): publish v3.0.3-alpha.0 ***NO_CI*** [`9b8de3b`](https://github.com/AxaGuilDEv/react-oidc/commit/9b8de3bf60b0d5d58caf12513be1bd742d2936df) by Build-CI -- maj [`b2d63e8`](https://github.com/AxaGuilDEv/react-oidc/commit/b2d63e8c92e0526abe1ba242759c1716f56bdac6) by guillaume chervet -- wip [`5613367`](https://github.com/AxaGuilDEv/react-oidc/commit/5613367e95de9aa5399b9ccdacd268e5d0ef4516) by guillaume chervet -- docs(changelog) update to new 3.0.2-alpha.0 [`5f22a9f`](https://github.com/AxaGuilDEv/react-oidc/commit/5f22a9fe09da0b803abd0c7698638acdfe6620f9) by Build-CI - -#### [v3.0.2-alpha.0](https://github.com/AxaGuilDEv/react-oidc/compare/v3.0.1-alpha.0...v3.0.2-alpha.0) -> 15 October 2019 -- Fix component prop type validation to use elementType [`#321`](https://github.com/AxaGuilDEv/react-oidc/pull/321) by Henri Koskenranta -- fix(router) : Add some polyfills for IE [`#314`](https://github.com/AxaGuilDEv/react-oidc/pull/314) by Olivier YOUF -- chore(mergify) add sonar rules [`#313`](https://github.com/AxaGuilDEv/react-oidc/pull/313) by Guillaume Chervet -- refactor(context) : remove callback OR in core [`#310`](https://github.com/AxaGuilDEv/react-oidc/pull/310) by Olivier YOUF -- chore(deps-dev): bump cross-env from 5.2.0 to 6.0.0 [`#306`](https://github.com/AxaGuilDEv/react-oidc/pull/306) by dependabot-preview[bot] -- chore(deps-dev): bump redux from 4.0.1 to 4.0.4 [`#243`](https://github.com/AxaGuilDEv/react-oidc/pull/243) by dependabot-preview[bot] -- fix(all) F5 always re-auth and manage session lost [`ee0c13f`](https://github.com/AxaGuilDEv/react-oidc/commit/ee0c13f437244fad58fa352da9a1a35f8e3eb3f7) by guillaume chervet -- wip [`b2a7185`](https://github.com/AxaGuilDEv/react-oidc/commit/b2a71851d0cb91653e1d77e2fe037aa2114e9037) by guillaume chervet -- chore(release): publish v3.0.2-alpha.0 ***NO_CI*** [`1e74a2c`](https://github.com/AxaGuilDEv/react-oidc/commit/1e74a2cdf751debd3e5fe6bb84c55d0850b27ba8) by Build-CI -- docs(changelog) update to new 3.0.1-alpha.0 [`75b21af`](https://github.com/AxaGuilDEv/react-oidc/commit/75b21af75f890f3b08c46847dcb7ab7fd52df1ef) by Build-CI -- wip [`e20c897`](https://github.com/AxaGuilDEv/react-oidc/commit/e20c8975f80f71ac7a6b4eaebf44025744359d93) by guillaume chervet - -#### [v3.0.1-alpha.0](https://github.com/AxaGuilDEv/react-oidc/compare/v3.0.0-alpha.0...v3.0.1-alpha.0) -> 25 September 2019 -- feat(all) Add callback component [`#303`](https://github.com/AxaGuilDEv/react-oidc/pull/303) by Madebymaurice -- doc(package) add better keword and update licence [`#304`](https://github.com/AxaGuilDEv/react-oidc/pull/304) by Guillaume Chervet -- docs: fixed a typo on AuthenticationProvider [`#295`](https://github.com/AxaGuilDEv/react-oidc/pull/295) by Jean-Lou Piermé -- fix(sample:redux) typo to get the condition working [`#292`](https://github.com/AxaGuilDEv/react-oidc/pull/292) by Markus Lasermann -- chore(release): publish v3.0.1-alpha.0 ***NO_CI*** [`3a6de2e`](https://github.com/AxaGuilDEv/react-oidc/commit/3a6de2ef2af3a7a51ee191a0c003907ba7fbcd7e) by Build-CI -- docs(changelog) update to new 3.0.0-alpha.0 [`1953c47`](https://github.com/AxaGuilDEv/react-oidc/commit/1953c479773b6181a3e0acf308e63bdc4d12cf8b) by Build-CI - -#### [v3.0.0-alpha.0](https://github.com/AxaGuilDEv/react-oidc/compare/v2.0.8...v3.0.0-alpha.0) -> 2 September 2019 -- feat(router) Agnostic router [`#290`](https://github.com/AxaGuilDEv/react-oidc/pull/290) by Arnaud Foraison -- fix(licence)set correct entity name [`#285`](https://github.com/AxaGuilDEv/react-oidc/pull/285) by Guillaume Chervet -- chore(release): publish v3.0.0-alpha.0 ***NO_CI*** [`6192d98`](https://github.com/AxaGuilDEv/react-oidc/commit/6192d98c63cd30ee2b69561bd78c4c313353dff7) by Build-CI -- docs(changelog) update to new 2.0.8 [`db610e3`](https://github.com/AxaGuilDEv/react-oidc/commit/db610e36a5874fc05ee3130eb116223b8122aa29) by Build-CI - -#### [v2.0.8](https://github.com/AxaGuilDEv/react-oidc/compare/v2.0.7...v2.0.8) -> 20 August 2019 -- fix(redux) F5 always re-auth [`#278`](https://github.com/AxaGuilDEv/react-oidc/pull/278) by Guillaume Chervet -- fix(redux) sample react route problem [`#277`](https://github.com/AxaGuilDEv/react-oidc/pull/277) by Guillaume Chervet -- chore(release): publish v2.0.8 ***NO_CI*** [`ebe2d7d`](https://github.com/AxaGuilDEv/react-oidc/commit/ebe2d7de2a76557b0bd95110afdf7b63192ead46) by Build-CI -- docs(changelog) update to new 2.0.7 [`5264289`](https://github.com/AxaGuilDEv/react-oidc/commit/5264289a48a6c0fd863757cf57b3e4bec89a6fe1) by Build-CI - -#### [v2.0.7](https://github.com/AxaGuilDEv/react-oidc/compare/v2.0.6...v2.0.7) -> 13 August 2019 -- fix(redux) workflow [`#271`](https://github.com/AxaGuilDEv/react-oidc/pull/271) by Guillaume Chervet -- chore(release): publish v2.0.7 ***NO_CI*** [`a45f630`](https://github.com/AxaGuilDEv/react-oidc/commit/a45f6300cac5f37efc9fdbdf76f7c97107a49b2f) by Build-CI -- docs(changelog) update to new 2.0.6 [`356afab`](https://github.com/AxaGuilDEv/react-oidc/commit/356afab343cd06f9535feb3a64fdf9675a84b8f2) by Build-CI - -#### [v2.0.6](https://github.com/AxaGuilDEv/react-oidc/compare/v2.0.5...v2.0.6) -> 9 August 2019 -- fix(redux) workflow [`#266`](https://github.com/AxaGuilDEv/react-oidc/pull/266) by Guillaume Chervet -- chore(release): publish v2.0.6 ***NO_CI*** [`a55b16e`](https://github.com/AxaGuilDEv/react-oidc/commit/a55b16edadb67903e51db9e359d7f260107c4a39) by Build-CI -- docs(changelog) update to new 2.0.5 [`44cc13b`](https://github.com/AxaGuilDEv/react-oidc/commit/44cc13b9b33de9184fd87cb3beae48aea33837a7) by Build-CI - -#### [v2.0.5](https://github.com/AxaGuilDEv/react-oidc/compare/v2.0.4...v2.0.5) -> 5 August 2019 -- fix(redux) incorrect properties [`#262`](https://github.com/AxaGuilDEv/react-oidc/pull/262) by Guillaume Chervet -- chore(release): publish v2.0.5 ***NO_CI*** [`fff2d20`](https://github.com/AxaGuilDEv/react-oidc/commit/fff2d20ae08b5a0f5af84452db2376280df70f55) by Build-CI -- docs(changelog) update to new 2.0.4 [`a43242b`](https://github.com/AxaGuilDEv/react-oidc/commit/a43242b6cc33d096e9c85303f8d63c8d6be7ad07) by Build-CI - -#### [v2.0.4](https://github.com/AxaGuilDEv/react-oidc/compare/v2.0.3...v2.0.4) -> 5 August 2019 -- fix(redux) upgrade react router version [`#261`](https://github.com/AxaGuilDEv/react-oidc/pull/261) by Guillaume Chervet -- chore(release): publish v2.0.4 ***NO_CI*** [`2b63d3e`](https://github.com/AxaGuilDEv/react-oidc/commit/2b63d3ee4a925b045c1a711b2a92cace4448d3b0) by Build-CI -- docs(changelog) update to new 2.0.3 [`dba7b1d`](https://github.com/AxaGuilDEv/react-oidc/commit/dba7b1d899ab58c7351ffe065e0ad4b01fb3ef21) by Build-CI - -#### [v2.0.3](https://github.com/AxaGuilDEv/react-oidc/compare/v2.0.2...v2.0.3) -> 2 August 2019 -- fix(redux) token renew [`#260`](https://github.com/AxaGuilDEv/react-oidc/pull/260) by Guillaume Chervet -- fix(redux) token renew [`#260`](https://github.com/AxaGuilDEv/react-oidc/pull/260) by Guillaume Chervet -- chore(build) add and regenerate package.lock [`ef392f1`](https://github.com/AxaGuilDEv/react-oidc/commit/ef392f1c2294a150444dc370531ba17f46d30701) by guillaume chervet -- chore(package) fix package version [`0a2a2bf`](https://github.com/AxaGuilDEv/react-oidc/commit/0a2a2bf84740fac0586cdd137ae5cf47102957ac) by guillaume chervet -- chore(release): publish v2.0.3 ***NO_CI*** [`bcac2f4`](https://github.com/AxaGuilDEv/react-oidc/commit/bcac2f4e37e6bc280fed9e6690224d552f8ee229) by Build-CI -- docs(changelog) update to new 2.0.2 [`ff075a4`](https://github.com/AxaGuilDEv/react-oidc/commit/ff075a4b400a074d376a4c07f4a751e6914f6cb4) by Build-CI - -#### [v2.0.2](https://github.com/AxaGuilDEv/react-oidc/compare/v2.0.2-alpha.0...v2.0.2) -> 30 July 2019 -- fix(redux) token renew [`#258`](https://github.com/AxaGuilDEv/react-oidc/pull/258) by Guillaume Chervet -- chore(release): publish v2.0.2 ***NO_CI*** [`8e50a29`](https://github.com/AxaGuilDEv/react-oidc/commit/8e50a29d6302b3c0487dd5102fe115458760705a) by Build-CI -- docs(changelog) update to new 2.0.2-alpha.0 [`df80867`](https://github.com/AxaGuilDEv/react-oidc/commit/df808675ee9d524f15d2c1f493d8cfe5a36d926e) by Build-CI - -#### [v2.0.2-alpha.0](https://github.com/AxaGuilDEv/react-oidc/compare/v2.0.1-alpha.0...v2.0.2-alpha.0) -> 25 July 2019 -- doc : add optionnal parameter for post logout uri redirect [`#253`](https://github.com/AxaGuilDEv/react-oidc/pull/253) by Olivier YOUF -- chore(release): publish v2.0.2-alpha.0 ***NO_CI*** [`3c1230a`](https://github.com/AxaGuilDEv/react-oidc/commit/3c1230ab6b950a100ba58190f4f9781ed06af82f) by Build-CI -- docs(changelog) update to new 2.0.1-alpha.0 [`c7c32a2`](https://github.com/AxaGuilDEv/react-oidc/commit/c7c32a21992e8f907671788f4d49a3af7a76fcd6) by Build-CI - -#### [v2.0.1-alpha.0](https://github.com/AxaGuilDEv/react-oidc/compare/v2.0.0-alpha.0...v2.0.1-alpha.0) -> 15 July 2019 -- Fix/infiniteloop [`#238`](https://github.com/AxaGuilDEv/react-oidc/pull/238) by Olivier YOUF -- fix(infiniteLoop) : package upgrade [`e98e7d6`](https://github.com/AxaGuilDEv/react-oidc/commit/e98e7d6febdc820dc66122db43d443a8eb3d7d26) by Olivier YOUF -- fix(infiniteLoop) : change hooks to avoid infinite loop [`2168254`](https://github.com/AxaGuilDEv/react-oidc/commit/21682540fdf9a71e178942ee31b711a1f2c1575d) by Olivier YOUF -- chore(release): publish v2.0.1-alpha.0 ***NO_CI*** [`d906ddf`](https://github.com/AxaGuilDEv/react-oidc/commit/d906ddf3b2d269a362604e0a1418699bd4d10371) by Build-CI -- docs(changelog) update to new 2.0.0-alpha.0 [`2b07917`](https://github.com/AxaGuilDEv/react-oidc/commit/2b0791737eaf7c42b96ba77900d1bf10aa6a1e09) by Build-CI - -#### [v2.0.0-alpha.0](https://github.com/AxaGuilDEv/react-oidc/compare/v1.3.3...v2.0.0-alpha.0) -> 11 July 2019 -- fix(packages) : rebuild packages for package lock [`#229`](https://github.com/AxaGuilDEv/react-oidc/pull/229) by Olivier YOUF -- Feature/hooks [`#225`](https://github.com/AxaGuilDEv/react-oidc/pull/225) by Olivier YOUF -- fix(packages) : resolves missing scripts [`043a66e`](https://github.com/AxaGuilDEv/react-oidc/commit/043a66eee2f3862e1a0ab5b4155fd2475206fa35) by Olivier YOUF -- upgrading React Context + Hooks [`dafe353`](https://github.com/AxaGuilDEv/react-oidc/commit/dafe353e93456cbb2d4feba5f20cbc563029e50c) by Olivier YOUF -- babel 7 migration [`d4a8b09`](https://github.com/AxaGuilDEv/react-oidc/commit/d4a8b099fb5f1cf47a57f418b93f12f9afd04efb) by Olivier YOUF -- refacto : Package managament & dependency [`d8e8d39`](https://github.com/AxaGuilDEv/react-oidc/commit/d8e8d394cfcad14bc2bec0b58a13d440aa9a6702) by Olivier YOUF -- fix(lint) : fix lint return and deps [`c275ea3`](https://github.com/AxaGuilDEv/react-oidc/commit/c275ea3daaff745b691f083788c0155f6c89bb3c) by Olivier YOUF -- fix : fix isEnabled flag [`fe5779c`](https://github.com/AxaGuilDEv/react-oidc/commit/fe5779c5d47986223cafab81ca09bfb3f5b7a83b) by Olivier YOUF -- PR remarks [`8b6ed4d`](https://github.com/AxaGuilDEv/react-oidc/commit/8b6ed4d3e3a1a0600d9d7011a551c07c500f82cf) by Olivier YOUF -- feat : new hooks function to get oidc props [`e7c4530`](https://github.com/AxaGuilDEv/react-oidc/commit/e7c45303167c37b5322955f1274e6cf90bd698cf) by Olivier YOUF -- chore(release): publish v2.0.0-alpha.0 ***NO_CI*** [`06e5a77`](https://github.com/AxaGuilDEv/react-oidc/commit/06e5a7743225a652142defc6dd658fa9c45f8d26) by Build-CI -- refacto(packags) : bump babel [`fcf3169`](https://github.com/AxaGuilDEv/react-oidc/commit/fcf31694c9235da8c9d0b6d5d11f226efb65af43) by Olivier YOUF -- docs(changelog) update to new 1.3.3 [`eea8716`](https://github.com/AxaGuilDEv/react-oidc/commit/eea8716a1b2e8ac098c0f388682f1e1f166f22ca) by Build-CI -- override conf for example [`c6342f5`](https://github.com/AxaGuilDEv/react-oidc/commit/c6342f57d2952676d96b0990099c2741c63ac736) by Olivier YOUF - -#### [v1.3.3](https://github.com/AxaGuilDEv/react-oidc/compare/v1.3.3-alpha.0...v1.3.3) -> 4 April 2019 -- chore(release): publish v1.3.3 ***NO_CI*** [`3e011c2`](https://github.com/AxaGuilDEv/react-oidc/commit/3e011c2ca20c48a0b3e308c8e42ba96fd30b4ef8) by Build-CI -- docs(changelog) update to new 1.3.3-alpha.0 [`e8b9de0`](https://github.com/AxaGuilDEv/react-oidc/commit/e8b9de0754c9c1df227f70912fd5b5acf67a4134) by Build-CI - -#### [v1.3.3-alpha.0](https://github.com/AxaGuilDEv/react-oidc/compare/v1.3.2-alpha.0...v1.3.3-alpha.0) -> 1 April 2019 -- Fetch Core : Fix for header props [`#182`](https://github.com/AxaGuilDEv/react-oidc/pull/182) by Olivier YOUF -- chore(release): publish v1.3.3-alpha.0 ***NO_CI*** [`1d9d9e3`](https://github.com/AxaGuilDEv/react-oidc/commit/1d9d9e3ecf855f56e961743d04533c1408bbbeb5) by Build-CI -- docs(changelog) update to new 1.3.2-alpha.0 [`100e4be`](https://github.com/AxaGuilDEv/react-oidc/commit/100e4be4321359fdb628bf52c93adae612576564) by Build-CI -- fix headers [`01dd579`](https://github.com/AxaGuilDEv/react-oidc/commit/01dd579c63401c99b097726b73658bfa84db7767) by Olivier YOUF - -#### [v1.3.2-alpha.0](https://github.com/AxaGuilDEv/react-oidc/compare/v1.3.1-alpha.0...v1.3.2-alpha.0) -> 27 March 2019 -- Few changes for multiple Auth requests and callbacks issue [`#180`](https://github.com/AxaGuilDEv/react-oidc/pull/180) by Olivier YOUF -- fix callback double tap [`35f5609`](https://github.com/AxaGuilDEv/react-oidc/commit/35f56093d6e0aedca493aabde779bef58f49d59d) by Olivier YOUF -- fix : flag for waiting auth [`89b32c0`](https://github.com/AxaGuilDEv/react-oidc/commit/89b32c0a411957d1e514602d72b52ddb25b5de97) by Olivier YOUF -- Fix : package changes [`b8692f2`](https://github.com/AxaGuilDEv/react-oidc/commit/b8692f29f4fae45fb64e74d1d89fa7cf10f2fc05) by Olivier YOUF -- chore(release): publish v1.3.2-alpha.0 ***NO_CI*** [`d814ada`](https://github.com/AxaGuilDEv/react-oidc/commit/d814ada43b9e826d3a5aa9317e4b8f7dcff0671f) by Build-CI -- wip : callback [`01af436`](https://github.com/AxaGuilDEv/react-oidc/commit/01af43636e2fc5cb99a08aa154b415857493956b) by Olivier YOUF -- docs(changelog) update to new 1.3.1-alpha.0 [`17c8683`](https://github.com/AxaGuilDEv/react-oidc/commit/17c86839884e63cf8c05459839f869f0ef55e019) by Build-CI - -#### [v1.3.1-alpha.0](https://github.com/AxaGuilDEv/react-oidc/compare/v1.3.0...v1.3.1-alpha.0) -> 14 March 2019 -- Fix(context) component should wait user loading [`#170`](https://github.com/AxaGuilDEv/react-oidc/pull/170) by Guillaume Chervet -- fix(tweet) set up good changelog link [`#169`](https://github.com/AxaGuilDEv/react-oidc/pull/169) by Guillaume Chervet -- docs(changelog) update to new 1.3.0 [`02de519`](https://github.com/AxaGuilDEv/react-oidc/commit/02de5196b3e7907acfeb631ee27fdea7ed15968a) by Build-CI -- chore(release): publish v1.3.1-alpha.0 ***NO_CI*** [`32d880f`](https://github.com/AxaGuilDEv/react-oidc/commit/32d880fb06a6b916d87e90d8f4c6b1d338969d06) by Build-CI - -#### [v1.3.0](https://github.com/AxaGuilDEv/react-oidc/compare/v1.2.1...v1.3.0) -> 12 March 2019 -- fix(context) no state found in storage [`#168`](https://github.com/AxaGuilDEv/react-oidc/pull/168) by Guillaume Chervet -- docs(changelog) update to new 1.2.1 [`4eed1e9`](https://github.com/AxaGuilDEv/react-oidc/commit/4eed1e91ad3ed5dce05827444334a598f747844c) by Guillaume Chervet -- chore(release): publish v1.3.0 ***NO_CI*** [`517804b`](https://github.com/AxaGuilDEv/react-oidc/commit/517804b569c31e2e6a43f32f0aac2ff960291988) by Build-CI - -#### [v1.2.1](https://github.com/AxaGuilDEv/react-oidc/compare/v1.2.0...v1.2.1) -> 8 March 2019 -- feature(context/routes) allow to configure callbacks [`#164`](https://github.com/AxaGuilDEv/react-oidc/pull/164) by Guillaume Chervet -- feature(vanilla) vanilla lib usefull for demo or debug or migration of old js application [`#160`](https://github.com/AxaGuilDEv/react-oidc/pull/160) by Guillaume Chervet -- chore(changelog) set up auto changelog and auto tweet [`#153`](https://github.com/AxaGuilDEv/react-oidc/pull/153) by Guillaume Chervet -- chore(release): publish v1.2.1 ***NO_CI*** [`fbb0339`](https://github.com/AxaGuilDEv/react-oidc/commit/fbb0339dad91dda54d2828c7d5a4ffa5cd071a6e) by Build-CI - -#### [v1.2.0](https://github.com/AxaGuilDEv/react-oidc/compare/v1.1.6...v1.2.0) -> 24 February 2019 -- English - change all instances of "authentified" to be "authenticated" [`#151`](https://github.com/AxaGuilDEv/react-oidc/pull/151) by Paul Hammond -- chore(release): publish v1.2.0 ***NO_CI*** [`5f491b7`](https://github.com/AxaGuilDEv/react-oidc/commit/5f491b75962664904720b3404ce80a27ed9967a6) by Build-CI - -#### [v1.1.6](https://github.com/AxaGuilDEv/react-oidc/compare/v1.1.5...v1.1.6) -> 24 February 2019 -- Customise authenticating component [context] [`#150`](https://github.com/AxaGuilDEv/react-oidc/pull/150) by Paul Hammond -- lock down version of jest [`#149`](https://github.com/AxaGuilDEv/react-oidc/pull/149) by Paul Hammond -- chore(deps-dev): bump enzyme from 3.7.0 to 3.8.0 [`#112`](https://github.com/AxaGuilDEv/react-oidc/pull/112) by dependabot[bot] -- chore(deps-dev): bump eslint-plugin-jsx-a11y from 6.1.2 to 6.2.0 [`#113`](https://github.com/AxaGuilDEv/react-oidc/pull/113) by dependabot[bot] -- chore(deps-dev): bump codacy-coverage from 3.3.0 to 3.4.0 [`#116`](https://github.com/AxaGuilDEv/react-oidc/pull/116) by dependabot[bot] -- chore(deps-dev): bump eslint-plugin-import from 2.14.0 to 2.16.0 [`#117`](https://github.com/AxaGuilDEv/react-oidc/pull/117) by dependabot[bot] -- chore(deps-dev): bump prettier from 1.16.1 to 1.16.4 [`#122`](https://github.com/AxaGuilDEv/react-oidc/pull/122) by dependabot[bot] -- chore(deps-dev): bump eslint-plugin-jest from 22.1.2 to 22.2.2 [`#126`](https://github.com/AxaGuilDEv/react-oidc/pull/126) by dependabot[bot] -- feat : adding enable prop in Provider [`#125`](https://github.com/AxaGuilDEv/react-oidc/pull/125) by Olivier YOUF -- chore(release): publish v1.1.6 ***NO_CI*** [`6f573cd`](https://github.com/AxaGuilDEv/react-oidc/commit/6f573cd08e1e39143c661553645fdc62adebd3d8) by Build-CI - -#### [v1.1.5](https://github.com/AxaGuilDEv/react-oidc/compare/v1.1.5-alpha.0...v1.1.5) -> 1 February 2019 -- chore(deps-dev): bump eslint-config-prettier from 3.3.0 to 4.0.0 [`#111`](https://github.com/AxaGuilDEv/react-oidc/pull/111) by dependabot[bot] -- chore(deps-dev): bump prettier from 1.16.0 to 1.16.1 [`#110`](https://github.com/AxaGuilDEv/react-oidc/pull/110) by dependabot[bot] -- chore(deps-dev): bump jest from 23.6.0 to 24.0.0 [`#118`](https://github.com/AxaGuilDEv/react-oidc/pull/118) by dependabot[bot] -- Fix: Add react-router to dependencies [`#120`](https://github.com/AxaGuilDEv/react-oidc/pull/120) by Hamza Hamidi -- chore: remove react-redux dependency [`#119`](https://github.com/AxaGuilDEv/react-oidc/pull/119) by rpetigny -- chore(deps-dev): bump expect from 23.6.0 to 24.0.0 [`#106`](https://github.com/AxaGuilDEv/react-oidc/pull/106) by dependabot[bot] -- chore(deps-dev): bump lerna from 3.5.0 to 3.10.7 [`#107`](https://github.com/AxaGuilDEv/react-oidc/pull/107) by dependabot[bot] -- chore(deps-dev): bump eslint-plugin-jest from 22.1.2 to 22.1.3 [`#108`](https://github.com/AxaGuilDEv/react-oidc/pull/108) by dependabot[bot] -- chore(deps-dev): bump enzyme-adapter-react-16 from 1.7.1 to 1.8.0 [`#109`](https://github.com/AxaGuilDEv/react-oidc/pull/109) by dependabot[bot] -- chore(deps): bump react-dom from 16.6.3 to 16.7.0 [`#95`](https://github.com/AxaGuilDEv/react-oidc/pull/95) by dependabot[bot] -- chore(deps): bump oidc-client from 1.5.4 to 1.6.1 [`#96`](https://github.com/AxaGuilDEv/react-oidc/pull/96) by dependabot[bot] -- chore(deps): bump react from 16.6.3 to 16.7.0 [`#97`](https://github.com/AxaGuilDEv/react-oidc/pull/97) by dependabot[bot] -- chore(deps-dev): bump tslint from 5.11.0 to 5.12.1 [`#99`](https://github.com/AxaGuilDEv/react-oidc/pull/99) by dependabot[bot] -- chore(deps-dev): bump eslint from 5.11.1 to 5.12.1 [`#100`](https://github.com/AxaGuilDEv/react-oidc/pull/100) by dependabot[bot] -- chore(deps-dev): bump prettier from 1.15.3 to 1.16.0 [`#101`](https://github.com/AxaGuilDEv/react-oidc/pull/101) by dependabot[bot] -- chore(deps-dev): bump babel-plugin-macros from 2.4.4 to 2.4.5 [`#93`](https://github.com/AxaGuilDEv/react-oidc/pull/93) by dependabot[bot] -- chore(deps-dev): bump typescript from 3.2.2 to 3.2.4 [`#102`](https://github.com/AxaGuilDEv/react-oidc/pull/102) by dependabot[bot] -- chore(deps-dev): bump eslint-plugin-react from 7.12.3 to 7.12.4 [`#103`](https://github.com/AxaGuilDEv/react-oidc/pull/103) by dependabot[bot] -- chore(deps-dev): bump eslint-plugin-prettier from 3.0.0 to 3.0.1 [`#88`](https://github.com/AxaGuilDEv/react-oidc/pull/88) by dependabot[bot] -- chore(deps-dev): bump react-scripts from 2.1.1 to 2.1.3 [`#90`](https://github.com/AxaGuilDEv/react-oidc/pull/90) by dependabot[bot] -- chore(deps-dev): bump babel-plugin-macros from 2.4.2 to 2.4.4 [`#85`](https://github.com/AxaGuilDEv/react-oidc/pull/85) by mergify[bot] -- chore(deps-dev): bump chalk from 2.4.1 to 2.4.2 [`#86`](https://github.com/AxaGuilDEv/react-oidc/pull/86) by mergify[bot] -- chore(deps-dev): bump eslint-plugin-react from 7.11.1 to 7.12.3 [`#87`](https://github.com/AxaGuilDEv/react-oidc/pull/87) by mergify[bot] -- Fix example link [`#89`](https://github.com/AxaGuilDEv/react-oidc/pull/89) by Seth -- fix(redux) oidc default props [`#84`](https://github.com/AxaGuilDEv/react-oidc/pull/84) by julienbirgand -- chore(release): publish v1.1.5 ***NO_CI*** [`e21bbbc`](https://github.com/AxaGuilDEv/react-oidc/commit/e21bbbcadfec1fe70cc64d0b33c84910a9e8add1) by Build-CI - -#### [v1.1.5-alpha.0](https://github.com/AxaGuilDEv/react-oidc/compare/v1.1.0...v1.1.5-alpha.0) -> 4 January 2019 -- chore(pipeline-azure) branch variable was a bad one [`#83`](https://github.com/AxaGuilDEv/react-oidc/pull/83) by mergify[bot] -- fix : Context Fetch>oidcUser transmission [`#80`](https://github.com/AxaGuilDEv/react-oidc/pull/80) by Olivier YOUF -- fix token expired event [`#81`](https://github.com/AxaGuilDEv/react-oidc/pull/81) by mergify[bot] -- chore(deps-dev): bump eslint from 5.9.0 to 5.11.1 [`#82`](https://github.com/AxaGuilDEv/react-oidc/pull/82) by mergify[bot] -- chore(deps-dev): bump @angular/compiler from 7.1.0 to 7.1.4 [`#75`](https://github.com/AxaGuilDEv/react-oidc/pull/75) by mergify[bot] -- chore(deps-dev): bump typescript from 3.2.1 to 3.2.2 [`#63`](https://github.com/AxaGuilDEv/react-oidc/pull/63) by dependabot[bot] -- chore(deps-dev): bump @angular/core from 7.1.0 to 7.1.3 [`#65`](https://github.com/AxaGuilDEv/react-oidc/pull/65) by mergify[bot] -- chore(deps-dev): bump tslint-config-prettier from 1.16.0 to 1.17.0 [`#64`](https://github.com/AxaGuilDEv/react-oidc/pull/64) by mergify[bot] -- chore(deps-dev): bump regenerator-runtime from 0.12.1 to 0.13.1 [`#67`](https://github.com/AxaGuilDEv/react-oidc/pull/67) by mergify[bot] -- chore(deps-dev): bump babel-eslint from 8.2.6 to 10.0.1 [`#69`](https://github.com/AxaGuilDEv/react-oidc/pull/69) by mergify[bot] -- chore(deps-dev): bump inquirer from 6.2.0 to 6.2.1 [`#68`](https://github.com/AxaGuilDEv/react-oidc/pull/68) by mergify[bot] -- chore(deps-dev): bump react-test-renderer from 16.6.3 to 16.7.0 [`#70`](https://github.com/AxaGuilDEv/react-oidc/pull/70) by mergify[bot] -- chore(deps-dev): bump enzyme-adapter-react-16 from 1.7.0 to 1.7.1 [`#71`](https://github.com/AxaGuilDEv/react-oidc/pull/71) by mergify[bot] -- chore(deps-dev): bump eslint-plugin-jest from 22.0.1 to 22.1.2 [`#76`](https://github.com/AxaGuilDEv/react-oidc/pull/76) by mergify[bot] -- docs(readme) add keyword [`#79`](https://github.com/AxaGuilDEv/react-oidc/pull/79) by Guillaume Chervet -- chore(ci) remove travis and codeclimate [`#73`](https://github.com/AxaGuilDEv/react-oidc/pull/73) by Guillaume Chervet -- chore(deps-dev): bump lint-staged from 7.3.0 to 8.1.0 [`#74`](https://github.com/AxaGuilDEv/react-oidc/pull/74) by dependabot[bot] -- chore(ci) add azureDevops build + sonar [`#72`](https://github.com/AxaGuilDEv/react-oidc/pull/72) by Guillaume Chervet -- chore(deps-dev): bump react-scripts from 1.1.5 to 2.1.1 [`#57`](https://github.com/AxaGuilDEv/react-oidc/pull/57) by dependabot[bot] -- chore(deps-dev): bump typescript from 3.1.6 to 3.2.1 [`#59`](https://github.com/AxaGuilDEv/react-oidc/pull/59) by dependabot[bot] -- chore(deps-dev): bump react-test-renderer from 16.6.1 to 16.6.3 [`#60`](https://github.com/AxaGuilDEv/react-oidc/pull/60) by dependabot[bot] -- chore(deps-dev): bump prettier from 1.15.1 to 1.15.3 [`#61`](https://github.com/AxaGuilDEv/react-oidc/pull/61) by dependabot[bot] -- doc(readme) small fix for demo to internship [`#62`](https://github.com/AxaGuilDEv/react-oidc/pull/62) by Guillaume Chervet -- chore(azurepipeline) configure lerna to publish [`#58`](https://github.com/AxaGuilDEv/react-oidc/pull/58) by Guillaume Chervet -- chore(deps-dev): bump lerna from 3.4.3 to 3.5.0 [`#55`](https://github.com/AxaGuilDEv/react-oidc/pull/55) by dependabot[bot] -- chore(deps-dev): bump eslint-plugin-json from 1.2.1 to 1.3.2 [`#54`](https://github.com/AxaGuilDEv/react-oidc/pull/54) by dependabot[bot] -- chore(deps-dev): bump eslint-plugin-prettier from 2.7.0 to 3.0.0 [`#56`](https://github.com/AxaGuilDEv/react-oidc/pull/56) by dependabot[bot] -- chore(deps-dev): bump @angular/compiler from 7.0.4 to 7.1.0 [`#52`](https://github.com/AxaGuilDEv/react-oidc/pull/52) by dependabot[bot] -- chore(deps-dev): bump codacy-coverage from 3.2.0 to 3.3.0 [`#53`](https://github.com/AxaGuilDEv/react-oidc/pull/53) by dependabot[bot] -- chore(deps-dev): bump @angular/core from 7.0.3 to 7.1.0 [`#46`](https://github.com/AxaGuilDEv/react-oidc/pull/46) by dependabot[bot] -- chore(deps-dev): bump eslint from 5.8.0 to 5.9.0 [`#48`](https://github.com/AxaGuilDEv/react-oidc/pull/48) by dependabot[bot] -- chore(deps-dev): bump shelljs from 0.8.2 to 0.8.3 [`#49`](https://github.com/AxaGuilDEv/react-oidc/pull/49) by dependabot[bot] -- chore(deps-dev): bump eslint-plugin-jest from 22.0.0 to 22.0.1 [`#50`](https://github.com/AxaGuilDEv/react-oidc/pull/50) by dependabot[bot] -- chore(deps-dev): bump eslint-config-prettier from 3.1.0 to 3.3.0 [`#47`](https://github.com/AxaGuilDEv/react-oidc/pull/47) by dependabot[bot] -- chore(deps): bump react-dom from 16.6.1 to 16.6.3 [`#44`](https://github.com/AxaGuilDEv/react-oidc/pull/44) by dependabot[bot] -- chore(deps): bump rxjs from 6.3.2 to 6.3.3 [`#45`](https://github.com/AxaGuilDEv/react-oidc/pull/45) by dependabot[bot] -- chore(deps): bump react from 16.6.1 to 16.6.3 [`#43`](https://github.com/AxaGuilDEv/react-oidc/pull/43) by dependabot[bot] -- chore(deps): bump react-redux from 5.0.7 to 5.1.1 [`#38`](https://github.com/AxaGuilDEv/react-oidc/pull/38) by dependabot[bot] -- chore(deps-dev): bump @angular/compiler from 7.0.3 to 7.0.4 [`#42`](https://github.com/AxaGuilDEv/react-oidc/pull/42) by dependabot[bot] -- chore(deps-dev): bump tslint-config-prettier from 1.15.0 to 1.16.0 [`#40`](https://github.com/AxaGuilDEv/react-oidc/pull/40) by dependabot[bot] -- doc(redux): update code example and add more informations [`#41`](https://github.com/AxaGuilDEv/react-oidc/pull/41) by Benoit Fontaine -- doc(context) add more explanation [`#37`](https://github.com/AxaGuilDEv/react-oidc/pull/37) by Guillaume Chervet -- chore(deps): [security] bump merge from 1.2.0 to 1.2.1 [`#39`](https://github.com/AxaGuilDEv/react-oidc/pull/39) by dependabot[bot] -- Set up CI with Azure Pipelines [`#31`](https://github.com/AxaGuilDEv/react-oidc/pull/31) by Cyril Lakech -- chore(deps-dev): bump tslint-plugin-prettier from 1.3.0 to 2.0.1 [`#34`](https://github.com/AxaGuilDEv/react-oidc/pull/34) by dependabot[bot] -- chore(deps-dev): bump eslint-config-prettier from 2.10.0 to 3.1.0 [`#36`](https://github.com/AxaGuilDEv/react-oidc/pull/36) by dependabot[bot] -- chore(deps-dev): bump eslint-plugin-jest from 21.27.2 to 22.0.0 [`#35`](https://github.com/AxaGuilDEv/react-oidc/pull/35) by dependabot[bot] -- fix oidcUser transmission [`0ae83e1`](https://github.com/AxaGuilDEv/react-oidc/commit/0ae83e1e9e4d71683c190a32fb87a91b307539cc) by Olivier YOUF -- chore(release): publish v1.1.5-alpha.0 ***NO_CI*** [`41536de`](https://github.com/AxaGuilDEv/react-oidc/commit/41536de22955b5f7f6b5b0dc758554a7f1deff20) by Build-CI -- WIP [`b866401`](https://github.com/AxaGuilDEv/react-oidc/commit/b86640100253030ccc1386d198f9910f86c052a4) by Guillaume Chervet -- WIP [`cad214b`](https://github.com/AxaGuilDEv/react-oidc/commit/cad214b48575a759f9245bcf2651d78dba4435e6) by Guillaume Chervet -- WIP [`d36dd3d`](https://github.com/AxaGuilDEv/react-oidc/commit/d36dd3d1463b0a4f6c9b6f8f6be74707552b139f) by Guillaume Chervet -- it works! [`b58a355`](https://github.com/AxaGuilDEv/react-oidc/commit/b58a355821a53cb0229acc1a1a69d724655e9f77) by Guillaume Chervet -- WIP [`fd2c87a`](https://github.com/AxaGuilDEv/react-oidc/commit/fd2c87a4b8b67914526bdef213719c08d49cbd54) by Guillaume Chervet -- chore(pipeline-azure) bug during publish [`0f0935a`](https://github.com/AxaGuilDEv/react-oidc/commit/0f0935a84f5065c1b3beabbb0b4acdc04346bf1d) by Guillaume Chervet -- WIP [`7c238f5`](https://github.com/AxaGuilDEv/react-oidc/commit/7c238f59ed3221910ced2631bed5092a9a56d71d) by Guillaume Chervet -- WIP [`4d04233`](https://github.com/AxaGuilDEv/react-oidc/commit/4d04233963fe77e803626ed0752dfe66474f8e71) by Guillaume Chervet -- WIP [`1a2fdbd`](https://github.com/AxaGuilDEv/react-oidc/commit/1a2fdbd0b2fc8f9d611248c02987fc524c77ea94) by Guillaume Chervet -- it works! [`adb3ecf`](https://github.com/AxaGuilDEv/react-oidc/commit/adb3ecf6f64e626209dd18bc6b0949c321af3347) by Guillaume Chervet -- WIP [`0836ecd`](https://github.com/AxaGuilDEv/react-oidc/commit/0836ecdfe1a0ff9846621a78e44b3cacb9184436) by Guillaume Chervet - -#### [v1.1.0](https://github.com/AxaGuilDEv/react-oidc/compare/v1.0.2...v1.1.0) -> 9 November 2018 -- fix(publish) missing attribute to publish to npm [`36a05fa`](https://github.com/AxaGuilDEv/react-oidc/commit/36a05faf92e6f09a2fec01756ad9b15cb44253ac) by Guillaume Chervet -- chore(release): publish v1.1.0 [ci skip] [`cc39e30`](https://github.com/AxaGuilDEv/react-oidc/commit/cc39e30840195b6d00591f72bef879e671a6a0f6) by Travis CI User - -#### [v1.0.2](https://github.com/AxaGuilDEv/react-oidc/compare/v1.0.1...v1.0.2) -> 31 October 2018 -- feat: add fetch observable hoc [`7ef055c`](https://github.com/AxaGuilDEv/react-oidc/commit/7ef055cb555d2937c2fcac175ab6294ef0fe9662) by Thierno Barry -- chore(release): publish v1.0.2 [ci skip] [`689b36c`](https://github.com/AxaGuilDEv/react-oidc/commit/689b36c1820e07ab1bc4b8542c4914f3081fed54) by Travis CI User -- chore: remove codacy that return 404 errors [`52724bb`](https://github.com/AxaGuilDEv/react-oidc/commit/52724bb7171a1e117f893580a6df54bcacf6d584) by Cyril Lakech - -#### [v1.0.1](https://github.com/AxaGuilDEv/react-oidc/compare/v1.0.0...v1.0.1) -> 4 October 2018 -- replacing includes by indexof [`#25`](https://github.com/AxaGuilDEv/react-oidc/pull/25) by mergify[bot] -- chore(release): publish v1.0.1 [ci skip] [`7cfb06c`](https://github.com/AxaGuilDEv/react-oidc/commit/7cfb06ce706187d79fccf9fbcebf28e5f1c28e9b) by Travis CI User - -### [v1.0.0](https://github.com/AxaGuilDEv/react-oidc/compare/v0.0.1...v1.0.0) -> 3 October 2018 -- doc: add npm version badge [`#23`](https://github.com/AxaGuilDEv/react-oidc/pull/23) by mergify[bot] -- chore(release): publish v1.0.0 [ci skip] [`728051b`](https://github.com/AxaGuilDEv/react-oidc/commit/728051b8b5db2eba1c7d2f922d9c466c7eba62ea) by Travis CI User - -#### [v0.0.1](https://github.com/AxaGuilDEv/react-oidc/compare/v0.0.1-alpha.11...v0.0.1) -> 3 October 2018 -- Mergify initial configuration [`#22`](https://github.com/AxaGuilDEv/react-oidc/pull/22) by mergify[bot] -- chore(release): publish v0.0.1 [ci skip] [`94cbf05`](https://github.com/AxaGuilDEv/react-oidc/commit/94cbf05bf38ad190f0025d3ddee3bb584d42cc01) by Travis CI User - -#### [v0.0.1-alpha.11](https://github.com/AxaGuilDEv/react-oidc/compare/v0.0.1-alpha.10...v0.0.1-alpha.11) -> 3 October 2018 -- chore: exclude tests from codeclimate analysis [`#14`](https://github.com/AxaGuilDEv/react-oidc/pull/14) by Cyril Lakech -- chore(build) add user variable for releasing a specific version to npm [`#20`](https://github.com/AxaGuilDEv/react-oidc/pull/20) by Guillaume Chervet -- fix : recompose branch test in the consumer and add doc about dev [`#21`](https://github.com/AxaGuilDEv/react-oidc/pull/21) by youf-olivier -- chore(release): publish v0.0.1-alpha.11 [ci skip] [`82d326a`](https://github.com/AxaGuilDEv/react-oidc/commit/82d326a0abc8c89516c010e731f911cc27da29c6) by Travis CI User - -#### [v0.0.1-alpha.10](https://github.com/AxaGuilDEv/react-oidc/compare/v0.0.1-alpha.9...v0.0.1-alpha.10) -> 1 October 2018 -- chore: setup test coverage with codacy [`#17`](https://github.com/AxaGuilDEv/react-oidc/pull/17) by Cyril Lakech -- chore(release): publish v0.0.1-alpha.10 [ci skip] [`612031e`](https://github.com/AxaGuilDEv/react-oidc/commit/612031ea55e145672dd5c2d8fd078479cff5f706) by Travis CI User - -#### v0.0.1-alpha.9 -> 1 October 2018 -- wip chore: setup publishing to npm [`#19`](https://github.com/AxaGuilDEv/react-oidc/pull/19) by Cyril Lakech -- chore: do not share codeclimate token [`#16`](https://github.com/AxaGuilDEv/react-oidc/pull/16) by Guillaume Chervet -- Add a Codacy badge to readme.md [`#15`](https://github.com/AxaGuilDEv/react-oidc/pull/15) by Guillaume Chervet -- chore: setup coverage with codeclimate [`#13`](https://github.com/AxaGuilDEv/react-oidc/pull/13) by youf-olivier -- chore: add coverage badge [`#12`](https://github.com/AxaGuilDEv/react-oidc/pull/12) by Guillaume Chervet -- chore(packages) add require attributes to publish inside npm in publi… [`#11`](https://github.com/AxaGuilDEv/react-oidc/pull/11) by Cyril Lakech -- refactor: simplify oidc service authenticateUser [`#10`](https://github.com/AxaGuilDEv/react-oidc/pull/10) by Guillaume Chervet -- chore: add codeclimate badge [`#8`](https://github.com/AxaGuilDEv/react-oidc/pull/8) by Guillaume Chervet -- init [`8d1518e`](https://github.com/AxaGuilDEv/react-oidc/commit/8d1518edc28882b83f207e8dc19d3c805f5ce96d) by Olivier Youf -- chore(release): publish v0.0.1-alpha.9 [ci skip] [`df4adb5`](https://github.com/AxaGuilDEv/react-oidc/commit/df4adb51f25be9f6b49f46ad9c7620e6b91fc259) by Travis CI User -- chore(packages) add require attributes to publish inside npm in public with a scope [`bb6f22f`](https://github.com/AxaGuilDEv/react-oidc/commit/bb6f22fe4a2c93156ad5848e1e9e221f5d7f2e57) by Guillaume Chervet -- chore: add license [`3d2ba13`](https://github.com/AxaGuilDEv/react-oidc/commit/3d2ba1328685a2aec3b5d158db9047870ed236c4) by Cyril Lakech -- Add Codacy badge [`400b938`](https://github.com/AxaGuilDEv/react-oidc/commit/400b93878c0b55b831de63b5bd01de82ac6d389c) by The Codacy Badger +# Changelog + +## v7.29.6 + +- [59afc43a](https://github.com/AxaFrance/oidc-client/commit/59afc43a04579293811563a9ebebb363357e0521) - [skip ci] Update to version 7.29.6 in package.json, 2026-09-20 by *github-actions[bot]* +- [7463acfc](https://github.com/AxaFrance/oidc-client/commit/7463acfc45589823a47fad470ad07839ef1f1684) - Rework FAQ with verified troubleshooting guidance and Mermaid diagrams (release) (#1734), 2026-09-20 by *Copilot* + + +## v7.29.5 + +- [722e93f4](https://github.com/AxaFrance/oidc-client/commit/722e93f4ba24a81149d5e08d324eb47381c233bf) - [skip ci] Generate changelog to version 7.29.5, 2026-09-19 by *github-actions[bot]* +- [9c9fefb1](https://github.com/AxaFrance/oidc-client/commit/9c9fefb1080a8b059a3b67f7f8030e2b0e2a018d) - [skip ci] Update to version 7.29.5 in package.json, 2026-09-19 by *github-actions[bot]* +- [0a9dcf84](https://github.com/AxaFrance/oidc-client/commit/0a9dcf847eba12f63e97a9fd471d2cc2db9ade4b) - refactor: Add package regression tests and streamline token parsing and fetch hooks (#1732) (release), 2026-09-19 by *Copilot* + + +## v7.29.4 + +- [29ae343c](https://github.com/AxaFrance/oidc-client/commit/29ae343c169003b0b515cb106c6df9b6527ac357) - [skip ci] Generate changelog to version 7.29.4, 2026-09-19 by *github-actions[bot]* +- [58ff7b53](https://github.com/AxaFrance/oidc-client/commit/58ff7b539e340b315dda2a30ee884cdd2e07a9c0) - [skip ci] Update to version 7.29.4 in package.json, 2026-09-19 by *github-actions[bot]* +- [95ab5aa4](https://github.com/AxaFrance/oidc-client/commit/95ab5aa41904bf837c97c2c876e0d26892b2c3e2) - Clarify package documentation and replace architecture images with Mermaid (#1730) (release), 2026-09-19 by *Copilot* + + +## v7.29.3 + +- [e470a539](https://github.com/AxaFrance/oidc-client/commit/e470a539874b0f8521718a3ad85764dd50d1da08) - [skip ci] Generate changelog to version 7.29.3, 2026-09-15 by *github-actions[bot]* +- [84649474](https://github.com/AxaFrance/oidc-client/commit/846494746b2bba4571aa8b4f60e3c7ccd5f2fbe4) - [skip ci] Update to version 7.29.3 in package.json, 2026-09-15 by *github-actions[bot]* +- [3b18c50d](https://github.com/AxaFrance/oidc-client/commit/3b18c50d5fa7ab61522a7d962cfac7748fb7b638) - Make React Oidc publish reruns idempotent when versions already exist on npm (#1728) (release), 2026-09-15 by *Copilot* +- [eb0f40a1](https://github.com/AxaFrance/oidc-client/commit/eb0f40a1a5c4740be3ec13d4e83b716110bdd327) - chore(deps): update all pnpm workspace dependencies (#1727) (release), 2026-09-15 by *Guillaume Chervet* +- [f91327ff](https://github.com/AxaFrance/oidc-client/commit/f91327ff95a465cc8ab2ec635ef3147bb8eb59c4) - fix(oidc): ignore bare trailing hash in getPath (release) (#1725), 2026-09-13 by *Paul Martin* + + +## v7.29.2 + +- [9325ced4](https://github.com/AxaFrance/oidc-client/commit/9325ced4f10433e3db00c273749989c08727dcfa) - [skip ci] Generate changelog to version 7.29.2, 2026-09-02 by *github-actions[bot]* +- [8e7d95fb](https://github.com/AxaFrance/oidc-client/commit/8e7d95fba420b1dc5515b2e9e7ccd1f5e08f6b6e) - [skip ci] Update to version 7.29.2 in package.json, 2026-09-02 by *github-actions[bot]* +- [658d033f](https://github.com/AxaFrance/oidc-client/commit/658d033f02de693c03cd07a9a9dbab1d694d14f4) - fix: publish npm (release), 2026-09-02 by *Guillaume Chervet* +- [99eb8992](https://github.com/AxaFrance/oidc-client/commit/99eb8992664560757d3e5e2799057d0b69088e1e) - fix: npm publish (release), 2026-09-02 by *Guillaume Chervet* +- [afeeb194](https://github.com/AxaFrance/oidc-client/commit/afeeb194e2145491ec3598538b101b88488bd764) - fix: publish (release), 2026-09-02 by *Guillaume Chervet* +- [b896d948](https://github.com/AxaFrance/oidc-client/commit/b896d94843f37cee8f051790a02a25504367a968) - fix: publish now with npm OIDC (release), 2026-09-01 by *Guillaume Chervet* + + +## v7.29.1 + +- [8af7d30f](https://github.com/AxaFrance/oidc-client/commit/8af7d30f1a70427626284826ee87d81d46f64685) - [skip ci] Generate changelog to version 7.29.1, 2026-08-05 by *github-actions[bot]* +- [6f83c671](https://github.com/AxaFrance/oidc-client/commit/6f83c67152871291723782192721f90386dcd140) - [skip ci] Update to version 7.29.1 in package.json, 2026-08-05 by *github-actions[bot]* +- [e880d47a](https://github.com/AxaFrance/oidc-client/commit/e880d47ad37194d21e80897d343eabd08ff66625) - fix(ci): publish npm (release), 2026-08-05 by *Guillaume Chervet* +- [6a0b8511](https://github.com/AxaFrance/oidc-client/commit/6a0b8511bef2b33082ebdc5c77b855190e9d2064) - fix(ci): npm publish (release), 2026-08-05 by *Guillaume Chervet* +- [aff49c35](https://github.com/AxaFrance/oidc-client/commit/aff49c355c178d7fd54386d357e8f60e90a4b3ae) - fix(ci): publish npm (release), 2026-08-05 by *Guillaume Chervet* +- [61edf77f](https://github.com/AxaFrance/oidc-client/commit/61edf77f448db74aac2c709e7bb82eeb4752f88f) - fix(ci): publish npm (release), 2026-08-05 by *Guillaume Chervet* +- [9e1a60f6](https://github.com/AxaFrance/oidc-client/commit/9e1a60f6b54226e12adff103ce07557a1c79c08e) - fix(ci): publish npm (release), 2026-08-05 by *Guillaume Chervet* +- [cf4b796e](https://github.com/AxaFrance/oidc-client/commit/cf4b796e9e1e6ae9d82e1b8d02e9acf03a48d9c5) - [skip ci] Generate changelog to version 7.29.0, 2026-07-31 by *github-actions[bot]* +- [8207e25c](https://github.com/AxaFrance/oidc-client/commit/8207e25c892f63399522123ff01702337694b48a) - [skip ci] Update to version 7.29.0 in package.json, 2026-07-31 by *github-actions[bot]* +- [a0b3dbd7](https://github.com/AxaFrance/oidc-client/commit/a0b3dbd710ebc361e11d4abec364bc6f7fbd25df) - 7.29.0, 2026-07-31 by *github-actions[bot]* + + +## v7.29.0 + +- [2d671ac9](https://github.com/AxaFrance/oidc-client/commit/2d671ac95a13a0effc1472e3fac0d4a32250fc2e) - feat(oidc-client): add typed-error (#1720) (release), 2026-07-31 by *Guillaume Chervet* +- [ad25121d](https://github.com/AxaFrance/oidc-client/commit/ad25121d7164052890296eea064baaf267433c4d) - [skip ci] Generate changelog to version 7.28.2, 2026-07-30 by *github-actions[bot]* +- [cdf423ef](https://github.com/AxaFrance/oidc-client/commit/cdf423ef7bbeccc3127055d1b9c401a5bfb5fa81) - [skip ci] Update to version 7.28.2 in package.json, 2026-07-30 by *github-actions[bot]* +- [79502145](https://github.com/AxaFrance/oidc-client/commit/79502145c05480ae9e69dc1b46f38c97ab7d0680) - 7.28.2, 2026-07-30 by *github-actions[bot]* + + +## v7.28.2 + +- [bd1f0877](https://github.com/AxaFrance/oidc-client/commit/bd1f087747d5342fa68dd84d3e4fb8ba38171768) - fix(oidc-react): Side effect on anonymous routes when reaching watchdog timeout (release) (#1722), 2026-07-30 by *Guillaume Chervet* +- [01bd60a9](https://github.com/AxaFrance/oidc-client/commit/01bd60a962271ad9ba9b84a450bd5c9c3dbf3b92) - [skip ci] Generate changelog to version 7.28.1, 2026-07-28 by *github-actions[bot]* +- [84bbd033](https://github.com/AxaFrance/oidc-client/commit/84bbd033197363e00aecfd92e78f3dd8f2806d22) - [skip ci] Update to version 7.28.1 in package.json, 2026-07-28 by *github-actions[bot]* +- [c1441778](https://github.com/AxaFrance/oidc-client/commit/c14417787846f0bf6079c54c2baf61900353aeff) - 7.28.1, 2026-07-28 by *github-actions[bot]* + + +## v7.28.1 + +- [8de7e316](https://github.com/AxaFrance/oidc-client/commit/8de7e316cc14d3f420808e36fb390c4e99b3cc65) - fix(all): remove Math.random (release) (#1719), 2026-07-28 by *Guillaume Chervet* +- [c1235d2a](https://github.com/AxaFrance/oidc-client/commit/c1235d2ab53c11faa3275112e345130252ae87aa) - [skip ci] Generate changelog to version 7.28.0, 2026-07-28 by *github-actions[bot]* +- [87ed38bb](https://github.com/AxaFrance/oidc-client/commit/87ed38bb864af0d2db246e2c225facd7be7e4e8d) - [skip ci] Update to version 7.28.0 in package.json, 2026-07-28 by *github-actions[bot]* +- [69aa8f1e](https://github.com/AxaFrance/oidc-client/commit/69aa8f1e50709aa11067429acfc178f5e5dce23f) - 7.28.0, 2026-07-28 by *github-actions[bot]* + + +## v7.28.0 + +- [638eee8a](https://github.com/AxaFrance/oidc-client/commit/638eee8ab1bbfecfadfb05d64217c2ecec9749be) - feat(oidc-client): PAR (#1718) (release), 2026-07-28 by *Guillaume Chervet* +- [f90927c2](https://github.com/AxaFrance/oidc-client/commit/f90927c21f9bd0f94dc537391186937978a21b3d) - [skip ci] Generate changelog to version 7.27.23, 2026-07-15 by *github-actions[bot]* +- [b2cf323b](https://github.com/AxaFrance/oidc-client/commit/b2cf323be58da2c7e66d73fb7040dfddb0ab683f) - [skip ci] Update to version 7.27.23 in package.json, 2026-07-15 by *github-actions[bot]* +- [87081bbc](https://github.com/AxaFrance/oidc-client/commit/87081bbcd041a85ae8f0161b0dd241fe4a8c0279) - 7.27.23, 2026-07-15 by *github-actions[bot]* + + +## v7.27.23 + +- [ad4036a3](https://github.com/AxaFrance/oidc-client/commit/ad4036a3de0626eccb8ecfc6f6c2e3ad1b79211b) - Update npm-publish.yml (release), 2026-07-15 by *Guillaume Chervet* + + +## v7.27.22 + +- [c2d1602c](https://github.com/AxaFrance/oidc-client/commit/c2d1602ce489fad13c0cc0187692f6a90cbd1a72) - fix: replace expired GIT_TOKEN with GITHUB_TOKEN in build job checkout (#1714) (release), 2026-07-14 by *Copilot* +- [80e720f7](https://github.com/AxaFrance/oidc-client/commit/80e720f7773fd3f2bf98755cda433ec3bcbdd25a) - Fix publish workflow failure caused by missing git identity during versioning (#1713) (release), 2026-07-14 by *Copilot* + + +## v7.27.21 + +- [832173ff](https://github.com/AxaFrance/oidc-client/commit/832173ffa24f7c8b65e2246fe07ac0bd13b641b2) - fix running linters (release) (#1711), 2026-07-11 by *Guillaume Chervet* +- [e0d3c7bc](https://github.com/AxaFrance/oidc-client/commit/e0d3c7bcdf324583868aa2c6d6ebca452c30ea09) - fix: bump Node.js to v22 and pin TypeScript to 6.0.3 to unblock CI linter (#1708), 2026-07-10 by *Copilot* + + +## v7.27.20 + +- [a8ce57dd](https://github.com/AxaFrance/oidc-client/commit/a8ce57dd6ff970c6608c3f99a0fafc0264e654b7) - fix: fixes vulnarabilities by updating dependencies (#1706) (release), 2026-07-10 by *Pavan Kumar Jadda* +- [afad2667](https://github.com/AxaFrance/oidc-client/commit/afad2667d38f700a839fe6cfbff2a7ce9a02b792) - fix: remove hardcoded pnpm version from GitHub Actions workflows (#1707), 2026-07-10 by *Copilot* + + +## v7.27.19 + +- [de470065](https://github.com/AxaFrance/oidc-client/commit/de470065418f3c98099f5df0a039bc33392cb74d) - Handle service worker AbortError on tab shutdown to avoid uncaught rejections (#1702) (release), 2026-06-24 by *Guillaume Chervet* + + +## v7.27.18 + +- [e4afee9c](https://github.com/AxaFrance/oidc-client/commit/e4afee9ccf68a3905c1d071223c2cea0a435fb8b) - Handle corrupted or missing OIDC state with a clear, descriptive error (#1700) (release), 2026-06-23 by *Guillaume Chervet* + + +## v7.27.17 + +- [940aa8da](https://github.com/AxaFrance/oidc-client/commit/940aa8da6427e95dd2aa380575c7a27c7c8f9aac) - fix: Prevent logoutAsync from clearing local session before navigation to avoid re-auth race (#1701) (release), 2026-06-23 by *Guillaume Chervet* + + +## v7.27.16 + +- [cb8fc882](https://github.com/AxaFrance/oidc-client/commit/cb8fc88246229ee819a32a2164385ef05209b806) - Gracefully handle hooks outside OidcProvider and add test provider #1679 (#1698) (release), 2026-06-23 by *Guillaume Chervet* + + +## v7.27.15 + +- [e4cd2bd8](https://github.com/AxaFrance/oidc-client/commit/e4cd2bd885f404bedaa66200d10c6145f403823a) - Fix issue #1696: loadingTimeout_error fires on authenticated/idle state after a silent session restore (regression from #1681 / #1692) (#1697) (release), 2026-06-21 by *Guillaume Chervet* + + +## v7.27.14 + +- [416a1b52](https://github.com/AxaFrance/oidc-client/commit/416a1b52c64e8d9c764f605f19c8a21fe3a5f467) - Auto fix for issue #1682: CallbackManager: no observable signal that post-callback navigation has committed (Safari edge case) (#1691) (release), 2026-06-16 by *Guillaume Chervet* + + +## v7.27.13 + +- [7b97a98f](https://github.com/AxaFrance/oidc-client/commit/7b97a98f47b54ae4a9bc13dfc732c3e153dd8e60) - Auto fix for issue #1681: OidcProvider can sit in loading/authenticating indefinitely with no timeout event (#1692) (release), 2026-06-16 by *Guillaume Chervet* + + +## v7.27.12 + +- [db7af26e](https://github.com/AxaFrance/oidc-client/commit/db7af26e7979da303bc5d70df28a56854b8d5f3e) - Document and expose the public service worker message protocol (#1694) (release), 2026-06-16 by *Guillaume Chervet* + + +## v7.27.11 + + + +## v7.27.10 + +- [93629d92](https://github.com/AxaFrance/oidc-client/commit/93629d928b045116487860230fc2cd7875930be4) - fix: exclude navigate-mode requests from destination bypass (#1683) (#1687) (release), 2026-06-13 by *Guillaume Chervet* +- [31d441d3](https://github.com/AxaFrance/oidc-client/commit/31d441d3b37d92fd4f8daaa501b4c55ae1c58a0f) - chore: add copilot instruction, 2026-06-12 by *Guillaume Chervet* + + +## v7.27.9 + +- [4e9c33f9](https://github.com/AxaFrance/oidc-client/commit/4e9c33f9c46d2a17a5c29103707dd743a5acc5ba) - fix(oidc): prevent storage poisoning on undefined setters (alpha) (#1674) (release), 2026-06-10 by *Andreas Adam* + + +## v7.27.8 + +- [c67eabfb](https://github.com/AxaFrance/oidc-client/commit/c67eabfbe2bf437b490f37c63d58d79bdf82c4de) - Update all dependencies to latest versions (#1665) (release), 2026-05-15 by *Copilot* + + +## v7.27.7 + + + +## v7.27.6 + + + +## v7.27.5 + +- [debafc81](https://github.com/AxaFrance/oidc-client/commit/debafc81b9f6d24f2c2bfc81e160b1cc595cacc0) - Bypass non-OIDC requests in the service worker (#1663) (release), 2026-05-14 by *Copilot* + + +## v7.27.4 + +- [9d94b3d6](https://github.com/AxaFrance/oidc-client/commit/9d94b3d64a0cec857106b15e87ddcd1777ef42d7) - chore(all): Update workspace libraries (#1659) (release), 2026-04-27 by *Copilot* +- [78a0bfa6](https://github.com/AxaFrance/oidc-client/commit/78a0bfa6281c0891ce4b7abde786ffdcd119d964) - Remove sonar badges from all README.md files (#1658), 2026-04-24 by *Copilot* +- [a18f8101](https://github.com/AxaFrance/oidc-client/commit/a18f81018dc8cd24620028b05912a46bb1f2ff72) - docs: remove twitter badge, add npm weekly downloads badges to READMEs (#1657), 2026-04-24 by *Copilot* +- [06918197](https://github.com/AxaFrance/oidc-client/commit/06918197a2ba6d549e2927cdf60a7019deb0fea7) - Remove sonar badges from README.md (#1656), 2026-04-24 by *Copilot* + + +## v7.27.3 + +- [3ae9f19f](https://github.com/AxaFrance/oidc-client/commit/3ae9f19f7ff304a85de103424865ebc63675d866) - fix: cache service worker registration to prevent repeated register calls (#1655) (release), 2026-04-21 by *Copilot* + + +## v7.27.2 + +- [61b2ee62](https://github.com/AxaFrance/oidc-client/commit/61b2ee62093098e30cb15c9b2bb7102283876a3c) - fix(service-worker): add timeout to token renewal wait loop to prevent indefinite fetch blocking (#1654) (release), 2026-04-20 by *Copilot* + + +## v7.27.1 + +- [362cd3ad](https://github.com/AxaFrance/oidc-client/commit/362cd3adfecb4056558d9f9b503f503c091b0b7a) - fix(oid-client): service worker update (#1649) (release), 2026-03-18 by *Guillaume Chervet* + + +## v7.27.0 + +- [48d3ad02](https://github.com/AxaFrance/oidc-client/commit/48d3ad028d7b8ccbd6504bb2682f72e0b079643c) - feat(oidc-client): Add a 'login_state_storage' option to allow storing the auth flow state separately to tokens (#1646) (release), 2026-03-11 by *Alex O'Callaghan* + + +## v7.26.8 + +- [7c5dd661](https://github.com/AxaFrance/oidc-client/commit/7c5dd6612ae7ef5c492845eeae65dfdd23a75842) - fix(oidc): infinite service worker reload loop on version change (#1647) (release), 2026-03-10 by *Copilot* + + +## v7.26.7 + +- [46bd6a89](https://github.com/AxaFrance/oidc-client/commit/46bd6a89b803ca9c2c9aedf0a0fa1d73d229b995) - docs: clarify that allowMultiTabLogin requires OIDC fetch wrapper for API calls (#1643) (release), 2026-02-21 by *Copilot* +- [f35ae017](https://github.com/AxaFrance/oidc-client/commit/f35ae0174664ea8abcc6c79b608958d0b2164636) - Fix `useOidcUser` stuck in Loading state after reloadOidcUser() (#1642), 2026-02-21 by *Copilot* + + +## v7.26.6 + +- [46d74602](https://github.com/AxaFrance/oidc-client/commit/46d746024d7420f8386f18217c77ff5bc6ccc83e) - Fix null extras values being encoded as literal "null" string in query params (#1640) (release), 2026-02-21 by *Copilot* + + +## v7.26.5 + +- [da37c1c2](https://github.com/AxaFrance/oidc-client/commit/da37c1c29b79d20abc8aeeca17a18155f4274a7e) - fix(react-oidc): remove dangling `setLoading` call in OidcProvider (#1639) (release), 2026-02-21 by *Copilot* + + +## v7.26.4 + +- [9a44940d](https://github.com/AxaFrance/oidc-client/commit/9a44940d0f65255618d7727d7033afa1dbab7239) - fix(oidc-client): sw with safari 17 (#1636) (release), 2026-02-12 by *Guillaume Chervet* + + +## v7.26.3 + + + +## v7.26.2 + + + +## v7.26.1 + +- [49975e24](https://github.com/AxaFrance/oidc-client/commit/49975e24a7dd49d7dd6ddcc86da4ea47d74cac98) - refactor(all): update libraries (release) (#1633), 2026-01-28 by *Guillaume Chervet* +- [2bb456ed](https://github.com/AxaFrance/oidc-client/commit/2bb456ed057131748d2ff26169f166ffcd7e618b) - fix: do not handle all requests (#1625) (alpha), 2025-11-24 by *Andreas Adam* + + +## v7.26.0 + +- [b9656c25](https://github.com/AxaFrance/oidc-client/commit/b9656c2559993efe94a121c442000969b1eaba2b) - feat(react-oidc): enforce uniqueness of redirect_uri and silent_redirect_uri (#1606) (release), 2025-09-17 by *Jean-Marc Rakotoarisoa* +- [960a6947](https://github.com/AxaFrance/oidc-client/commit/960a69472b75e12490025ee3b46d15643c0e35a6) - fix(oidc-client): Clear userInfo in storage in clearAsync function (#1603), 2025-09-17 by *Mérill Téterel* + + +## v7.25.16 + +- [c79907b6](https://github.com/AxaFrance/oidc-client/commit/c79907b63af69352ed660e97d9e61265bc155131) - fix(oidc-client): renew token silent login (release) (#1598), 2025-09-04 by *Guillaume Chervet* + + +## v7.25.15 + +- [f4ce35e4](https://github.com/AxaFrance/oidc-client/commit/f4ce35e4e272de9e4f4387494877bab5939fa1a9) - fix(oidc-client): implement consistent storage caching for userInfo and well-known configuration (release) (#1592), 2025-09-02 by *Michael Sverdlov* + + +## v7.25.14 + +- [f5b86ffe](https://github.com/AxaFrance/oidc-client/commit/f5b86ffe656845c26b6f7bcf88da43cf8ece173e) - fix(renewTokens): prevent infinite loop on renew token flow (#1581) (release), 2025-08-02 by *Matheus Frigo* + + +## v7.25.13 + +- [31661bc1](https://github.com/AxaFrance/oidc-client/commit/31661bc16d67866b6e8d5b978b117600b769fe28) - refcator(all): update dependencies (#1567) (release), 2025-05-05 by *Guillaume Chervet* + + +## v7.25.12 + +- [f19cca8c](https://github.com/AxaFrance/oidc-client/commit/f19cca8c4a0aa24f5646e13c80610148037e3bad) - fix(oidc-service-worker): stream already consumed (#1558) (release), 2025-04-30 by *Guillaume Chervet* + + +## v7.25.11 + +- [e5e958a3](https://github.com/AxaFrance/oidc-client/commit/e5e958a34f39ab349b34e094bc17f5df3b49b8f9) - fix(oidc-service-worker): missing body (#1556) (release), 2025-04-29 by *Guillaume Chervet* + + +## v7.25.10 + +- [dbb52dad](https://github.com/AxaFrance/oidc-client/commit/dbb52dad456d2429d21fd5a645ff27d6b21e056a) - fix(serviceworker): ios (#1554) (release), 2025-04-24 by *Guillaume Chervet* +- [f2f447e3](https://github.com/AxaFrance/oidc-client/commit/f2f447e34805822387281126b93ec39153fb5672) - refactor(all): updates libraries, 2025-04-14 by *Guillaume Chervet* + + +## v7.25.9 + +- [c1ece663](https://github.com/AxaFrance/oidc-client/commit/c1ece663131db50a8c1152b18be0d3d5f6a0025e) - fix(oidc): make bearer fetch works on safari back (release) (#1550), 2025-04-11 by *Guillaume Chervet* + + +## v7.25.8 + +- [4c85da7d](https://github.com/AxaFrance/oidc-client/commit/4c85da7d0e0e802c95efe31c5de948ac5abd5494) - fix(oidc-service-worker): Error with latest Safari (#1547) (release), 2025-04-04 by *Guillaume Chervet* + + +## v7.25.7 + +- [930ad036](https://github.com/AxaFrance/oidc-client/commit/930ad0367cfcaa5bd15726c17501d70b0768ce5e) - fix(oidc-service-worker): fetch already consume (release) (#1534), 2025-04-01 by *Guillaume Chervet* + + +## v7.25.6 + +- [2cb8cdea](https://github.com/AxaFrance/oidc-client/commit/2cb8cdeafdb39fadc19beb878808a5d2291cd6fa) - fix(oidc): multi-tab dpop not working with (#1531) (release), 2025-03-28 by *Guillaume Chervet* + + +## v7.25.5 + +- [d7567ba2](https://github.com/AxaFrance/oidc-client/commit/d7567ba28c0dfe9b248be52ecfac4df6f98b397b) - feature(oidc-service-worker): multi tab auth (#1528) (release), 2025-03-27 by *Guillaume Chervet* + + +## v7.25.4 + +- [1b2fa1da](https://github.com/AxaFrance/oidc-client/commit/1b2fa1da513b419c2c28e67a331d12670b07a94d) - refactor(oidc): update librairie (release), 2025-03-25 by *Guillaume Chervet* + + +## v7.25.3 + +- [ca0f0645](https://github.com/AxaFrance/oidc-client/commit/ca0f0645c52dd827b97912d5ee6f1836e05f56e1) - fix(oidc): lost session back (release) (#1514), 2025-02-06 by *Guillaume Chervet* + + +## v7.25.2 + +- [15900f42](https://github.com/AxaFrance/oidc-client/commit/15900f42e42539a5b12a4c23959ec95c695426d9) - fix(oidc): MonitorSession/CheckSession initializes only on one tab (release) (#1513), 2025-02-05 by *Guillaume Chervet* + + +## v7.25.1 + +- [f0641a6f](https://github.com/AxaFrance/oidc-client/commit/f0641a6ff21a7cf600ee5e782a3e3114b462551c) - fix(oidc): remove use of localStorage for cache (release), 2025-02-05 by *Guillaume Chervet* + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index acd889a70..2231827a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,41 +1,69 @@ -# Contributing to @axa-fr/react-oidc +# Contributing to @axa-fr/oidc-client -First, ensure you have the [latest `npm`](https://docs.npmjs.com/). +First, ensure you have Node.js 22.22.2+, 24.15.0+, or 26+ and the version of +[pnpm](https://pnpm.io/) specified in `package.json`. + +The workspace uses TypeScript 7 for the `tsc` command through the +`@typescript/native` npm alias. The `typescript` dependency aliases +`@typescript/typescript6` to provide the compiler API required by typescript-eslint +and vite-plugin-dts. This follows the +[TypeScript side-by-side setup](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/#running-side-by-side-with-typescript-6-0). To get started with the repository: ```sh -git clone https://github.com/AxaGuilDEv/react-oidc.git -cd react-oidc/packages/react -npm install -npm start +git clone https://github.com/AxaFrance/oidc-client.git + +cd oidc-client +pnpm install + +# oidc client demo +cd examples/oidc-client-demo +pnpm install +pnpm start +# then navigate to http://localhost:5174 + +# react vite demo +cd examples/react-oidc-demo +pnpm install +pnpm start +# then navigate to http://localhost:4200 + +# react NextJS demo +cd examples/nextjs-demo +pnpm install +pnpm run dev +# then navigate to http://localhost:3001 ``` + You are now ready to contribute! ## Pull Request Please respect the following [PULL_REQUEST_TEMPLATE.md](./PULL_REQUEST_TEMPLATE.md) -Packages are automaticaly published on npm when a PR is merged on main. +Packages are automatically published on npm when a PR is merged on main. Example of commit messages : To publish a patch version (0.0.x) : + - fix(oidc): my message (alpha) => will publish next patch as an alpha - chore(oidc): my message (beta) => will publish next patch as an beta - refactor(oidc): my message (release) => will publish next patch release (with automatic git tag and release) To publish a minor version (0.x.0) : + - feat(oidc): my message (alpha) => will publish next minor as an alpha - feat(oidc): my message (beta) => will publish next minor as an beta - feat(oidc): my message (release) => will publish next minor release (with automatic git tag and release) To publish a major version (x.0.0) : + - fix(oidc): my message containing BREACKING word (alpha) => will publish next major as an alpha - fix(oidc): my message containing BREACKING word (beta) => will publish next major as an beta - fix(oidc): my message containing BREACKING word (release) => will publish next major release (with automatic git tag and release) - ## Issue Please respect the following [ISSUE_TEMPLATE.md](./ISSUE_TEMPLATE.md) diff --git a/FAQ.md b/FAQ.md new file mode 100644 index 000000000..f046c6c75 --- /dev/null +++ b/FAQ.md @@ -0,0 +1,402 @@ +# Frequently asked questions + +Start with the [React guide](./packages/react-oidc/README.md) or +[vanilla JavaScript guide](./packages/oidc-client/README.md) for installation. +These answers describe the current source; check the documentation for your +installed version when an option is unavailable. Linked issues provide context, +not guarantees that every reported problem is resolved. + +## Contents + +- [Do I need a service worker?](#do-i-need-a-service-worker) +- [Why is my access token a placeholder?](#why-is-my-access-token-a-placeholder) +- [Why does my API return 401, especially with multiple tabs?](#why-does-my-api-return-401-especially-with-multiple-tabs) +- [Why does login fail when two tabs sign in together?](#why-does-login-fail-when-two-tabs-sign-in-together) +- [How are tokens renewed?](#how-are-tokens-renewed) +- [Why are tokens renewed every few seconds?](#why-are-tokens-renewed-every-few-seconds) +- [Why does silent sign-in fail in some browsers?](#why-does-silent-sign-in-fail-in-some-browsers) +- [Will I stay signed in after sleep or a browser restart?](#will-i-stay-signed-in-after-sleep-or-a-browser-restart) +- [Does logout sign me out of every application?](#does-logout-sign-me-out-of-every-application) +- [What does session monitoring require?](#what-does-session-monitoring-require) +- [Why is the worker unavailable or outdated?](#why-is-the-worker-unavailable-or-outdated) +- [Can I use an existing PWA worker or a mobile WebView?](#can-i-use-an-existing-pwa-worker-or-a-mobile-webview) +- [Why is window.crypto.subtle unavailable?](#why-is-windowcryptosubtle-unavailable) +- [How do I use multiple identity providers?](#how-do-i-use-multiple-identity-providers) +- [Should I use this client or a Backend for Frontend?](#should-i-use-this-client-or-a-backend-for-frontend) +- [Does the service worker protect me from XSS?](#does-the-service-worker-protect-me-from-xss) +- [What should I include in a bug report?](#what-should-i-include-in-a-bug-report) + +## Do I need a service worker? + +No. Omit `service_worker_relative_url` and leave `service_worker_only: false` +(the default) to use browser storage: + +```javascript +const configuration = { + client_id: 'your-public-client', + authority: 'https://issuer.example.com', + redirect_uri: `${window.location.origin}/authentication/callback`, + scope: 'openid profile', + service_worker_only: false, +}; +``` + +Without a worker, token storage defaults to `sessionStorage`. You can choose +`localStorage`, but both are readable by same-origin JavaScript. + +Worker mode keeps access and refresh tokens in worker memory by default. +Follow the [worker setup guide](./packages/oidc-client-service-worker/README.md#getting-started); +installing the package alone does not serve or configure the worker files. + +In React, `service_worker_only: true` makes the provider show its unsupported-worker +screen when it receives the worker-unavailable event. **This is not a core-client +guarantee against browser-storage fallback:** the vanilla client can still use +browser storage when worker initialization returns no worker. If token isolation +is mandatory, do not rely on this flag alone; verify worker availability and +prevent authentication from proceeding without it in your integration. + +When testing a switch away from worker mode, unregister the application's old +OIDC worker in browser developer tools and reload the page. Removing the +configuration does not unregister an already installed worker. + +## Why is my access token a placeholder? + +In worker mode, values such as `ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER_...` +are expected when access-token hiding is enabled. The worker holds the real +token and replaces the placeholder on matching trusted requests. Refresh tokens +are also hidden; the ID token and decoded claims can still be available to +application code. + +Use `useOidcFetch()` in React or `oidcClient.fetchWithTokens(fetch)` in vanilla +JavaScript rather than trying to decode or send the placeholder yourself. +`showAccessToken: true` in the matching `OidcTrustedDomains.js` entry exposes +the access token to application JavaScript; use it only if your integration +requires that trade-off. It does not expose the refresh token. + +Developer tools can show the application request and the worker's outgoing +request. Inspect which request actually failed: duplicate-looking entries do +not by themselves prove a bug, but a failed token exchange is not something to +ignore. See [#1703](https://github.com/AxaFrance/oidc-client/issues/1703). + +## Why does my API return 401, especially with multiple tabs? + +Check these separately: + +1. Wait for authentication before calling a protected API. +2. Use the OIDC fetch wrapper and select the correct configuration name. +3. In worker mode, check that the worker controls the page and that the API URL + matches `accessTokenDomains` for that configuration in `OidcTrustedDomains.js`. + Follow the [trusted-domain rules](./packages/oidc-client-service-worker/README.md#trusted-domains); + do not broaden the allowlist to arbitrary destinations. +4. Check the API's expected issuer, audience, and scopes. Attaching a token does + not guarantee that the API will accept it. + +With `allowMultiTabLogin: true` in `OidcTrustedDomains.js`, the worker uses the +tab-specific placeholder in the `Authorization` header to select the session. +A plain `fetch` or default Axios call without that marker cannot select the +tab's token. + +In a React component beneath the matching `OidcProvider`, obtain the wrapper +with `useOidcFetch` imported from `@axa-fr/react-oidc`: + +```javascript +const { fetch: oidcFetch } = useOidcFetch(); +``` + +For an initialized vanilla client: + +```javascript +const oidcFetch = oidcClient.fetchWithTokens(fetch); +``` + +Then use it after authentication: + +```javascript +const response = await oidcFetch('https://api.example.com/data'); +if (!response.ok) { + throw new Error(`API request failed with status ${response.status}`); +} +const data = await response.json(); +``` + +React also provides `withOidcFetch`. For Axios, use an integration that actually +routes requests through the OIDC fetch wrapper, or use the wrapper directly; +changing libraries alone does not supply the tab marker. Only call trusted URLs +with the wrapper: it is not itself a destination allowlist. + +## Why does login fail when two tabs sign in together? + +Without a worker, `storage: localStorage` shares authorization state, the PKCE +verifier, and nonce between tabs using the same configuration. Concurrent login +flows can overwrite one another and cause state-validation failures. + +If you deliberately want shared token storage, merge these options into your +configuration to keep authorization-flow state isolated per tab: + +```javascript +const storageOptions = { + storage: localStorage, + login_state_storage: sessionStorage, +}; +``` + +This changes browser-storage behavior, not worker storage. For independent +worker-mode logins, configure `allowMultiTabLogin: true` and use the OIDC fetch +wrapper as described above. + +Do not disable state or nonce validation to work around a callback failure. +For other causes, see the [errors guide](./packages/oidc-client/README.md#errors-and-events). + +## How are tokens renewed? + +By default, the client schedules renewal before expiry. Normal renewal uses a +refresh token when one is available; otherwise it attempts the configured +iframe-based silent-login flow. + +```mermaid +flowchart TD + Due["Tokens need renewal"] --> Refresh{"Refresh token available?"} + Refresh -->|Yes| Endpoint["Request tokens from the token endpoint"] + Refresh -->|No| Silent["Attempt configured silent sign-in"] + Endpoint --> Result{"Renewal succeeds?"} + Silent --> Result + Result -->|Yes| Continue["Continue with renewed tokens"] + Result -->|No| Recovery["Handle the error; interactive sign-in may be needed"] +``` + +Your provider controls whether it issues refresh tokens, their lifetime, and +rotation policy. Some providers require `offline_access` and additional client +settings or consent; adding that scope alone is not a guarantee. + +Silent login requires separate silent-login/callback routes and a usable +provider session. See the [silent-login setup](./packages/oidc-client/README.md#silent-login). +The React provider handles these routes; vanilla applications must implement them. + +For renewal only when a protected request needs it, use +`TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted` with the OIDC fetch +wrapper or `getValidTokenAsync()`. See [token renewal](./packages/oidc-client/README.md#token-renewal). +This is not a user-inactivity detector. If you need an idle logout, implement +activity tracking in your application and enforce appropriate session limits +at the provider/server. + +Related questions: [#1368](https://github.com/AxaFrance/oidc-client/issues/1368) +and [#1070](https://github.com/AxaFrance/oidc-client/issues/1070). + +## Why are tokens renewed every few seconds? + +Compare your token lifetimes with `refresh_time_before_tokens_expiration_in_second`. +Its default is 120 seconds, with a random reduction of up to 39 seconds when +the configured value exceeds 60. A token whose lifetime is near or below the +renewal margin can trigger frequent renewal. + +The default `token_renew_mode`, `'access_token_or_id_token_invalid'`, uses the +earlier access-token or ID-token expiry. Check both lifetimes, not just the +access token. The other modes are `'access_token_invalid'` and +`'id_token_invalid'`; choose one only if it matches your application's needs. + +Adjust the renewal margin or provider-issued lifetimes together. No client-side +setting can extend a token's server-defined validity or guarantee timely +background execution. + +## Why does silent sign-in fail in some browsers? + +Silent sign-in loads the provider in a hidden iframe with `prompt=none`. It +depends on an existing provider session, cookies being available in that iframe, +and the provider allowing the flow and framing. + +Browser privacy settings and third-party-cookie restrictions can prevent this. +Hosting the application and provider on the same site can help with cookie +restrictions, but **does not guarantee success**. For example, +`https://app.example.com` and `https://login.example.com` are different origins, +even though they are normally same-site. + +Check the registered `silent_redirect_uri`, the iframe's network errors, +provider cookie settings, and CSP/frame restrictions. A provider response such +as `login_required` or `consent_required` may require interactive sign-in. +Refresh-token renewal does not use this iframe flow, although refresh tokens +can still expire or be revoked. + +## Will I stay signed in after sleep or a browser restart? + +Not necessarily. Browsers can suspend timers and terminate service workers; +worker tokens are held in memory, not durable token storage. Provider session +cookies may outlive the worker. If its tokens are lost, signing in again depends +on an available sign-in flow; silent sign-in also needs a usable provider session. + +```mermaid +flowchart TD + Session["Authenticated browser application"] --> Pause["Sleep, suspension, or browser restart"] + Pause --> Resume["Application resumes or starts"] + Resume --> Restore{"Existing session can be restored?"} + Restore -->|Yes| Use["Use or renew available tokens"] + Restore -->|No| SignIn["Sign in again"] +``` + +Network reconnection, token expiry, and cookie restrictions can all affect +recovery. Worker keep-alive requests are not a persistence guarantee. +With browser storage, `localStorage` can persist across browser sessions, but +that neither extends token validity nor makes tokens inaccessible to JavaScript. + +Handle session loss with a clear sign-in action rather than an endless retry +loop. React offers `sessionLostComponent` and `onSessionLost`; vanilla clients +can subscribe to [client events](./packages/oidc-client/README.md#errors-and-events). +See [#1147](https://github.com/AxaFrance/oidc-client/issues/1147) for sleep/renewal +reports and [#1662](https://github.com/AxaFrance/oidc-client/issues/1662) for a +persistence proposal, not an implemented persistence option. + +## Does logout sign me out of every application? + +Not automatically. These are distinct operations: + +- **Local session clearing:** `clearSessionAsync()` removes this client's local + session without provider logout or token revocation. +- **Standard logout:** `logoutAsync()` (or React's `logout()`) attempts configured + token revocation when the provider exposes a revocation endpoint, uses its + end-session endpoint when available, and clears the local session. Register + the intended post-logout URL with the provider. +- **Single logout across applications:** propagation depends on the provider, + its logout/session protocols, and the other applications. Ending one local + session is not proof that every application or API token is now signed out. + +Check discovery metadata, provider logout requirements, and failed revocation +requests. Do not assume that serving all applications on one domain enables +single logout. See [#820](https://github.com/AxaFrance/oidc-client/issues/820) +for an example of provider-specific logout configuration. + +## What does session monitoring require? + +`monitor_session` defaults to `false`. When enabled, it needs the provider's +`check_session_iframe`, a `session_state`, and configured silent-login routes. +It checks for changes to the provider session; it is not a general token-expiry +or user-idle timer. + +This iframe-based mechanism is also subject to browser cookie/privacy policies +and provider support. It cannot guarantee immediate logout detection across +applications. Keep API authorization and session-expiry enforcement on the server. + +## Why is the worker unavailable or outdated? + +Check the deployment before changing authentication logic: + +1. Serve `OidcServiceWorker.js` and `OidcTrustedDomains.js` from your application's + origin as JavaScript, not the SPA HTML fallback. +2. Confirm that `service_worker_relative_url` points to the deployed file and + that its scope covers the application and callback pages. +3. Run the package's worker-copy command after upgrades. It replaces + `OidcServiceWorker.js` but preserves an existing `OidcTrustedDomains.js`; + maintain your trusted-domain entries yourself. +4. In browser developer tools, inspect the active registration and which worker + controls the tab. Already-open tabs and cached assets can complicate updates. +5. Check secure-context support and registration errors. Review the + [`service_worker_only` limitation](#do-i-need-a-service-worker) before relying + on that flag to prevent browser-storage fallback. + +For an application under a subpath, account for the worker URL and allowed +scope. `service_worker_register` allows custom registration, but does not bypass +browser scope restrictions. See the [deployment guide](./packages/oidc-client-service-worker/README.md#deployment-and-troubleshooting), +[#1181](https://github.com/AxaFrance/oidc-client/issues/1181) (scope), and +[#1648](https://github.com/AxaFrance/oidc-client/issues/1648) (updates). + +## Can I use an existing PWA worker or a mobile WebView? + +Do not assume either works without integration testing. + +A page has one controlling service worker at a time. An existing PWA worker and +the OIDC worker need a deliberate registration, scope, and fetch/message-handling +design. Custom registration is available, but blindly combining worker scripts +can cause conflicts. [#914](https://github.com/AxaFrance/oidc-client/issues/914) +discusses integration approaches, not a universal supported recipe. + +WebView capabilities and restrictions vary by platform and host application. +`service_worker_activate` can change the library's activation decision, but +cannot add missing browser APIs. Decide whether browser-storage fallback is +acceptable, then test login, API calls, renewal, and logout in your actual +environment. See [#1389](https://github.com/AxaFrance/oidc-client/issues/1389). + +## Why is window.crypto.subtle unavailable? + +The Web Crypto `crypto.subtle` API and service workers require a secure context. Use HTTPS in +production. Browsers generally treat `http://localhost` as trustworthy for local +development; an HTTP LAN IP address or remote hostname is not the same exception. +Check `window.isSecureContext` and the APIs available in your browser or WebView. +Do not disable PKCE or substitute insecure cryptography. + +Related report: [#1028](https://github.com/AxaFrance/oidc-client/issues/1028). + +## How do I use multiple identity providers? + +Use distinct configuration names rather than swapping settings under the same +name. Give each configuration its own callback URLs and, in worker mode, a +matching entry in `OidcTrustedDomains.js`. + +In React, pass `configurationName` consistently to the provider and protected +components, and select the name in hooks: + +```javascript +const { login, isAuthenticated } = useOidc('payments'); +const { fetch: paymentsFetch } = useOidcFetch(undefined, 'payments'); +``` + +Nesting providers does not change the hooks' default configuration name. +Follow the [named-configuration examples](./packages/react-oidc/README.md#named-configurations). +Related questions: [#1590](https://github.com/AxaFrance/oidc-client/issues/1590) +and [#1283](https://github.com/AxaFrance/oidc-client/issues/1283). + +## Should I use this client or a Backend for Frontend? + +Choose based on your architecture and threat model, not a claim that either is +always safer, cheaper, or simpler. + +```mermaid +flowchart LR + subgraph ClientSide["Browser client with worker token hiding"] + App["Application JavaScript"] -->|"Token placeholders"| Worker["Service worker"] + Worker <-->|"Token exchange"| ProviderA["OIDC provider"] + Worker -->|"Access token"| APIA["API"] + end + subgraph ServerSide["Backend for Frontend"] + Browser["Browser"] -->|"Session cookie"| BFF["BFF server"] + BFF <-->|"Token exchange"| ProviderB["OIDC provider"] + BFF -->|"Access token"| APIB["API"] + end +``` + +This browser client can be used with a statically hosted application, without +adding a dedicated authentication backend. You still need an identity provider, +protected APIs, and appropriate CORS configuration. With worker token hiding +enabled, access and refresh tokens remain in the browser's worker context; +without the worker, they are accessible to application JavaScript. + +A BFF keeps OAuth tokens on the server and typically gives the browser a +session cookie. It requires backend hosting, session management, secure cookie +configuration, and CSRF defenses. XSS can still cause authenticated actions in +either architecture. This library does not implement a BFF or server-side access +control. + +## Does the service worker protect me from XSS? + +It reduces direct access to hidden tokens, but **does not prevent XSS or CSRF**. +Malicious code running in your application can still make authenticated +requests. Initializing `OidcProvider` early is not an XSS defense, and a CSP +such as `script-src 'self'` does not guarantee that worker unregistration or +iframe-based attacks are impossible. + +Use a restrictive CSP suited to your deployment, safe rendering and input +handling, reviewed dependencies, and narrowly scoped trusted destinations. +Never embed a client secret in browser code. Do not render or log tokens. +Your APIs must validate tokens and enforce authorization independently of +client-side route guards. + +## What should I include in a bug report? + +Include the package versions, browser/OS (and WebView host if applicable), +whether a worker is enabled, relevant configuration names, sanitized +configuration, reproduction steps, and expected versus actual behavior. +For worker problems, include registration/scope information and whether the +problem reproduces after a fresh registration in a test environment. + +Use React's `onEvent` or the vanilla client's `subscribeEvents` to identify the +failing phase. Record safe error codes and HTTP statuses, not complete event +payloads. Remove tokens, authorization headers, cookies, authorization codes, +PKCE verifiers, and personal data from screenshots, logs, or network exports +before posting an [issue](https://github.com/AxaFrance/oidc-client/issues). diff --git a/MIGRATION_GUIDE_V3_TO_V4.md b/MIGRATION_GUIDE_V3_TO_V4.md index dbf072330..91ecb47d7 100644 --- a/MIGRATION_GUIDE_V3_TO_V4.md +++ b/MIGRATION_GUIDE_V3_TO_V4.md @@ -18,24 +18,25 @@ Main provider component have been renamed ```javascript import { AuthenticationProvider } from '@axa-fr/react-oidc-context'; -// old v3 +// old v3 - - +; // in v4 becomes import { OidcProvider } from '@axa-fr/react-oidc-context'; // loggerLevel : Logger property has been removed in v4 - - +; ``` Provider properties have changed, you need to keep only required properties for v4 else it won't work. ```javascript -// old v3 +// old v3 const propTypes = { notAuthenticated: PropTypes.elementType, // react component displayed during authentication notAuthorized: PropTypes.elementType, // react component displayed in case user is not Authorised @@ -75,7 +76,7 @@ const propTypes = { UserStore: PropTypes.func, }; -// new v4 +// new v4 const propTypes = { loadingComponent: PropTypes.elementType, // you can inject your own loading component sessionLostComponent: PropTypes.elementType, // you can inject your own session lost component @@ -100,25 +101,27 @@ const propTypes = { Manage Oidc actions and information ```javascript - -// old v3 +// old v3 import { useReactOidc } from '@axa-fr/react-oidc-context'; -const { isEnabled, login, logout, oidcUser, events } = useReactOidc(); +const { isEnabled, login, logout, oidcUser, events } = useReactOidc(); - -// new v4 -import { useOidc, useOidcAccessToken, useOidcIdToken, useOidcUser } from '@axa-fr/react-oidc-context'; +// new v4 +import { + useOidc, + useOidcAccessToken, + useOidcIdToken, + useOidcUser, +} from '@axa-fr/react-oidc-context'; const { login, logout, isAuthenticated } = useOidc(); // login and logout return a Promise const { oidcUser, isOidcUserLoading } = useOidcUser(); // Return user_info endpoint data const { accessToken, accessTokenPayload } = useOidcAccessToken(); // Contain access_token metadata acess_token is a JWK const { idToken, idTokenPayload } = useOidcIdToken(); // contain IDToken metadata - ``` ```javascript -// old v3 +// old v3 import { withFetchRedirectionOn401, withFetchSilentAuthenticateAndRetryOn401, withFetchRedirectionOn403, @@ -160,7 +163,7 @@ Then edit `OidcTrustedDomains.js` in "public" folder for your need // OidcTrustedDomains.js // Add here trusted domains, access tokens will be send to const trustedDomains = { - default:["http://localhost:4200"], + default: ['http://localhost:4200'], }; ``` diff --git a/MIGRATION_GUIDE_V3_TO_V5.md b/MIGRATION_GUIDE_V3_TO_V5.md index f67cae5de..2e68c48bc 100644 --- a/MIGRATION_GUIDE_V3_TO_V5.md +++ b/MIGRATION_GUIDE_V3_TO_V5.md @@ -20,24 +20,25 @@ Main provider component have been renamed ```javascript import { AuthenticationProvider } from '@axa-fr/react-oidc-context'; -// old v3 +// old v3 - - +; // in v5 becomes import { OidcProvider } from '@axa-fr/react-oidc-context'; // loggerLevel : Logger property has been removed in v4 - - +; ``` Provider properties have changed, you need to keep only required properties for v4 else it won't work. ```javascript -// old v3 +// old v3 const propTypes = { notAuthenticated: PropTypes.elementType, // react component displayed during authentication notAuthorized: PropTypes.elementType, // react component displayed in case user is not Authorised @@ -103,16 +104,19 @@ const propTypes = { Manage Oidc actions and information ```javascript - -// old v3 +// old v3 import { useReactOidc } from '@axa-fr/react-oidc-context'; -const { isEnabled, login, logout, oidcUser, events } = useReactOidc(); - +const { isEnabled, login, logout, oidcUser, events } = useReactOidc(); // new v5 -import { useOidc, useOidcAccessToken, useOidcIdToken, useOidcUser } from '@axa-fr/react-oidc-context'; - -const { login, logout, isAuthenticated} = useOidc(); // login and logout return a Promise +import { + useOidc, + useOidcAccessToken, + useOidcIdToken, + useOidcUser, +} from '@axa-fr/react-oidc-context'; + +const { login, logout, isAuthenticated } = useOidc(); // login and logout return a Promise const { oidcUser, oidcUserLoadingState } = useOidcUser(); // Return user_info endpoint data const { accessToken, accessTokenPayload } = useOidcAccessToken(); // Contain access_token metadata acess_token is a JWK const { idToken, idTokenPayload } = useOidcIdToken(); // contain IDToken metadata @@ -120,7 +124,7 @@ const { idToken, idTokenPayload } = useOidcIdToken(); // contain IDToken metadat ```javascript -// old v3 +// old v3 import { withFetchRedirectionOn401, withFetchSilentAuthenticateAndRetryOn401, withFetchRedirectionOn403, @@ -136,7 +140,7 @@ import { withOidcFetch } from '@axa-fr/react-oidc-context'; // withFetchToken in v3 have been rename to withOidcFetch and set inside '@axa-fr/react-oidc-context' package withOidcFetch() - + ``` If you need a very secure mode where refresh_token and access_token will be hide behind a service worker that will proxify requests. @@ -162,7 +166,7 @@ Then edit `OidcTrustedDomains.js` in "public" folder for your need // OidcTrustedDomains.js // Add here trusted domains, access tokens will be send to const trustedDomains = { - default:["http://localhost:4200"], + default: ['http://localhost:4200'], }; ``` diff --git a/MIGRATION_GUIDE_V6_TO_V7.md b/MIGRATION_GUIDE_V6_TO_V7.md new file mode 100644 index 000000000..326686f77 --- /dev/null +++ b/MIGRATION_GUIDE_V6_TO_V7.md @@ -0,0 +1,18 @@ +# Migrating from v6 to v7 + +- Package `@axa-fr/vanilla-oidc` as been renamed to `@axa-fr/oidc-client` +- VanillaOidc class as been renamed to OidcClient +- On version 7.3.0 configuration.service_worker_convert_all_requests_to_cors as been moved to TrustedDomains.js + +```javascript +// Service worker will continue to give access token to the JavaScript client +// Ideal to hide refresh token from client JavaScript, but to retrieve access_token for some +// scenarios which require it. For example, to send it via websocket connection. +trustedDomains.config_show_access_token = { + domains: ['https://demo.duendesoftware.com'], + showAccessToken: true, + // convertAllRequestsToCorsExceptNavigate: false, // default value is false + // setAccessTokenToNavigateRequests: true, // default value is true + // bypassAllNonOidcRequests: false, // default value is false; when true, requests outside OIDC and accessTokenDomains are handled by the browser +}; +``` diff --git a/README.md b/README.md new file mode 100644 index 000000000..acf0dd7e6 --- /dev/null +++ b/README.md @@ -0,0 +1,173 @@ +# OIDC Client + +[![Continuous Integration](https://github.com/AxaFrance/oidc-client/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/AxaFrance/oidc-client/actions/workflows/npm-publish.yml) +[![npm version](https://img.shields.io/npm/v/@axa-fr/oidc-client)](https://www.npmjs.com/package/@axa-fr/oidc-client) +[![npm version](https://img.shields.io/npm/v/@axa-fr/react-oidc)](https://www.npmjs.com/package/@axa-fr/react-oidc) + +Add OpenID Connect (OIDC) sign-in to browser applications using the OAuth 2.0 +Authorization Code flow with PKCE. Use the framework-independent client directly, +or the React components and hooks. + +- [Choose a package](#choose-a-package) +- [Getting started](#getting-started) +- [How it works](#how-it-works) +- [Security and deployment](#security-and-deployment) +- [Run the demos](#run-the-demos) +- [FAQ](#faq) +- [Migrations](#migrations) +- [Contribute](#contribute) + +## Choose a package + +| Package | Use it for | Documentation | +| ------------------------------------ | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `@axa-fr/oidc-client` | Browser applications using any JavaScript framework, or no framework | [Installation, configuration, and API](./packages/oidc-client/README.md) | +| `@axa-fr/react-oidc` | React providers, protected components, authentication hooks, and authenticated fetch | [React quick start and recipes](./packages/react-oidc/README.md) | +| `@axa-fr/oidc-client-service-worker` | The worker used by both clients to isolate tokens and attach them to trusted requests | [Setup and deployment](./packages/oidc-client-service-worker/README.md) · [Protocol reference](./packages/oidc-client-service-worker/PROTOCOL.md) | + +Most applications install only one of the first two packages. The service worker +is included as a dependency; it does not replace the browser client. + +Features include automatic token renewal, named configurations for multiple +providers or scopes, optional service-worker token isolation, and support for +[DPoP](https://www.rfc-editor.org/rfc/rfc9449.html) and +[Pushed Authorization Requests (PAR)](./packages/oidc-client/README.md#pushed-authorization-requests-par) +when supported by your authorization server. + +## Getting started + +1. Register a **public browser client** with your OIDC provider. Enable the + Authorization Code flow with PKCE and register your exact callback and + post-logout URLs. Do not put a client secret in browser code. +2. Install the package for your application: + + ```sh + # Framework-independent applications + npm install @axa-fr/oidc-client + + # React applications — choose this instead + npm install @axa-fr/react-oidc + ``` + +3. Follow the [vanilla JavaScript quick start](./packages/oidc-client/README.md#getting-started) + or the [React quick start](./packages/react-oidc/README.md#getting-started). + Set your provider's `authority`, `client_id`, `redirect_uri`, and `scope`. +4. Choose whether to use the service worker. Its setup requires serving + `OidcServiceWorker.js` and configuring `OidcTrustedDomains.js`; installing the + npm package alone is not sufficient. + +Your provider must allow requests from your application origin to the endpoints +the browser calls, including the token endpoint. Request `offline_access` only +when your provider and client registration support refresh tokens. + +## How it works + +The client creates a PKCE challenge, redirects the browser to the provider, and +exchanges the returned authorization code for tokens. In service-worker mode, +the worker intercepts the token exchange and authenticated API requests: + +```mermaid +sequenceDiagram + actor User + participant App as Browser application + participant Worker as OIDC service worker + participant Provider as OIDC provider + participant API as Trusted API + User->>App: Select sign in + App->>Provider: Authorization request with PKCE challenge + Provider->>User: Authenticate and request consent + Provider-->>App: Redirect to callback with authorization code + App->>Worker: Exchange code with PKCE verifier + Worker->>Provider: Token request + Provider-->>Worker: Tokens + Worker-->>App: Token metadata and secured placeholders + App->>Worker: Request to a configured trusted API + Worker->>API: Request with access token + API-->>App: API response +``` + +With the default token-hiding settings, the real access and refresh tokens stay +in the worker; the application receives placeholders instead. Without the worker, +the client manages tokens in browser storage and the authenticated fetch wrapper +adds the access token to API requests. + +## Security and deployment + +- **Service-worker isolation is not an XSS or CSRF prevention mechanism.** Malicious + code running in the application can still make requests on the user's behalf. + Apply a restrictive Content Security Policy and normal input/output protections. +- Use HTTPS in production. Service workers require a secure context; localhost + is suitable for development. +- Restrict `OidcTrustedDomains.js` to the provider endpoints and API URLs that + should receive tokens. Review these rules whenever you add an API. +- Keep `OidcServiceWorker.js` aligned with the installed library version using the + copy command and `postinstall` instructions in the package guides. +- Decide explicitly whether to permit fallback to browser storage or require the + worker with `service_worker_only: true`. Browser storage is accessible to + same-origin JavaScript. +- Configure your host to serve the application at callback URLs. If you enable + silent sign-in, use a separate silent callback URL and account for browser + restrictions on third-party cookies. +- Protect APIs on the server: a client-side route guard is not an authorization + boundary. + +See the [FAQ](./FAQ.md) for deployment and security considerations. + +## Run the demos + +Try the hosted [React demo](https://black-rock-0dc6b0d03.1.azurestaticapps.net/) or +[vanilla JavaScript demo](https://icy-glacier-004ab4303.2.azurestaticapps.net/). +These are learning environments, not production security templates. + +To run locally, use a Node.js version supported by the root +[`package.json`](./package.json) and its pinned pnpm version. From the repository +root: + +```sh +corepack enable +pnpm install --frozen-lockfile +pnpm build +``` + +Then choose one command, also from the repository root: + +| Demo | Command | Guide | +| ------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| Vanilla JavaScript (Vite) | `pnpm --dir examples/oidc-client-demo start` | [Callbacks, session restoration, and token isolation](./examples/oidc-client-demo/README.md) | +| React (Vite) | `pnpm --dir examples/react-oidc-demo start` | [Hooks, protected components, API requests, and multiple configurations](./examples/react-oidc-demo/README.md) | +| Next.js (Pages Router) | `pnpm --dir examples/nextjs-demo dev` | [Browser authentication and custom history integration](./examples/nextjs-demo/README.md) | + +For Vite, open the URL printed in the terminal (normally `http://localhost:5173`; +another port is used if it is busy). The Next.js demo uses `http://localhost:3001`. +Register the actual origin and callback URLs with your provider before testing +sign-in. The demos use an external provider whose availability and registrations +are outside this repository's control. + +## FAQ + +Start with the [FAQ](./FAQ.md) for common integration questions, then consult the +package guides for configuration and error handling. To report a problem, open an +[issue](https://github.com/AxaFrance/oidc-client/issues) with a minimal reproduction, +browser and package versions, and sanitized configuration. Never include tokens +or credentials. + +## Migrations + +- [v3 to v4](./MIGRATION_GUIDE_V3_TO_V4.md) +- [v3 to v5](./MIGRATION_GUIDE_V3_TO_V5.md) +- [v4 to v5](./MIGRATION_GUIDE_V4_TO_V5.md) +- [v5 to v6](./MIGRATION_GUIDE_V5_TO_V6.md) +- [v6 to v7](./MIGRATION_GUIDE_V6_TO_V7.md) + +## Contribute + +Read the [contribution guide](./CONTRIBUTING.md) and +[code of conduct](./CODE_OF_CONDUCT.md). The workspace contains the client, React +bindings, service worker, and demo applications. + +```sh +pnpm lint-fix +pnpm lint +pnpm test:ci +pnpm build +``` diff --git a/__mocks__/fileMock.js b/__mocks__/fileMock.js deleted file mode 100644 index 86059f362..000000000 --- a/__mocks__/fileMock.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = 'test-file-stub'; diff --git a/__mocks__/styleMock.js b/__mocks__/styleMock.js deleted file mode 100644 index f053ebf79..000000000 --- a/__mocks__/styleMock.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/bin/generate-changelog.sh b/bin/generate-changelog.sh new file mode 100755 index 000000000..25084aaaf --- /dev/null +++ b/bin/generate-changelog.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +num_tags=60 +excluded_author="GitHub" +project_url="https://github.com/AxaFrance/oidc-client/commit" + +# Get all tag names in reverse order +tags=(`git tag -l --sort=-creatordate | head -$num_tags`) + +# File to save the log +outfile=CHANGELOG.md + +# Write the header +echo "# Changelog" > $outfile +echo "" >> $outfile + +# Iterate over tags array +for((i=0; i<${#tags[@]}-1; i++)) +do +# current tag +current=${tags[$i]} +# previous tag +previous=${tags[$i+1]} + +# Write header for current tag +echo "## $current" >> $outfile +echo "## $current" +echo "" >> $outfile + +# Get commit hashes: between current and previous tag +hashes=(`git log --pretty=format:"%H" $previous..$current`) + +# Output commit log for each hash +for hash in ${hashes[@]} +do + # Check the author of the commit + author=$(git log -1 --pretty=format:"%an" $hash) + + # Exclude commits from the specified author + if [ "$author" != "$excluded_author" ]; then + # Get commit log in the desired format. + # You can modify the 'format' as per your need. Please refer 'PRETTY FORMATS' section of git-log man page + log=$(git log -1 --pretty=format:"[%h]($project_url/%H) - %s, %ad by *%an*" --date=short $hash) + + # Write formatted log to CHANGELOG.md file + echo "- $log" >> $outfile + echo "- $log" + fi +done + +# Space between two tags +echo "" >> $outfile +echo "" >> $outfile +done diff --git a/changelog-template.hbs b/changelog-template.hbs deleted file mode 100644 index ff4d159ef..000000000 --- a/changelog-template.hbs +++ /dev/null @@ -1,27 +0,0 @@ -### Changelog -All notable changes to this project will be documented in this file. - -{{#each releases}} - {{#if href}} - {{#if major}} - ### [{{title}}]({{href}}) - {{else}} - #### [{{title}}]({{href}}) - {{/if}} - {{else}} - #### {{title}} - {{/if}} - {{#if tag}} - > {{niceDate}} - {{/if}} - {{#each merges}} - - {{message}}{{#if href}} [`#{{id}}`]({{href}}){{/if}} by {{author}} - {{/each}} - {{#each fixes}} - - {{commit.subject}}{{#each fixes}}{{#if href}} [`#{{id}}`]({{href}}) by {{author}}{{/if}}{{/each}} - {{/each}} - {{#each commits}} - - {{subject}}{{#if href}} [`{{shorthash}}`]({{href}}) by {{author}} {{/if}} - {{/each}} - -{{/each}} \ No newline at end of file diff --git a/config/defaultEslintConfig.cjs b/config/defaultEslintConfig.cjs deleted file mode 100644 index 712543dd7..000000000 --- a/config/defaultEslintConfig.cjs +++ /dev/null @@ -1,151 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - extends: [ - 'standard', - // 'plugin:react/recommended', - 'plugin:react-hooks/recommended', - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended', - // 'plugin:import/errors', - //'plugin:import/warnings', - 'plugin:import/typescript', - 'plugin:jsx-a11y/recommended', - ], - plugins: ['simple-import-sort', 'testing-library'], - env: { - node: true, - es6: true, - browser: true, - }, - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - // typescript-eslint specific options - warnOnUnsupportedTypeScriptVersion: true, - }, - rules: { - // '@typescript-eslint/indent': ['error', 2], - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': [ - 'error', - { - argsIgnorePattern: '^_|req|res|next|err|ctx|args|context|info', - ignoreRestSiblings: true, - }, - ], - 'no-array-constructor': 'off', - '@typescript-eslint/no-array-constructor': 'warn', - 'no-redeclare': 'off', - '@typescript-eslint/no-redeclare': 'warn', - 'no-use-before-define': 'off', - '@typescript-eslint/no-use-before-define': [ - 'warn', - { - functions: false, - classes: false, - variables: false, - typedefs: false, - }, - ], - 'no-unused-expressions': 'off', - '@typescript-eslint/no-unused-expressions': [ - 'error', - { - allowShortCircuit: true, - allowTernary: true, - allowTaggedTemplates: true, - }, - ], - '@typescript-eslint/triple-slash-reference': 'off', - '@typescript-eslint/member-delimiter-style': [ - 'error', - { - multiline: { - delimiter: 'semi', - requireLast: true, - }, - singleline: { - delimiter: 'semi', - requireLast: false, - }, - }, - ], - camelcase: 'off', - 'comma-dangle': [ - 'error', - { - arrays: 'always-multiline', - objects: 'always-multiline', - imports: 'always-multiline', - exports: 'always-multiline', - functions: 'always-multiline', - }, - ], - 'array-callback-return': 'warn', - 'jsx-quotes': ['error', 'prefer-double'], - // 'max-len': ['error', { code: 120 }], - indent: 'off', - // quotes: ['error', 'single'], - semi: ['error', 'always'], - 'space-before-function-paren': 'off', - - 'import/no-named-as-default': 'off', - 'import/no-named-as-default-member': 'off', - 'import/default': 'off', - 'import/named': 'off', - 'import/namespace': 'off', - 'import/no-unresolved': 'off', - 'simple-import-sort/imports': 'error', - 'simple-import-sort/exports': 'error', - 'react/prop-types': 'off', - 'react/jsx-wrap-multilines': 'error', - 'react/react-in-jsx-scope': 'off', - 'react/display-name': 'off', - // https://github.com/facebook/react/tree/master/packages/eslint-plugin-react-hooks - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'off', - }, - - overrides: [ - { - files: ['*.js', '*.jsx'], - rules: { - '@typescript-eslint/no-var-requires': 'off', - }, - }, - { - // 3) Now we enable eslint-plugin-testing-library rules or preset only for matching files! - files: ['**/?(*.)+(spec|test).[jt]s?(x)'], - extends: ['plugin:testing-library/react'], - rules: { - 'testing-library/await-async-query': 'error', - 'testing-library/no-await-sync-query': 'error', - 'testing-library/no-debugging-utils': 'warn', - 'testing-library/no-dom-import': 'off', - 'testing-library/no-unnecessary-act': 'off', - }, - }, - ], - - settings: { - react: { - version: 'detect', - }, - 'import/parsers': { - '@typescript-eslint/parser': ['.ts', '.tsx'], - }, - 'import/resolver': { - typescript: { - alwaysTryTypes: true, - }, - }, - }, - }; \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..719d3b183 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,220 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { fixupConfigRules, fixupPluginRules } from '@eslint/compat'; +import { FlatCompat } from '@eslint/eslintrc'; +import js from '@eslint/js'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import prettier from 'eslint-plugin-prettier/recommended'; +import react from 'eslint-plugin-react'; +import simpleImportSort from 'eslint-plugin-simple-import-sort'; +//import testingLibrary from "eslint-plugin-testing-library"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + { + ignores: [ + '**/*.d.ts', + 'packages/**/dist/**/*', + 'packages/**/public/**/*', + 'packages/**/coverage/**/*', + 'packages/**/fixtures/**/*', + '**/node_modules', + 'public/**/*', + 'examples/**/static/**/*', + 'examples/**/dist/**/*', + 'examples/**/OidcTrustedDomains.js', + 'examples/**/OidcServiceWorker.js', + 'examples/**/nextjs-demo/**', + 'scripts/**/*', + '**/.github', + '**/.changeset', + '**/vite.config.js', + '**/webpack-runtime.js', + '.prettierrc.cjs', + ], + }, + ...fixupConfigRules( + compat.extends( + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'eslint:recommended', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:import/typescript', + 'plugin:jsx-a11y/recommended', + ), + ), + { + plugins: { + '@typescript-eslint': fixupPluginRules(typescriptEslint), + 'simple-import-sort': simpleImportSort, + //"testing-library": testingLibrary, //Not compatible with ESLint9 yet https://github.com/jsx-eslint/eslint-plugin-react/issues/3699 + react: fixupPluginRules(react), + }, + languageOptions: { + parser: tsParser, + ecmaVersion: 2022, + sourceType: 'module', + + parserOptions: { + project: ['./tsconfig.eslint.json', './packages/*/tsconfig.eslint.json'], + + ecmaFeatures: { + jsx: true, + }, + + warnOnUnsupportedTypeScriptVersion: true, + }, + }, + + settings: { + env: { + browser: true, + node: true, + }, + react: { + version: '18', + }, + + 'import/resolver': { + typescript: { + alwaysTryTypes: true, + }, + }, + }, + + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + 'no-unused-vars': 'off', + + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_|req|res|next|err|ctx|args|context|info|index|data', + ignoreRestSiblings: true, + }, + ], + + 'no-array-constructor': 'off', + '@typescript-eslint/no-array-constructor': 'warn', + 'no-redeclare': 'off', + '@typescript-eslint/no-redeclare': 'warn', + 'no-use-before-define': 'off', + + '@typescript-eslint/no-use-before-define': [ + 'warn', + { + functions: false, + classes: false, + variables: false, + typedefs: false, + }, + ], + + 'no-unused-expressions': 'off', + + '@typescript-eslint/no-unused-expressions': [ + 'error', + { + allowShortCircuit: true, + allowTernary: true, + allowTaggedTemplates: true, + }, + ], + + '@typescript-eslint/triple-slash-reference': 'off', + + '@typescript-eslint/member-delimiter-style': [ + 'error', + { + multiline: { + delimiter: 'semi', + requireLast: true, + }, + + singleline: { + delimiter: 'semi', + requireLast: false, + }, + }, + ], + + camelcase: 'off', + + 'comma-dangle': [ + 'error', + { + arrays: 'always-multiline', + objects: 'always-multiline', + imports: 'always-multiline', + exports: 'always-multiline', + functions: 'always-multiline', + }, + ], + + 'array-callback-return': 'warn', + 'jsx-quotes': ['error', 'prefer-double'], + indent: 'off', + semi: ['error', 'always'], + 'space-before-function-paren': 'off', + 'import/no-named-as-default': 'off', + 'import/no-named-as-default-member': 'off', + 'import/default': 'off', + 'import/named': 'off', + 'import/namespace': 'off', + 'import/no-unresolved': 'off', + 'simple-import-sort/imports': 'error', + 'simple-import-sort/exports': 'error', + 'react/prop-types': 'off', + 'react/jsx-wrap-multilines': 'error', + 'react/react-in-jsx-scope': 'off', + 'react/display-name': 'off', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'off', + }, + }, + { + files: ['**/*.js', '**/*.jsx'], + + rules: { + '@typescript-eslint/no-var-requires': 'off', + + 'react/no-unknown-property': [ + 2, + { + ignore: ['jsx', 'global'], + }, + ], + }, + }, + prettier, + //Not compatible with ESLint9 yet https://github.com/jsx-eslint/eslint-plugin-react/issues/3699 + // ...compat.extends("plugin:testing-library/react").map(config => ({ + // ...config, + // files: ["**/?(*.)+(spec|test).[jt]s?(x)"], + // })), { + // files: ["**/?(*.)+(spec|test).[jt]s?(x)"], + + // rules: { + // "testing-library/no-container": "off" + // "testing-library/await-async-query": "error", + // "testing-library/no-await-sync-query": "error", + // testing-library/no-debugging-utils": "off", + // "testing-library/no-dom-import": "off", + // "testing-library/no-unnecessary-act": "off", + // }, + // } +]; diff --git a/examples/nextjs-demo/README.md b/examples/nextjs-demo/README.md index ee14292a3..18048e8c8 100644 --- a/examples/nextjs-demo/README.md +++ b/examples/nextjs-demo/README.md @@ -1,54 +1,92 @@ -# Nextjs @axa-fr/react-oidc demo +# Next.js OIDC demo + +This example integrates `@axa-fr/react-oidc` with the Next.js **Pages Router**. +Authentication runs in the browser. It does **not** create a server-side session, +protect server-rendered data, or demonstrate the App Router. + +For hooks and components, see the +[React package documentation](../../packages/react-oidc/README.md). +For workspace setup and other examples, see the [repository README](../../README.md). + +## Run locally + +Run these commands from the **repository root**, using the Node.js version required +by the [root package manifest](../../package.json): -## Getting Started ```sh -git clone https://github.com/AxaGuilDEv/react-oidc.git -cd react-oidc/packages/nextjs-demo -npm install -npm run dev +corepack enable +pnpm install --frozen-lockfile +pnpm build +pnpm --dir examples/nextjs-demo dev ``` -To work with NextJS you need to inject your own history surcharge like the sample below. - -component/layout.js -```javascript -import { OidcProvider } from '@axa-fr/react-oidc'; -import { useRouter } from 'next/router' - -const configuration = { - client_id: 'interactive.public.short', - redirect_uri: 'http://localhost:3001/#authentication/callback', - silent_redirect_uri: 'http://localhost:3001/#authentication/silent-callback', // Optional activate silent-signin that use cookies between OIDC server and client javascript to restore the session - scope: 'openid profile email api offline_access', - authority: 'https://demo.duendesoftware.com' -}; - -const onEvent=(configurationName, eventName, data )=>{ - console.log(`oidc:${configurationName}:${eventName}`, data); -} - -export default function Layout({ children }) { - const router = useRouter() - const withCustomHistory= () => { - return { - replaceState: (url) => { - router.replace({ - pathname: url, - }).then(() => { - window.dispatchEvent(new Event('popstate')); - }); - } - }; - }; - - return ( - <> - -
{children}
-
- - ) -} - - -``` \ No newline at end of file +Open **http://localhost:3001**. The `dev` script explicitly selects port `3001`, +and the OIDC callback configuration uses that same origin. + +## Configuration and provider requirements + +[`components/layout.js`](components/layout.js) contains the OIDC configuration. +It uses the external [Duende demo provider](https://demo.duendesoftware.com), +client ID `interactive.public`, and scopes `openid profile email api offline_access`. +Use that provider's published sign-in options; this repository does not manage its +accounts, availability, or client registrations. + +The current callback URLs are fixed strings, not derived from the browser origin: + +| Purpose | Configured URL | +| --------------- | ------------------------------------------------------- | +| Login callback | `http://localhost:3001/#authentication/callback` | +| Silent callback | `http://localhost:3001/#authentication/silent-callback` | + +If you change the host, port, or deployment URL, update the configuration and +provider registration together. OAuth redirect URIs must not contain fragments; +these legacy hash-based callbacks may be rejected by your provider. For a new +integration, use registered path-based callbacks and provide the corresponding +Next.js routes. + +For your own provider, register a public browser client supporting Authorization +Code with PKCE, enable the intended scopes and refresh-token support, and allow +the browser origin in its CORS settings. Register any post-logout return URLs you +use. Do not place confidential client secrets in this browser configuration. +Silent sign-in depends on provider session cookies and browser cookie policies. + +## How the integration works + +1. [`pages/_app.js`](pages/_app.js) dynamically loads the layout with `ssr: false`. + The layout wraps the page in `OidcProvider`. +2. [`pages/index.js`](pages/index.js) also loads the home component with + `ssr: false`, keeping the demonstration's authentication UI in the browser. +3. The layout passes `withCustomHistory` to `OidcProvider`. Its `replaceState` + implementation calls `router.replace({ pathname: url })` and dispatches a + `popstate` event **after the replacement completes**. This connects the + library's callback navigation to the Next.js router. +4. [`components/home.js`](components/home.js) wraps the profile in `OidcSecure` + and reads access-token, ID-token, and user information through React hooks. + +Use the source files above as the maintained example rather than copying a +separate layout implementation from this README. + +## Walkthrough + +1. Open the home page. `OidcSecure` starts authentication when no session exists; + there is no separate login button on this page. +2. Sign in at the provider and return to the application. +3. Inspect the access-token, ID-token, and user-information cards. Browser console + messages from `onEvent` show authentication lifecycle events. +4. Reload to observe session restoration. If the callback fails, check the exact + callback URL, the provider registration, browser cookie restrictions, and the + console/network logs. + +## Security boundaries + +Although installation copies worker assets into [`public`](public), the layout +does not configure `service_worker_relative_url`: **service worker token isolation +is not enabled in this demo**. Tokens are accessible to browser JavaScript and +displayed on the page. Copying worker files alone does not enable protection; see +the [service worker documentation](../../packages/oidc-client-service-worker/README.md) +for configuration and trusted-domain requirements. + +This is an educational integration, not a production authentication template. +Remove token displays and sensitive debug logging before adapting it. Enforce +authorization independently on APIs and server resources; a browser-side +`OidcSecure` boundary is not server-side access control. diff --git a/examples/nextjs-demo/components/home.js b/examples/nextjs-demo/components/home.js new file mode 100644 index 000000000..441d43a25 --- /dev/null +++ b/examples/nextjs-demo/components/home.js @@ -0,0 +1,280 @@ +import { + OidcSecure, + OidcUserStatus, + useOidc, + useOidcAccessToken, + useOidcIdToken, + useOidcUser, +} from '@axa-fr/react-oidc'; +import Head from 'next/head'; + +const isBrowser = () => typeof window !== 'undefined'; + +const DisplayUserInfo = () => { + const { oidcUser, oidcUserLoadingState } = useOidcUser(); + const { isAuthenticated } = useOidc(); + console.log( + 'isBrowser: ' + isBrowser() + ', isAuthenticated: ' + isAuthenticated + ', oidcUser: ', + ); + console.log(oidcUser); + + switch (oidcUserLoadingState) { + case OidcUserStatus.Loading: + return

User Information are loading

; + case OidcUserStatus.Unauthenticated: + return

you are not authenticated

; + case OidcUserStatus.LoadingError: + return

Fail to load user information

; + default: + return ( +
+
+
User information
+

{JSON.stringify(oidcUser)}

+
+
+ ); + } +}; + +export const Profile = () => { + return ( +
+ + + +
+ ); +}; + +const DisplayAccessToken = () => { + const { accessToken, accessTokenPayload } = useOidcAccessToken(); + + if (!accessToken) { + return

you are not authenticated

; + } + return ( +
+
+
Access Token
+

+ Please consider to configure the ServiceWorker in order to protect your application from + XSRF attacks. "access_token" and "refresh_token" will never be + accessible from your client side javascript. +

+ {

Access Token: {JSON.stringify(accessToken)}

} + {accessTokenPayload != null && ( +

Access Token Payload: {JSON.stringify(accessTokenPayload)}

+ )} +
+
+ ); +}; + +const DisplayIdToken = () => { + const { idToken, idTokenPayload } = useOidcIdToken(); + + if (!idToken) { + return

you are not authenticated

; + } + + return ( +
+
+
ID Token
+ {

IdToken: {JSON.stringify(idToken)}

} + {idTokenPayload != null && ( +

IdToken Payload: {JSON.stringify(idTokenPayload)}

+ )} +
+
+ ); +}; + +export default function Home() { + return ( +
+ + Create Next App + + + +
+

+ Welcome to{' '} + Next.js @axa-fr/react-oidc demo! +

+ + + +
+ +
+ + + + +
+ ); +} + diff --git a/examples/nextjs-demo/components/layout.js b/examples/nextjs-demo/components/layout.js index ca6a0aa79..c9363367d 100644 --- a/examples/nextjs-demo/components/layout.js +++ b/examples/nextjs-demo/components/layout.js @@ -1,37 +1,47 @@ import { OidcProvider } from '@axa-fr/react-oidc'; -import { useRouter } from 'next/router' +import { useRouter } from 'next/router'; +import React from 'react'; const configuration = { - client_id: 'interactive.public.short', + client_id: 'interactive.public', redirect_uri: 'http://localhost:3001/#authentication/callback', silent_redirect_uri: 'http://localhost:3001/#authentication/silent-callback', // Optional activate silent-signin that use cookies between OIDC server and client javascript to restore the session scope: 'openid profile email api offline_access', - authority: 'https://demo.duendesoftware.com' + authority: 'https://demo.duendesoftware.com', + par: 'auto', + preload_user_info: 'true', }; -const onEvent=(configurationName, eventName, data )=>{ - console.log(`oidc:${configurationName}:${eventName}`, data); - } +const onEvent = (configurationName, eventName, data) => { + // eslint-disable-next-line no-undef + console.log(`oidc:${configurationName}:${eventName}`, data); +}; export default function Layout({ children }) { - const router = useRouter() - const withCustomHistory= () => { - return { - replaceState: (url) => { - router.replace({ - pathname: url, - }).then(() => { - window.dispatchEvent(new Event('popstate')); - } - ) - } - }; + const router = useRouter(); + const withCustomHistory = () => { + return { + replaceState: url => { + router + .replace({ + pathname: url, + }) + .then(() => { + // eslint-disable-next-line no-undef + window.dispatchEvent(new Event('popstate')); + }); + }, }; + }; return ( <> - -
{children}
+ +
{children}
- ) -} \ No newline at end of file + ); +} diff --git a/examples/nextjs-demo/package-lock.json b/examples/nextjs-demo/package-lock.json deleted file mode 100644 index 211d03362..000000000 --- a/examples/nextjs-demo/package-lock.json +++ /dev/null @@ -1,611 +0,0 @@ -{ - "name": "nextjs-demo", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "dependencies": { - "@axa-fr/react-oidc": "6.22.15", - "next": "latest", - "react": "latest", - "react-dom": "latest" - } - }, - "node_modules/@axa-fr/react-oidc": { - "version": "6.22.15", - "resolved": "https://registry.npmjs.org/@axa-fr/react-oidc/-/react-oidc-6.22.15.tgz", - "integrity": "sha512-6/CpYONlBRFyJ8HW98dWkMX8LpdOD6EjGB2PvgL3p1K2fyPk/MVQKTsYGUBWnJILddxsyioqys3kWYprc4Cujw==", - "hasInstallScript": true, - "dependencies": { - "base64-js": "1.5.1" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - } - }, - "node_modules/@next/env": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-12.1.6.tgz", - "integrity": "sha512-Te/OBDXFSodPU6jlXYPAXpmZr/AkG6DCATAxttQxqOWaq6eDFX25Db3dK0120GZrSZmv4QCe9KsZmJKDbWs4OA==" - }, - "node_modules/@next/swc-android-arm-eabi": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.1.6.tgz", - "integrity": "sha512-BxBr3QAAAXWgk/K7EedvzxJr2dE014mghBSA9iOEAv0bMgF+MRq4PoASjuHi15M2zfowpcRG8XQhMFtxftCleQ==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-android-arm64": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.1.6.tgz", - "integrity": "sha512-EboEk3ROYY7U6WA2RrMt/cXXMokUTXXfnxe2+CU+DOahvbrO8QSWhlBl9I9ZbFzJx28AGB9Yo3oQHCvph/4Lew==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.6.tgz", - "integrity": "sha512-P0EXU12BMSdNj1F7vdkP/VrYDuCNwBExtRPDYawgSUakzi6qP0iKJpya2BuLvNzXx+XPU49GFuDC5X+SvY0mOw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.6.tgz", - "integrity": "sha512-9FptMnbgHJK3dRDzfTpexs9S2hGpzOQxSQbe8omz6Pcl7rnEp9x4uSEKY51ho85JCjL4d0tDLBcXEJZKKLzxNg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm-gnueabihf": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.6.tgz", - "integrity": "sha512-PvfEa1RR55dsik/IDkCKSFkk6ODNGJqPY3ysVUZqmnWMDSuqFtf7BPWHFa/53znpvVB5XaJ5Z1/6aR5CTIqxPw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.6.tgz", - "integrity": "sha512-53QOvX1jBbC2ctnmWHyRhMajGq7QZfl974WYlwclXarVV418X7ed7o/EzGY+YVAEKzIVaAB9JFFWGXn8WWo0gQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.6.tgz", - "integrity": "sha512-CMWAkYqfGdQCS+uuMA1A2UhOfcUYeoqnTW7msLr2RyYAys15pD960hlDfq7QAi8BCAKk0sQ2rjsl0iqMyziohQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.6.tgz", - "integrity": "sha512-AC7jE4Fxpn0s3ujngClIDTiEM/CQiB2N2vkcyWWn6734AmGT03Duq6RYtPMymFobDdAtZGFZd5nR95WjPzbZAQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.6.tgz", - "integrity": "sha512-c9Vjmi0EVk0Kou2qbrynskVarnFwfYIi+wKufR9Ad7/IKKuP6aEhOdZiIIdKsYWRtK2IWRF3h3YmdnEa2WLUag==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.6.tgz", - "integrity": "sha512-3UTOL/5XZSKFelM7qN0it35o3Cegm6LsyuERR3/OoqEExyj3aCk7F025b54/707HTMAnjlvQK3DzLhPu/xxO4g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.6.tgz", - "integrity": "sha512-8ZWoj6nCq6fI1yCzKq6oK0jE6Mxlz4MrEsRyu0TwDztWQWe7rh4XXGLAa2YVPatYcHhMcUL+fQQbqd1MsgaSDA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.6.tgz", - "integrity": "sha512-4ZEwiRuZEicXhXqmhw3+de8Z4EpOLQj/gp+D9fFWo6ii6W1kBkNNvvEx4A90ugppu+74pT1lIJnOuz3A9oQeJA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "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/caniuse-lite": { - "version": "1.0.30001359", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001359.tgz", - "integrity": "sha512-Xln/BAsPzEuiVLgJ2/45IaqD9jShtk3Y33anKb4+yLwQzws3+v6odKfpgES/cDEaZMLzSChpIGdbOYtH9MyuHw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - } - ] - }, - "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/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/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/next": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/next/-/next-12.1.6.tgz", - "integrity": "sha512-cebwKxL3/DhNKfg9tPZDQmbRKjueqykHHbgaoG4VBRH3AHQJ2HO0dbKFiS1hPhe1/qgc2d/hFeadsbPicmLD+A==", - "dependencies": { - "@next/env": "12.1.6", - "caniuse-lite": "^1.0.30001332", - "postcss": "8.4.5", - "styled-jsx": "5.0.2" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=12.22.0" - }, - "optionalDependencies": { - "@next/swc-android-arm-eabi": "12.1.6", - "@next/swc-android-arm64": "12.1.6", - "@next/swc-darwin-arm64": "12.1.6", - "@next/swc-darwin-x64": "12.1.6", - "@next/swc-linux-arm-gnueabihf": "12.1.6", - "@next/swc-linux-arm64-gnu": "12.1.6", - "@next/swc-linux-arm64-musl": "12.1.6", - "@next/swc-linux-x64-gnu": "12.1.6", - "@next/swc-linux-x64-musl": "12.1.6", - "@next/swc-win32-arm64-msvc": "12.1.6", - "@next/swc-win32-ia32-msvc": "12.1.6", - "@next/swc-win32-x64-msvc": "12.1.6" - }, - "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^6.0.0 || ^7.0.0", - "react": "^17.0.2 || ^18.0.0-0", - "react-dom": "^17.0.2 || ^18.0.0-0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "fibers": { - "optional": true - }, - "node-sass": { - "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==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "node_modules/postcss": { - "version": "8.4.5", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", - "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==", - "dependencies": { - "nanoid": "^3.1.30", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" - }, - "peerDependencies": { - "react": "17.0.2" - } - }, - "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.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/styled-jsx": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.2.tgz", - "integrity": "sha512-LqPQrbBh3egD57NBcHET4qcgshPks+yblyhPlH2GY8oaDgKs8SK4C3dBh3oSJjgzJ3G5t1SYEZGHkP+QEpX9EQ==", - "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 - } - } - } - }, - "dependencies": { - "@axa-fr/react-oidc": { - "version": "6.22.15", - "resolved": "https://registry.npmjs.org/@axa-fr/react-oidc/-/react-oidc-6.22.15.tgz", - "integrity": "sha512-6/CpYONlBRFyJ8HW98dWkMX8LpdOD6EjGB2PvgL3p1K2fyPk/MVQKTsYGUBWnJILddxsyioqys3kWYprc4Cujw==", - "requires": { - "base64-js": "1.5.1" - } - }, - "@next/env": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-12.1.6.tgz", - "integrity": "sha512-Te/OBDXFSodPU6jlXYPAXpmZr/AkG6DCATAxttQxqOWaq6eDFX25Db3dK0120GZrSZmv4QCe9KsZmJKDbWs4OA==" - }, - "@next/swc-android-arm-eabi": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.1.6.tgz", - "integrity": "sha512-BxBr3QAAAXWgk/K7EedvzxJr2dE014mghBSA9iOEAv0bMgF+MRq4PoASjuHi15M2zfowpcRG8XQhMFtxftCleQ==", - "optional": true - }, - "@next/swc-android-arm64": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.1.6.tgz", - "integrity": "sha512-EboEk3ROYY7U6WA2RrMt/cXXMokUTXXfnxe2+CU+DOahvbrO8QSWhlBl9I9ZbFzJx28AGB9Yo3oQHCvph/4Lew==", - "optional": true - }, - "@next/swc-darwin-arm64": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.6.tgz", - "integrity": "sha512-P0EXU12BMSdNj1F7vdkP/VrYDuCNwBExtRPDYawgSUakzi6qP0iKJpya2BuLvNzXx+XPU49GFuDC5X+SvY0mOw==", - "optional": true - }, - "@next/swc-darwin-x64": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.6.tgz", - "integrity": "sha512-9FptMnbgHJK3dRDzfTpexs9S2hGpzOQxSQbe8omz6Pcl7rnEp9x4uSEKY51ho85JCjL4d0tDLBcXEJZKKLzxNg==", - "optional": true - }, - "@next/swc-linux-arm-gnueabihf": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.6.tgz", - "integrity": "sha512-PvfEa1RR55dsik/IDkCKSFkk6ODNGJqPY3ysVUZqmnWMDSuqFtf7BPWHFa/53znpvVB5XaJ5Z1/6aR5CTIqxPw==", - "optional": true - }, - "@next/swc-linux-arm64-gnu": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.6.tgz", - "integrity": "sha512-53QOvX1jBbC2ctnmWHyRhMajGq7QZfl974WYlwclXarVV418X7ed7o/EzGY+YVAEKzIVaAB9JFFWGXn8WWo0gQ==", - "optional": true - }, - "@next/swc-linux-arm64-musl": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.6.tgz", - "integrity": "sha512-CMWAkYqfGdQCS+uuMA1A2UhOfcUYeoqnTW7msLr2RyYAys15pD960hlDfq7QAi8BCAKk0sQ2rjsl0iqMyziohQ==", - "optional": true - }, - "@next/swc-linux-x64-gnu": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.6.tgz", - "integrity": "sha512-AC7jE4Fxpn0s3ujngClIDTiEM/CQiB2N2vkcyWWn6734AmGT03Duq6RYtPMymFobDdAtZGFZd5nR95WjPzbZAQ==", - "optional": true - }, - "@next/swc-linux-x64-musl": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.6.tgz", - "integrity": "sha512-c9Vjmi0EVk0Kou2qbrynskVarnFwfYIi+wKufR9Ad7/IKKuP6aEhOdZiIIdKsYWRtK2IWRF3h3YmdnEa2WLUag==", - "optional": true - }, - "@next/swc-win32-arm64-msvc": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.6.tgz", - "integrity": "sha512-3UTOL/5XZSKFelM7qN0it35o3Cegm6LsyuERR3/OoqEExyj3aCk7F025b54/707HTMAnjlvQK3DzLhPu/xxO4g==", - "optional": true - }, - "@next/swc-win32-ia32-msvc": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.6.tgz", - "integrity": "sha512-8ZWoj6nCq6fI1yCzKq6oK0jE6Mxlz4MrEsRyu0TwDztWQWe7rh4XXGLAa2YVPatYcHhMcUL+fQQbqd1MsgaSDA==", - "optional": true - }, - "@next/swc-win32-x64-msvc": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.6.tgz", - "integrity": "sha512-4ZEwiRuZEicXhXqmhw3+de8Z4EpOLQj/gp+D9fFWo6ii6W1kBkNNvvEx4A90ugppu+74pT1lIJnOuz3A9oQeJA==", - "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==" - }, - "caniuse-lite": { - "version": "1.0.30001359", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001359.tgz", - "integrity": "sha512-Xln/BAsPzEuiVLgJ2/45IaqD9jShtk3Y33anKb4+yLwQzws3+v6odKfpgES/cDEaZMLzSChpIGdbOYtH9MyuHw==" - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" - }, - "next": { - "version": "12.1.6", - "resolved": "https://registry.npmjs.org/next/-/next-12.1.6.tgz", - "integrity": "sha512-cebwKxL3/DhNKfg9tPZDQmbRKjueqykHHbgaoG4VBRH3AHQJ2HO0dbKFiS1hPhe1/qgc2d/hFeadsbPicmLD+A==", - "requires": { - "@next/env": "12.1.6", - "@next/swc-android-arm-eabi": "12.1.6", - "@next/swc-android-arm64": "12.1.6", - "@next/swc-darwin-arm64": "12.1.6", - "@next/swc-darwin-x64": "12.1.6", - "@next/swc-linux-arm-gnueabihf": "12.1.6", - "@next/swc-linux-arm64-gnu": "12.1.6", - "@next/swc-linux-arm64-musl": "12.1.6", - "@next/swc-linux-x64-gnu": "12.1.6", - "@next/swc-linux-x64-musl": "12.1.6", - "@next/swc-win32-arm64-msvc": "12.1.6", - "@next/swc-win32-ia32-msvc": "12.1.6", - "@next/swc-win32-x64-msvc": "12.1.6", - "caniuse-lite": "^1.0.30001332", - "postcss": "8.4.5", - "styled-jsx": "5.0.2" - } - }, - "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==" - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "postcss": { - "version": "8.4.5", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", - "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==", - "requires": { - "nanoid": "^3.1.30", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.1" - } - }, - "react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" - } - }, - "scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.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==" - }, - "styled-jsx": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.2.tgz", - "integrity": "sha512-LqPQrbBh3egD57NBcHET4qcgshPks+yblyhPlH2GY8oaDgKs8SK4C3dBh3oSJjgzJ3G5t1SYEZGHkP+QEpX9EQ==", - "requires": {} - } - } -} diff --git a/examples/nextjs-demo/package.json b/examples/nextjs-demo/package.json index 4ba56d7f6..e4c0b3ffe 100644 --- a/examples/nextjs-demo/package.json +++ b/examples/nextjs-demo/package.json @@ -3,12 +3,14 @@ "scripts": { "dev": "next dev -p 3001", "build": "next build", - "start": "next start" + "start": "next start", + "postinstall": "node ./node_modules/@axa-fr/react-oidc/bin/copy-service-worker-files.mjs public" }, "dependencies": { "@axa-fr/react-oidc": "workspace:*", - "next": "latest", - "react": "latest", - "react-dom": "latest" + "next": "^16.3.5", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "styled-jsx": "^5.1.7" } } diff --git a/examples/nextjs-demo/pages/_app.js b/examples/nextjs-demo/pages/_app.js index 09bfde03a..680bef9ce 100644 --- a/examples/nextjs-demo/pages/_app.js +++ b/examples/nextjs-demo/pages/_app.js @@ -1,9 +1,11 @@ -import Layout from '../components/layout' +import dynamic from 'next/dynamic'; + +const Layout = dynamic(() => import('../components/layout'), { ssr: false }); export default function MyApp({ Component, pageProps }) { return ( - ) -} \ No newline at end of file + ); +} diff --git a/examples/nextjs-demo/pages/index.js b/examples/nextjs-demo/pages/index.js index 44b8015e2..035b89697 100644 --- a/examples/nextjs-demo/pages/index.js +++ b/examples/nextjs-demo/pages/index.js @@ -1,256 +1,5 @@ -import Head from 'next/head' -import { OidcSecure, useOidcAccessToken, useOidcIdToken, useOidcUser, OidcUserStatus} from '@axa-fr/react-oidc'; +import dynamic from 'next/dynamic'; +const Home = dynamic(() => import('../components/home'), { ssr: false }); -const DisplayUserInfo = () => { - const{ oidcUser, oidcUserLoadingState } = useOidcUser(); - - switch (oidcUserLoadingState){ - case OidcUserStatus.Loading: - return

User Information are loading

; - case OidcUserStatus.Unauthenticated: - return

you are not authenticated

; - case OidcUserStatus.LoadingError: - return

Fail to load user information

; - default: - return ( -
-
-
User information
-

{JSON.stringify(oidcUser)}

-
-
- ); - } -}; - -export const Profile = () => { - - return ( -
- - - -
- ); -} - -const DisplayAccessToken = () => { - const{ accessToken, accessTokenPayload } = useOidcAccessToken(); - - if(!accessToken){ - return

you are not authenticated

- } - return ( -
-
-
Access Token
-

Please consider to configure the ServiceWorker in order to protect your application from XSRF attacks. "access_token" and "refresh_token" will never be accessible from your client side javascript.

- {

Access Token: {JSON.stringify(accessToken)}

} - {accessTokenPayload != null &&

Access Token Payload: {JSON.stringify(accessTokenPayload)}

} -
-
- ) -}; - - -const DisplayIdToken =() => { - const{ idToken, idTokenPayload } = useOidcIdToken(); - - if(!idToken){ - return

you are not authenticated

- } - - return ( -
-
-
ID Token
- {

IdToken: {JSON.stringify(idToken)}

} - {idTokenPayload != null &&

IdToken Payload: {JSON.stringify(idTokenPayload)}

} -
-
- ); -} - -export default function Home({}) { - - - return ( -
- - Create Next App - - - -
- -

- Welcome to Next.js @axa-fr/react-oidc demo! -

- - - -
- -
- -
- - - - -
- ) -} - -import Layout from '../components/layout'; - -Home.getLayout = function getLayout(page) { - return ( - - {page} - - ) -} \ No newline at end of file +export default Home; diff --git a/examples/nextjs-demo/public/OidcTrustedDomains.js b/examples/nextjs-demo/public/OidcTrustedDomains.js new file mode 100644 index 000000000..e72a3d143 --- /dev/null +++ b/examples/nextjs-demo/public/OidcTrustedDomains.js @@ -0,0 +1,29 @@ +// Add bellow trusted domains, access tokens will automatically injected to be send to +// trusted domain can also be a path like https://www.myapi.com/users, +// then all subroute like https://www.myapi.com/useers/1 will be authorized to send access_token to. + +// Domains used by OIDC server must be also declared here +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const trustedDomains = { + default: ['https://demo.duendesoftware.com', 'https://kdhttps.auth0.com'], + config_classic: ['https://demo.duendesoftware.com'], + config_without_silent_login: ['https://demo.duendesoftware.com'], + config_without_refresh_token: ['https://demo.duendesoftware.com'], + config_without_refresh_token_silent_login: ['https://demo.duendesoftware.com'], + config_google: ['https://oauth2.googleapis.com', 'https://openidconnect.googleapis.com'], + config_with_hash: ['https://demo.duendesoftware.com'], +}; + +// Service worker will continue to give access token to the JavaScript client +// Ideal to hide refresh token from client JavaScript, but to retrieve access_token for some +// scenarios which require it. For example, to send it via websocket connection. +trustedDomains.config_show_access_token = { + domains: ['https://demo.duendesoftware.com'], + showAccessToken: true, +}; + +// This example defines domains used by OIDC server separately from domains to which access tokens will be injected. +trustedDomains.config_separate_oidc_access_token_domains = { + oidcDomains: ['https://demo.duendesoftware.com'], + accessTokenDomains: ['https://myapi'], +}; diff --git a/examples/oidc-client-demo/README.md b/examples/oidc-client-demo/README.md index b87cb0044..96270e86d 100644 --- a/examples/oidc-client-demo/README.md +++ b/examples/oidc-client-demo/README.md @@ -1,46 +1,89 @@ -# Getting Started with Create React App - -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). - -## Available Scripts - -In the project directory, you can run: - -### `npm start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - -The page will reload if you make edits.\ -You will also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `npm run build` - -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `npm run eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). +# Vanilla OIDC client demo + +This Vite application demonstrates `@axa-fr/oidc-client` without a UI framework. +It creates an OIDC client, starts login, processes the callback, restores an existing +session, and signs out. The implementation is in [`src/index.tsx`](src/index.tsx). + +For the library API and authentication flow, see the +[OIDC client documentation](../../packages/oidc-client/README.md). +For workspace requirements and other examples, see the [repository README](../../README.md). + +## Run locally + +Run these commands from the **repository root**, using the Node.js version required +by the [root package manifest](../../package.json): + +```sh +corepack enable +pnpm install --frozen-lockfile +pnpm build +pnpm --dir examples/oidc-client-demo start +``` + +Open the URL printed by Vite, normally `http://localhost:5173`. No port is fixed in +[`vite.config.js`](vite.config.js); if that port is busy, Vite may choose another. +The origin you use must match the identity provider's client registration. + +## Configuration and callbacks + +The `configuration` object in [`src/index.tsx`](src/index.tsx) uses the external +[Duende demo provider](https://demo.duendesoftware.com) with client ID +`interactive.public.short` and scopes `openid profile email api offline_access`. +Use the sign-in options published by that provider; this repository does not +manage its accounts, availability, or allowed redirect URLs. + +The callback URLs are built from `window.location.origin`. At the usual Vite +origin, they are: + +| Purpose | Configured URL | +| --------------- | -------------------------------------------------------- | +| Login callback | `http://localhost:5173/#/authentication/callback` | +| Silent callback | `http://localhost:5173/#/authentication/silent-callback` | + +These are the demo's existing hash-based URLs, not a recommendation for a new +client registration. OAuth redirect URIs must not contain fragments, and a +provider may reject this pattern. For a provider that requires path-based +callbacks, update both the configuration and callback handling before registering +the URLs. + +When using your own provider: + +- Register a public browser client using Authorization Code with PKCE, not a + confidential client requiring a secret in browser code. +- Set `authority`, `client_id`, scopes, and callbacks to match that registration. + Allow the browser origin through the provider's CORS settings and register any + post-logout return URLs you use. +- Enable refresh tokens and the relevant scopes if you want to test renewal with + `offline_access`. Silent sign-in also depends on provider session cookies and + browser cookie restrictions. +- Update [`public/OidcTrustedDomains.js`](public/OidcTrustedDomains.js) to allow + only the OIDC endpoints and API destinations you trust. + +This demo sets `service_worker_relative_url` to `/OidcServiceWorker.js` and +`service_worker_only` to `true`: it requires service worker support and a secure +context, such as HTTPS or localhost. See the +[service worker documentation](../../packages/oidc-client-service-worker/README.md) +before changing its trust rules. + +## Walkthrough + +1. Open the home page and select **Login**. The client redirects to the provider. +2. Complete the provider's sign-in flow. The callback calls + `loginCallbackAsync()`, returns to `/`, and displays the client token object. + In service worker mode, protected token values are represented by placeholders, + not the underlying access or refresh tokens. +3. Reload the page to exercise `tryKeepExistingSessionAsync()`. +4. Select **Logout** to call `logoutAsync()`. +5. Use browser developer tools to inspect redirects, the worker registration, and + network requests. If login fails, first check the actual origin, registered + callback URLs, provider availability, and worker trust configuration. + +## Educational code, not a production template + +The **Game Hack Challenge** deliberately evaluates text entered in a textarea. +The development CSP allows `'unsafe-eval'` for that exercise. Run only code you +understand, locally and with test accounts; do not paste untrusted scripts. + +Do not copy the evaluator, permissive CSP, or token display into a production +application. A service worker can reduce token exposure, but it does not eliminate +XSS or prevent malicious page code from making authenticated requests. diff --git a/examples/oidc-client-demo/index.html b/examples/oidc-client-demo/index.html index d0912cccb..c6cff7e96 100644 --- a/examples/oidc-client-demo/index.html +++ b/examples/oidc-client-demo/index.html @@ -1,4 +1,4 @@ - + @@ -9,7 +9,13 @@ +

@axa-de/oidc-client

+

+ OpenId Connect, OIDC client is free under licence MIT. Available on + github axa oidc-client +

- +
+ diff --git a/examples/oidc-client-demo/package.json b/examples/oidc-client-demo/package.json index bfc877e94..2a4eb2bad 100644 --- a/examples/oidc-client-demo/package.json +++ b/examples/oidc-client-demo/package.json @@ -3,18 +3,16 @@ "version": "1.0.0", "private": true, "dependencies": { - "@axa-fr/vanilla-oidc": "workspace:~", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/user-event": "^13.5.0", - "@types/jest": "^27.5.2", - "typescript": "^5.1.6", - "web-vitals": "^3.4.0" + "@axa-fr/oidc-client": "workspace:~", + "typescript": "npm:@typescript/typescript6@6.0.2", + "web-vitals": "6.2.1" }, "scripts": { "start": "vite", "build": "vite build", "serve": "vite preview", - "clean": "rimraf dist" + "clean": "rimraf dist", + "postinstall": "node ./node_modules/@axa-fr/oidc-client/bin/copy-service-worker-files.mjs public" }, "browserslist": { "production": [ @@ -29,8 +27,8 @@ ] }, "devDependencies": { - "@types/node": "^18.11.9", - "cross-env": "^7.0.3", - "vite": "^4.4.4" + "@types/node": "26.5.1", + "cross-env": "^10.1.0", + "vite": "8.3.0" } } diff --git a/examples/oidc-client-demo/public/OidcTrustedDomains.js b/examples/oidc-client-demo/public/OidcTrustedDomains.js index c1b0f6fc3..656ff28f4 100644 --- a/examples/oidc-client-demo/public/OidcTrustedDomains.js +++ b/examples/oidc-client-demo/public/OidcTrustedDomains.js @@ -4,21 +4,29 @@ // Domains used by OIDC server must be also declared here // eslint-disable-next-line @typescript-eslint/no-unused-vars const trustedDomains = { - default: ['https://demo.duendesoftware.com', 'https://kdhttps.auth0.com'], - config_classic: ['https://demo.duendesoftware.com'], - config_without_silent_login: ['https://demo.duendesoftware.com'], - config_without_refresh_token: ['https://demo.duendesoftware.com'], - config_without_refresh_token_silent_login: ['https://demo.duendesoftware.com'], - config_google: ['https://oauth2.googleapis.com', 'https://openidconnect.googleapis.com'], - config_with_hash: ['https://demo.duendesoftware.com'], + default: ['https://demo.duendesoftware.com', 'https://kdhttps.auth0.com'], + config_classic: ['https://demo.duendesoftware.com'], + config_with_monitor_session: ['https://demo.duendesoftware.com'], + config_without_silent_login: ['https://demo.duendesoftware.com'], + config_without_refresh_token: ['https://demo.duendesoftware.com'], + config_without_refresh_token_silent_login: ['https://demo.duendesoftware.com'], + config_google: [ + 'https://oauth2.googleapis.com', + 'https://openidconnect.googleapis.com', + 'https://accounts.google.com', + ], + config_with_hash: ['https://demo.duendesoftware.com'], }; // Service worker will continue to give access token to the JavaScript client // Ideal to hide refresh token from client JavaScript, but to retrieve access_token for some // scenarios which require it. For example, to send it via websocket connection. -trustedDomains.config_show_access_token = { domains: ["https://demo.duendesoftware.com"], showAccessToken: true }; +trustedDomains.config_show_access_token = { + domains: ['https://demo.duendesoftware.com'], + showAccessToken: true, +}; // This example defines domains used by OIDC server separately from domains to which access tokens will be injected. trustedDomains.config_separate_oidc_access_token_domains = { - oidcDomains: ["https://demo.duendesoftware.com"], - accessTokenDomains: ["https://myapi"] + oidcDomains: ['https://demo.duendesoftware.com'], + accessTokenDomains: ['https://myapi'], }; -//# sourceMappingURL=OidcTrustedDomains.js.map \ No newline at end of file +//# sourceMappingURL=OidcTrustedDomains.js.map diff --git a/examples/oidc-client-demo/public/staticwebapp.config.json b/examples/oidc-client-demo/public/staticwebapp.config.json new file mode 100644 index 000000000..ee582c8f9 --- /dev/null +++ b/examples/oidc-client-demo/public/staticwebapp.config.json @@ -0,0 +1,15 @@ +{ + "navigationFallback": { + "rewrite": "index.html", + "exclude": ["*.{svg,png,jpg,gif}", "*.{css,scss}", "*.js"] + }, + "globalHeaders": { + "content-security-policy": "script-src 'self' 'unsafe-eval'", + "Access-Control-Allow-Origin": "*", + "X-Frame-Options": "SAMEORIGIN", + "X-Permitted-Cross-Domain-Policies": "none", + "Referrer-Policy": "no-referrer", + "X-Content-Type-Options": "nosniff", + "Permissions-Policy": "autoplay=()" + } +} diff --git a/examples/oidc-client-demo/src/index.tsx b/examples/oidc-client-demo/src/index.tsx index 664937d13..8b0da2d93 100644 --- a/examples/oidc-client-demo/src/index.tsx +++ b/examples/oidc-client-demo/src/index.tsx @@ -1,151 +1,195 @@ -import { VanillaOidc } from '@axa-fr/vanilla-oidc'; -class Router -{ getCustomHistory(){ - const generateKey = () => - Math.random() - .toString(36) - .substr(2, 6); - - // Exported only for test - type WindowInternal = Window & { - CustomEvent?: new (typeArg: string, eventInitDict?: CustomEventInit) => CustomEvent; - Event: typeof Event; - }; - - type IPrototype = { - prototype: any; - }; - - type InitCustomEventParams = { - bubbles: boolean; - cancelable: boolean; - detail: T; - }; - - // IE Polyfill for CustomEvent - const CreateEvent = (windowInternal: Window, documentInternal: Document) => ( - event: string, - params: InitCustomEventParams, - ): CustomEvent => { - // @ts-ignore - if (typeof windowInternal.CustomEvent === 'function') { - // @ts-ignore - return new windowInternal.CustomEvent(event, params); - } - const paramsToFunction = params || { bubbles: false, cancelable: false, detail: undefined }; - const evt: CustomEvent = documentInternal.createEvent('CustomEvent'); - evt.initCustomEvent(event, paramsToFunction.bubbles, paramsToFunction.cancelable, paramsToFunction.detail); - // @ts-ignore - (evt as CustomEvent & IPrototype).prototype = windowInternal.Event.prototype; - return evt; - }; - - type WindowHistoryState = typeof window.history.state; - - type CustomHistory = { - replaceState(url?: string | null, stateHistory?: WindowHistoryState): void; +import { OidcClient } from '@axa-fr/oidc-client'; + +class Router { + getCustomHistory() { + const generateKey = () => Math.random().toString(36).substr(2, 6); + + // Exported only for test + type WindowInternal = Window & { + CustomEvent?: new (typeArg: string, eventInitDict?: CustomEventInit) => CustomEvent; + Event: typeof Event; + }; + + type IPrototype = { + prototype: any; + }; + + type InitCustomEventParams = { + bubbles: boolean; + cancelable: boolean; + detail: T; + }; + + // IE Polyfill for CustomEvent + const CreateEvent = + (windowInternal: Window, documentInternal: Document) => + (event: string, params: InitCustomEventParams): CustomEvent => { + // @ts-ignore + if (typeof windowInternal.CustomEvent === 'function') { + // @ts-ignore + return new windowInternal.CustomEvent(event, params); } - - const getHistory = ( - windowInternal: WindowInternal, - CreateEventInternal: (event: string, params?: InitCustomEventParams) => CustomEvent, - generateKeyInternal: typeof generateKey, - ): CustomHistory => { - return { - replaceState: (url?: string | null, stateHistory?: WindowHistoryState): void => { - const key = generateKeyInternal(); - const state = stateHistory || windowInternal.history.state; - // @ts-ignore - windowInternal.history.replaceState({ key, state }, null, url); - windowInternal.dispatchEvent(CreateEventInternal('popstate')); - }, - }; - }; - + const paramsToFunction = params || { bubbles: false, cancelable: false, detail: undefined }; + const evt: CustomEvent = documentInternal.createEvent('CustomEvent'); + evt.initCustomEvent( + event, + paramsToFunction.bubbles, + paramsToFunction.cancelable, + paramsToFunction.detail, + ); // @ts-ignore + (evt as CustomEvent & IPrototype).prototype = windowInternal.Event.prototype; + return evt; + }; + + type WindowHistoryState = typeof window.history.state; + + type CustomHistory = { + replaceState(url?: string | null, stateHistory?: WindowHistoryState): void; + }; + + const getHistory = ( + windowInternal: WindowInternal, + CreateEventInternal: (event: string, params?: InitCustomEventParams) => CustomEvent, + generateKeyInternal: typeof generateKey, + ): CustomHistory => { + return { + replaceState: (url?: string | null, stateHistory?: WindowHistoryState): void => { + const key = generateKeyInternal(); + const state = stateHistory || windowInternal.history.state; + // @ts-ignore + windowInternal.history.replaceState({ key, state }, null, url); + windowInternal.dispatchEvent(CreateEventInternal('popstate')); + }, + }; + }; + + // @ts-ignore const getCustomHistory = () => getHistory(window, CreateEvent(window, document), generateKey); - return getCustomHistory(); - } - + return getCustomHistory(); + } } -const router = new Router(); +const display = (element: any) => { + // @ts-ignore + element.innerHTML = `
+

Game Hack Challenge

+ +

Game, let's try to make an XSS attacks to retrieve some secure tokens !

+

Service Worker mode is not magic https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps#payload-new-flow + So let try to hack it ! +

+

Service Worker mode is secure if your follow 2 following rules:

+

Rule 1: Configure CSP

+

+ Add CSP header to forbid to write dynamic iframe with javascript dynamic inside. + You should never add "unsafe-inline" in your CSP header. +

+            Content-Security-Policy: script-src 'self';  // Secure
+            
+

Rule 2: Apply redirect URI before any WebService call

+ Set up the redirect_uri and redirect_silent_uri at the top level of your javascript application before any XSS attack could be executed. +

+

Let's play

+

To help you for this game, we set up 'unsafe-eval' in the CSP header to allow the eval function to be executed and allow you to hack the application like a big XSS attack.

+
+            Content-Security-Policy: script-src 'self' 'unsafe-eval'; 
+            
+ + + + +
`; + // @ts-ignore + window.document.getElementById('buttonxsshack').addEventListener('click', () => { + // @ts-ignore + eval(document.getElementById('xsshack').value); + }); +}; + +// @ts-ignore +export const execute = () => { + const router = new Router(); -document.body.innerHTML = `
`; -const element = document.getElementById("my-vanilla-app"); + const root = document.getElementById('root'); + const game = document.getElementById('game'); -export const configuration = { + const configuration = { client_id: 'interactive.public.short', redirect_uri: window.location.origin + '/#/authentication/callback', silent_redirect_uri: window.location.origin + '/#/authentication/silent-callback', scope: 'openid profile email api offline_access', authority: 'https://demo.duendesoftware.com', + par: 'auto' as const, refresh_time_before_tokens_expiration_in_second: 40, - service_worker_relative_url:'/OidcServiceWorker.js', - service_worker_only: false, - // monitor_session: true, -}; + service_worker_relative_url: '/OidcServiceWorker.js', + service_worker_only: true, + }; -const href = window.location.href; + const href = window.location.href; -const vanillaOidc = VanillaOidc.getOrCreate(() => fetch)(configuration); + const vanillaOidc = OidcClient.getOrCreate(() => fetch)(configuration); -console.log(href); + // @ts-ignore + function logout() { + vanillaOidc.logoutAsync(); + } + console.log(href); -vanillaOidc.tryKeepExistingSessionAsync().then(() => { - if(href.includes(configuration.redirect_uri)){ - // @ts-ignore - element.innerHTML = `
-

@axa-fr/vanilla-oidc demo

+ if (href.includes(configuration.redirect_uri)) { + // @ts-ignore + root.innerHTML = `
+

Login demo

Loading callback

`; - vanillaOidc.loginCallbackAsync().then(()=>{ - router.getCustomHistory().replaceState("/"); - // @ts-ignore - window.logout = () => vanillaOidc.logoutAsync(); - let tokens = vanillaOidc.tokens; - // @ts-ignore - element.innerHTML = `
-

@axa-fr/vanilla-oidc demo

- -

Authenticated

-
${JSON.stringify(tokens,null,'\t')}
-
` - }); - return - } - - let tokens = vanillaOidc.tokens; - - if(tokens){ - - // @ts-ignore - window.logout = () => vanillaOidc.logoutAsync(); - // @ts-ignore - element.innerHTML = `
-

@axa-fr/vanilla-oidc demo

- -

Authenticated

-
${JSON.stringify(tokens,null,'\t')}
-
` - - } - else { + vanillaOidc.loginCallbackAsync().then(() => { + router.getCustomHistory().replaceState('/'); + display(game); + // @ts-ignore + root.innerHTML = `
+

Login demo Authenticated

+ +
${JSON.stringify(vanillaOidc.tokens, null, '\t')}
+
`; + // @ts-ignore + window.document.getElementById('logout').addEventListener('click', logout); + }); + return; + } + + vanillaOidc.tryKeepExistingSessionAsync().then(() => { + const tokens = vanillaOidc.tokens; + if (tokens) { + display(game); + // @ts-ignore + root.innerHTML = `
+

Login demo Authenticated

+ +
${JSON.stringify(tokens, null, '\t')}
+
`; + // @ts-ignore + window.document.getElementById('logout').addEventListener('click', logout); + } else { + // @ts-ignore + function login() { // @ts-ignore - window.login= () => { - // @ts-ignore - element.innerHTML = `
-

@axa-fr/vanilla-oidc demo

+ root.innerHTML = `
+

Login demo

Loading

`; - vanillaOidc.loginAsync("/") - }; - // @ts-ignore - element.innerHTML = `
-

@axa-fr/vanilla-oidc demo

- -
` + vanillaOidc.loginAsync('/'); + } + display(game); + // @ts-ignore + root.innerHTML = `
+

Login demo

+ +
`; + // @ts-ignore + document.getElementById('login').addEventListener('click', login); } -}) + }); +}; +execute(); diff --git a/examples/oidc-client-demo/tsconfig.json b/examples/oidc-client-demo/tsconfig.json index a273b0cfc..4a009f409 100644 --- a/examples/oidc-client-demo/tsconfig.json +++ b/examples/oidc-client-demo/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { - "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "target": "ES2019", + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -14,13 +10,11 @@ "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, - "include": [ - "src" - ] + "include": ["src"] } diff --git a/examples/oidc-client-demo/vite.config.js b/examples/oidc-client-demo/vite.config.js index 755ce8f3c..ddb44ece7 100644 --- a/examples/oidc-client-demo/vite.config.js +++ b/examples/oidc-client-demo/vite.config.js @@ -4,5 +4,10 @@ export default defineConfig({ build: { sourcemap: true, minify: false, - } + }, + server: { + headers: { + 'Content-Security-Policy': "script-src 'self' 'unsafe-eval';", + }, + }, }); diff --git a/examples/react-oidc-demo/.eslintrc.cjs b/examples/react-oidc-demo/.eslintrc.cjs deleted file mode 100644 index 968c0320f..000000000 --- a/examples/react-oidc-demo/.eslintrc.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - extends: [__dirname+'/config/defaultEslintConfig.cjs'], - parserOptions: { - project: './tsconfig.eslint.json', - tsconfigRootDir: __dirname, - }, - rules: { - '@typescript-eslint/naming-convention': [ - 'error', - { - 'selector': 'variable', - 'types': ['boolean'], - 'format': ['PascalCase'], - 'prefix': ['is', 'with', 'should', 'has', 'can', 'did', 'will'] - } - ] - } - } \ No newline at end of file diff --git a/examples/react-oidc-demo/README.md b/examples/react-oidc-demo/README.md index 102b48cfa..e555c634a 100644 --- a/examples/react-oidc-demo/README.md +++ b/examples/react-oidc-demo/README.md @@ -1,635 +1,125 @@ -# @axa-fr/react-oidc +# React OIDC demo -[![Continuous Integration](https://github.com/AxaGuilDEv/react-oidc/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/AxaGuilDEv/react-oidc/actions/workflows/npm-publish.yml) -[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=AxaGuilDEv_react-oidc&metric=alert_status)](https://sonarcloud.io/dashboard?id=AxaGuilDEv_react-oidc) [![Reliability](https://sonarcloud.io/api/project_badges/measure?project=AxaGuilDEv_react-oidc&metric=reliability_rating)](https://sonarcloud.io/component_measures?id=AxaGuilDEv_react-oidc&metric=reliability_rating) [![Security](https://sonarcloud.io/api/project_badges/measure?project=AxaGuilDEv_react-oidc&metric=security_rating)](https://sonarcloud.io/component_measures?id=AxaGuilDEv_react-oidc&metric=security_rating) [![Code Corevage](https://sonarcloud.io/api/project_badges/measure?project=AxaGuilDEv_react-oidc&metric=coverage)](https://sonarcloud.io/component_measures?id=AxaGuilDEv_react-oidc&metric=Coverage) [![Twitter](https://img.shields.io/twitter/follow/GuildDEvOpen?style=social)](https://twitter.com/intent/follow?screen_name=GuildDEvOpen) +This Vite application demonstrates `@axa-fr/react-oidc` with React Router: +login and logout, protected components, authenticated requests, token renewal, +service worker options, and multiple named OIDC configurations. -Try the demo at https://black-rock-0dc6b0d03.1.azurestaticapps.net/ +This README is a walkthrough of the demo. For component and hook usage, see the +[React package documentation](../../packages/react-oidc/README.md). +For workspace requirements and the authentication flow, see the +[repository README](../../README.md). -![Sample React OIDC](https://github.com/AxaGuilDEv/react-oidc/blob/master/docs/img/introduction.gif?raw=true) +## Run locally -A set of react components to make OIDC (OpenID Connect) client easy. It aim to simplify OAuth authentication between multiples providers. - -- [About](#about) -- [Getting Started](#getting-started) -- [Run The Demo](#run-the-demo) -- [Examples](#examples) -- [How It Works](#how-it-works) -- [NextJS](#NextJS) -- [Hash route](#Hash-route) -- [Service Worker Support](#service-worker-support) - -## About - -Easy set up of OIDC for react. -It is a real alternative to existing oidc-client libraries. - -- **Secure** : - - With the use of Service Worker, your tokens (refresh_token and access_token) are not accessible to the JavaScript client code (big protection against XSRF attacks) - - OIDC using client side Code Credential Grant with PKCE only -- **Lightweight** -- **Simple** : - - refresh_token and access_token are auto refreshed in background - - with the use of the Service Worker, you do not need to inject the access_token in every fetch, you have only to configure `OidcTrustedDomains.js` file -- **No cookies problem** : You can disable silent signin (that internally use an iframe). For your information, your OIDC server should be in the same domain of your website in order to be able to send OIDC server cookies from your website via an internal IFRAME, else, you may encounter COOKIES problem. -- **Multiple Authentication** : - - You can authenticate many times to the same provider with different scope (for example you can acquire a new 'payment' scope for a payment) - - You can authenticate to multiple different providers inside the same SPA (single page application) website -- **Flexible** : - - Work with Service Worker (more secure) and without for older browser (less secure) - -![](https://github.com/AxaGuilDEv/react-oidc/blob/master/docs/img/schema_pcke_client_side_with_service_worker.png?raw=true) - -The service worker catch **access_token** and **refresh_token** that will never be accessible to the client. - -## Getting Started +Run these commands from the **repository root**, using the Node.js version required +by the [root package manifest](../../package.json): ```sh -npm install @axa-fr/react-oidc --save - -# If you have a "public" folder, the 2 files will be created : -# ./public/OidcServiceWorker.js <-- will be updated at each "npm install" -# ./public/OidcTrustedDomains.js <-- won't be updated if already exist -``` - -If you need a very secure mode where refresh_token and access_token will be hide behind a service worker that will proxify requests. -The only file you should edit is "OidcTrustedDomains.js". - -```javascript -// OidcTrustedDomains.js - -// Add bellow trusted domains, access tokens will automatically injected to be send to -// trusted domain can also be a path like https://www.myapi.com/users, -// then all subroute like https://www.myapi.com/useers/1 will be authorized to send access_token to. - -// Domains used by OIDC server must be also declared here -const trustedDomains = { - default: ["https://demo.duendesoftware.com", "https://www.myapi.com/users"], -}; - -// Service worker will continue to give access token to the JavaScript client -// Ideal to hide refresh token from client JavaScript, but to retrieve access_token for some -// scenarios which require it. For example, to send it via websocket connection. -trustedDomains.config_show_access_token = { domains : ["https://demo.duendesoftware.com"], showAccessToken: true }; - -``` - -## Run The Demo - -```sh -git clone https://github.com/AxaGuilDEv/react-oidc.git -cd react-oidc/packages/react -npm install -npm start -# then navigate to http://localhost:4200 -``` - -## Examples - -### Application startup - -The library is router agnostic and use native History API. - -The default routes used internally : - -- www.your-app.fr/authentication/callback - -```javascript -import React from "react"; -import { render } from "react-dom"; -import { BrowserRouter as Router } from "react-router-dom"; -import { OidcProvider } from "@axa-fr/react-oidc"; -import Header from "./Layout/Header"; -import Routes from "./Router"; - -// This configuration use hybrid mode -// ServiceWorker are used if available (more secure) else tokens are given to the client -// You need to give inside your code the "access_token" when using fetch -const configuration = { - client_id: "interactive.public.short", - redirect_uri: window.location.origin + "/authentication/callback", - silent_redirect_uri: - window.location.origin + "/authentication/silent-callback", - scope: "openid profile email api offline_access", // offline_access scope allow your client to retrieve the refresh_token - authority: "https://demo.duendesoftware.com", - service_worker_relative_url: "/OidcServiceWorker.js", - service_worker_only: false, -}; - -const App = () => ( - - -
- - - -); - -render(, document.getElementById("root")); -``` - -```javascript -const propTypes = { - loadingComponent: PropTypes.elementType, // you can inject your own loading component - sessionLostComponent: PropTypes.elementType, // you can inject your own session lost component - authenticating: PropTypes.elementType, // you can inject your own authenticationg component - authenticatingErrorComponent: PropTypes.elementType, - callbackSuccessComponent: PropTypes.elementType, // you can inject your own call back success component - serviceWorkerNotSupportedComponent: PropTypes.elementType, // you can inject your page that explain your require a more modern browser - onSessionLost: PropTypes.function, // If set "sessionLostComponent" is not displayed and onSessionLost callback is called instead - configuration: PropTypes.shape({ - client_id: PropTypes.string.isRequired, // oidc client id - redirect_uri: PropTypes.string.isRequired, // oidc redirect url - silent_redirect_uri: PropTypes.string, // Optional activate silent-signin that use cookies between OIDC server and client javascript to restore sessions - silent_login_uri: PropTypes.string, // Optional, route that trigger the signin - silent_login_timeout: PropTypes.number, // Optional default is 12000 milliseconds - scope: PropTypes.string.isRequired, // oidc scope (you need to set "offline_access") - authority: PropTypes.string.isRequired, - storage: Storage, // Default sessionStorage, you can set localStorage but it is less secure to XSS attacks - authority_configuration: PropTypes.shape({ - // Optional for providers that does not implement OIDC server auto discovery via a .wellknowurl - authorization_endpoint: PropTypes.string, - token_endpoint: PropTypes.string, - userinfo_endpoint: PropTypes.string, - end_session_endpoint: PropTypes.string, - revocation_endpoint: PropTypes.string, - check_session_iframe: PropTypes.string, - issuer: PropTypes.string, - }), - refresh_time_before_tokens_expiration_in_second: PropTypes.number, // default is 120 seconds - service_worker_relative_url: PropTypes.string, - service_worker_only: PropTypes.boolean, // default false - service_worker_convert_all_requests_to_cors: PropTypes.boolean, // force all requests that servie worker upgrades to have 'cors' mode. This allows setting authentication token on requests initialted by html parsing(e.g. img tags, download links etc). - extras: StringMap | undefined, // ex: {'prompt': 'consent', 'access_type': 'offline'} list of key/value that are send to the oidc server (more info: https://github.com/openid/AppAuth-JS) - token_request_extras: StringMap | undefined, // ex: {'prompt': 'consent', 'access_type': 'offline'} list of key/value that are send to the oidc server during token request (more info: https://github.com/openid/AppAuth-JS) - withCustomHistory: PropTypes.function, // Override history modification, return instance with replaceState(url, stateHistory) implemented (like History.replaceState()) - authority_time_cache_wellknowurl_in_second: 60 * 60, // Time to cache in second of openid wellknowurl, default is 1 hour - authority_timeout_wellknowurl_in_millisecond: 10000, // Timeout in millisecond of openid wellknowurl, default is 10 seconds, then error is throwed - monitor_session: PropTypes.boolean, // Add OpenId monitor session, default is false (more information https://openid.net/specs/openid-connect-session-1_0.html), if you need to set it to true consider https://infi.nl/nieuws/spa-necromancy/ - onLogoutFromAnotherTab: Function, // Optional, can be set to override the default behavior, this function is triggered when user with the same subject is logged out from another tab when session_monitor is active - onLogoutFromSameTab: Function, // Optional, can be set to override the default behavior, this function is triggered when user is logged out from same tab when session_monitor is active - token_renew_mode: PropTypes.string, // Optional, update tokens base on the selected token(s) lifetime: "access_token_or_id_token_invalid" (default), "access_token_invalid" , "id_token_invalid" - logout_tokens_to_invalidate : Array // Optional tokens to invalidate during logout, default: ['access_token', 'refresh_token'] - }).isRequired, -}; -``` - -## How to consume - -"useOidc" returns all props from the Hook : - -```javascript -import React from "react"; -import { useOidc } from "./oidc"; - -export const Home = () => { - const { login, logout, renewTokens, isAuthenticated } = useOidc(); - - return ( -
-
-
-
Welcome !!!
-

- React Demo Application protected by OpenId Connect -

- {!isAuthenticated && ( - - )} - {isAuthenticated && ( - - )} - {isAuthenticated && ( - - )} -
-
-
- ); -}; -``` - -The Hook method exposes : - -- isAuthenticated : if the user is logged in or not -- logout: logout function (return a promise) -- login: login function 'return a promise' -- renewTokens: renew tokens function 'return a promise' - -## How to secure a component - -`OidcSecure` component trigger authentication in case user is not authenticated. So, the children of that component can be accessible only once you are connected. - -```javascript -import React from "react"; -import { OidcSecure } from "@axa-fr/react-oidc"; - -const AdminSecure = () => ( - -

My sub component

} -
-); - -// adding the oidc user in the props -export default AdminSecure; -``` - -## How to secure a component : HOC method - -"withOidcSecure" act the same as "OidcSecure" it also trigger authentication in case user is not authenticated. - -```javascript -import React from "react"; -import { Switch, Route } from "react-router-dom"; -import { withOidcSecure } from "@axa-fr/react-oidc"; -import Home from "../Pages/Home"; -import Dashboard from "../Pages/Dashboard"; -import Admin from "../Pages/Admin"; - -const Routes = () => ( - - - - - - -); - -export default Routes; -``` - -## How to get "Access Token" : Hook method - -```javascript -import { useOidcAccessToken } from "@axa-fr/react-oidc"; - -const DisplayAccessToken = () => { - const { accessToken, accessTokenPayload } = useOidcAccessToken(); - - if (!accessToken) { - return

you are not authentified

; - } - return ( -
-
-
Access Token
-

- Please consider to configure the ServiceWorker in order to protect - your application from XSRF attacks. ""access_token" and - "refresh_token" will never be accessible from your client side - javascript. -

- {

{JSON.stringify(accessToken)}

} - {accessTokenPayload != null && ( -

{JSON.stringify(accessTokenPayload)}

- )} -
-
- ); -}; -``` - -## How to get IDToken : Hook method - -```javascript -import { useOidcIdToken } from "@axa-fr/react-oidc"; - -const DisplayIdToken = () => { - const { idToken, idTokenPayload } = useOidcIdToken(); - - if (!idToken) { - return

you are not authentified

; - } - - return ( -
-
-
ID Token
- {

{JSON.stringify(idToken)}

} - {idTokenPayload != null && ( -

{JSON.stringify(idTokenPayload)}

- )} -
-
- ); -}; -``` - -## How to get User Information : Hook method - -```javascript -import { useOidcUser, UserStatus } from "@axa-fr/react-oidc"; - -const DisplayUserInfo = () => { - const { oidcUser, oidcUserLoadingState } = useOidcUser(); - - switch (oidcUserLoadingState) { - case UserStatus.Loading: - return

User Information are loading

; - case UserStatus.Unauthenticated: - return

you are not authenticated

; - case UserStatus.LoadingError: - return

Fail to load user information

; - default: - return ( -
-
-
User information
-

{JSON.stringify(oidcUser)}

-
-
- ); - } -}; -``` - -## How to get a fetch that inject Access_Token : Hook method - -If your are not using the service worker. Fetch function need to send AccessToken. -This Hook give you a wrapped fetch that add the access token for you. - -```javascript -import React, { useEffect, useState } from "react"; -import { useOidcFetch, OidcSecure } from "@axa-fr/react-oidc"; - -const DisplayUserInfo = ({ fetch }) => { - const [oidcUser, setOidcUser] = useState(null); - const [isLoading, setLoading] = useState(true); - - useEffect(() => { - const fetchUserInfoAsync = async () => { - const res = await fetch( - "https://demo.duendesoftware.com/connect/userinfo", - ); - if (res.status != 200) { - return null; - } - return res.json(); - }; - let isMounted = true; - fetchUserInfoAsync().then((userInfo) => { - if (isMounted) { - setLoading(false); - setOidcUser(userInfo); - } - }); - return () => { - isMounted = false; - }; - }, []); - - if (isLoading) { - return <>Loading; - } - - return ( -
-
-
-
User information
- {oidcUser != null && ( -

{JSON.stringify(oidcUser)}

- )} -
-
-
- ); -}; - -export const FetchUserHook = () => { - const { fetch } = useOidcFetch(); - return ( - - - - ); -}; -``` - -## How to get a fetch that inject Access_Token : HOC method - -If your are not using the service worker. Fetch function need to send AccessToken. -This HOC give you a wrapped fetch that add the access token for you. - -```javascript -import React, { useEffect, useState } from "react"; -import { useOidcFetch, OidcSecure } from "@axa-fr/react-oidc"; - -const DisplayUserInfo = ({ fetch }) => { - const [oidcUser, setOidcUser] = useState(null); - const [isLoading, setLoading] = useState(true); - - useEffect(() => { - const fetchUserInfoAsync = async () => { - const res = await fetch( - "https://demo.duendesoftware.com/connect/userinfo", - ); - if (res.status != 200) { - return null; - } - return res.json(); - }; - let isMounted = true; - fetchUserInfoAsync().then((userInfo) => { - if (isMounted) { - setLoading(false); - setOidcUser(userInfo); - } - }); - return () => { - isMounted = false; - }; - }, []); - - if (isLoading) { - return <>Loading; - } - - return ( -
-
-
-
User information
- {oidcUser != null && ( -

{JSON.stringify(oidcUser)}

- )} -
-
-
- ); -}; - -const UserInfoWithFetchHoc = withOidcFetch(fetch)(DisplayUserInfo); -export const FetchUserHoc = () => ( - - - -); -``` - -## Components override - -You can inject your own components. -All components definition receive props `configurationName`. Please checkout the demo for more complete example. - -```javascript -import React from "react"; -import { render } from "react-dom"; -import { BrowserRouter as Router } from "react-router-dom"; -import { OidcProvider } from "@axa-fr/react-oidc"; -import Header from "./Layout/Header"; -import Routes from "./Router"; - -// This configuration use hybrid mode -// ServiceWorker are used if available (more secure) else tokens are given to the client -// You need to give inside your code the "access_token" when using fetch -const configuration = { - client_id: "interactive.public.short", - redirect_uri: "http://localhost:4200/authentication/callback", - silent_redirect_uri: "http://localhost:4200/authentication/silent-callback", - scope: "openid profile email api offline_access", - authority: "https://demo.identityserver.io", - service_worker_relative_url: "/OidcServiceWorker.js", - service_worker_only: false, -}; - -const Loading = () =>

Loading

; -const AuthenticatingError = () =>

Authenticating error

; -const Authenticating = () =>

Authenticating

; -const SessionLost = () =>

Session Lost

; -const ServiceWorkerNotSupported = () =>

Not supported

; -const CallBackSuccess = () =>

Success

; - -//const [isSessionLost, setIsSessionLost] = useState(false); - -//const onSessionLost = ()=>{ -// setIsSessionLost(true); -//} - -const App = () => ( - - {/* isSessionLost && */} - -
- - - -); - -render(, document.getElementById("root")); -``` - -## How It Works - -These components encapsulate the use of "@axa-fr/vanilla-oidc" in order to hide workflow complexity. -Internally, native History API is used to be router library agnostic. - -More information about OIDC - -- [French : Augmentez la sécurité et la simplicité de votre Système d’Information OpenID Connect](https://medium.com/just-tech-it-now/augmentez-la-s%C3%A9curit%C3%A9-et-la-simplicit%C3%A9-de-votre-syst%C3%A8me-dinformation-avec-oauth-2-0-cf0732d71284) -- [English : Increase the security and simplicity of your information system with openid connect](https://medium.com/just-tech-it-now/increase-the-security-and-simplicity-of-your-information-system-with-openid-connect-fa8c26b99d6d) - -## NextJS - -To work with NextJS you need to inject your own history surcharge like the sample below. - -**component/layout.js** - -```javascript -import { OidcProvider } from "@axa-fr/react-oidc"; -import { useRouter } from "next/router"; - -const configuration = { - client_id: "interactive.public.short", - redirect_uri: "http://localhost:3001/#authentication/callback", - silent_redirect_uri: "http://localhost:3001/#authentication/silent-callback", // Optional activate silent-login that use cookies between OIDC server and client javascript to restore the session - scope: "openid profile email api offline_access", - authority: "https://demo.duendesoftware.com", -}; - -const onEvent = (configurationName, eventName, data) => { - console.log(`oidc:${configurationName}:${eventName}`, data); -}; - -export default function Layout({ children }) { - const router = useRouter(); - const withCustomHistory = () => { - return { - replaceState: (url) => { - router - .replace({ - pathname: url, - }) - .then(() => { - window.dispatchEvent(new Event("popstate")); - }); - }, - }; - }; - - return ( - <> - -
{children}
-
- - ); -} -``` - -For more information checkout the [NextJS React OIDC demo](https://github.com/AxaGuilDEv/react-oidc/tree/master/packages/nextjs-demo) - -## Hash route - -`react-oidc` work also with hash router. - -```javascript -export const configurationIdentityServerWithHash = { - client_id: "interactive.public.short", - redirect_uri: window.location.origin + "#authentication-callback", - silent_redirect_uri: - window.location.origin + "#authentication-silent-callback", - scope: "openid profile email api offline_access", - authority: "https://demo.duendesoftware.com", - refresh_time_before_tokens_expiration_in_second: 70, - service_worker_relative_url: "/OidcServiceWorker.js", - service_worker_only: false, -}; -``` - -## Service Worker Support - -- Firefox : tested on Firefox 98.0.2 -- Chrome/Edge : tested on version upper to 90 -- Opera : tested on version upper to 80 -- Safari : tested on Safari/605.1.15 +corepack enable +pnpm install --frozen-lockfile +pnpm build +pnpm --dir examples/react-oidc-demo start +``` + +The start script refreshes the service worker assets before starting Vite. Open +the URL printed in the terminal, normally `http://localhost:5173`. +[`vite.config.js`](vite.config.js) does not fix a port; a busy port may cause Vite +to choose another. Register the actual browser origin with your identity provider. + +## Configuration and callbacks + +- [`src/configurations.ts`](src/configurations.ts) defines the default provider, + a hash-routing variant, a configuration without discovery, and a Google example. +- [`src/App.tsx`](src/App.tsx) mounts the default `OidcProvider` and routes. +- [`src/MultiAuth.tsx`](src/MultiAuth.tsx) defines the named configurations used + by the **Multi Auth** page. +- [`public/OidcTrustedDomains.js`](public/OidcTrustedDomains.js) defines service + worker trust rules for each configuration name. + +The default configuration uses the external +[Duende demo provider](https://demo.duendesoftware.com), client ID +`interactive.public.short`, and scopes `openid profile email api offline_access`. +Use the provider's published sign-in options. Its accounts, availability, and +registered redirect URLs are outside this repository's control. + +The following suffixes are appended to `window.location.origin`. For example, the +default login callback is normally +`http://localhost:5173/authentication/callback`. + +| Configuration | Login callback suffix | Silent callback suffix | +| ------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------- | +| Default | `/authentication/callback` | `/authentication/silent-callback` | +| Multi Auth, except the variants below | `/multi-auth/authentification/callback` | `/multi-auth/authentification/silent-callback` | +| `config_with_hash` | `/multi-auth/authentification#authentication-callback` | `/multi-auth/authentification#authentication-silent-callback` | +| Google | `/multi-auth/callback-google` | `/multi-auth/silent-callback-google` | + +The spelling `authentification` is intentional here: it matches the existing +source. The hash variant also sets `silent_login_uri` to +`/multi-auth/authentification#authentication-silent-login`. +The variants with `without_silent_login` or `without_refresh_token_silent_login` +in their names disable the silent callback. + +OAuth redirect URIs must not contain fragments. The hash variant is a legacy +demonstration and may be rejected by your provider; prefer registered path-based +callbacks for a new integration. + +To use your own provider, register a **public browser client** supporting +Authorization Code with PKCE. Update the authority, client ID, allowed scopes, +exact redirect URLs, post-logout return URLs, and provider CORS settings for your +origin. Refresh-token, session-monitoring, PAR, and DPoP support depend on the +provider. Silent sign-in can be affected by browser restrictions on third-party +cookies. + +The Google entry is not a ready-to-use local registration: configure your own +provider-approved browser flow and allowed URLs. Never ship a confidential client +secret in browser code. + +## What to try + +| Page or control | What it demonstrates | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| **Home** | Login returning to `/profile`, several logout options, manual token renewal, and navigation without forcing login. | +| **Profile** | Authentication state, token information, and user information. | +| **Secure Profile Component** | Protecting content with `OidcSecure`. | +| **Secure Profile Hoc** | Protecting content with `withOidcSecure`. | +| **Secure User Fetch Hoc** | Calling the provider's user-info endpoint through `withOidcFetch`. | +| **Secure User Fetch Hook** | The same request through `useOidcFetch`. | +| **Multi Auth** | Switching between named configurations and observing their independent authentication state. | + +Start with **Login** on Home, inspect Profile, and then try a protected page after +logging out. Return to Home to renew tokens manually. The **Default configuration +Events** panel shows lifecycle events; Multi Auth adds a panel for its selected +configuration. + +In Multi Auth, compare configurations with and without refresh tokens or silent +sign-in, then explore session monitoring, hash callbacks, token visibility, +separate OIDC/API trust rules, DPoP, and multi-tab login. The selected configuration +name is kept in session storage. Some examples need provider-side setup; the +separate-domain example includes an API placeholder rather than a working API. +The fetch demonstrations call the Duende user-info endpoint, so adapt those +requests as well when testing another provider. + +## Service worker behavior + +The default configuration enables `/OidcServiceWorker.js` but sets +`service_worker_only: false`, allowing fallback when the worker cannot be used. +Use HTTPS or localhost for worker support. Review the active mode in browser +developer tools instead of assuming tokens are always hidden. + +In worker mode, access and refresh tokens normally remain in the worker, while +the page receives placeholders. The `config_show_access_token` trust rule +deliberately exposes the access token. Changing providers or API destinations also +requires updating the matching trust rules; do not solve trust errors by allowing +all destinations. See the +[service worker documentation](../../packages/oidc-client-service-worker/README.md). + +## Educational code, not a production template + +The **Execute your JavaScript Code** panel in +[`src/CodeExecutor.tsx`](src/CodeExecutor.tsx) uses `eval`. It intentionally runs +code in the page's context; use only code you understand with test accounts. + +Do not deploy that executor or copy permissive CSP settings, token displays, or +debug logging into a production application. Service worker token isolation is +not a complete defense against XSS: malicious page code can still make +authenticated requests. diff --git a/examples/react-oidc-demo/index.html b/examples/react-oidc-demo/index.html index 0bb0aea6f..8a5870109 100644 --- a/examples/react-oidc-demo/index.html +++ b/examples/react-oidc-demo/index.html @@ -1,13 +1,10 @@ - + - + React App diff --git a/examples/react-oidc-demo/package.json b/examples/react-oidc-demo/package.json index 66c06e02f..4fb96885a 100644 --- a/examples/react-oidc-demo/package.json +++ b/examples/react-oidc-demo/package.json @@ -11,41 +11,33 @@ "url": "https://github.com/AxaGuilDEv/react-oidc.git" }, "scripts": { - "start": "vite", + "start": "pnpm prestart && pnpm vite", + "prestart": "node ./node_modules/@axa-fr/react-oidc/bin/copy-service-worker-files.mjs public", "build": "vite build", "serve": "vite preview", - "clean": "rimraf dist" + "clean": "rimraf dist", + "postinstall": "node ./node_modules/@axa-fr/react-oidc/bin/copy-service-worker-files.mjs public" + }, + "dependencies": { + "@axa-fr/react-oidc": "workspace:*", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "react-router-dom": "^7.18.3" }, "devDependencies": { - "@axa-fr/vanilla-oidc": "workspace:*", - "@axa-fr/react-oidc":"workspace:*", - "@axa-fr/oidc-client-service-worker": "workspace:*", - "@testing-library/jest-dom": "5.16.5", - "@testing-library/react": "13.3.0", - "@testing-library/user-event": "14.4.3", - "@types/react": "^18.2.15", - "@typescript-eslint/eslint-plugin": "^5.50.0", - "@typescript-eslint/parser": "^5.50.0", - "@vitejs/plugin-react": "4.0.3", - "@vitest/coverage-c8": "^0.33.0", - "bootstrap": "^4.6.2", + "@testing-library/jest-dom": "7.0.1", + "@testing-library/react": "16.3.3", + "@testing-library/user-event": "14.6.7", + "@types/react": "19.3.0", + "@vitejs/plugin-react": "6.1.1", + "bootstrap": "^5.3.8", "copyfiles": "2.4.1", - "cross-env": "^7.0.3", - "eslint": "^8.26.0", - "eslint-config-standard": "^17.1.0", - "eslint-config-standard-with-typescript": "^36.1.0", - "eslint-import-resolver-typescript": "^3.5.5", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-simple-import-sort": "^10.0.0", - "jsdom": "22.1.0", - "msw": "1.2.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.14.1", - "typescript": "5.1.6", - "vite": "^4.4.4", - "vite-plugin-dts": "^3.3.0", - "vitest": "^0.33.0" + "cross-env": "^10.1.0", + "jsdom": "30.0.1", + "typescript": "npm:@typescript/typescript6@6.0.2", + "vite": "8.3.0", + "vite-plugin-dts": "5.1.0", + "vitest": "5.0.0" }, "license": "MIT", "publishConfig": { diff --git a/examples/react-oidc-demo/public/OidcServiceWorker.js.map b/examples/react-oidc-demo/public/OidcServiceWorker.js.map deleted file mode 100644 index 07cfaecde..000000000 --- a/examples/react-oidc-demo/public/OidcServiceWorker.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"OidcServiceWorker.js","sources":["../src/constants.ts","../src/utils/domains.ts","../src/utils/strings.ts","../src/utils/tokens.ts","../src/utils/serializeHeaders.ts","../src/utils/sleep.ts","../src/utils/codeVerifier.ts","../src/OidcServiceWorker.ts"],"sourcesContent":["const scriptFilename = 'OidcTrustedDomains.js'; /* global trustedDomains */\r\nconst acceptAnyDomainToken = '*';\r\n\r\ntype TokenType = {\r\n readonly REFRESH_TOKEN: string;\r\n readonly ACCESS_TOKEN: string;\r\n readonly NONCE_TOKEN: string;\r\n readonly CODE_VERIFIER: string;\r\n};\r\n\r\nconst TOKEN: TokenType = {\r\n REFRESH_TOKEN: 'REFRESH_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER',\r\n ACCESS_TOKEN: 'ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER',\r\n NONCE_TOKEN: 'NONCE_SECURED_BY_OIDC_SERVICE_WORKER',\r\n CODE_VERIFIER: 'CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER',\r\n};\r\n\r\ntype TokenRenewModeType = {\r\n readonly access_token_or_id_token_invalid: string;\r\n readonly access_token_invalid: string;\r\n readonly id_token_invalid: string;\r\n};\r\n\r\nconst TokenRenewMode: TokenRenewModeType = {\r\n access_token_or_id_token_invalid: 'access_token_or_id_token_invalid',\r\n access_token_invalid: 'access_token_invalid',\r\n id_token_invalid: 'id_token_invalid',\r\n};\r\n\r\nconst openidWellknownUrlEndWith = '/.well-known/openid-configuration';\r\n\r\nexport { scriptFilename, acceptAnyDomainToken, TOKEN, TokenRenewMode, openidWellknownUrlEndWith };\r\n","import { DomainDetails, TrustedDomains } from './../types';\r\nimport {\r\n acceptAnyDomainToken,\r\n openidWellknownUrlEndWith,\r\n scriptFilename,\r\n} from '../constants';\r\nimport { Database, Domain, OidcConfig } from '../types';\r\n\r\nfunction checkDomain(domains: Domain[], endpoint: string) {\r\n if (!endpoint) {\r\n return;\r\n }\r\n\r\n const domain = domains.find((domain) => {\r\n let testable: RegExp;\r\n\r\n if (typeof domain === 'string') {\r\n testable = new RegExp(`^${domain}`);\r\n } else {\r\n testable = domain;\r\n }\r\n\r\n return testable.test?.(endpoint);\r\n });\r\n if (!domain) {\r\n throw new Error(\r\n 'Domain ' +\r\n endpoint +\r\n ' is not trusted, please add domain in ' +\r\n scriptFilename\r\n );\r\n }\r\n}\r\n\r\nexport const getDomains = (trustedDomain: Domain[] | DomainDetails, type: 'oidc' | 'accessToken') => {\r\n if(Array.isArray(trustedDomain)) {\r\n return trustedDomain;\r\n }\r\n\r\n return trustedDomain[`${type}Domains`] ?? trustedDomain.domains ?? [];\r\n}\r\n\r\nconst getCurrentDatabaseDomain = (\r\n database: Database,\r\n url: string,\r\n trustedDomains: TrustedDomains\r\n) => {\r\n if (url.endsWith(openidWellknownUrlEndWith)) {\r\n return null;\r\n }\r\n for (const [key, currentDatabase] of Object.entries(database)) {\r\n const oidcServerConfiguration = currentDatabase.oidcServerConfiguration;\r\n\r\n if (!oidcServerConfiguration) {\r\n continue;\r\n }\r\n\r\n if (\r\n oidcServerConfiguration.tokenEndpoint &&\r\n url === oidcServerConfiguration.tokenEndpoint\r\n ) {\r\n continue;\r\n }\r\n if (\r\n oidcServerConfiguration.revocationEndpoint &&\r\n url === oidcServerConfiguration.revocationEndpoint\r\n ) {\r\n continue;\r\n }\r\n const trustedDomain = trustedDomains == null ? [] : trustedDomains[key];\r\n\r\n const domains = getDomains(trustedDomain, 'accessToken');\r\n const domainsToSendTokens = oidcServerConfiguration.userInfoEndpoint\r\n ? [oidcServerConfiguration.userInfoEndpoint, ...domains]\r\n : [...domains];\r\n\r\n let hasToSendToken = false;\r\n if (domainsToSendTokens.find((f) => f === acceptAnyDomainToken)) {\r\n hasToSendToken = true;\r\n } else {\r\n for (let i = 0; i < domainsToSendTokens.length; i++) {\r\n let domain = domainsToSendTokens[i];\r\n\r\n if (typeof domain === 'string') {\r\n domain = new RegExp(`^${domain}`);\r\n }\r\n\r\n if (domain.test?.(url)) {\r\n hasToSendToken = true;\r\n break;\r\n }\r\n }\r\n }\r\n\r\n if (hasToSendToken) {\r\n if (!currentDatabase.tokens) {\r\n return null;\r\n }\r\n return currentDatabase;\r\n }\r\n }\r\n return null;\r\n};\r\n\r\nexport { checkDomain, getCurrentDatabaseDomain };\r\n","/**\r\n * Count occurances of letter in string\r\n * @param str\r\n * @param find\r\n * @returns\r\n */\r\nexport function countLetter(str: string, find: string) {\r\n return str.split(find).length - 1;\r\n}\r\n","import { TOKEN, TokenRenewMode } from '../constants';\r\nimport { OidcConfig, OidcConfiguration, OidcServerConfiguration, Tokens } from '../types';\r\nimport { countLetter } from './strings';\r\n\r\nfunction parseJwt(token: string) {\r\n return JSON.parse(\r\n b64DecodeUnicode(token.split('.')[1].replace('-', '+').replace('_', '/'))\r\n );\r\n}\r\nfunction b64DecodeUnicode(str: string) {\r\n return decodeURIComponent(\r\n Array.prototype.map\r\n .call(\r\n atob(str),\r\n (c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)\r\n )\r\n .join('')\r\n );\r\n}\r\n\r\nfunction computeTimeLeft(\r\n refreshTimeBeforeTokensExpirationInSecond: number,\r\n expiresAt: number\r\n) {\r\n const currentTimeUnixSecond = new Date().getTime() / 1000;\r\n return Math.round(\r\n expiresAt -\r\n refreshTimeBeforeTokensExpirationInSecond -\r\n currentTimeUnixSecond\r\n );\r\n}\r\n\r\nfunction isTokensValid(tokens: Tokens | null) {\r\n if (!tokens) {\r\n return false;\r\n }\r\n return computeTimeLeft(0, tokens.expiresAt) > 0;\r\n}\r\n\r\nconst extractTokenPayload = (token?: string) => {\r\n try {\r\n if (!token) {\r\n return null;\r\n }\r\n if (countLetter(token, '.') === 2) {\r\n return parseJwt(token);\r\n } else {\r\n return null;\r\n }\r\n } catch (e) {\r\n console.warn(e);\r\n }\r\n return null;\r\n};\r\n\r\n// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation (excluding rules #1, #4, #5, #7, #8, #12, and #13 which did not apply).\r\n// https://github.com/openid/AppAuth-JS/issues/65\r\nconst isTokensOidcValid = (\r\n tokens: Tokens,\r\n nonce: string | null,\r\n oidcServerConfiguration: OidcServerConfiguration\r\n): { isValid: boolean; reason: string } => {\r\n if (tokens.idTokenPayload) {\r\n const idTokenPayload = tokens.idTokenPayload;\r\n // 2: The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) MUST exactly match the value of the iss (issuer) Claim.\r\n if (oidcServerConfiguration.issuer !== idTokenPayload.iss) {\r\n return { isValid: false, reason: 'Issuer does not match' };\r\n }\r\n // 3: The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified by the iss (issuer) Claim as an audience. The aud (audience) Claim MAY contain an array with more than one element. The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences not trusted by the Client.\r\n\r\n // 6: If the ID Token is received via direct communication between the Client and the Token Endpoint (which it is in this flow), the TLS server validation MAY be used to validate the issuer in place of checking the token signature. The Client MUST validate the signature of all other ID Tokens according to JWS [JWS] using the algorithm specified in the JWT alg Header Parameter. The Client MUST use the keys provided by the Issuer.\r\n\r\n // 9: The current time MUST be before the time represented by the exp Claim.\r\n const currentTimeUnixSecond = new Date().getTime() / 1000;\r\n if (idTokenPayload.exp && idTokenPayload.exp < currentTimeUnixSecond) {\r\n return { isValid: false, reason: 'Token expired' };\r\n }\r\n // 10: The iat Claim can be used to reject tokens that were issued too far away from the current time, limiting the amount of time that nonces need to be stored to prevent attacks. The acceptable range is Client specific.\r\n const timeInSevenDays = 60 * 60 * 24 * 7;\r\n if (\r\n idTokenPayload.iat &&\r\n idTokenPayload.iat + timeInSevenDays < currentTimeUnixSecond\r\n ) {\r\n return { isValid: false, reason: 'Token is used from too long time' };\r\n }\r\n // 11: If a nonce value was sent in the Authentication Request, a nonce Claim MUST be present and its value checked to verify that it is the same value as the one that was sent in the Authentication Request. The Client SHOULD check the nonce value for replay attacks. The precise method for detecting replay attacks is Client specific.\r\n if (nonce && idTokenPayload.nonce && idTokenPayload.nonce !== nonce) {\r\n return { isValid: false, reason: 'Nonce does not match' };\r\n }\r\n }\r\n return { isValid: true, reason: '' };\r\n};\r\n\r\nfunction _hideTokens(tokens: Tokens, currentDatabaseElement: OidcConfig, configurationName: string) {\r\n if (!tokens.issued_at) {\r\n const currentTimeUnixSecond = new Date().getTime() / 1000;\r\n tokens.issued_at = currentTimeUnixSecond;\r\n }\r\n\r\n const accessTokenPayload = extractTokenPayload(tokens.access_token);\r\n const secureTokens = {\r\n ...tokens,\r\n accessTokenPayload,\r\n };\r\n if (currentDatabaseElement.hideAccessToken) {\r\n secureTokens.access_token = TOKEN.ACCESS_TOKEN + '_' + configurationName;\r\n }\r\n tokens.accessTokenPayload = accessTokenPayload;\r\n\r\n let _idTokenPayload = null;\r\n if (tokens.id_token) {\r\n _idTokenPayload = extractTokenPayload(tokens.id_token);\r\n tokens.idTokenPayload = {..._idTokenPayload};\r\n if (_idTokenPayload.nonce && currentDatabaseElement.nonce != null) {\r\n const keyNonce =\r\n TOKEN.NONCE_TOKEN + '_' + currentDatabaseElement.configurationName;\r\n _idTokenPayload.nonce = keyNonce;\r\n }\r\n secureTokens.idTokenPayload = _idTokenPayload;\r\n }\r\n if (tokens.refresh_token) {\r\n secureTokens.refresh_token =\r\n TOKEN.REFRESH_TOKEN + '_' + configurationName;\r\n }\r\n\r\n const idTokenExpiresAt =\r\n _idTokenPayload && _idTokenPayload.exp\r\n ? _idTokenPayload.exp\r\n : Number.MAX_VALUE;\r\n const accessTokenExpiresAt =\r\n accessTokenPayload && accessTokenPayload.exp\r\n ? accessTokenPayload.exp\r\n : tokens.issued_at + tokens.expires_in;\r\n\r\n let expiresAt: number;\r\n const tokenRenewMode = (\r\n currentDatabaseElement.oidcConfiguration as OidcConfiguration\r\n ).token_renew_mode;\r\n if (tokenRenewMode === TokenRenewMode.access_token_invalid) {\r\n expiresAt = accessTokenExpiresAt;\r\n } else if (tokenRenewMode === TokenRenewMode.id_token_invalid) {\r\n expiresAt = idTokenExpiresAt;\r\n } else {\r\n expiresAt =\r\n idTokenExpiresAt < accessTokenExpiresAt\r\n ? idTokenExpiresAt\r\n : accessTokenExpiresAt;\r\n }\r\n secureTokens.expiresAt = expiresAt;\r\n\r\n tokens.expiresAt = expiresAt;\r\n const nonce = currentDatabaseElement.nonce\r\n ? currentDatabaseElement.nonce.nonce\r\n : null;\r\n const {isValid, reason} = isTokensOidcValid(\r\n tokens,\r\n nonce,\r\n currentDatabaseElement.oidcServerConfiguration as OidcServerConfiguration\r\n ); //TODO: Type assertion, could be null.\r\n if (!isValid) {\r\n throw Error(`Tokens are not OpenID valid, reason: ${reason}`);\r\n }\r\n\r\n // When refresh_token is not rotated we reuse ald refresh_token\r\n if (\r\n currentDatabaseElement.tokens != null &&\r\n 'refresh_token' in currentDatabaseElement.tokens &&\r\n !('refresh_token' in tokens)\r\n ) {\r\n const refreshToken = currentDatabaseElement.tokens.refresh_token;\r\n\r\n currentDatabaseElement.tokens = {\r\n ...tokens,\r\n refresh_token: refreshToken,\r\n };\r\n } else {\r\n currentDatabaseElement.tokens = tokens;\r\n }\r\n\r\n currentDatabaseElement.status = 'LOGGED_IN';\r\n return secureTokens;\r\n}\r\n\r\nfunction hideTokens(currentDatabaseElement: OidcConfig) {\r\n const configurationName = currentDatabaseElement.configurationName;\r\n return (response: Response) => {\r\n if (response.status !== 200) {\r\n return response;\r\n }\r\n return response.json().then((tokens: Tokens) => {\r\n const secureTokens = _hideTokens(tokens, currentDatabaseElement, configurationName);\r\n const body = JSON.stringify(secureTokens);\r\n return new Response(body, response);\r\n });\r\n };\r\n}\r\n\r\nexport {\r\n b64DecodeUnicode,\r\n computeTimeLeft,\r\n isTokensValid,\r\n extractTokenPayload,\r\n isTokensOidcValid,\r\n hideTokens,\r\n _hideTokens\r\n};\r\n","import { FetchHeaders } from '../types';\r\n\r\nfunction serializeHeaders(headers: Headers) {\r\n const headersObj: Record = {};\r\n for (const key of (headers as FetchHeaders).keys()) {\r\n if (headers.has(key)) {\r\n headersObj[key] = headers.get(key) as string;\r\n }\r\n }\r\n return headersObj;\r\n}\r\nexport {serializeHeaders};","const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\r\nexport { sleep };\r\n","\r\n\r\nexport function replaceCodeVerifier(codeVerifier:string, newCodeVerifier:string):string {\r\n const regex = /code_verifier=[A-Za-z0-9_-]+/i;\r\n return codeVerifier.replace(regex, `code_verifier=${newCodeVerifier}`);\r\n}","import { acceptAnyDomainToken, TOKEN, scriptFilename } from './constants';\r\nimport {\r\n TrustedDomains,\r\n Database,\r\n OidcConfig,\r\n OidcConfiguration,\r\n MessageEventData,\r\n // TrustedDomainsShowAccessToken,\r\n} from './types';\r\nimport {\r\n checkDomain,\r\n getCurrentDatabaseDomain,\r\n hideTokens,\r\n isTokensValid,\r\n serializeHeaders,\r\n sleep,\r\n getDomains,\r\n} from './utils';\r\nimport {replaceCodeVerifier} from \"./utils/codeVerifier\";\r\n\r\nconst _self = self as ServiceWorkerGlobalScope & typeof globalThis;\r\n\r\ndeclare let trustedDomains: TrustedDomains;\r\n\r\n_self.importScripts(scriptFilename);\r\n\r\nconst id = Math.round(new Date().getTime() / 1000).toString();\r\n\r\nconst keepAliveJsonFilename = 'OidcKeepAliveServiceWorker.json';\r\nconst handleInstall = (event: ExtendableEvent) => {\r\n console.log('[OidcServiceWorker] service worker installed ' + id);\r\n event.waitUntil(_self.skipWaiting());\r\n};\r\n\r\nconst handleActivate = (event: ExtendableEvent) => {\r\n console.log('[OidcServiceWorker] service worker activated ' + id);\r\n event.waitUntil(_self.clients.claim());\r\n};\r\n\r\nlet currentLoginCallbackConfigurationName: string | null = null;\r\nconst database: Database = {\r\n default: {\r\n configurationName: 'default',\r\n tokens: null,\r\n status: null,\r\n state: null,\r\n codeVerifier: null,\r\n nonce: null,\r\n oidcServerConfiguration: null,\r\n hideAccessToken: true,\r\n },\r\n};\r\n\r\nconst getCurrentDatabasesTokenEndpoint = (database: Database, url: string) => {\r\n const databases: OidcConfig[] = [];\r\n for (const [, value] of Object.entries(database)) {\r\n if (\r\n value.oidcServerConfiguration != null &&\r\n url.startsWith(value.oidcServerConfiguration.tokenEndpoint)\r\n ) {\r\n databases.push(value);\r\n } else if (\r\n value.oidcServerConfiguration != null &&\r\n value.oidcServerConfiguration.revocationEndpoint &&\r\n url.startsWith(value.oidcServerConfiguration.revocationEndpoint)\r\n ) {\r\n databases.push(value);\r\n }\r\n }\r\n return databases;\r\n};\r\n\r\nconst keepAliveAsync = async (event: FetchEvent) => {\r\n const originalRequest = event.request;\r\n const isFromVanilla = originalRequest.headers.has('oidc-vanilla');\r\n const init = { status: 200, statusText: 'oidc-service-worker' };\r\n const response = new Response('{}', init);\r\n if (!isFromVanilla) {\r\n const originalRequestUrl = new URL(originalRequest.url);\r\n const minSleepSeconds = Number(originalRequestUrl.searchParams.get('minSleepSeconds')) || 240;\r\n for (let i = 0; i < minSleepSeconds; i++) {\r\n await sleep(1000 + Math.floor(Math.random() * 1000));\r\n const cache = await caches.open('oidc_dummy_cache');\r\n await cache.put(event.request, response.clone());\r\n }\r\n }\r\n return response;\r\n};\r\n\r\nconst handleFetch = async (event: FetchEvent) => {\r\n const originalRequest = event.request;\r\n const url = originalRequest.url;\r\n if (originalRequest.url.includes(keepAliveJsonFilename)) {\r\n event.respondWith(keepAliveAsync(event));\r\n return;\r\n }\r\n\r\n const currentDatabaseForRequestAccessToken = getCurrentDatabaseDomain(\r\n database,\r\n originalRequest.url,\r\n trustedDomains\r\n );\r\n if (\r\n currentDatabaseForRequestAccessToken &&\r\n currentDatabaseForRequestAccessToken.tokens &&\r\n currentDatabaseForRequestAccessToken.tokens.access_token\r\n ) {\r\n while (\r\n currentDatabaseForRequestAccessToken.tokens &&\r\n !isTokensValid(currentDatabaseForRequestAccessToken.tokens)\r\n ) {\r\n await sleep(200);\r\n }\r\n const newRequest =\r\n originalRequest.mode == 'navigate'\r\n ? new Request(originalRequest, {\r\n headers: {\r\n ...serializeHeaders(originalRequest.headers),\r\n authorization:\r\n 'Bearer ' +\r\n currentDatabaseForRequestAccessToken.tokens.access_token,\r\n },\r\n })\r\n : new Request(originalRequest, {\r\n headers: {\r\n ...serializeHeaders(originalRequest.headers),\r\n authorization:\r\n 'Bearer ' +\r\n currentDatabaseForRequestAccessToken.tokens.access_token,\r\n },\r\n mode: (\r\n currentDatabaseForRequestAccessToken.oidcConfiguration as OidcConfiguration\r\n ).service_worker_convert_all_requests_to_cors\r\n ? 'cors'\r\n : originalRequest.mode,\r\n });\r\n\r\n //@ts-ignore -- TODO: review, waitUntil takes a promise, this returns a void\r\n event.waitUntil(event.respondWith(fetch(newRequest)));\r\n\r\n return;\r\n }\r\n\r\n if (event.request.method !== 'POST') {\r\n return;\r\n }\r\n\r\n let currentDatabase: OidcConfig | null = null;\r\n const currentDatabases = getCurrentDatabasesTokenEndpoint(\r\n database,\r\n originalRequest.url\r\n );\r\n const numberDatabase = currentDatabases.length;\r\n if (numberDatabase > 0) {\r\n const maPromesse = new Promise((resolve, reject) => {\r\n const clonedRequest = originalRequest.clone();\r\n const response = clonedRequest.text().then((actualBody) => {\r\n if (\r\n actualBody.includes(TOKEN.REFRESH_TOKEN) ||\r\n actualBody.includes(TOKEN.ACCESS_TOKEN)\r\n ) {\r\n let newBody = actualBody;\r\n for (let i = 0; i < numberDatabase; i++) {\r\n const currentDb = currentDatabases[i];\r\n\r\n if (currentDb && currentDb.tokens != null) {\r\n const keyRefreshToken =\r\n TOKEN.REFRESH_TOKEN + '_' + currentDb.configurationName;\r\n if (actualBody.includes(keyRefreshToken)) {\r\n newBody = newBody.replace(\r\n keyRefreshToken,\r\n encodeURIComponent(currentDb.tokens.refresh_token as string)\r\n );\r\n currentDatabase = currentDb;\r\n break;\r\n }\r\n const keyAccessToken =\r\n TOKEN.ACCESS_TOKEN + '_' + currentDb.configurationName;\r\n if (actualBody.includes(keyAccessToken)) {\r\n newBody = newBody.replace(\r\n keyAccessToken,\r\n encodeURIComponent(currentDb.tokens.access_token)\r\n );\r\n currentDatabase = currentDb;\r\n break;\r\n }\r\n }\r\n }\r\n const fetchPromise = fetch(originalRequest, {\r\n body: newBody,\r\n method: clonedRequest.method,\r\n headers: {\r\n ...serializeHeaders(originalRequest.headers),\r\n },\r\n mode: clonedRequest.mode,\r\n cache: clonedRequest.cache,\r\n redirect: clonedRequest.redirect,\r\n referrer: clonedRequest.referrer,\r\n credentials: clonedRequest.credentials,\r\n integrity: clonedRequest.integrity,\r\n });\r\n\r\n if (\r\n currentDatabase &&\r\n currentDatabase.oidcServerConfiguration != null &&\r\n currentDatabase.oidcServerConfiguration.revocationEndpoint &&\r\n url.startsWith(\r\n currentDatabase.oidcServerConfiguration.revocationEndpoint\r\n )\r\n ) {\r\n return fetchPromise.then(async (response) => {\r\n const text = await response.text();\r\n return new Response(text, response);\r\n });\r\n }\r\n return fetchPromise.then(hideTokens(currentDatabase as OidcConfig)); //todo type assertion to OidcConfig but could be null, NEEDS REVIEW\r\n } else if (\r\n actualBody.includes('code_verifier=') &&\r\n currentLoginCallbackConfigurationName\r\n ) {\r\n currentDatabase = database[currentLoginCallbackConfigurationName];\r\n currentLoginCallbackConfigurationName = null;\r\n let newBody = actualBody;\r\n if (currentDatabase && currentDatabase.codeVerifier != null) {\r\n newBody = replaceCodeVerifier(newBody, currentDatabase.codeVerifier);\r\n }\r\n\r\n return fetch(originalRequest, {\r\n body: newBody,\r\n method: clonedRequest.method,\r\n headers: {\r\n ...serializeHeaders(originalRequest.headers),\r\n },\r\n mode: clonedRequest.mode,\r\n cache: clonedRequest.cache,\r\n redirect: clonedRequest.redirect,\r\n referrer: clonedRequest.referrer,\r\n credentials: clonedRequest.credentials,\r\n integrity: clonedRequest.integrity,\r\n }).then(hideTokens(currentDatabase));\r\n }\r\n return undefined;\r\n });\r\n response\r\n .then((r) => {\r\n if (r !== undefined) {\r\n resolve(r);\r\n } else {\r\n console.log('success undefined');\r\n reject(new Error('Response is undefined inside a success'));\r\n }\r\n })\r\n .catch((err) => {\r\n if (err !== undefined) {\r\n reject(err);\r\n } else {\r\n console.log('error undefined');\r\n reject(new Error('Response is undefined inside a error'));\r\n }\r\n });\r\n });\r\n\r\n //@ts-ignore -- TODO: review, waitUntil takes a promise, this returns a void\r\n event.waitUntil(event.respondWith(maPromesse));\r\n }\r\n};\r\n\r\ntype TrustedDomainsShowAccessToken = {\r\n [key: string]: boolean\r\n}\r\n\r\nconst trustedDomainsShowAccessToken: TrustedDomainsShowAccessToken = {};\r\n\r\nconst handleMessage = (event: ExtendableMessageEvent) => {\r\n const port = event.ports[0];\r\n const data = event.data as MessageEventData;\r\n const configurationName = data.configurationName;\r\n let currentDatabase = database[configurationName];\r\n if(trustedDomains== null){\r\n trustedDomains = {};\r\n }\r\n if (!currentDatabase) {\r\n \r\n if (trustedDomainsShowAccessToken[configurationName] === undefined) {\r\n let trustedDomain = trustedDomains[configurationName];\r\n trustedDomainsShowAccessToken[configurationName] = Array.isArray(trustedDomain) ? false : trustedDomain.showAccessToken;\r\n }\r\n database[configurationName] = {\r\n tokens: null,\r\n state: null,\r\n codeVerifier: null,\r\n oidcServerConfiguration: null,\r\n oidcConfiguration: undefined,\r\n nonce: null,\r\n status: null,\r\n configurationName,\r\n hideAccessToken: !trustedDomainsShowAccessToken[configurationName],\r\n };\r\n currentDatabase = database[configurationName];\r\n \r\n if (!trustedDomains[configurationName]) {\r\n trustedDomains[configurationName] = [];\r\n }\r\n }\r\n\r\n switch (data.type) {\r\n case 'clear':\r\n currentDatabase.tokens = null;\r\n currentDatabase.state = null;\r\n currentDatabase.codeVerifier = null;\r\n currentDatabase.status = data.data.status;\r\n port.postMessage({ configurationName });\r\n return;\r\n case 'init': {\r\n const oidcServerConfiguration = data.data.oidcServerConfiguration;\r\n let trustedDomain = trustedDomains[configurationName];\r\n const domains = getDomains(trustedDomain, 'oidc');\r\n if (!domains.find((f) => f === acceptAnyDomainToken)) {\r\n [\r\n oidcServerConfiguration.tokenEndpoint,\r\n oidcServerConfiguration.revocationEndpoint,\r\n oidcServerConfiguration.userInfoEndpoint,\r\n oidcServerConfiguration.issuer,\r\n ].forEach((url) => {\r\n checkDomain(domains, url);\r\n });\r\n }\r\n currentDatabase.oidcServerConfiguration = oidcServerConfiguration;\r\n currentDatabase.oidcConfiguration = data.data.oidcConfiguration;\r\n const where = data.data.where;\r\n if (\r\n where === 'loginCallbackAsync' ||\r\n where === 'tryKeepExistingSessionAsync'\r\n ) {\r\n currentLoginCallbackConfigurationName = configurationName;\r\n } else {\r\n currentLoginCallbackConfigurationName = null;\r\n }\r\n\r\n if (!currentDatabase.tokens) {\r\n port.postMessage({\r\n tokens: null,\r\n status: currentDatabase.status,\r\n configurationName,\r\n });\r\n } else {\r\n const tokens = {\r\n ...currentDatabase.tokens,\r\n };\r\n if(currentDatabase.hideAccessToken) {\r\n tokens.access_token = TOKEN.ACCESS_TOKEN + '_' + configurationName;\r\n }\r\n if (tokens.refresh_token) {\r\n tokens.refresh_token = TOKEN.REFRESH_TOKEN + '_' + configurationName;\r\n }\r\n if (\r\n tokens.idTokenPayload &&\r\n tokens.idTokenPayload.nonce &&\r\n currentDatabase.nonce != null\r\n ) {\r\n tokens.idTokenPayload.nonce =\r\n TOKEN.NONCE_TOKEN + '_' + configurationName;\r\n }\r\n port.postMessage({\r\n tokens,\r\n status: currentDatabase.status,\r\n configurationName,\r\n });\r\n }\r\n return;\r\n }\r\n case 'setState':\r\n currentDatabase.state = data.data.state;\r\n port.postMessage({ configurationName });\r\n return;\r\n case 'getState': {\r\n const state = currentDatabase.state;\r\n port.postMessage({ configurationName, state });\r\n return;\r\n }\r\n case 'setCodeVerifier':\r\n currentDatabase.codeVerifier = data.data.codeVerifier;\r\n port.postMessage({ configurationName });\r\n return;\r\n case 'getCodeVerifier': {\r\n port.postMessage({\r\n configurationName,\r\n codeVerifier: currentDatabase.codeVerifier != null ? TOKEN.CODE_VERIFIER + '_' + configurationName : null,\r\n });\r\n return;\r\n }\r\n case 'setSessionState':\r\n currentDatabase.sessionState = data.data.sessionState;\r\n port.postMessage({ configurationName });\r\n return;\r\n case 'getSessionState': {\r\n const sessionState = currentDatabase.sessionState;\r\n port.postMessage({ configurationName, sessionState });\r\n return;\r\n }\r\n case 'setNonce': {\r\n let nonce = data.data.nonce;\r\n if (nonce) {\r\n currentDatabase.nonce = nonce;\r\n }\r\n port.postMessage({configurationName});\r\n return;\r\n }\r\n case 'getNonce': {\r\n const keyNonce = TOKEN.NONCE_TOKEN + '_' + configurationName;\r\n const nonce = currentDatabase.nonce ? keyNonce : null;\r\n port.postMessage({configurationName, nonce});\r\n return;\r\n }\r\n default:\r\n currentDatabase.items = { ...data.data };\r\n port.postMessage({ configurationName });\r\n }\r\n};\r\n\r\n_self.addEventListener('install', handleInstall);\r\n_self.addEventListener('activate', handleActivate);\r\n_self.addEventListener('fetch', handleFetch);\r\n_self.addEventListener('message', handleMessage);\r\n"],"names":["domain","database","trustedDomains","response"],"mappings":"AAAA,MAAM,iBAAiB;AACvB,MAAM,uBAAuB;AAS7B,MAAM,QAAmB;AAAA,EACvB,eAAe;AAAA,EACf,cAAc;AAAA,EACd,aAAa;AAAA,EACb,eAAe;AACjB;AAQA,MAAM,iBAAqC;AAAA,EACzC,kCAAkC;AAAA,EAClC,sBAAsB;AAAA,EACtB,kBAAkB;AACpB;AAEA,MAAM,4BAA4B;ACrBlC,SAAS,YAAY,SAAmB,UAAkB;AACxD,MAAI,CAAC,UAAU;AACb;AAAA,EACF;AAEA,QAAM,SAAS,QAAQ,KAAK,CAACA,YAAW;ADb1C;ACcQ,QAAA;AAEA,QAAA,OAAOA,YAAW,UAAU;AAC9B,iBAAW,IAAI,OAAO,IAAIA,OAAM,EAAE;AAAA,IAAA,OAC7B;AACMA,iBAAAA;AAAAA,IACb;AAEO,YAAA,cAAS,SAAT,kCAAgB;AAAA,EAAQ,CAChC;AACD,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI;AAAA,MACR,YACE,WACA,2CACA;AAAA,IAAA;AAAA,EAEN;AACF;AAEa,MAAA,aAAa,CAAC,eAAyC,SAAiC;AAChG,MAAA,MAAM,QAAQ,aAAa,GAAG;AACxB,WAAA;AAAA,EACT;AAEA,SAAO,cAAc,GAAG,IAAI,SAAS,KAAK,cAAc,WAAW;AACrE;AAEA,MAAM,2BAA2B,CAC/BC,WACA,KACAC,oBACG;AD9CL;AC+CM,MAAA,IAAI,SAAS,yBAAyB,GAAG;AACpC,WAAA;AAAA,EACT;AACA,aAAW,CAAC,KAAK,eAAe,KAAK,OAAO,QAAoBD,SAAQ,GAAG;AACzE,UAAM,0BAA0B,gBAAgB;AAEhD,QAAI,CAAC,yBAAyB;AAC5B;AAAA,IACF;AAEA,QACE,wBAAwB,iBACxB,QAAQ,wBAAwB,eAChC;AACA;AAAA,IACF;AACA,QACE,wBAAwB,sBACxB,QAAQ,wBAAwB,oBAChC;AACA;AAAA,IACF;AACA,UAAM,gBAAgBC,mBAAkB,OAAO,CAAA,IAAKA,gBAAe,GAAG;AAEhE,UAAA,UAAU,WAAW,eAAe,aAAa;AACjD,UAAA,sBAAsB,wBAAwB,mBAChD,CAAC,wBAAwB,kBAAkB,GAAG,OAAO,IACrD,CAAC,GAAG,OAAO;AAEf,QAAI,iBAAiB;AACrB,QAAI,oBAAoB,KAAK,CAAC,MAAM,MAAM,oBAAoB,GAAG;AAC9C,uBAAA;AAAA,IAAA,OACZ;AACL,eAAS,IAAI,GAAG,IAAI,oBAAoB,QAAQ,KAAK;AAC/C,YAAA,SAAS,oBAAoB,CAAC;AAE9B,YAAA,OAAO,WAAW,UAAU;AAC9B,mBAAS,IAAI,OAAO,IAAI,MAAM,EAAE;AAAA,QAClC;AAEI,aAAA,YAAO,SAAP,gCAAc,MAAM;AACL,2BAAA;AACjB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,QAAI,gBAAgB;AACd,UAAA,CAAC,gBAAgB,QAAQ;AACpB,eAAA;AAAA,MACT;AACO,aAAA;AAAA,IACT;AAAA,EACF;AACO,SAAA;AACT;AChGgB,SAAA,YAAY,KAAa,MAAc;AACrD,SAAO,IAAI,MAAM,IAAI,EAAE,SAAS;AAClC;ACJA,SAAS,SAAS,OAAe;AAC/B,SAAO,KAAK;AAAA,IACV,iBAAiB,MAAM,MAAM,GAAG,EAAE,CAAC,EAAE,QAAQ,KAAK,GAAG,EAAE,QAAQ,KAAK,GAAG,CAAC;AAAA,EAAA;AAE5E;AACA,SAAS,iBAAiB,KAAa;AAC9B,SAAA;AAAA,IACL,MAAM,UAAU,IACb;AAAA,MACC,KAAK,GAAG;AAAA,MACR,CAAC,MAAM,OAAO,OAAO,EAAE,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE;AAAA,IAAA,EAE5D,KAAK,EAAE;AAAA,EAAA;AAEd;AAEA,SAAS,gBACP,2CACA,WACA;AACA,QAAM,yBAAwB,oBAAI,KAAK,GAAE,YAAY;AACrD,SAAO,KAAK;AAAA,IACV,YACE,4CACA;AAAA,EAAA;AAEN;AAEA,SAAS,cAAc,QAAuB;AAC5C,MAAI,CAAC,QAAQ;AACJ,WAAA;AAAA,EACT;AACA,SAAO,gBAAgB,GAAG,OAAO,SAAS,IAAI;AAChD;AAEA,MAAM,sBAAsB,CAAC,UAAmB;AAC1C,MAAA;AACF,QAAI,CAAC,OAAO;AACH,aAAA;AAAA,IACT;AACA,QAAI,YAAY,OAAO,GAAG,MAAM,GAAG;AACjC,aAAO,SAAS,KAAK;AAAA,IAAA,OAChB;AACE,aAAA;AAAA,IACT;AAAA,WACO,GAAG;AACV,YAAQ,KAAK,CAAC;AAAA,EAChB;AACO,SAAA;AACT;AAIA,MAAM,oBAAoB,CACxB,QACA,OACA,4BACyC;AACzC,MAAI,OAAO,gBAAgB;AACzB,UAAM,iBAAiB,OAAO;AAE1B,QAAA,wBAAwB,WAAW,eAAe,KAAK;AACzD,aAAO,EAAE,SAAS,OAAO,QAAQ,wBAAwB;AAAA,IAC3D;AAMA,UAAM,yBAAwB,oBAAI,KAAK,GAAE,YAAY;AACrD,QAAI,eAAe,OAAO,eAAe,MAAM,uBAAuB;AACpE,aAAO,EAAE,SAAS,OAAO,QAAQ,gBAAgB;AAAA,IACnD;AAEM,UAAA,kBAAkB,KAAK,KAAK,KAAK;AACvC,QACE,eAAe,OACf,eAAe,MAAM,kBAAkB,uBACvC;AACA,aAAO,EAAE,SAAS,OAAO,QAAQ,mCAAmC;AAAA,IACtE;AAEA,QAAI,SAAS,eAAe,SAAS,eAAe,UAAU,OAAO;AACnE,aAAO,EAAE,SAAS,OAAO,QAAQ,uBAAuB;AAAA,IAC1D;AAAA,EACF;AACA,SAAO,EAAE,SAAS,MAAM,QAAQ,GAAG;AACrC;AAEA,SAAS,YAAY,QAAgB,wBAAoC,mBAA2B;AAC9F,MAAA,CAAC,OAAO,WAAW;AACrB,UAAM,yBAAwB,oBAAI,KAAK,GAAE,YAAY;AACrD,WAAO,YAAY;AAAA,EACrB;AAEM,QAAA,qBAAqB,oBAAoB,OAAO,YAAY;AAClE,QAAM,eAAe;AAAA,IACnB,GAAG;AAAA,IACH;AAAA,EAAA;AAEF,MAAI,uBAAuB,iBAAiB;AAC7B,iBAAA,eAAe,MAAM,eAAe,MAAM;AAAA,EACzD;AACA,SAAO,qBAAqB;AAE5B,MAAI,kBAAkB;AACtB,MAAI,OAAO,UAAU;AACD,sBAAA,oBAAoB,OAAO,QAAQ;AAC9C,WAAA,iBAAiB,EAAC,GAAG;AAC5B,QAAI,gBAAgB,SAAS,uBAAuB,SAAS,MAAM;AACjE,YAAM,WACF,MAAM,cAAc,MAAM,uBAAuB;AACrD,sBAAgB,QAAQ;AAAA,IAC1B;AACA,iBAAa,iBAAiB;AAAA,EAChC;AACA,MAAI,OAAO,eAAe;AACX,iBAAA,gBACT,MAAM,gBAAgB,MAAM;AAAA,EAClC;AAEA,QAAM,mBACF,mBAAmB,gBAAgB,MAC7B,gBAAgB,MAChB,OAAO;AACX,QAAA,uBACF,sBAAsB,mBAAmB,MACnC,mBAAmB,MACnB,OAAO,YAAY,OAAO;AAEhC,MAAA;AACE,QAAA,iBACF,uBAAuB,kBACzB;AACE,MAAA,mBAAmB,eAAe,sBAAsB;AAC9C,gBAAA;AAAA,EAAA,WACH,mBAAmB,eAAe,kBAAkB;AACjD,gBAAA;AAAA,EAAA,OACP;AAED,gBAAA,mBAAmB,uBACb,mBACA;AAAA,EACZ;AACA,eAAa,YAAY;AAEzB,SAAO,YAAY;AACnB,QAAM,QAAQ,uBAAuB,QAC/B,uBAAuB,MAAM,QAC7B;AACA,QAAA,EAAC,SAAS,OAAA,IAAU;AAAA,IACtB;AAAA,IACA;AAAA,IACA,uBAAuB;AAAA,EAAA;AAE3B,MAAI,CAAC,SAAS;AACN,UAAA,MAAM,wCAAwC,MAAM,EAAE;AAAA,EAC9D;AAII,MAAA,uBAAuB,UAAU,QACjC,mBAAmB,uBAAuB,UAC1C,EAAE,mBAAmB,SACvB;AACM,UAAA,eAAe,uBAAuB,OAAO;AAEnD,2BAAuB,SAAS;AAAA,MAC9B,GAAG;AAAA,MACH,eAAe;AAAA,IAAA;AAAA,EACjB,OACK;AACL,2BAAuB,SAAS;AAAA,EAClC;AAEA,yBAAuB,SAAS;AACzB,SAAA;AACT;AAEA,SAAS,WAAW,wBAAoC;AACtD,QAAM,oBAAoB,uBAAuB;AACjD,SAAO,CAAC,aAAuB;AACzB,QAAA,SAAS,WAAW,KAAK;AACpB,aAAA;AAAA,IACT;AACA,WAAO,SAAS,KAAA,EAAO,KAAe,CAAC,WAAmB;AACxD,YAAM,eAAe,YAAY,QAAQ,wBAAwB,iBAAiB;AAC5E,YAAA,OAAO,KAAK,UAAU,YAAY;AACjC,aAAA,IAAI,SAAS,MAAM,QAAQ;AAAA,IAAA,CACnC;AAAA,EAAA;AAEL;ACjMA,SAAS,iBAAiB,SAAkB;AAC1C,QAAM,aAAqC,CAAA;AAChC,aAAA,OAAQ,QAAyB,QAAQ;AAC9C,QAAA,QAAQ,IAAI,GAAG,GAAG;AACpB,iBAAW,GAAG,IAAI,QAAQ,IAAI,GAAG;AAAA,IACnC;AAAA,EACF;AACO,SAAA;AACT;ACVA,MAAM,QAAQ,CAAC,OAAe,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;ACE9D,SAAA,oBAAoB,cAAqB,iBAA+B;AACpF,QAAM,QAAQ;AACd,SAAO,aAAa,QAAQ,OAAO,iBAAiB,eAAe,EAAE;AACzE;ACeA,MAAM,QAAQ;AAId,MAAM,cAAc,cAAc;AAElC,MAAM,KAAK,KAAK,OAAU,oBAAA,QAAO,YAAY,GAAI,EAAE;AAEnD,MAAM,wBAAwB;AAC9B,MAAM,gBAAgB,CAAC,UAA2B;AACxC,UAAA,IAAI,kDAAkD,EAAE;AAC1D,QAAA,UAAU,MAAM,YAAa,CAAA;AACrC;AAEA,MAAM,iBAAiB,CAAC,UAA2B;AACzC,UAAA,IAAI,kDAAkD,EAAE;AAChE,QAAM,UAAU,MAAM,QAAQ,MAAO,CAAA;AACvC;AAEA,IAAI,wCAAuD;AAC3D,MAAM,WAAqB;AAAA,EACzB,SAAS;AAAA,IACP,mBAAmB;AAAA,IACnB,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,cAAc;AAAA,IACd,OAAO;AAAA,IACP,yBAAyB;AAAA,IACzB,iBAAiB;AAAA,EACnB;AACF;AAEA,MAAM,mCAAmC,CAACD,WAAoB,QAAgB;AAC5E,QAAM,YAA0B,CAAA;AAChC,aAAW,CAAG,EAAA,KAAK,KAAK,OAAO,QAAoBA,SAAQ,GAAG;AAE1D,QAAA,MAAM,2BAA2B,QACjC,IAAI,WAAW,MAAM,wBAAwB,aAAa,GAC1D;AACA,gBAAU,KAAK,KAAK;AAAA,IAEpB,WAAA,MAAM,2BAA2B,QACjC,MAAM,wBAAwB,sBAC9B,IAAI,WAAW,MAAM,wBAAwB,kBAAkB,GAC/D;AACA,gBAAU,KAAK,KAAK;AAAA,IACtB;AAAA,EACF;AACO,SAAA;AACT;AAEA,MAAM,iBAAiB,OAAO,UAAsB;AAClD,QAAM,kBAAkB,MAAM;AAC9B,QAAM,gBAAgB,gBAAgB,QAAQ,IAAI,cAAc;AAChE,QAAM,OAAO,EAAE,QAAQ,KAAK,YAAY,sBAAsB;AAC9D,QAAM,WAAW,IAAI,SAAS,MAAM,IAAI;AACxC,MAAI,CAAC,eAAe;AAClB,UAAM,qBAAqB,IAAI,IAAI,gBAAgB,GAAG;AACtD,UAAM,kBAAkB,OAAO,mBAAmB,aAAa,IAAI,iBAAiB,CAAC,KAAK;AAC1F,aAAS,IAAI,GAAG,IAAI,iBAAiB,KAAK;AAClC,YAAA,MAAM,MAAO,KAAK,MAAM,KAAK,OAAO,IAAI,GAAI,CAAC;AACnD,YAAM,QAAQ,MAAM,OAAO,KAAK,kBAAkB;AAClD,YAAM,MAAM,IAAI,MAAM,SAAS,SAAS,OAAO;AAAA,IACjD;AAAA,EACF;AACO,SAAA;AACT;AAEA,MAAM,cAAc,OAAO,UAAsB;AAC/C,QAAM,kBAAkB,MAAM;AAC9B,QAAM,MAAM,gBAAgB;AAC5B,MAAI,gBAAgB,IAAI,SAAS,qBAAqB,GAAG;AACjD,UAAA,YAAY,eAAe,KAAK,CAAC;AACvC;AAAA,EACF;AAEA,QAAM,uCAAuC;AAAA,IAC3C;AAAA,IACA,gBAAgB;AAAA,IAChB;AAAA,EAAA;AAEF,MACE,wCACA,qCAAqC,UACrC,qCAAqC,OAAO,cAC5C;AACA,WACE,qCAAqC,UACrC,CAAC,cAAc,qCAAqC,MAAM,GAC1D;AACA,YAAM,MAAM,GAAG;AAAA,IACjB;AACA,UAAM,aACJ,gBAAgB,QAAQ,aACpB,IAAI,QAAQ,iBAAiB;AAAA,MAC3B,SAAS;AAAA,QACP,GAAG,iBAAiB,gBAAgB,OAAO;AAAA,QAC3C,eACE,YACA,qCAAqC,OAAO;AAAA,MAChD;AAAA,IAAA,CACD,IACD,IAAI,QAAQ,iBAAiB;AAAA,MAC3B,SAAS;AAAA,QACP,GAAG,iBAAiB,gBAAgB,OAAO;AAAA,QAC3C,eACE,YACA,qCAAqC,OAAO;AAAA,MAChD;AAAA,MACA,MACE,qCAAqC,kBACrC,8CACE,SACA,gBAAgB;AAAA,IAAA,CACrB;AAGP,UAAM,UAAU,MAAM,YAAY,MAAM,UAAU,CAAC,CAAC;AAEpD;AAAA,EACF;AAEI,MAAA,MAAM,QAAQ,WAAW,QAAQ;AACnC;AAAA,EACF;AAEA,MAAI,kBAAqC;AACzC,QAAM,mBAAmB;AAAA,IACvB;AAAA,IACA,gBAAgB;AAAA,EAAA;AAElB,QAAM,iBAAiB,iBAAiB;AACxC,MAAI,iBAAiB,GAAG;AACtB,UAAM,aAAa,IAAI,QAAkB,CAAC,SAAS,WAAW;AACtD,YAAA,gBAAgB,gBAAgB;AACtC,YAAM,WAAW,cAAc,KAAO,EAAA,KAAK,CAAC,eAAe;AAEvD,YAAA,WAAW,SAAS,MAAM,aAAa,KACvC,WAAW,SAAS,MAAM,YAAY,GACtC;AACA,cAAI,UAAU;AACd,mBAAS,IAAI,GAAG,IAAI,gBAAgB,KAAK;AACjC,kBAAA,YAAY,iBAAiB,CAAC;AAEhC,gBAAA,aAAa,UAAU,UAAU,MAAM;AACzC,oBAAM,kBACJ,MAAM,gBAAgB,MAAM,UAAU;AACpC,kBAAA,WAAW,SAAS,eAAe,GAAG;AACxC,0BAAU,QAAQ;AAAA,kBAChB;AAAA,kBACA,mBAAmB,UAAU,OAAO,aAAuB;AAAA,gBAAA;AAE3C,kCAAA;AAClB;AAAA,cACF;AACA,oBAAM,iBACJ,MAAM,eAAe,MAAM,UAAU;AACnC,kBAAA,WAAW,SAAS,cAAc,GAAG;AACvC,0BAAU,QAAQ;AAAA,kBAChB;AAAA,kBACA,mBAAmB,UAAU,OAAO,YAAY;AAAA,gBAAA;AAEhC,kCAAA;AAClB;AAAA,cACF;AAAA,YACF;AAAA,UACF;AACM,gBAAA,eAAe,MAAM,iBAAiB;AAAA,YAC1C,MAAM;AAAA,YACN,QAAQ,cAAc;AAAA,YACtB,SAAS;AAAA,cACP,GAAG,iBAAiB,gBAAgB,OAAO;AAAA,YAC7C;AAAA,YACA,MAAM,cAAc;AAAA,YACpB,OAAO,cAAc;AAAA,YACrB,UAAU,cAAc;AAAA,YACxB,UAAU,cAAc;AAAA,YACxB,aAAa,cAAc;AAAA,YAC3B,WAAW,cAAc;AAAA,UAAA,CAC1B;AAED,cACE,mBACA,gBAAgB,2BAA2B,QAC3C,gBAAgB,wBAAwB,sBACxC,IAAI;AAAA,YACF,gBAAgB,wBAAwB;AAAA,UAAA,GAE1C;AACO,mBAAA,aAAa,KAAK,OAAOE,cAAa;AACrC,oBAAA,OAAO,MAAMA,UAAS;AACrB,qBAAA,IAAI,SAAS,MAAMA,SAAQ;AAAA,YAAA,CACnC;AAAA,UACH;AACA,iBAAO,aAAa,KAAK,WAAW,eAA6B,CAAC;AAAA,QAElE,WAAA,WAAW,SAAS,gBAAgB,KACpC,uCACA;AACA,4BAAkB,SAAS,qCAAqC;AACxB,kDAAA;AACxC,cAAI,UAAU;AACV,cAAA,mBAAmB,gBAAgB,gBAAgB,MAAM;AACjD,sBAAA,oBAAoB,SAAS,gBAAgB,YAAY;AAAA,UACrE;AAEA,iBAAO,MAAM,iBAAiB;AAAA,YAC5B,MAAM;AAAA,YACN,QAAQ,cAAc;AAAA,YACtB,SAAS;AAAA,cACP,GAAG,iBAAiB,gBAAgB,OAAO;AAAA,YAC7C;AAAA,YACA,MAAM,cAAc;AAAA,YACpB,OAAO,cAAc;AAAA,YACrB,UAAU,cAAc;AAAA,YACxB,UAAU,cAAc;AAAA,YACxB,aAAa,cAAc;AAAA,YAC3B,WAAW,cAAc;AAAA,UAC1B,CAAA,EAAE,KAAK,WAAW,eAAe,CAAC;AAAA,QACrC;AACO,eAAA;AAAA,MAAA,CACR;AAEE,eAAA,KAAK,CAAC,MAAM;AACX,YAAI,MAAM,QAAW;AACnB,kBAAQ,CAAC;AAAA,QAAA,OACJ;AACL,kBAAQ,IAAI,mBAAmB;AACxB,iBAAA,IAAI,MAAM,wCAAwC,CAAC;AAAA,QAC5D;AAAA,MAAA,CACD,EACA,MAAM,CAAC,QAAQ;AACd,YAAI,QAAQ,QAAW;AACrB,iBAAO,GAAG;AAAA,QAAA,OACL;AACL,kBAAQ,IAAI,iBAAiB;AACtB,iBAAA,IAAI,MAAM,sCAAsC,CAAC;AAAA,QAC1D;AAAA,MAAA,CACD;AAAA,IAAA,CACJ;AAGD,UAAM,UAAU,MAAM,YAAY,UAAU,CAAC;AAAA,EAC/C;AACF;AAMA,MAAM,gCAA+D,CAAA;AAErE,MAAM,gBAAgB,CAAC,UAAkC;AACjD,QAAA,OAAO,MAAM,MAAM,CAAC;AAC1B,QAAM,OAAO,MAAM;AACnB,QAAM,oBAAoB,KAAK;AAC3B,MAAA,kBAAkB,SAAS,iBAAiB;AAChD,MAAG,kBAAiB,MAAK;AACvB,qBAAiB,CAAA;AAAA,EACnB;AACA,MAAI,CAAC,iBAAiB;AAEhB,QAAA,8BAA8B,iBAAiB,MAAM,QAAW;AAC9D,UAAA,gBAAgB,eAAe,iBAAiB;AACpD,oCAA8B,iBAAiB,IAAI,MAAM,QAAQ,aAAa,IAAI,QAAQ,cAAc;AAAA,IAC1G;AACA,aAAS,iBAAiB,IAAI;AAAA,MAC5B,QAAQ;AAAA,MACR,OAAO;AAAA,MACP,cAAc;AAAA,MACd,yBAAyB;AAAA,MACzB,mBAAmB;AAAA,MACnB,OAAO;AAAA,MACP,QAAQ;AAAA,MACR;AAAA,MACA,iBAAiB,CAAC,8BAA8B,iBAAiB;AAAA,IAAA;AAEnE,sBAAkB,SAAS,iBAAiB;AAExC,QAAA,CAAC,eAAe,iBAAiB,GAAG;AACvB,qBAAA,iBAAiB,IAAI;IACtC;AAAA,EACF;AAEA,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK;AACH,sBAAgB,SAAS;AACzB,sBAAgB,QAAQ;AACxB,sBAAgB,eAAe;AACf,sBAAA,SAAS,KAAK,KAAK;AAC9B,WAAA,YAAY,EAAE,kBAAA,CAAmB;AACtC;AAAA,IACF,KAAK,QAAQ;AACL,YAAA,0BAA0B,KAAK,KAAK;AACtC,UAAA,gBAAgB,eAAe,iBAAiB;AAC9C,YAAA,UAAU,WAAW,eAAe,MAAM;AAChD,UAAI,CAAC,QAAQ,KAAK,CAAC,MAAM,MAAM,oBAAoB,GAAG;AACpD;AAAA,UACE,wBAAwB;AAAA,UACxB,wBAAwB;AAAA,UACxB,wBAAwB;AAAA,UACxB,wBAAwB;AAAA,QAAA,EACxB,QAAQ,CAAC,QAAQ;AACjB,sBAAY,SAAS,GAAG;AAAA,QAAA,CACzB;AAAA,MACH;AACF,sBAAgB,0BAA0B;AACxB,sBAAA,oBAAoB,KAAK,KAAK;AACxC,YAAA,QAAQ,KAAK,KAAK;AAEtB,UAAA,UAAU,wBACV,UAAU,+BACV;AACwC,gDAAA;AAAA,MAAA,OACnC;AACmC,gDAAA;AAAA,MAC1C;AAEI,UAAA,CAAC,gBAAgB,QAAQ;AAC3B,aAAK,YAAY;AAAA,UACf,QAAQ;AAAA,UACR,QAAQ,gBAAgB;AAAA,UACxB;AAAA,QAAA,CACD;AAAA,MAAA,OACI;AACL,cAAM,SAAS;AAAA,UACb,GAAG,gBAAgB;AAAA,QAAA;AAErB,YAAG,gBAAgB,iBAAiB;AAC3B,iBAAA,eAAe,MAAM,eAAe,MAAM;AAAA,QACnD;AACA,YAAI,OAAO,eAAe;AACjB,iBAAA,gBAAgB,MAAM,gBAAgB,MAAM;AAAA,QACrD;AACA,YACE,OAAO,kBACP,OAAO,eAAe,SACtB,gBAAgB,SAAS,MACzB;AACA,iBAAO,eAAe,QACpB,MAAM,cAAc,MAAM;AAAA,QAC9B;AACA,aAAK,YAAY;AAAA,UACf;AAAA,UACA,QAAQ,gBAAgB;AAAA,UACxB;AAAA,QAAA,CACD;AAAA,MACH;AACA;AAAA,IACF;AAAA,IACA,KAAK;AACa,sBAAA,QAAQ,KAAK,KAAK;AAC7B,WAAA,YAAY,EAAE,kBAAA,CAAmB;AACtC;AAAA,IACF,KAAK,YAAY;AACf,YAAM,QAAQ,gBAAgB;AAC9B,WAAK,YAAY,EAAE,mBAAmB,MAAO,CAAA;AAC7C;AAAA,IACF;AAAA,IACA,KAAK;AACa,sBAAA,eAAe,KAAK,KAAK;AACpC,WAAA,YAAY,EAAE,kBAAA,CAAmB;AACtC;AAAA,IACF,KAAK,mBAAmB;AACtB,WAAK,YAAY;AAAA,QACf;AAAA,QACA,cAAc,gBAAgB,gBAAgB,OAAO,MAAM,gBAAgB,MAAM,oBAAoB;AAAA,MAAA,CACtG;AACD;AAAA,IACF;AAAA,IACA,KAAK;AACa,sBAAA,eAAe,KAAK,KAAK;AACpC,WAAA,YAAY,EAAE,kBAAA,CAAmB;AACtC;AAAA,IACF,KAAK,mBAAmB;AACtB,YAAM,eAAe,gBAAgB;AACrC,WAAK,YAAY,EAAE,mBAAmB,aAAc,CAAA;AACpD;AAAA,IACF;AAAA,IACA,KAAK,YAAY;AACX,UAAA,QAAQ,KAAK,KAAK;AACtB,UAAI,OAAO;AACT,wBAAgB,QAAQ;AAAA,MAC1B;AACK,WAAA,YAAY,EAAC,kBAAA,CAAkB;AACpC;AAAA,IACF;AAAA,IACA,KAAK,YAAY;AACT,YAAA,WAAW,MAAM,cAAc,MAAM;AACrC,YAAA,QAAQ,gBAAgB,QAAQ,WAAW;AACjD,WAAK,YAAY,EAAC,mBAAmB,MAAM,CAAA;AAC3C;AAAA,IACF;AAAA,IACA;AACE,sBAAgB,QAAQ,EAAE,GAAG,KAAK,KAAK;AAClC,WAAA,YAAY,EAAE,kBAAA,CAAmB;AAAA,EAC1C;AACF;AAEA,MAAM,iBAAiB,WAAW,aAAa;AAC/C,MAAM,iBAAiB,YAAY,cAAc;AACjD,MAAM,iBAAiB,SAAS,WAAW;AAC3C,MAAM,iBAAiB,WAAW,aAAa;"} \ No newline at end of file diff --git a/examples/react-oidc-demo/public/OidcTrustedDomains.js b/examples/react-oidc-demo/public/OidcTrustedDomains.js index c1b0f6fc3..a6fc4a35a 100644 --- a/examples/react-oidc-demo/public/OidcTrustedDomains.js +++ b/examples/react-oidc-demo/public/OidcTrustedDomains.js @@ -4,21 +4,41 @@ // Domains used by OIDC server must be also declared here // eslint-disable-next-line @typescript-eslint/no-unused-vars const trustedDomains = { - default: ['https://demo.duendesoftware.com', 'https://kdhttps.auth0.com'], - config_classic: ['https://demo.duendesoftware.com'], - config_without_silent_login: ['https://demo.duendesoftware.com'], - config_without_refresh_token: ['https://demo.duendesoftware.com'], - config_without_refresh_token_silent_login: ['https://demo.duendesoftware.com'], - config_google: ['https://oauth2.googleapis.com', 'https://openidconnect.googleapis.com'], - config_with_hash: ['https://demo.duendesoftware.com'], + default: ['https://demo.duendesoftware.com', 'https://kdhttps.auth0.com'], + config_classic: ['https://demo.duendesoftware.com'], + config_with_monitor_session: ['https://demo.duendesoftware.com'], + config_without_silent_login: ['https://demo.duendesoftware.com'], + config_without_refresh_token: ['https://demo.duendesoftware.com'], + config_without_refresh_token_silent_login: ['https://demo.duendesoftware.com'], + config_google: [ + 'https://oauth2.googleapis.com', + 'https://openidconnect.googleapis.com', + 'https://accounts.google.com', + ], + config_with_hash: ['https://demo.duendesoftware.com'], }; // Service worker will continue to give access token to the JavaScript client // Ideal to hide refresh token from client JavaScript, but to retrieve access_token for some // scenarios which require it. For example, to send it via websocket connection. -trustedDomains.config_show_access_token = { domains: ["https://demo.duendesoftware.com"], showAccessToken: true }; +trustedDomains.config_show_access_token = { + domains: ['https://demo.duendesoftware.com'], + showAccessToken: true, +}; // This example defines domains used by OIDC server separately from domains to which access tokens will be injected. trustedDomains.config_separate_oidc_access_token_domains = { - oidcDomains: ["https://demo.duendesoftware.com"], - accessTokenDomains: ["https://myapi"] + oidcDomains: ['https://demo.duendesoftware.com'], + accessTokenDomains: ['https://myapi'], +}; + +trustedDomains.config_with_dpop = { + domains: ['https://demo.duendesoftware.com'], + demonstratingProofOfPossession: true, + demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent: false, + allowMultiTabLogin: true, +}; + +trustedDomains.config_multi_tab_login = { + domains: ['https://demo.duendesoftware.com'], + allowMultiTabLogin: true, }; //# sourceMappingURL=OidcTrustedDomains.js.map \ No newline at end of file diff --git a/examples/react-oidc-demo/public/package.json b/examples/react-oidc-demo/public/package.json index cf6f816bf..1ddfbbf0b 100644 --- a/examples/react-oidc-demo/public/package.json +++ b/examples/react-oidc-demo/public/package.json @@ -58,4 +58,4 @@ "last 1 safari version" ] } -} \ No newline at end of file +} diff --git a/examples/react-oidc-demo/public/staticwebapp.config.json b/examples/react-oidc-demo/public/staticwebapp.config.json index ba338eae6..c0be511eb 100644 --- a/examples/react-oidc-demo/public/staticwebapp.config.json +++ b/examples/react-oidc-demo/public/staticwebapp.config.json @@ -1,6 +1,15 @@ { - "navigationFallback": { - "rewrite": "index.html", - "exclude": ["*.{svg,png,jpg,gif}","*.{css,scss}","*.js"] - } -} \ No newline at end of file + "navigationFallback": { + "rewrite": "index.html", + "exclude": ["*.{svg,png,jpg,gif}", "*.{css,scss}", "*.js"] + }, + "globalHeaders": { + "content-security-policy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://demo.duendesoftware.com; media-src 'self'; object-src 'none'; frame-src 'self' https://demo.duendesoftware.com; base-uri 'self'; form-action 'self'; frame-ancestors 'self' https://demo.duendesoftware.com; block-all-mixed-content; upgrade-insecure-requests;", + "Access-Control-Allow-Origin": "*", + "X-Frame-Options": "SAMEORIGIN", + "X-Permitted-Cross-Domain-Policies": "none", + "Referrer-Policy": "no-referrer", + "X-Content-Type-Options": "nosniff", + "Permissions-Policy": "autoplay=()" + } +} diff --git a/examples/react-oidc-demo/public/tsconfig.eslint.json b/examples/react-oidc-demo/public/tsconfig.eslint.json index e9041fd6b..b90fc83e0 100644 --- a/examples/react-oidc-demo/public/tsconfig.eslint.json +++ b/examples/react-oidc-demo/public/tsconfig.eslint.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "include": ["src"] -} \ No newline at end of file +} diff --git a/examples/react-oidc-demo/public/tsconfig.json b/examples/react-oidc-demo/public/tsconfig.json index cde526795..84021634e 100644 --- a/examples/react-oidc-demo/public/tsconfig.json +++ b/examples/react-oidc-demo/public/tsconfig.json @@ -7,6 +7,7 @@ "module": "ESNext", "target": "ESNext", "moduleResolution": "Node", + "ignoreDeprecations": "6.0", "sourceMap": true, "declarationMap": true, "outDir": "../dist", diff --git a/examples/react-oidc-demo/public/vite.config.js b/examples/react-oidc-demo/public/vite.config.js index 13467b083..62d54db3b 100644 --- a/examples/react-oidc-demo/public/vite.config.js +++ b/examples/react-oidc-demo/public/vite.config.js @@ -3,13 +3,15 @@ import { defineConfig } from 'vite'; import dts from 'vite-plugin-dts'; export default defineConfig({ - plugins: [dts({ - insertTypesEntry: true, - })], + plugins: [ + dts({ + insertTypesEntry: true, + }), + ], test: { coverage: { - provider: 'c8' - } + provider: 'c8', + }, }, build: { minify: false, //default esbuild @@ -29,8 +31,7 @@ export default defineConfig({ output: { // Provide global variables to use in the UMD build // for externalized deps - globals: { - }, + globals: {}, }, }, }, diff --git a/examples/react-oidc-demo/src/App.spec.tsx b/examples/react-oidc-demo/src/App.spec.tsx index 3b3523295..ee2438010 100644 --- a/examples/react-oidc-demo/src/App.spec.tsx +++ b/examples/react-oidc-demo/src/App.spec.tsx @@ -1,64 +1,130 @@ -import React from 'react' -import {rest} from 'msw' -import {setupServer} from 'msw/node' -import {render, fireEvent, waitFor, screen} from '@testing-library/react' -import '@testing-library/jest-dom' -import App from "./App"; -import {sleepAsync} from "./oidc/vanilla/initWorker"; -import { describe, it, expect } from 'vitest'; - +import '@testing-library/jest-dom'; + +import { fireEvent, render, screen } from '@testing-library/react'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { describe, expect, it } from 'vitest'; + +import App from './App'; +import { sleepAsync } from './utils/sleep'; + const server = setupServer( - rest.get('http://api/.well-known/openid-configuration', (req, res, ctx) => { - return res( ctx.status(200),ctx.json({ - "issuer":"https://demo.identityserver.io", - "jwks_uri":"https://demo.identityserver.io/.well-known/openid-configuration/jwks", - "authorization_endpoint":"https://demo.identityserver.io/connect/authorize", - "token_endpoint":"https://demo.identityserver.io/connect/token", - "userinfo_endpoint":"https://demo.identityserver.io/connect/userinfo", - "end_session_endpoint":"https://demo.identityserver.io/connect/endsession", - "check_session_iframe":"https://demo.identityserver.io/connect/checksession", - "revocation_endpoint":"https://demo.identityserver.io/connect/revocation", - "introspection_endpoint":"https://demo.identityserver.io/connect/introspect", - "device_authorization_endpoint":"https://demo.identityserver.io/connect/deviceauthorization","frontchannel_logout_supported":true,"frontchannel_logout_session_supported":true,"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"scopes_supported":["openid","profile","email","api","api.scope1","api.scope2","scope2","policyserver.runtime","policyserver.management","offline_access"],"claims_supported":["sub","name","family_name","given_name","middle_name","nickname","preferred_username","profile","picture","website","gender","birthdate","zoneinfo","locale","updated_at","email","email_verified"],"grant_types_supported":["authorization_code","client_credentials","refresh_token","implicit","password","urn:ietf:params:oauth:grant-type:device_code"],"response_types_supported":["code","token","id_token","id_token token","code id_token","code token","code id_token token"],"response_modes_supported":["form_post","query","fragment"],"token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post"],"id_token_signing_alg_values_supported":["RS256"],"subject_types_supported":["public"],"code_challenge_methods_supported":["plain","S256"],"request_parameter_supported":true})) - }), -) + rest.get('http://api/.well-known/openid-configuration', (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + issuer: 'https://demo.identityserver.io', + jwks_uri: 'https://demo.identityserver.io/.well-known/openid-configuration/jwks', + authorization_endpoint: 'https://demo.identityserver.io/connect/authorize', + token_endpoint: 'https://demo.identityserver.io/connect/token', + userinfo_endpoint: 'https://demo.identityserver.io/connect/userinfo', + end_session_endpoint: 'https://demo.identityserver.io/connect/endsession', + check_session_iframe: 'https://demo.identityserver.io/connect/checksession', + revocation_endpoint: 'https://demo.identityserver.io/connect/revocation', + introspection_endpoint: 'https://demo.identityserver.io/connect/introspect', + device_authorization_endpoint: 'https://demo.identityserver.io/connect/deviceauthorization', + frontchannel_logout_supported: true, + frontchannel_logout_session_supported: true, + backchannel_logout_supported: true, + backchannel_logout_session_supported: true, + scopes_supported: [ + 'openid', + 'profile', + 'email', + 'api', + 'api.scope1', + 'api.scope2', + 'scope2', + 'policyserver.runtime', + 'policyserver.management', + 'offline_access', + ], + claims_supported: [ + 'sub', + 'name', + 'family_name', + 'given_name', + 'middle_name', + 'nickname', + 'preferred_username', + 'profile', + 'picture', + 'website', + 'gender', + 'birthdate', + 'zoneinfo', + 'locale', + 'updated_at', + 'email', + 'email_verified', + ], + grant_types_supported: [ + 'authorization_code', + 'client_credentials', + 'refresh_token', + 'implicit', + 'password', + 'urn:ietf:params:oauth:grant-type:device_code', + ], + response_types_supported: [ + 'code', + 'token', + 'id_token', + 'id_token token', + 'code id_token', + 'code token', + 'code id_token token', + ], + response_modes_supported: ['form_post', 'query', 'fragment'], + token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'], + id_token_signing_alg_values_supported: ['RS256'], + subject_types_supported: ['public'], + code_challenge_methods_supported: ['plain', 'S256'], + request_parameter_supported: true, + }), + ); + }), +); -// @ts-ignore -global.window["crypto"]={ - // @ts-ignore - getRandomValues:(buffer)=>{return ""}, - // @ts-ignore - subtle:{ - // @ts-ignore - digest:(algo, code) => {return Promise.resolve(new ArrayBuffer(32))}} -} -// @ts-ignore -const url = "http://dummy.com"; +global.window['crypto'] = { + // @ts-ignore + getRandomValues: () => { + return ''; + }, + // @ts-ignore + subtle: { + digest: () => { + return Promise.resolve(new ArrayBuffer(32)); + }, + }, +}; +const url = 'http://dummy.com'; Object.defineProperty(global.window, 'location', { - value: { - href: url - } + value: { + href: url, + }, }); -beforeAll(() => server.listen()) -afterEach(() => server.resetHandlers()) -afterAll(() => server.close()) +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); describe('Authenticating test suite', () => { - it('Load home page then login should log', async () => { - - const configuration = { - client_id: 'interactive.public.short', - redirect_uri: 'http://localhost:4200/authentication/callback', - scope: 'openid profile email api offline_access', - authority: 'http://api', - refresh_time_before_tokens_expiration_in_second: 70, - }; - // @ts-ignore - const { getByText } = render(); - await waitFor(() => getByText('GitHub @axa-fr/react-oidc')); - fireEvent.click(screen.getByText('Login')); - await waitFor(() => getByText('Authentication in progress')); - await sleepAsync(4000); - expect(global.window.location.href).toBe("https://demo.duendesoftware.com/connect/authorize?client_id=interactive.public.short&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauthentication%2Fcallback&scope=openid%20profile%20email%20api%20offline_access&response_type=code&youhou_demo=youhou&state=AAAAAAAAAAAAAAAA&nonce=AAAAAAAAAAAA&code_challenge=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&code_challenge_method=S256"); - }); + it('Load home page then login should log', async () => { + const configuration = { + client_id: 'interactive.public.short', + redirect_uri: 'http://localhost:4200/authentication/callback', + scope: 'openid profile email api offline_access', + authority: 'http://api', + refresh_time_before_tokens_expiration_in_second: 70, + }; + // @ts-ignore + render(); + await screen.findByText('GitHub @axa-fr/react-oidc'); + fireEvent.click(screen.getByText('Login')); + await screen.findByText('Authentication in progress'); + await sleepAsync(4000); + expect(global.window.location.href).toBe( + 'https://demo.duendesoftware.com/connect/authorize?client_id=interactive.public.short&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauthentication%2Fcallback&scope=openid%20profile%20email%20api%20offline_access&response_type=code&youhou_demo=youhou&state=AAAAAAAAAAAAAAAA&nonce=AAAAAAAAAAAA&code_challenge=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&code_challenge_method=S256', + ); + }); }); diff --git a/examples/react-oidc-demo/src/App.tsx b/examples/react-oidc-demo/src/App.tsx index 62d6615d5..5f3d32335 100644 --- a/examples/react-oidc-demo/src/App.tsx +++ b/examples/react-oidc-demo/src/App.tsx @@ -1,105 +1,138 @@ +import { OidcProvider, withOidcSecure } from '@axa-fr/react-oidc'; import React, { useReducer } from 'react'; import { BrowserRouter, NavLink, Route, Routes } from 'react-router-dom'; +import CodeExecutor from './CodeExecutor'; import { configurationIdentityServer } from './configurations.js'; import { FetchUserHoc, FetchUserHook } from './FetchUser.js'; import { Home } from './Home.js'; import { MultiAuthContainer } from './MultiAuth.js'; -import { OidcProvider, withOidcSecure } from '@axa-fr/react-oidc'; import { Profile, SecureProfile } from './Profile.js'; const OidcSecureHoc = withOidcSecure(Profile); -const getRandomInt = (max) => { +const getRandomInt = max => { return Math.floor(Math.random() * max); }; function reducer(state, action) { switch (action.type) { - case 'event': - { - const id = getRandomInt(9999999999999).toString(); - return [{ ...action.data, id, date: Date.now() }, ...state]; - } + case 'event': { + const id = getRandomInt(9999999999999).toString(); + return [{ ...action.data, id, date: Date.now() }, ...state]; + } default: throw new Error(); } } - function App() { - // eslint-disable-next-line @typescript-eslint/naming-convention const [show, setShow] = React.useState(false); const [events, dispatch] = useReducer(reducer, []); const onEvent = (configurationName, eventName, data) => { - // console.log(`oidc:${configurationName}:${eventName}`, data); + // console.log(`oidc:${configurationName}:${eventName}`, data); dispatch({ type: 'event', data: { name: `oidc:${configurationName}:${eventName}`, data } }); }; - return (<> + return ( + <> + + + - - - - -
- - } /> - } /> - } /> - } /> - } /> - } /> - } /> - -
+
+
-
-
-
+
-
+
Default configuration Events
{events.map(e => { const date = new Date(e.date); const dateFormated = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; - return

{dateFormated} {e.name}: { JSON.stringify(e.data)}

; + return ( +

+ {dateFormated} {e.name}: {JSON.stringify(e.data)} +

+ ); })}
-
+
+ + ); } diff --git a/examples/react-oidc-demo/src/CodeExecutor.tsx b/examples/react-oidc-demo/src/CodeExecutor.tsx new file mode 100644 index 000000000..1767b1df1 --- /dev/null +++ b/examples/react-oidc-demo/src/CodeExecutor.tsx @@ -0,0 +1,41 @@ +import React, { useState } from 'react'; + +const CodeExecutor: React.FC = () => { + const [code, setCode] = useState(''); + const [output, setOutput] = useState(''); + + const executeCode = () => { + try { + const result = eval(` + (function() { + ${code} + })() + `); + setOutput(String(result)); + } catch (error) { + setOutput(`Erreur : ${(error as Error).message}`); + } + }; + + return ( +
+

Execute your JavaScript Code

+ + +
+

Result :

+
{output}
+
+
+ ); +}; + +export default CodeExecutor; diff --git a/examples/react-oidc-demo/src/FetchUser.spec.tsx b/examples/react-oidc-demo/src/FetchUser.spec.tsx new file mode 100644 index 000000000..e3373cddf --- /dev/null +++ b/examples/react-oidc-demo/src/FetchUser.spec.tsx @@ -0,0 +1,76 @@ +// @vitest-environment jsdom +import type { Fetch } from '@axa-fr/react-oidc'; +import { OidcSecure, useOidcFetch } from '@axa-fr/react-oidc'; +import { act, cleanup, render, screen } from '@testing-library/react'; +import type { ComponentType, PropsWithChildren } from 'react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { FetchUserHook } from './FetchUser'; + +vi.mock('@axa-fr/react-oidc', () => ({ + OidcSecure: vi.fn(({ children }: PropsWithChildren) => <>{children}), + useOidcFetch: vi.fn(), + withOidcFetch: (fetch: Fetch) => (Component: ComponentType<{ fetch: Fetch }>) => + function WithFetch() { + return ; + }, +})); + +describe('user-info fetch demo', () => { + const authenticatedFetch = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + authenticatedFetch.mockReset(); + vi.mocked(useOidcFetch).mockReturnValue({ fetch: authenticatedFetch }); + }); + + afterEach(() => { + cleanup(); + }); + + it('shows loading until the response arrives, then displays the user', async () => { + let resolveResponse: (response: Response) => void; + authenticatedFetch.mockReturnValue( + new Promise(resolve => { + resolveResponse = resolve; + }), + ); + const user = { sub: 'example', name: 'Example user' }; + + render(); + expect(screen.getByText('Loading')).toBeDefined(); + expect(useOidcFetch).toHaveBeenCalledWith(window.fetch, 'custom', true); + expect(vi.mocked(OidcSecure).mock.calls[0][0].configurationName).toBe('custom'); + expect(authenticatedFetch).toHaveBeenCalledExactlyOnceWith( + 'https://demo.duendesoftware.com/connect/userinfo', + ); + + await act(async () => resolveResponse(new Response(JSON.stringify(user)))); + + expect(screen.queryByText('Loading')).toBeNull(); + expect(screen.getByText(JSON.stringify(user))).toBeDefined(); + }); + + it.each([201, 400, 401, 500])('does not display user data for HTTP %i', async status => { + const user = { sub: 'example' }; + authenticatedFetch.mockResolvedValue(new Response(JSON.stringify(user), { status })); + + render(); + + await screen.findByText('User information'); + expect(screen.queryByText('Loading')).toBeNull(); + expect(screen.queryByText(JSON.stringify(user))).toBeNull(); + }); + + it('defaults proof of possession to false and does not refetch on rerender', async () => { + authenticatedFetch.mockResolvedValue(new Response('{}')); + const { rerender } = render(); + await screen.findByText('User information'); + + rerender(); + + expect(useOidcFetch).toHaveBeenCalledWith(window.fetch, undefined, false); + expect(authenticatedFetch).toHaveBeenCalledOnce(); + }); +}); diff --git a/examples/react-oidc-demo/src/FetchUser.tsx b/examples/react-oidc-demo/src/FetchUser.tsx index e1e7fd920..81e6e91ba 100644 --- a/examples/react-oidc-demo/src/FetchUser.tsx +++ b/examples/react-oidc-demo/src/FetchUser.tsx @@ -1,52 +1,70 @@ -import React, { useEffect, useState } from 'react'; -import { OidcSecure, useOidcFetch, withOidcFetch } from '@axa-fr/react-oidc'; - -const DisplayUserInfo = ({ fetch }) => { - const [oidcUser, setOidcUser] = useState(null); - const [isLoading, setLoading] = useState(true); - - useEffect(() => { - const fetchUserInfoAsync = async () => { - const res = await fetch('https://demo.duendesoftware.com/connect/userinfo'); - if (res.status !== 200) { - return null; - } - return res.json(); - }; - let isMounted = true; - fetchUserInfoAsync().then((userInfo) => { - if (isMounted) { - setLoading(false); - setOidcUser(userInfo); - } - }); - return () => { - isMounted = false; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (isLoading) { - return <>Loading; - } - - return ( -
-
-
-
User information
- {oidcUser != null &&

{JSON.stringify(oidcUser)}

} -
-
+import { + type Fetch, + OidcSecure, + type OidcUserInfo, + useOidcFetch, + withOidcFetch, +} from '@axa-fr/react-oidc'; +import React, { type ReactElement, useEffect, useState } from 'react'; + +const fetchUserInfoAsync = async (fetch: Fetch): Promise => { + const response = await fetch('https://demo.duendesoftware.com/connect/userinfo'); + return response.status === 200 ? response.json() : null; +}; + +const DisplayUserInfo = ({ fetch }: { fetch: Fetch }): ReactElement => { + const [oidcUser, setOidcUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + let isMounted = true; + fetchUserInfoAsync(fetch).then(userInfo => { + if (isMounted) { + setIsLoading(false); + setOidcUser(userInfo); + } + }); + return () => { + isMounted = false; + }; + }, []); + + if (isLoading) { + return <>Loading; + } + + return ( +
+
+
+
User information
+ {oidcUser != null &&

{JSON.stringify(oidcUser)}

}
- ); +
+
+ ); }; const UserInfoWithFetchHoc = withOidcFetch(fetch)(DisplayUserInfo); -export const FetchUserHoc = () => ; +export const FetchUserHoc = () => ( + + + +); -export const FetchUserHook = () => { - const { fetch } = useOidcFetch(); - return ; +export const FetchUserHook = (props: { + configurationName?: string; + demonstratingProofOfPossession?: boolean; +}): ReactElement => { + const { fetch } = useOidcFetch( + window.fetch, + props.configurationName, + props.demonstratingProofOfPossession ?? false, + ); + return ( + + + + ); }; diff --git a/examples/react-oidc-demo/src/Home.tsx b/examples/react-oidc-demo/src/Home.tsx index 420695fa5..2b9d269c7 100644 --- a/examples/react-oidc-demo/src/Home.tsx +++ b/examples/react-oidc-demo/src/Home.tsx @@ -1,23 +1,81 @@ -import React from 'react'; - import { useOidc } from '@axa-fr/react-oidc'; +import React from 'react'; +import { useNavigate } from 'react-router-dom'; export const Home = () => { - const { login, logout, renewTokens, isAuthenticated } = useOidc(); + const { login, logout, renewTokens, isAuthenticated } = useOidc(); + const navigate = useNavigate(); + + const navigateProfile = () => { + navigate('/profile'); + }; - return ( -
-
-
-
Home
-

React Demo Application protected by OpenId Connect. More info on about oidc on GitHub @axa-fr/react-oidc

- {!isAuthenticated &&

} - {isAuthenticated &&

} - {isAuthenticated &&

} - {isAuthenticated &&

} - {isAuthenticated &&

} -
-
+ return ( +
+
+
+
Home
+

+ React Demo Application protected by OpenId Connect. More info on about oidc on{' '} + GitHub @axa-fr/react-oidc +

+ {!isAuthenticated && ( +

+ +

+ )} + {isAuthenticated && ( +

+ +

+ )} + {isAuthenticated && ( +

+ +

+ )} + {isAuthenticated && ( +

+ +

+ )} + {isAuthenticated && ( +

+ +

+ )} + {isAuthenticated && ( +

+ +

+ )} +

+ +

- ); +
+
+ ); }; diff --git a/examples/react-oidc-demo/src/MultiAuth.tsx b/examples/react-oidc-demo/src/MultiAuth.tsx index 85a88266c..1bf2550e2 100644 --- a/examples/react-oidc-demo/src/MultiAuth.tsx +++ b/examples/react-oidc-demo/src/MultiAuth.tsx @@ -1,7 +1,18 @@ +import { + Fetch, + OidcProvider, + useOidc, + useOidcAccessToken, + useOidcIdToken, +} from '@axa-fr/react-oidc'; import React, { useReducer, useState } from 'react'; -import { configurationGoogle, configurationIdentityServer, configurationIdentityServerWithHash } from './configurations'; -import { Fetch, OidcProvider, useOidc, useOidcAccessToken, useOidcIdToken } from '@axa-fr/react-oidc'; +import { + configurationGoogle, + configurationIdentityServer, + configurationIdentityServerWithHash, +} from './configurations'; +import { FetchUserHook } from './FetchUser'; import AuthenticatingError from './override/AuthenticateError.component'; import Authenticating from './override/Authenticating.component'; import { CallBackSuccess } from './override/Callback.component'; @@ -9,182 +20,280 @@ import Loading from './override/Loading.component'; import ServiceWorkerNotSupported from './override/ServiceWorkerNotSupported.component'; import SessionLost from './override/SessionLost.component'; -const fetchWithLogs = (fetch: Fetch) => async (...params: Parameters) => { +const fetchWithLogs = + (fetch: Fetch) => + async (...params: Parameters) => { const [url, options, ...rest] = params; console.log('fetchWithLogs', url, options, ...rest); return await fetch(url, options, ...rest); -}; + }; const MultiAuth = ({ configurationName, handleConfigurationChange }) => { - const { login, logout, isAuthenticated } = useOidc(configurationName); - const { isAuthenticated: isAuthenticatedDefault } = useOidc('default'); - const [fname, setFname] = useState(''); + const { login, logout, isAuthenticated } = useOidc(configurationName); + const { isAuthenticated: isAuthenticatedDefault } = useOidc('default'); + const [fname, setFname] = useState(''); - const handleChange = e => { - setFname(e.target.value); - }; - return ( -
-
-
-
Multiple Authentication
-
- -
-

React Demo Application protected by OpenId Connect with MultipleAuthentication. -
For example, config_1 can have other sensitive scope, config_2 does not ask for the "offline_access" so it does not retrieve the most sensitive token "refresh_token" for very sensitive operation, it retrive only access_token valid for a small amout of time.

- - {!isAuthenticated && } - {isAuthenticatedDefault && } - {isAuthenticated && } -
-
+ const handleChange = e => { + setFname(e.target.value); + }; + return ( +
+
+
+
Multiple Authentication
+
+ +
+

+ React Demo Application protected by OpenId Connect with MultipleAuthentication. +
+ For example, config_1 can have other sensitive scope, config_2 does not ask for the + "offline_access" so it does not retrieve the most sensitive token + "refresh_token" for very sensitive operation, it retrive only access_token + valid for a small amout of time. +

+ + {!isAuthenticated && ( + + )} + {isAuthenticatedDefault && ( + + )} + {isAuthenticated && ( + + )}
- ); +
+
+ ); }; if (!sessionStorage.configurationName) { - sessionStorage.configurationName = 'config_classic'; + sessionStorage.configurationName = 'config_classic'; } -const getRandomInt = (max) => { - return Math.floor(Math.random() * max); +const getRandomInt = max => { + return Math.floor(Math.random() * max); }; function reducer(state, action) { - switch (action.type) { - case 'event': - { - const id = getRandomInt(9999999999999).toString(); - return [{ ...action.data, id, date: Date.now() }, ...state]; - } - default: - throw new Error(); + switch (action.type) { + case 'event': { + const id = getRandomInt(9999999999999).toString(); + return [{ ...action.data, id, date: Date.now() }, ...state]; } + default: + throw new Error(); + } } export const MultiAuthContainer = () => { - const [isSessionLost, setIsSessionLost] = useState(false); - const [configurationName, setConfigurationName] = useState(sessionStorage.configurationName); - const [events, dispatch] = useReducer(reducer, []); - const callBack = window.location.origin + '/multi-auth/authentification/callback2'; - const silent_redirect_uri = window.location.origin + '/multi-auth/authentification/silent-callback2'; - const configurations = { - config_classic: { - ...configurationIdentityServer, - redirect_uri: callBack, - silent_redirect_uri, - scope: 'openid profile email api offline_access', - client_id: 'interactive.public.short', - }, - config_without_refresh_token: { - ...configurationIdentityServer, - redirect_uri: callBack, - silent_redirect_uri, - scope: 'openid profile email api', - }, - config_without_silent_login: { - ...configurationIdentityServer, - redirect_uri: callBack, - silent_redirect_uri: '', - scope: 'openid profile email api offline_access', - }, - config_without_refresh_token_silent_login: { - ...configurationIdentityServer, - redirect_uri: callBack, - silent_redirect_uri: '', - scope: 'openid profile email api', -}, - config_show_access_token: { - ...configurationIdentityServer, - redirect_uri: callBack, - silent_redirect_uri, - }, - config_google: { ...configurationGoogle }, - config_with_hash: { ...configurationIdentityServerWithHash }, - config_separate_oidc_access_token_domains: { - ...configurationIdentityServer, - redirect_uri: callBack, - silent_redirect_uri, - }, - }; - const handleConfigurationChange = (event) => { - const configurationName = event.target.value; - sessionStorage.configurationName = configurationName; - setConfigurationName(configurationName); - }; + const [isSessionLost, setIsSessionLost] = useState(false); + const [configurationName, setConfigurationName] = useState(sessionStorage.configurationName); + const [events, dispatch] = useReducer(reducer, []); + const callBack = window.location.origin + '/multi-auth/authentification/callback'; + const silent_redirect_uri = + window.location.origin + '/multi-auth/authentification/silent-callback'; + const configurations = { + config_classic: { + ...configurationIdentityServer, + redirect_uri: callBack, + silent_redirect_uri, + scope: 'openid profile email api offline_access', + client_id: 'interactive.public.short', + }, + config_without_refresh_token: { + ...configurationIdentityServer, + redirect_uri: callBack, + silent_redirect_uri, + scope: 'openid profile email api', + }, + config_without_silent_login: { + ...configurationIdentityServer, + redirect_uri: callBack, + silent_redirect_uri: '', + scope: 'openid profile email api offline_access', + }, + config_with_monitor_session: { + ...configurationIdentityServer, + redirect_uri: callBack, + silent_redirect_uri, + monitor_session: true, + }, + config_without_refresh_token_silent_login: { + ...configurationIdentityServer, + redirect_uri: callBack, + silent_redirect_uri: '', + scope: 'openid profile email api', + }, + config_show_access_token: { + ...configurationIdentityServer, + redirect_uri: callBack, + silent_redirect_uri, + }, + config_google: { ...configurationGoogle }, + config_with_hash: { ...configurationIdentityServerWithHash }, + config_separate_oidc_access_token_domains: { + ...configurationIdentityServer, + redirect_uri: callBack, + silent_redirect_uri, + }, + config_with_dpop: { + ...configurationIdentityServer, + redirect_uri: callBack, + silent_redirect_uri, + demonstrating_proof_of_possession: true, + /*demonstrating_proof_of_possession_configuration: { + importKeyAlgorithm: { + name: "RSASSA-PKCS1-v1_5", + hash: { name: "SHA-256" }, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512" + }, + signAlgorithm: { name: "RSASSA-PKCS1-v1_5" }, + generateKeyAlgorithm: { + name: "RSASSA-PKCS1-v1_5", + modulusLength: 2048, //can be 1024, 2048, or 4096 + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: { name: "SHA-256" }, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512" + }, + digestAlgorithm: { name: "SHA-256" }, + jwtHeaderAlgorithm: "RS256", + },*/ + }, + config_multi_tab_login: { + ...configurationIdentityServer, + redirect_uri: callBack, + silent_redirect_uri, + scope: 'openid profile email api offline_access', + client_id: 'interactive.public.short', + }, + }; + const handleConfigurationChange = event => { + const configurationName = event.target.value; + sessionStorage.configurationName = configurationName; + setConfigurationName(configurationName); + }; - const onSessionLost = () => { - setIsSessionLost(true); - }; - const onEvent = (configurationName, eventName, data) => { - // console.log(`oidc:${configurationName}:${eventName}`, data); - dispatch({ type: 'event', data: { name: `oidc:${configurationName}:${eventName}`, data } }); - }; + const onSessionLost = () => { + setIsSessionLost(true); + }; + const onEvent = (configurationName, eventName, data) => { + // console.log(`oidc:${configurationName}:${eventName}`, data); + dispatch({ type: 'event', data: { name: `oidc:${configurationName}:${eventName}`, data } }); + }; - return ( - <> - fetchWithLogs(fetch)} - > - { isSessionLost && } - - - -
-
-
-
Current configuration Events
-
- {events.map(e => { - const date = new Date(e.date); - const dateFormated = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; - return

{dateFormated} {e.name}: { JSON.stringify(e.data)}

; - })} -
-
-
+ return ( + <> + fetchWithLogs(fetch)} + > + {isSessionLost && } + + + +
+
+
+
Current configuration Events
+
+ {events.map(e => { + const date = new Date(e.date); + const dateFormated = `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; + return ( +

+ {dateFormated} {e.name}: {JSON.stringify(e.data)} +

+ ); + })}
+
+
+
- ); + ); }; const DisplayAccessToken = ({ configurationName }) => { - const { accessToken, accessTokenPayload } = useOidcAccessToken(configurationName); - const { idTokenPayload } = useOidcIdToken(configurationName); - - if (!accessToken) { - return

you are not authentified

; - } - return ( -
-
-
Access Token
-

Please consider to configure the ServiceWorker in order to protect your application from XSRF attacks. "access_token" and "refresh_token" will never be accessible from your client side javascript.

- {

Access Token: {JSON.stringify(accessToken)}

} - {accessTokenPayload != null &&

Access Token Payload: {JSON.stringify(accessTokenPayload)}

} -
Id Token
- {idTokenPayload != null &&

Access Token Payload: {JSON.stringify(idTokenPayload)}

} -
+ const { accessToken, accessTokenPayload } = useOidcAccessToken(configurationName); + const { idTokenPayload } = useOidcIdToken(configurationName); + const demonstratingProofOfPossession = configurationName == 'config_with_dpop'; + if (!accessToken) { + return

you are not authentified

; + } + return ( + <> +
+
+
Access Token
+

+ Please consider to configure the ServiceWorker in order to protect your application from + XSRF attacks. "access_token" and "refresh_token" will never be + accessible from your client side javascript. +

+ {

Access Token: {JSON.stringify(accessToken)}

} + {accessTokenPayload != null && ( +

Access Token Payload: {JSON.stringify(accessTokenPayload)}

+ )} +
Id Token
+ {idTokenPayload != null && ( +

Access Token Payload: {JSON.stringify(idTokenPayload)}

+ )}
- ); +
+ + + ); }; diff --git a/examples/react-oidc-demo/src/Profile.tsx b/examples/react-oidc-demo/src/Profile.tsx index dd2cfcb95..0ac98e57d 100644 --- a/examples/react-oidc-demo/src/Profile.tsx +++ b/examples/react-oidc-demo/src/Profile.tsx @@ -1,77 +1,109 @@ +import { + OidcSecure, + type OidcUserInfo, + OidcUserStatus, + useOidc, + useOidcAccessToken, + useOidcIdToken, + useOidcUser, +} from '@axa-fr/react-oidc'; import React from 'react'; -import { type OidcUserInfo, OidcSecure, OidcUserStatus, useOidcAccessToken, useOidcIdToken, useOidcUser } from '@axa-fr/react-oidc'; -interface OidcUserRoleInfo extends OidcUserInfo{ - role?: string[]; +interface OidcUserRoleInfo extends OidcUserInfo { + role?: string[]; } const DisplayUserInfo = () => { - const { oidcUser, oidcUserLoadingState, reloadOidcUser } = useOidcUser(); + const { oidcUser, oidcUserLoadingState, reloadOidcUser } = useOidcUser(); - switch (oidcUserLoadingState) { - case OidcUserStatus.Loading: - return

User Information are loading

; - case OidcUserStatus.Unauthenticated: - return

you are not authenticated

; - case OidcUserStatus.LoadingError: - return

Fail to load user information

; - default: - return ( -
-
-
User information
-

{JSON.stringify(oidcUser)}

-

-
-
- ); - } + switch (oidcUserLoadingState) { + case OidcUserStatus.Loading: + return

User Information are loading

; + case OidcUserStatus.Unauthenticated: + return

you are not authenticated

; + case OidcUserStatus.LoadingError: + return

Fail to load user information

; + default: + return ( +
+
+
User information
+

{JSON.stringify(oidcUser)}

+

+ +

+
+
+ ); + } }; export const Profile = () => { - return ( -
- - - -
- ); + const { logout, isAuthenticated } = useOidc(); + return ( +
+ {isAuthenticated && ( +

+ +

+ )} + + + +
+ ); }; const DisplayAccessToken = () => { - const { accessToken, accessTokenPayload } = useOidcAccessToken(); + const { accessToken, accessTokenPayload } = useOidcAccessToken(); - if (!accessToken) { - return

you are not authenticated

; - } - return ( -
-
-
Access Token
-

Please consider to configure the ServiceWorker in order to protect your application from XSRF attacks. "access_token" and "refresh_token" will never be accessible from your client side javascript.

- {

Access Token: {JSON.stringify(accessToken)}

} - {accessTokenPayload != null &&

Access Token Payload: {JSON.stringify(accessTokenPayload)}

} -
-
- ); + if (!accessToken) { + return

you are not authenticated

; + } + return ( +
+
+
Access Token
+

+ Please consider to configure the ServiceWorker in order to protect your application from + XSRF attacks. "access_token" and "refresh_token" will never be + accessible from your client side javascript. +

+ {

Access Token: {JSON.stringify(accessToken)}

} + {accessTokenPayload != null && ( +

Access Token Payload: {JSON.stringify(accessTokenPayload)}

+ )} +
+
+ ); }; const DisplayIdToken = () => { - const { idToken, idTokenPayload } = useOidcIdToken(); + const { idToken, idTokenPayload } = useOidcIdToken(); - if (!idToken) { - return

you are not authenticated

; - } + if (!idToken) { + return

you are not authenticated

; + } - return ( -
-
-
ID Token
- {

IdToken: {JSON.stringify(idToken)}

} - {idTokenPayload != null &&

IdToken Payload: {JSON.stringify(idTokenPayload)}

} -
-
- ); + return ( +
+
+
ID Token
+ {

IdToken: {JSON.stringify(idToken)}

} + {idTokenPayload != null && ( +

IdToken Payload: {JSON.stringify(idTokenPayload)}

+ )} +
+
+ ); }; -export const SecureProfile = () => ; +export const SecureProfile = () => ( + + + +); diff --git a/examples/react-oidc-demo/src/configurations.ts b/examples/react-oidc-demo/src/configurations.ts index 99526d406..213908d60 100644 --- a/examples/react-oidc-demo/src/configurations.ts +++ b/examples/react-oidc-demo/src/configurations.ts @@ -1,91 +1,67 @@ -import { TokenRenewMode } from '@axa-fr/react-oidc'; +import { TokenAutomaticRenewMode, TokenRenewMode } from '@axa-fr/react-oidc'; export const configurationIdentityServer = { - client_id: 'interactive.public.short', - redirect_uri: window.location.origin + '/authentication/callback', - silent_redirect_uri: window.location.origin + '/authentication/silent-callback', - // silent_login_uri: window.location.origin + '/authentication/silent-login', - scope: 'openid profile email api offline_access', - authority: 'https://demo.duendesoftware.com', - // authority_time_cache_wellknowurl_in_second: 60* 60, - refresh_time_before_tokens_expiration_in_second: 40, - service_worker_relative_url: '/OidcServiceWorker.js', - service_worker_only: false, - // storage: localStorage, - // silent_login_timeout: 3333000 - // monitor_session: true, - extras: { youhou_demo: 'youhou' }, - token_renew_mode: TokenRenewMode.access_token_invalid, -}; - -export const configurationIdentityServer1 = { - client_id: 'balosar-blazo', - redirect_uri: window.location.origin + '/authentication/callback', - silent_redirect_uri: window.location.origin + '/authentication/silent-callback', - // silent_login_uri: window.location.origin + '/authentication/silent-login', - scope: 'openid offline_access profile email', - authority: 'https://localhost:44310', - // authority_time_cache_wellknowurl_in_second: 60* 60, - refresh_time_before_tokens_expiration_in_second: 40, - // service_worker_relative_url: '/OidcServiceWorker.js', - service_worker_only: false, - // storage: localStorage, - // silent_login_timeout: 3333000 - // monitor_session: true, - token_renew_mode: TokenRenewMode.access_token_invalid, + client_id: 'interactive.public.short', + redirect_uri: window.location.origin + '/authentication/callback', + silent_redirect_uri: window.location.origin + '/authentication/silent-callback', + scope: 'openid profile email api offline_access', + authority: 'https://demo.duendesoftware.com', + par: 'auto' as const, + refresh_time_before_tokens_expiration_in_second: 40, + token_renew_mode: TokenRenewMode.access_token_invalid, + token_automatic_renew_mode: TokenAutomaticRenewMode.AutomaticBeforeTokenExpiration, + service_worker_relative_url: '/OidcServiceWorker.js', + service_worker_only: false, }; export const configurationIdentityServerWithHash = { - client_id: 'interactive.public.short', - redirect_uri: window.location.origin + '/multi-auth/authentification#authentication-callback', - silent_redirect_uri: window.location.origin + '/multi-auth/authentification#authentication-silent-callback', - silent_login_uri: window.location.origin + '/multi-auth/authentification#authentication-silent-login', - scope: 'openid profile email api offline_access', - authority: 'https://demo.duendesoftware.com', - refresh_time_before_tokens_expiration_in_second: 10, - service_worker_relative_url: '/OidcServiceWorker.js', - service_worker_only: false, + client_id: 'interactive.public.short', + redirect_uri: window.location.origin + '/multi-auth/authentification#authentication-callback', + silent_redirect_uri: + window.location.origin + '/multi-auth/authentification#authentication-silent-callback', + silent_login_uri: + window.location.origin + '/multi-auth/authentification#authentication-silent-login', + scope: 'openid profile email api offline_access', + authority: 'https://demo.duendesoftware.com', + par: 'auto' as const, + refresh_time_before_tokens_expiration_in_second: 10, + service_worker_relative_url: '/OidcServiceWorker.js', + service_worker_only: false, }; export const configurationIdentityServerWithoutDiscovery = { - client_id: 'interactive.public.short', - redirect_uri: window.location.origin + '/authentication/callback', - silent_redirect_uri: window.location.origin + '/authentication/silent-callback', - scope: 'openid profile email api offline_access', - authority: 'https://demo.duendesoftware.com', - authority_configuration: { - authorization_endpoint: 'https://demo.duendesoftware.com/connect/authorize', - token_endpoint: 'https://demo.duendesoftware.com/connect/token', - userinfo_endpoint: 'https://demo.duendesoftware.com/connect/userinfo', - end_session_endpoint: 'https://demo.duendesoftware.com/connect/endsession', - revocation_endpoint: 'https://demo.duendesoftware.com/connect/revocation', - check_session_iframe: 'https://demo.duendesoftware.com/connect/checksession', - }, - refresh_time_before_tokens_expiration_in_second: 10, - service_worker_relative_url: '/OidcServiceWorker.js', - service_worker_only: false, -}; - -export const configurationAuth0 = { - client_id: 'xGZxEAJhzlkuQUlWl90y1ntIX-0UDWHx', - redirect_uri: window.location.origin + '/callback', - scope: 'openid profile email api offline_access', - authority: 'https://kdhttps.auth0.com', - refresh_time_before_tokens_expiration_in_second: 10, - service_worker_relative_url: '/OidcServiceWorker.js', - service_worker_only: false, + client_id: 'interactive.public.short', + redirect_uri: window.location.origin + '/authentication/callback', + silent_redirect_uri: window.location.origin + '/authentication/silent-callback', + scope: 'openid profile email api offline_access', + authority: 'https://demo.duendesoftware.com', + par: 'auto' as const, + authority_configuration: { + authorization_endpoint: 'https://demo.duendesoftware.com/connect/authorize', + pushed_authorization_request_endpoint: 'https://demo.duendesoftware.com/connect/par', + token_endpoint: 'https://demo.duendesoftware.com/connect/token', + userinfo_endpoint: 'https://demo.duendesoftware.com/connect/userinfo', + end_session_endpoint: 'https://demo.duendesoftware.com/connect/endsession', + revocation_endpoint: 'https://demo.duendesoftware.com/connect/revocation', + check_session_iframe: 'https://demo.duendesoftware.com/connect/checksession', + issuer: 'https://demo.duendesoftware.com', + }, + refresh_time_before_tokens_expiration_in_second: 10, + service_worker_relative_url: '/OidcServiceWorker.js', + service_worker_only: false, }; export const configurationGoogle = { - client_id: '908893276222-f2drloh56ll0g99md38lv2k810d0nk0p.apps.googleusercontent.com', - redirect_uri: `${window.location.origin}/multi-auth/callback-google`, - silent_redirect_uri: window.location.origin + '/multi-auth/silent-callback-google', - scope: 'openid profile email', - authority: 'https://accounts.google.com/', - service_worker_relative_url: '/OidcServiceWorker.js', - service_worker_only: false, - token_request_extras: { - client_secret: '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', - }, - monitor_session: false, + client_id: '908893276222-f2drloh56ll0g99md38lv2k810d0nk0p.apps.googleusercontent.com', + redirect_uri: `${window.location.origin}/multi-auth/callback-google`, + silent_redirect_uri: window.location.origin + '/multi-auth/silent-callback-google', + scope: 'openid profile email', + authority: 'https://accounts.google.com/', + par: 'auto' as const, + service_worker_relative_url: '/OidcServiceWorker.js', + service_worker_only: false, + token_request_extras: { + client_secret: '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', + }, + monitor_session: false, }; diff --git a/examples/react-oidc-demo/src/index.css b/examples/react-oidc-demo/src/index.css index ec2585e8c..89e57c7cc 100644 --- a/examples/react-oidc-demo/src/index.css +++ b/examples/react-oidc-demo/src/index.css @@ -1,13 +1,11 @@ body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', + 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } diff --git a/examples/react-oidc-demo/src/index.tsx b/examples/react-oidc-demo/src/index.tsx index 2583fb626..103bf6bb2 100644 --- a/examples/react-oidc-demo/src/index.tsx +++ b/examples/react-oidc-demo/src/index.tsx @@ -8,4 +8,8 @@ import App from './App'; const container = document.getElementById('root'); const root = createRoot(container); -root.render(); +root.render( + + + , +); diff --git a/examples/react-oidc-demo/src/override/AuthenticateError.component.tsx b/examples/react-oidc-demo/src/override/AuthenticateError.component.tsx index 90678b533..abe4434e9 100644 --- a/examples/react-oidc-demo/src/override/AuthenticateError.component.tsx +++ b/examples/react-oidc-demo/src/override/AuthenticateError.component.tsx @@ -3,12 +3,12 @@ import { ComponentType } from 'react'; import { style } from './style.js'; const AuthenticatingError: ComponentType = ({ configurationName }) => ( -
-
-

Error authentication for {configurationName}

-

An error occurred during authentication.

-
-
+
+
+

Error authentication for {configurationName}

+

An error occurred during authentication.

+
+
); export default AuthenticatingError; diff --git a/examples/react-oidc-demo/src/override/Authenticating.component.tsx b/examples/react-oidc-demo/src/override/Authenticating.component.tsx index ff39ba67d..0341dfb4e 100644 --- a/examples/react-oidc-demo/src/override/Authenticating.component.tsx +++ b/examples/react-oidc-demo/src/override/Authenticating.component.tsx @@ -2,10 +2,12 @@ import { PropsWithChildren } from 'react'; import { style } from './style'; -const Authenticating : PropsWithChildren = ({ configurationName }) => ( -
+const Authenticating: PropsWithChildren = ({ configurationName }) => ( +
-

Authentication in progress for {configurationName}

+

+ Authentication in progress for {configurationName} +

You will be redirected to the login page.

diff --git a/examples/react-oidc-demo/src/override/Callback.component.tsx b/examples/react-oidc-demo/src/override/Callback.component.tsx index a814590f5..e2dd98838 100644 --- a/examples/react-oidc-demo/src/override/Callback.component.tsx +++ b/examples/react-oidc-demo/src/override/Callback.component.tsx @@ -2,13 +2,14 @@ import { ComponentType } from 'react'; import { style } from './style.js'; -export const CallBackSuccess: ComponentType = ({ configurationName }) => (<>
-
-

Authentication complete for {configurationName}

-

You will be redirected to your application.

-
-
-
-
- +export const CallBackSuccess: ComponentType = ({ configurationName }) => ( + <> +
+
+

Authentication complete for {configurationName}

+

You will be redirected to your application.

+
+
+
+ ); diff --git a/examples/react-oidc-demo/src/override/Loading.component.tsx b/examples/react-oidc-demo/src/override/Loading.component.tsx index f5e35dc5e..5e037ae7b 100644 --- a/examples/react-oidc-demo/src/override/Loading.component.tsx +++ b/examples/react-oidc-demo/src/override/Loading.component.tsx @@ -2,8 +2,8 @@ import { ComponentType } from 'react'; import { style } from './style.js'; -const Loading : ComponentType = ({ configurationName }) => ( - +const Loading: ComponentType = ({ configurationName }) => ( + Loading for {configurationName} ); diff --git a/examples/react-oidc-demo/src/override/ServiceWorkerNotSupported.component.tsx b/examples/react-oidc-demo/src/override/ServiceWorkerNotSupported.component.tsx index 1e4be2e1c..1a11d624d 100644 --- a/examples/react-oidc-demo/src/override/ServiceWorkerNotSupported.component.tsx +++ b/examples/react-oidc-demo/src/override/ServiceWorkerNotSupported.component.tsx @@ -2,11 +2,16 @@ import { ComponentType } from 'react'; import { style } from './style.js'; -const ServiceWorkerNotSupported : ComponentType = ({ configurationName }) => ( +const ServiceWorkerNotSupported: ComponentType = ({ configurationName }) => (
-

Unable to authenticate on this browser for {configurationName}

-

Your browser is not secure enough to make authentication work. Try updating your browser or use a newer browser.

+

+ Unable to authenticate on this browser for {configurationName} +

+

+ Your browser is not secure enough to make authentication work. Try updating your browser or + use a newer browser. +

); diff --git a/examples/react-oidc-demo/src/override/SessionLost.component.tsx b/examples/react-oidc-demo/src/override/SessionLost.component.tsx index b90000eab..1b932023e 100644 --- a/examples/react-oidc-demo/src/override/SessionLost.component.tsx +++ b/examples/react-oidc-demo/src/override/SessionLost.component.tsx @@ -1,22 +1,32 @@ +import { useOidc } from '@axa-fr/react-oidc'; import { ComponentType } from 'react'; +import { useNavigate } from 'react-router-dom'; -import { useOidc } from '@axa-fr/react-oidc'; import { style } from './style.js'; export const SessionLost: ComponentType = ({ configurationName }) => { - const { login } = useOidc(configurationName); + const { login } = useOidc(configurationName); + const navigate = useNavigate(); + const navigateProfile = () => { + navigate('/profile'); + }; - return ( -
-
-

Session timed out for {configurationName}

-

- Your session has expired. Please re-authenticate. -

- -
+ return ( +
+
+

Session timed out for {configurationName}

+

+ Your session has expired. Please re-authenticate. +

+ +
- ); +
+ ); }; export default SessionLost; diff --git a/examples/react-oidc-demo/src/utils/sleep.ts b/examples/react-oidc-demo/src/utils/sleep.ts new file mode 100644 index 000000000..529268d5c --- /dev/null +++ b/examples/react-oidc-demo/src/utils/sleep.ts @@ -0,0 +1,3 @@ +export const sleepAsync = milliseconds => { + return new Promise(resolve => setTimeout(resolve, milliseconds)); +}; diff --git a/examples/react-oidc-demo/tsconfig.eslint.json b/examples/react-oidc-demo/tsconfig.eslint.json index e9041fd6b..b90fc83e0 100644 --- a/examples/react-oidc-demo/tsconfig.eslint.json +++ b/examples/react-oidc-demo/tsconfig.eslint.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "include": ["src"] -} \ No newline at end of file +} diff --git a/examples/react-oidc-demo/tsconfig.json b/examples/react-oidc-demo/tsconfig.json index cb88bdd34..393c2f9f2 100644 --- a/examples/react-oidc-demo/tsconfig.json +++ b/examples/react-oidc-demo/tsconfig.json @@ -1,11 +1,10 @@ -{ +{ "compilerOptions": { "target": "ES2019", "lib": ["ES2021", "DOM"], "outDir": "dist", - "baseUrl": "./", "jsx": "react-jsx", - "module": "CommonJS", + "module": "ESNext", "declaration": true, "declarationMap": true, "sourceMap": true, @@ -15,17 +14,14 @@ "strictFunctionTypes": false, "strictPropertyInitialization": false, "noImplicitThis": false, - "alwaysStrict": false, "noUnusedLocals": false, "noUnusedParameters": false, "noImplicitReturns": false, "noFallthroughCasesInSwitch": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true }, - "exclude": [ - "node_modules", - ] -} \ No newline at end of file + "exclude": ["node_modules"] +} diff --git a/examples/react-oidc-demo/vite.config.js b/examples/react-oidc-demo/vite.config.js index e7b88685e..197494e22 100644 --- a/examples/react-oidc-demo/vite.config.js +++ b/examples/react-oidc-demo/vite.config.js @@ -9,5 +9,10 @@ export default defineConfig({ build: { sourcemap: true, minify: false, - } + }, + server: { + headers: { + //"Content-Security-Policy": "script-src 'unsafe-inline' https://www.google-analitics.com;", + }, + }, }); diff --git a/package.json b/package.json index 830679489..662f5382d 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,8 @@ "license": "MIT", "workspaces": [ "packages/oidc-service-worker", - "packages/vanilla", - "packages/react", - "packages/vanilla-demo" + "packages/oidc-client", + "packages/react-oidc" ], "keywords": [ "react", @@ -22,26 +21,39 @@ "preinstall": "npx only-allow pnpm", "format": "prettier --write --cache .", "lint": "eslint --cache .", + "lint-fix": "eslint --cache --fix .", "outdated": "pnpm outdated -r", - "test": "pnpm run test --workspaces --if-present", + "test": "pnpm --filter=./packages/* run test", + "test:ci": "pnpm --filter=./packages/* run test --run", "build": "pnpm -r --filter=./packages/* run build" }, "devDependencies": { - "eslint": "^8.45.0", - "eslint-define-config": "^1.21.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-n": "^16.0.1", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-regexp": "^1.15.0", - "eslint-plugin-testing-library": "^5.11.0", - "tslib": "^2.6.0", - "tsx": "^3.12.7", - "typescript": "^5.1.6", - "vitest": "^0.33.0" + "@eslint/compat": "^2.1.1", + "@eslint/eslintrc": "^3.3.7", + "@eslint/js": "^10.0.1", + "@typescript-eslint/eslint-plugin": "^8.70.0", + "@typescript-eslint/parser": "^8.70.0", + "@typescript/native": "npm:typescript@7.0.2", + "eslint": "^10.10.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-n": "^18.3.0", + "eslint-plugin-no-only-tests": "^3.4.0", + "eslint-plugin-prettier": "^5.5.6", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-regexp": "^3.3.0", + "eslint-plugin-simple-import-sort": "^14.0.0", + "eslint-plugin-testing-library": "^7.16.2", + "prettier": "^3.9.6", + "tslib": "^2.8.1", + "tsx": "4.23.13", + "typescript": "npm:@typescript/typescript6@6.0.2", + "vitest": "5.0.0" }, "engines": { - "node": "16.* || >= 18.0.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" }, - "packageManager": "pnpm@8.6.7" + "packageManager": "pnpm@11.10.0" } diff --git a/packages/oidc-client-service-worker/.eslintrc.cjs b/packages/oidc-client-service-worker/.eslintrc.cjs deleted file mode 100644 index a37905353..000000000 --- a/packages/oidc-client-service-worker/.eslintrc.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - extends: [__dirname + '/config/defaultEslintConfig.cjs'], - parserOptions: { - project: './tsconfig.eslint.json', - tsconfigRootDir: __dirname, - }, - rules: { - '@typescript-eslint/naming-convention': [ - 'error', - { - selector: 'variable', - types: ['boolean'], - format: ['PascalCase'], - prefix: ['is', 'with', 'should', 'has', 'can', 'did', 'will'], - }, - ], - }, -}; diff --git a/packages/oidc-client-service-worker/PROTOCOL.md b/packages/oidc-client-service-worker/PROTOCOL.md new file mode 100644 index 000000000..9335b1a0a --- /dev/null +++ b/packages/oidc-client-service-worker/PROTOCOL.md @@ -0,0 +1,169 @@ +# Service Worker Protocol + +This document describes the public, supported `postMessage` protocol exposed +by the OIDC service worker shipped with `@axa-fr/oidc-client-service-worker`, +together with the storage key conventions used by `@axa-fr/oidc-client`. + +The protocol is reachable through the dedicated entry point: + +```ts +import { + PROTOCOL_VERSION, + ServiceWorkerMessageType, + ServiceWorkerMessage, + ServiceWorkerResponse, + TOKEN_PLACEHOLDERS, + STORAGE_KEY_PREFIX, + buildSecuredTokenPlaceholder, + buildDpopSecuredPlaceholder, + buildStorageKey, + isServiceWorkerMessageType, +} from '@axa-fr/oidc-client-service-worker/protocol'; +``` + +`@axa-fr/oidc-client` re-exports the same symbols on its public entry point +for convenience. + +## Stability guarantees + +The protocol is versioned with [Semantic Versioning](https://semver.org/) via +the exported `PROTOCOL_VERSION` constant. + +| Change | Version bump | +| -------------------------------------------------------- | ------------ | +| Removing a message type, response field or storage key | major | +| Renaming or repurposing an existing field | major | +| Adding a new message type, response field or helper | minor | +| Documentation-only or behaviour-preserving fixes | patch | + +Any breaking change is announced at least one minor version in advance with +a deprecation notice in the [CHANGELOG](../../CHANGELOG.md), and the +`PROTOCOL_VERSION` major number is bumped on the release that ships the +breaking change. + +The wire format itself (string values of `ServiceWorkerMessageType`) is +considered _public_ and stable: tools that rely on the literal strings +`init`, `claim`, `getState`, etc. will continue to work as long as the major +`PROTOCOL_VERSION` is unchanged. + +## Message envelope + +Every message sent _to_ the service worker conforms to the following shape +(see also `ServiceWorkerMessage` in the typings): + +```ts +interface ServiceWorkerMessage { + type: ServiceWorkerMessageTypeValue; // see table below + configurationName: string; // OIDC configuration identifier + data: object | null; // payload, depends on `type` + tabId?: string; // optional tab id (defaults to "default") +} +``` + +Every message is sent through a `MessageChannel`; the service worker replies +on `port2` exactly once. Responses always include a top-level +`configurationName` (when applicable) and may include `error` if the SW +could not service the request. + +Use [`OidcClient.signalServiceWorker`](../../packages/oidc-client/README.md) +to send a message without having to manage the channel yourself. + +## Message types + +| `ServiceWorkerMessageType` | Wire value (`type`) | Description | +| -------------------------- | ------------------- | ----------- | +| `SKIP_WAITING` | `SKIP_WAITING` | Lifecycle: ask the worker to call `skipWaiting()`. | +| `CLAIM` | `claim` | Lifecycle: ask the worker to claim the current page. | +| `CLEAR` | `clear` | Reset the in-memory entry for `configurationName` (tokens, state, nonce, DPoP, …). | +| `INIT` | `init` | Provide the OIDC server configuration; returns the masked tokens and the SW version. | +| `SET_STATE` / `GET_STATE` | `setState` / `getState` | Persist / retrieve the OAuth `state` value. | +| `SET_CODE_VERIFIER` / `GET_CODE_VERIFIER` | `setCodeVerifier` / `getCodeVerifier` | Persist / retrieve the PKCE code verifier. | +| `SET_SESSION_STATE` / `GET_SESSION_STATE` | `setSessionState` / `getSessionState` | Persist / retrieve the `session_state` claim. | +| `SET_NONCE` / `GET_NONCE` | `setNonce` / `getNonce` | Persist / retrieve the OIDC nonce. | +| `SET_DPOP_NONCE` / `GET_DPOP_NONCE` | `setDemonstratingProofOfPossessionNonce` / `getDemonstratingProofOfPossessionNonce` | Persist / retrieve the DPoP server nonce. | +| `SET_DPOP_JWK` / `GET_DPOP_JWK` | `setDemonstratingProofOfPossessionJwk` / `getDemonstratingProofOfPossessionJwk` | Persist / retrieve the JSON serialised DPoP JWK. | + +### Payloads + +Below are the supported `data` payloads. All others fields on `data` are +ignored; sending unknown extra fields is allowed and will not cause a +protocol-version bump. + +| Type | Request payload | Response payload | +| ---- | --------------- | ---------------- | +| `SKIP_WAITING` | `null` | `{}` | +| `claim` | `null` | `{}` | +| `clear` | `{ status: Status }` | `{ configurationName }` | +| `init` | `{ oidcServerConfiguration, oidcConfiguration, where }` | `{ configurationName, tokens, status, version }` | +| `setState` | `{ state: string }` | `{ configurationName }` | +| `getState` | `null` | `{ configurationName, state }` | +| `setCodeVerifier` | `{ codeVerifier: string }` | `{ configurationName }` | +| `getCodeVerifier` | `null` | `{ configurationName, codeVerifier }` | +| `setSessionState` | `{ sessionState: string }` | `{ configurationName }` | +| `getSessionState` | `null` | `{ configurationName, sessionState }` | +| `setNonce` | `{ nonce: { nonce: string } }` | `{ configurationName }` | +| `getNonce` | `null` | `{ configurationName, nonce }` | +| `setDemonstratingProofOfPossessionNonce` | `{ demonstratingProofOfPossessionNonce: string }` | `{ configurationName }` | +| `getDemonstratingProofOfPossessionNonce` | `null` | `{ configurationName, demonstratingProofOfPossessionNonce }` | +| `setDemonstratingProofOfPossessionJwk` | `{ demonstratingProofOfPossessionJwkJson: string }` | `{ configurationName }` | +| `getDemonstratingProofOfPossessionJwk` | `null` | `{ configurationName, demonstratingProofOfPossessionJwkJson }` | + +## Token placeholders + +Secret values (access token, refresh token, nonce, code verifier) are never +returned to the page. Instead, the service worker emits stable placeholder +strings that are recognised when the page later sends a `fetch`/`request` to +a trusted endpoint: + +```text +ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER_#tabId= +REFRESH_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER_#tabId= +NONCE_SECURED_BY_OIDC_SERVICE_WORKER_#tabId= +CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER_#tabId= +DPOP_SECURED_BY_OIDC_SERVICE_WORKER_#tabId= +``` + +Use `buildSecuredTokenPlaceholder(TOKEN_PLACEHOLDERS., configurationName, tabId)` +or `buildDpopSecuredPlaceholder(configurationName, tabId)` to construct the +exact string a consumer will receive. + +## Storage key conventions + +When the service worker is unavailable (or during graceful fallback paths), +`@axa-fr/oidc-client` mirrors the same data into Web Storage. The exported +`STORAGE_KEY_PREFIX` map plus the `buildStorageKey(prefix, configurationName)` +helper produce the canonical key: + +| Symbol | Storage | Pattern | +| ------ | ------- | ------- | +| `STORAGE_KEY_PREFIX.TAB_ID` | `sessionStorage` | `oidc.tabId.` | +| `STORAGE_KEY_PREFIX.STATE` | `sessionStorage` | `oidc.state.` | +| `STORAGE_KEY_PREFIX.NONCE` | `sessionStorage` | `oidc.nonce.` | +| `STORAGE_KEY_PREFIX.CODE_VERIFIER` | `sessionStorage` | `oidc.code_verifier.` | +| `STORAGE_KEY_PREFIX.LOGIN_PARAMS` | `localStorage` | `oidc.login.` | +| `STORAGE_KEY_PREFIX.SW_VERSION_MISMATCH_RELOAD` | `sessionStorage` | `oidc.sw.version_mismatch_reload.` | +| `SW_CONTROLLER_CHANGE_RELOAD_COUNT_KEY` | `sessionStorage` | `oidc.sw.controllerchange_reload_count` | + +The `oidc.` namespace is reserved by the library; consumers should not write +keys under it directly. + +## High-level helper + +For most use-cases, prefer the high-level helper exposed by +`OidcClient`: + +```ts +import { ServiceWorkerMessageType } from '@axa-fr/oidc-client-service-worker/protocol'; + +const oidcClient = OidcClient.get(); + +const response = await oidcClient.signalServiceWorker({ + type: ServiceWorkerMessageType.GET_STATE, + data: null, +}); + +console.log(response.state); +``` + +`signalServiceWorker` resolves with the response sent by the SW, rejects on +timeout (default 5 s) or when the SW is not registered. diff --git a/packages/oidc-client-service-worker/README.md b/packages/oidc-client-service-worker/README.md new file mode 100644 index 000000000..b144b44e8 --- /dev/null +++ b/packages/oidc-client-service-worker/README.md @@ -0,0 +1,147 @@ +# @axa-fr/oidc-client-service-worker + +The service worker used by [`@axa-fr/oidc-client`](../oidc-client/README.md) and +[`@axa-fr/react-oidc`](../react-oidc/README.md). It intercepts OIDC token responses, +keeps access and refresh tokens in worker memory by default, and adds access +tokens to requests that match your trusted-domain configuration. + +Most applications should install one of the client packages rather than use this +package directly. The clients handle worker registration and communication. + +- [How it works](#how-it-works) +- [Getting started](#getting-started) +- [Trusted domains](#trusted-domains) +- [Deployment and troubleshooting](#deployment-and-troubleshooting) +- [Protocol reference](#protocol-reference) + +## How it works + +```mermaid +flowchart LR + App["Browser application"] --> Client["OIDC client or React bindings"] + Client <-->|"Messages and token placeholders"| Worker["OIDC service worker"] + Worker <-->|"Token requests and responses"| Provider["OIDC provider"] + App -->|"API request"| Worker + Worker -->|"Access token on trusted requests"| API["Trusted API"] + Config["OidcTrustedDomains.js"] --> Worker +``` + +The application can use token metadata without receiving the real access or +refresh token. `showAccessToken: true` explicitly exposes the access token to +application JavaScript while keeping the refresh token in the worker. + +> [!IMPORTANT] +> Token isolation does not prevent XSS or CSRF. Injected code can still make +> authenticated requests from the application. Use a restrictive Content Security +> Policy, prevent script injection, and enforce authorization on your APIs. + +## Getting started + +1. Install and configure the [vanilla client](../oidc-client/README.md#getting-started) + or [React bindings](../react-oidc/README.md#getting-started). +2. Create your application's public-assets directory if it does not exist, then + run the copy command for the client you installed: + + ```sh + # Vanilla client + node ./node_modules/@axa-fr/oidc-client/bin/copy-service-worker-files.mjs public + + # React — use this instead + node ./node_modules/@axa-fr/react-oidc/bin/copy-service-worker-files.mjs public + ``` + + Replace `public` if your framework serves static files from another directory. + The command overwrites `OidcServiceWorker.js` but preserves an existing + `OidcTrustedDomains.js`. + +3. Replace the sample entries in `public/OidcTrustedDomains.js` with your provider + and API URLs, as described below. +4. Add these fields to your OIDC client configuration: + + ```js + const configuration = { + ...oidcConfiguration, + service_worker_relative_url: '/OidcServiceWorker.js', + service_worker_only: true, + }; + ``` + + With `service_worker_only: true`, authentication requires worker support. + Set it to `false` only if you accept fallback to JavaScript-accessible browser + storage when the worker is unavailable. + +5. Add the appropriate copy command to your application's `postinstall` script. + For example, for React: + + ```json + { + "scripts": { + "postinstall": "node ./node_modules/@axa-fr/react-oidc/bin/copy-service-worker-files.mjs public" + } + } + ``` + + Merge this with existing installation steps rather than replacing them. + Deploy the copied worker with every client upgrade. + +## Trusted domains + +Keep provider endpoints and API destinations separate: + +```js +const trustedDomains = { + default: { + oidcDomains: [/^https:\/\/identity\.example\.com(?:\/|$)/], + accessTokenDomains: [/^https:\/\/api\.example\.com(?:\/|$)/], + }, +}; +``` + +`default` is the default client configuration name. Add a matching entry for +every named client or `OidcProvider`. These rules use regular-expression matching, +including when rules are strings. Anchor patterns, escape hostname dots, and +include a host or path boundary so that lookalike URLs do not match. +Include the actual OIDC endpoint origins +if your provider serves discovery, tokens, or user information from different +hosts. Allow only the API destinations that should receive this configuration's +access token; do not copy the demo allowlist into production. + +Use `oidcClient.fetchWithTokens(fetch)` in vanilla applications or `useOidcFetch()` +in React. In particular, `allowMultiTabLogin: true` **requires** the OIDC fetch +wrapper: its placeholder identifies the tab whose token the worker must inject. +A plain `fetch` cannot supply that information. + +See the [client's service-worker guide](../oidc-client/README.md#service-worker-support) +for additional options, including DPoP, access-token visibility, and request +handling. + +## Deployment and troubleshooting + +- Serve both JavaScript files from your application's origin over HTTPS + (localhost is supported for development). +- The worker must control the pages that use it. Serving it at the origin root + is the simplest setup; account for worker scope when hosting under a subpath. +- Serve the files as JavaScript, not the HTML fallback used for SPA routes. + Check the Network and Application panels in browser developer tools if + registration fails. +- Keep the worker version aligned with the client. Edit the trusted-domain file, + not the generated `OidcServiceWorker.js`. +- If API requests return 401, check the active configuration name, the matching + `accessTokenDomains` entry, and whether you are using the OIDC fetch wrapper. + The API must also accept the token's issuer, audience, and scopes. +- After changing worker assets, check which worker version controls your page; + an already-open tab may still be controlled by an older worker. + +The [FAQ](../../FAQ.md) covers broader security and integration questions. + +## Protocol reference + +Most applications do not need to send worker messages directly. For custom +integrations, [`PROTOCOL.md`](./PROTOCOL.md) describes the versioned message +envelope, payloads, token placeholders, storage keys, and compatibility guarantees. + +Protocol constants and types are exported from +`@axa-fr/oidc-client-service-worker/protocol` and re-exported by +`@axa-fr/oidc-client`. + +[Back to the project overview](../../README.md) diff --git a/packages/oidc-client-service-worker/config/defaultEslintConfig.cjs b/packages/oidc-client-service-worker/config/defaultEslintConfig.cjs deleted file mode 100644 index c3fa61f5b..000000000 --- a/packages/oidc-client-service-worker/config/defaultEslintConfig.cjs +++ /dev/null @@ -1,148 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - extends: [ - 'standard', - 'plugin:react/recommended', - 'plugin:react-hooks/recommended', - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:import/typescript', - 'plugin:jsx-a11y/recommended', - ], - plugins: ['simple-import-sort', 'testing-library'], - env: { - node: true, - es6: true, - browser: true, - }, - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - // typescript-eslint specific options - warnOnUnsupportedTypeScriptVersion: true, - }, - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': [ - 'error', - { - argsIgnorePattern: '^_|req|res|next|err|ctx|args|context|info', - ignoreRestSiblings: true, - }, - ], - 'no-array-constructor': 'off', - '@typescript-eslint/no-array-constructor': 'warn', - 'no-redeclare': 'off', - '@typescript-eslint/no-redeclare': 'warn', - 'no-use-before-define': 'off', - '@typescript-eslint/no-use-before-define': [ - 'warn', - { - functions: false, - classes: false, - variables: false, - typedefs: false, - }, - ], - 'no-unused-expressions': 'off', - '@typescript-eslint/no-unused-expressions': [ - 'error', - { - allowShortCircuit: true, - allowTernary: true, - allowTaggedTemplates: true, - }, - ], - '@typescript-eslint/triple-slash-reference': 'off', - '@typescript-eslint/member-delimiter-style': [ - 'error', - { - multiline: { - delimiter: 'semi', - requireLast: true, - }, - singleline: { - delimiter: 'semi', - requireLast: false, - }, - }, - ], - camelcase: 'off', - 'comma-dangle': [ - 'error', - { - arrays: 'always-multiline', - objects: 'always-multiline', - imports: 'always-multiline', - exports: 'always-multiline', - functions: 'always-multiline', - }, - ], - 'array-callback-return': 'warn', - 'jsx-quotes': ['error', 'prefer-double'], - // 'max-len': ['error', { code: 120 }], - indent: 'off', - // quotes: ['error', 'single'], - semi: ['error', 'always'], - 'space-before-function-paren': 'off', - - 'import/no-named-as-default': 'off', - 'import/no-named-as-default-member': 'off', - 'import/default': 'off', - 'import/named': 'off', - 'import/namespace': 'off', - 'import/no-unresolved': 'off', - 'simple-import-sort/imports': 'error', - 'simple-import-sort/exports': 'error', - 'react/prop-types': 'off', - 'react/jsx-wrap-multilines': 'error', - 'react/react-in-jsx-scope': 'off', - 'react/display-name': 'off', - // https://github.com/facebook/react/tree/master/packages/eslint-plugin-react-hooks - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'off', - }, - - overrides: [ - { - files: ['*.js', '*.jsx'], - rules: { - '@typescript-eslint/no-var-requires': 'off', - }, - }, - { - // 3) Now we enable eslint-plugin-testing-library rules or preset only for matching files! - files: ['**/?(*.)+(spec|test).[jt]s?(x)'], - extends: ['plugin:testing-library/react'], - rules: { - 'testing-library/await-async-query': 'error', - 'testing-library/no-await-sync-query': 'error', - 'testing-library/no-debugging-utils': 'warn', - 'testing-library/no-dom-import': 'off', - 'testing-library/no-unnecessary-act': 'off', - }, - }, - ], - - settings: { - react: { - version: 'detect', - }, - 'import/parsers': { - '@typescript-eslint/parser': ['.ts', '.tsx'], - }, - 'import/resolver': { - typescript: { - alwaysTryTypes: true, - }, - }, - }, - }; \ No newline at end of file diff --git a/packages/oidc-client-service-worker/package.json b/packages/oidc-client-service-worker/package.json index 399a8169c..264895178 100644 --- a/packages/oidc-client-service-worker/package.json +++ b/packages/oidc-client-service-worker/package.json @@ -1,10 +1,23 @@ { "name": "@axa-fr/oidc-client-service-worker", - "version": "6.16.1", - "private": true, + "version": "7.29.6", "type": "module", + "private": false, "main": "dist/OidcServiceWorker.js", "types": "dist/OidcServiceWorker.d.ts", + "exports": { + ".": { + "types": "./dist/OidcServiceWorker.d.ts", + "import": "./dist/OidcServiceWorker.js", + "default": "./dist/OidcServiceWorker.js" + }, + "./protocol": { + "types": "./dist/protocol.d.ts", + "import": "./dist/protocol.js", + "default": "./dist/protocol.js" + }, + "./package.json": "./package.json" + }, "description": "OpenID Connect & OAuth authentication service worker", "files": [ "dist", @@ -19,6 +32,10 @@ "oauth2", "oauth" ], + "repository": { + "type": "git", + "url": "https://github.com/AxaFrance/oidc-client.git" + }, "scripts": { "copy": "cpy --flat ./src/OidcTrustedDomains.js ./dist/", "build": "tsc && vite build && pnpm run copy", @@ -30,23 +47,18 @@ "lint": "eslint src" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^5.50.0", - "@typescript-eslint/parser": "^5.50.0", - "@vitest/coverage-c8": "^0.33.0", - "cpy": "^10.1.0", - "cpy-cli": "^5.0.0", - "eslint": "^8.26.0", - "eslint-config-standard": "^17.1.0", - "eslint-config-standard-with-typescript": "^36.1.0", - "eslint-import-resolver-typescript": "^3.5.5", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-simple-import-sort": "^10.0.0", - "rimraf": "5.0.1", - "msw": "1.2.2", - "typescript": "5.1.6", - "vite": "^4.4.4", - "vite-plugin-dts": "^3.3.0", - "vitest": "^0.33.0" + "@vitest/coverage-v8": "5.0.0", + "cpy": "13.2.3", + "cpy-cli": "^7.0.0", + "rimraf": "6.1.3", + "typescript": "npm:@typescript/typescript6@6.0.2", + "vite": "8.3.0", + "vite-plugin-dts": "5.1.0", + "vitest": "5.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" }, "license": "MIT", "browserslist": { @@ -61,4 +73,4 @@ "last 1 safari version" ] } -} \ No newline at end of file +} diff --git a/packages/oidc-client-service-worker/src/OidcServiceWorker.ts b/packages/oidc-client-service-worker/src/OidcServiceWorker.ts index 22c940865..7a5baf2ed 100644 --- a/packages/oidc-client-service-worker/src/OidcServiceWorker.ts +++ b/packages/oidc-client-service-worker/src/OidcServiceWorker.ts @@ -1,80 +1,61 @@ import { acceptAnyDomainToken, scriptFilename, TOKEN } from './constants'; -import { - Database, - MessageEventData, - OidcConfig, - OidcConfiguration, - TrustedDomains, - // TrustedDomainsShowAccessToken, -} from './types'; +import { base64urlOfHashOfASCIIEncodingAsync } from './crypto'; +import { getDpopConfiguration, getDpopOnlyWhenDpopHeaderPresent } from './dpop'; +import { generateJwkAsync, generateJwtDemonstratingProofOfPossessionAsync } from './jwt'; +import { getCurrentDatabasesTokenEndpoint, shouldBypassNonOidcRequest } from './oidcConfig'; +import { Database, MessageEventData, OidcConfig, TrustedDomains } from './types'; import { checkDomain, getCurrentDatabaseDomain, getDomains, hideTokens, - isTokensValid, + normalizeUrl, serializeHeaders, + shouldBypassDestination, sleep, + waitForValidTokens, } from './utils'; -import { replaceCodeVerifier } from './utils/codeVerifier'; +import { + extractConfigurationNameFromCodeVerifier, + replaceCodeVerifier, +} from './utils/codeVerifier'; +import version from './version'; + +// @ts-ignore +if (typeof trustedTypes !== 'undefined' && typeof trustedTypes.createPolicy === 'function') { + // @ts-ignore + trustedTypes.createPolicy('default', { + createScriptURL: function (url: string) { + if (url === scriptFilename) { + return url; + } else { + throw new Error('Untrusted script URL blocked: ' + url); + } + }, + }); +} const _self = self as ServiceWorkerGlobalScope & typeof globalThis; +// `trustedDomains` is declared in the externally loaded script (OidcTrustedDomains.js) declare let trustedDomains: TrustedDomains; _self.importScripts(scriptFilename); const id = Math.round(new Date().getTime() / 1000).toString(); - +console.log('init service worker with id', id); const keepAliveJsonFilename = 'OidcKeepAliveServiceWorker.json'; -const handleInstall = (event: ExtendableEvent) => { - console.log('[OidcServiceWorker] service worker installed ' + id); - event.waitUntil(_self.skipWaiting()); -}; - -const handleActivate = (event: ExtendableEvent) => { - console.log('[OidcServiceWorker] service worker activated ' + id); - event.waitUntil(_self.clients.claim()); -}; - -let currentLoginCallbackConfigurationName: string | null = null; -const database: Database = { - default: { - configurationName: 'default', - tokens: null, - status: null, - state: null, - codeVerifier: null, - nonce: null, - oidcServerConfiguration: null, - hideAccessToken: true, - }, -}; - -const getCurrentDatabasesTokenEndpoint = (database: Database, url: string) => { - const databases: OidcConfig[] = []; - for (const [, value] of Object.entries(database)) { - if ( - value.oidcServerConfiguration != null && - url.startsWith(value.oidcServerConfiguration.tokenEndpoint) - ) { - databases.push(value); - } else if ( - value.oidcServerConfiguration != null && - value.oidcServerConfiguration.revocationEndpoint && - url.startsWith(value.oidcServerConfiguration.revocationEndpoint) - ) { - databases.push(value); - } - } - return databases; -}; +const database: Database = {}; +/** + * Keeps the service worker alive by responding with a cached response after a sleep. + */ const keepAliveAsync = async (event: FetchEvent) => { const originalRequest = event.request; const isFromVanilla = originalRequest.headers.has('oidc-vanilla'); const init = { status: 200, statusText: 'oidc-service-worker' }; const response = new Response('{}', init); + if (!isFromVanilla) { const originalRequestUrl = new URL(originalRequest.url); const minSleepSeconds = Number(originalRequestUrl.searchParams.get('minSleepSeconds')) || 240; @@ -87,204 +68,382 @@ const keepAliveAsync = async (event: FetchEvent) => { return response; }; -const handleFetch = async (event: FetchEvent) => { - const originalRequest = event.request; - const url = originalRequest.url; - if (originalRequest.url.includes(keepAliveJsonFilename)) { - event.respondWith(keepAliveAsync(event)); - return; - } - - const currentDatabaseForRequestAccessToken = getCurrentDatabaseDomain( - database, - originalRequest.url, - trustedDomains, - ); +/** + * Generates DPoP headers when a DPoP configuration is present. + */ +async function generateDpopAsync( + originalRequest: Request, + currentDatabase: OidcConfig | null, + url: string, + extrasClaims = {}, +) { + const headersExtras = serializeHeaders(originalRequest.headers); if ( - currentDatabaseForRequestAccessToken && - currentDatabaseForRequestAccessToken.tokens && - currentDatabaseForRequestAccessToken.tokens.access_token + currentDatabase?.demonstratingProofOfPossessionConfiguration && + currentDatabase.demonstratingProofOfPossessionJwkJson && + (!currentDatabase.demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent || + (currentDatabase.demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent && + headersExtras.dpop)) ) { - while ( - currentDatabaseForRequestAccessToken.tokens && - !isTokensValid(currentDatabaseForRequestAccessToken.tokens) - ) { - await sleep(200); - } - const newRequest = - originalRequest.mode === 'navigate' - ? new Request(originalRequest, { - headers: { - ...serializeHeaders(originalRequest.headers), - authorization: - 'Bearer ' + - currentDatabaseForRequestAccessToken.tokens.access_token, - }, - }) - : new Request(originalRequest, { - headers: { - ...serializeHeaders(originalRequest.headers), - authorization: - 'Bearer ' + - currentDatabaseForRequestAccessToken.tokens.access_token, - }, - mode: ( - currentDatabaseForRequestAccessToken.oidcConfiguration as OidcConfiguration - ).service_worker_convert_all_requests_to_cors - ? 'cors' - : originalRequest.mode, - }); + const dpopConfiguration = currentDatabase.demonstratingProofOfPossessionConfiguration; + const jwk = currentDatabase.demonstratingProofOfPossessionJwkJson; + const method = originalRequest.method; + const dpop = await generateJwtDemonstratingProofOfPossessionAsync(self)(dpopConfiguration)( + jwk, + method, + url, + extrasClaims, + ); - // @ts-ignore -- TODO: review, waitUntil takes a promise, this returns a void - event.waitUntil(event.respondWith(fetch(newRequest))); + headersExtras.dpop = dpop; + if (currentDatabase.demonstratingProofOfPossessionNonce != null) { + headersExtras.nonce = currentDatabase.demonstratingProofOfPossessionNonce; + } + } + return headersExtras; +} - return; +/** + * Intercepts fetch requests to inject access tokens and handle token endpoints. + */ +const handleFetch = (event: FetchEvent): void => { + /** + * Exit early for requests that do not need to have an auth token attached. + */ + if (shouldBypassDestination(event.request.destination, event.request.mode)) { + return; // Don't call event.respondWith() - let browser handle naturally } - if (event.request.method !== 'POST') { - return; + const normalizedUrl = normalizeUrl(event.request.url); + if ( + !normalizedUrl.includes(keepAliveJsonFilename) && + shouldBypassNonOidcRequest(database, normalizedUrl, trustedDomains) + ) { + return; // Don't call event.respondWith() - let browser handle naturally } - let currentDatabase: OidcConfig | null = null; - const currentDatabases = getCurrentDatabasesTokenEndpoint( - database, - originalRequest.url, - ); - const numberDatabase = currentDatabases.length; - if (numberDatabase > 0) { - const maPromesse = new Promise((resolve, reject) => { - const clonedRequest = originalRequest.clone(); - const response = clonedRequest.text().then((actualBody) => { - if ( - actualBody.includes(TOKEN.REFRESH_TOKEN) || - actualBody.includes(TOKEN.ACCESS_TOKEN) - ) { - let newBody = actualBody; - for (let i = 0; i < numberDatabase; i++) { - const currentDb = currentDatabases[i]; - - if (currentDb && currentDb.tokens != null) { - const keyRefreshToken = - TOKEN.REFRESH_TOKEN + '_' + currentDb.configurationName; - if (actualBody.includes(keyRefreshToken)) { - newBody = newBody.replace( - keyRefreshToken, - encodeURIComponent(currentDb.tokens.refresh_token as string), - ); - currentDatabase = currentDb; - break; - } - const keyAccessToken = - TOKEN.ACCESS_TOKEN + '_' + currentDb.configurationName; - if (actualBody.includes(keyAccessToken)) { - newBody = newBody.replace( - keyAccessToken, - encodeURIComponent(currentDb.tokens.access_token), - ); - currentDatabase = currentDb; - break; - } - } + event.respondWith( + (async (): Promise => { + try { + const originalRequest = event.request; + const url = normalizedUrl; + + // 1) Handle keep-alive requests + if (url.includes(keepAliveJsonFilename)) { + return keepAliveAsync(event); + } + + // Check if an access token is available for this request + const currentDatabasesForRequestAccessToken = getCurrentDatabaseDomain( + database, + url, + trustedDomains, + ); + + const authorization = originalRequest.headers.get('authorization'); + let authenticationMode = 'Bearer'; + let key = 'default'; + + if (authorization) { + const split = authorization.split(' '); + authenticationMode = split[0]; + if (split[1]?.includes('ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER_')) { + key = split[1].split('ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER_')[1]; + } + } + + const currentDatabaseForRequestAccessToken = currentDatabasesForRequestAccessToken?.find( + c => c.configurationName.endsWith(key), + ); + + // Inject the access token into the request if one is available + if (currentDatabaseForRequestAccessToken?.tokens?.access_token) { + // Wait for token to become valid (a parallel refresh may be in progress) + const tokenError = await waitForValidTokens(currentDatabaseForRequestAccessToken); + if (tokenError) { + return tokenError; } - const fetchPromise = fetch(originalRequest, { - body: newBody, - method: clonedRequest.method, - headers: { - ...serializeHeaders(originalRequest.headers), - }, - mode: clonedRequest.mode, - cache: clonedRequest.cache, - redirect: clonedRequest.redirect, - referrer: clonedRequest.referrer, - credentials: clonedRequest.credentials, - integrity: clonedRequest.integrity, - }); + // Adjust request mode for CORS if configured + let requestMode = originalRequest.mode; if ( - currentDatabase && - currentDatabase.oidcServerConfiguration != null && - currentDatabase.oidcServerConfiguration.revocationEndpoint && - url.startsWith( - currentDatabase.oidcServerConfiguration.revocationEndpoint, - ) + originalRequest.mode !== 'navigate' && + currentDatabaseForRequestAccessToken.convertAllRequestsToCorsExceptNavigate ) { - return fetchPromise.then(async (response) => { - const text = await response.text(); - return new Response(text, response); - }); - } - return fetchPromise.then(hideTokens(currentDatabase as OidcConfig)); // todo type assertion to OidcConfig but could be null, NEEDS REVIEW - } else if ( - actualBody.includes('code_verifier=') && - currentLoginCallbackConfigurationName - ) { - currentDatabase = database[currentLoginCallbackConfigurationName]; - currentLoginCallbackConfigurationName = null; - let newBody = actualBody; - if (currentDatabase && currentDatabase.codeVerifier != null) { - newBody = replaceCodeVerifier(newBody, currentDatabase.codeVerifier); + requestMode = 'cors'; } - return fetch(originalRequest, { - body: newBody, - method: clonedRequest.method, - headers: { + // Build request headers + let headers: { [p: string]: string }; + + // Skip the access token for navigate requests when setAccessTokenToNavigateRequests is false + if ( + originalRequest.mode === 'navigate' && + !currentDatabaseForRequestAccessToken.setAccessTokenToNavigateRequests + ) { + headers = { ...serializeHeaders(originalRequest.headers), - }, - mode: clonedRequest.mode, - cache: clonedRequest.cache, - redirect: clonedRequest.redirect, - referrer: clonedRequest.referrer, - credentials: clonedRequest.credentials, - integrity: clonedRequest.integrity, - }).then(hideTokens(currentDatabase)); - } - return undefined; - }); - response - .then((r) => { - if (r !== undefined) { - resolve(r); + }; } else { - console.log('success undefined'); - reject(new Error('Response is undefined inside a success')); + if ( + authenticationMode.toLowerCase() === 'dpop' || + (!currentDatabaseForRequestAccessToken.demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent && + currentDatabaseForRequestAccessToken.demonstratingProofOfPossessionConfiguration) + ) { + // DPoP mode + const claimsExtras = { + ath: await base64urlOfHashOfASCIIEncodingAsync( + currentDatabaseForRequestAccessToken.tokens.access_token, + ), + }; + const dpopHeaders = await generateDpopAsync( + originalRequest, + currentDatabaseForRequestAccessToken, + url, + claimsExtras, + ); + headers = { + ...dpopHeaders, + authorization: `DPoP ${currentDatabaseForRequestAccessToken.tokens.access_token}`, + }; + } else { + // Bearer mode + headers = { + ...serializeHeaders(originalRequest.headers), + authorization: `${authenticationMode} ${currentDatabaseForRequestAccessToken.tokens.access_token}`, + }; + } } - }) - .catch((err) => { - if (err !== undefined) { - reject(err); + + let init: RequestInit; + if (originalRequest.mode === 'navigate') { + init = { + headers: headers, + }; } else { - console.log('error undefined'); - reject(new Error('Response is undefined inside a error')); + init = { + headers: headers, + mode: requestMode, + }; } - }); - }); - // @ts-ignore -- TODO: review, waitUntil takes a promise, this returns a void - event.waitUntil(event.respondWith(maPromesse)); - } -}; + const newRequest = new Request(originalRequest, init); + return fetch(newRequest); + } -type TrustedDomainsShowAccessToken = { - [key: string]: boolean; -} + // Pass through non-POST requests without modification + if (event.request.method !== 'POST') { + return fetch(originalRequest); + } -const trustedDomainsShowAccessToken: TrustedDomainsShowAccessToken = {}; + // Handle POST requests to known token/revocation endpoints + const currentDatabases = getCurrentDatabasesTokenEndpoint(database, url); + const numberDatabase = currentDatabases.length; -const handleMessage = (event: ExtendableMessageEvent) => { + if (numberDatabase > 0) { + const responsePromise = new Promise((resolve, reject) => { + const clonedRequest = originalRequest.clone(); + clonedRequest + .text() + .then(async actualBody => { + let currentDatabase: OidcConfig | null = null; + try { + // Replace hidden token placeholders with the real token values + if ( + actualBody.includes(TOKEN.REFRESH_TOKEN) || + actualBody.includes(TOKEN.ACCESS_TOKEN) + ) { + let headers = serializeHeaders(originalRequest.headers); + let newBody = actualBody; + + for (let i = 0; i < numberDatabase; i++) { + const currentDb = currentDatabases[i]; + if (currentDb?.tokens) { + const claimsExtras = { + ath: await base64urlOfHashOfASCIIEncodingAsync( + currentDb.tokens.access_token, + ), + }; + headers = await generateDpopAsync( + originalRequest, + currentDb, + url, + claimsExtras, + ); + + const keyRefreshToken = encodeURIComponent( + `${TOKEN.REFRESH_TOKEN}_${currentDb.configurationName}`, + ); + if (actualBody.includes(keyRefreshToken)) { + newBody = newBody.replace( + keyRefreshToken, + encodeURIComponent(currentDb.tokens.refresh_token as string), + ); + currentDatabase = currentDb; + break; + } + + const keyAccessToken = encodeURIComponent( + `${TOKEN.ACCESS_TOKEN}_${currentDb.configurationName}`, + ); + if (actualBody.includes(keyAccessToken)) { + newBody = newBody.replace( + keyAccessToken, + encodeURIComponent(currentDb.tokens.access_token), + ); + currentDatabase = currentDb; + break; + } + } + } + + const fetchPromise = fetch(originalRequest, { + body: newBody, + method: clonedRequest.method, + headers, + mode: clonedRequest.mode, + cache: clonedRequest.cache, + redirect: clonedRequest.redirect, + referrer: clonedRequest.referrer, + credentials: clonedRequest.credentials, + integrity: clonedRequest.integrity, + }); + + // Forward revocation requests without modifying the response body + if ( + currentDatabase?.oidcServerConfiguration?.revocationEndpoint && + url.startsWith( + normalizeUrl(currentDatabase.oidcServerConfiguration.revocationEndpoint), + ) + ) { + const resp = await fetchPromise; + const txt = await resp.text(); + resolve(new Response(txt, resp)); + return; + } + + // Hide real token values in the response + const hidden = await fetchPromise.then( + hideTokens(currentDatabase as OidcConfig), + ); + resolve(hidden); + return; + } + + // Handle authorization code exchange: replace the PKCE code_verifier placeholder + const isCodeVerifier = actualBody.includes('code_verifier='); + if (isCodeVerifier) { + const currentLoginCallbackConfigurationName = + extractConfigurationNameFromCodeVerifier(actualBody); + if ( + !currentLoginCallbackConfigurationName || + currentLoginCallbackConfigurationName === '' + ) { + throw new Error('No configuration name found in code_verifier'); + } + currentDatabase = database[currentLoginCallbackConfigurationName]; + let newBody = actualBody; + const codeVerifier = currentDatabase.codeVerifier; + if (codeVerifier != null) { + newBody = replaceCodeVerifier(newBody, codeVerifier); + } + + const headersExtras = await generateDpopAsync( + originalRequest, + currentDatabase, + url, + ); + const resp = await fetch(originalRequest, { + body: newBody, + method: clonedRequest.method, + headers: headersExtras, + mode: clonedRequest.mode, + cache: clonedRequest.cache, + redirect: clonedRequest.redirect, + referrer: clonedRequest.referrer, + credentials: clonedRequest.credentials, + integrity: clonedRequest.integrity, + }); + const hidden = await hideTokens(currentDatabase)(resp); + resolve(hidden); + return; + } + + // Pass through all other POST requests unchanged + const normalResp = await fetch(originalRequest, { + body: actualBody, + method: clonedRequest.method, + headers: serializeHeaders(originalRequest.headers), + mode: clonedRequest.mode, + cache: clonedRequest.cache, + redirect: clonedRequest.redirect, + referrer: clonedRequest.referrer, + credentials: clonedRequest.credentials, + integrity: clonedRequest.integrity, + }); + resolve(normalResp); + } catch (err) { + reject(err); + } + }) + .catch(reject); + }); + + return responsePromise; + } + + // Default: pass through the request unchanged + return fetch(originalRequest); + } catch (err) { + // Surface unexpected errors as a 500 rather than silently hanging + console.error('[OidcServiceWorker] handleFetch error:', err); + return new Response('Service Worker Error', { status: 500 }); + } + })(), + ); +}; + +const handleMessage = async (event: ExtendableMessageEvent) => { const port = event.ports[0]; const data = event.data as MessageEventData; - const configurationName = data.configurationName; - let currentDatabase = database[configurationName]; + + if (event.data?.type === 'SKIP_WAITING') { + await _self.skipWaiting(); + port?.postMessage?.({}); + return; + } else if (event.data.type === 'claim') { + _self.clients.claim().then(() => port.postMessage({})); + return; + } + + const configurationName = data.configurationName.split('#')[0]; + if (trustedDomains == null) { trustedDomains = {}; } + + const trustedDomain = trustedDomains[configurationName]; + const allowMultiTabLogin = Array.isArray(trustedDomain) + ? false + : trustedDomain.allowMultiTabLogin; + + const tabId = allowMultiTabLogin ? data.tabId : 'default'; + const configurationNameWithTabId = `${configurationName}#tabId=${tabId}`; + + let currentDatabase = database[configurationNameWithTabId]; if (!currentDatabase) { - if (trustedDomainsShowAccessToken[configurationName] === undefined) { - const trustedDomain = trustedDomains[configurationName]; - trustedDomainsShowAccessToken[configurationName] = Array.isArray(trustedDomain) ? false : trustedDomain.showAccessToken; - } - database[configurationName] = { + const showAccessToken = Array.isArray(trustedDomain) ? false : trustedDomain.showAccessToken; + const doNotSetAccessTokenToNavigateRequests = Array.isArray(trustedDomain) + ? true + : trustedDomain.setAccessTokenToNavigateRequests; + const convertAllRequestsToCorsExceptNavigate = Array.isArray(trustedDomain) + ? false + : trustedDomain.convertAllRequestsToCorsExceptNavigate; + const bypassAllNonOidcRequests = Array.isArray(trustedDomain) + ? false + : trustedDomain.bypassAllNonOidcRequests; + + database[configurationNameWithTabId] = { tokens: null, state: null, codeVerifier: null, @@ -292,10 +451,18 @@ const handleMessage = (event: ExtendableMessageEvent) => { oidcConfiguration: undefined, nonce: null, status: null, - configurationName, - hideAccessToken: !trustedDomainsShowAccessToken[configurationName], + configurationName: configurationNameWithTabId, + hideAccessToken: !showAccessToken, + setAccessTokenToNavigateRequests: doNotSetAccessTokenToNavigateRequests ?? true, + convertAllRequestsToCorsExceptNavigate: convertAllRequestsToCorsExceptNavigate ?? false, + demonstratingProofOfPossessionNonce: null, + demonstratingProofOfPossessionJwkJson: null, + demonstratingProofOfPossessionConfiguration: null, + demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent: false, + allowMultiTabLogin: allowMultiTabLogin ?? false, + bypassAllNonOidcRequests: bypassAllNonOidcRequests ?? false, }; - currentDatabase = database[configurationName]; + currentDatabase = database[configurationNameWithTabId]; if (!trustedDomains[configurationName]) { trustedDomains[configurationName] = []; @@ -307,33 +474,49 @@ const handleMessage = (event: ExtendableMessageEvent) => { currentDatabase.tokens = null; currentDatabase.state = null; currentDatabase.codeVerifier = null; + currentDatabase.nonce = null; + currentDatabase.demonstratingProofOfPossessionNonce = null; + currentDatabase.demonstratingProofOfPossessionJwkJson = null; + currentDatabase.demonstratingProofOfPossessionConfiguration = null; + currentDatabase.demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent = false; currentDatabase.status = data.data.status; port.postMessage({ configurationName }); return; + case 'init': { const oidcServerConfiguration = data.data.oidcServerConfiguration; - const trustedDomain = trustedDomains[configurationName]; const domains = getDomains(trustedDomain, 'oidc'); - if (!domains.find((f) => f === acceptAnyDomainToken)) { + + if (!domains.some(domain => domain === acceptAnyDomainToken)) { [ oidcServerConfiguration.tokenEndpoint, oidcServerConfiguration.revocationEndpoint, oidcServerConfiguration.userInfoEndpoint, oidcServerConfiguration.issuer, - ].forEach((url) => { - checkDomain(domains, url); + ].forEach(u => { + checkDomain(domains, u); }); } - currentDatabase.oidcServerConfiguration = oidcServerConfiguration; + + currentDatabase.oidcServerConfiguration = oidcServerConfiguration; currentDatabase.oidcConfiguration = data.data.oidcConfiguration; - const where = data.data.where; - if ( - where === 'loginCallbackAsync' || - where === 'tryKeepExistingSessionAsync' - ) { - currentLoginCallbackConfigurationName = configurationName; - } else { - currentLoginCallbackConfigurationName = null; + + if (currentDatabase.demonstratingProofOfPossessionConfiguration == null) { + const demonstratingProofOfPossessionConfiguration = getDpopConfiguration(trustedDomain); + if (demonstratingProofOfPossessionConfiguration != null) { + if (currentDatabase.oidcConfiguration.demonstrating_proof_of_possession) { + console.warn( + 'In service worker, demonstrating_proof_of_possession must be configured from trustedDomains file', + ); + } + currentDatabase.demonstratingProofOfPossessionConfiguration = + demonstratingProofOfPossessionConfiguration; + currentDatabase.demonstratingProofOfPossessionJwkJson = await generateJwkAsync(self)( + demonstratingProofOfPossessionConfiguration.generateKeyAlgorithm, + ); + currentDatabase.demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent = + getDpopOnlyWhenDpopHeaderPresent(trustedDomain) ?? false; + } } if (!currentDatabase.tokens) { @@ -341,62 +524,88 @@ const handleMessage = (event: ExtendableMessageEvent) => { tokens: null, status: currentDatabase.status, configurationName, + version, }); } else { - const tokens = { - ...currentDatabase.tokens, - }; + const tokens = { ...currentDatabase.tokens }; if (currentDatabase.hideAccessToken) { - tokens.access_token = TOKEN.ACCESS_TOKEN + '_' + configurationName; + tokens.access_token = `${TOKEN.ACCESS_TOKEN}_${configurationName}#tabId=${tabId}`; } if (tokens.refresh_token) { - tokens.refresh_token = TOKEN.REFRESH_TOKEN + '_' + configurationName; + tokens.refresh_token = `${TOKEN.REFRESH_TOKEN}_${configurationName}#tabId=${tabId}`; } - if ( - tokens.idTokenPayload && - tokens.idTokenPayload.nonce && - currentDatabase.nonce != null - ) { - tokens.idTokenPayload.nonce = - TOKEN.NONCE_TOKEN + '_' + configurationName; + if (tokens?.idTokenPayload?.nonce && currentDatabase.nonce != null) { + tokens.idTokenPayload.nonce = `${TOKEN.NONCE_TOKEN}_${configurationName}#tabId=${tabId}`; } port.postMessage({ tokens, status: currentDatabase.status, configurationName, + version, }); } return; } - case 'setState': + + case 'setDemonstratingProofOfPossessionNonce': { + currentDatabase.demonstratingProofOfPossessionNonce = + data.data.demonstratingProofOfPossessionNonce; + port.postMessage({ configurationName }); + return; + } + + case 'getDemonstratingProofOfPossessionNonce': { + const demonstratingProofOfPossessionNonce = + currentDatabase.demonstratingProofOfPossessionNonce; + port.postMessage({ + configurationName, + demonstratingProofOfPossessionNonce, + }); + return; + } + + case 'setState': { currentDatabase.state = data.data.state; port.postMessage({ configurationName }); return; + } + case 'getState': { const state = currentDatabase.state; port.postMessage({ configurationName, state }); return; } - case 'setCodeVerifier': + + case 'setCodeVerifier': { currentDatabase.codeVerifier = data.data.codeVerifier; port.postMessage({ configurationName }); return; + } + case 'getCodeVerifier': { + const codeVerifier = + currentDatabase.codeVerifier != null + ? `${TOKEN.CODE_VERIFIER}_${configurationName}#tabId=${tabId}` + : null; port.postMessage({ configurationName, - codeVerifier: currentDatabase.codeVerifier != null ? TOKEN.CODE_VERIFIER + '_' + configurationName : null, + codeVerifier, }); return; } - case 'setSessionState': + + case 'setSessionState': { currentDatabase.sessionState = data.data.sessionState; port.postMessage({ configurationName }); return; + } + case 'getSessionState': { const sessionState = currentDatabase.sessionState; port.postMessage({ configurationName, sessionState }); return; } + case 'setNonce': { const nonce = data.data.nonce; if (nonce) { @@ -405,19 +614,19 @@ const handleMessage = (event: ExtendableMessageEvent) => { port.postMessage({ configurationName }); return; } + case 'getNonce': { - const keyNonce = TOKEN.NONCE_TOKEN + '_' + configurationName; + const keyNonce = `${TOKEN.NONCE_TOKEN}_${configurationName}#tabId=${tabId}`; const nonce = currentDatabase.nonce ? keyNonce : null; port.postMessage({ configurationName, nonce }); return; } + default: - currentDatabase.items = { ...data.data }; - port.postMessage({ configurationName }); + return; } }; -_self.addEventListener('install', handleInstall); -_self.addEventListener('activate', handleActivate); +// Event listeners _self.addEventListener('fetch', handleFetch); _self.addEventListener('message', handleMessage); diff --git a/packages/oidc-client-service-worker/src/OidcTrustedDomains.js b/packages/oidc-client-service-worker/src/OidcTrustedDomains.js index 1aea5cd1a..97ad89303 100644 --- a/packages/oidc-client-service-worker/src/OidcTrustedDomains.js +++ b/packages/oidc-client-service-worker/src/OidcTrustedDomains.js @@ -3,24 +3,29 @@ // then all subroute like https://www.myapi.com/useers/1 will be authorized to send access_token to. // Domains used by OIDC server must be also declared here -// eslint-disable-next-line @typescript-eslint/no-unused-vars + const trustedDomains = { - default: ['https://demo.duendesoftware.com', 'https://kdhttps.auth0.com'], - config_classic: ['https://demo.duendesoftware.com'], - config_without_silent_login: ['https://demo.duendesoftware.com'], - config_without_refresh_token: ['https://demo.duendesoftware.com'], - config_without_refresh_token_silent_login: ['https://demo.duendesoftware.com'], - config_google: ['https://oauth2.googleapis.com', 'https://openidconnect.googleapis.com'], - config_with_hash: ['https://demo.duendesoftware.com'], + default: ['https://demo.duendesoftware.com', 'https://kdhttps.auth0.com'], + config_classic: ['https://demo.duendesoftware.com'], + config_without_silent_login: ['https://demo.duendesoftware.com'], + config_without_refresh_token: ['https://demo.duendesoftware.com'], + config_without_refresh_token_silent_login: ['https://demo.duendesoftware.com'], + config_google: ['https://oauth2.googleapis.com', 'https://openidconnect.googleapis.com'], + config_with_hash: ['https://demo.duendesoftware.com'], }; // Service worker will continue to give access token to the JavaScript client // Ideal to hide refresh token from client JavaScript, but to retrieve access_token for some // scenarios which require it. For example, to send it via websocket connection. -trustedDomains.config_show_access_token = { domains: ['https://demo.duendesoftware.com'], showAccessToken: true }; +trustedDomains.config_show_access_token = { + domains: ['https://demo.duendesoftware.com'], + showAccessToken: true, + // convertAllRequestsToCorsExceptNavigate: false, + // setAccessTokenToNavigateRequests: true, +}; // This example defines domains used by OIDC server separately from domains to which access tokens will be injected. trustedDomains.config_separate_oidc_access_token_domains = { - oidcDomains: ['https://demo.duendesoftware.com'], - accessTokenDomains: ['https://myapi'], + oidcDomains: ['https://demo.duendesoftware.com'], + accessTokenDomains: ['https://myapi'], }; diff --git a/packages/oidc-client-service-worker/src/__tests__/jwt.spec.ts b/packages/oidc-client-service-worker/src/__tests__/jwt.spec.ts new file mode 100644 index 000000000..6a8af5cd5 --- /dev/null +++ b/packages/oidc-client-service-worker/src/__tests__/jwt.spec.ts @@ -0,0 +1,33 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { + defaultDemonstratingProofOfPossessionConfiguration, + generateJwkAsync, + generateJwtDemonstratingProofOfPossessionAsync, +} from '../jwt'; + +const decodePayload = (jwt: string) => { + const payload = jwt.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'); + return JSON.parse(atob(payload)); +}; + +describe('generateJwtDemonstratingProofOfPossessionAsync', () => { + it('uses a cryptographically secure UUID for the DPoP jti in the service worker', async () => { + const uuid = '123e4567-e89b-42d3-a456-426614174000'; + const randomUUID = vi.fn(() => uuid); + const webCrypto = { + subtle: globalThis.crypto.subtle, + randomUUID, + }; + const jwk = await generateJwkAsync(globalThis)( + defaultDemonstratingProofOfPossessionConfiguration.generateKeyAlgorithm, + ); + + const jwt = await generateJwtDemonstratingProofOfPossessionAsync({ crypto: webCrypto })( + defaultDemonstratingProofOfPossessionConfiguration, + )(jwk, 'GET', 'https://api.example.com/resource'); + + expect(randomUUID).toHaveBeenCalledOnce(); + expect(decodePayload(jwt).jti).toBe(btoa(uuid)); + }); +}); diff --git a/packages/oidc-client-service-worker/src/__tests__/oidcConfig.spec.ts b/packages/oidc-client-service-worker/src/__tests__/oidcConfig.spec.ts new file mode 100644 index 000000000..2fa07829b --- /dev/null +++ b/packages/oidc-client-service-worker/src/__tests__/oidcConfig.spec.ts @@ -0,0 +1,319 @@ +import { describe, expect, it } from 'vitest'; + +import { + getCurrentDatabasesTokenEndpoint, + isAccessTokenDomainRequest, + isOidcServerRequest, + shouldBypassNonOidcRequest, +} from '../oidcConfig'; +import { Database, TrustedDomains } from '../types'; + +const oidcConfigDefaults = { + demonstratingProofOfPossessionConfiguration: null, + configurationName: '', + tokens: null, + status: null, + state: null, + codeVerifier: null, + nonce: null, + hideAccessToken: false, + convertAllRequestsToCorsExceptNavigate: true, + setAccessTokenToNavigateRequests: true, + demonstratingProofOfPossessionNonce: null, + demonstratingProofOfPossessionJwkJson: null, + demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent: false, + allowMultiTabLogin: true, + bypassAllNonOidcRequests: false, +}; + +const oidcServerConfigDefault = { + revocationEndpoint: '', + tokenEndpoint: '', + issuer: '', + userInfoEndpoint: '', + authorizationEndpoint: '', +}; + +describe('getCurrentDatabasesTokenEndpoint', () => { + it.each([ + ['https://EXAMPLE.com:443/token?code=example', 'https://example.com/token'], + ['http://example.com:80/token', 'http://EXAMPLE.com/token'], + ['https://example.com/token/child', 'https://example.com/token'], + ['https://example.com/token2', 'https://example.com/token'], + ['not-a-url', 'not-a-url'], + ])('preserves normalization and prefix matching for %s', (url, endpoint) => { + const database: Database = { + uninitialized: { ...oidcConfigDefaults, oidcServerConfiguration: null }, + token: { + ...oidcConfigDefaults, + oidcServerConfiguration: { ...oidcServerConfigDefault, tokenEndpoint: endpoint }, + }, + revocation: { + ...oidcConfigDefaults, + oidcServerConfiguration: { ...oidcServerConfigDefault, revocationEndpoint: endpoint }, + }, + emptyEndpoints: { + ...oidcConfigDefaults, + oidcServerConfiguration: { ...oidcServerConfigDefault }, + }, + }; + + const result = getCurrentDatabasesTokenEndpoint(database, url); + + expect(result).toEqual([database.token, database.revocation]); + expect(result[0]).toBe(database.token); + expect(result[1]).toBe(database.revocation); + }); + + it('returns no matches for an empty database', () => { + expect(getCurrentDatabasesTokenEndpoint({}, 'https://example.com/token')).toEqual([]); + }); + + it('keeps non-default ports distinct', () => { + const database: Database = { + config: { + ...oidcConfigDefaults, + oidcServerConfiguration: { + ...oidcServerConfigDefault, + tokenEndpoint: 'https://example.com:8443/token', + }, + }, + }; + + expect(getCurrentDatabasesTokenEndpoint(database, 'https://example.com/token')).toEqual([]); + }); + + it('should return configs with matching token endpoint', () => { + const database: Database = { + config1: { + ...oidcConfigDefaults, + oidcServerConfiguration: { + ...oidcServerConfigDefault, + tokenEndpoint: 'https://example.com/token', + }, + }, + config2: { + ...oidcConfigDefaults, + oidcServerConfiguration: { + ...oidcServerConfigDefault, + tokenEndpoint: 'https://example.org/token', + }, + }, + config3: { + ...oidcConfigDefaults, + oidcServerConfiguration: { + ...oidcServerConfigDefault, + revocationEndpoint: 'https://example.net/revoke', + }, + }, + }; + + const url = 'https://example.com/token'; + const result = getCurrentDatabasesTokenEndpoint(database, url); + + expect(result).toHaveLength(1); + expect(result[0]).toBe(database.config1); + }); + + it('should return configs with matching revocation endpoint', () => { + const database = { + config1: { + ...oidcConfigDefaults, + oidcServerConfiguration: { + ...oidcServerConfigDefault, + revocationEndpoint: 'https://example.com/revoke', + }, + }, + config2: { + ...oidcConfigDefaults, + oidcServerConfiguration: { + ...oidcServerConfigDefault, + revocationEndpoint: 'https://example.org/revoke', + }, + }, + config3: { + ...oidcConfigDefaults, + oidcServerConfiguration: { + ...oidcServerConfigDefault, + tokenEndpoint: 'https://example.net/token', + }, + }, + }; + + const url = 'https://example.com/revoke'; + const result = getCurrentDatabasesTokenEndpoint(database, url); + + expect(result).toHaveLength(1); + expect(result[0]).toBe(database.config1); + }); + + it('should return multiple matching configs', () => { + const database = { + config1: { + ...oidcConfigDefaults, + oidcServerConfiguration: { + ...oidcServerConfigDefault, + tokenEndpoint: 'https://example.com/token', + revocationEndpoint: 'https://example.com/revoke', + }, + }, + config2: { + ...oidcConfigDefaults, + oidcServerConfiguration: { + ...oidcServerConfigDefault, + tokenEndpoint: 'https://example.org/token', + }, + }, + config3: { + ...oidcConfigDefaults, + oidcServerConfiguration: { + ...oidcServerConfigDefault, + tokenEndpoint: 'https://example.com/token', + revocationEndpoint: 'https://example.com/revoke', + }, + }, + }; + + const url = 'https://example.com/token'; + const result = getCurrentDatabasesTokenEndpoint(database, url); + + expect(result).toHaveLength(2); + expect(result).toContain(database.config1); + expect(result).toContain(database.config3); + }); + + it('should return empty array for no matching configs', () => { + const database = { + config1: { + ...oidcConfigDefaults, + oidcServerConfiguration: { + ...oidcServerConfigDefault, + tokenEndpoint: 'https://example.com/token', + }, + }, + config2: { + ...oidcConfigDefaults, + oidcServerConfiguration: { + ...oidcServerConfigDefault, + revocationEndpoint: 'https://example.org/revoke', + }, + }, + }; + + const url = 'https://example.net/other'; + const result = getCurrentDatabasesTokenEndpoint(database, url); + + expect(result).toHaveLength(0); + }); +}); + +describe('shouldBypassNonOidcRequest', () => { + const database: Database = { + config1: { + ...oidcConfigDefaults, + bypassAllNonOidcRequests: true, + oidcServerConfiguration: { + ...oidcServerConfigDefault, + issuer: 'https://oidc.example.com', + authorizationEndpoint: 'https://oidc.example.com/connect/authorize', + tokenEndpoint: 'https://oidc.example.com/connect/token', + revocationEndpoint: 'https://oidc.example.com/connect/revoke', + userInfoEndpoint: 'https://oidc.example.com/connect/userinfo', + }, + }, + }; + + it('should bypass non-OIDC requests when enabled', () => { + expect(shouldBypassNonOidcRequest(database, 'https://api.example.com/users', null)).toBe(true); + }); + + it.each([ + 'https://oidc.example.com/.well-known/openid-configuration', + 'https://oidc.example.com/connect/authorize', + 'https://oidc.example.com/connect/token', + 'https://oidc.example.com/connect/revoke', + 'https://oidc.example.com/connect/userinfo', + ])('should never bypass OIDC server request %s', url => { + expect(isOidcServerRequest(database, url)).toBe(true); + expect(shouldBypassNonOidcRequest(database, url, null)).toBe(false); + }); + + it('should keep existing behavior when disabled', () => { + expect( + shouldBypassNonOidcRequest( + { + config1: { + ...database.config1, + bypassAllNonOidcRequests: false, + }, + }, + 'https://api.example.com/users', + null, + ), + ).toBe(false); + }); + + it('should keep existing behavior until OIDC server configuration is initialized', () => { + expect( + shouldBypassNonOidcRequest( + { + config1: { + ...oidcConfigDefaults, + bypassAllNonOidcRequests: true, + oidcServerConfiguration: null, + }, + }, + 'https://api.example.com/users', + null, + ), + ).toBe(false); + }); + + it('should keep accessTokenDomains requests intercepted when bypass is enabled', () => { + const trustedDomains: TrustedDomains = { + config1: { + accessTokenDomains: ['https://api.example.com'], + showAccessToken: false, + }, + }; + + expect( + isAccessTokenDomainRequest(database, 'https://api.example.com/users', trustedDomains), + ).toBe(true); + expect( + shouldBypassNonOidcRequest(database, 'https://api.example.com/users', trustedDomains), + ).toBe(false); + }); + + it('should keep domains fallback requests intercepted when bypass is enabled', () => { + const trustedDomains: TrustedDomains = { + config1: { + domains: ['https://api.example.com'], + showAccessToken: false, + }, + }; + + expect( + shouldBypassNonOidcRequest(database, 'https://api.example.com/users', trustedDomains), + ).toBe(false); + }); + + it('should keep existing behavior unless all initialized configurations enable bypass', () => { + expect( + shouldBypassNonOidcRequest( + { + ...database, + config2: { + ...oidcConfigDefaults, + oidcServerConfiguration: { + ...oidcServerConfigDefault, + issuer: 'https://other-oidc.example.com', + }, + }, + }, + 'https://api.example.com/users', + null, + ), + ).toBe(false); + }); +}); diff --git a/packages/oidc-client-service-worker/src/__tests__/protocol.spec.ts b/packages/oidc-client-service-worker/src/__tests__/protocol.spec.ts new file mode 100644 index 000000000..45d6b4200 --- /dev/null +++ b/packages/oidc-client-service-worker/src/__tests__/protocol.spec.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from 'vitest'; + +import { TOKEN } from '../constants'; +import { + buildDpopSecuredPlaceholder, + buildSecuredTokenPlaceholder, + buildStorageKey, + DPOP_TOKEN_PLACEHOLDER_PREFIX, + isServiceWorkerMessageType, + PROTOCOL_VERSION, + ServiceWorkerMessageType, + STORAGE_KEY_PREFIX, + SW_CONTROLLER_CHANGE_RELOAD_COUNT_KEY, + TOKEN_PLACEHOLDERS, +} from '../protocol'; + +describe('service worker protocol – public surface', () => { + it('exposes a stable PROTOCOL_VERSION', () => { + expect(PROTOCOL_VERSION).toMatch(/^\d+\.\d+\.\d+$/); + }); + + it('keeps every documented message type accessible by canonical key', () => { + expect(ServiceWorkerMessageType).toEqual({ + SKIP_WAITING: 'SKIP_WAITING', + CLAIM: 'claim', + CLEAR: 'clear', + INIT: 'init', + SET_STATE: 'setState', + GET_STATE: 'getState', + SET_CODE_VERIFIER: 'setCodeVerifier', + GET_CODE_VERIFIER: 'getCodeVerifier', + SET_SESSION_STATE: 'setSessionState', + GET_SESSION_STATE: 'getSessionState', + SET_NONCE: 'setNonce', + GET_NONCE: 'getNonce', + SET_DPOP_NONCE: 'setDemonstratingProofOfPossessionNonce', + GET_DPOP_NONCE: 'getDemonstratingProofOfPossessionNonce', + SET_DPOP_JWK: 'setDemonstratingProofOfPossessionJwk', + GET_DPOP_JWK: 'getDemonstratingProofOfPossessionJwk', + }); + }); + + it('matches the internal TOKEN constants used by the service worker', () => { + expect(TOKEN_PLACEHOLDERS.ACCESS_TOKEN).toBe(TOKEN.ACCESS_TOKEN); + expect(TOKEN_PLACEHOLDERS.REFRESH_TOKEN).toBe(TOKEN.REFRESH_TOKEN); + expect(TOKEN_PLACEHOLDERS.NONCE_TOKEN).toBe(TOKEN.NONCE_TOKEN); + expect(TOKEN_PLACEHOLDERS.CODE_VERIFIER).toBe(TOKEN.CODE_VERIFIER); + }); +}); + +describe('buildSecuredTokenPlaceholder', () => { + it('produces the same placeholder format the service worker emits', () => { + expect(buildSecuredTokenPlaceholder(TOKEN_PLACEHOLDERS.ACCESS_TOKEN, 'demo', 'tab-1')).toBe( + 'ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER_demo#tabId=tab-1', + ); + }); + + it('defaults the tab id to "default"', () => { + expect(buildSecuredTokenPlaceholder(TOKEN_PLACEHOLDERS.REFRESH_TOKEN, 'demo')).toBe( + 'REFRESH_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER_demo#tabId=default', + ); + }); +}); + +describe('buildDpopSecuredPlaceholder', () => { + it('uses the documented DPoP prefix', () => { + expect(buildDpopSecuredPlaceholder('demo', 'tab-2')).toBe( + `${DPOP_TOKEN_PLACEHOLDER_PREFIX}_demo#tabId=tab-2`, + ); + }); +}); + +describe('buildStorageKey', () => { + it.each([ + [STORAGE_KEY_PREFIX.STATE, 'demo', 'oidc.state.demo'], + [STORAGE_KEY_PREFIX.NONCE, 'demo', 'oidc.nonce.demo'], + [STORAGE_KEY_PREFIX.CODE_VERIFIER, 'demo', 'oidc.code_verifier.demo'], + [STORAGE_KEY_PREFIX.LOGIN_PARAMS, 'demo', 'oidc.login.demo'], + [STORAGE_KEY_PREFIX.TAB_ID, 'demo', 'oidc.tabId.demo'], + [STORAGE_KEY_PREFIX.SW_VERSION_MISMATCH_RELOAD, 'demo', 'oidc.sw.version_mismatch_reload.demo'], + ])('builds %s + %s into %s', (prefix, configurationName, expected) => { + expect(buildStorageKey(prefix, configurationName)).toBe(expected); + }); + + it('exposes the SW controllerchange reload counter key', () => { + expect(SW_CONTROLLER_CHANGE_RELOAD_COUNT_KEY).toBe('oidc.sw.controllerchange_reload_count'); + }); +}); + +describe('isServiceWorkerMessageType', () => { + it.each(Object.values(ServiceWorkerMessageType))( + 'returns true for known message type "%s"', + type => { + expect(isServiceWorkerMessageType(type)).toBe(true); + }, + ); + + it.each(['unknown', '', 42, null, undefined, {}])( + 'returns false for invalid message type %p', + value => { + expect(isServiceWorkerMessageType(value)).toBe(false); + }, + ); +}); diff --git a/packages/oidc-client-service-worker/src/crypto.ts b/packages/oidc-client-service-worker/src/crypto.ts new file mode 100644 index 000000000..31172f2b2 --- /dev/null +++ b/packages/oidc-client-service-worker/src/crypto.ts @@ -0,0 +1,22 @@ +import { uint8ToUrlBase64 } from './jwt'; + +export function textEncodeLite(str: string) { + const buf = new ArrayBuffer(str.length); + const bufView = new Uint8Array(buf); + + for (let i = 0; i < str.length; i++) { + bufView[i] = str.charCodeAt(i); + } + return bufView; +} + +export function base64urlOfHashOfASCIIEncodingAsync(code: string): Promise { + return new Promise((resolve, reject) => { + crypto.subtle.digest('SHA-256', textEncodeLite(code)).then( + buffer => { + return resolve(uint8ToUrlBase64(new Uint8Array(buffer))); + }, + error => reject(error), + ); + }); +} diff --git a/packages/oidc-client-service-worker/src/dpop.ts b/packages/oidc-client-service-worker/src/dpop.ts new file mode 100644 index 000000000..79a668d41 --- /dev/null +++ b/packages/oidc-client-service-worker/src/dpop.ts @@ -0,0 +1,36 @@ +import { defaultDemonstratingProofOfPossessionConfiguration } from './jwt'; +import { Domain, DomainDetails } from './types.js'; + +const isDpop = (trustedDomain: Domain[] | DomainDetails): boolean => { + if (Array.isArray(trustedDomain)) { + return false; + } + return trustedDomain.demonstratingProofOfPossession ?? false; +}; + +export const getDpopConfiguration = (trustedDomain: Domain[] | DomainDetails) => { + if (!isDpop(trustedDomain)) { + return null; + } + + if (Array.isArray(trustedDomain)) { + return null; + } + + return ( + trustedDomain.demonstratingProofOfPossessionConfiguration ?? + defaultDemonstratingProofOfPossessionConfiguration + ); +}; + +export const getDpopOnlyWhenDpopHeaderPresent = (trustedDomain: Domain[] | DomainDetails) => { + if (!isDpop(trustedDomain)) { + return null; + } + + if (Array.isArray(trustedDomain)) { + return null; + } + + return trustedDomain.demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent ?? true; +}; diff --git a/packages/oidc-client-service-worker/src/jwt.ts b/packages/oidc-client-service-worker/src/jwt.ts new file mode 100644 index 000000000..2672477bb --- /dev/null +++ b/packages/oidc-client-service-worker/src/jwt.ts @@ -0,0 +1,233 @@ +// code base on https://coolaj86.com/articles/sign-jwt-webcrypto-vanilla-js/ + +// String (UCS-2) to Uint8Array +// +// because... JavaScript, Strings, and Buffers +// @ts-ignore +import { DemonstratingProofOfPossessionConfiguration } from './types'; + +function strToUint8(str: string) { + return new TextEncoder().encode(str); +} + +// Binary String to URL-Safe Base64 +// +// btoa (Binary-to-Ascii) means "binary string" to base64 +// @ts-ignore +function binToUrlBase64(bin) { + return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+/g, ''); +} + +// UTF-8 to Binary String +// +// Because JavaScript has a strange relationship with strings +// https://coolaj86.com/articles/base64-unicode-utf-8-javascript-and-you/ +// @ts-ignore +function utf8ToBinaryString(str) { + const escstr = encodeURIComponent(str); + // replaces any uri escape sequence, such as %0A, + // with binary escape, such as 0x0A + // @ts-ignore + return escstr.replace(/%([0-9A-F]{2})/g, function (match: string, p1) { + return String.fromCharCode(parseInt(p1, 16)); + }); +} + +// Uint8Array to URL Safe Base64 +// +// the shortest distant between two encodings... binary string +// @ts-ignore +export const uint8ToUrlBase64 = (uint8: Uint8Array) => { + let bin = ''; + // @ts-ignore + uint8.forEach(function (code) { + bin += String.fromCharCode(code); + }); + return binToUrlBase64(bin); +}; + +// UCS-2 String to URL-Safe Base64 +// +// btoa doesn't work on UTF-8 strings +// @ts-ignore +function strToUrlBase64(str) { + return binToUrlBase64(utf8ToBinaryString(str)); +} + +export const defaultDemonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration = + { + importKeyAlgorithm: { + name: 'ECDSA', + namedCurve: 'P-256', + hash: { name: 'ES256' }, + }, + signAlgorithm: { name: 'ECDSA', hash: { name: 'SHA-256' } }, + generateKeyAlgorithm: { + name: 'ECDSA', + namedCurve: 'P-256', + }, + digestAlgorithm: { name: 'SHA-256' }, + jwtHeaderAlgorithm: 'ES256', + }; + +// @ts-ignore +const sign = + (w: any) => + async ( + jwk: any, + headers: any, + claims: any, + demonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration, + jwtHeaderType = 'dpop+jwt', + ) => { + // Make a shallow copy of the key + // (to set ext if it wasn't already set) + jwk = Object.assign({}, jwk); + + // The headers should probably be empty + headers.typ = jwtHeaderType; + headers.alg = demonstratingProofOfPossessionConfiguration.jwtHeaderAlgorithm; + switch (headers.alg) { + case 'ES256': //if (!headers.kid) { + // alternate: see thumbprint function below + headers.jwk = { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y }; + //} + break; + case 'RS256': + headers.jwk = { kty: jwk.kty, n: jwk.n, e: jwk.e, kid: headers.kid }; + break; + default: + throw new Error('Unknown or not implemented JWS algorithm'); + } + + const jws = { + // @ts-ignore + // JWT "headers" really means JWS "protected headers" + protected: strToUrlBase64(JSON.stringify(headers)), + // @ts-ignore + // JWT "claims" are really a JSON-defined JWS "payload" + payload: strToUrlBase64(JSON.stringify(claims)), + }; + + // To import as EC (ECDSA, P-256, SHA-256, ES256) + const keyType = demonstratingProofOfPossessionConfiguration.importKeyAlgorithm; + + // To make re-exportable as JSON (or DER/PEM) + const exportable = true; + + // Import as a private key that isn't black-listed from signing + const privileges = ['sign']; + + // Actually do the import, which comes out as an abstract key type + // @ts-ignore + const privateKey = await w.crypto.subtle.importKey('jwk', jwk, keyType, exportable, privileges); + // Convert UTF-8 to Uint8Array ArrayBuffer + // @ts-ignore + const data = strToUint8(`${jws.protected}.${jws.payload}`); + + // The signature and hash should match the bit-entropy of the key + // https://tools.ietf.org/html/rfc7518#section-3 + const signatureType = demonstratingProofOfPossessionConfiguration.signAlgorithm; + + const signature = await w.crypto.subtle.sign(signatureType, privateKey, data); + // returns an ArrayBuffer containing a JOSE (not X509) signature, + // which must be converted to Uint8 to be useful + // @ts-ignore + jws.signature = uint8ToUrlBase64(new Uint8Array(signature)); + // JWT is just a "compressed", "protected" JWS + // @ts-ignore + return `${jws.protected}.${jws.payload}.${jws.signature}`; + }; + +export const JWT = { sign }; + +// @ts-ignore +const generate = + (w: any) => async (generateKeyAlgorithm: RsaHashedKeyGenParams | EcKeyGenParams) => { + const keyType = generateKeyAlgorithm; + const exportable = true; + const privileges = ['sign', 'verify']; + // @ts-ignore + const key = await w.crypto.subtle.generateKey(keyType, exportable, privileges); + // returns an abstract and opaque WebCrypto object, + // which in most cases you'll want to export as JSON to be able to save + return await w.crypto.subtle.exportKey('jwk', key.privateKey); + }; + +// Create a Public Key from a Private Key +// +// chops off the private parts +// @ts-ignore +const neuter = jwk => { + const copy = Object.assign({}, jwk); + delete copy.d; + copy.key_ops = ['verify']; + return copy; +}; + +const EC = { + generate, + neuter, +}; +// @ts-ignore +const thumbprint = (w: any) => async (jwk, digestAlgorithm: AlgorithmIdentifier) => { + let sortedPub; + // lexigraphically sorted, no spaces + switch (jwk.kty) { + case 'EC': + sortedPub = '{"crv":"CRV","kty":"EC","x":"X","y":"Y"}' + .replace('CRV', jwk.crv) + .replace('X', jwk.x) + .replace('Y', jwk.y); + break; + case 'RSA': + sortedPub = '{"e":"E","kty":"RSA","n":"N"}'.replace('E', jwk.e).replace('N', jwk.n); + break; + default: + throw new Error('Unknown or not implemented JWK type'); + } + // The hash should match the size of the key, + // but we're only dealing with P-256 + const hash = await w.crypto.subtle.digest(digestAlgorithm, strToUint8(sortedPub)); + return uint8ToUrlBase64(new Uint8Array(hash)); +}; + +export const JWK = { thumbprint }; + +export const generateJwkAsync = + (w: any) => async (generateKeyAlgorithm: RsaHashedKeyGenParams | EcKeyGenParams) => { + // @ts-ignore + const jwk = await EC.generate(w)(generateKeyAlgorithm); + // console.info('Private Key:', JSON.stringify(jwk)); + // @ts-ignore + // console.info('Public Key:', JSON.stringify(EC.neuter(jwk))); + return jwk; + }; + +export const generateJwtDemonstratingProofOfPossessionAsync = + (w: any) => + (demonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration) => + async (jwk: any, method = 'POST', url: string, extrasClaims = {}) => { + const claims = { + // https://www.rfc-editor.org/rfc/rfc9449.html#name-concept + jti: btoa(w.crypto.randomUUID()), + htm: method, + htu: url, + iat: Math.round(Date.now() / 1000), + ...extrasClaims, + }; + // @ts-ignore + const kid = await JWK.thumbprint(w)( + jwk, + demonstratingProofOfPossessionConfiguration.digestAlgorithm, + ); + // @ts-ignore + const jwt = await JWT.sign(w)( + jwk, + { kid: kid }, + claims, + demonstratingProofOfPossessionConfiguration, + ); + // console.info('JWT:', jwt); + return jwt; + }; diff --git a/packages/oidc-client-service-worker/src/oidcConfig.ts b/packages/oidc-client-service-worker/src/oidcConfig.ts new file mode 100644 index 000000000..2aab2bade --- /dev/null +++ b/packages/oidc-client-service-worker/src/oidcConfig.ts @@ -0,0 +1,102 @@ +import { acceptAnyDomainToken } from './constants'; +import { Database, Domain, OidcConfig, OidcServerConfiguration, TrustedDomains } from './types'; +import { getDomains, normalizeUrl } from './utils'; + +const getOidcServerUrls = (oidcServerConfiguration: OidcServerConfiguration): string[] => { + return [ + oidcServerConfiguration.issuer, + oidcServerConfiguration.authorizationEndpoint, + oidcServerConfiguration.tokenEndpoint, + oidcServerConfiguration.revocationEndpoint, + oidcServerConfiguration.userInfoEndpoint, + ] + .filter(Boolean) + .map(normalizeUrl); +}; + +const isOidcServerRequest = (database: Database, normalizedUrl: string): boolean => { + return Object.values(database).some(config => { + const { oidcServerConfiguration } = config || {}; + if (!oidcServerConfiguration) { + return false; + } + + return getOidcServerUrls(oidcServerConfiguration).some(oidcUrl => + normalizedUrl.startsWith(oidcUrl), + ); + }); +}; + +const isDomainMatchingUrl = (domain: Domain, normalizedUrl: string): boolean => { + if (typeof domain === 'string') { + domain = new RegExp(`^${domain}`); + } + + return domain.test?.(normalizedUrl) ?? false; +}; + +const isAccessTokenDomainRequest = ( + database: Database, + normalizedUrl: string, + trustedDomains: TrustedDomains, +): boolean => { + return Object.entries(database).some(([key, currentDatabase]) => { + if (!currentDatabase.oidcServerConfiguration) { + return false; + } + + const trustedDomain = trustedDomains?.[key.split('#')[0]] ?? []; + const domains = getDomains(trustedDomain, 'accessToken'); + + if (domains.some(domain => domain === acceptAnyDomainToken)) { + return true; + } + + return domains.some(domain => isDomainMatchingUrl(domain, normalizedUrl)); + }); +}; + +const shouldBypassNonOidcRequest = ( + database: Database, + normalizedUrl: string, + trustedDomains: TrustedDomains, +): boolean => { + const configurations = Object.values(database); + + if (configurations.length === 0) { + return false; + } + + if (!configurations.every(config => config.bypassAllNonOidcRequests)) { + return false; + } + + if (!configurations.every(config => config.oidcServerConfiguration != null)) { + return false; + } + + return ( + !isOidcServerRequest(database, normalizedUrl) && + !isAccessTokenDomainRequest(database, normalizedUrl, trustedDomains) + ); +}; + +const getMatchingOidcConfigurations = (database: Database, url: string): OidcConfig[] => { + const normalizedUrl = normalizeUrl(url); + return Object.values(database).filter(config => { + const { oidcServerConfiguration } = config || {}; + const { tokenEndpoint, revocationEndpoint } = oidcServerConfiguration || {}; + + return ( + (tokenEndpoint && normalizedUrl.startsWith(normalizeUrl(tokenEndpoint))) || + (revocationEndpoint && normalizedUrl.startsWith(normalizeUrl(revocationEndpoint))) + ); + }); +}; + +export { + getMatchingOidcConfigurations as getCurrentDatabasesTokenEndpoint, + isAccessTokenDomainRequest, + isOidcServerRequest, + shouldBypassNonOidcRequest, +}; diff --git a/packages/oidc-client-service-worker/src/protocol.ts b/packages/oidc-client-service-worker/src/protocol.ts new file mode 100644 index 000000000..65021a55e --- /dev/null +++ b/packages/oidc-client-service-worker/src/protocol.ts @@ -0,0 +1,194 @@ +/** + * Public, supported entry point for the oidc-client service worker + * `postMessage` protocol. + * + * See `PROTOCOL.md` (in this package) for a full description of every + * message type, payload, response shape, storage key convention and the + * stability guarantees that apply to those exports. + */ + +import type { + MessageData, + MessageEventData, + MessageEventType, + Nonce, + OidcConfiguration, + OidcServerConfiguration, + Status, +} from './types'; + +export type { + MessageData, + MessageEventData, + MessageEventType, + Nonce, + OidcConfiguration, + OidcServerConfiguration, + Status, +}; + +/** + * Semver-protected version of the service worker `postMessage` protocol. + * + * - The `MAJOR` component changes only on a breaking change to any of the + * exports in this module. + * - The `MINOR` component changes when new (additive) message types or + * helpers are introduced. + * - The `PATCH` component changes for documentation or non-behavioural + * tweaks. + */ +export const PROTOCOL_VERSION = '1.0.0' as const; + +/** + * Every supported service worker message type. + * + * The values are also the literal string used on the wire as + * `MessageEventData.type` and must therefore remain stable across patch and + * minor versions of the protocol. + */ +export const ServiceWorkerMessageType = { + /** Lifecycle: tells the SW to skip the `waiting` state. */ + SKIP_WAITING: 'SKIP_WAITING', + /** Lifecycle: asks the SW to claim the current page. */ + CLAIM: 'claim', + /** Resets the session entry kept inside the SW for a configuration. */ + CLEAR: 'clear', + /** Initialises a configuration entry inside the SW. */ + INIT: 'init', + SET_STATE: 'setState', + GET_STATE: 'getState', + SET_CODE_VERIFIER: 'setCodeVerifier', + GET_CODE_VERIFIER: 'getCodeVerifier', + SET_SESSION_STATE: 'setSessionState', + GET_SESSION_STATE: 'getSessionState', + SET_NONCE: 'setNonce', + GET_NONCE: 'getNonce', + SET_DPOP_NONCE: 'setDemonstratingProofOfPossessionNonce', + GET_DPOP_NONCE: 'getDemonstratingProofOfPossessionNonce', + SET_DPOP_JWK: 'setDemonstratingProofOfPossessionJwk', + GET_DPOP_JWK: 'getDemonstratingProofOfPossessionJwk', +} as const; + +export type ServiceWorkerMessageTypeKey = keyof typeof ServiceWorkerMessageType; +export type ServiceWorkerMessageTypeValue = + (typeof ServiceWorkerMessageType)[ServiceWorkerMessageTypeKey]; + +/** + * Structural shape of a message sent to the service worker. The shape is + * identical to {@link MessageEventData} but typed against the public + * {@link ServiceWorkerMessageTypeValue} union and with optional fields where + * the underlying SW tolerates them. + */ +export interface ServiceWorkerMessage< + TData extends Partial | null = Partial | null, +> { + type: ServiceWorkerMessageTypeValue | 'SKIP_WAITING' | 'claim'; + configurationName: string; + data: TData; + /** + * Optional tab identifier injected by the high-level helpers. Consumers + * sending raw messages can omit it; the SW falls back to `'default'`. + */ + tabId?: string; +} + +/** + * Generic envelope used by every response. Each individual message type may + * extend it with additional, well-known fields (see PROTOCOL.md). + */ +export interface ServiceWorkerResponse { + configurationName?: string; + /** Set by the SW when it cannot service the request. */ + error?: unknown; + [key: string]: unknown; +} + +/** + * Stable internal token placeholders. They are returned by the service + * worker in lieu of secret values (access/refresh tokens, nonces and code + * verifier) so consumers never see the cleartext. + */ +export const TOKEN_PLACEHOLDERS = { + ACCESS_TOKEN: 'ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER', + REFRESH_TOKEN: 'REFRESH_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER', + NONCE_TOKEN: 'NONCE_SECURED_BY_OIDC_SERVICE_WORKER', + CODE_VERIFIER: 'CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER', +} as const; + +/** Prefix used for every DPoP placeholder returned by the SW. */ +export const DPOP_TOKEN_PLACEHOLDER_PREFIX = 'DPOP_SECURED_BY_OIDC_SERVICE_WORKER' as const; + +/** + * Browser storage key conventions used by the OIDC client when no service + * worker is available, or as a fallback alongside the SW. They are exported + * so external integrations can read/clean up the same entries. + */ +export const STORAGE_KEY_PREFIX = { + /** sessionStorage – per-tab tab identifier (`oidc.tabId.`). */ + TAB_ID: 'oidc.tabId.', + /** sessionStorage – `oidc.state.`. */ + STATE: 'oidc.state.', + /** sessionStorage – `oidc.nonce.`. */ + NONCE: 'oidc.nonce.', + /** sessionStorage – `oidc.code_verifier.`. */ + CODE_VERIFIER: 'oidc.code_verifier.', + /** localStorage – `oidc.login.`. */ + LOGIN_PARAMS: 'oidc.login.', + /** sessionStorage – `oidc.sw.version_mismatch_reload.`. */ + SW_VERSION_MISMATCH_RELOAD: 'oidc.sw.version_mismatch_reload.', +} as const; + +/** sessionStorage key tracking the SW controllerchange reload counter. */ +export const SW_CONTROLLER_CHANGE_RELOAD_COUNT_KEY = + 'oidc.sw.controllerchange_reload_count' as const; + +/** + * Builds a typed sessionStorage / localStorage key from one of the known + * prefixes and a configuration name. The function is intentionally + * single-purpose so we never lose the prefix-based stability guarantee. + */ +export const buildStorageKey = ( + prefix: (typeof STORAGE_KEY_PREFIX)[keyof typeof STORAGE_KEY_PREFIX], + configurationName: string, +): string => `${prefix}${configurationName}`; + +/** + * Builds the stable placeholder string the SW returns instead of an + * access/refresh/nonce/code-verifier secret. Useful for consumers that need + * to detect whether a value comes from the SW. + */ +export const buildSecuredTokenPlaceholder = ( + placeholder: (typeof TOKEN_PLACEHOLDERS)[keyof typeof TOKEN_PLACEHOLDERS], + configurationName: string, + tabId: string = 'default', +): string => `${placeholder}_${configurationName}#tabId=${tabId}`; + +/** + * Builds the placeholder returned for DPoP-secured access tokens + * (`DPOP_SECURED_BY_OIDC_SERVICE_WORKER_#tabId=`). + */ +export const buildDpopSecuredPlaceholder = ( + configurationName: string, + tabId: string = 'default', +): string => `${DPOP_TOKEN_PLACEHOLDER_PREFIX}_${configurationName}#tabId=${tabId}`; + +/** + * Type guard returning `true` when the provided value is one of the + * supported, public service worker message types. + */ +export const isServiceWorkerMessageType = ( + value: unknown, +): value is ServiceWorkerMessageTypeValue => { + if (typeof value !== 'string') { + return false; + } + return Object.values(ServiceWorkerMessageType).includes(value as ServiceWorkerMessageTypeValue); +}; + +/** + * Internal wire-level type, re-exported for legacy parity. New code should + * prefer {@link ServiceWorkerMessageTypeValue}. + * + * @internal + */ +export type ServiceWorkerMessageEventType = MessageEventType; diff --git a/packages/oidc-client-service-worker/src/types.ts b/packages/oidc-client-service-worker/src/types.ts index 4fbce2143..fcd909f50 100644 --- a/packages/oidc-client-service-worker/src/types.ts +++ b/packages/oidc-client-service-worker/src/types.ts @@ -1,101 +1,151 @@ export type DomainDetails = { - domains?: Domain[]; - oidcDomains?: Domain[]; - accessTokenDomains?: Domain[]; - showAccessToken: boolean; + domains?: Domain[]; + oidcDomains?: Domain[]; + accessTokenDomains?: Domain[]; + showAccessToken: boolean; + convertAllRequestsToCorsExceptNavigate?: boolean; + setAccessTokenToNavigateRequests?: boolean; + demonstratingProofOfPossession?: boolean; + demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent?: boolean; + demonstratingProofOfPossessionConfiguration?: DemonstratingProofOfPossessionConfiguration; + allowMultiTabLogin?: boolean; + bypassAllNonOidcRequests?: boolean; +}; + +export interface DemonstratingProofOfPossessionConfiguration { + generateKeyAlgorithm: RsaHashedKeyGenParams | EcKeyGenParams; + digestAlgorithm: AlgorithmIdentifier; + importKeyAlgorithm: + | AlgorithmIdentifier + | RsaHashedImportParams + | EcKeyImportParams + | HmacImportParams + | AesKeyAlgorithm; + signAlgorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams; + jwtHeaderAlgorithm: string; } export type Domain = string | RegExp; export type TrustedDomains = { - [key: string]: Domain[] | DomainDetails; + [key: string]: Domain[] | DomainDetails; } | null; export type OidcServerConfiguration = { - revocationEndpoint: string; - issuer: string; - authorizationEndpoint: string; - tokenEndpoint: string; - userInfoEndpoint: string; -} + revocationEndpoint: string; + issuer: string; + authorizationEndpoint: string; + tokenEndpoint: string; + userInfoEndpoint: string; +}; export type OidcConfiguration = { - token_renew_mode: string; - service_worker_convert_all_requests_to_cors: boolean; -} - -// Uncertain why the Headers interface in lib.webworker.d.ts does not have a keys() function, so extending -export interface FetchHeaders extends Headers { - keys(): string[]; -} + token_renew_mode: string; + demonstrating_proof_of_possession: boolean; +}; -export type Status = 'LOGGED' | 'LOGGED_IN' | 'LOGGED_OUT' | 'NOT_CONNECTED' | 'LOGOUT_FROM_ANOTHER_TAB' | 'SESSION_LOST' | 'REQUIRE_SYNC_TOKENS' | 'FORCE_REFRESH' | null; -export type MessageEventType = 'clear' | 'init' | 'setState' | 'getState' | 'setCodeVerifier' | 'getCodeVerifier' | 'setSessionState' | 'getSessionState' | 'setNonce' | 'getNonce'; +export type Status = + | 'LOGGED' + | 'LOGGED_IN' + | 'LOGGED_OUT' + | 'NOT_CONNECTED' + | 'LOGOUT_FROM_ANOTHER_TAB' + | 'SESSION_LOST' + | 'REQUIRE_SYNC_TOKENS' + | 'FORCE_REFRESH' + | null; +export type MessageEventType = + | 'clear' + | 'init' + | 'setState' + | 'getState' + | 'setCodeVerifier' + | 'getCodeVerifier' + | 'setSessionState' + | 'getSessionState' + | 'setNonce' + | 'getNonce' + | 'setDemonstratingProofOfPossessionNonce' + | 'getDemonstratingProofOfPossessionNonce' + | 'setDemonstratingProofOfPossessionJwk' + | 'getDemonstratingProofOfPossessionJwk'; export type MessageData = { - status: Status; - oidcServerConfiguration: OidcServerConfiguration; - oidcConfiguration: OidcConfiguration; - where: string; - state: string; - codeVerifier: string; - sessionState: string; - nonce: Nonce; -} + status: Status; + oidcServerConfiguration: OidcServerConfiguration; + oidcConfiguration: OidcConfiguration; + where: string; + state: string; + codeVerifier: string; + sessionState: string; + demonstratingProofOfPossessionNonce: string; + demonstratingProofOfPossessionJwkJson: string; + nonce: Nonce; +}; export type MessageEventData = { - configurationName: string; - type: MessageEventType; - data: MessageData; -} + configurationName: string; + tabId: string; + type: MessageEventType; + data: MessageData; +}; export type Nonce = { - nonce: string; + nonce: string; } | null; export type OidcConfig = { - configurationName: string; - tokens: Tokens | null; - status: Status; - state: string | null; - codeVerifier: string | null; - nonce: Nonce; - oidcServerConfiguration: OidcServerConfiguration | null; - oidcConfiguration?: OidcConfiguration; - sessionState?: string | null; - items?: MessageData; - hideAccessToken: boolean; -} + demonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration | null; + configurationName: string; + tokens: Tokens | null; + status: Status; + state: string | null; + codeVerifier: string | null; + nonce: Nonce; + oidcServerConfiguration: OidcServerConfiguration | null; + oidcConfiguration?: OidcConfiguration; + sessionState?: string | null; + items?: MessageData; + hideAccessToken: boolean; + convertAllRequestsToCorsExceptNavigate: boolean; + setAccessTokenToNavigateRequests: boolean; + demonstratingProofOfPossessionNonce: string | null; + demonstratingProofOfPossessionJwkJson: string | null; + demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent: boolean; + allowMultiTabLogin: boolean; + bypassAllNonOidcRequests: boolean; +}; export type IdTokenPayload = { - iss: string; - /** - * (Expiration Time) Claim - */ - exp: number; - /** - * (Issued At) Claim - */ - iat: number; - nonce: string | null; -} + iss: string; + /** + * (Expiration Time) Claim + */ + exp: number; + /** + * (Issued At) Claim + */ + iat: number; + nonce: string | null; +}; export type AccessTokenPayload = { - exp: number; - sub: string; -} + exp: number; + sub: string; + iat: number; +}; export type Tokens = { - issued_at: number; - access_token: string; - accessTokenPayload: AccessTokenPayload | null; - id_token: null | string; - idTokenPayload: IdTokenPayload; - refresh_token?: string; - expiresAt: number; - expires_in: number; + issued_at: number | string; + access_token: string; + accessTokenPayload: AccessTokenPayload | null; + id_token: null | string; + idTokenPayload: IdTokenPayload; + refresh_token?: string; + expiresAt: number; + expires_in: number | string; }; export type Database = { - [key: string]: OidcConfig; -} + [key: string]: OidcConfig; +}; diff --git a/packages/oidc-client-service-worker/src/utils/__tests__/codeVerifier.spec.ts b/packages/oidc-client-service-worker/src/utils/__tests__/codeVerifier.spec.ts index e2ad25a05..8e6f7b605 100644 --- a/packages/oidc-client-service-worker/src/utils/__tests__/codeVerifier.spec.ts +++ b/packages/oidc-client-service-worker/src/utils/__tests__/codeVerifier.spec.ts @@ -1,13 +1,70 @@ import { describe, expect, it } from 'vitest'; -import { replaceCodeVerifier } from '../codeVerifier'; +import { extractConfigurationNameFromCodeVerifier, replaceCodeVerifier } from '../codeVerifier'; describe('replaceCodeVerifier should', () => { - it.each([ - { body: 'code=F5CDCDB9AADB9ADA59560DE80CAAA6688BC5C8BA1CC1C1F9839F7E7B32171B3D-1&grant_type=authorization_code&client_id=interactive.public.short&redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Fauthentication%2Fcallback&code_verifier=ONskPfcbfAYPp5xqhpMstHSz017896R7sy3wqrRdqC8lYB8yQciCCNLooqLC9qHFTF2FFhDQP4m8PEFNSry8eoCbQ9baYcoWjF1bEH6vGWExdTIMqauicjeVxqz58FO8', bodyExpected: 'code=F5CDCDB9AADB9ADA59560DE80CAAA6688BC5C8BA1CC1C1F9839F7E7B32171B3D-1&grant_type=authorization_code&client_id=interactive.public.short&redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Fauthentication%2Fcallback&code_verifier=1234' }, - { body: 'code=F5CDCDB9AADB9ADA59560DE80CAAA6688BC5C8BA1CC1C1F9839F7E7B32171B3D-1&code_verifier=ONskPfcbfAYPp5xqhpMstHSz017896R7sy3wqrRdqC8lYB8yQciCCNLooqLC9qHFTF2FFhDQP4m8PEFNSry8eoCbQ9baYcoWjF1bEH6vGWExdTIMqauicjeVxqz58FO8&grant_type=authorization_code&client_id=interactive.public.short&redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Fauthentication%2Fcallback', bodyExpected: 'code=F5CDCDB9AADB9ADA59560DE80CAAA6688BC5C8BA1CC1C1F9839F7E7B32171B3D-1&code_verifier=1234&grant_type=authorization_code&client_id=interactive.public.short&redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Fauthentication%2Fcallback' }, - ])('inject new codeVerifier', async ({ body, bodyExpected }) => { - const result = replaceCodeVerifier(body, '1234'); - expect(bodyExpected).toEqual(result); - }); + it.each([ + ['?code_verifier=old&state=example', '&code_verifier=new&state=example'], + ['&CODE_VERIFIER=old', '&code_verifier=new'], + ['&code_verifier=first&code_verifier=second', '&code_verifier=new&code_verifier=second'], + ['code_verifier=old', 'code_verifier=old'], + ['&code_verifier=&state=example', '&code_verifier=&state=example'], + ['&other=example', '&other=example'], + ['', ''], + ])('preserve replacement boundaries for %s', (body, expected) => { + expect(replaceCodeVerifier(body, 'new')).toBe(expected); + }); + + it.each([ + { + body: 'code=F5CDCDB9AADB9ADA59560DE80CAAA6688BC5C8BA1CC1C1F9839F7E7B32171B3D-1&grant_type=authorization_code&client_id=interactive.public.short&redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Fauthentication%2Fcallback&code_verifier=ONskPfcbfAYPp5xqhpMstHSz017896R7sy3wqrRdqC8lYB8yQciCCNLooqLC9qHFTF2FFhDQP4m8PEFNSry8eoCbQ9baYcoWjF1bEH6vGWExdTIMqauicjeVxqz58FO8', + bodyExpected: + 'code=F5CDCDB9AADB9ADA59560DE80CAAA6688BC5C8BA1CC1C1F9839F7E7B32171B3D-1&grant_type=authorization_code&client_id=interactive.public.short&redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Fauthentication%2Fcallback&code_verifier=1234', + }, + { + body: 'code=F5CDCDB9AADB9ADA59560DE80CAAA6688BC5C8BA1CC1C1F9839F7E7B32171B3D-1&code_verifier=ONskPfcbfAYPp5xqhpMstHSz017896R7sy3wqrRdqC8lYB8yQciCCNLooqLC9qHFTF2FFhDQP4m8PEFNSry8eoCbQ9baYcoWjF1bEH6vGWExdTIMqauicjeVxqz58FO8&grant_type=authorization_code&client_id=interactive.public.short&redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Fauthentication%2Fcallback', + bodyExpected: + 'code=F5CDCDB9AADB9ADA59560DE80CAAA6688BC5C8BA1CC1C1F9839F7E7B32171B3D-1&code_verifier=1234&grant_type=authorization_code&client_id=interactive.public.short&redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Fauthentication%2Fcallback', + }, + ])('inject new codeVerifier', async ({ body, bodyExpected }) => { + const result = replaceCodeVerifier(body, '1234'); + expect(bodyExpected).toEqual(result); + }); +}); + +describe('extractConfigurationNameFromCodeVerifier should', () => { + it.each([ + ['?code_verifier=CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER_team%20one&state=x', 'team one'], + ['&code_verifier=CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER_team%23tab', 'team#tab'], + ['&code_verifier=CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER_team+one', 'team+one'], + ['code_verifier=CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER_default', ''], + ['&CODE_VERIFIER=CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER_default', ''], + ['&code_verifier=CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER_', ''], + ['&code_verifier=ordinary', ''], + ['', ''], + ])('preserve extraction boundaries and decoding for %s', (body, expected) => { + expect(extractConfigurationNameFromCodeVerifier(body)).toBe(expected); + }); + + it('preserve malformed configuration escape errors', () => { + expect(() => + extractConfigurationNameFromCodeVerifier( + '&code_verifier=CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER_%ZZ', + ), + ).toThrow(URIError); + }); + + it.each([ + { + body: 'code=56DB8E3592FBD48DCF6F65B38B12845FF0186ECF6D66ECB5425C0F7E658B7951-1&grant_type=authorization_code&client_id=interactive.public.short&redirect_uri=https%3A%2F%2Fblack-rock-0dc6b0d03.1.azurestaticapps.net%2Fauthentication%2Fcallback&code_verifier=CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER_default_tab1', + expected: 'default_tab1', + }, + { + body: 'code=56DB8E3592FBD48DCF6F65B38B12845FF0186ECF6D66ECB5425C0F7E658B7951-1&code_verifier=CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER_youhou_tab2&grant_type=authorization_code&client_id=interactive.public.short&redirect_uri=https%3A%2F%2Fblack-rock-0dc6b0d03.1.azurestaticapps.net%2Fauthentication%2Fcallback', + expected: 'youhou_tab2', + }, + ])('inject new codeVerifier', async ({ body, expected }) => { + const configurationName = extractConfigurationNameFromCodeVerifier(body); + expect(configurationName).toEqual(expected); + }); }); diff --git a/packages/oidc-client-service-worker/src/utils/__tests__/domains.spec.ts b/packages/oidc-client-service-worker/src/utils/__tests__/domains.spec.ts index 9ba81d569..20fb8f016 100644 --- a/packages/oidc-client-service-worker/src/utils/__tests__/domains.spec.ts +++ b/packages/oidc-client-service-worker/src/utils/__tests__/domains.spec.ts @@ -1,68 +1,118 @@ -import { describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it } from 'vitest'; import { openidWellknownUrlEndWith } from '../../constants'; +import { Database, Tokens, TrustedDomains } from '../../types'; import { checkDomain, getCurrentDatabaseDomain } from '..'; -import { Database, OidcServerConfiguration, Tokens, TrustedDomains } from './../../types'; describe('domains', () => { describe('can check domain matches', () => { - it('can check string domains and return void', () => { - const result = () => - checkDomain( - ['https://securesite.com:3000'], - 'https://securesite.com:3000', - ); - expect(result()).toBeUndefined(); + it('can check string domains without throwing an exception', () => { + expect(() => { + checkDomain(['https://securesite.com:3000'], 'https://securesite.com:3000'); + }).not.toThrow(); }); - it('can check regExp domains and return void when valid', () => { - const result = () => - checkDomain( - [/^https:\/\/securesite\.com/], - 'https://securesite.com:3000', - ); - expect(result()).toBeUndefined(); + it('can check regExp domains without throwing an exception when valid', () => { + expect(() => { + checkDomain([/^https:\/\/securesite\.com/], 'https://securesite.com:3000'); + }).not.toThrow(); }); it('will throw error when domain is not trusted', () => { const result = () => - checkDomain( - ['https://notsecuresite.com'], - 'https://securesite.com:3000', - ); + checkDomain(['https://notsecuresite.com'], 'https://securesite.com:3000'); expect(result).toThrowError(); }); - it('will return void when endpoint is falsy', () => { - const result = () => checkDomain(['https://securesite.com:3000'], ''); - expect(result()).toBeUndefined(); + it('will not throw an exception when endpoint is falsy', () => { + expect(() => { + checkDomain(['https://securesite.com:3000'], ''); + }).not.toThrow(); }); }); describe('getCurrentDatabaseDomain', () => { - const db: Database = { - default: { - configurationName: 'config', - tokens: {} as Tokens, - status: 'NOT_CONNECTED', - state: null, - codeVerifier: null, - nonce: null, - oidcServerConfiguration: {} as OidcServerConfiguration, - hideAccessToken: true, - }, - }; + let db: Database; + + beforeEach(() => { + db = { + default: { + configurationName: 'config', + tokens: {} as Tokens, + status: 'NOT_CONNECTED', + state: null, + codeVerifier: null, + nonce: null, + oidcServerConfiguration: { + authorizationEndpoint: 'https://demo.duendesoftware.com/connect/authorize', + issuer: 'https://demo.duendesoftware.com', + revocationEndpoint: 'https://demo.duendesoftware.com/connect/revocation', + tokenEndpoint: 'https://demo.duendesoftware.com/connect/token', + userInfoEndpoint: 'https://demo.duendesoftware.com/connect/userinfo', + }, + hideAccessToken: true, + convertAllRequestsToCorsExceptNavigate: false, + setAccessTokenToNavigateRequests: true, + demonstratingProofOfPossessionNonce: null, + demonstratingProofOfPossessionJwkJson: null, + demonstratingProofOfPossessionConfiguration: null, + demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent: false, + allowMultiTabLogin: true, + bypassAllNonOidcRequests: false, + }, + }; + }); it('will return null when url ends with openidWellknownUrlEndWith', () => { const trustedDomains: TrustedDomains = { - default: [ - 'https://demo.duendesoftware.com', - 'https://kdhttps.auth0.com', - ], + default: ['https://demo.duendesoftware.com', 'https://kdhttps.auth0.com'], }; const url = 'http://url' + openidWellknownUrlEndWith; expect(getCurrentDatabaseDomain(db, url, trustedDomains)).toBeNull(); }); + it('will return null when url is the token endpoint', () => { + const trustedDomains: TrustedDomains = { + default: ['https://demo.duendesoftware.com', 'https://kdhttps.auth0.com'], + }; + const url = 'https://demo.duendesoftware.com/connect/token'; + expect(getCurrentDatabaseDomain(db, url, trustedDomains)).toStrictEqual([]); + }); + + it('will return null when url is the token endpoint oidc config token endpoint has a default port', () => { + const trustedDomains: TrustedDomains = { + default: ['https://demo.duendesoftware.com', 'https://kdhttps.auth0.com'], + }; + db['default'].oidcServerConfiguration!.tokenEndpoint = + 'https://demo.duendesoftware.com:443/connect/token'; + + const url = 'https://demo.duendesoftware.com/connect/token'; + expect(getCurrentDatabaseDomain(db, url, trustedDomains)).toStrictEqual([]); + }); + + it('will return null when url is the revocation endpoint', () => { + const trustedDomains: TrustedDomains = { + default: ['https://demo.duendesoftware.com', 'https://kdhttps.auth0.com'], + }; + const url = 'https://demo.duendesoftware.com/connect/revocation'; + expect(getCurrentDatabaseDomain(db, url, trustedDomains)).toStrictEqual([]); + }); + + it('will not return null when url is the userinfo endpoint', () => { + const trustedDomains: TrustedDomains = { + default: ['https://demo.duendesoftware.com', 'https://kdhttps.auth0.com'], + }; + const url = 'https://demo.duendesoftware.com/connect/userinfo'; + expect(getCurrentDatabaseDomain(db, url, trustedDomains)).not.toBeNull(); + }); + + it('will not return null, url is the userinfo endpoint on other domain, default port is set', () => { + db['default'].oidcServerConfiguration!.userInfoEndpoint = + 'https://otherdomain.com:443/connect/userinfo'; + + const url = 'https://otherdomain.com/connect/userinfo'; + expect(getCurrentDatabaseDomain(db, url, null)).not.toBeNull(); + }); + it('will test urls against domains list if accessTokenDomains list is not present', () => { const trustedDomains: TrustedDomains = { default: { @@ -71,7 +121,9 @@ describe('domains', () => { }, }; - expect(getCurrentDatabaseDomain(db, 'https://domain/test', trustedDomains)).toBe(db.default); + expect(getCurrentDatabaseDomain(db, 'https://domain/test', trustedDomains)).toStrictEqual([ + db.default, + ]); }); it('will test urls against accessTokenDomains list if it is present and ignore domains list', () => { @@ -83,8 +135,10 @@ describe('domains', () => { }, }; - expect(getCurrentDatabaseDomain(db, 'https://myapi/test', trustedDomains)).toBe(db.default); - expect(getCurrentDatabaseDomain(db, 'https://domain/test', trustedDomains)).toBeNull(); + expect(getCurrentDatabaseDomain(db, 'https://myapi/test', trustedDomains)).toStrictEqual([ + db.default, + ]); + expect(getCurrentDatabaseDomain(db, 'https://domain/test', trustedDomains)).toStrictEqual([]); }); }); }); diff --git a/packages/oidc-client-service-worker/src/utils/__tests__/normalizeUrl.spec.ts b/packages/oidc-client-service-worker/src/utils/__tests__/normalizeUrl.spec.ts new file mode 100644 index 000000000..98c6826fe --- /dev/null +++ b/packages/oidc-client-service-worker/src/utils/__tests__/normalizeUrl.spec.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; + +import { normalizeUrl } from '../normalizeUrl'; + +describe('normalizeUrl', () => { + it('keeps urls the same', () => { + expect(normalizeUrl('http://foo.com/')).toBe('http://foo.com/'); + expect(normalizeUrl('https://foo.com/')).toBe('https://foo.com/'); + }); + it('adds slashes', () => { + expect(normalizeUrl('http://foo.com')).toBe('http://foo.com/'); + }); + it('removes port numbers', () => { + expect(normalizeUrl('http://foo.com:80/')).toBe('http://foo.com/'); + expect(normalizeUrl('https://foo.com:443/')).toBe('https://foo.com/'); + }); + it('removed port numbers and adds slashes', () => { + expect(normalizeUrl('http://foo.com:80')).toBe('http://foo.com/'); + expect(normalizeUrl('https://foo.com:443')).toBe('https://foo.com/'); + }); + it('lowercases urls', () => { + expect(normalizeUrl('http://FOO.com/')).toBe('http://foo.com/'); + }); + + it('keeps invalid urls', () => { + expect(normalizeUrl('foo')).toBe('foo'); + }); +}); diff --git a/packages/oidc-client-service-worker/src/utils/__tests__/serializeHeaders.spec.ts b/packages/oidc-client-service-worker/src/utils/__tests__/serializeHeaders.spec.ts index 945badb74..44d14c928 100644 --- a/packages/oidc-client-service-worker/src/utils/__tests__/serializeHeaders.spec.ts +++ b/packages/oidc-client-service-worker/src/utils/__tests__/serializeHeaders.spec.ts @@ -4,9 +4,43 @@ import { serializeHeaders } from '..'; describe('serializeHeaders', () => { it('can serialize basic header', () => { - const result = serializeHeaders( - new Headers({ 'Content-Type': 'application/json' }), - ); // Error: Argument of type 'Headers' is not assignable to parameter of type 'Headers'.(2345 + const result = serializeHeaders(new Headers({ 'Content-Type': 'application/json' })); expect(result).toEqual({ 'content-type': 'application/json' }); }); + + it('returns an empty object for empty headers', () => { + expect(serializeHeaders(new Headers())).toEqual({}); + }); + + it('normalizes names and preserves empty and combined header values', () => { + const headers = new Headers({ + 'X-Empty': '', + 'X-Custom': 'first', + 'Content-Type': 'text/plain', + }); + headers.append('x-custom', 'second'); + + expect(serializeHeaders(headers)).toEqual({ + 'content-type': 'text/plain', + 'x-custom': 'first, second', + 'x-empty': '', + }); + expect(headers.get('x-custom')).toBe('first, second'); + }); + + it('preserves the combined value of repeated Set-Cookie headers', () => { + const headers = new Headers(); + headers.append('Set-Cookie', 'first=1'); + headers.append('Set-Cookie', 'second=2'); + + expect(serializeHeaders(headers)).toEqual({ 'set-cookie': 'first=1, second=2' }); + }); + + it('returns a detached object that can be changed without mutating the headers', () => { + const headers = new Headers({ 'X-Custom': 'original' }); + const serialized = serializeHeaders(headers); + serialized['x-custom'] = 'changed'; + + expect(headers.get('X-Custom')).toBe('original'); + }); }); diff --git a/packages/oidc-client-service-worker/src/utils/__tests__/shouldBypassDestination.spec.ts b/packages/oidc-client-service-worker/src/utils/__tests__/shouldBypassDestination.spec.ts new file mode 100644 index 000000000..267bf2bad --- /dev/null +++ b/packages/oidc-client-service-worker/src/utils/__tests__/shouldBypassDestination.spec.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; + +import { shouldBypassDestination } from '../shouldBypassDestination'; + +describe('shouldBypassDestination', () => { + it.each(['image', 'font', 'media', 'document', 'iframe', 'script'])( + 'should bypass %s destination for non-navigate requests', + destination => { + expect(shouldBypassDestination(destination, 'cors')).toBe(true); + expect(shouldBypassDestination(destination, 'same-origin')).toBe(true); + expect(shouldBypassDestination(destination, 'no-cors')).toBe(true); + }, + ); + + it.each(['image', 'font', 'media', 'document', 'iframe', 'script'])( + 'should NOT bypass %s destination for navigate requests', + destination => { + expect(shouldBypassDestination(destination, 'navigate')).toBe(false); + }, + ); + + it('should not bypass non-listed destinations', () => { + expect(shouldBypassDestination('', 'cors')).toBe(false); + expect(shouldBypassDestination('worker', 'same-origin')).toBe(false); + expect(shouldBypassDestination('audio', 'no-cors')).toBe(false); + }); + + it('should not bypass empty destination with navigate mode', () => { + expect(shouldBypassDestination('', 'navigate')).toBe(false); + }); +}); diff --git a/packages/oidc-client-service-worker/src/utils/__tests__/testHelper.ts b/packages/oidc-client-service-worker/src/utils/__tests__/testHelper.ts index 37e11373b..84c5c3b3b 100644 --- a/packages/oidc-client-service-worker/src/utils/__tests__/testHelper.ts +++ b/packages/oidc-client-service-worker/src/utils/__tests__/testHelper.ts @@ -44,7 +44,7 @@ class TokenBuilder { return this; } - public WithNonExpiredToken(): TokenBuilder { + public withNonExpiredToken(): TokenBuilder { this.withExpiresAt(currentTimeUnixSeconds() + 60); this.withExpiresIn(currentTimeUnixSeconds() + 60); this.withIssuedAt(currentTimeUnixSeconds() - 60); @@ -56,12 +56,12 @@ class TokenBuilder { return this; } - public withIssuedAt(issued_at: number): TokenBuilder { + public withIssuedAt(issued_at: number | string): TokenBuilder { this.tokens.issued_at = issued_at; return this; } - public withExpiresIn(expires_in: number): TokenBuilder { + public withExpiresIn(expires_in: number | string): TokenBuilder { this.tokens.expires_in = expires_in; return this; } @@ -71,9 +71,7 @@ class TokenBuilder { return this; } - public withAccessTokenPayload( - accessTokenPayload: AccessTokenPayload, - ): TokenBuilder { + public withAccessTokenPayload(accessTokenPayload: AccessTokenPayload): TokenBuilder { this.tokens.accessTokenPayload = accessTokenPayload; return this; } @@ -96,24 +94,14 @@ class TokenBuilder { class OidcConfigurationBuilder { private oidcConfiguration: OidcConfiguration = { token_renew_mode: 'offline', - service_worker_convert_all_requests_to_cors: true, + demonstrating_proof_of_possession: false, }; - public withTokenRenewMode( - token_renew_mode: string, - ): OidcConfigurationBuilder { + public withTokenRenewMode(token_renew_mode: string): OidcConfigurationBuilder { this.oidcConfiguration.token_renew_mode = token_renew_mode; return this; } - public withServiceWorkerConvertAllRequestsToCors( - service_worker_convert_all_requests_to_cors: boolean, - ): OidcConfigurationBuilder { - this.oidcConfiguration.service_worker_convert_all_requests_to_cors = - service_worker_convert_all_requests_to_cors; - return this; - } - public build(): OidcConfiguration { return this.oidcConfiguration; } @@ -124,19 +112,27 @@ class OidcConfigBuilder { configurationName: '', tokens: null, status: 'NOT_CONNECTED', - state: '', - codeVerifier: '', + state: null, + codeVerifier: null, nonce: null, oidcServerConfiguration: null, oidcConfiguration: undefined, sessionState: null, items: undefined, hideAccessToken: true, + convertAllRequestsToCorsExceptNavigate: false, + setAccessTokenToNavigateRequests: true, + demonstratingProofOfPossessionNonce: null, + demonstratingProofOfPossessionJwkJson: null, + demonstratingProofOfPossessionConfiguration: null, + demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent: false, + allowMultiTabLogin: true, + bypassAllNonOidcRequests: false, }; public withTestingDefault(): OidcConfigBuilder { this.oidcConfig.configurationName = 'test'; - this.oidcConfig.tokens = new TokenBuilder().WithNonExpiredToken().build(); + this.oidcConfig.tokens = new TokenBuilder().withNonExpiredToken().build(); this.oidcConfig.status = 'NOT_CONNECTED'; this.oidcConfig.state = 'state'; this.oidcConfig.codeVerifier = 'codeVerifier'; @@ -193,6 +189,11 @@ class OidcConfigBuilder { return this; } + public withOidcConfiguration(oidcConfiguration: OidcConfiguration): OidcConfigBuilder { + this.oidcConfig.oidcConfiguration = oidcConfiguration; + return this; + } + public build() { return this.oidcConfig; } @@ -208,19 +209,15 @@ class OidcServerConfigBuilder { }; public withTestingDefault(): OidcServerConfigBuilder { - this.oidcServerConfig.revocationEndpoint = - 'http://localhost:3000/revocation'; + this.oidcServerConfig.revocationEndpoint = 'http://localhost:3000/revocation'; this.oidcServerConfig.issuer = 'http://localhost:3000'; - this.oidcServerConfig.authorizationEndpoint = - 'http://localhost:3000/authorization'; + this.oidcServerConfig.authorizationEndpoint = 'http://localhost:3000/authorization'; this.oidcServerConfig.tokenEndpoint = 'http://localhost:3000/token'; this.oidcServerConfig.userInfoEndpoint = 'http://localhost:3000/userinfo'; return this; } - public withRevocationEndpoint( - revocationEndpoint: string, - ): OidcServerConfigBuilder { + public withRevocationEndpoint(revocationEndpoint: string): OidcServerConfigBuilder { this.oidcServerConfig.revocationEndpoint = revocationEndpoint; return this; } @@ -230,9 +227,7 @@ class OidcServerConfigBuilder { return this; } - public withAuthorizationEndpoint( - authorizationEndpoint: string, - ): OidcServerConfigBuilder { + public withAuthorizationEndpoint(authorizationEndpoint: string): OidcServerConfigBuilder { this.oidcServerConfig.authorizationEndpoint = authorizationEndpoint; return this; } @@ -242,9 +237,7 @@ class OidcServerConfigBuilder { return this; } - public withUserInfoEndpoint( - userInfoEndpoint: string, - ): OidcServerConfigBuilder { + public withUserInfoEndpoint(userInfoEndpoint: string): OidcServerConfigBuilder { this.oidcServerConfig.userInfoEndpoint = userInfoEndpoint; return this; } @@ -323,7 +316,7 @@ class ResponseBuilder { throw new Error('Function not implemented.'); }, json: function (): Promise { - return new Promise((resolve) => { + return new Promise(resolve => { resolve(this.bodyContent); }); }, diff --git a/packages/oidc-client-service-worker/src/utils/__tests__/tokens.spec.ts b/packages/oidc-client-service-worker/src/utils/__tests__/tokens.spec.ts index e8b9d0de5..02fb887cd 100644 --- a/packages/oidc-client-service-worker/src/utils/__tests__/tokens.spec.ts +++ b/packages/oidc-client-service-worker/src/utils/__tests__/tokens.spec.ts @@ -1,27 +1,23 @@ -import { beforeEach, describe, expect, it } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { OidcServerConfiguration } from '../../types'; -import { _hideTokens, extractTokenPayload, isTokensOidcValid, isTokensValid } from '..'; +import { _hideTokens, extractTokenPayload, isTokensOidcValid, isTokensValid, parseJwt } from '..'; import { OidcConfigBuilder, OidcServerConfigBuilder, TokenBuilder } from './testHelper'; describe('tokens', () => { let oidcServerConfig: OidcServerConfiguration; beforeEach(() => { - oidcServerConfig = new OidcServerConfigBuilder() - .withTestingDefault() - .build(); + oidcServerConfig = new OidcServerConfigBuilder().withTestingDefault().build(); }); describe('isTokensValid', () => { it('can check expired token', () => { - expect( - isTokensValid(new TokenBuilder().withExpiredToken().build()), - ).toBeFalsy(); + expect(isTokensValid(new TokenBuilder().withExpiredToken().build())).toBeFalsy(); }); it('can check non-expired token', () => { - const token = new TokenBuilder().WithNonExpiredToken().build(); + const token = new TokenBuilder().withNonExpiredToken().build(); expect(isTokensValid(token)).toBeTruthy(); }); @@ -30,7 +26,74 @@ describe('tokens', () => { }); }); + describe.each([ + [ + 'eyJzZXNzaW9uX3N0YXRlIjoiNzVjYzVlZDItZGYyZC00NTY5LWJmYzUtMThhOThlNjhiZTExIiwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoixrTHosOBw6zDhyDlsI_lkI0t44Ob44Or44OYIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidGVzdGluZ2NoYXJhY3RlcnNAaW52ZW50ZWRtYWlsLmNvbSIsImdpdmVuX25hbWUiOiLGtMeiw4HDrMOHIiwiZmFtaWx5X25hbWUiOiLlsI_lkI0t44Ob44Or44OYIn0', + { + session_state: '75cc5ed2-df2d-4569-bfc5-18a98e68be11', + scope: 'openid email profile', + email_verified: true, + name: 'ƴǢÁìÇ 小名-ホルヘ', + preferred_username: 'testingcharacters@inventedmail.com', + given_name: 'ƴǢÁìÇ', + family_name: '小名-ホルヘ', + }, + ], + [ + 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCI_IjoiYWE_In0', + { + '?': 'aa?', + iat: 1516239022, + name: 'John Doe', + sub: '1234567890', + }, + ], + ])('parseJwtShouldExtractData', (claimsPart, expectedResult) => { + it('should parseJwtShouldExtractData ', async () => { + const result = parseJwt(claimsPart); + expect(expectedResult).toStrictEqual(result); + }); + }); + describe('extractTokenPayload', () => { + const payload = { sub: 'unit-test', name: 'Test user' }; + const encodedPayload = btoa(JSON.stringify(payload)); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it.each([`header.${encodedPayload}.signature`, `.${encodedPayload}.`])( + 'extracts the payload from a three-part token: %s', + token => { + expect(extractTokenPayload(token)).toEqual(payload); + }, + ); + + it.each([ + undefined, + '', + 'opaque-token', + `header.${encodedPayload}`, + `header.${encodedPayload}.signature.extra`, + `header.${encodedPayload}.signature.`, + ])('returns null for a token without exactly three parts: %s', token => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + expect(extractTokenPayload(token)).toBeNull(); + expect(warn).not.toHaveBeenCalled(); + }); + + it.each(['header..signature', 'header.!.signature', 'header.bm90IGpzb24=.signature'])( + 'warns and returns null when decoding fails: %s', + token => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + expect(extractTokenPayload(token)).toBeNull(); + expect(warn).toHaveBeenCalledExactlyOnceWith(expect.any(Error)); + }, + ); + it('can extract token payload', () => { const result = extractTokenPayload( 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', @@ -53,7 +116,7 @@ describe('tokens', () => { describe('isTokensOidcValid', () => { it('can validate valid token', () => { const token = new TokenBuilder() - .WithNonExpiredToken() + .withNonExpiredToken() .withIdTokenPayload({ iss: oidcServerConfig.issuer, exp: 0, @@ -69,22 +132,76 @@ describe('tokens', () => { describe('_hideTokens', () => { it.each([ - { hideAccessToken: true, expectedAccessToken: 'ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER_test' }, - { hideAccessToken: false, expectedAccessToken: 'test_access_token' }, - ])('accesstoken will be hide $hideAccessToken result should be $expectedAccessToken', ({ hideAccessToken, expectedAccessToken }) => { - const token = new TokenBuilder() + { + hideAccessToken: true, + expectedAccessToken: 'ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER_test', + issued_at: '0', + expires_in: '2', + }, + { + hideAccessToken: false, + expectedAccessToken: 'test_access_token', + issued_at: 0, + expires_in: 2, + }, + ])( + 'accesstoken will be hide $hideAccessToken result should be $expectedAccessToken', + ({ hideAccessToken, expectedAccessToken, issued_at, expires_in }) => { + const token = new TokenBuilder() .withIdTokenPayload({ iss: oidcServerConfig.issuer, exp: 0, iat: 0, nonce: null, }) - .WithNonExpiredToken() + .withNonExpiredToken() .withAccessToken('test_access_token') + .withExpiresIn(expires_in) + .withIssuedAt(issued_at) .build(); - const oidcConfiguration = new OidcConfigBuilder().withTestingDefault().withHideAccessToken(hideAccessToken).build(); - const secureTokens = _hideTokens(token, oidcConfiguration, 'test'); - expect(secureTokens.access_token).toBe(expectedAccessToken); + const oidcConfiguration = new OidcConfigBuilder() + .withTestingDefault() + .withHideAccessToken(hideAccessToken) + .build(); + const secureTokens = _hideTokens(token, oidcConfiguration, 'test'); + expect(secureTokens.access_token).toBe(expectedAccessToken); + expect(typeof secureTokens.expiresAt).toBe('number'); + }, + ); + + it('should reuse old id_token', () => { + const token = new TokenBuilder().withNonExpiredToken().build(); + // @ts-ignore + delete token.id_token; + // @ts-ignore + delete token.idTokenPayload; + const oidcConfiguration = new OidcConfigBuilder() + .withOidcConfiguration({ + token_renew_mode: 'access_token_invalid', + demonstrating_proof_of_possession: false, + }) + .withOidcServerConfiguration({ + issuer: '', + authorizationEndpoint: '', + revocationEndpoint: '', + tokenEndpoint: '', + userInfoEndpoint: '', + }) + .withTokens( + new TokenBuilder() + .withNonExpiredToken() + .withIdToken('old_id_token') + .withIdTokenPayload({ + iss: oidcServerConfig.issuer, + exp: 0, + iat: 0, + nonce: null, + }) + .build(), + ) + .build(); + _hideTokens(token, oidcConfiguration, 'test'); + expect(token.id_token).toBe('old_id_token'); }); }); }); diff --git a/packages/oidc-client-service-worker/src/utils/__tests__/waitForValidTokens.spec.ts b/packages/oidc-client-service-worker/src/utils/__tests__/waitForValidTokens.spec.ts new file mode 100644 index 000000000..f87ab8819 --- /dev/null +++ b/packages/oidc-client-service-worker/src/utils/__tests__/waitForValidTokens.spec.ts @@ -0,0 +1,88 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { waitForValidTokens } from '../waitForValidTokens'; +import { OidcConfigBuilder, TokenBuilder } from './testHelper'; + +describe('waitForValidTokens', () => { + beforeEach(() => { + // Fake all timers including Date so that Date.now() advances with the clock + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('returns null immediately when tokens are already valid', async () => { + const config = new OidcConfigBuilder() + .withTestingDefault() + .withTokens(new TokenBuilder().withNonExpiredToken().withAccessToken('valid-token').build()) + .build(); + + const result = await waitForValidTokens(config); + + expect(result).toBeNull(); + }); + + it('returns null when tokens become valid before the timeout', async () => { + const config = new OidcConfigBuilder() + .withTestingDefault() + .withTokens(new TokenBuilder().withExpiredToken().withAccessToken('expired-token').build()) + .build(); + + const resultPromise = waitForValidTokens(config, 5000, 200); + + // Let the first poll complete + await vi.advanceTimersByTimeAsync(200); + // Simulate a successful token renewal + config.tokens = new TokenBuilder() + .withNonExpiredToken() + .withAccessToken('renewed-token') + .build(); + await vi.advanceTimersByTimeAsync(200); + + const result = await resultPromise; + + expect(result).toBeNull(); + }); + + it('returns 401 with timeout statusText when tokens remain expired past maxWaitMs', async () => { + const config = new OidcConfigBuilder() + .withTestingDefault() + .withTokens(new TokenBuilder().withExpiredToken().withAccessToken('expired-token').build()) + .build(); + + const resultPromise = waitForValidTokens(config, 1000, 200); + + // Advance past maxWaitMs=1000ms so the elapsed-time check triggers the timeout response + await vi.advanceTimersByTimeAsync(1200); + + const result = await resultPromise; + + expect(result).not.toBeNull(); + expect(result?.status).toBe(401); + expect(result?.statusText).toBe('Token expired - service worker renewal timeout'); + }); + + it('returns 401 with missing-token statusText when tokens are cleared during the wait', async () => { + const config = new OidcConfigBuilder() + .withTestingDefault() + .withTokens(new TokenBuilder().withExpiredToken().withAccessToken('expired-token').build()) + .build(); + + const resultPromise = waitForValidTokens(config, 5000, 200); + + // Let the first poll complete while tokens are still expired + await vi.advanceTimersByTimeAsync(200); + // Simulate a parallel logout clearing the tokens + config.tokens = null; + // Let the second poll run so the loop can observe the cleared tokens + await vi.advanceTimersByTimeAsync(200); + + const result = await resultPromise; + + expect(result).not.toBeNull(); + expect(result?.status).toBe(401); + expect(result?.statusText).toBe('Missing access token'); + }); +}); diff --git a/packages/oidc-client-service-worker/src/utils/codeVerifier.ts b/packages/oidc-client-service-worker/src/utils/codeVerifier.ts index a011716db..8cecf4b81 100644 --- a/packages/oidc-client-service-worker/src/utils/codeVerifier.ts +++ b/packages/oidc-client-service-worker/src/utils/codeVerifier.ts @@ -1,4 +1,11 @@ -export function replaceCodeVerifier(codeVerifier:string, newCodeVerifier:string):string { - const regex = /code_verifier=[A-Za-z0-9_-]+/i; - return codeVerifier.replace(regex, `code_verifier=${newCodeVerifier}`); +export function replaceCodeVerifier(codeVerifier: string, newCodeVerifier: string): string { + const regex = /[?&]code_verifier=([^&]+)/i; + return codeVerifier.replace(regex, `&code_verifier=${newCodeVerifier}`); } + +export const extractConfigurationNameFromCodeVerifier = (body: string): string => { + const regex = /[?&]code_verifier=CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER_([^&]+)/; + const match = body.match(regex); + + return match ? decodeURIComponent(match[1]) : ''; +}; diff --git a/packages/oidc-client-service-worker/src/utils/domains.ts b/packages/oidc-client-service-worker/src/utils/domains.ts index 72c8bfa51..aac61e0b4 100644 --- a/packages/oidc-client-service-worker/src/utils/domains.ts +++ b/packages/oidc-client-service-worker/src/utils/domains.ts @@ -1,16 +1,13 @@ -import { - acceptAnyDomainToken, - openidWellknownUrlEndWith, - scriptFilename, -} from '../constants'; +import { acceptAnyDomainToken, openidWellknownUrlEndWith, scriptFilename } from '../constants'; import { Database, Domain, DomainDetails, OidcConfig, TrustedDomains } from '../types'; +import { normalizeUrl } from './normalizeUrl'; -function checkDomain(domains: Domain[], endpoint: string) { +export function checkDomain(domains: Domain[], endpoint: string) { if (!endpoint) { return; } - const domain = domains.find((domain) => { + const domain = domains.find(domain => { let testable: RegExp; if (typeof domain === 'string') { @@ -23,15 +20,15 @@ function checkDomain(domains: Domain[], endpoint: string) { }); if (!domain) { throw new Error( - 'Domain ' + - endpoint + - ' is not trusted, please add domain in ' + - scriptFilename, + 'Domain ' + endpoint + ' is not trusted, please add domain in ' + scriptFilename, ); } } -export const getDomains = (trustedDomain: Domain[] | DomainDetails, type: 'oidc' | 'accessToken') => { +export const getDomains = ( + trustedDomain: Domain[] | DomainDetails, + type: 'oidc' | 'accessToken', +) => { if (Array.isArray(trustedDomain)) { return trustedDomain; } @@ -39,7 +36,7 @@ export const getDomains = (trustedDomain: Domain[] | DomainDetails, type: 'oidc' return trustedDomain[`${type}Domains`] ?? trustedDomain.domains ?? []; }; -const getCurrentDatabaseDomain = ( +export const getCurrentDatabaseDomain = ( database: Database, url: string, trustedDomains: TrustedDomains, @@ -47,6 +44,7 @@ const getCurrentDatabaseDomain = ( if (url.endsWith(openidWellknownUrlEndWith)) { return null; } + const datatases = []; for (const [key, currentDatabase] of Object.entries(database)) { const oidcServerConfiguration = currentDatabase.oidcServerConfiguration; @@ -56,25 +54,25 @@ const getCurrentDatabaseDomain = ( if ( oidcServerConfiguration.tokenEndpoint && - url === oidcServerConfiguration.tokenEndpoint + url === normalizeUrl(oidcServerConfiguration.tokenEndpoint) ) { continue; } if ( oidcServerConfiguration.revocationEndpoint && - url === oidcServerConfiguration.revocationEndpoint + url === normalizeUrl(oidcServerConfiguration.revocationEndpoint) ) { continue; } - const trustedDomain = trustedDomains == null ? [] : trustedDomains[key]; + const trustedDomain = trustedDomains == null ? [] : trustedDomains[key.split('#')[0]]; const domains = getDomains(trustedDomain, 'accessToken'); const domainsToSendTokens = oidcServerConfiguration.userInfoEndpoint - ? [oidcServerConfiguration.userInfoEndpoint, ...domains] + ? [normalizeUrl(oidcServerConfiguration.userInfoEndpoint), ...domains] : [...domains]; let hasToSendToken = false; - if (domainsToSendTokens.find((f) => f === acceptAnyDomainToken)) { + if (domainsToSendTokens.find(f => f === acceptAnyDomainToken)) { hasToSendToken = true; } else { for (let i = 0; i < domainsToSendTokens.length; i++) { @@ -92,13 +90,10 @@ const getCurrentDatabaseDomain = ( } if (hasToSendToken) { - if (!currentDatabase.tokens) { - return null; + if (currentDatabase.tokens) { + datatases.push(currentDatabase); } - return currentDatabase; } } - return null; + return datatases; }; - -export { checkDomain, getCurrentDatabaseDomain }; diff --git a/packages/oidc-client-service-worker/src/utils/index.ts b/packages/oidc-client-service-worker/src/utils/index.ts index 724a65bc7..a70369373 100644 --- a/packages/oidc-client-service-worker/src/utils/index.ts +++ b/packages/oidc-client-service-worker/src/utils/index.ts @@ -1,5 +1,8 @@ export * from './domains'; +export * from './normalizeUrl'; export * from './serializeHeaders'; +export * from './shouldBypassDestination'; export * from './sleep'; export * from './strings'; export * from './tokens'; +export * from './waitForValidTokens'; diff --git a/packages/oidc-client-service-worker/src/utils/normalizeUrl.ts b/packages/oidc-client-service-worker/src/utils/normalizeUrl.ts new file mode 100644 index 000000000..caf2e5cfe --- /dev/null +++ b/packages/oidc-client-service-worker/src/utils/normalizeUrl.ts @@ -0,0 +1,8 @@ +export function normalizeUrl(url: string) { + try { + return new URL(url).toString(); + } catch (error) { + console.error(`Failed to normalize url: ${url}`, error); + return url; + } +} diff --git a/packages/oidc-client-service-worker/src/utils/serializeHeaders.ts b/packages/oidc-client-service-worker/src/utils/serializeHeaders.ts index d9d9bc3d4..894514743 100644 --- a/packages/oidc-client-service-worker/src/utils/serializeHeaders.ts +++ b/packages/oidc-client-service-worker/src/utils/serializeHeaders.ts @@ -1,10 +1,9 @@ -import { FetchHeaders } from '../types'; - -function serializeHeaders(headers: Headers) { +function serializeHeaders(headers: Headers): Record { const headersObj: Record = {}; - for (const key of (headers as FetchHeaders).keys()) { - if (headers.has(key)) { - headersObj[key] = headers.get(key) as string; + for (const key of headers.keys()) { + const value = headers.get(key); + if (value !== null) { + headersObj[key] = value; } } return headersObj; diff --git a/packages/oidc-client-service-worker/src/utils/shouldBypassDestination.ts b/packages/oidc-client-service-worker/src/utils/shouldBypassDestination.ts new file mode 100644 index 000000000..84007c140 --- /dev/null +++ b/packages/oidc-client-service-worker/src/utils/shouldBypassDestination.ts @@ -0,0 +1,10 @@ +const bypassedDestinations = ['image', 'font', 'media', 'document', 'iframe', 'script']; + +/** + * Determines whether a fetch event should be bypassed (not intercepted by the service worker). + * Navigation requests (mode === 'navigate') are never bypassed, even when their destination + * is in the bypassed list, so that access tokens can be injected into new-tab navigations. + */ +export const shouldBypassDestination = (destination: string, mode: string): boolean => { + return bypassedDestinations.includes(destination) && mode !== 'navigate'; +}; diff --git a/packages/oidc-client-service-worker/src/utils/sleep.ts b/packages/oidc-client-service-worker/src/utils/sleep.ts index 195f8233b..9b8f42807 100644 --- a/packages/oidc-client-service-worker/src/utils/sleep.ts +++ b/packages/oidc-client-service-worker/src/utils/sleep.ts @@ -1,2 +1,2 @@ -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); export { sleep }; diff --git a/packages/oidc-client-service-worker/src/utils/tokens.ts b/packages/oidc-client-service-worker/src/utils/tokens.ts index 75e5cb95f..29802458d 100644 --- a/packages/oidc-client-service-worker/src/utils/tokens.ts +++ b/packages/oidc-client-service-worker/src/utils/tokens.ts @@ -1,34 +1,28 @@ /* eslint-disable simple-import-sort/exports */ import { TOKEN, TokenRenewMode } from '../constants'; -import { OidcConfig, OidcConfiguration, OidcServerConfiguration, Tokens } from '../types'; -import { countLetter } from './strings'; - -function parseJwt(token: string) { - return JSON.parse( - b64DecodeUnicode(token.split('.')[1].replace('-', '+').replace('_', '/')), - ); -} +import { + AccessTokenPayload, + IdTokenPayload, + OidcConfig, + OidcConfiguration, + OidcServerConfiguration, + Tokens, +} from '../types'; + +export const parseJwt = (payload: string) => { + return JSON.parse(b64DecodeUnicode(payload.replaceAll(/-/g, '+').replaceAll(/_/g, '/'))); +}; function b64DecodeUnicode(str: string) { return decodeURIComponent( Array.prototype.map - .call( - atob(str), - (c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2), - ) + .call(atob(str), c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) .join(''), ); } -function computeTimeLeft( - refreshTimeBeforeTokensExpirationInSecond: number, - expiresAt: number, -) { +function computeTimeLeft(refreshTimeBeforeTokensExpirationInSecond: number, expiresAt: number) { const currentTimeUnixSecond = new Date().getTime() / 1000; - return Math.round( - expiresAt - - refreshTimeBeforeTokensExpirationInSecond - - currentTimeUnixSecond, - ); + return Math.round(expiresAt - refreshTimeBeforeTokensExpirationInSecond - currentTimeUnixSecond); } function isTokensValid(tokens: Tokens | null) { @@ -43,11 +37,8 @@ const extractTokenPayload = (token?: string) => { if (!token) { return null; } - if (countLetter(token, '.') === 2) { - return parseJwt(token); - } else { - return null; - } + const parts = token.split('.'); + return parts.length === 3 ? parseJwt(parts[1]) : null; } catch (e) { console.warn(e); } @@ -64,8 +55,11 @@ const isTokensOidcValid = ( if (tokens.idTokenPayload) { const idTokenPayload = tokens.idTokenPayload; // 2: The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) MUST exactly match the value of the iss (issuer) Claim. - if (oidcServerConfiguration.issuer !== idTokenPayload.iss) { - return { isValid: false, reason: 'Issuer does not match' }; + if (idTokenPayload && oidcServerConfiguration.issuer !== idTokenPayload.iss) { + return { + isValid: false, + reason: `Issuer does not match (oidcServerConfiguration issuer) ${oidcServerConfiguration.issuer} !== (idTokenPayload issuer) ${idTokenPayload.iss}`, + }; } // 3: The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified by the iss (issuer) Claim as an audience. The aud (audience) Claim MAY contain an array with more than one element. The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences not trusted by the Client. @@ -73,29 +67,65 @@ const isTokensOidcValid = ( // 9: The current time MUST be before the time represented by the exp Claim. const currentTimeUnixSecond = new Date().getTime() / 1000; - if (idTokenPayload.exp && idTokenPayload.exp < currentTimeUnixSecond) { - return { isValid: false, reason: 'Token expired' }; + if (idTokenPayload && idTokenPayload.exp && idTokenPayload.exp < currentTimeUnixSecond) { + return { + isValid: false, + reason: `Token expired at (idTokenPayload exp) ${idTokenPayload.exp} < (currentTimeUnixSecond) ${currentTimeUnixSecond}`, + }; } // 10: The iat Claim can be used to reject tokens that were issued too far away from the current time, limiting the amount of time that nonces need to be stored to prevent attacks. The acceptable range is Client specific. const timeInSevenDays = 60 * 60 * 24 * 7; if ( + idTokenPayload && idTokenPayload.iat && idTokenPayload.iat + timeInSevenDays < currentTimeUnixSecond ) { - return { isValid: false, reason: 'Token is used from too long time' }; + return { + isValid: false, + reason: `Token is used from too long time (idTokenPayload iat + timeInSevenDays) ${idTokenPayload.iat + timeInSevenDays} < (currentTimeUnixSecond) ${currentTimeUnixSecond}`, + }; } // 11: If a nonce value was sent in the Authentication Request, a nonce Claim MUST be present and its value checked to verify that it is the same value as the one that was sent in the Authentication Request. The Client SHOULD check the nonce value for replay attacks. The precise method for detecting replay attacks is Client specific. - if (nonce && idTokenPayload.nonce && idTokenPayload.nonce !== nonce) { - return { isValid: false, reason: 'Nonce does not match' }; + if (idTokenPayload && nonce && idTokenPayload.nonce && idTokenPayload.nonce !== nonce) { + return { + isValid: false, + reason: `Nonce does not match (nonce) ${nonce} !== (idTokenPayload nonce) ${idTokenPayload.nonce}`, + }; } } return { isValid: true, reason: '' }; }; -function _hideTokens(tokens: Tokens, currentDatabaseElement: OidcConfig, configurationName: string) { +function extractedIssueAt( + tokens: Tokens, + accessTokenPayload: AccessTokenPayload | null, + _idTokenPayload: IdTokenPayload, +) { + if (!tokens.issued_at) { + if (accessTokenPayload && accessTokenPayload.iat) { + return accessTokenPayload.iat; + } else if (_idTokenPayload && _idTokenPayload.iat) { + return _idTokenPayload.iat; + } else { + const currentTimeUnixSecond = new Date().getTime() / 1000; + return currentTimeUnixSecond; + } + } else if (typeof tokens.issued_at == 'string') { + return parseInt(tokens.issued_at, 10); + } + return tokens.issued_at; +} + +function _hideTokens( + tokens: Tokens, + currentDatabaseElement: OidcConfig, + configurationName: string, +) { if (!tokens.issued_at) { const currentTimeUnixSecond = new Date().getTime() / 1000; tokens.issued_at = currentTimeUnixSecond; + } else if (typeof tokens.issued_at == 'string') { + tokens.issued_at = parseInt(tokens.issued_at, 10); } const accessTokenPayload = extractTokenPayload(tokens.access_token); @@ -104,71 +134,72 @@ function _hideTokens(tokens: Tokens, currentDatabaseElement: OidcConfig, configu accessTokenPayload, }; if (currentDatabaseElement.hideAccessToken) { - secureTokens.access_token = TOKEN.ACCESS_TOKEN + '_' + configurationName; + secureTokens.access_token = `${TOKEN.ACCESS_TOKEN}_${configurationName}`; } tokens.accessTokenPayload = accessTokenPayload; + // When id_token is not rotated we reuse old id_token + const oldTokens = currentDatabaseElement.tokens; + let id_token: string | null; + if (oldTokens != null && 'id_token' in oldTokens && !('id_token' in tokens)) { + id_token = oldTokens.id_token; + } else { + id_token = tokens.id_token; + } + tokens.id_token = id_token; + let _idTokenPayload = null; - if (tokens.id_token) { - _idTokenPayload = extractTokenPayload(tokens.id_token); - tokens.idTokenPayload = { ..._idTokenPayload }; - if (_idTokenPayload.nonce && currentDatabaseElement.nonce != null) { - const keyNonce = - TOKEN.NONCE_TOKEN + '_' + currentDatabaseElement.configurationName; + if (id_token) { + _idTokenPayload = extractTokenPayload(id_token); + tokens.idTokenPayload = _idTokenPayload != null ? { ..._idTokenPayload } : null; + if (_idTokenPayload && _idTokenPayload.nonce && currentDatabaseElement.nonce != null) { + const keyNonce = `${TOKEN.NONCE_TOKEN}_${currentDatabaseElement.configurationName}`; _idTokenPayload.nonce = keyNonce; } secureTokens.idTokenPayload = _idTokenPayload; } if (tokens.refresh_token) { - secureTokens.refresh_token = - TOKEN.REFRESH_TOKEN + '_' + configurationName; + secureTokens.refresh_token = `${TOKEN.REFRESH_TOKEN}_${configurationName}`; } + tokens.issued_at = extractedIssueAt(tokens, accessTokenPayload, _idTokenPayload); + + const expireIn = + typeof tokens.expires_in == 'string' ? parseInt(tokens.expires_in, 10) : tokens.expires_in; + const idTokenExpiresAt = - _idTokenPayload && _idTokenPayload.exp - ? _idTokenPayload.exp - : Number.MAX_VALUE; + _idTokenPayload && _idTokenPayload.exp ? _idTokenPayload.exp : Number.MAX_VALUE; const accessTokenExpiresAt = - accessTokenPayload && accessTokenPayload.exp - ? accessTokenPayload.exp - : tokens.issued_at + tokens.expires_in; + accessTokenPayload && accessTokenPayload.exp + ? accessTokenPayload.exp + : tokens.issued_at + expireIn; let expiresAt: number; - const tokenRenewMode = ( - currentDatabaseElement.oidcConfiguration as OidcConfiguration - ).token_renew_mode; + const tokenRenewMode = (currentDatabaseElement.oidcConfiguration as OidcConfiguration) + .token_renew_mode; if (tokenRenewMode === TokenRenewMode.access_token_invalid) { expiresAt = accessTokenExpiresAt; } else if (tokenRenewMode === TokenRenewMode.id_token_invalid) { expiresAt = idTokenExpiresAt; } else { - expiresAt = - idTokenExpiresAt < accessTokenExpiresAt - ? idTokenExpiresAt - : accessTokenExpiresAt; + expiresAt = idTokenExpiresAt < accessTokenExpiresAt ? idTokenExpiresAt : accessTokenExpiresAt; } secureTokens.expiresAt = expiresAt; tokens.expiresAt = expiresAt; - const nonce = currentDatabaseElement.nonce - ? currentDatabaseElement.nonce.nonce - : null; + const nonce = currentDatabaseElement.nonce ? currentDatabaseElement.nonce.nonce : null; const { isValid, reason } = isTokensOidcValid( - tokens, - nonce, - currentDatabaseElement.oidcServerConfiguration as OidcServerConfiguration, + tokens, + nonce as string, + currentDatabaseElement.oidcServerConfiguration as OidcServerConfiguration, ); // TODO: Type assertion, could be null. if (!isValid) { throw Error(`Tokens are not OpenID valid, reason: ${reason}`); } - // When refresh_token is not rotated we reuse ald refresh_token - if ( - currentDatabaseElement.tokens != null && - 'refresh_token' in currentDatabaseElement.tokens && - !('refresh_token' in tokens) - ) { - const refreshToken = currentDatabaseElement.tokens.refresh_token; + // When refresh_token is not rotated we reuse old refresh_token + if (oldTokens != null && 'refresh_token' in oldTokens && !('refresh_token' in tokens)) { + const refreshToken = oldTokens.refresh_token; currentDatabaseElement.tokens = { ...tokens, @@ -182,16 +213,29 @@ function _hideTokens(tokens: Tokens, currentDatabaseElement: OidcConfig, configu return secureTokens; } +const demonstratingProofOfPossessionNonceResponseHeader = 'DPoP-Nonce'; function hideTokens(currentDatabaseElement: OidcConfig) { const configurationName = currentDatabaseElement.configurationName; return (response: Response) => { if (response.status !== 200) { return response; } + const newHeaders = new Headers(response.headers); + if (response.headers.has(demonstratingProofOfPossessionNonceResponseHeader)) { + currentDatabaseElement.demonstratingProofOfPossessionNonce = response.headers.get( + demonstratingProofOfPossessionNonceResponseHeader, + ); + newHeaders.delete(demonstratingProofOfPossessionNonceResponseHeader); + } + return response.json().then((tokens: Tokens) => { const secureTokens = _hideTokens(tokens, currentDatabaseElement, configurationName); const body = JSON.stringify(secureTokens); - return new Response(body, response); + return new Response(body, { + status: response.status, + statusText: response.statusText, + headers: newHeaders, + }); }); }; } diff --git a/packages/oidc-client-service-worker/src/utils/waitForValidTokens.ts b/packages/oidc-client-service-worker/src/utils/waitForValidTokens.ts new file mode 100644 index 000000000..1408dff3e --- /dev/null +++ b/packages/oidc-client-service-worker/src/utils/waitForValidTokens.ts @@ -0,0 +1,38 @@ +import { OidcConfig } from '../types'; +import { sleep } from './sleep'; +import { isTokensValid } from './tokens'; + +export const TOKEN_RENEWAL_TIMEOUT_MS = 5000; +export const TOKEN_RENEWAL_POLL_INTERVAL_MS = 200; + +/** + * Polls until the tokens in the given config are valid, or until a timeout elapses. + * + * @returns `null` when tokens become valid; a synthetic 401 `Response` on timeout or + * when the tokens are cleared while waiting (e.g. due to a parallel logout). + */ +export async function waitForValidTokens( + config: OidcConfig, + maxWaitMs = TOKEN_RENEWAL_TIMEOUT_MS, + pollIntervalMs = TOKEN_RENEWAL_POLL_INTERVAL_MS, +): Promise { + const startTime = Date.now(); + while (config.tokens && !isTokensValid(config.tokens)) { + if (Date.now() - startTime >= maxWaitMs) { + return new Response(null, { + status: 401, + statusText: 'Token expired - service worker renewal timeout', + }); + } + await sleep(pollIntervalMs); + } + + if (!config.tokens?.access_token) { + return new Response(null, { + status: 401, + statusText: 'Missing access token', + }); + } + + return null; +} diff --git a/packages/oidc-client-service-worker/src/version.ts b/packages/oidc-client-service-worker/src/version.ts new file mode 100644 index 000000000..bcc50f8bf --- /dev/null +++ b/packages/oidc-client-service-worker/src/version.ts @@ -0,0 +1 @@ +export default '7.29.6'; diff --git a/packages/oidc-client-service-worker/tsconfig.eslint.json b/packages/oidc-client-service-worker/tsconfig.eslint.json index e9041fd6b..b90fc83e0 100644 --- a/packages/oidc-client-service-worker/tsconfig.eslint.json +++ b/packages/oidc-client-service-worker/tsconfig.eslint.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "include": ["src"] -} \ No newline at end of file +} diff --git a/packages/oidc-client-service-worker/tsconfig.json b/packages/oidc-client-service-worker/tsconfig.json index cde526795..8e1609583 100644 --- a/packages/oidc-client-service-worker/tsconfig.json +++ b/packages/oidc-client-service-worker/tsconfig.json @@ -6,7 +6,7 @@ "composite": true, "module": "ESNext", "target": "ESNext", - "moduleResolution": "Node", + "moduleResolution": "bundler", "sourceMap": true, "declarationMap": true, "outDir": "../dist", diff --git a/packages/oidc-client-service-worker/vite.config.js b/packages/oidc-client-service-worker/vite.config.js index 13467b083..1c4c7854d 100644 --- a/packages/oidc-client-service-worker/vite.config.js +++ b/packages/oidc-client-service-worker/vite.config.js @@ -3,24 +3,24 @@ import { defineConfig } from 'vite'; import dts from 'vite-plugin-dts'; export default defineConfig({ - plugins: [dts({ - insertTypesEntry: true, - })], - test: { - coverage: { - provider: 'c8' - } - }, + plugins: [ + dts({ + insertTypesEntry: true, + }), + ], build: { minify: false, //default esbuild sourcemap: true, lib: { // Could also be a dictionary or array of multiple entry points - entry: resolve(__dirname, './src/OidcServiceWorker.ts'), + entry: { + OidcServiceWorker: resolve(__dirname, './src/OidcServiceWorker.ts'), + protocol: resolve(__dirname, './src/protocol.ts'), + }, name: 'OidcServiceWorker', formats: ['es'], // the proper extensions will be added - fileName: 'OidcServiceWorker', + fileName: format => `[name].${format === 'es' ? 'js' : format}`, }, rollupOptions: { // make sure to externalize deps that shouldn't be bundled @@ -29,8 +29,7 @@ export default defineConfig({ output: { // Provide global variables to use in the UMD build // for externalized deps - globals: { - }, + globals: {}, }, }, }, diff --git a/packages/oidc-client/.eslintrc.cjs b/packages/oidc-client/.eslintrc.cjs deleted file mode 100644 index 6f9d07624..000000000 --- a/packages/oidc-client/.eslintrc.cjs +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = { - extends: [__dirname+'/config/defaultEslintConfig.cjs'], - parserOptions: { - tsconfigRootDir: __dirname, - }, - rules: { - '@typescript-eslint/naming-convention': [ - 'error', - { - 'selector': 'variable', - 'types': ['boolean'], - 'format': ['PascalCase'], - 'prefix': ['is', 'with', 'should', 'has', 'can', 'did', 'will'] - } - ] - } - } \ No newline at end of file diff --git a/packages/oidc-client/README.md b/packages/oidc-client/README.md index e0144279d..163bbb435 100644 --- a/packages/oidc-client/README.md +++ b/packages/oidc-client/README.md @@ -1,209 +1,396 @@ -# @axa-fr/vanilla-oidc +# @axa-fr/oidc-client -[![Continuous Integration](https://github.com/AxaGuilDEv/react-oidc/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/AxaGuilDEv/react-oidc/actions/workflows/npm-publish.yml) -[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=AxaGuilDEv_react-oidc&metric=alert_status)](https://sonarcloud.io/dashboard?id=AxaGuilDEv_react-oidc) [![Reliability](https://sonarcloud.io/api/project_badges/measure?project=AxaGuilDEv_react-oidc&metric=reliability_rating)](https://sonarcloud.io/component_measures?id=AxaGuilDEv_react-oidc&metric=reliability_rating) [![Security](https://sonarcloud.io/api/project_badges/measure?project=AxaGuilDEv_react-oidc&metric=security_rating)](https://sonarcloud.io/component_measures?id=AxaGuilDEv_react-oidc&metric=security_rating) [![Code Corevage](https://sonarcloud.io/api/project_badges/measure?project=AxaGuilDEv_react-oidc&metric=coverage)](https://sonarcloud.io/component_measures?id=AxaGuilDEv_react-oidc&metric=Coverage) [![Twitter](https://img.shields.io/twitter/follow/GuildDEvOpen?style=social)](https://twitter.com/intent/follow?screen_name=GuildDEvOpen) +A framework-independent OpenID Connect (OIDC) client for browser applications, written in TypeScript. It supports Authorization Code Flow with PKCE, session restoration, token renewal, multiple named configurations, optional service-worker token isolation, Pushed Authorization Requests (PAR), and DPoP. -Try the demo at https://icy-glacier-004ab4303.2.azurestaticapps.net/ +For React applications, use [`@axa-fr/react-oidc`](../react-oidc/README.md). +- [Quick start](#quick-start) +- [Service worker](#service-worker) +- [Configuration](#configuration) +- [Token renewal](#token-renewal) +- [Pushed Authorization Requests (PAR)](#pushed-authorization-requests-par) +- [DPoP](#dpop) +- [API](#api) +- [Errors and events](#errors-and-events) +- [Named configurations and routing](#named-configurations-and-routing) +- [Service-worker protocol](#service-worker-protocol) +- [Examples and further reading](#examples-and-further-reading) -- [About](#about) -- [Getting Started](#getting-started) -- [Run The Demo](#run-the-demo) -- [How It Works](#how-it-works) -- [Hash route](#Hash-route) -- [Service Worker Support](#service-worker-support) + +## Quick start -## About +```sh +npm install @axa-fr/oidc-client +``` -@axa-fr/vanilla-oidc is a pure OIDC client library agnostic to any framework. It is used by @axa-fr/react-oidc and can be used by any framework. +Register a **public browser client** with your identity provider: -It is a real alternative to existing oidc-client libraries. +1. Enable Authorization Code Flow with PKCE. +2. Register your application's exact callback URL, such as `https://app.example.com/authentication/callback`, and its post-logout URL. +3. Allow your application's origin to call the provider's browser-accessible endpoints through CORS. +4. Choose the scopes your application needs. Request `offline_access` only if your provider uses it to issue refresh tokens. -- **Secure** : - - With the use of Service Worker, your tokens (refresh_token and access_token) are not accessible to the JavaScript client code (big protection against XSRF attacks) - - OIDC using client side Code Credential Grant with PKCE only -- **Lightweight** -- **Simple** : - - refresh_token and access_token are auto refreshed in background - - with the use of the Service Worker, you do not need to inject the access_token in every fetch, you have only to configure `OidcTrustedDomains.js` file -- **No cookies problem** : You can disable silent signin (that internally use an iframe). For your information, your OIDC server should be in the same domain of your website in order to be able to send OIDC server cookies from your website via an internal IFRAME, else, you may encounter COOKIES problem. -- **Multiple Authentication** : - - You can authenticate many times to the same provider with different scope (for example you can acquire a new 'payment' scope for a payment) - - You can authenticate to multiple different providers inside the same SPA (single page application) website -- **Flexible** : - - Work with Service Worker (more secure) and without for older browser (less secure) +Never put a client secret in browser code. Replace the example issuer and client ID below with your own values. Configure your web server to serve the application on the callback route as well as `/`. -![](https://github.com/AxaGuilDEv/react-oidc/blob/master/docs/img/schema_pcke_client_side_with_service_worker.png?raw=true) +Add these elements to your page: -The service worker catch **access_token** and **refresh_token** that will never be accessible to the client. +```html +

Loading session…

+ + +``` +Run this module when those elements are available, including on the callback route: -### Getting Started +```javascript +import { OidcClient } from '@axa-fr/oidc-client'; -```sh -npm install @axa-fr/vanilla-oidc --save +const configuration = { + client_id: 'your-public-client', + authority: 'https://issuer.example.com', + redirect_uri: `${window.location.origin}/authentication/callback`, + scope: 'openid profile', +}; -# If you have a "public" folder, the 2 files will be created : -# ./public/OidcServiceWorker.js <-- will be updated at each "npm install" -# ./public/OidcTrustedDomains.js <-- won't be updated if already exist +const oidcClient = OidcClient.getOrCreate(() => fetch)(configuration); +const status = document.getElementById('session-status'); +const loginButton = document.getElementById('login'); +const logoutButton = document.getElementById('logout'); + +function showError() { + status.textContent = 'Authentication could not be completed. Please try again.'; +} + +loginButton.addEventListener('click', () => { + oidcClient.loginAsync('/').catch(showError); +}); +logoutButton.addEventListener('click', () => { + oidcClient.logoutAsync('/').catch(showError); +}); + +async function start() { + if (window.location.pathname === new URL(configuration.redirect_uri).pathname) { + const { callbackPath } = await oidcClient.loginCallbackAsync(); + window.history.replaceState(null, '', callbackPath || '/'); + } else { + await oidcClient.tryKeepExistingSessionAsync(); + } + + const isAuthenticated = oidcClient.tokens != null; + status.textContent = isAuthenticated ? 'Signed in' : 'Not signed in'; + loginButton.hidden = isAuthenticated; + logoutButton.hidden = !isAuthenticated; +} + +start().catch(showError); ``` -If you need a very secure mode where refresh_token and access_token will be hide behind a service worker that will proxify requests. -The only file you should edit is "OidcTrustedDomains.js". +This minimal example uses browser storage, not a service worker. Read the next section before choosing a token-storage strategy for production. + +### Call a protected API + +Use the client's fetch wrapper after authentication: ```javascript -// OidcTrustedDomains.js +const oidcFetch = oidcClient.fetchWithTokens(fetch); +const response = await oidcFetch('https://api.example.com/profile'); -// Add bellow trusted domains, access tokens will automatically injected to be send to -// trusted domain can also be a path like https://www.myapi.com/users, -// then all subroute like https://www.myapi.com/useers/1 will be authorized to send access_token to. +if (!response.ok) { + throw new Error(`API request failed with status ${response.status}`); +} +const profile = await response.json(); +``` -// Domains used by OIDC server must be also declared here -const trustedDomains = { - default: ["https://demo.duendesoftware.com", "https://www.myapi.com/users"], -}; +The wrapper attaches an access token, or a token placeholder when the service worker hides it, and integrates with token renewal. Only use it for API URLs you trust: the wrapper itself is not a destination allowlist. HTTP error responses still need an explicit `response.ok` check. + + + +## Service worker + +The optional service worker can keep access and refresh tokens outside the application's JavaScript context and attach access tokens to configured API requests. It does **not** prevent XSS: injected code can still act through your application and make requests. Continue to use normal XSS defenses, carefully restrict trusted destinations, and avoid rendering or logging raw tokens. + +```mermaid +sequenceDiagram + participant App as Browser application + participant IdP as Identity provider + participant SW as OIDC service worker + participant API as Trusted API + App->>IdP: Authorization request with PKCE + IdP-->>App: Redirect with authorization code + App->>SW: Token request + SW->>IdP: Exchange code and verifier + IdP-->>SW: Tokens + SW-->>App: Token placeholders and session information + App->>SW: Protected API request + SW->>API: Request with access token + API-->>App: Response through service worker ``` -The code of the demo : +The diagram shows worker mode with access-token hiding enabled. The ID token and decoded claims may still be available to application code. + +### Install and update the worker + +```sh +node ./node_modules/@axa-fr/oidc-client/bin/copy-service-worker-files.mjs public +``` -```js -import { VanillaOidc } from '@axa-fr/vanilla-oidc' +Replace `public` with your application's static-assets directory, and ensure that directory exists before running the command. This creates or updates `OidcServiceWorker.js` and creates `OidcTrustedDomains.js` if it does not already exist. Keep the worker version aligned with the client package; for example, merge this script into your `package.json`: -export const configuration = { - client_id: 'interactive.public.short', - redirect_uri: window.location.origin + '/#/authentication/callback', - silent_redirect_uri: window.location.origin + '/#/authentication/silent-callback', - scope: 'openid profile email api offline_access', - authority: 'https://demo.duendesoftware.com', - service_worker_relative_url:'/OidcServiceWorker.js', - service_worker_only: false, +```json +{ + "scripts": { + "postinstall": "node ./node_modules/@axa-fr/oidc-client/bin/copy-service-worker-files.mjs public" + } +} +``` + +Edit `public/OidcTrustedDomains.js`, not the generated worker: + +```javascript +const trustedDomains = { + default: { + oidcDomains: [/^https:\/\/issuer\.example\.com(?:\/|$)/], + accessTokenDomains: [/^https:\/\/api\.example\.com\//], + }, }; +``` -const href = window.location.href; -const vanillaOidc = VanillaOidc.getOrCreate(() => fetch)(configuration); - -console.log(href); - -vanillaOidc.tryKeepExistingSessionAsync().then(() => { - if(href.includes(configuration.redirect_uri)){ - vanillaOidc.loginCallbackAsync().then(()=>{ - window.location.href = "/"; - }); - document.body.innerHTML = `
-

@axa-fr/vanilla-oidc demo

-

Loading

-
`; - return - } - - let tokens = vanillaOidc.tokens; - - if(tokens){ - - // @ts-ignore - window.logout = () => vanillaOidc.logoutAsync(); - document.body.innerHTML = `
-

@axa-fr/vanilla-oidc demo

- -

Authenticated

-
${JSON.stringify(tokens,null,'\t')}
-
` - - } - else { - // @ts-ignore - window.login= () => vanillaOidc.loginAsync("/"); - document.body.innerHTML = `
-

@axa-fr/vanilla-oidc demo

- -
` - } -}) +Include the provider's endpoint origins if they differ from its issuer URL. Keep access-token destinations as narrow as possible; patterns can include paths. String entries are interpreted as regular-expression prefixes, not exact origin matches. The example uses anchored regular expressions with escaped dots and hostname boundaries to avoid unintended matches. The `default` key must match the OIDC configuration name. +Add these options to your client configuration: +```javascript +service_worker_relative_url: '/OidcServiceWorker.js', +service_worker_only: true, ``` +Serve the worker from a URL whose scope covers your application, using HTTPS (or localhost for development). With `service_worker_only: true`, login requires an available service worker. With `false` (the default), the client can fall back to browser storage when worker mode is unavailable. Choose that fallback deliberately. + +### Trusted-domain options + +These options belong in each named entry in `OidcTrustedDomains.js`, not in `OidcConfiguration`. + +| Option | Purpose | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `oidcDomains` | Provider URLs the worker is allowed to handle. | +| `accessTokenDomains` | API URLs to which the worker may attach an access token. | +| `domains` | Shorthand for a shared provider/API destination list; separate lists give finer control. | +| `showAccessToken` | Defaults to `false`. Set to `true` only when an integration requires the real access token in application JavaScript; refresh tokens remain hidden in worker mode. | +| `allowMultiTabLogin` | Isolates login state, nonce, and PKCE verifier by tab. Requires the OIDC fetch wrapper for protected API requests; see below. | +| `convertAllRequestsToCorsExceptNavigate` | Defaults to `false`; optionally changes non-navigation requests to CORS mode. | +| `setAccessTokenToNavigateRequests` | Defaults to `true`; controls token attachment to matching navigation requests. | +| `bypassAllNonOidcRequests` | Defaults to `false`. When enabled for all initialized worker configurations, requests outside OIDC and access-token destinations are left to the browser. | +| `demonstratingProofOfPossession` | Enables worker-side DPoP for this configuration. | +| `demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent` | Defaults to `true` when worker DPoP is enabled. Requires a DPoP header before worker-side DPoP injection; set to `false` to remove that condition. | +| `demonstratingProofOfPossessionConfiguration` | Overrides the worker's cryptographic algorithms; see [DPoP](#dpop). | + +With `allowMultiTabLogin: true`, **use `oidcClient.fetchWithTokens(fetch)` for protected API requests**. Its tab-specific token placeholder tells the worker which session to use. Plain `fetch` or a default Axios request does not provide that marker, so automatic token attachment cannot select the tab's token and requests may receive HTTP 401. Use the OIDC fetch directly, or an integration that actually routes requests through it. + ## Configuration +The public [`OidcConfiguration` type](./src/types.ts) is the complete reference. Required fields are `client_id`, `authority`, `redirect_uri`, and `scope`. + +| Option | Default / behavior | +| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `authority_configuration` | Supply endpoint metadata instead of discovery. See `AuthorityConfiguration` in the type reference, including PAR metadata. | +| `storage` | `sessionStorage` when worker storage is not used. `localStorage` persists across tabs and browser sessions, but both are accessible to application JavaScript. | +| `login_state_storage` | Defaults to `storage`; separates authorization state, verifier, nonce, and login parameters from token storage. If tokens use `localStorage`, consider `sessionStorage` here to avoid cross-tab login races. | +| `silent_redirect_uri` | Enables the iframe-based silent-login flow. Must differ from `redirect_uri`; register it with the provider. | +| `silent_login_uri` | Page that starts silent login; derived from `silent_redirect_uri` if omitted. | +| `silent_login_timeout` | `12000` milliseconds. | +| `refresh_time_before_tokens_expiration_in_second` | `120` seconds; the client applies random jitter when this setting is greater than 60. | +| `token_renew_mode` | `'access_token_or_id_token_invalid'`; alternatives are `'access_token_invalid'` and `'id_token_invalid'`. | +| `token_automatic_renew_mode` | `TokenAutomaticRenewMode.AutomaticBeforeTokenExpiration`; see [token renewal](#token-renewal). | +| `token_request_timeout` | Token-request timeout in milliseconds. | +| `extras` | Additional authorization-request parameters, such as `{ prompt: 'consent' }`. | +| `token_request_extras` | Additional token-request parameters. Never use this to embed a browser client secret. | +| `par`, `par_request_timeout` | `'disabled'` and `10000` milliseconds; see [PAR](#pushed-authorization-requests-par). | +| `authority_time_cache_wellknowurl_in_second` | Discovery-cache lifetime; defaults to one hour. | +| `authority_timeout_wellknowurl_in_millisecond` | Discovery timeout; defaults to `10000` milliseconds. | +| `monitor_session` | `false`; enables OIDC session monitoring when supported by the provider and browser. | +| `logout_tokens_to_invalidate` | `['access_token', 'refresh_token']`; token types to revoke during standard logout. | +| `preload_user_info` | `false`; fetch user information during login/session restoration instead of waiting for a consumer. | +| `demonstrating_proof_of_possession` | `false`; see [DPoP](#dpop). | +| `demonstrating_proof_of_possession_configuration` | Cryptographic settings for client-side DPoP. | +| `service_worker_relative_url` | URL of the worker; omit to disable worker mode. | +| `service_worker_only` | `false`; disallows browser-storage fallback when `true`. | +| `service_worker_keep_alive_path` | `'/'`; path used by worker keep-alive requests. | +| `service_worker_activate` | Function to override the default browser-based activation decision. | +| `service_worker_register` | Custom `(url) => Promise` registration function. | +| `loading_timeout_ms` | Used by the React provider's loading watchdog; see the [React guide](../react-oidc/README.md#custom-components-and-provider-options). | + +### Silent login + +Silent login uses an iframe and the provider's existing session. It depends on provider support and browser cookie/privacy policies; it is not a guaranteed replacement for refresh tokens. + +For a vanilla application, implement both the `silent_login_uri` page and the `silent_redirect_uri` callback page. The login page starts `loginAsync` with `isSilentSignin: true` (the third argument); the callback page calls `silentLoginCallbackAsync()`. Keep these routes separate from the interactive callback. The React provider handles these routes for you; its [silent-login route implementation](../react-oidc/src/core/default-component/SilentLogin.component.tsx) is also a reference for forwarding silent-login parameters. + +## Token renewal + +By default, the client schedules renewal before tokens expire. It uses a refresh token when available, or the configured silent-login flow. Refresh-token issuance and lifetime remain provider decisions. + +For renewal only when an API request needs it: + +```typescript +import { TokenAutomaticRenewMode } from '@axa-fr/oidc-client'; + +const renewalOptions = { + token_automatic_renew_mode: TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted, +}; +``` + +Merge this option into your configuration and use `fetchWithTokens(fetch)` or `getValidTokenAsync()` before protected requests. Plain fetch does not trigger this renewal mode. + +- `renewTokensAsync(extras?, scope?)` requests renewal and preserves the non-throwing behavior for terminal OIDC failures. +- `renewTokensOrThrowAsync(extras?, scope?)` returns tokens or rejects, for callers that need explicit failure handling. +- Automatic renewal reports failures through [events](#errors-and-events). + +## Pushed Authorization Requests (PAR) + +[PAR (RFC 9126)](https://www.rfc-editor.org/rfc/rfc9126.html) sends authorization parameters to the provider before navigating the browser. + +| `par` mode | Behavior | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `'disabled'` (default) | Uses the normal front-channel request and ignores PAR metadata. | +| `'auto'` | Uses a discovered or configured `pushed_authorization_request_endpoint`. Falls back only when no endpoint exists and the server does not require PAR. | +| `'required'` | Requires a PAR endpoint; fails before navigation if none is available. | + +If metadata says `require_pushed_authorization_requests: true`, `'auto'` also fails when the endpoint is missing. Set `par_request_timeout` to adjust the default 10-second timeout. + +Once PAR is selected, the client sends authorization parameters (including state, nonce, PKCE, and login extras) as `application/x-www-form-urlencoded` data. The subsequent browser redirect carries `client_id` and the returned `request_uri`. An endpoint failure or invalid response **never silently downgrades** to the front-channel flow. + +Failures use `PushedAuthorizationRequestError`, with codes from `PushedAuthorizationRequestErrorCode`: `ENDPOINT_UNAVAILABLE`, `REQUEST_FAILED`, or `INVALID_RESPONSE`. Use `isPushedAuthorizationRequestError(error)` and inspect `code`, plus `status`, `oauthError`, and `oauthErrorDescription` when available. + +The PAR endpoint must allow CORS from your application and accept public clients without a client secret. If discovery is unavailable, configure `pushed_authorization_request_endpoint` and, where applicable, `require_pushed_authorization_requests` in `authority_configuration`. + +## DPoP + +[Demonstrating Proof of Possession (RFC 9449)](https://www.rfc-editor.org/rfc/rfc9449.html) binds supported tokens to a key and uses signed proofs for requests. It requires support from the authorization server and resource server. It reduces the usefulness of a stolen token without its key; it does not make an application immune to XSS. + +For client-side DPoP, set `demonstrating_proof_of_possession: true` in the OIDC configuration and opt in for protected requests: + ```javascript -const configuration: { - client_id: PropTypes.string.isRequired, // oidc client id - redirect_uri: PropTypes.string.isRequired, // oidc redirect url - silent_redirect_uri: PropTypes.string, // Optional activate silent-signin that use cookies between OIDC server and client javascript to restore sessions - silent_login_uri: PropTypes.string, // Optional, route that trigger the signin - silent_login_timeout: PropTypes.number, // Optional default is 12000 milliseconds - scope: PropTypes.string.isRequired, // oidc scope (you need to set "offline_access") - authority: PropTypes.string.isRequired, - storage: Storage, // Default sessionStorage, you can set localStorage but it is less secure to XSS attacks - authority_configuration: PropTypes.shape({ - // Optional for providers that does not implement OIDC server auto discovery via a .wellknowurl - authorization_endpoint: PropTypes.string, - token_endpoint: PropTypes.string, - userinfo_endpoint: PropTypes.string, - end_session_endpoint: PropTypes.string, - revocation_endpoint: PropTypes.string, - check_session_iframe: PropTypes.string, - issuer: PropTypes.string, - }), - refresh_time_before_tokens_expiration_in_second: PropTypes.number, // default is 120 seconds - service_worker_relative_url: PropTypes.string, - service_worker_only: PropTypes.boolean, // default false - service_worker_convert_all_requests_to_cors: PropTypes.boolean, // force all requests that servie worker upgrades to have 'cors' mode. This allows setting authentication token on requests initialted by html parsing(e.g. img tags, download links etc). - extras: StringMap | undefined, // ex: {'prompt': 'consent', 'access_type': 'offline'} list of key/value that are send to the oidc server (more info: https://github.com/openid/AppAuth-JS) - token_request_extras: StringMap | undefined, // ex: {'prompt': 'consent', 'access_type': 'offline'} list of key/value that are send to the oidc server during token request (more info: https://github.com/openid/AppAuth-JS) - withCustomHistory: PropTypes.function, // Override history modification, return instance with replaceState(url, stateHistory) implemented (like History.replaceState()) - authority_time_cache_wellknowurl_in_second: 60 * 60, // Time to cache in second of openid wellknowurl, default is 1 hour - authority_timeout_wellknowurl_in_millisecond: 10000, // Timeout in millisecond of openid wellknowurl, default is 10 seconds, then error is throwed - monitor_session: PropTypes.boolean, // Add OpenId monitor session, default is false (more information https://openid.net/specs/openid-connect-session-1_0.html), if you need to set it to true consider https://infi.nl/nieuws/spa-necromancy/ - onLogoutFromAnotherTab: Function, // Optional, can be set to override the default behavior, this function is triggered when user with the same subject is logged out from another tab when session_monitor is active - onLogoutFromSameTab: Function, // Optional, can be set to override the default behavior, this function is triggered when user is logged out from same tab when session_monitor is active - token_renew_mode: PropTypes.string, // Optional, update tokens base on the selected token(s) lifetime: "access_token_or_id_token_invalid" (default), "access_token_invalid" , "id_token_invalid" - logout_tokens_to_invalidate : Array // Optional tokens to invalidate during logout, default: ['access_token', 'refresh_token'] - }; +const dpopFetch = oidcClient.fetchWithTokens(fetch, true); +const response = await dpopFetch('https://api.example.com/profile', { method: 'GET' }); +``` + +Similarly, pass `true` as the second argument to `userInfoAsync(false, true)` if the user-info endpoint requires DPoP. For worker-side DPoP, configure `demonstratingProofOfPossession` in the matching trusted-domain entry and review `demonstratingProofOfPossessionOnlyWhenDpopHeaderPresent`. + +The default cryptographic configuration uses ECDSA P-256, SHA-256, and the `ES256` JWT algorithm. Override algorithms through `demonstrating_proof_of_possession_configuration` (client) or `demonstratingProofOfPossessionConfiguration` (worker). See [`DemonstratingProofOfPossessionConfiguration`](./src/types.ts). + +## API + +See [`OidcClient`](./src/oidcClient.ts) for full TypeScript signatures. + +| API | Use | +| ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `OidcClient.getOrCreate(getFetch, location?)(configuration, name?)` | Create or reuse a named client. Pass `() => fetch` for the browser fetch implementation; the default name is `'default'`. | +| `OidcClient.get(name?)` | Retrieve an existing client, or `null` if it has not been initialized. | +| `OidcClient.getOrThrow(name?)` | Retrieve an existing client, or throw on missing initialization. | +| `tryKeepExistingSessionAsync()` | Restore an existing session; returns whether it was kept. | +| `loginAsync(callbackPath?, extras?, isSilentSignin?, scope?, silentLoginOnly?)` | Start authentication. `callbackPath` is the application destination after the callback, not the registered `redirect_uri`. | +| `loginCallbackAsync()` | Complete interactive authentication, start automatic renewal, and return `{ callbackPath }`. | +| `silentLoginCallbackAsync()` | Complete the iframe silent-login callback. | +| `logoutAsync(callbackPathOrUrl?, extras?)` | Clear the session, revoke configured tokens, and use the provider's logout endpoint when available. | +| `clearSessionAsync()` | Clear the local session without provider logout or token revocation. | +| `isLoggingOut` | Indicates logout is in progress; avoid starting a competing login flow. | +| `renewTokensAsync(extras?, scope?)` | Request token renewal. | +| `renewTokensOrThrowAsync(extras?, scope?)` | Request renewal with a rejected promise on failure. | +| `getValidTokenAsync(waitMs?, numberWait?)` | Wait for usable tokens; also supports on-demand renewal. | +| `fetchWithTokens(fetch, demonstratingProofOfPossession?)` | Wrap fetch for protected API requests. | +| `userInfoAsync(noCache?, demonstratingProofOfPossession?)` | Load user information, optionally bypassing the cache. | +| `userInfo()` | Read cached user information. | +| `tokens`, `configuration` | Inspect current session data and effective configuration. Tokens can contain worker placeholders; do not display or log them. | +| `subscribeEvents(handler)` | Subscribe to `(eventName, data)` and receive a subscription ID. | +| `removeEventSubscription(id)` | Remove an event subscription. | +| `OidcClient.eventNames` | Discover supported event names. | +| `publishEvent(name, data)` | Publish an event to the client's subscribers. | +| `generateDemonstrationOfProofOfPossessionAsync(accessToken, url, method, extras?)` | Low-level proof generation; prefer the fetch wrapper for normal requests. | +| `signalServiceWorker(message, options?)` | Send a typed worker-protocol message. | + +You can inject a custom location adapter as the second argument to `getOrCreate`, using the exported `ILOidcLocation` interface and `OidcLocation` implementation. + +## Errors and events + +Known authentication, callback, renewal, and network failures use `OidcError`. Inspect stable `code` and `phase` fields rather than parsing messages. Other fields include `retryable`, optional `status`, `oauthError`, `oauthErrorDescription`, and `cause`. + +```typescript +import { OidcError, OidcErrorCode } from '@axa-fr/oidc-client'; + +try { + await oidcClient.renewTokensOrThrowAsync(); +} catch (error) { + if (error instanceof OidcError && error.code === OidcErrorCode.LOGIN_REQUIRED) { + await oidcClient.loginAsync('/'); + } else { + throw error; + } +} ``` -## Run The Demo +`retryable` means a retry without user interaction may succeed, not that one is always performed automatically. Network failures, HTTP 408/429/5xx responses, and DPoP nonce challenges can be retryable; `LOGIN_REQUIRED` needs an interactive login. -```sh -git clone https://github.com/AxaGuilDEv/react-oidc.git -cd react-oidc/packages/vanilla-demo -npm install -npm start -# then navigate to http://localhost:3000 +Non-throwing flows also expose errors through events. Refresh event payloads include an `error` field; user-info, logout, and protected API requests have dedicated error events. Log only selected diagnostic fields, not complete event payloads: + +```typescript +const subscriptionId = oidcClient.subscribeEvents((name, data) => { + const error = data instanceof OidcError ? data : data?.error; + if (error instanceof OidcError) { + console.warn(name, error.code, error.phase, error.status); + } +}); + +// When the subscriber is no longer needed: +oidcClient.removeEventSubscription(subscriptionId); ``` -## How It Works +### Missing or mismatched login state -This component is a pure vanilla JS OIDC client library agnostic to any framework. -It is a real alternative to existing oidc-client libraries. +`OidcStateError` extends `OidcError` and identifies callback-state failures: -More information about OIDC +- `OidcStateErrorCode.STATE_MISSING`: stored authorization state is missing. +- `OidcStateErrorCode.STATE_MISMATCH`: returned state does not match stored state. +- `OidcStateErrorCode.NONCE_MISSING`: stored nonce is missing. -- [French : Augmentez la sécurité et la simplicité de votre Système d’Information OpenID Connect](https://medium.com/just-tech-it-now/augmentez-la-s%C3%A9curit%C3%A9-et-la-simplicit%C3%A9-de-votre-syst%C3%A8me-dinformation-avec-oauth-2-0-cf0732d71284) -- [English : Increase the security and simplicity of your information system with openid connect](https://medium.com/just-tech-it-now/increase-the-security-and-simplicity-of-your-information-system-with-openid-connect-fa8c26b99d6d) +Use `isOidcStateError(error)` and inspect `error.code`, especially for errors passed through silent-login handling. Storage clearing, eviction, and competing login attempts can cause these failures. Do not bypass state or nonce checks; offer a fresh login. During silent renewal, a missing nonce is reported through the session-lost flow. -## Hash route +## Named configurations and routing -`vanilla-oidc` work also with hash route. +Use distinct names for separate providers or sessions with different scopes: ```javascript -export const configurationIdentityServerWithHash = { - client_id: "interactive.public.short", - redirect_uri: window.location.origin + "#authentication-callback", - silent_redirect_uri: - window.location.origin + "#authentication-silent-callback", - scope: "openid profile email api offline_access", - authority: "https://demo.duendesoftware.com", - refresh_time_before_tokens_expiration_in_second: 70, - service_worker_relative_url: "/OidcServiceWorker.js", - service_worker_only: false, -}; +const paymentsClient = OidcClient.getOrCreate(() => fetch)(paymentsConfiguration, 'payments'); +const existingClient = OidcClient.get('payments'); +``` + +Each name reuses its initialized configuration. Give configurations distinct callback routes and matching keys in `OidcTrustedDomains.js`. + +The library retains hash-route callback matching for legacy integrations. However, OAuth redirect URIs must not contain a fragment: use path-based callback URLs, as in the quick start, for new deployments. Existing hash-callback setups depend on provider-specific behavior and need matching application routing. Interactive and silent callback URLs must be different. + +## Service-worker protocol + +The worker's versioned `postMessage` protocol is documented in [`PROTOCOL.md`](../oidc-client-service-worker/PROTOCOL.md). Constants and helpers such as `PROTOCOL_VERSION`, `ServiceWorkerMessageType`, `TOKEN_PLACEHOLDERS`, and `buildSecuredTokenPlaceholder` are exported from `@axa-fr/oidc-client` and `@axa-fr/oidc-client-service-worker/protocol`. + +```typescript +import { OidcClient, ServiceWorkerMessageType } from '@axa-fr/oidc-client'; + +const client = OidcClient.getOrThrow(); +const result = await client.signalServiceWorker<{ state: string }>({ + type: ServiceWorkerMessageType.GET_STATE, + configurationName: 'default', + data: null, +}); ``` -## Service Worker Support +This requires an active worker. See the protocol reference for payloads, timeouts, and compatibility guarantees; avoid logging protocol responses containing session state. + +## Examples and further reading + +- [Vanilla JavaScript demo](../../examples/oidc-client-demo/README.md) — complete application and local development instructions. +- [React guide](../react-oidc/README.md) and [React demo](../../examples/react-oidc-demo/README.md). +- [FAQ and deployment guidance](../../FAQ.md). +- [Service-worker source and protocol](../oidc-client-service-worker/PROTOCOL.md). +- [Service-worker package guide](../oidc-client-service-worker/README.md). -- Firefox : tested on Firefox 98.0.2 -- Chrome/Edge : tested on version upper to 90 -- Opera : tested on version upper to 80 -- Safari : tested on Safari/605.1.15 +The demos belong to the pnpm monorepo. Follow their READMEs rather than installing each workspace independently. diff --git a/packages/oidc-client/bin/copy-service-worker-files.mjs b/packages/oidc-client/bin/copy-service-worker-files.mjs new file mode 100644 index 000000000..e26b84a8a --- /dev/null +++ b/packages/oidc-client/bin/copy-service-worker-files.mjs @@ -0,0 +1,76 @@ +/* global console, process */ +/* eslint no-console: "off" */ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +try { + /** + * Script to run after npm install + * + * Copy selected files to user's directory + */ + const script_prefix = 'oidc-client'; + + const copyFile = async (src, dest, overwrite) => { + if (!fileExists(src)) { + console.log(`[${script_prefix}:skip] file does not exist ${src}`); + return false; + } + if (!overwrite) { + if (fileExists(dest)) { + console.log(`[${script_prefix}:skip] file exists not overwriting ${dest}`); + return true; + } + } + await fs.promises.copyFile(src, dest); + console.log(`[${script_prefix}:copy] ${dest}`); + return true; + }; + + const fileExists = path => { + return !!fs.existsSync(path); + }; + + const initPath = process.cwd(); + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const srcDir = path.join(__dirname, '..', '..', 'oidc-client-service-worker', 'dist'); + const srcDirFallback = path.join( + __dirname, + '..', + 'node_modules', + '@axa-fr', + 'oidc-client-service-worker', + 'dist', + ); + const destinationFolder = process.argv.length >= 3 ? process.argv[2] : 'public'; + const destinationDir = path.join(initPath, destinationFolder); + + const files = [ + { + fileName: 'OidcServiceWorker.js', + overwrite: true, + }, + { + fileName: 'OidcTrustedDomains.js', + overwrite: false, + }, + ]; + + for await (const file of files) { + const success = await copyFile( + path.join(srcDir, file.fileName), + path.join(destinationDir, file.fileName), + file.overwrite, + ); + if (!success) { + await copyFile( + path.join(srcDirFallback, file.fileName), + path.join(destinationDir, file.fileName), + file.overwrite, + ); + } + } +} catch (err) { + console.warn(err); +} diff --git a/packages/oidc-client/bin/post-install.mjs b/packages/oidc-client/bin/post-install.mjs deleted file mode 100644 index e6dde94e8..000000000 --- a/packages/oidc-client/bin/post-install.mjs +++ /dev/null @@ -1,37 +0,0 @@ -import cpy from 'cpy'; -import path from 'path'; - -/** - * Script to run after npm install - * - * Copy selected files to user's directory - */ - -const initPath = process.env.INIT_CWD; -// console.log('current dir:', process.cwd()); -// console.log('userPath:', initPath); - -function copyProgress(progress) { - console.log('✓ [oidc-client:copy] ', progress.destinationPath); -} - -//TODO: Fragile.. need to find a better way to get the path -const srcDir = '../oidc-client-service-worker/dist/'; -const destinationDir = path.join(initPath, 'public'); -const FILE_EXISTS_CODE = 'EEXIST'; - -await cpy([path.join(srcDir,'OidcServiceWorker.js')], destinationDir, { - overwrite: true, -}).on('progress', copyProgress); - -try { - await cpy([path.join(srcDir,'OidcTrustedDomains.js')], destinationDir, { - overwrite: false, - }).on('progress', copyProgress); -} catch (e) { - if (e.code === FILE_EXISTS_CODE) { - console.log( - `✗ [oidc-client:skip] OidcTrustedDomains.js not copied, already exists in ${destinationDir}` - ); - } else throw e; -} diff --git a/packages/oidc-client/config/defaultEslintConfig.cjs b/packages/oidc-client/config/defaultEslintConfig.cjs deleted file mode 100644 index c3fa61f5b..000000000 --- a/packages/oidc-client/config/defaultEslintConfig.cjs +++ /dev/null @@ -1,148 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - extends: [ - 'standard', - 'plugin:react/recommended', - 'plugin:react-hooks/recommended', - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:import/typescript', - 'plugin:jsx-a11y/recommended', - ], - plugins: ['simple-import-sort', 'testing-library'], - env: { - node: true, - es6: true, - browser: true, - }, - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - // typescript-eslint specific options - warnOnUnsupportedTypeScriptVersion: true, - }, - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': [ - 'error', - { - argsIgnorePattern: '^_|req|res|next|err|ctx|args|context|info', - ignoreRestSiblings: true, - }, - ], - 'no-array-constructor': 'off', - '@typescript-eslint/no-array-constructor': 'warn', - 'no-redeclare': 'off', - '@typescript-eslint/no-redeclare': 'warn', - 'no-use-before-define': 'off', - '@typescript-eslint/no-use-before-define': [ - 'warn', - { - functions: false, - classes: false, - variables: false, - typedefs: false, - }, - ], - 'no-unused-expressions': 'off', - '@typescript-eslint/no-unused-expressions': [ - 'error', - { - allowShortCircuit: true, - allowTernary: true, - allowTaggedTemplates: true, - }, - ], - '@typescript-eslint/triple-slash-reference': 'off', - '@typescript-eslint/member-delimiter-style': [ - 'error', - { - multiline: { - delimiter: 'semi', - requireLast: true, - }, - singleline: { - delimiter: 'semi', - requireLast: false, - }, - }, - ], - camelcase: 'off', - 'comma-dangle': [ - 'error', - { - arrays: 'always-multiline', - objects: 'always-multiline', - imports: 'always-multiline', - exports: 'always-multiline', - functions: 'always-multiline', - }, - ], - 'array-callback-return': 'warn', - 'jsx-quotes': ['error', 'prefer-double'], - // 'max-len': ['error', { code: 120 }], - indent: 'off', - // quotes: ['error', 'single'], - semi: ['error', 'always'], - 'space-before-function-paren': 'off', - - 'import/no-named-as-default': 'off', - 'import/no-named-as-default-member': 'off', - 'import/default': 'off', - 'import/named': 'off', - 'import/namespace': 'off', - 'import/no-unresolved': 'off', - 'simple-import-sort/imports': 'error', - 'simple-import-sort/exports': 'error', - 'react/prop-types': 'off', - 'react/jsx-wrap-multilines': 'error', - 'react/react-in-jsx-scope': 'off', - 'react/display-name': 'off', - // https://github.com/facebook/react/tree/master/packages/eslint-plugin-react-hooks - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'off', - }, - - overrides: [ - { - files: ['*.js', '*.jsx'], - rules: { - '@typescript-eslint/no-var-requires': 'off', - }, - }, - { - // 3) Now we enable eslint-plugin-testing-library rules or preset only for matching files! - files: ['**/?(*.)+(spec|test).[jt]s?(x)'], - extends: ['plugin:testing-library/react'], - rules: { - 'testing-library/await-async-query': 'error', - 'testing-library/no-await-sync-query': 'error', - 'testing-library/no-debugging-utils': 'warn', - 'testing-library/no-dom-import': 'off', - 'testing-library/no-unnecessary-act': 'off', - }, - }, - ], - - settings: { - react: { - version: 'detect', - }, - 'import/parsers': { - '@typescript-eslint/parser': ['.ts', '.tsx'], - }, - 'import/resolver': { - typescript: { - alwaysTryTypes: true, - }, - }, - }, - }; \ No newline at end of file diff --git a/packages/oidc-client/package-lock.json b/packages/oidc-client/package-lock.json deleted file mode 100644 index 65ddfd28d..000000000 --- a/packages/oidc-client/package-lock.json +++ /dev/null @@ -1,4725 +0,0 @@ -{ - "name": "@axa-fr/vanilla-oidc", - "version": "6.24.1", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "@axa-fr/vanilla-oidc", - "version": "6.24.1", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "base64-js": "1.5.1" - }, - "devDependencies": { - "@craco/types": "^7.1.0", - "@types/react": "^18.0.21", - "copyfiles": "2.4.1", - "rimraf": "3.0.2", - "typescript": "4.5.5" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@craco/types": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@craco/types/-/types-7.1.0.tgz", - "integrity": "sha512-zdyk2G9UfEItrvnB+sd3xDHB5Mf3dsD6wE+Ex6V+Nch+GSXdFGQfXD/l+ZX9hO03R1rmnJPCxrIRPJUib8Q/MQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.19.3", - "@jest/types": "^27.5.1", - "@types/eslint": "^8.4.6", - "autoprefixer": "^10.4.12", - "eslint-webpack-plugin": "^3.2.0", - "webpack": "^5.74.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, - "peer": 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.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", - "dev": true, - "peer": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", - "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", - "dev": true, - "peer": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "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.43.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz", - "integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==", - "dev": true, - "peer": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", - "dev": true, - "peer": true, - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true, - "peer": true - }, - "node_modules/@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.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, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "node_modules/@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, - "peer": 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, - "peer": 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, - "peer": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@types/eslint": { - "version": "8.40.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.2.tgz", - "integrity": "sha512-PRVjQ4Eh9z9pmmtaq8nTjZjQwKFk7YIHIud3lRoKRBgUQjgjRmoGxxGEPXQkF+lH7QkHJRNr5F4aBgYCW0lqpQ==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", - "dev": true - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", - "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "20.3.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", - "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==", - "dev": true - }, - "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.13", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.13.tgz", - "integrity": "sha512-vJ+zElvi/Zn9cVXB5slX2xL8PZodPCwPRDpittQdw43JR2AJ5k3vKdgJJyneV/cYgIbLQUwXa9JVDvUZXGba+Q==", - "dev": true, - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true - }, - "node_modules/@types/yargs": { - "version": "16.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.5.tgz", - "integrity": "sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "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/@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/acorn": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", - "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", - "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/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, - "peer": 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/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.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/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-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "peer": true - }, - "node_modules/autoprefixer": { - "version": "10.4.14", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", - "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - } - ], - "dependencies": { - "browserslist": "^4.21.5", - "caniuse-lite": "^1.0.30001464", - "fraction.js": "^4.2.0", - "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/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==", - "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/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/browserslist": { - "version": "4.21.9", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", - "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", - "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.30001503", - "electron-to-chromium": "^1.4.431", - "node-releases": "^2.0.12", - "update-browserslist-db": "^1.0.11" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "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/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/caniuse-lite": { - "version": "1.0.30001506", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001506.tgz", - "integrity": "sha512-6XNEcpygZMCKaufIcgpQNZNf00GEqc7VQON+9Rd0K1bMYo8xhMZRAo5zpbnbMNizi4YNgIDAFrdykWsvY3H4Hw==", - "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": "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/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/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/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/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "node_modules/copyfiles": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", - "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", - "dev": true, - "dependencies": { - "glob": "^7.0.5", - "minimatch": "^3.0.3", - "mkdirp": "^1.0.4", - "noms": "0.0.0", - "through2": "^2.0.1", - "untildify": "^4.0.0", - "yargs": "^16.1.0" - }, - "bin": { - "copyfiles": "copyfiles", - "copyup": "copyfiles" - } - }, - "node_modules/copyfiles/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/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.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/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "dev": true - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "peer": 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, - "peer": 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, - "peer": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.437", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.437.tgz", - "integrity": "sha512-ZFekRuBOHUXp21wrR5lshT6pZa/KmjkhKBAtmZz4NN5sCWlHOk3kdhiwFINrDBsRLX6FjyBAb1TRN+KBeNlyzQ==", - "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/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/es-module-lexer": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", - "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", - "dev": true - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.43.0.tgz", - "integrity": "sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==", - "dev": true, - "peer": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.43.0", - "@humanwhocodes/config-array": "^0.11.10", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "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.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "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", - "import-fresh": "^3.0.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.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "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-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", - "dev": true, - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", - "dev": true, - "peer": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", - "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", - "dev": true, - "dependencies": { - "@types/eslint": "^7.29.0 || ^8.4.1", - "jest-worker": "^28.0.2", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0", - "webpack": "^5.0.0" - } - }, - "node_modules/espree": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", - "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", - "dev": true, - "peer": true, - "dependencies": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "peer": 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, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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/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-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, - "peer": 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, - "peer": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "peer": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "peer": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "peer": true, - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", - "dev": true, - "peer": true - }, - "node_modules/fraction.js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", - "dev": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://www.patreon.com/infusion" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "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/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "peer": 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/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", - "dev": true, - "peer": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "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/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "peer": true - }, - "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/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true, - "peer": 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, - "peer": 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, - "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": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "peer": 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, - "peer": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/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, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "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, - "peer": true - }, - "node_modules/jest-worker": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", - "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", - "dev": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.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, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "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, - "peer": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "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": "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, - "peer": true - }, - "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, - "peer": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "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/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, - "peer": 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, - "peer": true - }, - "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/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-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "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/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "peer": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "peer": 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/node-releases": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", - "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", - "dev": true - }, - "node_modules/noms": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", - "integrity": "sha1-2o69nzr51nYJGbJ9nNyAkqczKFk=", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "~1.0.31" - } - }, - "node_modules/noms/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/noms/node_modules/readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/noms/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": 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, - "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/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "peer": 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.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==", - "dev": true, - "peer": 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, - "peer": 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, - "peer": 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, - "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": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "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, - "peer": 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==", - "dev": 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, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.4.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", - "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", - "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" - } - ], - "peer": true, - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "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/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, - "peer": true, - "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==", - "dev": true - }, - "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "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" - } - ], - "peer": true - }, - "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/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==", - "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/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "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-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, - "peer": 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, - "peer": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-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" - } - ], - "peer": true, - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/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/schema-utils/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/schema-utils/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/schema-utils/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/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/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/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==", - "dev": true, - "peer": 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/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "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, - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/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/terser": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.1.tgz", - "integrity": "sha512-j1n0Ao919h/Ai5r43VAnfV/7azUYW43GPxK7qSATzrsERfW7+y2QW9Cp9ufnRF5CQUWbnLSo7UJokSWCqg4tsQ==", - "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/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/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-webpack-plugin/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/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, - "peer": 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==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "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/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, - "peer": 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, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", - "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": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "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/webpack": { - "version": "5.88.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.0.tgz", - "integrity": "sha512-O3jDhG5e44qIBSi/P6KpcCcH7HD+nYIHVBhdWFxcLOcIGN8zGo5nqF3BjyNCxIh4p1vFdNnreZv2h2KkoAw3lw==", - "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-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/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/webpack/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/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/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.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "peer": 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==", - "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/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": 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==", - "dev": true, - "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==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "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, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - }, - "dependencies": { - "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", - "dev": true - }, - "@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - } - }, - "@craco/types": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@craco/types/-/types-7.1.0.tgz", - "integrity": "sha512-zdyk2G9UfEItrvnB+sd3xDHB5Mf3dsD6wE+Ex6V+Nch+GSXdFGQfXD/l+ZX9hO03R1rmnJPCxrIRPJUib8Q/MQ==", - "dev": true, - "requires": { - "@babel/types": "^7.19.3", - "@jest/types": "^27.5.1", - "@types/eslint": "^8.4.6", - "autoprefixer": "^10.4.12", - "eslint-webpack-plugin": "^3.2.0", - "webpack": "^5.74.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, - "peer": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", - "dev": true, - "peer": true - }, - "@eslint/eslintrc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", - "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", - "dev": true, - "peer": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz", - "integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==", - "dev": true, - "peer": true - }, - "@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", - "dev": true, - "peer": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "peer": true - }, - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true, - "peer": true - }, - "@jest/types": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", - "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" - } - }, - "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true - }, - "@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "@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, - "peer": 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, - "peer": 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, - "peer": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@types/eslint": { - "version": "8.40.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.2.tgz", - "integrity": "sha512-PRVjQ4Eh9z9pmmtaq8nTjZjQwKFk7YIHIud3lRoKRBgUQjgjRmoGxxGEPXQkF+lH7QkHJRNr5F4aBgYCW0lqpQ==", - "dev": true, - "requires": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", - "dev": true, - "requires": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", - "dev": true - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true - }, - "@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" - } - }, - "@types/json-schema": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", - "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", - "dev": true - }, - "@types/node": { - "version": "20.3.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", - "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==", - "dev": true - }, - "@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.13", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.13.tgz", - "integrity": "sha512-vJ+zElvi/Zn9cVXB5slX2xL8PZodPCwPRDpittQdw43JR2AJ5k3vKdgJJyneV/cYgIbLQUwXa9JVDvUZXGba+Q==", - "dev": true, - "requires": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true - }, - "@types/yargs": { - "version": "16.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.5.tgz", - "integrity": "sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@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 - }, - "@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, - "requires": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "@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 - }, - "@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 - }, - "@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 - }, - "@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, - "requires": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "@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 - }, - "@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, - "requires": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "dev": true, - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "dev": true, - "requires": { - "@xtuc/long": "4.2.2" - } - }, - "@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 - }, - "@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, - "requires": { - "@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" - } - }, - "@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, - "requires": { - "@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" - } - }, - "@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, - "requires": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" - } - }, - "@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, - "requires": { - "@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" - } - }, - "@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, - "requires": { - "@webassemblyjs/ast": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "@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 - }, - "@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 - }, - "acorn": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", - "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", - "dev": true - }, - "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, - "requires": {} - }, - "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, - "peer": 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" - } - }, - "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, - "requires": { - "ajv": "^8.0.0" - }, - "dependencies": { - "ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - } - } - }, - "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, - "requires": {} - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "peer": true - }, - "autoprefixer": { - "version": "10.4.14", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", - "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", - "dev": true, - "requires": { - "browserslist": "^4.21.5", - "caniuse-lite": "^1.0.30001464", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - } - }, - "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 - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "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" - } - }, - "browserslist": { - "version": "4.21.9", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", - "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001503", - "electron-to-chromium": "^1.4.431", - "node-releases": "^2.0.12", - "update-browserslist-db": "^1.0.11" - } - }, - "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 - }, - "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 - }, - "caniuse-lite": { - "version": "1.0.30001506", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001506.tgz", - "integrity": "sha512-6XNEcpygZMCKaufIcgpQNZNf00GEqc7VQON+9Rd0K1bMYo8xhMZRAo5zpbnbMNizi4YNgIDAFrdykWsvY3H4Hw==", - "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" - } - }, - "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 - }, - "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 - }, - "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 - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "copyfiles": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", - "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", - "dev": true, - "requires": { - "glob": "^7.0.5", - "minimatch": "^3.0.3", - "mkdirp": "^1.0.4", - "noms": "0.0.0", - "through2": "^2.0.1", - "untildify": "^4.0.0", - "yargs": "^16.1.0" - }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - } - } - }, - "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.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "peer": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", - "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, - "peer": 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, - "peer": true - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "peer": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "electron-to-chromium": { - "version": "1.4.437", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.437.tgz", - "integrity": "sha512-ZFekRuBOHUXp21wrR5lshT6pZa/KmjkhKBAtmZz4NN5sCWlHOk3kdhiwFINrDBsRLX6FjyBAb1TRN+KBeNlyzQ==", - "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 - }, - "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, - "requires": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - } - }, - "es-module-lexer": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", - "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", - "dev": true - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "peer": true - }, - "eslint": { - "version": "8.43.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.43.0.tgz", - "integrity": "sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==", - "dev": true, - "peer": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.43.0", - "@humanwhocodes/config-array": "^0.11.10", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", - "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.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", - "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", - "import-fresh": "^3.0.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.1", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - } - }, - "eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", - "dev": true, - "peer": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "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==", - "dev": true, - "peer": true - }, - "eslint-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.2.0.tgz", - "integrity": "sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==", - "dev": true, - "requires": { - "@types/eslint": "^7.29.0 || ^8.4.1", - "jest-worker": "^28.0.2", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0" - } - }, - "espree": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", - "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", - "dev": true, - "peer": true, - "requires": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "peer": 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, - "peer": true - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "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-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, - "peer": true - }, - "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, - "peer": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "peer": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "peer": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "peer": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", - "dev": true, - "peer": true - }, - "fraction.js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", - "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "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==", - "dev": true - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "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, - "peer": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true - }, - "globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", - "dev": true, - "peer": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "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, - "peer": 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 - }, - "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true, - "peer": 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, - "peer": 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, - "peer": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "peer": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "peer": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "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, - "peer": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "peer": true - }, - "jest-worker": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", - "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", - "dev": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "dependencies": { - "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" - } - } - } - }, - "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, - "peer": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "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-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, - "peer": 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, - "peer": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "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 - }, - "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, - "peer": 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, - "peer": true - }, - "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 - }, - "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-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "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==", - "dev": true, - "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==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "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 - }, - "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, - "peer": true - }, - "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 - }, - "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-releases": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", - "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", - "dev": true - }, - "noms": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", - "integrity": "sha1-2o69nzr51nYJGbJ9nNyAkqczKFk=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "~1.0.31" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "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=", - "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==", - "dev": true - }, - "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 - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "peer": 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" - } - }, - "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, - "peer": 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, - "peer": 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, - "peer": 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, - "peer": true - }, - "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=", - "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, - "peer": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "postcss": { - "version": "8.4.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", - "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", - "dev": true, - "peer": true, - "requires": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "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 - }, - "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, - "peer": 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 - }, - "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "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, - "peer": 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" - } - }, - "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==", - "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" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "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 - }, - "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, - "peer": 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, - "peer": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-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, - "peer": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "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, - "requires": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "dependencies": { - "ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "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" - } - }, - "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, - "requires": { - "fast-deep-equal": "^3.1.3" - } - }, - "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 - } - } - }, - "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, - "requires": { - "randombytes": "^2.1.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, - "peer": 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, - "peer": 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-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, - "peer": 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" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "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, - "peer": 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" - } - }, - "tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true - }, - "terser": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.1.tgz", - "integrity": "sha512-j1n0Ao919h/Ai5r43VAnfV/7azUYW43GPxK7qSATzrsERfW7+y2QW9Cp9ufnRF5CQUWbnLSo7UJokSWCqg4tsQ==", - "dev": true, - "requires": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - } - }, - "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, - "requires": { - "@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" - }, - "dependencies": { - "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, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - } - }, - "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, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - }, - "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" - } - } - } - }, - "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, - "peer": true - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "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, - "peer": 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, - "peer": true - }, - "typescript": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", - "dev": true - }, - "untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true - }, - "update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", - "dev": true, - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - } - }, - "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" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "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, - "requires": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - } - }, - "webpack": { - "version": "5.88.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.0.tgz", - "integrity": "sha512-O3jDhG5e44qIBSi/P6KpcCcH7HD+nYIHVBhdWFxcLOcIGN8zGo5nqF3BjyNCxIh4p1vFdNnreZv2h2KkoAw3lw==", - "dev": true, - "requires": { - "@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" - }, - "dependencies": { - "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" - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "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, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - } - } - }, - "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 - }, - "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, - "requires": { - "isexe": "^2.0.0" - } - }, - "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==", - "dev": true, - "peer": true - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "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": "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 - }, - "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, - "peer": true - } - } -} diff --git a/packages/oidc-client/package.json b/packages/oidc-client/package.json index b566b1bfd..1143b81aa 100644 --- a/packages/oidc-client/package.json +++ b/packages/oidc-client/package.json @@ -1,6 +1,6 @@ { - "name": "@axa-fr/vanilla-oidc", - "version": "6.24.1", + "name": "@axa-fr/oidc-client", + "version": "7.29.6", "private": false, "type": "module", "main": "./dist/index.umd.cjs", @@ -17,28 +17,24 @@ ], "repository": { "type": "git", - "url": "https://github.com/AxaGuilDEv/react-oidc.git" + "url": "https://github.com/AxaFrance/oidc-client.git" + }, + "dependencies": { + "@axa-fr/oidc-client-service-worker": "workspace:*" }, "devDependencies": { - "@axa-fr/oidc-client-service-worker": "workspace:*", - "@testing-library/dom": "^9.3.1", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "13.3.0", - "@vitest/coverage-v8": "^0.33.0", - "base64-js": "^1.5.1", - "cpy": "^10.1.0", - "cpy-cli": "^5.0.0", - "eslint": "^8.26.0", - "eslint-config-standard": "^17.1.0", - "eslint-config-standard-with-typescript": "^36.1.0", - "eslint-import-resolver-typescript": "^3.5.5", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-simple-import-sort": "^10.0.0", - "rimraf": "5.0.1", - "typescript": "5.1.6", - "vite": "^4.4.4", - "vite-plugin-dts": "^3.3.0", - "vitest": "^0.33.0" + "@testing-library/dom": "10.4.2", + "@testing-library/jest-dom": "7.0.1", + "@testing-library/react": "16.3.3", + "@types/node": "^26.5.1", + "@vitest/coverage-v8": "5.0.0", + "cpy": "13.2.3", + "cpy-cli": "^7.0.0", + "rimraf": "6.1.3", + "typescript": "npm:@typescript/typescript6@6.0.2", + "vite": "8.3.0", + "vite-plugin-dts": "5.1.0", + "vitest": "5.0.0" }, "keywords": [ "oidc", @@ -46,7 +42,8 @@ "openid", "oauth2", "oauth", - "vanilla" + "vanilla", + "vanillajs" ], "scripts": { "clean": "rimraf dist", @@ -54,7 +51,7 @@ "build": "tsc && vite build", "test": "vitest --root . --coverage", "prepare": "pnpm run clean && pnpm run copy-service-worker && pnpm run build", - "postinstall": "node ./bin/post-install.mjs" + "postinstall": "echo 'WARNING keep sink OidcServiceWorker.js version file'" }, "license": "MIT", "publishConfig": { diff --git a/packages/oidc-client/public/OidcServiceWorker.js b/packages/oidc-client/public/OidcServiceWorker.js deleted file mode 100644 index b70fc4b42..000000000 --- a/packages/oidc-client/public/OidcServiceWorker.js +++ /dev/null @@ -1,559 +0,0 @@ -const scriptFilename = "OidcTrustedDomains.js"; -const acceptAnyDomainToken = "*"; -const TOKEN = { - REFRESH_TOKEN: "REFRESH_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER", - ACCESS_TOKEN: "ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER", - NONCE_TOKEN: "NONCE_SECURED_BY_OIDC_SERVICE_WORKER", - CODE_VERIFIER: "CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER" -}; -const TokenRenewMode = { - access_token_or_id_token_invalid: "access_token_or_id_token_invalid", - access_token_invalid: "access_token_invalid", - id_token_invalid: "id_token_invalid" -}; -const openidWellknownUrlEndWith = "/.well-known/openid-configuration"; -function checkDomain(domains, endpoint) { - if (!endpoint) { - return; - } - const domain = domains.find((domain2) => { - var _a; - let testable; - if (typeof domain2 === "string") { - testable = new RegExp(`^${domain2}`); - } else { - testable = domain2; - } - return (_a = testable.test) == null ? void 0 : _a.call(testable, endpoint); - }); - if (!domain) { - throw new Error( - "Domain " + endpoint + " is not trusted, please add domain in " + scriptFilename - ); - } -} -const getDomains = (trustedDomain, type) => { - if (Array.isArray(trustedDomain)) { - return trustedDomain; - } - return trustedDomain[`${type}Domains`] ?? trustedDomain.domains ?? []; -}; -const getCurrentDatabaseDomain = (database2, url, trustedDomains2) => { - var _a; - if (url.endsWith(openidWellknownUrlEndWith)) { - return null; - } - for (const [key, currentDatabase] of Object.entries(database2)) { - const oidcServerConfiguration = currentDatabase.oidcServerConfiguration; - if (!oidcServerConfiguration) { - continue; - } - if (oidcServerConfiguration.tokenEndpoint && url === oidcServerConfiguration.tokenEndpoint) { - continue; - } - if (oidcServerConfiguration.revocationEndpoint && url === oidcServerConfiguration.revocationEndpoint) { - continue; - } - const trustedDomain = trustedDomains2 == null ? [] : trustedDomains2[key]; - const domains = getDomains(trustedDomain, "accessToken"); - const domainsToSendTokens = oidcServerConfiguration.userInfoEndpoint ? [oidcServerConfiguration.userInfoEndpoint, ...domains] : [...domains]; - let hasToSendToken = false; - if (domainsToSendTokens.find((f) => f === acceptAnyDomainToken)) { - hasToSendToken = true; - } else { - for (let i = 0; i < domainsToSendTokens.length; i++) { - let domain = domainsToSendTokens[i]; - if (typeof domain === "string") { - domain = new RegExp(`^${domain}`); - } - if ((_a = domain.test) == null ? void 0 : _a.call(domain, url)) { - hasToSendToken = true; - break; - } - } - } - if (hasToSendToken) { - if (!currentDatabase.tokens) { - return null; - } - return currentDatabase; - } - } - return null; -}; -function serializeHeaders(headers) { - const headersObj = {}; - for (const key of headers.keys()) { - if (headers.has(key)) { - headersObj[key] = headers.get(key); - } - } - return headersObj; -} -const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); -function countLetter(str, find) { - return str.split(find).length - 1; -} -function parseJwt(token) { - return JSON.parse( - b64DecodeUnicode(token.split(".")[1].replace("-", "+").replace("_", "/")) - ); -} -function b64DecodeUnicode(str) { - return decodeURIComponent( - Array.prototype.map.call( - atob(str), - (c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2) - ).join("") - ); -} -function computeTimeLeft(refreshTimeBeforeTokensExpirationInSecond, expiresAt) { - const currentTimeUnixSecond = (/* @__PURE__ */ new Date()).getTime() / 1e3; - return Math.round( - expiresAt - refreshTimeBeforeTokensExpirationInSecond - currentTimeUnixSecond - ); -} -function isTokensValid(tokens) { - if (!tokens) { - return false; - } - return computeTimeLeft(0, tokens.expiresAt) > 0; -} -const extractTokenPayload = (token) => { - try { - if (!token) { - return null; - } - if (countLetter(token, ".") === 2) { - return parseJwt(token); - } else { - return null; - } - } catch (e) { - console.warn(e); - } - return null; -}; -const isTokensOidcValid = (tokens, nonce, oidcServerConfiguration) => { - if (tokens.idTokenPayload) { - const idTokenPayload = tokens.idTokenPayload; - if (oidcServerConfiguration.issuer !== idTokenPayload.iss) { - return { isValid: false, reason: "Issuer does not match" }; - } - const currentTimeUnixSecond = (/* @__PURE__ */ new Date()).getTime() / 1e3; - if (idTokenPayload.exp && idTokenPayload.exp < currentTimeUnixSecond) { - return { isValid: false, reason: "Token expired" }; - } - const timeInSevenDays = 60 * 60 * 24 * 7; - if (idTokenPayload.iat && idTokenPayload.iat + timeInSevenDays < currentTimeUnixSecond) { - return { isValid: false, reason: "Token is used from too long time" }; - } - if (nonce && idTokenPayload.nonce && idTokenPayload.nonce !== nonce) { - return { isValid: false, reason: "Nonce does not match" }; - } - } - return { isValid: true, reason: "" }; -}; -function _hideTokens(tokens, currentDatabaseElement, configurationName) { - if (!tokens.issued_at) { - const currentTimeUnixSecond = (/* @__PURE__ */ new Date()).getTime() / 1e3; - tokens.issued_at = currentTimeUnixSecond; - } - const accessTokenPayload = extractTokenPayload(tokens.access_token); - const secureTokens = { - ...tokens, - accessTokenPayload - }; - if (currentDatabaseElement.hideAccessToken) { - secureTokens.access_token = TOKEN.ACCESS_TOKEN + "_" + configurationName; - } - tokens.accessTokenPayload = accessTokenPayload; - let _idTokenPayload = null; - if (tokens.id_token) { - _idTokenPayload = extractTokenPayload(tokens.id_token); - tokens.idTokenPayload = { ..._idTokenPayload }; - if (_idTokenPayload.nonce && currentDatabaseElement.nonce != null) { - const keyNonce = TOKEN.NONCE_TOKEN + "_" + currentDatabaseElement.configurationName; - _idTokenPayload.nonce = keyNonce; - } - secureTokens.idTokenPayload = _idTokenPayload; - } - if (tokens.refresh_token) { - secureTokens.refresh_token = TOKEN.REFRESH_TOKEN + "_" + configurationName; - } - const idTokenExpiresAt = _idTokenPayload && _idTokenPayload.exp ? _idTokenPayload.exp : Number.MAX_VALUE; - const accessTokenExpiresAt = accessTokenPayload && accessTokenPayload.exp ? accessTokenPayload.exp : tokens.issued_at + tokens.expires_in; - let expiresAt; - const tokenRenewMode = currentDatabaseElement.oidcConfiguration.token_renew_mode; - if (tokenRenewMode === TokenRenewMode.access_token_invalid) { - expiresAt = accessTokenExpiresAt; - } else if (tokenRenewMode === TokenRenewMode.id_token_invalid) { - expiresAt = idTokenExpiresAt; - } else { - expiresAt = idTokenExpiresAt < accessTokenExpiresAt ? idTokenExpiresAt : accessTokenExpiresAt; - } - secureTokens.expiresAt = expiresAt; - tokens.expiresAt = expiresAt; - const nonce = currentDatabaseElement.nonce ? currentDatabaseElement.nonce.nonce : null; - const { isValid, reason } = isTokensOidcValid( - tokens, - nonce, - currentDatabaseElement.oidcServerConfiguration - ); - if (!isValid) { - throw Error(`Tokens are not OpenID valid, reason: ${reason}`); - } - if (currentDatabaseElement.tokens != null && "refresh_token" in currentDatabaseElement.tokens && !("refresh_token" in tokens)) { - const refreshToken = currentDatabaseElement.tokens.refresh_token; - currentDatabaseElement.tokens = { - ...tokens, - refresh_token: refreshToken - }; - } else { - currentDatabaseElement.tokens = tokens; - } - currentDatabaseElement.status = "LOGGED_IN"; - return secureTokens; -} -function hideTokens(currentDatabaseElement) { - const configurationName = currentDatabaseElement.configurationName; - return (response) => { - if (response.status !== 200) { - return response; - } - return response.json().then((tokens) => { - const secureTokens = _hideTokens(tokens, currentDatabaseElement, configurationName); - const body = JSON.stringify(secureTokens); - return new Response(body, response); - }); - }; -} -function replaceCodeVerifier(codeVerifier, newCodeVerifier) { - const regex = /code_verifier=[A-Za-z0-9_-]+/i; - return codeVerifier.replace(regex, `code_verifier=${newCodeVerifier}`); -} -const _self = self; -_self.importScripts(scriptFilename); -const id = Math.round((/* @__PURE__ */ new Date()).getTime() / 1e3).toString(); -const keepAliveJsonFilename = "OidcKeepAliveServiceWorker.json"; -const handleInstall = (event) => { - console.log("[OidcServiceWorker] service worker installed " + id); - event.waitUntil(_self.skipWaiting()); -}; -const handleActivate = (event) => { - console.log("[OidcServiceWorker] service worker activated " + id); - event.waitUntil(_self.clients.claim()); -}; -let currentLoginCallbackConfigurationName = null; -const database = { - default: { - configurationName: "default", - tokens: null, - status: null, - state: null, - codeVerifier: null, - nonce: null, - oidcServerConfiguration: null, - hideAccessToken: true - } -}; -const getCurrentDatabasesTokenEndpoint = (database2, url) => { - const databases = []; - for (const [, value] of Object.entries(database2)) { - if (value.oidcServerConfiguration != null && url.startsWith(value.oidcServerConfiguration.tokenEndpoint)) { - databases.push(value); - } else if (value.oidcServerConfiguration != null && value.oidcServerConfiguration.revocationEndpoint && url.startsWith(value.oidcServerConfiguration.revocationEndpoint)) { - databases.push(value); - } - } - return databases; -}; -const keepAliveAsync = async (event) => { - const originalRequest = event.request; - const isFromVanilla = originalRequest.headers.has("oidc-vanilla"); - const init = { status: 200, statusText: "oidc-service-worker" }; - const response = new Response("{}", init); - if (!isFromVanilla) { - const originalRequestUrl = new URL(originalRequest.url); - const minSleepSeconds = Number(originalRequestUrl.searchParams.get("minSleepSeconds")) || 240; - for (let i = 0; i < minSleepSeconds; i++) { - await sleep(1e3 + Math.floor(Math.random() * 1e3)); - const cache = await caches.open("oidc_dummy_cache"); - await cache.put(event.request, response.clone()); - } - } - return response; -}; -const handleFetch = async (event) => { - const originalRequest = event.request; - const url = originalRequest.url; - if (originalRequest.url.includes(keepAliveJsonFilename)) { - event.respondWith(keepAliveAsync(event)); - return; - } - const currentDatabaseForRequestAccessToken = getCurrentDatabaseDomain( - database, - originalRequest.url, - trustedDomains - ); - if (currentDatabaseForRequestAccessToken && currentDatabaseForRequestAccessToken.tokens && currentDatabaseForRequestAccessToken.tokens.access_token) { - while (currentDatabaseForRequestAccessToken.tokens && !isTokensValid(currentDatabaseForRequestAccessToken.tokens)) { - await sleep(200); - } - const newRequest = originalRequest.mode === "navigate" ? new Request(originalRequest, { - headers: { - ...serializeHeaders(originalRequest.headers), - authorization: "Bearer " + currentDatabaseForRequestAccessToken.tokens.access_token - } - }) : new Request(originalRequest, { - headers: { - ...serializeHeaders(originalRequest.headers), - authorization: "Bearer " + currentDatabaseForRequestAccessToken.tokens.access_token - }, - mode: currentDatabaseForRequestAccessToken.oidcConfiguration.service_worker_convert_all_requests_to_cors ? "cors" : originalRequest.mode - }); - event.waitUntil(event.respondWith(fetch(newRequest))); - return; - } - if (event.request.method !== "POST") { - return; - } - let currentDatabase = null; - const currentDatabases = getCurrentDatabasesTokenEndpoint( - database, - originalRequest.url - ); - const numberDatabase = currentDatabases.length; - if (numberDatabase > 0) { - const maPromesse = new Promise((resolve, reject) => { - const clonedRequest = originalRequest.clone(); - const response = clonedRequest.text().then((actualBody) => { - if (actualBody.includes(TOKEN.REFRESH_TOKEN) || actualBody.includes(TOKEN.ACCESS_TOKEN)) { - let newBody = actualBody; - for (let i = 0; i < numberDatabase; i++) { - const currentDb = currentDatabases[i]; - if (currentDb && currentDb.tokens != null) { - const keyRefreshToken = TOKEN.REFRESH_TOKEN + "_" + currentDb.configurationName; - if (actualBody.includes(keyRefreshToken)) { - newBody = newBody.replace( - keyRefreshToken, - encodeURIComponent(currentDb.tokens.refresh_token) - ); - currentDatabase = currentDb; - break; - } - const keyAccessToken = TOKEN.ACCESS_TOKEN + "_" + currentDb.configurationName; - if (actualBody.includes(keyAccessToken)) { - newBody = newBody.replace( - keyAccessToken, - encodeURIComponent(currentDb.tokens.access_token) - ); - currentDatabase = currentDb; - break; - } - } - } - const fetchPromise = fetch(originalRequest, { - body: newBody, - method: clonedRequest.method, - headers: { - ...serializeHeaders(originalRequest.headers) - }, - mode: clonedRequest.mode, - cache: clonedRequest.cache, - redirect: clonedRequest.redirect, - referrer: clonedRequest.referrer, - credentials: clonedRequest.credentials, - integrity: clonedRequest.integrity - }); - if (currentDatabase && currentDatabase.oidcServerConfiguration != null && currentDatabase.oidcServerConfiguration.revocationEndpoint && url.startsWith( - currentDatabase.oidcServerConfiguration.revocationEndpoint - )) { - return fetchPromise.then(async (response2) => { - const text = await response2.text(); - return new Response(text, response2); - }); - } - return fetchPromise.then(hideTokens(currentDatabase)); - } else if (actualBody.includes("code_verifier=") && currentLoginCallbackConfigurationName) { - currentDatabase = database[currentLoginCallbackConfigurationName]; - currentLoginCallbackConfigurationName = null; - let newBody = actualBody; - if (currentDatabase && currentDatabase.codeVerifier != null) { - newBody = replaceCodeVerifier(newBody, currentDatabase.codeVerifier); - } - return fetch(originalRequest, { - body: newBody, - method: clonedRequest.method, - headers: { - ...serializeHeaders(originalRequest.headers) - }, - mode: clonedRequest.mode, - cache: clonedRequest.cache, - redirect: clonedRequest.redirect, - referrer: clonedRequest.referrer, - credentials: clonedRequest.credentials, - integrity: clonedRequest.integrity - }).then(hideTokens(currentDatabase)); - } - return void 0; - }); - response.then((r) => { - if (r !== void 0) { - resolve(r); - } else { - console.log("success undefined"); - reject(new Error("Response is undefined inside a success")); - } - }).catch((err) => { - if (err !== void 0) { - reject(err); - } else { - console.log("error undefined"); - reject(new Error("Response is undefined inside a error")); - } - }); - }); - event.waitUntil(event.respondWith(maPromesse)); - } -}; -const trustedDomainsShowAccessToken = {}; -const handleMessage = (event) => { - const port = event.ports[0]; - const data = event.data; - const configurationName = data.configurationName; - let currentDatabase = database[configurationName]; - if (trustedDomains == null) { - trustedDomains = {}; - } - if (!currentDatabase) { - if (trustedDomainsShowAccessToken[configurationName] === void 0) { - const trustedDomain = trustedDomains[configurationName]; - trustedDomainsShowAccessToken[configurationName] = Array.isArray(trustedDomain) ? false : trustedDomain.showAccessToken; - } - database[configurationName] = { - tokens: null, - state: null, - codeVerifier: null, - oidcServerConfiguration: null, - oidcConfiguration: void 0, - nonce: null, - status: null, - configurationName, - hideAccessToken: !trustedDomainsShowAccessToken[configurationName] - }; - currentDatabase = database[configurationName]; - if (!trustedDomains[configurationName]) { - trustedDomains[configurationName] = []; - } - } - switch (data.type) { - case "clear": - currentDatabase.tokens = null; - currentDatabase.state = null; - currentDatabase.codeVerifier = null; - currentDatabase.status = data.data.status; - port.postMessage({ configurationName }); - return; - case "init": { - const oidcServerConfiguration = data.data.oidcServerConfiguration; - const trustedDomain = trustedDomains[configurationName]; - const domains = getDomains(trustedDomain, "oidc"); - if (!domains.find((f) => f === acceptAnyDomainToken)) { - [ - oidcServerConfiguration.tokenEndpoint, - oidcServerConfiguration.revocationEndpoint, - oidcServerConfiguration.userInfoEndpoint, - oidcServerConfiguration.issuer - ].forEach((url) => { - checkDomain(domains, url); - }); - } - currentDatabase.oidcServerConfiguration = oidcServerConfiguration; - currentDatabase.oidcConfiguration = data.data.oidcConfiguration; - const where = data.data.where; - if (where === "loginCallbackAsync" || where === "tryKeepExistingSessionAsync") { - currentLoginCallbackConfigurationName = configurationName; - } else { - currentLoginCallbackConfigurationName = null; - } - if (!currentDatabase.tokens) { - port.postMessage({ - tokens: null, - status: currentDatabase.status, - configurationName - }); - } else { - const tokens = { - ...currentDatabase.tokens - }; - if (currentDatabase.hideAccessToken) { - tokens.access_token = TOKEN.ACCESS_TOKEN + "_" + configurationName; - } - if (tokens.refresh_token) { - tokens.refresh_token = TOKEN.REFRESH_TOKEN + "_" + configurationName; - } - if (tokens.idTokenPayload && tokens.idTokenPayload.nonce && currentDatabase.nonce != null) { - tokens.idTokenPayload.nonce = TOKEN.NONCE_TOKEN + "_" + configurationName; - } - port.postMessage({ - tokens, - status: currentDatabase.status, - configurationName - }); - } - return; - } - case "setState": - currentDatabase.state = data.data.state; - port.postMessage({ configurationName }); - return; - case "getState": { - const state = currentDatabase.state; - port.postMessage({ configurationName, state }); - return; - } - case "setCodeVerifier": - currentDatabase.codeVerifier = data.data.codeVerifier; - port.postMessage({ configurationName }); - return; - case "getCodeVerifier": { - port.postMessage({ - configurationName, - codeVerifier: currentDatabase.codeVerifier != null ? TOKEN.CODE_VERIFIER + "_" + configurationName : null - }); - return; - } - case "setSessionState": - currentDatabase.sessionState = data.data.sessionState; - port.postMessage({ configurationName }); - return; - case "getSessionState": { - const sessionState = currentDatabase.sessionState; - port.postMessage({ configurationName, sessionState }); - return; - } - case "setNonce": { - const nonce = data.data.nonce; - if (nonce) { - currentDatabase.nonce = nonce; - } - port.postMessage({ configurationName }); - return; - } - case "getNonce": { - const keyNonce = TOKEN.NONCE_TOKEN + "_" + configurationName; - const nonce = currentDatabase.nonce ? keyNonce : null; - port.postMessage({ configurationName, nonce }); - return; - } - default: - currentDatabase.items = { ...data.data }; - port.postMessage({ configurationName }); - } -}; -_self.addEventListener("install", handleInstall); -_self.addEventListener("activate", handleActivate); -_self.addEventListener("fetch", handleFetch); -_self.addEventListener("message", handleMessage); -//# sourceMappingURL=OidcServiceWorker.js.map diff --git a/packages/oidc-client/public/OidcTrustedDomains.js b/packages/oidc-client/public/OidcTrustedDomains.js index 25851f75e..4447b02f6 100644 --- a/packages/oidc-client/public/OidcTrustedDomains.js +++ b/packages/oidc-client/public/OidcTrustedDomains.js @@ -5,23 +5,26 @@ // Domains used by OIDC server must be also declared here // eslint-disable-next-line @typescript-eslint/no-unused-vars const trustedDomains = { - default: ['https://demo.duendesoftware.com', 'https://kdhttps.auth0.com'], - config_classic: ['https://demo.duendesoftware.com'], - config_without_silent_login: ['https://demo.duendesoftware.com'], - config_without_refresh_token: ['https://demo.duendesoftware.com'], - config_without_refresh_token_silent_login: ['https://demo.duendesoftware.com'], - config_google: ['https://oauth2.googleapis.com', 'https://openidconnect.googleapis.com'], - config_with_hash: ['https://demo.duendesoftware.com'], + default: ['https://demo.duendesoftware.com', 'https://kdhttps.auth0.com'], + config_classic: ['https://demo.duendesoftware.com'], + config_with_monitor_session: ['https://demo.duendesoftware.com'], + config_without_silent_login: ['https://demo.duendesoftware.com'], + config_without_refresh_token: ['https://demo.duendesoftware.com'], + config_without_refresh_token_silent_login: ['https://demo.duendesoftware.com'], + config_google: ['https://oauth2.googleapis.com', 'https://openidconnect.googleapis.com'], + config_with_hash: ['https://demo.duendesoftware.com'], }; // Service worker will continue to give access token to the JavaScript client // Ideal to hide refresh token from client JavaScript, but to retrieve access_token for some // scenarios which require it. For example, to send it via websocket connection. -trustedDomains.config_show_access_token = { domains : ["https://demo.duendesoftware.com"], showAccessToken: true }; - +trustedDomains.config_show_access_token = { + domains: ['https://demo.duendesoftware.com'], + showAccessToken: true, +}; // This example defines domains used by OIDC server separately from domains to which access tokens will be injected. trustedDomains.config_separate_oidc_access_token_domains = { - oidcDomains: ["https://demo.duendesoftware.com"], - accessTokenDomains: ["https://myapi"] -}; \ No newline at end of file + oidcDomains: ['https://demo.duendesoftware.com'], + accessTokenDomains: ['https://myapi'], +}; diff --git a/packages/oidc-client/src/cache.spec.ts b/packages/oidc-client/src/cache.spec.ts new file mode 100644 index 000000000..0dfd827ca --- /dev/null +++ b/packages/oidc-client/src/cache.spec.ts @@ -0,0 +1,118 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +describe('issuer configuration cache', () => { + let cache: typeof import('./cache'); + let storage: Storage; + const configuration = { issuer: 'https://issuer.example.com' }; + + beforeEach(async () => { + vi.resetModules(); + vi.useFakeTimers(); + vi.setSystemTime(10000); + storage = { + length: 0, + clear: vi.fn(), + key: vi.fn(), + removeItem: vi.fn(), + getItem: vi.fn().mockReturnValue(null), + setItem: vi.fn(), + }; + vi.stubGlobal('window', { sessionStorage: storage }); + cache = await import('./cache'); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it('returns null for a missing entry', () => { + expect(cache.getFromCache('issuer', storage, 60)).toBeNull(); + expect(storage.getItem).toHaveBeenCalledExactlyOnceWith('issuer'); + }); + + it('persists the same timestamp and result that are cached in memory', () => { + cache.setCache('issuer', configuration, storage); + + expect(storage.setItem).toHaveBeenCalledExactlyOnceWith( + 'issuer', + '{"result":{"issuer":"https://issuer.example.com"},"timestamp":10000}', + ); + expect(cache.getFromCache('issuer', storage, 60)).toBe(configuration); + expect(storage.getItem).not.toHaveBeenCalled(); + }); + + it('uses session storage when no storage is supplied', () => { + cache.setCache('issuer', configuration); + + expect(storage.setItem).toHaveBeenCalledOnce(); + expect(cache.getFromCache('issuer', undefined, 60)).toBe(configuration); + }); + + it('supports an in-memory cache without storage', () => { + expect(cache.getFromCache('issuer', null, 60)).toBeNull(); + cache.setCache('issuer', configuration, null); + + expect(cache.getFromCache('issuer', null, 60)).toBe(configuration); + expect(storage.setItem).not.toHaveBeenCalled(); + }); + + it('hydrates a persisted entry once and then uses the in-memory value', () => { + vi.mocked(storage.getItem).mockReturnValue( + '{"result":{"issuer":"https://issuer.example.com"},"timestamp":10000}', + ); + + const result = cache.getFromCache('issuer', storage, 60); + expect(result).toEqual(configuration); + expect(cache.getFromCache('issuer', storage, 60)).toBe(result); + expect(storage.getItem).toHaveBeenCalledOnce(); + }); + + it.each([ + [10999, 1, true], + [11000, 1, false], + [11001, 1, false], + [10000, 0, false], + [10000, -1, false], + ])('honors the expiry boundary at %i with a TTL of %i seconds', (now, ttl, isValid) => { + cache.setCache('issuer', configuration, storage); + vi.setSystemTime(now); + + expect(cache.getFromCache('issuer', storage, ttl)).toEqual(isValid ? configuration : null); + }); + + it('keeps issuer entries independent and replaces an existing entry', () => { + const replacement = { issuer: 'https://replacement.example.com' }; + cache.setCache('first', configuration, storage); + cache.setCache('second', replacement, storage); + cache.setCache('first', replacement, storage); + + expect(cache.getFromCache('first', storage, 60)).toBe(replacement); + expect(cache.getFromCache('second', storage, 60)).toBe(replacement); + }); + + it('does not rehydrate an expired in-memory entry', () => { + cache.setCache('issuer', configuration, storage); + vi.setSystemTime(70000); + + expect(cache.getFromCache('issuer', storage, 60)).toBeNull(); + expect(storage.getItem).not.toHaveBeenCalled(); + }); + + it('preserves JSON parsing failures for malformed persisted data', () => { + vi.mocked(storage.getItem).mockReturnValue('{'); + + expect(() => cache.getFromCache('issuer', storage, 60)).toThrow(SyntaxError); + }); + + it('preserves storage write failures while keeping the memory entry', () => { + const error = new Error('Storage unavailable'); + vi.mocked(storage.setItem).mockImplementation(() => { + throw error; + }); + + expect(() => cache.setCache('issuer', configuration, storage)).toThrow(error); + expect(cache.getFromCache('issuer', storage, 60)).toBe(configuration); + }); +}); diff --git a/packages/oidc-client/src/cache.ts b/packages/oidc-client/src/cache.ts index d25aa62d2..c39c087f5 100644 --- a/packages/oidc-client/src/cache.ts +++ b/packages/oidc-client/src/cache.ts @@ -1,26 +1,24 @@ const fetchFromIssuerCache = {}; export const getFromCache = (localStorageKey, storage = window.sessionStorage, timeCacheSecond) => { - if (!fetchFromIssuerCache[localStorageKey]) { - if (storage) { - const cacheJson = storage.getItem(localStorageKey); - if (cacheJson) { - fetchFromIssuerCache[localStorageKey] = JSON.parse(cacheJson); - } - } + if (!fetchFromIssuerCache[localStorageKey] && storage) { + const cacheJson = storage.getItem(localStorageKey); + if (cacheJson) { + fetchFromIssuerCache[localStorageKey] = JSON.parse(cacheJson); } - const oneHourMinisecond = 1000 * timeCacheSecond; - // @ts-ignore - if (fetchFromIssuerCache[localStorageKey] && (fetchFromIssuerCache[localStorageKey].timestamp + oneHourMinisecond) > Date.now()) { - return fetchFromIssuerCache[localStorageKey].result; - } - return null; + } + const cachedEntry = fetchFromIssuerCache[localStorageKey]; + const cacheDurationMilliseconds = 1000 * timeCacheSecond; + if (cachedEntry && cachedEntry.timestamp + cacheDurationMilliseconds > Date.now()) { + return cachedEntry.result; + } + return null; }; export const setCache = (localStorageKey, result, storage = window.sessionStorage) => { - const timestamp = Date.now(); - fetchFromIssuerCache[localStorageKey] = { result, timestamp }; - if (storage) { - storage.setItem(localStorageKey, JSON.stringify({ result, timestamp })); - } + const cachedEntry = { result, timestamp: Date.now() }; + fetchFromIssuerCache[localStorageKey] = cachedEntry; + if (storage) { + storage.setItem(localStorageKey, JSON.stringify(cachedEntry)); + } }; diff --git a/packages/oidc-client/src/checkSession.ts b/packages/oidc-client/src/checkSession.ts index 66124d9e9..4e33a3d7f 100644 --- a/packages/oidc-client/src/checkSession.ts +++ b/packages/oidc-client/src/checkSession.ts @@ -1,60 +1,99 @@ import { CheckSessionIFrame } from './checkSessionIFrame.js'; +import Oidc from './oidc'; import { _silentLoginAsync, SilentLoginResponse } from './silentLogin.js'; import { OidcConfiguration } from './types.js'; -// eslint-disable-next-line @typescript-eslint/ban-types -export const startCheckSessionAsync = (oidc:any, oidcDatabase:any, configuration :OidcConfiguration) => (checkSessionIFrameUri, clientId, sessionState, isSilentSignin = false) => { - const silentLoginAsync = (extras, state = undefined, scope = undefined):Promise => { - return _silentLoginAsync(oidc.configurationName, configuration, oidc.publishEvent.bind(oidc))(extras, state, scope); +export const startCheckSessionAsync = + (oidc: Oidc, oidcDatabase: any, configuration: OidcConfiguration) => + (checkSessionIFrameUri, clientId, sessionState, isSilentSignin = false) => { + const silentLoginAsync = ( + extras, + state = undefined, + scope = undefined, + ): Promise => { + return _silentLoginAsync(oidc.configurationName, configuration, oidc.publishEvent.bind(oidc))( + extras, + state, + scope, + ); }; return new Promise((resolve, reject): void => { - if (configuration.silent_login_uri && configuration.silent_redirect_uri && configuration.monitor_session && checkSessionIFrameUri && sessionState && !isSilentSignin) { - const checkSessionCallback = () => { - oidc.checkSessionIFrame.stop(); - const tokens = oidc.tokens; - if (tokens === null) { - return; + if ( + configuration.silent_login_uri && + configuration.silent_redirect_uri && + configuration.monitor_session && + checkSessionIFrameUri && + sessionState && + !isSilentSignin + ) { + const checkSessionCallback = () => { + oidc.checkSessionIFrame.stop(); + const tokens = oidc.tokens; + if (tokens === null) { + return; + } + const idToken = tokens.idToken; + const idTokenPayload = tokens.idTokenPayload; + return silentLoginAsync({ + prompt: 'none', + id_token_hint: idToken, + scope: configuration.scope || 'openid', + }) + .then(silentSigninResponse => { + if (silentSigninResponse.error) { + throw new Error(silentSigninResponse.error); + } + const iFrameIdTokenPayload = silentSigninResponse.tokens.idTokenPayload; + if (idTokenPayload.sub === iFrameIdTokenPayload.sub) { + const sessionState = silentSigninResponse.sessionState; + oidc.checkSessionIFrame.start(silentSigninResponse.sessionState); + if (idTokenPayload.sid === iFrameIdTokenPayload.sid) { + console.debug( + 'SessionMonitor._callback: Same sub still logged in at OP, restarting check session iframe; session_state:', + sessionState, + ); + } else { + console.debug( + 'SessionMonitor._callback: Same sub still logged in at OP, session state has changed, restarting check session iframe; session_state:', + sessionState, + ); } - const idToken = tokens.idToken; - const idTokenPayload = tokens.idTokenPayload; - return silentLoginAsync({ - prompt: 'none', - id_token_hint: idToken, - scope: configuration.scope || 'openid', - }).then((silentSigninResponse) => { - const iFrameIdTokenPayload = silentSigninResponse.tokens.idTokenPayload; - if (idTokenPayload.sub === iFrameIdTokenPayload.sub) { - const sessionState = silentSigninResponse.sessionState; - oidc.checkSessionIFrame.start(silentSigninResponse.sessionState); - if (idTokenPayload.sid === iFrameIdTokenPayload.sid) { - console.debug('SessionMonitor._callback: Same sub still logged in at OP, restarting check session iframe; session_state:', sessionState); - } else { - console.debug('SessionMonitor._callback: Same sub still logged in at OP, session state has changed, restarting check session iframe; session_state:', sessionState); - } - } else { - console.debug('SessionMonitor._callback: Different subject signed into OP:', iFrameIdTokenPayload.sub); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - }).catch(async (e) => { - console.warn('SessionMonitor._callback: Silent login failed, logging out other tabs:', e); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const [key, oidc] of Object.entries(oidcDatabase)) { - // @ts-ignore - await oidc.logoutOtherTabAsync(configuration.client_id, idTokenPayload.sub); - } - }); - }; - - oidc.checkSessionIFrame = new CheckSessionIFrame(checkSessionCallback, clientId, checkSessionIFrameUri); - oidc.checkSessionIFrame.load().then(() => { - oidc.checkSessionIFrame.start(sessionState); - resolve(oidc.checkSessionIFrame); - }).catch((e) => { - reject(e); + } else { + console.debug( + 'SessionMonitor._callback: Different subject signed into OP:', + iFrameIdTokenPayload.sub, + ); + } + }) + .catch(async e => { + console.warn( + 'SessionMonitor._callback: Silent login failed, logging out other tabs:', + e, + ); + for (const [, oidc] of Object.entries(oidcDatabase)) { + // @ts-ignore + await oidc.logoutOtherTabAsync(configuration.client_id, idTokenPayload.sub); + } }); - } else { - resolve(null); - } + }; + + oidc.checkSessionIFrame = new CheckSessionIFrame( + checkSessionCallback, + clientId, + checkSessionIFrameUri, + ); + oidc.checkSessionIFrame + .load() + .then(() => { + oidc.checkSessionIFrame.start(sessionState); + resolve(oidc.checkSessionIFrame); + }) + .catch(e => { + reject(e); + }); + } else { + resolve(null); + } }); -}; + }; diff --git a/packages/oidc-client/src/checkSessionIFrame.ts b/packages/oidc-client/src/checkSessionIFrame.ts index 247cbb24e..c82191415 100644 --- a/packages/oidc-client/src/checkSessionIFrame.ts +++ b/packages/oidc-client/src/checkSessionIFrame.ts @@ -3,81 +3,82 @@ const DefaultInterval = 2000; const Log = console; export class CheckSessionIFrame { - private readonly _client_id: any; - private readonly _callback: any; - private _url: any; - private readonly _interval: number; - private readonly _stopOnError: boolean; - private readonly _frame_origin: string; - private readonly _frame: HTMLIFrameElement; - private _boundMessageEvent: any; - private _timer: number; - constructor(callback, client_id, url, interval = DefaultInterval, stopOnError = true) { - this._callback = callback; - this._client_id = client_id; - this._url = url; - this._interval = interval || DefaultInterval; - this._stopOnError = stopOnError; - const idx = url.indexOf('/', url.indexOf('//') + 2); - this._frame_origin = url.substr(0, idx); - this._frame = window.document.createElement('iframe'); - this._frame.style.visibility = 'hidden'; - this._frame.style.position = 'absolute'; - this._frame.style.display = 'none'; - // @ts-ignore - this._frame.width = 0; - // @ts-ignore - this._frame.height = 0; + private readonly _client_id: any; + private readonly _callback: any; + private _url: any; + private readonly _interval: number; + private readonly _stopOnError: boolean; + private readonly _frame_origin: string; + private readonly _frame: HTMLIFrameElement; + private _boundMessageEvent: any; + private _timer: number; + constructor(callback, client_id, url, interval = DefaultInterval, stopOnError = true) { + this._callback = callback; + this._client_id = client_id; + this._url = url; + this._interval = interval || DefaultInterval; + this._stopOnError = stopOnError; + const idx = url.indexOf('/', url.indexOf('//') + 2); + this._frame_origin = url.substring(0, idx); + this._frame = window.document.createElement('iframe'); + this._frame.style.visibility = 'hidden'; + this._frame.style.position = 'absolute'; + this._frame.style.display = 'none'; + // @ts-ignore + this._frame.width = 0; + // @ts-ignore + this._frame.height = 0; - this._frame.src = url; - } + this._frame.src = url; + } - load() { - return new Promise((resolve) => { - this._frame.onload = () => { - resolve(); - }; - window.document.body.appendChild(this._frame); - this._boundMessageEvent = this._message.bind(this); - window.addEventListener('message', this._boundMessageEvent, false); - }); - } + load() { + return new Promise(resolve => { + this._frame.onload = () => { + resolve(); + }; + window.document.body.appendChild(this._frame); + this._boundMessageEvent = this._message.bind(this); + window.addEventListener('message', this._boundMessageEvent, false); + }); + } - _message(e) { - if (e.origin === this._frame_origin && - e.source === this._frame.contentWindow - ) { - if (e.data === 'error') { - Log.error('CheckSessionIFrame: error message from check session op iframe'); - if (this._stopOnError) { - this.stop(); - } - } else if (e.data === 'changed') { - Log.debug(e); - Log.debug('CheckSessionIFrame: changed message from check session op iframe'); - this.stop(); - this._callback(); - } else { - Log.debug('CheckSessionIFrame: ' + e.data + ' message from check session op iframe'); - } + _message(e) { + if (e.origin === this._frame_origin && e.source === this._frame.contentWindow) { + if (e.data === 'error') { + Log.error('CheckSessionIFrame: error message from check session op iframe'); + if (this._stopOnError) { + this.stop(); } + } else if (e.data === 'changed') { + Log.debug(e); + Log.debug('CheckSessionIFrame: changed message from check session op iframe'); + this.stop(); + this._callback(); + } else { + Log.debug('CheckSessionIFrame: ' + e.data + ' message from check session op iframe'); + } } + } - start(session_state) { - Log.debug('CheckSessionIFrame.start :' + session_state); - this.stop(); - const send = () => { - this._frame.contentWindow.postMessage(this._client_id + ' ' + session_state, this._frame_origin); - }; - send(); - this._timer = window.setInterval(send, this._interval); - } + start(session_state) { + Log.debug('CheckSessionIFrame.start :' + session_state); + this.stop(); + const send = () => { + this._frame.contentWindow.postMessage( + this._client_id + ' ' + session_state, + this._frame_origin, + ); + }; + send(); + this._timer = window.setInterval(send, this._interval); + } - stop() { - if (this._timer) { - Log.debug('CheckSessionIFrame.stop'); - window.clearInterval(this._timer); - this._timer = null; - } + stop() { + if (this._timer) { + Log.debug('CheckSessionIFrame.stop'); + window.clearInterval(this._timer); + this._timer = null; } + } } diff --git a/packages/oidc-client/src/crypto.spec.ts b/packages/oidc-client/src/crypto.spec.ts new file mode 100644 index 000000000..ad5e4f927 --- /dev/null +++ b/packages/oidc-client/src/crypto.spec.ts @@ -0,0 +1,40 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { generateRandom } from './crypto'; + +describe('generateRandom', () => { + const originalCrypto = Object.getOwnPropertyDescriptor(globalThis, 'crypto'); + + afterEach(() => { + if (originalCrypto) { + Object.defineProperty(globalThis, 'crypto', originalCrypto); + } else { + delete (globalThis as { crypto?: Crypto }).crypto; + } + }); + + it('uses Web Crypto to generate authentication-related random values', () => { + const getRandomValues = vi.fn((buffer: Uint8Array) => { + buffer.fill(1); + return buffer; + }); + Object.defineProperty(globalThis, 'crypto', { + configurable: true, + value: { getRandomValues }, + }); + + expect(generateRandom(4)).toBe('BBBB'); + expect(getRandomValues).toHaveBeenCalledOnce(); + }); + + it('does not fall back to a non-cryptographic random source', () => { + Object.defineProperty(globalThis, 'crypto', { + configurable: true, + value: undefined, + }); + + expect(() => generateRandom(16)).toThrow( + 'Web Crypto API is unavailable; secure random values cannot be generated.', + ); + }); +}); diff --git a/packages/oidc-client/src/crypto.ts b/packages/oidc-client/src/crypto.ts index e99391035..a7e7b057d 100644 --- a/packages/oidc-client/src/crypto.ts +++ b/packages/oidc-client/src/crypto.ts @@ -1,8 +1,11 @@ -import * as base64 from 'base64-js'; +import { uint8ToUrlBase64 } from './jwt'; + +const getWebCrypto = () => (typeof globalThis !== 'undefined' ? globalThis.crypto : undefined); const cryptoInfo = () => { - const hasCrypto = typeof window !== 'undefined' && !!(window.crypto as any); - const hasSubtleCrypto = hasCrypto && !!(window.crypto.subtle as any); + const webCrypto = getWebCrypto(); + const hasCrypto = !!webCrypto; + const hasSubtleCrypto = !!webCrypto?.subtle; return { hasCrypto, hasSubtleCrypto }; }; const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; @@ -16,24 +19,15 @@ const bufferToString = (buffer: Uint8Array) => { return state.join(''); }; -const urlSafe = (buffer: Uint8Array): string => { - const encoded = base64.fromByteArray(new Uint8Array(buffer)); - return encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); -}; - export const generateRandom = (size: number) => { - const buffer = new Uint8Array(size); - const { hasCrypto } = cryptoInfo(); - if (hasCrypto) { - window.crypto.getRandomValues(buffer); - } else { - // fall back to Math.random() if nothing else is available - for (let i = 0; i < size; i += 1) { - buffer[i] = (Math.random() * charset.length) | 0; - } - } - return bufferToString(buffer); - }; + const buffer = new Uint8Array(size); + const webCrypto = getWebCrypto(); + if (!webCrypto?.getRandomValues) { + throw new Error('Web Crypto API is unavailable; secure random values cannot be generated.'); + } + webCrypto.getRandomValues(buffer); + return bufferToString(buffer); +}; export function textEncodeLite(str: string) { const buf = new ArrayBuffer(str.length); @@ -44,18 +38,26 @@ export function textEncodeLite(str: string) { } return bufView; } - export const deriveChallengeAsync = (code: string): Promise => { - if (code.length < 43 || code.length > 128) { - return Promise.reject(new Error('Invalid code length.')); - } - const { hasSubtleCrypto } = cryptoInfo(); - if (!hasSubtleCrypto) { - return Promise.reject(new Error('window.crypto.subtle is unavailable.')); - } - - return new Promise((resolve, reject) => { - crypto.subtle.digest('SHA-256', textEncodeLite(code)).then(buffer => { - return resolve(urlSafe(new Uint8Array(buffer))); - }, error => reject(error)); - }); + +export function base64urlOfHashOfASCIIEncodingAsync(code: string): Promise { + return new Promise((resolve, reject) => { + crypto.subtle.digest('SHA-256', textEncodeLite(code)).then( + buffer => { + return resolve(uint8ToUrlBase64(new Uint8Array(buffer))); + }, + error => reject(error), + ); + }); +} + +export const deriveChallengeAsync = (code: string): Promise => { + if (code.length < 43 || code.length > 128) { + return Promise.reject(new Error('Invalid code length.')); + } + const { hasSubtleCrypto } = cryptoInfo(); + if (!hasSubtleCrypto) { + return Promise.reject(new Error('window.crypto.subtle is unavailable.')); + } + + return base64urlOfHashOfASCIIEncodingAsync(code); }; diff --git a/packages/oidc-client/src/events.ts b/packages/oidc-client/src/events.ts index ac2690113..bd93e3d17 100644 --- a/packages/oidc-client/src/events.ts +++ b/packages/oidc-client/src/events.ts @@ -1,28 +1,36 @@ export const eventNames = { - service_worker_not_supported_by_browser: 'service_worker_not_supported_by_browser', - token_aquired: 'token_aquired', - logout_from_another_tab: 'logout_from_another_tab', - logout_from_same_tab: 'logout_from_same_tab', - token_renewed: 'token_renewed', - token_timer: 'token_timer', - loginAsync_begin: 'loginAsync_begin', - loginAsync_error: 'loginAsync_error', - loginCallbackAsync_begin: 'loginCallbackAsync_begin', - loginCallbackAsync_end: 'loginCallbackAsync_end', - loginCallbackAsync_error: 'loginCallbackAsync_error', - refreshTokensAsync_begin: 'refreshTokensAsync_begin', - refreshTokensAsync: 'refreshTokensAsync', - refreshTokensAsync_end: 'refreshTokensAsync_end', - refreshTokensAsync_error: 'refreshTokensAsync_error', - refreshTokensAsync_silent_error: 'refreshTokensAsync_silent_error', - tryKeepExistingSessionAsync_begin: 'tryKeepExistingSessionAsync_begin', - tryKeepExistingSessionAsync_end: 'tryKeepExistingSessionAsync_end', - tryKeepExistingSessionAsync_error: 'tryKeepExistingSessionAsync_error', - silentLoginAsync_begin: 'silentLoginAsync_begin', - silentLoginAsync: 'silentLoginAsync', - silentLoginAsync_end: 'silentLoginAsync_end', - silentLoginAsync_error: 'silentLoginAsync_error', - syncTokensAsync_begin: 'syncTokensAsync_begin', - syncTokensAsync_end: 'syncTokensAsync_end', - syncTokensAsync_error: 'syncTokensAsync_error', + service_worker_not_supported_by_browser: 'service_worker_not_supported_by_browser', + token_acquired: 'token_acquired', + logout_from_another_tab: 'logout_from_another_tab', + logout_from_same_tab: 'logout_from_same_tab', + token_renewed: 'token_renewed', + token_timer: 'token_timer', + loginAsync_begin: 'loginAsync_begin', + loginAsync_error: 'loginAsync_error', + loginCallbackAsync_begin: 'loginCallbackAsync_begin', + loginCallbackAsync_end: 'loginCallbackAsync_end', + loginCallbackAsync_error: 'loginCallbackAsync_error', + loginCallbackAsync_navigated: 'loginCallbackAsync_navigated', + loginCallbackAsync_navigation_error: 'loginCallbackAsync_navigation_error', + logoutAsync_error: 'logoutAsync_error', + refreshTokensAsync_begin: 'refreshTokensAsync_begin', + refreshTokensAsync: 'refreshTokensAsync', + refreshTokensAsync_end: 'refreshTokensAsync_end', + refreshTokensAsync_error: 'refreshTokensAsync_error', + refreshTokensAsync_silent_error: 'refreshTokensAsync_silent_error', + tryKeepExistingSessionAsync_begin: 'tryKeepExistingSessionAsync_begin', + tryKeepExistingSessionAsync_end: 'tryKeepExistingSessionAsync_end', + tryKeepExistingSessionAsync_error: 'tryKeepExistingSessionAsync_error', + silentLoginAsync_begin: 'silentLoginAsync_begin', + silentLoginAsync: 'silentLoginAsync', + silentLoginAsync_end: 'silentLoginAsync_end', + silentLoginAsync_error: 'silentLoginAsync_error', + userInfoAsync_error: 'userInfoAsync_error', + apiRequest_error: 'apiRequest_error', + syncTokensAsync_begin: 'syncTokensAsync_begin', + syncTokensAsync_lock_not_available: 'syncTokensAsync_lock_not_available', + syncTokensAsync_end: 'syncTokensAsync_end', + syncTokensAsync_error: 'syncTokensAsync_error', + tokensInvalidAndWaitingActionsToRefresh: 'tokensInvalidAndWaitingActionsToRefresh', + loadingTimeout_error: 'loadingTimeout_error', }; diff --git a/packages/oidc-client/src/fetch.spec.ts b/packages/oidc-client/src/fetch.spec.ts new file mode 100644 index 000000000..dd6c9c68b --- /dev/null +++ b/packages/oidc-client/src/fetch.spec.ts @@ -0,0 +1,105 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { fetchWithTokens } from './fetch'; +import { OidcError, OidcErrorCode } from './oidcError'; +import { Tokens } from './parseTokens'; +import { userInfoAsync } from './user'; + +const buildOidc = () => { + const tokens = { + accessToken: 'access-token', + expiresAt: Date.now() / 1000 + 3600, + issuedAt: Date.now() / 1000, + } as Tokens; + const storage = { + 'oidc.default': JSON.stringify({ tokens }), + } as unknown as Storage; + const events: Array<{ name: string; data: unknown }> = []; + const oidc: any = { + configuration: { + authority: 'https://issuer.example.com', + client_id: 'client', + redirect_uri: 'https://client.example.com/callback', + scope: 'openid', + storage, + token_automatic_renew_mode: 'AutomaticBeforeTokenExpiration', + refresh_time_before_tokens_expiration_in_second: 30, + }, + configurationName: 'default', + initAsync: vi.fn(async () => ({ + userInfoEndpoint: 'https://issuer.example.com/userinfo', + })), + publishEvent: (name: string, data: unknown) => events.push({ name, data }), + renewTokensAsync: vi.fn(), + tokens, + userInfo: null, + }; + return { events, oidc }; +}; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('fetchWithTokens typed errors', () => { + it('returns a DPoP challenge response unchanged and publishes an api_request error', async () => { + const { events, oidc } = buildOidc(); + const response = new Response('', { + status: 401, + headers: { + 'DPoP-Nonce': 'nonce', + 'WWW-Authenticate': 'DPoP error="use_dpop_nonce"', + }, + }); + const fetchMock = vi.fn(async () => response); + + const result = await fetchWithTokens( + fetchMock as any, + oidc, + )('https://api.example.com/resource'); + + expect(result).toBe(response); + expect(events.find(event => event.name === 'apiRequest_error')?.data).toMatchObject({ + code: OidcErrorCode.DPOP_NONCE_REQUIRED, + phase: 'api_request', + retryable: true, + status: 401, + }); + }); + + it('wraps a rejected fetch as NETWORK_ERROR', async () => { + const { events, oidc } = buildOidc(); + const cause = new TypeError('Failed to fetch'); + const fetchMock = vi.fn(async () => { + throw cause; + }); + + await expect( + fetchWithTokens(fetchMock as any, oidc)('https://api.example.com/resource'), + ).rejects.toMatchObject({ + code: OidcErrorCode.NETWORK_ERROR, + phase: 'api_request', + retryable: true, + cause, + }); + expect(events.find(event => event.name === 'apiRequest_error')?.data).toBeInstanceOf(OidcError); + }); +}); + +describe('userInfoAsync typed errors', () => { + it('keeps returning null on HTTP failure and publishes a userinfo error', async () => { + const { events, oidc } = buildOidc(); + vi.stubGlobal( + 'fetch', + vi.fn(async () => new Response('', { status: 503 })), + ); + + await expect(userInfoAsync(oidc)(true)).resolves.toBeNull(); + expect(events.find(event => event.name === 'userInfoAsync_error')?.data).toMatchObject({ + code: OidcErrorCode.REQUEST_FAILED, + phase: 'userinfo', + retryable: true, + status: 503, + }); + }); +}); diff --git a/packages/oidc-client/src/fetch.ts b/packages/oidc-client/src/fetch.ts new file mode 100644 index 000000000..b8007e0b4 --- /dev/null +++ b/packages/oidc-client/src/fetch.ts @@ -0,0 +1,102 @@ +import { eventNames } from './events'; +import Oidc from './oidc'; +import { createNetworkError, OidcError, OidcErrorCode, OidcErrorPhase } from './oidcError'; +import { getValidTokenAsync, OidcToken } from './parseTokens'; +import { syncTokensInfoAsync } from './renewTokens'; +import { Fetch } from './types'; + +// @ts-ignore +export const fetchWithTokens = + ( + fetch: Fetch, + oidc: Oidc, + demonstrating_proof_of_possession: boolean = false, + phase: OidcErrorPhase = 'api_request', + ): Fetch => + async (...params: Parameters): Promise => { + const [url, options, ...rest] = params; + const optionTmp = options ? { ...options } : { method: 'GET' }; + let headers = new Headers(); + if (optionTmp.headers) { + headers = !(optionTmp.headers instanceof Headers) + ? new Headers(optionTmp.headers) + : optionTmp.headers; + } + const oidcToken: OidcToken = { + getTokens: () => oidc.tokens, + configuration: { + token_automatic_renew_mode: oidc.configuration.token_automatic_renew_mode, + refresh_time_before_tokens_expiration_in_second: + oidc.configuration.refresh_time_before_tokens_expiration_in_second, + }, + + syncTokensInfoAsync: async () => { + const { status } = await syncTokensInfoAsync(oidc)( + oidc.configuration, + oidc.configurationName, + oidc.tokens, + false, + ); + return status; + }, + renewTokensAsync: oidc.renewTokensAsync.bind(oidc), + }; + + // @ts-ignore + const getValidToken = await getValidTokenAsync(oidcToken); + const accessToken = getValidToken?.tokens?.accessToken; + if (!headers.has('Accept')) { + headers.set('Accept', 'application/json'); + } + if (accessToken) { + if ( + oidc.configuration.demonstrating_proof_of_possession && + demonstrating_proof_of_possession + ) { + const demonstrationOdProofOfPossession = + await oidc.generateDemonstrationOfProofOfPossessionAsync( + accessToken, + url.toString(), + optionTmp.method, + ); + headers.set('Authorization', `DPoP ${accessToken}`); + headers.set('DPoP', demonstrationOdProofOfPossession); + } else { + headers.set('Authorization', `Bearer ${accessToken}`); + } + if (!optionTmp.credentials) { + optionTmp.credentials = 'same-origin'; + } + } + const newOptions = { ...optionTmp, headers }; + let response: Response; + try { + response = await fetch(url, newOptions, ...rest); + } catch (cause) { + const error = createNetworkError(cause, phase); + if (phase === 'api_request') { + oidc.publishEvent(eventNames.apiRequest_error, error); + } + throw error; + } + + if ( + !response.ok && + response.headers?.has('DPoP-Nonce') && + (response.status === 400 || + response.status === 401 || + response.headers.get('WWW-Authenticate')?.includes('use_dpop_nonce')) + ) { + const error = new OidcError(OidcErrorCode.DPOP_NONCE_REQUIRED, 'DPoP nonce required', { + phase, + retryable: true, + status: response.status, + oauthError: 'use_dpop_nonce', + }); + if (phase === 'api_request') { + oidc.publishEvent(eventNames.apiRequest_error, error); + } + } + + return response; + }; diff --git a/packages/oidc-client/src/index.ts b/packages/oidc-client/src/index.ts index 8e6bbd857..3ed93e966 100644 --- a/packages/oidc-client/src/index.ts +++ b/packages/oidc-client/src/index.ts @@ -1,10 +1,44 @@ +export type { ServiceWorkerSignalMessage, ServiceWorkerSignalOptions } from './initWorker.js'; +export { signalServiceWorkerAsync } from './initWorker.js'; +export type { ILOidcLocation } from './location.js'; +export { OidcLocation } from './location.js'; export { getFetchDefault } from './oidc.js'; +export type { OidcUserInfo } from './oidcClient.js'; +export { OidcClient } from './oidcClient.js'; +export type { OidcErrorOptions, OidcErrorPhase } from './oidcError.js'; +export { isOidcError, OidcError, OidcErrorCode } from './oidcError.js'; +export { isOidcStateError, OidcStateError, OidcStateErrorCode } from './oidcStateError.js'; +export type { Tokens } from './parseTokens.js'; export { TokenRenewMode } from './parseTokens.js'; +export type { + ServiceWorkerMessage, + ServiceWorkerMessageTypeKey, + ServiceWorkerMessageTypeValue, + ServiceWorkerResponse, +} from './protocol.js'; +export { + buildDpopSecuredPlaceholder, + buildSecuredTokenPlaceholder, + buildStorageKey, + DPOP_TOKEN_PLACEHOLDER_PREFIX, + isServiceWorkerMessageType, + PROTOCOL_VERSION, + ServiceWorkerMessageType, + STORAGE_KEY_PREFIX, + SW_CONTROLLER_CHANGE_RELOAD_COUNT_KEY, + TOKEN_PLACEHOLDERS, +} from './protocol.js'; +export { + isPushedAuthorizationRequestError, + PushedAuthorizationRequestError, + PushedAuthorizationRequestErrorCode, +} from './pushedAuthorizationRequestError.js'; export { getParseQueryStringFromLocation, getPath } from './route-utils'; export type { AuthorityConfiguration, Fetch, OidcConfiguration, + PushedAuthorizationRequestMode, StringMap, } from './types.js'; -export { type OidcUserInfo, VanillaOidc } from './vanillaOidc.js'; +export { TokenAutomaticRenewMode } from './types.js'; diff --git a/packages/oidc-client/src/iniWorker.spec.ts b/packages/oidc-client/src/iniWorker.spec.ts index a9b00b2a2..ac21fccb6 100644 --- a/packages/oidc-client/src/iniWorker.spec.ts +++ b/packages/oidc-client/src/iniWorker.spec.ts @@ -1,21 +1,31 @@ -import { excludeOs, getOperatingSystem } from './initWorker'; +import { describe, expect, it } from 'vitest'; -import { describe, it, expect } from 'vitest'; +import { excludeOs, getOperatingSystem } from './initWorkerOption'; describe('initWorker test Suite', () => { + it.each([ + [ + 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1', + 'iOS', + '12.1.0', + ], + [ + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/69.0.3497.105 Mobile/15E148 Safari/605.1', + 'Mac OS X', + '10_15_6', + ], + ])( + 'getOperatingSystem should return OS for Version', + (userAgent, expectedOs, expectedVersion) => { + const operatingSystem = getOperatingSystem({ + userAgent, + appVersion: 'OS ' + expectedVersion.replaceAll('.', '_'), + }); + expect(expectedOs).toBe(operatingSystem.os); + expect(expectedVersion).toBe(operatingSystem.osVersion); - it.each([['Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1', 'iOS', '12.1.0'], - ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/69.0.3497.105 Mobile/15E148 Safari/605.1', 'Mac OS X', '10_15_6'], - ])( - 'getOperatingSystem should return OS for Version', - (userAgent, expectedOs, expectedVersion) => { - const operatingSystem = getOperatingSystem({ userAgent, appVersion: 'OS ' + expectedVersion.replaceAll('.', '_') }); - expect(expectedOs).toBe(operatingSystem.os); - expect(expectedVersion).toBe(operatingSystem.osVersion); - - const isExcluded = excludeOs(operatingSystem); - expect(isExcluded).toBe(true); - }, - ); - -}); \ No newline at end of file + const isExcluded = excludeOs(operatingSystem); + expect(isExcluded).toBe(true); + }, + ); +}); diff --git a/packages/oidc-client/src/initSession.spec.ts b/packages/oidc-client/src/initSession.spec.ts new file mode 100644 index 000000000..b4e5db0f2 --- /dev/null +++ b/packages/oidc-client/src/initSession.spec.ts @@ -0,0 +1,280 @@ +import { beforeEach, describe, expect, it } from 'vitest'; + +import { initSession } from './initSession'; + +const makeStorage = (): Storage => { + const store: Record = {}; + return { + getItem: (key: string) => store[key] ?? null, + setItem: (key: string, value: string) => { + store[key] = value; + }, + removeItem: (key: string) => { + delete store[key]; + }, + clear: () => { + for (const key of Object.keys(store)) { + delete store[key]; + } + }, + get length() { + return Object.keys(store).length; + }, + key: (index: number) => Object.keys(store)[index] ?? null, + [Symbol.iterator]: function* () { + yield* Object.entries(store); + }, + } as unknown as Storage; +}; + +describe('initSession', () => { + const configName = 'default'; + + describe('single storage (existing behaviour)', () => { + let storage: Storage; + let session: ReturnType; + + beforeEach(() => { + storage = makeStorage(); + session = initSession(configName, storage); + }); + + it('stores tokens in storage', () => { + session.setTokens({ accessToken: 'at', idToken: 'id' }); + expect(storage[`oidc.${configName}`]).toContain('accessToken'); + }); + + it('stores login params in same storage', () => { + session.setLoginParams({ callbackPath: '/callback', extras: null, scope: 'openid' }); + expect(storage[`oidc.login.${configName}`]).toBeTruthy(); + }); + + it('stores state in same storage', async () => { + await session.setStateAsync('abc123'); + expect(storage[`oidc.state.${configName}`]).toBe('abc123'); + }); + + it('stores code verifier in same storage', async () => { + await session.setCodeVerifierAsync('verifier'); + expect(storage[`oidc.code_verifier.${configName}`]).toBe('verifier'); + }); + + it('stores nonce in same storage', async () => { + await session.setNonceAsync({ nonce: 'nonce-value' }); + expect(storage[`oidc.nonce.${configName}`]).toBe('nonce-value'); + }); + + it('clearAsync nulls the tokens entry', async () => { + session.setTokens({ accessToken: 'at' }); + await session.clearAsync('LOGGED_OUT'); + const stored = JSON.parse(storage[`oidc.${configName}`]); + expect(stored.tokens).toBeNull(); + }); + }); + + describe('dual storage — login state in separate storage', () => { + let tokenStorage: Storage; + let loginStateStorage: Storage; + let session: ReturnType; + + beforeEach(() => { + tokenStorage = makeStorage(); + loginStateStorage = makeStorage(); + session = initSession(configName, tokenStorage, loginStateStorage); + }); + + it('stores tokens in tokenStorage, not loginStateStorage', () => { + session.setTokens({ accessToken: 'at', idToken: 'id' }); + expect(tokenStorage[`oidc.${configName}`]).toContain('accessToken'); + expect(loginStateStorage[`oidc.${configName}`]).toBeUndefined(); + }); + + it('stores login params in loginStateStorage, not tokenStorage', () => { + session.setLoginParams({ callbackPath: '/callback', extras: null, scope: 'openid' }); + expect(loginStateStorage[`oidc.login.${configName}`]).toBeTruthy(); + expect(tokenStorage[`oidc.login.${configName}`]).toBeUndefined(); + }); + + it('stores state in loginStateStorage, not tokenStorage', async () => { + await session.setStateAsync('abc123'); + expect(loginStateStorage[`oidc.state.${configName}`]).toBe('abc123'); + expect(tokenStorage[`oidc.state.${configName}`]).toBeUndefined(); + }); + + it('retrieves state from loginStateStorage', async () => { + await session.setStateAsync('state-value'); + const retrieved = await session.getStateAsync(); + expect(retrieved).toBe('state-value'); + }); + + it('stores code verifier in loginStateStorage, not tokenStorage', async () => { + await session.setCodeVerifierAsync('verifier'); + expect(loginStateStorage[`oidc.code_verifier.${configName}`]).toBe('verifier'); + expect(tokenStorage[`oidc.code_verifier.${configName}`]).toBeUndefined(); + }); + + it('retrieves code verifier from loginStateStorage', async () => { + await session.setCodeVerifierAsync('cv-value'); + const retrieved = await session.getCodeVerifierAsync(); + expect(retrieved).toBe('cv-value'); + }); + + it('stores nonce in loginStateStorage, not tokenStorage', async () => { + await session.setNonceAsync({ nonce: 'nonce-value' }); + expect(loginStateStorage[`oidc.nonce.${configName}`]).toBe('nonce-value'); + expect(tokenStorage[`oidc.nonce.${configName}`]).toBeUndefined(); + }); + + it('retrieves nonce from loginStateStorage', async () => { + await session.setNonceAsync({ nonce: 'nonce-value' }); + const { nonce } = await session.getNonceAsync(); + expect(nonce).toBe('nonce-value'); + }); + + it('stores session_state in tokenStorage, not loginStateStorage', async () => { + await session.setSessionStateAsync('ss-value'); + expect(tokenStorage[`oidc.session_state.${configName}`]).toBe('ss-value'); + expect(loginStateStorage[`oidc.session_state.${configName}`]).toBeUndefined(); + }); + + it('clearAsync nulls tokens in tokenStorage', async () => { + session.setTokens({ accessToken: 'at' }); + await session.clearAsync('LOGGED_OUT'); + const stored = JSON.parse(tokenStorage[`oidc.${configName}`]); + expect(stored.tokens).toBeNull(); + }); + + it('clearAsync removes login state keys from loginStateStorage', async () => { + session.setLoginParams({ callbackPath: '/callback', extras: null, scope: 'openid' }); + await session.setStateAsync('abc'); + await session.setCodeVerifierAsync('verifier'); + await session.setNonceAsync({ nonce: 'n' }); + + await session.clearAsync('LOGGED_OUT'); + + expect(loginStateStorage[`oidc.login.${configName}`]).toBeUndefined(); + expect(loginStateStorage[`oidc.state.${configName}`]).toBeUndefined(); + expect(loginStateStorage[`oidc.code_verifier.${configName}`]).toBeUndefined(); + expect(loginStateStorage[`oidc.nonce.${configName}`]).toBeUndefined(); + }); + + it('clearAsync does not remove login state from tokenStorage when storages differ', async () => { + tokenStorage[`oidc.login.${configName}`] = 'should-not-be-touched'; + await session.clearAsync('LOGGED_OUT'); + expect(tokenStorage[`oidc.login.${configName}`]).toBe('should-not-be-touched'); + }); + }); + + describe('undefined/null guards (regression: #871 / #1257 / #1274)', () => { + let storage: Storage; + let session: ReturnType; + + beforeEach(() => { + storage = makeStorage(); + session = initSession(configName, storage); + }); + + it('setLoginParams(undefined) does not poison storage with the literal "undefined"', () => { + session.setLoginParams(undefined); + expect(storage[`oidc.login.${configName}`]).toBeUndefined(); + expect(() => session.getLoginParams()).not.toThrow(); + expect(session.getLoginParams()).toBeNull(); + }); + + it('setLoginParams(null) clears storage rather than writing "null"', () => { + session.setLoginParams({ callbackPath: '/cb', extras: null, scope: 'openid' }); + session.setLoginParams(null); + expect(storage[`oidc.login.${configName}`]).toBeUndefined(); + expect(session.getLoginParams()).toBeNull(); + }); + + it('getLoginParams tolerates a pre-existing poisoned "undefined" value', () => { + // simulate storage poisoned by an older version of the library + storage[`oidc.login.${configName}`] = 'undefined'; + expect(() => session.getLoginParams()).not.toThrow(); + expect(session.getLoginParams()).toBeNull(); + }); + + it('getLoginParams tolerates a pre-existing poisoned "null" value', () => { + storage[`oidc.login.${configName}`] = 'null'; + expect(() => session.getLoginParams()).not.toThrow(); + expect(session.getLoginParams()).toBeNull(); + }); + + it('initAsync tolerates a pre-existing poisoned "undefined" tokens entry', async () => { + storage[`oidc.${configName}`] = 'undefined'; + await expect(session.initAsync()).resolves.toEqual({ tokens: null, status: null }); + }); + + it('getTokens returns null for a pre-existing poisoned tokens entry', () => { + storage[`oidc.${configName}`] = 'undefined'; + expect(() => session.getTokens()).not.toThrow(); + expect(session.getTokens()).toBeNull(); + }); + + it('setNonceAsync({nonce: undefined}) does not poison storage', async () => { + await session.setNonceAsync({ nonce: undefined as unknown as string }); + expect(storage[`oidc.nonce.${configName}`]).toBeUndefined(); + const { nonce } = await session.getNonceAsync(); + expect(nonce).toBeUndefined(); + }); + + it('setStateAsync(undefined) does not poison storage', async () => { + await session.setStateAsync(undefined as unknown as string); + expect(storage[`oidc.state.${configName}`]).toBeUndefined(); + }); + + it('setCodeVerifierAsync(undefined) does not poison storage', async () => { + await session.setCodeVerifierAsync(undefined); + expect(storage[`oidc.code_verifier.${configName}`]).toBeUndefined(); + }); + }); + + describe('two-tab isolation', () => { + it('two sessions sharing tokenStorage but with independent loginStateStorages do not overwrite each other', async () => { + const sharedTokenStorage = makeStorage(); + const tab1LoginStorage = makeStorage(); + const tab2LoginStorage = makeStorage(); + + const tab1 = initSession(configName, sharedTokenStorage, tab1LoginStorage); + const tab2 = initSession(configName, sharedTokenStorage, tab2LoginStorage); + + await tab1.setStateAsync('state-tab1'); + await tab2.setStateAsync('state-tab2'); + + expect(await tab1.getStateAsync()).toBe('state-tab1'); + expect(await tab2.getStateAsync()).toBe('state-tab2'); + }); + + it('two sessions sharing tokenStorage but with independent loginStateStorages have isolated nonces', async () => { + const sharedTokenStorage = makeStorage(); + const tab1LoginStorage = makeStorage(); + const tab2LoginStorage = makeStorage(); + + const tab1 = initSession(configName, sharedTokenStorage, tab1LoginStorage); + const tab2 = initSession(configName, sharedTokenStorage, tab2LoginStorage); + + await tab1.setNonceAsync({ nonce: 'nonce-tab1' }); + await tab2.setNonceAsync({ nonce: 'nonce-tab2' }); + + const { nonce: nonce1 } = await tab1.getNonceAsync(); + const { nonce: nonce2 } = await tab2.getNonceAsync(); + expect(nonce1).toBe('nonce-tab1'); + expect(nonce2).toBe('nonce-tab2'); + }); + + it('token updates from one tab are visible to the other via shared tokenStorage', async () => { + const sharedTokenStorage = makeStorage(); + + const tab1 = initSession(configName, sharedTokenStorage, makeStorage()); + const tab2 = initSession(configName, sharedTokenStorage, makeStorage()); + + tab1.setTokens({ accessToken: 'new-at', idToken: 'new-id' }); + + const tokensJson = tab2.getTokens(); + expect(tokensJson).not.toBeNull(); + const parsed = JSON.parse(tokensJson!); + expect(parsed.tokens.accessToken).toBe('new-at'); + }); + }); +}); diff --git a/packages/oidc-client/src/initSession.ts b/packages/oidc-client/src/initSession.ts index e3b96efb0..e8438d6cd 100644 --- a/packages/oidc-client/src/initSession.ts +++ b/packages/oidc-client/src/initSession.ts @@ -1,89 +1,176 @@ -export const initSession = (configurationName, storage = sessionStorage) => { - const clearAsync = (status) => { - storage[`oidc.${configurationName}`] = JSON.stringify({ tokens: null, status }); - return Promise.resolve(); - }; - - const initAsync = async () => { - if (!storage[`oidc.${configurationName}`]) { - storage[`oidc.${configurationName}`] = JSON.stringify({ tokens: null, status: null }); - return { tokens: null, status: null }; - } - const data = JSON.parse(storage[`oidc.${configurationName}`]); - return Promise.resolve({ tokens: data.tokens, status: data.status }); - }; - - const setTokens = (tokens) => { - storage[`oidc.${configurationName}`] = JSON.stringify({ tokens }); - }; - - const setSessionStateAsync = async (sessionState) => { - storage[`oidc.session_state.${configurationName}`] = sessionState; - }; - - const getSessionStateAsync = async () => { - return storage[`oidc.session_state.${configurationName}`]; - }; - - const setNonceAsync = (nonce) => { - localStorage[`oidc.nonce.${configurationName}`] = nonce.nonce; - }; - - const getNonceAsync = async () => { - // @ts-ignore - return { nonce: localStorage[`oidc.nonce.${configurationName}`] }; - }; - - const getTokens = () => { - if (!storage[`oidc.${configurationName}`]) { - return null; - } - return JSON.stringify({ tokens: JSON.parse(storage[`oidc.${configurationName}`]).tokens }); - }; - - let getLoginParamsCache = null; - const setLoginParams = (configurationName:string, data) => { - getLoginParamsCache = data; - storage[`oidc.login.${configurationName}`] = JSON.stringify(data); - }; - const getLoginParams = (configurationName) => { - const dataString = storage[`oidc.login.${configurationName}`]; - if (!getLoginParamsCache) { - getLoginParamsCache = JSON.parse(dataString); - } - return getLoginParamsCache; - }; - - const getStateAsync = async () => { - return storage[`oidc.state.${configurationName}`]; - }; - - const setStateAsync = async (state:string) => { - storage[`oidc.state.${configurationName}`] = state; - }; - - const getCodeVerifierAsync = async () => { - return storage[`oidc.code_verifier.${configurationName}`]; - }; - - const setCodeVerifierAsync = async (codeVerifier) => { - storage[`oidc.code_verifier.${configurationName}`] = codeVerifier; - }; - - return { - clearAsync, - initAsync, - setTokens, - getTokens, - setSessionStateAsync, - getSessionStateAsync, - setNonceAsync, - getNonceAsync, - setLoginParams, - getLoginParams, - getStateAsync, - setStateAsync, - getCodeVerifierAsync, - setCodeVerifierAsync, - }; +// Guarded writes to storage. Assigning `undefined` or `null` through bracket +// notation (or `setItem`) coerces the value to the literal strings +// `"undefined"` / `"null"`, which then poison the next `JSON.parse` read. +// See https://github.com/AxaFrance/oidc-client/issues/1257 (and #871, #1274). +const writeJson = (storage: Storage, key: string, value: unknown) => { + if (value === undefined || value === null) { + delete storage[key]; + return; + } + storage[key] = JSON.stringify(value); +}; + +const writeRaw = (storage: Storage, key: string, value: string | null | undefined) => { + if (value === undefined || value === null) { + delete storage[key]; + return; + } + storage[key] = value; +}; + +const parseJsonOrNull = (raw: unknown): T | null => { + if (typeof raw !== 'string') { + return null; + } + // Defence in depth against pre-existing poisoned values written by older + // versions of this library before the setter guards above were in place. + if (raw === 'undefined' || raw === 'null' || raw === '') { + return null; + } + try { + return JSON.parse(raw) as T; + } catch { + return null; + } +}; + +export const initSession = ( + configurationName, + storage = sessionStorage, + loginStateStorage?: Storage, +) => { + const loginStorage = loginStateStorage ?? storage; + + const clearAsync = status => { + writeJson(storage, `oidc.${configurationName}`, { tokens: null, status }); + delete storage[`oidc.${configurationName}.userInfo`]; + if (loginStateStorage && loginStateStorage !== storage) { + delete loginStorage[`oidc.login.${configurationName}`]; + delete loginStorage[`oidc.state.${configurationName}`]; + delete loginStorage[`oidc.code_verifier.${configurationName}`]; + delete loginStorage[`oidc.nonce.${configurationName}`]; + } + return Promise.resolve(); + }; + + const initAsync = async () => { + const existing = parseJsonOrNull(storage[`oidc.${configurationName}`]) as { + tokens: any; + status: any; + } | null; + if (!existing) { + writeJson(storage, `oidc.${configurationName}`, { tokens: null, status: null }); + return { tokens: null, status: null }; + } + return Promise.resolve({ tokens: existing.tokens, status: existing.status }); + }; + + const setTokens = tokens => { + writeJson(storage, `oidc.${configurationName}`, { tokens }); + }; + + const setSessionStateAsync = async sessionState => { + writeRaw(storage, `oidc.session_state.${configurationName}`, sessionState); + }; + + const getSessionStateAsync = async () => { + return storage[`oidc.session_state.${configurationName}`]; + }; + + const setNonceAsync = nonce => { + writeRaw(loginStorage, `oidc.nonce.${configurationName}`, nonce?.nonce); + }; + + const setDemonstratingProofOfPossessionJwkAsync = (jwk: JsonWebKey) => { + writeJson(storage, `oidc.jwk.${configurationName}`, jwk); + }; + + const getDemonstratingProofOfPossessionJwkAsync = () => { + return parseJsonOrNull(storage[`oidc.jwk.${configurationName}`]); + }; + + const getNonceAsync = async () => { + // @ts-ignore + return { nonce: loginStorage[`oidc.nonce.${configurationName}`] }; + }; + + const setDemonstratingProofOfPossessionNonce = async (dpopNonce: string) => { + writeRaw(storage, `oidc.dpop_nonce.${configurationName}`, dpopNonce); + }; + + const getDemonstratingProofOfPossessionNonce = (): string => { + return storage[`oidc.dpop_nonce.${configurationName}`]; + }; + + const getTokens = () => { + const parsed = parseJsonOrNull(storage[`oidc.${configurationName}`]) as { + tokens: any; + } | null; + if (!parsed) { + return null; + } + return JSON.stringify({ tokens: parsed.tokens }); + }; + + const getLoginParamsCache = {}; + const setLoginParams = data => { + if (data === undefined || data === null) { + delete getLoginParamsCache[configurationName]; + delete loginStorage[`oidc.login.${configurationName}`]; + return; + } + getLoginParamsCache[configurationName] = data; + writeJson(loginStorage, `oidc.login.${configurationName}`, data); + }; + const getLoginParams = () => { + if (getLoginParamsCache[configurationName]) { + return getLoginParamsCache[configurationName]; + } + const parsed = parseJsonOrNull(loginStorage[`oidc.login.${configurationName}`]); + if (parsed === null) { + console.warn( + `storage[oidc.login.${configurationName}] is empty, you should have an bad OIDC or code configuration somewhere.`, + ); + return null; + } + getLoginParamsCache[configurationName] = parsed; + return parsed; + }; + + const getStateAsync = async () => { + return loginStorage[`oidc.state.${configurationName}`]; + }; + + const setStateAsync = async (state: string) => { + writeRaw(loginStorage, `oidc.state.${configurationName}`, state); + }; + + const getCodeVerifierAsync = async () => { + return loginStorage[`oidc.code_verifier.${configurationName}`]; + }; + + const setCodeVerifierAsync = async codeVerifier => { + writeRaw(loginStorage, `oidc.code_verifier.${configurationName}`, codeVerifier); + }; + + return { + clearAsync, + initAsync, + setTokens, + getTokens, + setSessionStateAsync, + getSessionStateAsync, + setNonceAsync, + getNonceAsync, + setLoginParams, + getLoginParams, + getStateAsync, + setStateAsync, + getCodeVerifierAsync, + setCodeVerifierAsync, + setDemonstratingProofOfPossessionNonce, + getDemonstratingProofOfPossessionNonce, + setDemonstratingProofOfPossessionJwkAsync, + getDemonstratingProofOfPossessionJwkAsync, + }; }; diff --git a/packages/oidc-client/src/initWorker.ts b/packages/oidc-client/src/initWorker.ts index e38f718c8..64b95b48b 100644 --- a/packages/oidc-client/src/initWorker.ts +++ b/packages/oidc-client/src/initWorker.ts @@ -1,321 +1,765 @@ +import { ILOidcLocation } from './location'; import { parseOriginalTokens } from './parseTokens.js'; import timer from './timer.js'; import { OidcConfiguration } from './types.js'; +import codeVersion from './version.js'; -export const getOperatingSystem = (navigator) => { - const nVer = navigator.appVersion; - const nAgt = navigator.userAgent; - const unknown = '-'; - // system - let os = unknown; - const clientStrings = [ - { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ }, - { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ }, - { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ }, - { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ }, - { s: 'Windows Vista', r: /Windows NT 6.0/ }, - { s: 'Windows Server 2003', r: /Windows NT 5.2/ }, - { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ }, - { s: 'Windows 2000', r: /(Windows NT 5.0|Windows 2000)/ }, - { s: 'Windows ME', r: /(Win 9x 4.90|Windows ME)/ }, - { s: 'Windows 98', r: /(Windows 98|Win98)/ }, - { s: 'Windows 95', r: /(Windows 95|Win95|Windows_95)/ }, - { s: 'Windows NT 4.0', r: /(Windows NT 4.0|WinNT4.0|WinNT|Windows NT)/ }, - { s: 'Windows CE', r: /Windows CE/ }, - { s: 'Windows 3.11', r: /Win16/ }, - { s: 'Android', r: /Android/ }, - { s: 'Open BSD', r: /OpenBSD/ }, - { s: 'Sun OS', r: /SunOS/ }, - { s: 'Chrome OS', r: /CrOS/ }, - { s: 'Linux', r: /(Linux|X11(?!.*CrOS))/ }, - { s: 'iOS', r: /(iPhone|iPad|iPod)/ }, - { s: 'Mac OS X', r: /Mac OS X/ }, - { s: 'Mac OS', r: /(Mac OS|MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ }, - { s: 'QNX', r: /QNX/ }, - { s: 'UNIX', r: /UNIX/ }, - { s: 'BeOS', r: /BeOS/ }, - { s: 'OS/2', r: /OS\/2/ }, - { s: 'Search Bot', r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/ }, - ]; - for (const id in clientStrings) { - const cs = clientStrings[id]; - if (cs.r.test(nAgt)) { - os = cs.s; - break; - } - } +export const DEFAULT_SW_MESSAGE_TIMEOUT_MS = 5000; - let osVersion = unknown; +export interface ServiceWorkerSignalMessage { + type: string; + configurationName?: string; + data?: unknown; + tabId?: string; + [key: string]: unknown; +} - if (/Windows/.test(os)) { - osVersion = /Windows (.*)/.exec(os)[1]; - os = 'Windows'; - } +export interface ServiceWorkerSignalOptions { + timeoutMs?: number; +} - switch (os) { - case 'Mac OS': - case 'Mac OS X': - case 'Android': - osVersion = /(?:Android|Mac OS|Mac OS X|MacPPC|MacIntel|Mac_PowerPC|Macintosh) ([._\d]+)/.exec(nAgt)[1]; - break; - - case 'iOS': { - const osVersionArray = /OS (\d+)_(\d+)_?(\d+)?/.exec(nVer); - osVersion = osVersionArray[1] + '.' + osVersionArray[2] + '.' + (parseInt(osVersionArray[3]) | 0); - break; - } - } - return { - os, - osVersion, - }; +let keepAliveServiceWorkerTimeoutId = null; +let keepAliveController: AbortController | undefined; + +export const sleepAsync = ({ milliseconds }: { milliseconds: any }) => { + return new Promise(resolve => timer.setTimeout(resolve, milliseconds)); }; -function getBrowser() { - const ua = navigator.userAgent; let tem; - let M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; - if (/trident/i.test(M[1])) { - tem = /\brv[ :]+(\d+)/g.exec(ua) || []; - return { name: 'ie', version: (tem[1] || '') }; - } - if (M[1] === 'Chrome') { - tem = ua.match(/\bOPR|Edge\/(\d+)/); - - if (tem != null) { - let version = tem[1]; - if (!version) { - const splits = ua.split(tem[0] + '/'); - if (splits.length > 1) { - version = splits[1]; - } - } +const keepAlive = (service_worker_keep_alive_path = '/') => { + try { + const minSleepSeconds = 150; + keepAliveController = new AbortController(); + const promise = fetch( + `${service_worker_keep_alive_path}OidcKeepAliveServiceWorker.json?minSleepSeconds=${minSleepSeconds}`, + { signal: keepAliveController.signal }, + ); + promise.catch(error => { + console.log(error); + }); + + sleepAsync({ milliseconds: minSleepSeconds * 1000 }).then(() => + keepAlive(service_worker_keep_alive_path), + ); + } catch (error) { + console.log(error); + } +}; + +const stopKeepAlive = () => { + if (keepAliveController) { + keepAliveController.abort(); + } +}; + +export const defaultServiceWorkerUpdateRequireCallback = + (location: ILOidcLocation) => async (registration: any, stopKeepAlive: () => void) => { + stopKeepAlive(); + await registration.update(); + const isSuccess = await registration.unregister(); + console.log(`Service worker unregistration ${isSuccess ? 'successful' : 'failed'}`); + await sleepAsync({ milliseconds: 2000 }); + location.reload(); + }; - return { name: 'opera', version }; +export const getTabId = (configurationName: string) => { + const key = `oidc.tabId.${configurationName}`; + const tabId = sessionStorage.getItem(key); + if (tabId) return tabId; + + const newTabId = globalThis.crypto.randomUUID(); + sessionStorage.setItem(key, newTabId); + return newTabId; +}; + +const getServiceWorkerTarget = (registration: ServiceWorkerRegistration): ServiceWorker | null => { + return ( + navigator.serviceWorker.controller ?? + registration.active ?? + registration.waiting ?? + registration.installing ?? + null + ); +}; + +const sendMessageAsync = + (registration: ServiceWorkerRegistration, opts?: { timeoutMs?: number }) => + (data: any): Promise => { + const timeoutMs = opts?.timeoutMs ?? DEFAULT_SW_MESSAGE_TIMEOUT_MS; + + return new Promise((resolve, reject) => { + const target = getServiceWorkerTarget(registration); + + if (!target) { + reject( + new Error( + 'Service worker target not available (controller/active/waiting/installing missing)', + ), + ); + return; + } + + const messageChannel = new MessageChannel(); + let timeoutId: any = null; + + const cleanup = () => { + try { + if (timeoutId != null) { + timer.clearTimeout(timeoutId); + timeoutId = null; + } + messageChannel.port1.onmessage = null; + messageChannel.port1.close(); + messageChannel.port2.close(); + } catch (ex) { + console.error(ex); } - } - M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?']; - if ((tem = ua.match(/version\/(\d+)/i)) != null) { M.splice(1, 1, tem[1]); } - return { - name: M[0].toLowerCase(), - version: M[1], + }; + + timeoutId = timer.setTimeout(() => { + cleanup(); + reject( + new Error(`Service worker did not respond within ${timeoutMs}ms (type=${data?.type})`), + ); + }, timeoutMs); + + messageChannel.port1.onmessage = event => { + cleanup(); + if (event?.data?.error) reject(event.data.error); + else resolve(event.data); + }; + + try { + const configurationName = data?.configurationName; + target.postMessage({ ...data, tabId: getTabId(configurationName ?? 'default') }, [ + messageChannel.port2, + ]); + } catch (err) { + cleanup(); + reject(err); + } + }); + }; + +const waitForControllerAsync = async (timeoutMs: number) => { + if (navigator.serviceWorker.controller) return navigator.serviceWorker.controller; + + return new Promise(resolve => { + let settled = false; + const onChange = () => { + if (settled) return; + settled = true; + navigator.serviceWorker.removeEventListener('controllerchange', onChange); + resolve(navigator.serviceWorker.controller ?? null); }; -} -let keepAliveServiceWorkerTimeoutId = null; + navigator.serviceWorker.addEventListener('controllerchange', onChange); -export const sleepAsync = (milliseconds) => { - return new Promise(resolve => timer.setTimeout(resolve, milliseconds)); + timer.setTimeout(() => { + if (settled) return; + settled = true; + navigator.serviceWorker.removeEventListener('controllerchange', onChange); + resolve(navigator.serviceWorker.controller ?? null); + }, timeoutMs); + }); }; -const keepAlive = () => { - try { - const operatingSystem = getOperatingSystem(navigator); - const minSleepSeconds = operatingSystem.os === 'Android' ? 240 : 150; - const promise = fetch(`/OidcKeepAliveServiceWorker.json?minSleepSeconds=${minSleepSeconds}`); - promise.catch(error => { console.log(error); }); - sleepAsync(minSleepSeconds * 1000).then(keepAlive); - } catch (error) { console.log(error); } +// Module-level guards to prevent: +// - registering multiple controllerchange listeners (one per initWorkerAsync call) +// - reloading more than once per page lifetime +let controllerChangeListenerRegistered = false; +let controllerChangeReloading = false; + +// Cache registration promises by URL so that navigator.serviceWorker.register (or a custom +// service_worker_register) is called at most once per JavaScript session (page lifetime), +// regardless of how many times initWorkerAsync is invoked. +export const registrationCache = new Map>(); + +// AbortError surfaces during transient lifecycle events (e.g. iOS Safari tab shutdown or +// backgrounding) and does not represent a genuine configuration problem. We treat it as +// non-fatal, clear the cache entry so future calls can retry, and fall back to the +// non–service-worker path by returning null from initWorkerAsync. +const isAbortError = (error: unknown): boolean => { + if (error instanceof DOMException) { + return error.name === 'AbortError'; + } + return (error as { name?: string } | null)?.name === 'AbortError'; }; -const isServiceWorkerProxyActiveAsync = () => { - return fetch('/OidcKeepAliveServiceWorker.json', { - headers: { - 'oidc-vanilla': 'true', - }, - }).then((response) => { - return response.statusText === 'oidc-service-worker'; - }).catch(error => { console.log(error); }); +// Session-level guard to prevent infinite reload loops caused by SW update cycles. +// The controllerchange listener triggers a page reload, but after reload the module-level +// guards above are reset. If the SW still hasn't been updated correctly (e.g. stale cache, +// Firefox issues), the cycle would repeat forever. This key tracks reloads across page loads +// via sessionStorage so we can break the loop. +const SW_RELOAD_SESSION_KEY = 'oidc.sw.controllerchange_reload_count'; +const SW_RELOAD_MAX = 3; + +const getControllerChangeReloadCount = (): number => { + try { + return parseInt(sessionStorage.getItem(SW_RELOAD_SESSION_KEY) ?? '0', 10); + } catch { + return 0; + } }; -export const excludeOs = (operatingSystem) => { - if (operatingSystem.os === 'iOS' && operatingSystem.osVersion.startsWith('12')) { - return true; - } - if (operatingSystem.os === 'Mac OS X' && operatingSystem.osVersion.startsWith('10_15_6')) { - return true; - } - return false; +const incrementControllerChangeReloadCount = (): number => { + const count = getControllerChangeReloadCount() + 1; + try { + sessionStorage.setItem(SW_RELOAD_SESSION_KEY, String(count)); + } catch { + // ignore + } + return count; +}; + +const clearControllerChangeReloadCount = () => { + try { + sessionStorage.removeItem(SW_RELOAD_SESSION_KEY); + } catch { + // ignore + } }; -const sendMessageAsync = (registration) => (data) => { - return new Promise(function(resolve, reject) { +export const initWorkerAsync = async ( + configuration: OidcConfiguration, + configurationName: string, +) => { + const serviceWorkerRelativeUrl = configuration.service_worker_relative_url; + + if ( + typeof window === 'undefined' || + typeof navigator === 'undefined' || + !navigator.serviceWorker || + !serviceWorkerRelativeUrl + ) { + return null; + } + + if (configuration.service_worker_activate() === false) { + return null; + } + + const swUrl = `${serviceWorkerRelativeUrl}?v=${codeVersion}`; + + const cacheKey = configuration.service_worker_register ? serviceWorkerRelativeUrl : swUrl; + + if (!registrationCache.has(cacheKey)) { + registrationCache.set( + cacheKey, + configuration.service_worker_register + ? configuration.service_worker_register(serviceWorkerRelativeUrl) + : navigator.serviceWorker.register(swUrl, { + updateViaCache: 'none', + }), + ); + } + + let registration: ServiceWorkerRegistration = null as any; + try { + registration = await registrationCache.get(cacheKey)!; + } catch (error) { + if (isAbortError(error)) { + // Drop the rejected promise so a later call can attempt registration again + // once the browser is in a stable state (e.g. tab is foregrounded again). + registrationCache.delete(cacheKey); + console.warn( + 'oidc-client: service worker registration was aborted (likely tab shutdown or backgrounding); falling back to non–service-worker mode.', + error, + ); + return null; + } + throw error; + } + + const versionMismatchKey = `oidc.sw.version_mismatch_reload.${configurationName}`; + + const sendSkipWaitingToWorker = async (targetSw: ServiceWorker) => { + stopKeepAlive(); + console.log('New SW waiting – SKIP_WAITING'); + try { + await new Promise((resolve, reject) => { const messageChannel = new MessageChannel(); - messageChannel.port1.onmessage = function (event) { - if (event.data && event.data.error) { - reject(event.data.error); - } else { - resolve(event.data); + let timeoutId: any = null; + + const cleanup = () => { + try { + if (timeoutId != null) { + timer.clearTimeout(timeoutId); + timeoutId = null; } + messageChannel.port1.onmessage = null; + messageChannel.port1.close(); + messageChannel.port2.close(); + } catch (ex) { + console.error(ex); + } }; - registration.active.postMessage(data, [messageChannel.port2]); - }); -}; -export const initWorkerAsync = async(serviceWorkerRelativeUrl, configurationName) => { - if (typeof window === 'undefined' || typeof navigator === 'undefined' || !navigator.serviceWorker || !serviceWorkerRelativeUrl) { - return null; + timeoutId = timer.setTimeout(() => { + cleanup(); + reject(new Error('SKIP_WAITING did not respond within 8000ms')); + }, 8000); + + messageChannel.port1.onmessage = event => { + cleanup(); + if (event?.data?.error) reject(event.data.error); + else resolve(); + }; + + try { + targetSw.postMessage( + { + type: 'SKIP_WAITING', + configurationName, + data: null, + tabId: getTabId(configurationName ?? 'default'), + }, + [messageChannel.port2], + ); + } catch (err) { + cleanup(); + reject(err); + } + }); + } catch (e) { + console.warn('SKIP_WAITING failed', e); } - const { name, version } = getBrowser(); - if (name === 'chrome' && parseInt(version) < 90) { - return null; + }; + + const sendSkipWaiting = async () => { + const waitingSw = registration.waiting; + if (waitingSw) { + await sendSkipWaitingToWorker(waitingSw); + } else { + console.warn('sendSkipWaiting called but no waiting service worker found'); } - if (name === 'opera') { - if (!version) { - return null; - } - if (parseInt(version.split('.')[0]) < 80) { - return null; + }; + + const trackInstallingWorker = (newSW: ServiceWorker) => { + stopKeepAlive(); + newSW.addEventListener('statechange', async () => { + if (newSW.state === 'installed' && navigator.serviceWorker.controller) { + // Guard against infinite SKIP_WAITING → controllerchange → reload loops. + // If we've already exhausted the reload budget, don't force activation – let the + // browser handle it naturally on the next navigation instead. + if (getControllerChangeReloadCount() >= SW_RELOAD_MAX) { + console.warn( + 'SW trackInstallingWorker: skipping SKIP_WAITING because the reload budget is exhausted', + ); + return; } + await sendSkipWaitingToWorker(newSW); + } + }); + }; + + // 1) Détection updatefound – registered BEFORE update() to avoid missing the event + registration.addEventListener('updatefound', () => { + const newSW = registration.installing; + if (newSW) { + trackInstallingWorker(newSW); } - if (name === 'ie') { - return null; - } + }); - const operatingSystem = getOperatingSystem(navigator); - if (excludeOs(operatingSystem)) { - return null; + // Handle a SW that is already installing or waiting (e.g. when the listener above was + // registered after the updatefound event already fired in a previous call) + if (registration.installing) { + trackInstallingWorker(registration.installing); + } else if (registration.waiting && navigator.serviceWorker.controller) { + // A new SW is already waiting – activate it straight away (unless reload budget exhausted) + if (getControllerChangeReloadCount() < SW_RELOAD_MAX) { + sendSkipWaiting(); + } else { + console.warn( + 'SW: a waiting worker exists but reload budget is exhausted – skipping activation', + ); } + } - const registration = await navigator.serviceWorker.register(serviceWorkerRelativeUrl); + // (Optional but useful on Safari) ask for update early – non-blocking to avoid slowing init + registration.update().catch(ex => { + console.error(ex); + }); - try { - await navigator.serviceWorker.ready; - } catch (err) { - return null; + // 2) Claim + init classique (Safari-safe) + // IMPORTANT: claim() is done BEFORE registering the controllerchange listener, + // because claim() can trigger a controllerchange event on first visit and we don't + // want that initial claim to cause a reload loop. + try { + await navigator.serviceWorker.ready; + + // If the callback page is not yet controlled, ask claim then wait a bit. + if (!navigator.serviceWorker.controller) { + await sendMessageAsync(registration, { timeoutMs: 8000 })({ + type: 'claim', + configurationName, + data: null, + }); + + await waitForControllerAsync(2000); } + } catch (err: any) { + console.warn(`Failed init ServiceWorker ${err?.toString?.() ?? String(err)}`); + return null; + } - const unregisterAsync = async () => { - return await registration.unregister(); - }; + // 3) Register the controllerchange listener AFTER claim, and only once per page lifetime. + // This prevents: + // - claim() from triggering a reload on first visit + // - multiple listeners being stacked (initWorkerAsync is called many times) + // - more than one reload per page lifetime (guard via controllerChangeReloading) + // - infinite loops across page reloads (guard via sessionStorage counter) + if (!controllerChangeListenerRegistered) { + controllerChangeListenerRegistered = true; + navigator.serviceWorker.addEventListener('controllerchange', () => { + if (controllerChangeReloading) { + return; + } - registration.addEventListener('updatefound', () => { - const newWorker = registration.installing; - newWorker.addEventListener('statechange', () => { - switch (newWorker.state) { - case 'installed': - if (navigator.serviceWorker.controller) { - registration.unregister().then(() => { - window.location.reload(); - }); - } - break; - } - }); + // Session-level guard: prevent infinite reload loops when the SW never converges + // to the expected version (e.g. stale cache, Firefox issues, Electron quirks). + const reloadCount = incrementControllerChangeReloadCount(); + if (reloadCount > SW_RELOAD_MAX) { + console.warn( + `SW controllerchange: reload budget exhausted (${reloadCount - 1} reloads). ` + + 'Skipping reload to avoid infinite loop.', + ); + return; + } + + controllerChangeReloading = true; + console.log('SW controller changed – reloading page'); + stopKeepAlive(); + window.location.reload(); }); + } - const clearAsync = async (status) => { - return sendMessageAsync(registration)({ type: 'clear', data: { status }, configurationName }); - }; - const initAsync = async (oidcServerConfiguration, where, oidcConfiguration:OidcConfiguration) => { - const result = await sendMessageAsync(registration)({ - type: 'init', - data: { - oidcServerConfiguration, - where, - oidcConfiguration: { - token_renew_mode: oidcConfiguration.token_renew_mode, - service_worker_convert_all_requests_to_cors: oidcConfiguration.service_worker_convert_all_requests_to_cors, - }, - }, - configurationName, - }); - // @ts-ignore - return { tokens: parseOriginalTokens(result.tokens, null, oidcConfiguration.token_renew_mode), status: result.status }; - }; + const clearAsync = async status => { + return sendMessageAsync(registration)({ type: 'clear', data: { status }, configurationName }); + }; - const startKeepAliveServiceWorker = () => { - if (keepAliveServiceWorkerTimeoutId == null) { - keepAliveServiceWorkerTimeoutId = 'not_null'; - keepAlive(); - } - }; + const initAsync = async ( + oidcServerConfiguration, + where, + oidcConfiguration: OidcConfiguration, + ) => { + const result = await sendMessageAsync(registration)({ + type: 'init', + data: { + oidcServerConfiguration, + where, + oidcConfiguration: { + token_renew_mode: oidcConfiguration.token_renew_mode, + service_worker_convert_all_requests_to_cors: + oidcConfiguration.service_worker_convert_all_requests_to_cors, + }, + }, + configurationName, + }); - const setSessionStateAsync = (sessionState:string) => { - return sendMessageAsync(registration)({ type: 'setSessionState', data: { sessionState }, configurationName }); - }; + // @ts-ignore + const serviceWorkerVersion = result.version; + if (serviceWorkerVersion !== codeVersion) { + console.warn( + `Service worker ${serviceWorkerVersion} version mismatch with js client version ${codeVersion}, unregistering and reloading`, + ); - const getSessionStateAsync = async () => { - const result = await sendMessageAsync(registration)({ type: 'getSessionState', data: null, configurationName }); - // @ts-ignore - return result.sessionState; - }; + const reloadCount = parseInt(sessionStorage.getItem(versionMismatchKey) ?? '0', 10); + if (reloadCount < 3) { + sessionStorage.setItem(versionMismatchKey, String(reloadCount + 1)); - const setNonceAsync = (nonce) => { - sessionStorage['oidc.nonce'] = nonce.nonce; - return sendMessageAsync(registration)({ type: 'setNonce', data: { nonce }, configurationName }); - }; - const getNonceAsync = async () => { - // @ts-ignore - const result = await sendMessageAsync(registration)({ type: 'getNonce', data: null, configurationName }); - // @ts-ignore - let nonce = result.nonce; - if (!nonce) { - nonce = sessionStorage['oidc.nonce']; - console.warn('nonce not found in service worker, using sessionStorage'); + if (registration.waiting) { + // A new SW is already waiting – activate it; controllerchange will trigger reload + await sendSkipWaiting(); + // If controllerchange did not reload yet, wait a moment then force reload + await sleepAsync({ milliseconds: 500 }); + if (!controllerChangeReloading) { + controllerChangeReloading = true; + window.location.reload(); + } + // Return a never-resolving promise to avoid returning stale tokens + return new Promise(() => {}); + } else { + // No waiting SW – force a fresh update and reload + stopKeepAlive(); + try { + await registration.update(); + } catch (ex) { + console.error(ex); + } + const isSuccess = await registration.unregister(); + console.log(`Service worker unregistering ${isSuccess}`); + await sleepAsync({ milliseconds: 500 }); + if (!controllerChangeReloading) { + controllerChangeReloading = true; + window.location.reload(); + } + return new Promise(() => {}); } - return { nonce }; - }; + } else { + // Max retries reached – do NOT clear the key so future initAsync calls + // won't restart the cycle of 3 reloads + console.error( + `Service worker version mismatch persists after ${reloadCount} attempt(s). Continuing with mismatched version.`, + ); + } + } else { + // Version matches – clear any leftover mismatch counter and reload counter + sessionStorage.removeItem(versionMismatchKey); + clearControllerChangeReloadCount(); + } - let getLoginParamsCache = null; - const setLoginParams = (configurationName:string, data) => { - getLoginParamsCache = data; - localStorage[`oidc.login.${configurationName}`] = JSON.stringify(data); - }; - const getLoginParams = (configurationName) => { - const dataString = localStorage[`oidc.login.${configurationName}`]; - if (!getLoginParamsCache) { - getLoginParamsCache = JSON.parse(dataString); - } - return getLoginParamsCache; + // @ts-ignore + return { + tokens: parseOriginalTokens(result.tokens, null, oidcConfiguration.token_renew_mode), + status: result.status, }; + }; - const getStateAsync = async () => { - const result = await sendMessageAsync(registration)({ type: 'getState', data: null, configurationName }); - // @ts-ignore - let state = result.state; - if (!state) { - state = sessionStorage[`oidc.state.${configurationName}`]; - console.warn('state not found in service worker, using sessionStorage'); - } - return state; - }; + const startKeepAliveServiceWorker = (service_worker_keep_alive_path = '/') => { + if (keepAliveServiceWorkerTimeoutId == null) { + keepAliveServiceWorkerTimeoutId = 'not_null'; + keepAlive(service_worker_keep_alive_path); + } + }; - const setStateAsync = async (state:string) => { - sessionStorage[`oidc.state.${configurationName}`] = state; - return sendMessageAsync(registration)({ type: 'setState', data: { state }, configurationName }); - }; + const setSessionStateAsync = (sessionState: string) => { + return sendMessageAsync(registration)({ + type: 'setSessionState', + data: { sessionState }, + configurationName, + }); + }; - const getCodeVerifierAsync = async () => { - const result = await sendMessageAsync(registration)({ type: 'getCodeVerifier', data: null, configurationName }); + const getSessionStateAsync = async () => { + const result = await sendMessageAsync(registration)({ + type: 'getSessionState', + data: null, + configurationName, + }); + // @ts-ignore + return result.sessionState; + }; + + const setNonceAsync = nonce => { + sessionStorage[`oidc.nonce.${configurationName}`] = nonce.nonce; + return sendMessageAsync(registration)({ + type: 'setNonce', + data: { nonce }, + configurationName, + }); + }; + + const getNonceAsync = async (fallback: boolean = true) => { + const result = await sendMessageAsync(registration)({ + type: 'getNonce', + data: null, + configurationName, + }); + + // @ts-ignore + let nonce = result.nonce; + if (!nonce) { + nonce = sessionStorage[`oidc.nonce.${configurationName}`]; + console.warn('nonce not found in service worker, using sessionStorage'); + if (fallback) { + await setNonceAsync(nonce); + const data = await getNonceAsync(false); // @ts-ignore - let codeVerifier = result.codeVerifier; - if (!codeVerifier) { - codeVerifier = sessionStorage[`oidc.code_verifier.${configurationName}`]; - console.warn('codeVerifier not found in service worker, using sessionStorage'); - } - return codeVerifier; - }; + nonce = data.nonce; + } + } + return { nonce }; + }; - const setCodeVerifierAsync = async (codeVerifier:string) => { - sessionStorage[`oidc.code_verifier.${configurationName}`] = codeVerifier; - return sendMessageAsync(registration)({ type: 'setCodeVerifier', data: { codeVerifier }, configurationName }); - }; + const getLoginParamsCache = {}; + const setLoginParams = data => { + if (data === undefined || data === null) { + delete getLoginParamsCache[configurationName]; + delete localStorage[`oidc.login.${configurationName}`]; + return; + } + getLoginParamsCache[configurationName] = data; + localStorage[`oidc.login.${configurationName}`] = JSON.stringify(data); + }; - return { - clearAsync, - initAsync, - startKeepAliveServiceWorker, - isServiceWorkerProxyActiveAsync, - setSessionStateAsync, - getSessionStateAsync, - setNonceAsync, - getNonceAsync, - unregisterAsync, - setLoginParams, - getLoginParams, - getStateAsync, - setStateAsync, - getCodeVerifierAsync, - setCodeVerifierAsync, - }; + const getLoginParams = () => { + if (getLoginParamsCache[configurationName]) { + return getLoginParamsCache[configurationName]; + } + const dataString = localStorage[`oidc.login.${configurationName}`]; + // Guard against the literal strings "undefined" / "null" written by older + // builds of this library through bracket-notation assignment. + if ( + typeof dataString !== 'string' || + dataString === '' || + dataString === 'undefined' || + dataString === 'null' + ) { + return null; + } + try { + getLoginParamsCache[configurationName] = JSON.parse(dataString); + } catch { + return null; + } + return getLoginParamsCache[configurationName]; + }; + + const setDemonstratingProofOfPossessionNonce = async ( + demonstratingProofOfPossessionNonce: string, + ) => { + await sendMessageAsync(registration)({ + type: 'setDemonstratingProofOfPossessionNonce', + data: { demonstratingProofOfPossessionNonce }, + configurationName, + }); + }; + + const getDemonstratingProofOfPossessionNonce = async () => { + const result = await sendMessageAsync(registration)({ + type: 'getDemonstratingProofOfPossessionNonce', + data: null, + configurationName, + }); + return result.demonstratingProofOfPossessionNonce; + }; + + const setDemonstratingProofOfPossessionJwkAsync = async ( + demonstratingProofOfPossessionJwk: JsonWebKey, + ) => { + const demonstratingProofOfPossessionJwkJson = JSON.stringify(demonstratingProofOfPossessionJwk); + await sendMessageAsync(registration)({ + type: 'setDemonstratingProofOfPossessionJwk', + data: { demonstratingProofOfPossessionJwkJson }, + configurationName, + }); + }; + + const getDemonstratingProofOfPossessionJwkAsync = async () => { + const result = await sendMessageAsync(registration)({ + type: 'getDemonstratingProofOfPossessionJwk', + data: null, + configurationName, + }); + if (!result.demonstratingProofOfPossessionJwkJson) { + return null; + } + return JSON.parse(result.demonstratingProofOfPossessionJwkJson); + }; + + const getStateAsync = async (fallback: boolean = true) => { + const result = await sendMessageAsync(registration)({ + type: 'getState', + data: null, + configurationName, + }); + + // @ts-ignore + let state = result.state; + if (!state) { + state = sessionStorage[`oidc.state.${configurationName}`]; + console.warn('state not found in service worker, using sessionStorage'); + if (fallback) { + await setStateAsync(state); + state = await getStateAsync(false); + } + } + return state; + }; + + const setStateAsync = async (state: string) => { + sessionStorage[`oidc.state.${configurationName}`] = state; + return sendMessageAsync(registration)({ + type: 'setState', + data: { state }, + configurationName, + }); + }; + + const getCodeVerifierAsync = async (fallback: boolean = true) => { + const result = await sendMessageAsync(registration)({ + type: 'getCodeVerifier', + data: null, + configurationName, + }); + + // @ts-ignore + let codeVerifier = result.codeVerifier; + if (!codeVerifier) { + codeVerifier = sessionStorage[`oidc.code_verifier.${configurationName}`]; + console.warn('codeVerifier not found in service worker, using sessionStorage'); + if (fallback) { + await setCodeVerifierAsync(codeVerifier); + codeVerifier = await getCodeVerifierAsync(false); + } + } + return codeVerifier; + }; + + const setCodeVerifierAsync = async (codeVerifier: string) => { + sessionStorage[`oidc.code_verifier.${configurationName}`] = codeVerifier; + return sendMessageAsync(registration)({ + type: 'setCodeVerifier', + data: { codeVerifier }, + configurationName, + }); + }; + + const signalAsync = ( + message: ServiceWorkerSignalMessage, + options?: ServiceWorkerSignalOptions, + ): Promise => + sendMessageAsync( + registration, + options, + )({ + ...message, + configurationName: message.configurationName ?? configurationName, + }); + + return { + clearAsync, + initAsync, + startKeepAliveServiceWorker: () => + startKeepAliveServiceWorker(configuration.service_worker_keep_alive_path), + setSessionStateAsync, + getSessionStateAsync, + setNonceAsync, + getNonceAsync, + setLoginParams, + getLoginParams, + getStateAsync, + setStateAsync, + getCodeVerifierAsync, + setCodeVerifierAsync, + setDemonstratingProofOfPossessionNonce, + getDemonstratingProofOfPossessionNonce, + setDemonstratingProofOfPossessionJwkAsync, + getDemonstratingProofOfPossessionJwkAsync, + signalAsync, + }; +}; + +/** + * Sends a typed message to the OIDC service worker for the given + * configuration. Wraps `MessageChannel` setup, request/response correlation + * and timeouts. + * + * Resolves with the SW response, rejects on timeout or when no SW is + * registered for the configuration. The provided `configurationName` is + * used when the message itself does not carry one. + */ +export const signalServiceWorkerAsync = async ( + configuration: OidcConfiguration, + configurationName: string, + message: ServiceWorkerSignalMessage, + options?: ServiceWorkerSignalOptions, +): Promise => { + const worker = await initWorkerAsync(configuration, configurationName); + if (!worker) { + throw new Error( + `signalServiceWorkerAsync: no service worker registered for configuration "${configurationName}"`, + ); + } + return worker.signalAsync(message, options); }; diff --git a/packages/oidc-client/src/initWorkerAbortError.spec.ts b/packages/oidc-client/src/initWorkerAbortError.spec.ts new file mode 100644 index 000000000..db4344b9a --- /dev/null +++ b/packages/oidc-client/src/initWorkerAbortError.spec.ts @@ -0,0 +1,147 @@ +// Tests covering the AbortError handling added to initWorkerAsync. +// See https://github.com/AxaFrance/oidc-client/issues/1675 +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { initWorkerAsync, registrationCache } from './initWorker'; +import { OidcConfiguration } from './types'; + +const SERVICE_WORKER_RELATIVE_URL = '/OidcServiceWorker.js'; + +const buildConfiguration = (overrides: Partial = {}): OidcConfiguration => { + return { + client_id: 'test-client', + redirect_uri: 'http://localhost/callback', + scope: 'openid', + authority: 'http://authority', + service_worker_relative_url: SERVICE_WORKER_RELATIVE_URL, + service_worker_activate: () => true, + ...overrides, + } as OidcConfiguration; +}; + +const createAbortError = (): DOMException => { + // DOMException is available in the test environment (jsdom / happy-dom). + return new DOMException('The operation was aborted.', 'AbortError'); +}; + +describe('initWorkerAsync AbortError handling', () => { + let originalNavigator: PropertyDescriptor | undefined; + let originalWindow: PropertyDescriptor | undefined; + let warnSpy: ReturnType; + + beforeEach(() => { + registrationCache.clear(); + originalNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator'); + originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window'); + Object.defineProperty(globalThis, 'window', { + configurable: true, + value: {}, + }); + Object.defineProperty(globalThis, 'navigator', { + configurable: true, + value: { + serviceWorker: { + register: vi.fn(), + ready: Promise.resolve({} as ServiceWorkerRegistration), + controller: null, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + }, + }, + }); + warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + registrationCache.clear(); + if (originalNavigator) { + Object.defineProperty(globalThis, 'navigator', originalNavigator); + } else { + delete (globalThis as { navigator?: unknown }).navigator; + } + if (originalWindow) { + Object.defineProperty(globalThis, 'window', originalWindow); + } else { + delete (globalThis as { window?: unknown }).window; + } + vi.restoreAllMocks(); + }); + + it('returns null when a custom service_worker_register rejects with an AbortError', async () => { + const abortError = createAbortError(); + const service_worker_register = vi.fn(() => Promise.reject(abortError)); + + const result = await initWorkerAsync( + buildConfiguration({ service_worker_register }), + 'default', + ); + + expect(result).toBeNull(); + expect(service_worker_register).toHaveBeenCalledOnce(); + expect(registrationCache.has(SERVICE_WORKER_RELATIVE_URL)).toBe(false); + expect(warnSpy).toHaveBeenCalled(); + }); + + it('returns null when navigator.serviceWorker.register rejects with an AbortError', async () => { + const abortError = createAbortError(); + (navigator.serviceWorker.register as unknown as ReturnType).mockImplementation( + () => Promise.reject(abortError), + ); + + const result = await initWorkerAsync(buildConfiguration(), 'default'); + + expect(result).toBeNull(); + // The cache entry for the SW URL should have been cleared so a later retry is possible. + expect(Array.from(registrationCache.keys())).toHaveLength(0); + expect(warnSpy).toHaveBeenCalled(); + }); + + it('also treats plain { name: "AbortError" } rejections as aborts', async () => { + const plainAbort = { name: 'AbortError', message: 'aborted' }; + const service_worker_register = vi.fn(() => Promise.reject(plainAbort)); + + const result = await initWorkerAsync( + buildConfiguration({ service_worker_register }), + 'default', + ); + + expect(result).toBeNull(); + expect(registrationCache.has(SERVICE_WORKER_RELATIVE_URL)).toBe(false); + }); + + it('allows a subsequent call to retry registration after an AbortError', async () => { + const abortError = createAbortError(); + const service_worker_register = vi + .fn() + .mockImplementationOnce(() => Promise.reject(abortError)) + // Returning a never-resolving promise on the retry is fine for the assertion below: + // we only need to confirm that the function was called a second time after the + // failed cache entry was cleared. + .mockImplementationOnce(() => new Promise(() => {})); + + const configuration = buildConfiguration({ service_worker_register }); + + const firstResult = await initWorkerAsync(configuration, 'default'); + expect(firstResult).toBeNull(); + expect(service_worker_register).toHaveBeenCalledTimes(1); + + // Kick off the second call (don't await – the mocked promise never resolves). + void initWorkerAsync(configuration, 'default'); + // Allow the microtask queue to drain so the registration call is observed. + await Promise.resolve(); + + expect(service_worker_register).toHaveBeenCalledTimes(2); + }); + + it('propagates non-AbortError rejections to the caller', async () => { + const genericError = new Error('boom'); + const service_worker_register = vi.fn(() => Promise.reject(genericError)); + + await expect( + initWorkerAsync(buildConfiguration({ service_worker_register }), 'default'), + ).rejects.toBe(genericError); + + // Non-AbortError rejections keep the cache entry in place (existing behavior). + expect(registrationCache.has(SERVICE_WORKER_RELATIVE_URL)).toBe(true); + }); +}); diff --git a/packages/oidc-client/src/initWorkerOption.ts b/packages/oidc-client/src/initWorkerOption.ts new file mode 100644 index 000000000..3a9bfd37c --- /dev/null +++ b/packages/oidc-client/src/initWorkerOption.ts @@ -0,0 +1,140 @@ +import { ServiceWorkerActivate } from './types'; + +export const excludeOs = operatingSystem => { + if (operatingSystem.os === 'iOS' && operatingSystem.osVersion.startsWith('12')) { + return true; + } + if (operatingSystem.os === 'Mac OS X' && operatingSystem.osVersion.startsWith('10_15_6')) { + return true; + } + return false; +}; +export const getOperatingSystem = navigator => { + const nVer = navigator.appVersion; + const nAgt = navigator.userAgent; + const unknown = '-'; + // system + let os = unknown; + const clientStrings = [ + { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ }, + { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ }, + { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ }, + { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ }, + { s: 'Windows Vista', r: /Windows NT 6.0/ }, + { s: 'Windows Server 2003', r: /Windows NT 5.2/ }, + { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ }, + { s: 'Windows 2000', r: /(Windows NT 5.0|Windows 2000)/ }, + { s: 'Windows ME', r: /(Win 9x 4.90|Windows ME)/ }, + { s: 'Windows 98', r: /(Windows 98|Win98)/ }, + { s: 'Windows 95', r: /(Windows 95|Win95|Windows_95)/ }, + { s: 'Windows NT 4.0', r: /(Windows NT 4.0|WinNT4.0|WinNT|Windows NT)/ }, + { s: 'Windows CE', r: /Windows CE/ }, + { s: 'Windows 3.11', r: /Win16/ }, + { s: 'Android', r: /Android/ }, + { s: 'Open BSD', r: /OpenBSD/ }, + { s: 'Sun OS', r: /SunOS/ }, + { s: 'Chrome OS', r: /CrOS/ }, + { s: 'Linux', r: /(Linux|X11(?!.*CrOS))/ }, + { s: 'iOS', r: /(iPhone|iPad|iPod)/ }, + { s: 'Mac OS X', r: /Mac OS X/ }, + { s: 'Mac OS', r: /(Mac OS|MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ }, + { s: 'QNX', r: /QNX/ }, + { s: 'UNIX', r: /UNIX/ }, + { s: 'BeOS', r: /BeOS/ }, + { s: 'OS/2', r: /OS\/2/ }, + { + s: 'Search Bot', + r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/, + }, + ]; + for (const id in clientStrings) { + const cs = clientStrings[id]; + if (cs.r.test(nAgt)) { + os = cs.s; + break; + } + } + + let osVersion = unknown; + + if (/Windows/.test(os)) { + osVersion = /Windows (.*)/.exec(os)[1]; + os = 'Windows'; + } + + switch (os) { + case 'Mac OS': + case 'Mac OS X': + case 'Android': + osVersion = + /(?:Android|Mac OS|Mac OS X|MacPPC|MacIntel|Mac_PowerPC|Macintosh) ([._\d]+)/.exec(nAgt)[1]; + break; + + case 'iOS': { + const osVersionArray = /OS (\d+)_(\d+)_?(\d+)?/.exec(nVer); + if (osVersionArray != null && osVersionArray.length > 2) { + osVersion = + osVersionArray[1] + '.' + osVersionArray[2] + '.' + (parseInt(osVersionArray[3]) | 0); + } + break; + } + } + return { + os, + osVersion, + }; +}; + +function getBrowser() { + const ua = navigator.userAgent; + let tem; + let M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || []; + if (/trident/i.test(M[1])) { + tem = /\brv[ :]+(\d+)/g.exec(ua) || []; + return { name: 'ie', version: tem[1] || '' }; + } + if (M[1] === 'Chrome') { + tem = ua.match(/\bOPR|Edge\/(\d+)/); + + if (tem != null) { + let version = tem[1]; + if (!version) { + const splits = ua.split(tem[0] + '/'); + if (splits.length > 1) { + version = splits[1]; + } + } + + return { name: 'opera', version }; + } + } + M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?']; + if ((tem = ua.match(/version\/(\d+)/i)) != null) { + M.splice(1, 1, tem[1]); + } + return { + name: M[0].toLowerCase(), + version: M[1], + }; +} + +export const activateServiceWorker: ServiceWorkerActivate = (): boolean => { + const { name, version } = getBrowser(); + if (name === 'chrome' && parseInt(version) <= 70) { + return false; + } + if (name === 'opera') { + if (!version) { + return false; + } + if (parseInt(version.split('.')[0]) < 80) { + return false; + } + } + if (name === 'ie') { + return false; + } + + const operatingSystem = getOperatingSystem(navigator); + return !excludeOs(operatingSystem); +}; diff --git a/packages/oidc-client/src/jwt.spec.ts b/packages/oidc-client/src/jwt.spec.ts new file mode 100644 index 000000000..f6b6761e8 --- /dev/null +++ b/packages/oidc-client/src/jwt.spec.ts @@ -0,0 +1,33 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { + defaultDemonstratingProofOfPossessionConfiguration, + generateJwkAsync, + generateJwtDemonstratingProofOfPossessionAsync, +} from './jwt'; + +const decodePayload = (jwt: string) => { + const payload = jwt.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'); + return JSON.parse(atob(payload)); +}; + +describe('generateJwtDemonstratingProofOfPossessionAsync', () => { + it('uses a cryptographically secure UUID for the DPoP jti', async () => { + const uuid = '123e4567-e89b-42d3-a456-426614174000'; + const randomUUID = vi.fn(() => uuid); + const webCrypto = { + subtle: globalThis.crypto.subtle, + randomUUID, + }; + const jwk = await generateJwkAsync(globalThis)( + defaultDemonstratingProofOfPossessionConfiguration.generateKeyAlgorithm, + ); + + const jwt = await generateJwtDemonstratingProofOfPossessionAsync({ crypto: webCrypto })( + defaultDemonstratingProofOfPossessionConfiguration, + )(jwk, 'GET', 'https://api.example.com/resource'); + + expect(randomUUID).toHaveBeenCalledOnce(); + expect(decodePayload(jwt).jti).toBe(btoa(uuid)); + }); +}); diff --git a/packages/oidc-client/src/jwt.ts b/packages/oidc-client/src/jwt.ts new file mode 100644 index 000000000..a74b0fffc --- /dev/null +++ b/packages/oidc-client/src/jwt.ts @@ -0,0 +1,232 @@ +// code base on https://coolaj86.com/articles/sign-jwt-webcrypto-vanilla-js/ + +// String (UCS-2) to Uint8Array +// +// because... JavaScript, Strings, and Buffers +// @ts-ignore +import { DemonstratingProofOfPossessionConfiguration } from './types'; + +function strToUint8(str) { + return new TextEncoder().encode(str); +} + +// Binary String to URL-Safe Base64 +// +// btoa (Binary-to-Ascii) means "binary string" to base64 +// @ts-ignore +function binToUrlBase64(bin) { + return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+/g, ''); +} + +// UTF-8 to Binary String +// +// Because JavaScript has a strange relationship with strings +// https://coolaj86.com/articles/base64-unicode-utf-8-javascript-and-you/ +// @ts-ignore +function utf8ToBinaryString(str) { + const escstr = encodeURIComponent(str); + // replaces any uri escape sequence, such as %0A, + // with binary escape, such as 0x0A + return escstr.replace(/%([0-9A-F]{2})/g, function (match, p1) { + return String.fromCharCode(parseInt(p1, 16)); + }); +} + +// Uint8Array to URL Safe Base64 +// +// the shortest distant between two encodings... binary string +// @ts-ignore +export const uint8ToUrlBase64 = (uint8: Uint8Array) => { + let bin = ''; + // @ts-ignore + uint8.forEach(function (code) { + bin += String.fromCharCode(code); + }); + return binToUrlBase64(bin); +}; + +// UCS-2 String to URL-Safe Base64 +// +// btoa doesn't work on UTF-8 strings +// @ts-ignore +function strToUrlBase64(str) { + return binToUrlBase64(utf8ToBinaryString(str)); +} + +export const defaultDemonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration = + { + importKeyAlgorithm: { + name: 'ECDSA', + namedCurve: 'P-256', + hash: { name: 'ES256' }, + }, + signAlgorithm: { name: 'ECDSA', hash: { name: 'SHA-256' } }, + generateKeyAlgorithm: { + name: 'ECDSA', + namedCurve: 'P-256', + }, + digestAlgorithm: { name: 'SHA-256' }, + jwtHeaderAlgorithm: 'ES256', + }; + +// @ts-ignore +const sign = + (w: any) => + async ( + jwk, + headers, + claims, + demonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration, + jwtHeaderType = 'dpop+jwt', + ) => { + // Make a shallow copy of the key + // (to set ext if it wasn't already set) + jwk = Object.assign({}, jwk); + + // The headers should probably be empty + headers.typ = jwtHeaderType; + headers.alg = demonstratingProofOfPossessionConfiguration.jwtHeaderAlgorithm; + switch (headers.alg) { + case 'ES256': //if (!headers.kid) { + // alternate: see thumbprint function below + headers.jwk = { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y }; + //} + break; + case 'RS256': + headers.jwk = { kty: jwk.kty, n: jwk.n, e: jwk.e, kid: headers.kid }; + break; + default: + throw new Error('Unknown or not implemented JWS algorithm'); + } + + const jws = { + // @ts-ignore + // JWT "headers" really means JWS "protected headers" + protected: strToUrlBase64(JSON.stringify(headers)), + // @ts-ignore + // JWT "claims" are really a JSON-defined JWS "payload" + payload: strToUrlBase64(JSON.stringify(claims)), + }; + + // To import as EC (ECDSA, P-256, SHA-256, ES256) + const keyType = demonstratingProofOfPossessionConfiguration.importKeyAlgorithm; + + // To make re-exportable as JSON (or DER/PEM) + const exportable = true; + + // Import as a private key that isn't black-listed from signing + const privileges = ['sign']; + + // Actually do the import, which comes out as an abstract key type + // @ts-ignore + const privateKey = await w.crypto.subtle.importKey('jwk', jwk, keyType, exportable, privileges); + // Convert UTF-8 to Uint8Array ArrayBuffer + // @ts-ignore + const data = strToUint8(`${jws.protected}.${jws.payload}`); + + // The signature and hash should match the bit-entropy of the key + // https://tools.ietf.org/html/rfc7518#section-3 + const signatureType = demonstratingProofOfPossessionConfiguration.signAlgorithm; + + const signature = await w.crypto.subtle.sign(signatureType, privateKey, data); + // returns an ArrayBuffer containing a JOSE (not X509) signature, + // which must be converted to Uint8 to be useful + // @ts-ignore + jws.signature = uint8ToUrlBase64(new Uint8Array(signature)); + // JWT is just a "compressed", "protected" JWS + // @ts-ignore + return `${jws.protected}.${jws.payload}.${jws.signature}`; + }; + +export const JWT = { sign }; + +// @ts-ignore +const generate = + (w: any) => async (generateKeyAlgorithm: RsaHashedKeyGenParams | EcKeyGenParams) => { + const keyType = generateKeyAlgorithm; + const exportable = true; + const privileges = ['sign', 'verify']; + // @ts-ignore + const key = await w.crypto.subtle.generateKey(keyType, exportable, privileges); + // returns an abstract and opaque WebCrypto object, + // which in most cases you'll want to export as JSON to be able to save + return await w.crypto.subtle.exportKey('jwk', key.privateKey); + }; + +// Create a Public Key from a Private Key +// +// chops off the private parts +// @ts-ignore +const neuter = jwk => { + const copy = Object.assign({}, jwk); + delete copy.d; + copy.key_ops = ['verify']; + return copy; +}; + +const EC = { + generate, + neuter, +}; +// @ts-ignore +const thumbprint = (w: any) => async (jwk, digestAlgorithm: AlgorithmIdentifier) => { + let sortedPub; + // lexigraphically sorted, no spaces + switch (jwk.kty) { + case 'EC': + sortedPub = '{"crv":"CRV","kty":"EC","x":"X","y":"Y"}' + .replace('CRV', jwk.crv) + .replace('X', jwk.x) + .replace('Y', jwk.y); + break; + case 'RSA': + sortedPub = '{"e":"E","kty":"RSA","n":"N"}'.replace('E', jwk.e).replace('N', jwk.n); + break; + default: + throw new Error('Unknown or not implemented JWK type'); + } + // The hash should match the size of the key, + // but we're only dealing with P-256 + const hash = await w.crypto.subtle.digest(digestAlgorithm, strToUint8(sortedPub)); + return uint8ToUrlBase64(new Uint8Array(hash)); +}; + +export const JWK = { thumbprint }; + +export const generateJwkAsync = + (w: any) => async (generateKeyAlgorithm: RsaHashedKeyGenParams | EcKeyGenParams) => { + // @ts-ignore + const jwk = await EC.generate(w)(generateKeyAlgorithm); + // console.info('Private Key:', JSON.stringify(jwk)); + // @ts-ignore + // console.info('Public Key:', JSON.stringify(EC.neuter(jwk))); + return jwk; + }; + +export const generateJwtDemonstratingProofOfPossessionAsync = + (w: any) => + (demonstratingProofOfPossessionConfiguration: DemonstratingProofOfPossessionConfiguration) => + async (jwk, method = 'POST', url: string, extrasClaims = {}) => { + const claims = { + // https://www.rfc-editor.org/rfc/rfc9449.html#name-concept + jti: btoa(w.crypto.randomUUID()), + htm: method, + htu: url, + iat: Math.round(Date.now() / 1000), + ...extrasClaims, + }; + // @ts-ignore + const kid = await JWK.thumbprint(w)( + jwk, + demonstratingProofOfPossessionConfiguration.digestAlgorithm, + ); + // @ts-ignore + const jwt = await JWT.sign(w)( + jwk, + { kid: kid }, + claims, + demonstratingProofOfPossessionConfiguration, + ); + // console.info('JWT:', jwt); + return jwt; + }; diff --git a/packages/oidc-client/src/keepSession.ts b/packages/oidc-client/src/keepSession.ts new file mode 100644 index 000000000..9a6f4a805 --- /dev/null +++ b/packages/oidc-client/src/keepSession.ts @@ -0,0 +1,117 @@ +import { eventNames } from './events'; +import { initSession } from './initSession'; +import { initWorkerAsync } from './initWorker'; +import Oidc from './oidc'; +import { setTokens } from './parseTokens'; +import { autoRenewTokens } from './renewTokens'; + +export const tryKeepSessionAsync = async (oidc: Oidc) => { + let serviceWorker; + if (oidc.tokens != null) { + return false; + } + oidc.publishEvent(eventNames.tryKeepExistingSessionAsync_begin, {}); + try { + const configuration = oidc.configuration; + const oidcServerConfiguration = await oidc.initAsync( + configuration.authority, + configuration.authority_configuration, + ); + serviceWorker = await initWorkerAsync(configuration, oidc.configurationName); + if (serviceWorker) { + const { tokens } = await serviceWorker.initAsync( + oidcServerConfiguration, + 'tryKeepExistingSessionAsync', + configuration, + ); + if (tokens) { + serviceWorker.startKeepAliveServiceWorker(); + // @ts-ignore + oidc.tokens = tokens; + const getLoginParams = serviceWorker.getLoginParams(oidc.configurationName); + // @ts-ignore + oidc.timeoutId = autoRenewTokens( + oidc, + oidc.tokens.expiresAt, + getLoginParams.extras, + getLoginParams.scope, + ); + const sessionState = await serviceWorker.getSessionStateAsync(); + // @ts-ignore + await oidc.startCheckSessionAsync( + oidcServerConfiguration.checkSessionIframe, + configuration.client_id, + sessionState, + ); + if (configuration.preload_user_info) { + await oidc.userInfoAsync(); + } + oidc.publishEvent(eventNames.tryKeepExistingSessionAsync_end, { + success: true, + message: 'tokens inside ServiceWorker are valid', + }); + return true; + } + oidc.publishEvent(eventNames.tryKeepExistingSessionAsync_end, { + success: false, + message: 'no exiting session found', + }); + } else { + if (configuration.service_worker_relative_url) { + oidc.publishEvent(eventNames.service_worker_not_supported_by_browser, { + message: 'service worker is not supported by this browser', + }); + } + const session = initSession( + oidc.configurationName, + configuration.storage ?? sessionStorage, + configuration.login_state_storage ?? configuration.storage ?? sessionStorage, + ); + const { tokens } = await session.initAsync(); + if (tokens) { + // @ts-ignore + oidc.tokens = setTokens(tokens, null, configuration.token_renew_mode); + const getLoginParams = session.getLoginParams(); + // @ts-ignore + oidc.timeoutId = autoRenewTokens( + oidc, + oidc.tokens.expiresAt, + getLoginParams.extras, + getLoginParams.scope, + ); + const sessionState = await session.getSessionStateAsync(); + // @ts-ignore + await oidc.startCheckSessionAsync( + oidcServerConfiguration.checkSessionIframe, + configuration.client_id, + sessionState, + ); + if (configuration.preload_user_info) { + await oidc.userInfoAsync(); + } + oidc.publishEvent(eventNames.tryKeepExistingSessionAsync_end, { + success: true, + message: 'tokens inside storage are valid', + }); + return true; + } + } + oidc.publishEvent(eventNames.tryKeepExistingSessionAsync_end, { + success: false, + message: serviceWorker + ? 'service worker sessions not retrieved' + : 'session storage sessions not retrieved', + }); + return false; + } catch (exception) { + console.error(exception); + if (serviceWorker) { + await serviceWorker.clearAsync(); + } + oidc.publishEvent( + eventNames.tryKeepExistingSessionAsync_error, + 'tokens inside ServiceWorker are invalid', + ); + return false; + } +}; diff --git a/packages/oidc-client/src/location.ts b/packages/oidc-client/src/location.ts new file mode 100644 index 000000000..b3cd5808a --- /dev/null +++ b/packages/oidc-client/src/location.ts @@ -0,0 +1,30 @@ +export interface ILOidcLocation { + open(url: string): void; + reload(): void; + getCurrentHref(): string; + getPath(): string; + getOrigin(): string; +} + +export class OidcLocation implements ILOidcLocation { + open(url: string) { + window.location.href = url; + } + + reload() { + window.location.reload(); + } + + getCurrentHref() { + return window.location.href; + } + + getPath() { + const location = window.location; + return location.pathname + (location.search || '') + (location.hash || ''); + } + + getOrigin(): string { + return window.origin; + } +} diff --git a/packages/oidc-client/src/login.spec.ts b/packages/oidc-client/src/login.spec.ts new file mode 100644 index 000000000..804243e56 --- /dev/null +++ b/packages/oidc-client/src/login.spec.ts @@ -0,0 +1,185 @@ +// Tests for the guards added to loginCallbackAsync to surface missing / +// mismatched state and missing nonce as typed `OidcStateError` instead of a +// generic TypeError. See https://github.com/AxaFrance/oidc-client/issues/1678 +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { ILOidcLocation } from './location'; +import { loginCallbackAsync } from './login'; +import { OidcError, OidcErrorCode } from './oidcError'; +import { OidcStateError, OidcStateErrorCode } from './oidcStateError'; + +const makeStorage = (): Storage => { + const store: Record = {}; + return { + getItem: (key: string) => store[key] ?? null, + setItem: (key: string, value: string) => { + store[key] = value; + }, + removeItem: (key: string) => { + delete store[key]; + }, + clear: () => { + for (const key of Object.keys(store)) { + delete store[key]; + } + }, + get length() { + return Object.keys(store).length; + }, + key: (index: number) => Object.keys(store)[index] ?? null, + [Symbol.iterator]: function* () { + yield* Object.entries(store); + }, + } as unknown as Storage; +}; + +class FakeLocation implements ILOidcLocation { + constructor(private currentHref: string) {} + open(): void {} + reload(): void {} + getCurrentHref(): string { + return this.currentHref; + } + getPath(): string { + return '/callback'; + } + getOrigin(): string { + return 'http://localhost:4200'; + } +} + +const buildOidc = ({ href, storage }: { href: string; storage: Storage }) => { + const publishedEvents: Array<{ name: string; data: unknown }> = []; + const configurationName = 'default'; + const configuration = { + client_id: 'interactive.public.short', + redirect_uri: 'http://localhost:4200/authentication/callback', + silent_redirect_uri: 'http://localhost:4200/authentication/silent-callback', + scope: 'openid profile email', + authority: 'http://api', + refresh_time_before_tokens_expiration_in_second: 70, + token_request_timeout: 30000, + authority_configuration: null, + storage, + login_state_storage: storage, + // no service_worker_relative_url -> initWorkerAsync returns null + }; + const oidc: any = { + configuration, + configurationName, + location: new FakeLocation(href), + publishEvent: (name: string, data: unknown) => { + publishedEvents.push({ name, data }); + }, + initAsync: vi.fn(async () => ({ + issuer: 'http://api', + authorizationEndpoint: 'http://api/connect/authorize', + tokenEndpoint: 'http://api/connect/token', + checkSessionIframe: 'http://api/connect/checksession', + })), + startCheckSessionAsync: vi.fn(async () => undefined), + }; + return { oidc, publishedEvents }; +}; + +describe('loginCallbackAsync — state/nonce guards (issue #1678)', () => { + let storage: Storage; + + beforeEach(() => { + storage = makeStorage(); + }); + + it('throws OidcStateError(STATE_MISSING) when stored state is missing but callback URL contains state', async () => { + // No `oidc.state.default` written into storage at all (simulates a + // private-browsing tab, manual storage clear, or browser eviction + // between the authorize redirect and the callback). + const href = 'http://localhost:4200/authentication/callback?code=abc&state=server-state-value'; + const { oidc, publishedEvents } = buildOidc({ href, storage }); + + await expect(loginCallbackAsync(oidc)()).rejects.toBeInstanceOf(OidcStateError); + await expect(loginCallbackAsync(oidc)()).rejects.toMatchObject({ + code: OidcStateErrorCode.STATE_MISSING, + }); + + // The error must also be published as a loginCallbackAsync_error event + // so listeners (incl. the React provider) can react to it. + const errorEvent = publishedEvents.find(e => e.name === 'loginCallbackAsync_error'); + expect(errorEvent).toBeDefined(); + expect(errorEvent!.data).toBeInstanceOf(OidcStateError); + }); + + it('throws OidcStateError(STATE_MISMATCH) when the stored state differs from the returned one', async () => { + storage[`oidc.state.default`] = 'stored-state-value'; + storage[`oidc.nonce.default`] = 'stored-nonce-value'; + const href = + 'http://localhost:4200/authentication/callback?code=abc&state=different-state-value'; + const { oidc } = buildOidc({ href, storage }); + + await expect(loginCallbackAsync(oidc)()).rejects.toBeInstanceOf(OidcStateError); + await expect(loginCallbackAsync(oidc)()).rejects.toMatchObject({ + code: OidcStateErrorCode.STATE_MISMATCH, + }); + }); + + it('throws OidcStateError(NONCE_MISSING) when state is valid but nonce is missing from storage', async () => { + storage[`oidc.state.default`] = 'matching-state'; + // No oidc.nonce.default written -> getNonceAsync returns { nonce: undefined } + const href = 'http://localhost:4200/authentication/callback?code=abc&state=matching-state'; + const { oidc } = buildOidc({ href, storage }); + + await expect(loginCallbackAsync(oidc)()).rejects.toBeInstanceOf(OidcStateError); + await expect(loginCallbackAsync(oidc)()).rejects.toMatchObject({ + code: OidcStateErrorCode.NONCE_MISSING, + }); + }); + + it('does not throw a generic TypeError when state and nonce are both missing', async () => { + // Regression: before the fix, a missing nonce would surface as + // "Cannot read properties of undefined (reading 'nonce')" when reaching + // isTokensOidcValid(..., nonceData.nonce, ...). + const href = 'http://localhost:4200/authentication/callback?code=abc&state=server-state-value'; + const { oidc } = buildOidc({ href, storage }); + + let caught: unknown; + try { + await loginCallbackAsync(oidc)(); + } catch (e) { + caught = e; + } + expect(caught).toBeInstanceOf(OidcStateError); + expect(caught).not.toBeInstanceOf(TypeError); + }); +}); + +describe('loginCallbackAsync — OAuth errors', () => { + it.each([ + ['login_required', OidcErrorCode.LOGIN_REQUIRED], + ['consent_required', OidcErrorCode.CONSENT_REQUIRED], + ['interaction_required', OidcErrorCode.INTERACTION_REQUIRED], + ['access_denied', OidcErrorCode.OAUTH_ERROR], + ] as const)('throws a typed %s callback error', async (oauthError, code) => { + const storage = makeStorage(); + const description = 'Authentication is required'; + const href = `http://localhost:4200/authentication/callback?error=${oauthError}&error_description=${encodeURIComponent(description)}`; + const { oidc, publishedEvents } = buildOidc({ href, storage }); + + let caught: unknown; + try { + await loginCallbackAsync(oidc)(); + } catch (error) { + caught = error; + } + + expect(caught).toBeInstanceOf(OidcError); + expect(caught).toMatchObject({ + code, + phase: 'callback', + oauthError, + oauthErrorDescription: description, + message: `Error from OIDC server: ${oauthError} - ${description}`, + }); + expect(publishedEvents.find(event => event.name === 'loginCallbackAsync_error')?.data).toBe( + caught, + ); + }); +}); diff --git a/packages/oidc-client/src/login.ts b/packages/oidc-client/src/login.ts index f738b1791..df43516d3 100644 --- a/packages/oidc-client/src/login.ts +++ b/packages/oidc-client/src/login.ts @@ -2,173 +2,418 @@ import { generateRandom } from './crypto.js'; import { eventNames } from './events.js'; import { initSession } from './initSession.js'; import { initWorkerAsync } from './initWorker.js'; +import { generateJwkAsync, generateJwtDemonstratingProofOfPossessionAsync } from './jwt'; +import { ILOidcLocation } from './location'; +import Oidc from './oidc'; +import { + createNetworkError, + createOAuthError, + isNetworkErrorCause, + isOidcError, + isRetryableHttpStatus, + OidcError, + OidcErrorCode, +} from './oidcError.js'; +import { OidcStateError, OidcStateErrorCode } from './oidcStateError.js'; import { isTokensOidcValid } from './parseTokens.js'; +import { + PushedAuthorizationRequestError, + PushedAuthorizationRequestErrorCode, +} from './pushedAuthorizationRequestError.js'; import { performAuthorizationRequestAsync, performFirstTokenRequestAsync } from './requests.js'; import { getParseQueryStringFromLocation } from './route-utils.js'; -import { OidcConfiguration, StringMap } from './types.js'; +import { Fetch, OidcConfiguration, StringMap } from './types.js'; -// eslint-disable-next-line @typescript-eslint/ban-types -export const defaultLoginAsync = (window, configurationName, configuration:OidcConfiguration, publishEvent :(string, any)=>void, initAsync:Function) => (callbackPath:string = undefined, extras:StringMap = null, isSilentSignin = false, scope:string = undefined) => { +export type InitAsyncFunction = (authority: string, authorityConfiguration: any) => Promise; + +export const defaultLoginAsync = + ( + configurationName: string, + configuration: OidcConfiguration, + publishEvent: (string, any) => void, + initAsync: InitAsyncFunction, + oidcLocation: ILOidcLocation, + getFetch: () => Fetch = () => window.fetch, + ) => + ( + callbackPath: string = undefined, + extras: StringMap = null, + isSilentSignin = false, + scope: string = undefined, + ): Promise => { const originExtras = extras; extras = { ...extras }; const loginLocalAsync = async () => { - const location = window.location; - const url = callbackPath || location.pathname + (location.search || '') + (location.hash || ''); - - if (!('state' in extras)) { - extras.state = generateRandom(16); - } - - publishEvent(eventNames.loginAsync_begin, {}); - if (extras) { - for (const key of Object.keys(extras)) { - if (key.endsWith(':token_request')) { - delete extras[key]; - } - } - } - try { - const redirectUri = isSilentSignin ? configuration.silent_redirect_uri : configuration.redirect_uri; - if (!scope) { - scope = configuration.scope; - } - - const extraFinal = !configuration.extras ? extras : { ...configuration.extras, ...extras }; - if (!extraFinal.nonce) { - extraFinal.nonce = generateRandom(12); - } - const nonce = { nonce: extraFinal.nonce }; - const serviceWorker = await initWorkerAsync(configuration.service_worker_relative_url, configurationName); - const oidcServerConfiguration = await initAsync(configuration.authority, configuration.authority_configuration); - let storage; - if (serviceWorker) { - serviceWorker.setLoginParams(configurationName, { callbackPath: url, extras: originExtras }); - serviceWorker.startKeepAliveServiceWorker(); - await serviceWorker.initAsync(oidcServerConfiguration, 'loginAsync', configuration); - await serviceWorker.setNonceAsync(nonce); - storage = serviceWorker; - } else { - const session = initSession(configurationName, configuration.storage ?? sessionStorage); - session.setLoginParams(configurationName, { callbackPath: url, extras: originExtras }); - await session.setNonceAsync(nonce); - storage = session; - } - - // @ts-ignore - const extraInternal = { - client_id: configuration.client_id, - redirect_uri: redirectUri, - scope, - response_type: 'code', - ...extraFinal, - }; - await performAuthorizationRequestAsync(storage)(oidcServerConfiguration.authorizationEndpoint, extraInternal); - } catch (exception) { - publishEvent(eventNames.loginAsync_error, exception); - throw exception; + const url = callbackPath || oidcLocation.getPath(); + + if (!('state' in extras)) { + extras.state = generateRandom(16); + } + + publishEvent(eventNames.loginAsync_begin, {}); + if (extras) { + for (const key of Object.keys(extras)) { + if (key.endsWith(':token_request')) { + delete extras[key]; + } + } + } + try { + const redirectUri = isSilentSignin + ? configuration.silent_redirect_uri + : configuration.redirect_uri; + if (!scope) { + scope = configuration.scope; } - }; - return loginLocalAsync(); -}; -export const loginCallbackAsync = (oidc) => async (isSilentSignin = false) => { - try { - oidc.publishEvent(eventNames.loginCallbackAsync_begin, {}); - const configuration = oidc.configuration; - const clientId = configuration.client_id; - const redirectUri = isSilentSignin ? configuration.silent_redirect_uri : configuration.redirect_uri; - const authority = configuration.authority; - const tokenRequestTimeout = configuration.token_request_timeout; - const oidcServerConfiguration = await oidc.initAsync(authority, configuration.authority_configuration); - const queryParams = getParseQueryStringFromLocation(window.location.href); - const sessionState = queryParams.session_state; - const serviceWorker = await initWorkerAsync(configuration.service_worker_relative_url, oidc.configurationName); + const extraFinal = !configuration.extras ? extras : { ...configuration.extras, ...extras }; + if (!extraFinal.nonce) { + extraFinal.nonce = generateRandom(12); + } + const nonce = { nonce: extraFinal.nonce }; + const serviceWorker = await initWorkerAsync(configuration, configurationName); + const oidcServerConfiguration = await initAsync( + configuration.authority, + configuration.authority_configuration, + ); let storage; - let nonceData; - let getLoginParams; - let state; if (serviceWorker) { - serviceWorker.startKeepAliveServiceWorker(); - await serviceWorker.initAsync(oidcServerConfiguration, 'loginCallbackAsync', configuration); - await serviceWorker.setSessionStateAsync(sessionState); - nonceData = await serviceWorker.getNonceAsync(); - getLoginParams = serviceWorker.getLoginParams(oidc.configurationName); - state = await serviceWorker.getStateAsync(); - storage = serviceWorker; + serviceWorker.setLoginParams({ callbackPath: url, extras: originExtras, scope: scope }); + await serviceWorker.initAsync(oidcServerConfiguration, 'loginAsync', configuration); + await serviceWorker.setNonceAsync(nonce); + serviceWorker.startKeepAliveServiceWorker(); + storage = serviceWorker; } else { - const session = initSession(oidc.configurationName, configuration.storage ?? sessionStorage); - await session.setSessionStateAsync(sessionState); - nonceData = await session.getNonceAsync(); - getLoginParams = session.getLoginParams(oidc.configurationName); - state = await session.getStateAsync(); - storage = session; + const session = initSession( + configurationName, + configuration.storage ?? sessionStorage, + configuration.login_state_storage ?? configuration.storage ?? sessionStorage, + ); + session.setLoginParams({ callbackPath: url, extras: originExtras, scope: scope }); + await session.setNonceAsync(nonce); + storage = session; } - const params = getParseQueryStringFromLocation(window.location.toString()); + // @ts-ignore + const extraInternal = { + client_id: configuration.client_id, + redirect_uri: redirectUri, + scope, + response_type: 'code', + ...extraFinal, + }; - if (params.iss && params.iss !== oidcServerConfiguration.issuer) { - throw new Error('issuer not valid'); - } - if (params.state && params.state !== state) { - throw new Error('state not valid'); + const parMode = configuration.par ?? 'disabled'; + const pushedAuthorizationRequestEndpoint = + oidcServerConfiguration.pushedAuthorizationRequestEndpoint; + const serverRequiresPar = oidcServerConfiguration.requirePushedAuthorizationRequests; + if ( + !pushedAuthorizationRequestEndpoint && + (parMode === 'required' || (parMode === 'auto' && serverRequiresPar)) + ) { + throw new PushedAuthorizationRequestError( + PushedAuthorizationRequestErrorCode.ENDPOINT_UNAVAILABLE, + 'Pushed Authorization Requests are required, but the authorization server metadata does not provide a pushed_authorization_request_endpoint.', + ); } - const data = { - code: params.code, - grant_type: 'authorization_code', - client_id: configuration.client_id, - redirect_uri: redirectUri, - }; + const pushedAuthorizationRequest = + parMode !== 'disabled' && pushedAuthorizationRequestEndpoint + ? { + endpoint: pushedAuthorizationRequestEndpoint, + fetch: getFetch(), + timeoutMs: configuration.par_request_timeout, + } + : undefined; - const extras = {}; - // @ts-ignore - if (configuration.token_request_extras) { - for (const [key, value] of Object.entries(configuration.token_request_extras)) { - extras[key] = value; - } + await performAuthorizationRequestAsync(storage, oidcLocation)( + oidcServerConfiguration.authorizationEndpoint, + extraInternal, + pushedAuthorizationRequest, + ); + } catch (exception) { + const error = isNetworkErrorCause(exception) + ? createNetworkError(exception, 'login') + : exception; + publishEvent(eventNames.loginAsync_error, error); + throw error; + } + }; + return loginLocalAsync(); + }; + +export const loginCallbackAsync = + (oidc: Oidc) => + async (isSilentSignin = false) => { + try { + oidc.publishEvent(eventNames.loginCallbackAsync_begin, {}); + const configuration = oidc.configuration; + const clientId = configuration.client_id; + const redirectUri = isSilentSignin + ? configuration.silent_redirect_uri + : configuration.redirect_uri; + const authority = configuration.authority; + const tokenRequestTimeout = configuration.token_request_timeout; + const oidcServerConfiguration = await oidc.initAsync( + authority, + configuration.authority_configuration, + ); + const href = oidc.location.getCurrentHref(); + const queryParams = getParseQueryStringFromLocation(href); + const sessionState = queryParams.session_state; + const serviceWorker = await initWorkerAsync(configuration, oidc.configurationName); + let storage; + let nonceData; + let getLoginParams; + let state; + if (serviceWorker) { + await serviceWorker.initAsync(oidcServerConfiguration, 'loginCallbackAsync', configuration); + await serviceWorker.setSessionStateAsync(sessionState); + nonceData = await serviceWorker.getNonceAsync(); + getLoginParams = serviceWorker.getLoginParams(); + state = await serviceWorker.getStateAsync(); + serviceWorker.startKeepAliveServiceWorker(); + storage = serviceWorker; + } else { + const session = initSession( + oidc.configurationName, + configuration.storage ?? sessionStorage, + configuration.login_state_storage ?? configuration.storage ?? sessionStorage, + ); + await session.setSessionStateAsync(sessionState); + nonceData = await session.getNonceAsync(); + getLoginParams = session.getLoginParams(); + state = await session.getStateAsync(); + storage = session; + } + + if (queryParams.error || queryParams.error_description) { + throw createOAuthError( + queryParams.error, + queryParams.error_description, + `Error from OIDC server: ${queryParams.error} - ${queryParams.error_description}`, + isSilentSignin ? 'refresh' : 'callback', + ); + } + + if (queryParams.iss && queryParams.iss !== oidcServerConfiguration.issuer) { + console.error(); + throw new Error( + `Issuer not valid (expected: ${oidcServerConfiguration.issuer}, received: ${queryParams.iss})`, + ); + } + // Surface missing / mismatched login state as a typed, identifiable error + // rather than a generic TypeError when later dereferencing nonceData.nonce. + // See https://github.com/AxaFrance/oidc-client/issues/1678 + if (queryParams.state) { + if (!state) { + throw new OidcStateError( + OidcStateErrorCode.STATE_MISSING, + 'OIDC state is missing from storage. The login state may have been cleared between the authorization redirect and the callback (e.g., private browsing, storage cleared, or browser eviction).', + isSilentSignin ? 'refresh' : 'callback', + ); } - if (getLoginParams && getLoginParams.extras) { - for (const [key, value] of Object.entries(getLoginParams.extras)) { - if (key.endsWith(':token_request')) { - extras[key.replace(':token_request', '')] = value; - } - } + if (queryParams.state !== state) { + throw new OidcStateError( + OidcStateErrorCode.STATE_MISMATCH, + `OIDC state does not match the stored one (expected: ${state}, received: ${queryParams.state}).`, + isSilentSignin ? 'refresh' : 'callback', + ); } + } + if (!nonceData || !nonceData.nonce) { + throw new OidcStateError( + OidcStateErrorCode.NONCE_MISSING, + 'OIDC nonce is missing from storage. The login state may have been cleared between the authorization redirect and the callback (e.g., private browsing, storage cleared, or browser eviction).', + isSilentSignin ? 'refresh' : 'callback', + ); + } - const tokenResponse = await performFirstTokenRequestAsync(storage)(oidcServerConfiguration.tokenEndpoint, { ...data, ...extras }, oidc.configuration.token_renew_mode, tokenRequestTimeout); + const data = { + code: queryParams.code, + grant_type: 'authorization_code', + client_id: configuration.client_id, + redirect_uri: redirectUri, + }; - if (!tokenResponse.success) { - throw new Error('Token request failed'); + const extras = {}; + // @ts-ignore + if (configuration.token_request_extras) { + for (const [key, value] of Object.entries(configuration.token_request_extras)) { + extras[key] = value; + } + } + if (getLoginParams?.extras) { + for (const [key, value] of Object.entries(getLoginParams.extras)) { + if (key.endsWith(':token_request')) { + extras[key.replace(':token_request', '')] = value; + } } + } - let loginParams; - const formattedTokens = tokenResponse.data.tokens; + const url = oidcServerConfiguration.tokenEndpoint; + const headersExtras = {}; + let demonstratingProofOfPossessionJwk; + if (configuration.demonstrating_proof_of_possession) { if (serviceWorker) { - await serviceWorker.initAsync(redirectUri, 'syncTokensAsync', configuration); - loginParams = serviceWorker.getLoginParams(oidc.configurationName); + headersExtras['DPoP'] = `DPOP_SECURED_BY_OIDC_SERVICE_WORKER_${oidc.configurationName}`; } else { - const session = initSession(oidc.configurationName, configuration.storage); - loginParams = session.getLoginParams(oidc.configurationName); + demonstratingProofOfPossessionJwk = await generateJwkAsync(window)( + configuration.demonstrating_proof_of_possession_configuration.generateKeyAlgorithm, + ); + const session = initSession( + oidc.configurationName, + configuration.storage, + configuration.login_state_storage ?? configuration.storage, + ); + await session.setDemonstratingProofOfPossessionJwkAsync( + demonstratingProofOfPossessionJwk, + ); + headersExtras['DPoP'] = await generateJwtDemonstratingProofOfPossessionAsync(window)( + configuration.demonstrating_proof_of_possession_configuration, + )(demonstratingProofOfPossessionJwk, 'POST', url); } - // @ts-ignore - if (tokenResponse.data.state !== extras.state) { - throw new Error('state is not valid'); + } + + let tokenResponse = await performFirstTokenRequestAsync(storage)( + url, + { ...data, ...extras }, + headersExtras, + oidc.configuration.token_renew_mode, + tokenRequestTimeout, + configuration.demonstrating_proof_of_possession, + ); + + if ( + !tokenResponse.success && + tokenResponse.oauthError === 'use_dpop_nonce' && + tokenResponse.demonstratingProofOfPossessionNonce && + configuration.demonstrating_proof_of_possession + ) { + await storage.setDemonstratingProofOfPossessionNonce( + tokenResponse.demonstratingProofOfPossessionNonce, + ); + if (!serviceWorker) { + headersExtras['DPoP'] = await generateJwtDemonstratingProofOfPossessionAsync(window)( + configuration.demonstrating_proof_of_possession_configuration, + )(demonstratingProofOfPossessionJwk, 'POST', url, { + nonce: tokenResponse.demonstratingProofOfPossessionNonce, + }); } - const { isValid, reason } = isTokensOidcValid(formattedTokens, nonceData.nonce, oidcServerConfiguration); - if (!isValid) { - throw new Error(`Tokens are not OpenID valid, reason: ${reason}`); + tokenResponse = await performFirstTokenRequestAsync(storage)( + url, + { ...data, ...extras }, + headersExtras, + oidc.configuration.token_renew_mode, + tokenRequestTimeout, + ); + } else if (!tokenResponse.success && configuration.demonstrating_proof_of_possession) { + await Promise.all([storage.setCodeVerifierAsync(null), storage.setStateAsync(null)]); + } + + if (!tokenResponse.success) { + const code = + tokenResponse.oauthError === 'use_dpop_nonce' + ? OidcErrorCode.DPOP_NONCE_REQUIRED + : OidcErrorCode.TOKEN_REQUEST_FAILED; + throw new OidcError(code, 'Token request failed', { + phase: isSilentSignin ? 'refresh' : 'callback', + retryable: + code === OidcErrorCode.DPOP_NONCE_REQUIRED || + isRetryableHttpStatus(tokenResponse.status), + status: tokenResponse.status, + oauthError: tokenResponse.oauthError, + oauthErrorDescription: tokenResponse.oauthErrorDescription, + }); + } + + let loginParams; + const formattedTokens = tokenResponse.data.tokens; + const demonstratingProofOfPossessionNonce = + tokenResponse.data.demonstratingProofOfPossessionNonce; + + // @ts-ignore + if (tokenResponse.data.state !== extras.state) { + throw new OidcError(OidcErrorCode.INVALID_STATE, 'state is not valid', { + phase: isSilentSignin ? 'refresh' : 'callback', + retryable: false, + }); + } + const { isValid, reason } = isTokensOidcValid( + formattedTokens, + nonceData.nonce, + oidcServerConfiguration, + ); + if (!isValid) { + const message = `Tokens are not OpenID valid, reason: ${reason}`; + if (reason.startsWith('Nonce does not match')) { + throw new OidcError(OidcErrorCode.INVALID_NONCE, message, { + phase: isSilentSignin ? 'refresh' : 'callback', + retryable: false, + }); } + throw new Error(message); + } - await oidc.startCheckSessionAsync(oidcServerConfiguration.checkSessionIframe, clientId, sessionState, isSilentSignin); - oidc.publishEvent(eventNames.loginCallbackAsync_end, {}); - return { - tokens: formattedTokens, - state: 'request.state', - callbackPath: loginParams.callbackPath, - }; + if (serviceWorker) { + if ( + formattedTokens.refreshToken && + !formattedTokens.refreshToken.includes('SECURED_BY_OIDC_SERVICE_WORKER') + ) { + throw new Error('Refresh token should be hidden by service worker'); + } + + if ( + demonstratingProofOfPossessionNonce && + formattedTokens?.accessToken.includes('SECURED_BY_OIDC_SERVICE_WORKER') + ) { + throw new Error( + 'Demonstration of proof of possession require Access token not hidden by service worker', + ); + } + } + + if (serviceWorker) { + await serviceWorker.initAsync(oidcServerConfiguration, 'syncTokensAsync', configuration); + loginParams = serviceWorker.getLoginParams(); + if (demonstratingProofOfPossessionNonce) { + await serviceWorker.setDemonstratingProofOfPossessionNonce( + demonstratingProofOfPossessionNonce, + ); + } + } else { + const session = initSession( + oidc.configurationName, + configuration.storage, + configuration.login_state_storage ?? configuration.storage, + ); + loginParams = session.getLoginParams(); + if (demonstratingProofOfPossessionNonce) { + await session.setDemonstratingProofOfPossessionNonce(demonstratingProofOfPossessionNonce); + } + } + + await oidc.startCheckSessionAsync( + oidcServerConfiguration.checkSessionIframe, + clientId, + sessionState, + isSilentSignin, + ); + oidc.publishEvent(eventNames.loginCallbackAsync_end, {}); + return { + tokens: formattedTokens, + state: 'request.state', + callbackPath: loginParams.callbackPath, + scope: queryParams.scope, + extras: loginParams.extras, + }; } catch (exception) { - console.error(exception); - oidc.publishEvent(eventNames.loginCallbackAsync_error, exception); - throw exception; + const error = + !isOidcError(exception) && isNetworkErrorCause(exception) + ? createNetworkError(exception, isSilentSignin ? 'refresh' : 'callback') + : exception; + console.error(error); + oidc.publishEvent(eventNames.loginCallbackAsync_error, error); + throw error; } -}; + }; diff --git a/packages/oidc-client/src/logout.spec.ts b/packages/oidc-client/src/logout.spec.ts index 82035047c..b7fbcfa90 100644 --- a/packages/oidc-client/src/logout.spec.ts +++ b/packages/oidc-client/src/logout.spec.ts @@ -1,63 +1,406 @@ -import '@testing-library/jest-dom' -import { logoutAsync } from "./logout"; -import { describe, it, expect, vi } from 'vitest'; +// import '@testing-library/jest-dom'; + +import { describe, expect, it, vi } from 'vitest'; + +import { eventNames } from './events'; +import { ILOidcLocation } from './location'; +import { clearSessionAsync, logoutAsync } from './logout'; +import { OidcErrorCode } from './oidcError'; describe('Logout test suite', () => { + const expectedFinalUrl = + 'http://api/connect/endsession?id_token_hint=abcd&post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Flogged_out'; + it.each([ + { + logout_tokens_to_invalidate: ['access_token', 'refresh_token'], + extras: null, + expectedResults: [ + 'token=abcd&token_type_hint=access_token&client_id=interactive.public.short', + 'token=abdc&token_type_hint=refresh_token&client_id=interactive.public.short', + ], + expectedFinalUrl, + }, + { + logout_tokens_to_invalidate: ['refresh_token'], + extras: null, + expectedResults: [ + 'token=abdc&token_type_hint=refresh_token&client_id=interactive.public.short', + ], + expectedFinalUrl, + }, + { + logout_tokens_to_invalidate: ['access_token'], + extras: null, + expectedResults: [ + 'token=abcd&token_type_hint=access_token&client_id=interactive.public.short', + ], + expectedFinalUrl, + }, + { logout_tokens_to_invalidate: [], extras: null, expectedResults: [], expectedFinalUrl }, + { + logout_tokens_to_invalidate: [], + extras: { id_token_hint: undefined }, + expectedResults: [], + expectedFinalUrl: + 'http://api/connect/endsession?post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Flogged_out', + }, + { + logout_tokens_to_invalidate: [], + extras: { 'no_reload:oidc': 'true' }, + expectedResults: [], + expectedFinalUrl: '', + }, + { + logout_tokens_to_invalidate: ['refresh_token'], + extras: { 'client_secret:revoke_refresh_token': 'secret' }, + expectedResults: [ + 'token=abdc&token_type_hint=refresh_token&client_id=interactive.public.short&client_secret=secret', + ], + expectedFinalUrl, + }, + { + logout_tokens_to_invalidate: ['access_token'], + extras: { 'client_secret:revoke_access_token': 'secret' }, + expectedResults: [ + 'token=abcd&token_type_hint=access_token&client_id=interactive.public.short&client_secret=secret', + ], + expectedFinalUrl, + }, + ])( + 'Logout should revoke tokens $logout_tokens_to_invalidate', + async ({ logout_tokens_to_invalidate, extras = null, expectedResults, expectedFinalUrl }) => { + const configuration = { + client_id: 'interactive.public.short', + redirect_uri: 'http://localhost:4200/authentication/callback', + scope: 'openid profile email api offline_access', + authority: 'http://api', + refresh_time_before_tokens_expiration_in_second: 70, + logout_tokens_to_invalidate, + }; + + const fetch = (url, data) => { + if (url === 'http://api/connect/revocation') { + return Promise.resolve({ status: 200 }); + } + return Promise.resolve({ + status: 200, + }); + }; + + const mockFetchFn = vi.fn().mockImplementation(fetch); + + const oidc = { + configuration, + tokens: { idToken: 'abcd', accessToken: 'abcd', refreshToken: 'abdc' }, + initAsync: () => + Promise.resolve({ + revocationEndpoint: 'http://api/connect/revocation', + endSessionEndpoint: 'http://api/connect/endsession', + }), + destroyAsync: () => Promise.resolve(), + logoutSameTabAsync: () => Promise.resolve(), + }; - it.each([ - {logout_tokens_to_invalidate:['access_token', 'refresh_token'], expectedResults: ["token=abcd&token_type_hint=access_token&client_id=interactive.public.short","token=abdc&token_type_hint=refresh_token&client_id=interactive.public.short"]}, - {logout_tokens_to_invalidate:['refresh_token'], expectedResults: ["token=abdc&token_type_hint=refresh_token&client_id=interactive.public.short"]}, - {logout_tokens_to_invalidate:['access_token'], expectedResults: ["token=abcd&token_type_hint=access_token&client_id=interactive.public.short"]}, - {logout_tokens_to_invalidate:[], expectedResults: []}, - ])('Logout should revoke tokens $logout_tokens_to_invalidate', async ({ logout_tokens_to_invalidate, expectedResults}) => { - - const configuration = { - client_id: 'interactive.public.short', - redirect_uri: 'http://localhost:4200/authentication/callback', - scope: 'openid profile email api offline_access', - authority: 'http://api', - refresh_time_before_tokens_expiration_in_second: 70, - logout_tokens_to_invalidate - }; - - const fetch = (url, data) => { - if(url === "http://api/connect/revocation") { - return Promise.resolve({status: 200}); - } - return Promise.resolve({ - status : 200, - }); - }; - - const mockFetchFn = vi.fn().mockImplementation(fetch) - - const oidc = { - configuration, - tokens : {idToken: "abcd", accessToken: "abcd", refreshToken: "abdc" }, - initAsync: () => Promise.resolve({ - revocationEndpoint: "http://api/connect/revocation", - endSessionEndpoint: "http://api/connect/endsession", - }), - destroyAsync: () => Promise.resolve(), - logoutSameTabAsync: () => Promise.resolve(), - }; - - const oidcDatabase = {default: () => oidc}; - - const window = { - location: { - href: "", - origin: "http://localhost:4200" - } + const oidcDatabase = { default: () => oidc }; + + let finalUrl = ''; + class OidcLocationMock implements ILOidcLocation { + open(url: string): void { + finalUrl = url; + } + + getCurrentHref(): string { + return ''; } - await logoutAsync(oidc, oidcDatabase, mockFetchFn, window, console)("/logged_out"); - - // @ts-ignore + getPath(): string { + return ''; + } + + reload(): void {} + + getOrigin(): string { + return 'http://localhost:4200'; + } + } + + await logoutAsync( + oidc, + oidcDatabase, + mockFetchFn, + console, + new OidcLocationMock(), + )('/logged_out', extras); + + // @ts-ignore + + const results = mockFetchFn.mock.calls.map((call, index) => call[1].body); + + expect(results).toEqual(expectedResults); + expect(finalUrl).toBe(expectedFinalUrl); + }, + ); + + it('navigates to end_session_endpoint before clearing the local session (issue #1677)', async () => { + // This is the regression test for the race described in issue #1677. + // `OidcProvider`/`OidcSecure` watches `oidc.tokens`; if `destroyAsync` + // runs before `oicLocation.open`, the React tree can briefly observe a + // null token state and kick off a new auth flow before the navigation + // to the IdP's end-session endpoint commits. + const configuration = { + client_id: 'interactive.public.short', + authority: 'http://api', + logout_tokens_to_invalidate: ['access_token', 'refresh_token'], + }; + + const callOrder: string[] = []; + + const mockFetchFn = vi.fn().mockImplementation(() => { + callOrder.push('revoke'); + return Promise.resolve({ status: 200 }); + }); + + const oidc: any = { + configuration, + tokens: { + idToken: 'abcd', + accessToken: 'abcd', + refreshToken: 'abdc', + idTokenPayload: { sub: 'sub-123' }, + }, + isLoggingOut: false, + initAsync: () => + Promise.resolve({ + revocationEndpoint: 'http://api/connect/revocation', + endSessionEndpoint: 'http://api/connect/endsession', + }), + destroyAsync: vi.fn().mockImplementation(() => { + callOrder.push('destroyAsync'); + oidc.tokens = null; + return Promise.resolve(); + }), + logoutSameTabAsync: () => Promise.resolve(), + publishEvent: (name: string) => callOrder.push(`publishEvent:${name}`), + }; + + const oidcDatabase = { default: oidc }; + + let navigatedUrl = ''; + class OidcLocationMock implements ILOidcLocation { + open(url: string): void { + callOrder.push('open'); + navigatedUrl = url; + } + getCurrentHref() { + return ''; + } + getPath() { + return ''; + } + reload() { + callOrder.push('reload'); + } + getOrigin() { + return 'http://localhost:4200'; + } + } + + await logoutAsync( + oidc, + oidcDatabase, + mockFetchFn, + console, + new OidcLocationMock(), + )('/logged_out', null); + + // Revocation comes first (so tokens are still valid when revoked), + // navigation comes second (page starts unloading), and only then we + // clear local state and broadcast `logout_from_same_tab`. + expect(callOrder[0]).toBe('revoke'); + expect(callOrder[1]).toBe('revoke'); + expect(callOrder[2]).toBe('open'); + expect(callOrder.indexOf('destroyAsync')).toBeGreaterThan(callOrder.indexOf('open')); + expect(callOrder.indexOf(`publishEvent:${eventNames.logout_from_same_tab}`)).toBeGreaterThan( + callOrder.indexOf('open'), + ); + + // The id_token_hint must still be present on the navigation URL, even + // though local tokens have been cleared by `destroyAsync` afterwards. + expect(navigatedUrl).toContain('id_token_hint=abcd'); + expect(navigatedUrl).toContain( + 'post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Flogged_out', + ); + + // The flag stays set after the call returns: the page is expected to be + // unloading and any UI re-render that briefly observes `tokens === null` + // should skip starting a new login flow. + expect(oidc.isLoggingOut).toBe(true); + }); + + it('resets isLoggingOut and does not navigate when no_reload is requested', async () => { + const configuration = { + client_id: 'interactive.public.short', + authority: 'http://api', + logout_tokens_to_invalidate: [], + }; + + const events: string[] = []; + let navigated = false; + const oidc: any = { + configuration, + tokens: { + idToken: 'abcd', + accessToken: 'abcd', + refreshToken: 'abdc', + idTokenPayload: { sub: 'sub-123' }, + }, + isLoggingOut: false, + initAsync: () => + Promise.resolve({ + revocationEndpoint: 'http://api/connect/revocation', + endSessionEndpoint: 'http://api/connect/endsession', + }), + destroyAsync: () => Promise.resolve(), + logoutSameTabAsync: () => Promise.resolve(), + publishEvent: (name: string) => events.push(name), + }; + const oidcDatabase = { default: oidc }; + + class OidcLocationMock implements ILOidcLocation { + open() { + navigated = true; + } + getCurrentHref() { + return ''; + } + getPath() { + return ''; + } + reload() { + navigated = true; + } + getOrigin() { + return 'http://localhost:4200'; + } + } + + await logoutAsync( + oidc, + oidcDatabase, + vi.fn(), + console, + new OidcLocationMock(), + )('/logged_out', { 'no_reload:oidc': 'true' }); + + expect(navigated).toBe(false); + expect(events).toContain(eventNames.logout_from_same_tab); + expect(oidc.isLoggingOut).toBe(false); + }); + + it('keeps revocation best-effort and publishes a typed logout error', async () => { + const events: Array<{ name: string; data: any }> = []; + const oidc: any = { + configuration: { + authority: 'http://api', + client_id: 'client', + logout_tokens_to_invalidate: ['access_token'], + }, + tokens: { + accessToken: 'access-token', + idToken: 'id-token', + idTokenPayload: { sub: 'subject' }, + }, + isLoggingOut: false, + initAsync: vi.fn(async () => ({ + revocationEndpoint: 'http://api/revoke', + })), + destroyAsync: vi.fn(async () => { + oidc.tokens = null; + }), + logoutSameTabAsync: vi.fn(), + publishEvent: (name: string, data: unknown) => events.push({ name, data }), + }; + const location: ILOidcLocation = { + getCurrentHref: () => '', + getOrigin: () => 'http://client', + getPath: () => '/', + open: vi.fn(), + reload: vi.fn(), + }; + const fetchMock = vi.fn(async () => { + return new Response(JSON.stringify({ error: 'temporarily_unavailable' }), { + status: 503, + headers: { 'Content-Type': 'application/json' }, + }); + }); + + await logoutAsync( + oidc, + { default: oidc }, + fetchMock, + console, + location, + )(null, { + 'no_reload:oidc': 'true', + }); + + expect(events.find(event => event.name === eventNames.logoutAsync_error)?.data).toMatchObject({ + code: OidcErrorCode.REQUEST_FAILED, + phase: 'logout', + retryable: true, + status: 503, + oauthError: 'temporarily_unavailable', + }); + expect(oidc.isLoggingOut).toBe(false); + }); + + describe('clearSessionAsync', () => { + it('clears the local session and emits logout_from_same_tab without contacting the IdP', async () => { + const events: { name: string; data: unknown }[] = []; + const oidc: any = { + configuration: { client_id: 'interactive.public.short' }, + tokens: { + idToken: 'abcd', + accessToken: 'abcd', + refreshToken: 'abdc', + idTokenPayload: { sub: 'sub-123' }, + }, + destroyAsync: vi.fn().mockImplementation(status => { + oidc.tokens = null; + events.push({ name: 'destroyAsync', data: status }); + return Promise.resolve(); + }), + logoutSameTabAsync: vi.fn().mockResolvedValue(undefined), + publishEvent: (name: string, data: unknown) => events.push({ name, data }), + }; + const oidcDatabase = { default: oidc }; + + await clearSessionAsync(oidc, oidcDatabase)(); + + expect(oidc.destroyAsync).toHaveBeenCalledWith('LOGGED_OUT'); + expect(oidc.tokens).toBeNull(); + expect(events.some(e => e.name === eventNames.logout_from_same_tab)).toBe(true); + }); + + it('calls logoutSameTabAsync for sibling OIDC clients registered in the same tab', async () => { + const oidc: any = { + configuration: { client_id: 'interactive.public.short' }, + tokens: { + idToken: 'abcd', + accessToken: 'abcd', + refreshToken: 'abdc', + idTokenPayload: { sub: 'sub-123' }, + }, + destroyAsync: () => Promise.resolve(), + logoutSameTabAsync: vi.fn().mockResolvedValue(undefined), + publishEvent: () => undefined, + }; + const sibling: any = { configuration: { client_id: 'other' } }; + const oidcDatabase = { default: oidc, other: sibling }; + + await clearSessionAsync(oidc, oidcDatabase)(); - const results = mockFetchFn.mock.calls.map((call, index) => call[1].body) - - expect(results).toEqual(expectedResults); - expect(window.location.href).toBe("http://api/connect/endsession?id_token_hint=abcd&post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Flogged_out"); + expect(oidc.logoutSameTabAsync).toHaveBeenCalledWith('interactive.public.short', 'sub-123'); }); + }); }); diff --git a/packages/oidc-client/src/logout.ts b/packages/oidc-client/src/logout.ts index 76296ecfb..8c9cf64ab 100644 --- a/packages/oidc-client/src/logout.ts +++ b/packages/oidc-client/src/logout.ts @@ -1,101 +1,280 @@ +import { eventNames } from './events'; import { initSession } from './initSession.js'; import { initWorkerAsync } from './initWorker.js'; +import { ILOidcLocation } from './location'; +import { + createNetworkError, + isNetworkErrorCause, + isOidcError, + isRetryableHttpStatus, + OidcError, + OidcErrorCode, +} from './oidcError.js'; import { performRevocationRequestAsync, TOKEN_TYPE } from './requests.js'; import timer from './timer.js'; import { StringMap } from './types.js'; export const oidcLogoutTokens = { - access_token: 'access_token', - refresh_token: 'refresh_token', + access_token: 'access_token', + refresh_token: 'refresh_token', }; -export const destroyAsync = (oidc) => async (status) => { - timer.clearTimeout(oidc.timeoutId); - oidc.timeoutId = null; - if (oidc.checkSessionIFrame) { - oidc.checkSessionIFrame.stop(); +const extractExtras = (extras: StringMap, postKey: string): StringMap => { + const postExtras: StringMap = {}; + if (extras) { + for (const [key, value] of Object.entries(extras)) { + if (key.endsWith(postKey)) { + const newKey = key.replace(postKey, ''); + postExtras[newKey] = value; + } } - const serviceWorker = await initWorkerAsync(oidc.configuration.service_worker_relative_url, oidc.configurationName); - if (!serviceWorker) { - const session = initSession(oidc.configurationName, oidc.configuration.storage); - await session.clearAsync(status); + return postExtras; + } + return postExtras; +}; + +const keepExtras = (extras: StringMap): StringMap => { + const postExtras: StringMap = {}; + if (extras) { + for (const [key, value] of Object.entries(extras)) { + if (!key.includes(':')) { + postExtras[key] = value; + } + } + return postExtras; + } + return postExtras; +}; + +export const destroyAsync = oidc => async status => { + timer.clearTimeout(oidc.timeoutId); + oidc.timeoutId = null; + if (oidc.checkSessionIFrame) { + oidc.checkSessionIFrame.stop(); + } + const serviceWorker = await initWorkerAsync(oidc.configuration, oidc.configurationName); + if (!serviceWorker) { + const session = initSession( + oidc.configurationName, + oidc.configuration.storage, + oidc.configuration.login_state_storage ?? oidc.configuration.storage, + ); + await session.clearAsync(status); + } else { + await serviceWorker.clearAsync(status); + } + oidc.tokens = null; + oidc.userInfo = null; +}; + +/** + * Clears the local OIDC session (tokens, user info, service-worker storage) + * and broadcasts `logout_from_same_tab` to any other OIDC clients registered + * in the same tab. + * + * It is intentionally decoupled from `logoutAsync`: callers that want to drop + * the local session without contacting the identity provider — for example a + * service-worker-only flow, a SPA-only logout, or an error-recovery path — + * can use this helper directly. `logoutAsync` itself calls it as the very + * last step, after the browser navigation to `end_session_endpoint` has been + * scheduled, so that the React tree never observes a transient "no tokens" + * state before the page is unloaded. + */ +export const clearSessionAsync = (oidc, oidcDatabase) => async () => { + const sub = oidc.tokens?.idTokenPayload?.sub ?? null; + await oidc.destroyAsync('LOGGED_OUT'); + for (const [, itemOidc] of Object.entries(oidcDatabase)) { + if (itemOidc !== oidc) { + // @ts-ignore + await oidc.logoutSameTabAsync(oidc.configuration.client_id, sub); } else { - await serviceWorker.clearAsync(status); + oidc.publishEvent(eventNames.logout_from_same_tab, {}); + } + } +}; + +const buildEndSessionUrl = ( + endSessionEndpoint: string, + endPointExtras: StringMap, + idToken: string, + postLogoutRedirectUri: string | null, +): string => { + if (!('id_token_hint' in endPointExtras)) { + endPointExtras['id_token_hint'] = idToken; + } + if (!('post_logout_redirect_uri' in endPointExtras) && postLogoutRedirectUri !== null) { + endPointExtras['post_logout_redirect_uri'] = postLogoutRedirectUri; + } + let queryString = ''; + for (const [key, value] of Object.entries(endPointExtras)) { + if (value !== null && value !== undefined) { + if (queryString === '') { + queryString += '?'; + } else { + queryString += '&'; + } + queryString += `${key}=${encodeURIComponent(value)}`; } - oidc.tokens = null; - oidc.userInfo = null; + } + return `${endSessionEndpoint}${queryString}`; }; -export const logoutAsync = (oidc, oidcDatabase, fetch, window, console) => async (callbackPathOrUrl: string | null | undefined = undefined, extras: StringMap = null) => { +export const logoutAsync = + (oidc, oidcDatabase, fetch, console, oicLocation: ILOidcLocation) => + async (callbackPathOrUrl: string | null | undefined = undefined, extras: StringMap = null) => { const configuration = oidc.configuration; - const oidcServerConfiguration = await oidc.initAsync(configuration.authority, configuration.authority_configuration); - if (callbackPathOrUrl && (typeof callbackPathOrUrl !== 'string')) { - callbackPathOrUrl = undefined; - console.warn('callbackPathOrUrl path is not a string'); + let oidcServerConfiguration; + try { + oidcServerConfiguration = await oidc.initAsync( + configuration.authority, + configuration.authority_configuration, + ); + } catch (cause) { + const error = isNetworkErrorCause(cause) ? createNetworkError(cause, 'logout') : cause; + oidc.publishEvent(eventNames.logoutAsync_error, error); + throw error; + } + if (callbackPathOrUrl && typeof callbackPathOrUrl !== 'string') { + callbackPathOrUrl = undefined; + console.warn('callbackPathOrUrl path is not a string'); } - const path = (callbackPathOrUrl === null || callbackPathOrUrl === undefined) ? location.pathname + (location.search || '') + (location.hash || '') : callbackPathOrUrl; + const path = + callbackPathOrUrl === null || callbackPathOrUrl === undefined + ? oicLocation.getPath() + : callbackPathOrUrl; let isUri = false; if (callbackPathOrUrl) { - isUri = callbackPathOrUrl.includes('https://') || callbackPathOrUrl.includes('http://'); + isUri = callbackPathOrUrl.includes('https://') || callbackPathOrUrl.includes('http://'); } - const url = isUri ? callbackPathOrUrl : window.location.origin + path; + const postLogoutRedirectUri = + callbackPathOrUrl === null + ? null + : isUri + ? callbackPathOrUrl + : oicLocation.getOrigin() + path; + // Capture identifiers from the live session *before* any clear happens, so the + // values stay valid no matter when we drop local state. // @ts-ignore const idToken = oidc.tokens ? oidc.tokens.idToken : ''; + + // Mark the instance as "logout in progress" so consumers (OidcSecure, route + // guards, silent renew, 401 retry interceptors, …) can back off from + // triggering a new auth flow during the window between us clearing the + // local session and the browser actually navigating away. + oidc.isLoggingOut = true; + try { + try { const revocationEndpoint = oidcServerConfiguration.revocationEndpoint; if (revocationEndpoint) { - const promises = []; - const accessToken = oidc.tokens.accessToken; - if (accessToken && configuration.logout_tokens_to_invalidate.includes(oidcLogoutTokens.access_token)) { - const revokeAccessTokenPromise = performRevocationRequestAsync(fetch)(revocationEndpoint, accessToken, TOKEN_TYPE.access_token, configuration.client_id); - promises.push(revokeAccessTokenPromise); - } - const refreshToken = oidc.tokens.refreshToken; - if (refreshToken && configuration.logout_tokens_to_invalidate.includes(oidcLogoutTokens.refresh_token)) { - const revokeRefreshTokenPromise = performRevocationRequestAsync(fetch)(revocationEndpoint, refreshToken, TOKEN_TYPE.refresh_token, configuration.client_id); - promises.push(revokeRefreshTokenPromise); - } - if (promises.length > 0) { - await Promise.all(promises); + const promises = []; + const accessToken = oidc.tokens ? oidc.tokens.accessToken : null; + if ( + accessToken && + configuration.logout_tokens_to_invalidate.includes(oidcLogoutTokens.access_token) + ) { + const revokeAccessTokenExtras = extractExtras(extras, ':revoke_access_token'); + const revokeAccessTokenPromise = performRevocationRequestAsync(fetch)( + revocationEndpoint, + accessToken, + TOKEN_TYPE.access_token, + configuration.client_id, + revokeAccessTokenExtras, + ); + promises.push(revokeAccessTokenPromise); + } + const refreshToken = oidc.tokens ? oidc.tokens.refreshToken : null; + if ( + refreshToken && + configuration.logout_tokens_to_invalidate.includes(oidcLogoutTokens.refresh_token) + ) { + const revokeAccessTokenExtras = extractExtras(extras, ':revoke_refresh_token'); + const revokeRefreshTokenPromise = performRevocationRequestAsync(fetch)( + revocationEndpoint, + refreshToken, + TOKEN_TYPE.refresh_token, + configuration.client_id, + revokeAccessTokenExtras, + ); + promises.push(revokeRefreshTokenPromise); + } + if (promises.length > 0) { + // Revocation must be awaited *before* navigation, so a cancelled + // navigation can never leave valid tokens behind both in storage + // and on the authorization server. + const results = await Promise.all(promises); + for (const result of results) { + if (!result.success) { + oidc.publishEvent( + eventNames.logoutAsync_error, + new OidcError(OidcErrorCode.REQUEST_FAILED, 'Token revocation request failed', { + phase: 'logout', + retryable: isRetryableHttpStatus(result.status), + status: result.status, + oauthError: result.oauthError, + oauthErrorDescription: result.oauthErrorDescription, + }), + ); + } } + } } - } catch (exception) { - console.warn('logoutAsync: error when revoking tokens, if the error persist, you ay configure property logout_tokens_to_invalidate from configuration to avoid this error'); - console.warn(exception); - } - // @ts-ignore - const sub = oidc.tokens && oidc.tokens.idTokenPayload ? oidc.tokens.idTokenPayload.sub : null; - await oidc.destroyAsync('LOGGED_OUT'); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const [key, itemOidc] of Object.entries(oidcDatabase)) { - if (itemOidc !== oidc) { - // @ts-ignore - await oidc.logoutSameTabAsync(oidc.configuration.client_id, sub); - } - } + } catch (exception) { + const error = + !isOidcError(exception) && isNetworkErrorCause(exception) + ? createNetworkError(exception, 'logout') + : exception; + oidc.publishEvent(eventNames.logoutAsync_error, error); + console.warn( + 'logoutAsync: error when revoking tokens, if the error persist, you ay configure property logout_tokens_to_invalidate from configuration to avoid this error', + ); + console.warn(error); + } - if (oidcServerConfiguration.endSessionEndpoint) { - if (!extras) { - extras = { - id_token_hint: idToken, - }; - if (callbackPathOrUrl !== null) { - extras.post_logout_redirect_uri = url; - } - } - let queryString = ''; - if (extras) { - for (const [key, value] of Object.entries(extras)) { - if (queryString === '') { - queryString += '?'; - } else { - queryString += '&'; - } - queryString += `${key}=${encodeURIComponent(value)}`; - } - } - window.location.href = `${oidcServerConfiguration.endSessionEndpoint}${queryString}`; - } else { - window.location.reload(); + const oidcExtras = extractExtras(extras, ':oidc'); + const noReload = oidcExtras && oidcExtras['no_reload'] === 'true'; + + if (noReload) { + // No navigation happens here: this branch is essentially a "clear local + // session" call dressed as a logout. We can drop state immediately and + // reset the flag since the call returns normally to the caller. + await clearSessionAsync(oidc, oidcDatabase)(); + oidc.isLoggingOut = false; + return; + } + + // Navigate to the end-session endpoint (or reload) *before* clearing the + // local session. This closes the race where `OidcProvider` / + // `OidcSecure` / silent-renew timers observe a null `tokens` and kick + // off a new auth flow in the window between local clear and navigation. + const endPointExtras = keepExtras(extras); + if (oidcServerConfiguration.endSessionEndpoint) { + const endSessionUrl = buildEndSessionUrl( + oidcServerConfiguration.endSessionEndpoint, + endPointExtras, + idToken, + postLogoutRedirectUri, + ); + oicLocation.open(endSessionUrl); + } else { + oicLocation.reload(); + } + + // Now that navigation has been scheduled, drop the local session. By the + // time React re-renders against the null tokens the page is already + // unloading; if for any reason it is not (e.g. navigation cancelled by a + // `beforeunload` handler) the `isLoggingOut` flag stays set so guards + // still know not to start a fresh auth flow. + await clearSessionAsync(oidc, oidcDatabase)(); + } catch (exception) { + // If anything went wrong, reset the flag so the app is not stuck in a + // "logging out forever" state. + oidc.isLoggingOut = false; + const error = + !isOidcError(exception) && isNetworkErrorCause(exception) + ? createNetworkError(exception, 'logout') + : exception; + oidc.publishEvent(eventNames.logoutAsync_error, error); + throw error; } -}; + }; diff --git a/packages/oidc-client/src/oidc.ts b/packages/oidc-client/src/oidc.ts index 318cd51cf..f1658731c 100644 --- a/packages/oidc-client/src/oidc.ts +++ b/packages/oidc-client/src/oidc.ts @@ -1,613 +1,622 @@ import { startCheckSessionAsync as defaultStartCheckSessionAsync } from './checkSession.js'; import { CheckSessionIFrame } from './checkSessionIFrame.js'; +import { base64urlOfHashOfASCIIEncodingAsync, generateRandom } from './crypto'; import { eventNames } from './events.js'; import { initSession } from './initSession.js'; -import { initWorkerAsync, sleepAsync } from './initWorker.js'; +import { getTabId, initWorkerAsync } from './initWorker.js'; +import { activateServiceWorker } from './initWorkerOption'; +import { + defaultDemonstratingProofOfPossessionConfiguration, + generateJwtDemonstratingProofOfPossessionAsync, +} from './jwt'; +import { tryKeepSessionAsync } from './keepSession'; +import { ILOidcLocation, OidcLocation } from './location'; import { defaultLoginAsync, loginCallbackAsync } from './login.js'; -import { destroyAsync, logoutAsync } from './logout.js'; +import { clearSessionAsync, destroyAsync, logoutAsync } from './logout.js'; +import { + createOAuthError, + isOidcError, + OidcError, + OidcErrorCode, + serializeOidcError, +} from './oidcError.js'; +import { TokenRenewMode, Tokens } from './parseTokens.js'; import { - computeTimeLeft, - isTokensOidcValid, - setTokens, TokenRenewMode, - Tokens, -} from './parseTokens.js'; -import { autoRenewTokens, renewTokensAndStartTimerAsync } from './renewTokens.js'; -import { fetchFromIssuer, performTokenRequestAsync } from './requests.js'; + autoRenewTokens, + renewTokensAndStartTimerResultAsync, + RenewTokensResult, +} from './renewTokens.js'; +import { fetchFromIssuer } from './requests.js'; import { getParseQueryStringFromLocation } from './route-utils.js'; -import defaultSilentLoginAsync, { _silentLoginAsync } from './silentLogin.js'; +import defaultSilentLoginAsync from './silentLogin.js'; import timer from './timer.js'; -import { AuthorityConfiguration, Fetch, OidcConfiguration, StringMap } from './types.js'; +import { + AuthorityConfiguration, + Fetch, + OidcConfiguration, + StringMap, + TokenAutomaticRenewMode, +} from './types.js'; import { userInfoAsync } from './user.js'; export const getFetchDefault = () => { - return fetch; + return fetch; }; export interface OidcAuthorizationServiceConfigurationJson { - check_session_iframe?: string; - issuer:string; + check_session_iframe?: string; + issuer: string; + pushed_authorization_request_endpoint?: string; + require_pushed_authorization_requests?: boolean; } export class OidcAuthorizationServiceConfiguration { - private checkSessionIframe: string; - private issuer: string; - private authorizationEndpoint: string; - private tokenEndpoint: string; - private revocationEndpoint: string; - private userInfoEndpoint: string; - private endSessionEndpoint: string; - - constructor(request: any) { - this.authorizationEndpoint = request.authorization_endpoint; - this.tokenEndpoint = request.token_endpoint; - this.revocationEndpoint = request.revocation_endpoint; - this.userInfoEndpoint = request.userinfo_endpoint; - this.checkSessionIframe = request.check_session_iframe; - this.issuer = request.issuer; - this.endSessionEndpoint = request.end_session_endpoint; - } + private checkSessionIframe: string; + private issuer: string; + private authorizationEndpoint: string; + private tokenEndpoint: string; + private revocationEndpoint: string; + private userInfoEndpoint: string; + private endSessionEndpoint: string; + private pushedAuthorizationRequestEndpoint: string; + private requirePushedAuthorizationRequests: boolean; + + constructor(request: any) { + this.authorizationEndpoint = request.authorization_endpoint; + this.tokenEndpoint = request.token_endpoint; + this.revocationEndpoint = request.revocation_endpoint; + this.userInfoEndpoint = request.userinfo_endpoint; + this.checkSessionIframe = request.check_session_iframe; + this.issuer = request.issuer; + this.endSessionEndpoint = request.end_session_endpoint; + this.pushedAuthorizationRequestEndpoint = request.pushed_authorization_request_endpoint; + this.requirePushedAuthorizationRequests = + request.require_pushed_authorization_requests ?? false; + } } const oidcDatabase = {}; -const oidcFactory = (getFetch : () => Fetch) => (configuration: OidcConfiguration, name = 'default') => { +const oidcFactory = + (getFetch: () => Fetch, location: ILOidcLocation = new OidcLocation()) => + (configuration: OidcConfiguration, name = 'default') => { if (oidcDatabase[name]) { - return oidcDatabase[name]; + return oidcDatabase[name]; } - oidcDatabase[name] = new Oidc(configuration, name, getFetch); + oidcDatabase[name] = new Oidc(configuration, name, getFetch, location); return oidcDatabase[name]; -}; + }; export type LoginCallback = { - callbackPath:string; -} + callbackPath: string; +}; export type InternalLoginCallback = { - callbackPath:string; - parsedTokens:Tokens; -} - -const loginCallbackWithAutoTokensRenewAsync = async (oidc) : Promise => { - const { parsedTokens, callbackPath } = await oidc.loginCallbackAsync(); - oidc.timeoutId = autoRenewTokens(oidc, parsedTokens.refreshToken, parsedTokens.expiresAt); - return { callbackPath }; + callbackPath: string; + state: string; + parsedTokens: Tokens; + scope: string; + extras: StringMap; }; -const getRandomInt = (max) => { - return Math.floor(Math.random() * max); +const loginCallbackWithAutoTokensRenewAsync = async (oidc): Promise => { + const { parsedTokens, callbackPath, extras, scope } = await oidc.loginCallbackAsync(); + oidc.timeoutId = autoRenewTokens(oidc, parsedTokens.expiresAt, extras, scope); + return { callbackPath }; }; export class Oidc { - public configuration: OidcConfiguration; - public userInfo: null; - public tokens?: Tokens; - public events: Array; - private timeoutId: NodeJS.Timeout; - public configurationName: string; - private checkSessionIFrame: CheckSessionIFrame; - private getFetch: () => Fetch; - constructor(configuration:OidcConfiguration, configurationName = 'default', getFetch : () => Fetch) { - let silent_login_uri = configuration.silent_login_uri; - if (configuration.silent_redirect_uri && !configuration.silent_login_uri) { - silent_login_uri = `${configuration.silent_redirect_uri.replace('-callback', '').replace('callback', '')}-login`; - } - let refresh_time_before_tokens_expiration_in_second = configuration.refresh_time_before_tokens_expiration_in_second ?? 120; - if (refresh_time_before_tokens_expiration_in_second > 60) { - refresh_time_before_tokens_expiration_in_second = refresh_time_before_tokens_expiration_in_second - Math.floor(Math.random() * 40); - } - if (!configuration.logout_tokens_to_invalidate) { - configuration.logout_tokens_to_invalidate = ['access_token', 'refresh_token']; - } - if (!configuration.authority_timeout_wellknowurl_in_millisecond) { - configuration.authority_timeout_wellknowurl_in_millisecond = 10000; - } - this.configuration = { - ...configuration, - silent_login_uri, - monitor_session: configuration.monitor_session ?? false, - refresh_time_before_tokens_expiration_in_second, - silent_login_timeout: configuration.silent_login_timeout ?? 12000, - token_renew_mode: configuration.token_renew_mode ?? TokenRenewMode.access_token_or_id_token_invalid, - }; - this.getFetch = getFetch ?? getFetchDefault; - this.configurationName = configurationName; - this.tokens = null; - this.userInfo = null; - this.events = []; - this.timeoutId = null; - this.synchroniseTokensAsync.bind(this); - this.loginCallbackWithAutoTokensRenewAsync.bind(this); - this.initAsync.bind(this); - this.loginCallbackAsync.bind(this); - this.subscribeEvents.bind(this); - this.removeEventSubscription.bind(this); - this.publishEvent.bind(this); - this.destroyAsync.bind(this); - this.logoutAsync.bind(this); - this.renewTokensAsync.bind(this); - this.initAsync(this.configuration.authority, this.configuration.authority_configuration); + public configuration: OidcConfiguration; + public userInfo: null; + public tokens?: Tokens; + public events: Array; + public timeoutId: NodeJS.Timeout | number; + public configurationName: string; + public checkSessionIFrame: CheckSessionIFrame; + public getFetch: () => Fetch; + public location: ILOidcLocation; + /** + * `true` while {@link logoutAsync} is executing or has scheduled a + * navigation to the identity provider's end-session endpoint that has not + * yet committed. Consumers (UI guards, silent-renew handlers, 401 retry + * interceptors, …) should check this flag and skip starting a new auth + * flow when it is set, even if `tokens` is null. + */ + public isLoggingOut = false; + constructor( + configuration: OidcConfiguration, + configurationName = 'default', + getFetch: () => Fetch, + location: ILOidcLocation = new OidcLocation(), + ) { + let silent_login_uri = configuration.silent_login_uri; + if (configuration.silent_redirect_uri && !configuration.silent_login_uri) { + silent_login_uri = `${configuration.silent_redirect_uri.replace('-callback', '').replace('callback', '')}-login`; + } + let refresh_time_before_tokens_expiration_in_second = + configuration.refresh_time_before_tokens_expiration_in_second ?? 120; + if (refresh_time_before_tokens_expiration_in_second > 60) { + refresh_time_before_tokens_expiration_in_second = + refresh_time_before_tokens_expiration_in_second - Math.floor(Math.random() * 40); } + this.location = location ?? new OidcLocation(); + + this.configuration = { + ...configuration, + silent_login_uri, + token_automatic_renew_mode: + configuration.token_automatic_renew_mode ?? + TokenAutomaticRenewMode.AutomaticBeforeTokenExpiration, + monitor_session: configuration.monitor_session ?? false, + refresh_time_before_tokens_expiration_in_second, + silent_login_timeout: configuration.silent_login_timeout ?? 12000, + token_renew_mode: + configuration.token_renew_mode ?? TokenRenewMode.access_token_or_id_token_invalid, + demonstrating_proof_of_possession: configuration.demonstrating_proof_of_possession ?? false, + authority_timeout_wellknowurl_in_millisecond: + configuration.authority_timeout_wellknowurl_in_millisecond ?? 10000, + logout_tokens_to_invalidate: configuration.logout_tokens_to_invalidate ?? [ + 'access_token', + 'refresh_token', + ], + service_worker_activate: configuration.service_worker_activate ?? activateServiceWorker, + demonstrating_proof_of_possession_configuration: + configuration.demonstrating_proof_of_possession_configuration ?? + defaultDemonstratingProofOfPossessionConfiguration, + preload_user_info: configuration.preload_user_info ?? false, + par: configuration.par ?? 'disabled', + par_request_timeout: configuration.par_request_timeout ?? 10000, + }; - subscribeEvents(func):string { - const id = getRandomInt(9999999999999).toString(); - this.events.push({ id, func }); - return id; + this.getFetch = getFetch ?? getFetchDefault; + this.configurationName = configurationName; + this.tokens = null; + this.userInfo = null; + this.events = []; + this.timeoutId = null; + this.loginCallbackWithAutoTokensRenewAsync.bind(this); + this.initAsync.bind(this); + this.loginCallbackAsync.bind(this); + this.subscribeEvents.bind(this); + this.removeEventSubscription.bind(this); + this.publishEvent.bind(this); + this.destroyAsync.bind(this); + this.logoutAsync.bind(this); + this.renewTokensAsync.bind(this); + this.renewTokensOrThrowAsync.bind(this); + this.initAsync(this.configuration.authority, this.configuration.authority_configuration); + } + + subscribeEvents(func): string { + const id = generateRandom(16); + this.events.push({ id, func }); + return id; + } + + removeEventSubscription(id): void { + const newEvents = this.events.filter(e => e.id !== id); + this.events = newEvents; + } + + publishEvent(eventName, data) { + this.events.forEach(event => { + event.func(eventName, data); + }); + } + + static getOrCreate = + (getFetch: () => Fetch, location: ILOidcLocation) => + (configuration, name = 'default') => { + return oidcFactory(getFetch, location)(configuration, name); + }; + + /** + * Retrieve an existing OIDC instance previously initialized via + * {@link Oidc.getOrCreate}. + * + * Since issue #1679, this method no longer throws when the requested + * configuration has not been initialized; it returns `null` instead. + * This makes hooks such as `useOidc`, `useOidcUser` and `useOidcIdToken` + * safe to call outside of an `` (e.g. in unit tests or + * Storybook stories). + * + * Use {@link Oidc.getOrThrow} to preserve the previous fail-fast + * behaviour. + */ + static get(name = 'default'): Oidc | null { + if (!Object.prototype.hasOwnProperty.call(oidcDatabase, name)) { + return null; } + return oidcDatabase[name]; + } - removeEventSubscription(id) :void { - const newEvents = this.events.filter(e => e.id !== id); - this.events = newEvents; + /** + * Retrieve an existing OIDC instance, throwing an explicit error if it + * has not been initialized. This preserves the historical (pre-#1679) + * fail-fast behaviour of `Oidc.get`. + */ + static getOrThrow(name = 'default'): Oidc { + const oidc = Oidc.get(name); + if (!oidc) { + throw Error(`OIDC library does seem initialized. +Please checkout that you are using OIDC hook inside a component.`); } + return oidc; + } - publishEvent(eventName, data) { - this.events.forEach(event => { - event.func(eventName, data); - }); + static eventNames = eventNames; + + _silentLoginCallbackFromIFrame() { + if (this.configuration.silent_redirect_uri && this.configuration.silent_login_uri) { + const location = this.location; + const queryParams = getParseQueryStringFromLocation(location.getCurrentHref()); + window.parent.postMessage( + `${this.configurationName}_oidc_tokens:${JSON.stringify({ tokens: this.tokens, sessionState: queryParams.session_state })}`, + location.getOrigin(), + ); } + } + + _silentLoginErrorCallbackFromIFrame(exception = null) { + if (this.configuration.silent_redirect_uri && this.configuration.silent_login_uri) { + const location = this.location; + const queryParams = getParseQueryStringFromLocation(location.getCurrentHref()); + if (queryParams.error) { + const error = createOAuthError( + queryParams.error, + queryParams.error_description, + `Error from OIDC server: ${queryParams.error} - ${queryParams.error_description}`, + 'refresh', + ); + window.parent.postMessage( + `${this.configurationName}_oidc_error:${JSON.stringify({ + error: queryParams.error, + error_description: queryParams.error_description, + oidcError: serializeOidcError(error), + })}`, + location.getOrigin(), + ); + } else { + window.parent.postMessage( + `${this.configurationName}_oidc_exception:${JSON.stringify({ + error: exception == null ? '' : exception.toString(), + oidcError: isOidcError(exception) ? serializeOidcError(exception) : undefined, + })}`, + location.getOrigin(), + ); + } + } + } + + async silentLoginCallbackAsync() { + try { + await this.loginCallbackAsync(true); + this._silentLoginCallbackFromIFrame(); + } catch (exception) { + console.error(exception); + this._silentLoginErrorCallbackFromIFrame(exception); + } + } + + initPromise = null; + async initAsync(authority: string, authorityConfiguration: AuthorityConfiguration) { + if (this.initPromise !== null) { + return this.initPromise; + } + const localFuncAsync = async () => { + if (authorityConfiguration != null) { + return new OidcAuthorizationServiceConfiguration({ + authorization_endpoint: authorityConfiguration.authorization_endpoint, + end_session_endpoint: authorityConfiguration.end_session_endpoint, + revocation_endpoint: authorityConfiguration.revocation_endpoint, + token_endpoint: authorityConfiguration.token_endpoint, + userinfo_endpoint: authorityConfiguration.userinfo_endpoint, + check_session_iframe: authorityConfiguration.check_session_iframe, + issuer: authorityConfiguration.issuer, + pushed_authorization_request_endpoint: + authorityConfiguration.pushed_authorization_request_endpoint, + require_pushed_authorization_requests: + authorityConfiguration.require_pushed_authorization_requests, + }); + } - static getOrCreate = (getFetch : () => Fetch) => (configuration, name = 'default') => { - return oidcFactory(getFetch)(configuration, name); + const serviceWorker = await initWorkerAsync(this.configuration, this.configurationName); + const storage = serviceWorker + ? this.configuration.storage || window.sessionStorage + : this.configuration.storage; + return await fetchFromIssuer(this.getFetch())( + authority, + this.configuration.authority_time_cache_wellknowurl_in_second ?? 60 * 60, + storage, + this.configuration.authority_timeout_wellknowurl_in_millisecond, + ); }; + this.initPromise = localFuncAsync(); + return this.initPromise.finally(() => { + // in case if anything went wrong with the promise, we should reset the initPromise to null too + // otherwise client can't re-init the OIDC client + // as the promise is already fulfilled with rejected state, so could not ever reach this point again, + // so that leads to infinite loop of calls, when client tries to re-init the OIDC client after error + this.initPromise = null; + }); + } - static get(name = 'default') { - const isInsideBrowser = (typeof process === 'undefined'); - if (!Object.prototype.hasOwnProperty.call(oidcDatabase, name) && isInsideBrowser) { - throw Error(`OIDC library does seem initialized. -Please checkout that you are using OIDC hook inside a compoment.`); - } - return oidcDatabase[name]; + tryKeepExistingSessionPromise = null; + async tryKeepExistingSessionAsync(): Promise { + if (this.tryKeepExistingSessionPromise !== null) { + return this.tryKeepExistingSessionPromise; } + this.tryKeepExistingSessionPromise = tryKeepSessionAsync(this); + return this.tryKeepExistingSessionPromise.finally(() => { + this.tryKeepExistingSessionPromise = null; + }); + } - static eventNames = eventNames; + async startCheckSessionAsync( + checkSessionIFrameUri, + clientId, + sessionState, + isSilentSignin = false, + ) { + await defaultStartCheckSessionAsync(this, oidcDatabase, this.configuration)( + checkSessionIFrameUri, + clientId, + sessionState, + isSilentSignin, + ); + } - _silentLoginCallbackFromIFrame() { - if (this.configuration.silent_redirect_uri && this.configuration.silent_login_uri) { - const queryParams = getParseQueryStringFromLocation(window.location.href); - window.top.postMessage(`${this.configurationName}_oidc_tokens:${JSON.stringify({ tokens: this.tokens, sessionState: queryParams.session_state })}`, window.location.origin); - } + loginPromise: Promise = null; + async loginAsync( + callbackPath: string = undefined, + extras: StringMap = null, + isSilentSignin = false, + scope: string = undefined, + silentLoginOnly = false, + ) { + if (this.logoutPromise) { + await this.logoutPromise; } - _silentLoginErrorCallbackFromIFrame() { - if (this.configuration.silent_redirect_uri && this.configuration.silent_login_uri) { - const queryParams = getParseQueryStringFromLocation(window.location.href); - window.top.postMessage(`${this.configurationName}_oidc_error:${JSON.stringify({ error: queryParams.error })}`, window.location.origin); - } + if (this.loginPromise !== null) { + return this.loginPromise; + } + if (silentLoginOnly) { + this.loginPromise = defaultSilentLoginAsync( + window, + this.configurationName, + this.configuration, + this.publishEvent.bind(this), + this, + )(extras, scope); + } else { + this.loginPromise = defaultLoginAsync( + this.configurationName, + this.configuration, + this.publishEvent.bind(this), + this.initAsync.bind(this), + this.location, + this.getFetch, + )(callbackPath, extras, isSilentSignin, scope); } + return this.loginPromise.finally(() => { + this.loginPromise = null; + }); + } - async silentLoginCallbackAsync() { - try { - await this.loginCallbackAsync(true); - this._silentLoginCallbackFromIFrame(); - } catch (error) { - console.error(error); - this._silentLoginErrorCallbackFromIFrame(); - } + loginCallbackPromise: Promise = null; + async loginCallbackAsync(isSilenSignin = false) { + if (this.loginCallbackPromise !== null) { + return this.loginCallbackPromise; } - initPromise = null; - async initAsync(authority:string, authorityConfiguration:AuthorityConfiguration) { - if (this.initPromise !== null) { - return this.initPromise; - } - const localFuncAsync = async () => { - if (authorityConfiguration != null) { - return new OidcAuthorizationServiceConfiguration({ - authorization_endpoint: authorityConfiguration.authorization_endpoint, - end_session_endpoint: authorityConfiguration.end_session_endpoint, - revocation_endpoint: authorityConfiguration.revocation_endpoint, - token_endpoint: authorityConfiguration.token_endpoint, - userinfo_endpoint: authorityConfiguration.userinfo_endpoint, - check_session_iframe: authorityConfiguration.check_session_iframe, - issuer: authorityConfiguration.issuer, - }); - } - - const serviceWorker = await initWorkerAsync(this.configuration.service_worker_relative_url, this.configurationName); - const storage = serviceWorker ? window.localStorage : null; - return await fetchFromIssuer(this.getFetch())(authority, this.configuration.authority_time_cache_wellknowurl_in_second ?? 60 * 60, storage, this.configuration.authority_timeout_wellknowurl_in_millisecond); - }; - this.initPromise = localFuncAsync(); - return this.initPromise.then((result) => { - this.initPromise = null; - return result; - }); + const loginCallbackLocalAsync = async (): Promise => { + const response = await loginCallbackAsync(this)(isSilenSignin); + // @ts-ignore + const parsedTokens = response.tokens; + // @ts-ignore + this.tokens = parsedTokens; + const serviceWorker = await initWorkerAsync(this.configuration, this.configurationName); + if (!serviceWorker) { + const session = initSession( + this.configurationName, + this.configuration.storage, + this.configuration.login_state_storage ?? this.configuration.storage, + ); + session.setTokens(parsedTokens); + } + this.publishEvent(Oidc.eventNames.token_acquired, parsedTokens); + if (this.configuration.preload_user_info) { + await this.userInfoAsync(); + } + // @ts-ignore + return { + parsedTokens, + state: response.state, + callbackPath: response.callbackPath, + scope: response.scope, + extras: response.extras, + }; + }; + this.loginCallbackPromise = loginCallbackLocalAsync(); + return this.loginCallbackPromise.finally(() => { + this.loginCallbackPromise = null; + }); + } + + async generateDemonstrationOfProofOfPossessionAsync( + accessToken: string, + url: string, + method: string, + extras: StringMap = {}, + ): Promise { + const configuration = this.configuration; + const claimsExtras = { + ath: await base64urlOfHashOfASCIIEncodingAsync(accessToken), + ...extras, + }; + + const serviceWorker = await initWorkerAsync(configuration, this.configurationName); + + if (serviceWorker) { + return `DPOP_SECURED_BY_OIDC_SERVICE_WORKER_${this.configurationName}#tabId=${getTabId(this.configurationName)}`; } - tryKeepExistingSessionPromise = null; - async tryKeepExistingSessionAsync() :Promise { - if (this.tryKeepExistingSessionPromise !== null) { - return this.tryKeepExistingSessionPromise; - } - const funcAsync = async () => { - let serviceWorker; - if (this.tokens != null) { - return false; - } - this.publishEvent(eventNames.tryKeepExistingSessionAsync_begin, {}); - try { - const configuration = this.configuration; - const oidcServerConfiguration = await this.initAsync(configuration.authority, configuration.authority_configuration); - serviceWorker = await initWorkerAsync(configuration.service_worker_relative_url, this.configurationName); - if (serviceWorker) { - const { tokens } = await serviceWorker.initAsync(oidcServerConfiguration, 'tryKeepExistingSessionAsync', configuration); - if (tokens) { - serviceWorker.startKeepAliveServiceWorker(); - // @ts-ignore - this.tokens = tokens; - const getLoginParams = serviceWorker.getLoginParams(this.configurationName); - // @ts-ignore - this.timeoutId = autoRenewTokens(this, this.tokens.refreshToken, this.tokens.expiresAt, getLoginParams.extras); - const sessionState = await serviceWorker.getSessionStateAsync(); - // @ts-ignore - await this.startCheckSessionAsync(oidcServerConfiguration.check_session_iframe, configuration.client_id, sessionState); - this.publishEvent(eventNames.tryKeepExistingSessionAsync_end, { - success: true, - message: 'tokens inside ServiceWorker are valid', - }); - return true; - } - this.publishEvent(eventNames.tryKeepExistingSessionAsync_end, { - success: false, - message: 'no exiting session found', - }); - } else { - if (configuration.service_worker_relative_url) { - this.publishEvent(eventNames.service_worker_not_supported_by_browser, { - message: 'service worker is not supported by this browser', - }); - } - const session = initSession(this.configurationName, configuration.storage ?? sessionStorage); - const { tokens } = await session.initAsync(); - if (tokens) { - // @ts-ignore - this.tokens = setTokens(tokens, null, configuration.token_renew_mode); - const getLoginParams = session.getLoginParams(this.configurationName); - // @ts-ignore - this.timeoutId = autoRenewTokens(this, tokens.refreshToken, this.tokens.expiresAt, getLoginParams.extras); - const sessionState = await session.getSessionStateAsync(); - // @ts-ignore - await this.startCheckSessionAsync(oidcServerConfiguration.check_session_iframe, configuration.client_id, sessionState); - this.publishEvent(eventNames.tryKeepExistingSessionAsync_end, { - success: true, - message: 'tokens inside storage are valid', - }); - return true; - } - } - this.publishEvent(eventNames.tryKeepExistingSessionAsync_end, { - success: false, - message: serviceWorker ? 'service worker sessions not retrieved' : 'session storage sessions not retrieved', - }); - return false; - } catch (exception) { - console.error(exception); - if (serviceWorker) { - await serviceWorker.clearAsync(); - } - this.publishEvent(eventNames.tryKeepExistingSessionAsync_error, 'tokens inside ServiceWorker are invalid'); - return false; - } - }; - - this.tryKeepExistingSessionPromise = funcAsync(); - return this.tryKeepExistingSessionPromise.then((result) => { - this.tryKeepExistingSessionPromise = null; - return result; - }); + const session = initSession( + this.configurationName, + configuration.storage, + configuration.login_state_storage ?? configuration.storage, + ); + const jwk = await session.getDemonstratingProofOfPossessionJwkAsync(); + const demonstratingProofOfPossessionNonce = session.getDemonstratingProofOfPossessionNonce(); + + if (demonstratingProofOfPossessionNonce) { + claimsExtras['nonce'] = demonstratingProofOfPossessionNonce; } - async startCheckSessionAsync(checkSessionIFrameUri, clientId, sessionState, isSilentSignin = false) { - await defaultStartCheckSessionAsync(this, oidcDatabase, this.configuration)(checkSessionIFrameUri, clientId, sessionState, isSilentSignin); + return await generateJwtDemonstratingProofOfPossessionAsync(window)( + configuration.demonstrating_proof_of_possession_configuration, + )(jwk, method, url, claimsExtras); + } + + loginCallbackWithAutoTokensRenewPromise: Promise = null; + loginCallbackWithAutoTokensRenewAsync(): Promise { + if (this.loginCallbackWithAutoTokensRenewPromise !== null) { + return this.loginCallbackWithAutoTokensRenewPromise; } + this.loginCallbackWithAutoTokensRenewPromise = loginCallbackWithAutoTokensRenewAsync(this); + return this.loginCallbackWithAutoTokensRenewPromise.finally(() => { + this.loginCallbackWithAutoTokensRenewPromise = null; + }); + } - loginPromise: Promise = null; - async loginAsync(callbackPath:string = undefined, extras:StringMap = null, isSilentSignin = false, scope:string = undefined, silentLoginOnly = false) { - if (this.loginPromise !== null) { - return this.loginPromise; - } - if (silentLoginOnly) { - return defaultSilentLoginAsync(window, this.configurationName, this.configuration, this.publishEvent.bind(this), this)(extras, scope); - } - this.loginPromise = defaultLoginAsync(window, this.configurationName, this.configuration, this.publishEvent.bind(this), this.initAsync.bind(this))(callbackPath, extras, isSilentSignin, scope); - return this.loginPromise.then(result => { - this.loginPromise = null; - return result; - }); + userInfoPromise: Promise = null; + userInfoAsync(noCache = false, demonstrating_proof_of_possession = false) { + if (this.userInfoPromise !== null) { + return this.userInfoPromise; } + this.userInfoPromise = userInfoAsync(this)(noCache, demonstrating_proof_of_possession); + return this.userInfoPromise.finally(() => { + this.userInfoPromise = null; + }); + } - loginCallbackPromise : Promise = null; - async loginCallbackAsync(isSilenSignin = false) { - if (this.loginCallbackPromise !== null) { - return this.loginCallbackPromise; - } - - const loginCallbackLocalAsync = async():Promise => { - const response = await loginCallbackAsync(this)(isSilenSignin); - // @ts-ignore - const parsedTokens = response.tokens; - // @ts-ignore - this.tokens = parsedTokens; - const serviceWorker = await initWorkerAsync(this.configuration.service_worker_relative_url, this.configurationName); - if (!serviceWorker) { - const session = initSession(this.configurationName, this.configuration.storage); - session.setTokens(parsedTokens); - } - this.publishEvent(Oidc.eventNames.token_aquired, parsedTokens); - // @ts-ignore - return { parsedTokens, state: response.state, callbackPath: response.callbackPath }; - }; - this.loginCallbackPromise = loginCallbackLocalAsync(); - return this.loginCallbackPromise.then(result => { - this.loginCallbackPromise = null; - return result; - }); + renewTokensPromise: Promise = null; + + private async renewTokensResultAsync( + extras: StringMap = null, + scope: string = null, + ): Promise { + if (this.renewTokensPromise !== null) { + return this.renewTokensPromise; } + if (!this.timeoutId) { + return null; + } + timer.clearTimeout(this.timeoutId); + const promise = renewTokensAndStartTimerResultAsync(this, true, extras, scope); + this.renewTokensPromise = promise; + return promise.finally(() => { + if (this.renewTokensPromise === promise) { + this.renewTokensPromise = null; + } + }); + } - async synchroniseTokensAsync(refreshToken, index = 0, forceRefresh = false, extras:StringMap = null, updateTokens) { - while (!navigator.onLine && document.hidden) { - await sleepAsync(1000); - this.publishEvent(eventNames.refreshTokensAsync, { message: 'wait because navigator is offline and hidden' }); - } - let numberTryOnline = 6; - while (!navigator.onLine && numberTryOnline > 0) { - await sleepAsync(1000); - numberTryOnline--; - this.publishEvent(eventNames.refreshTokensAsync, { message: `wait because navigator is offline try ${numberTryOnline}` }); - } - let numberTryHidden = Math.floor(Math.random() * 15) + 10; - while (document.hidden && numberTryHidden > 0) { - await sleepAsync(1000); - numberTryHidden--; - this.publishEvent(eventNames.refreshTokensAsync, { message: `wait because navigator is hidden try ${numberTryHidden}` }); - } - const isDocumentHidden = document.hidden; - const nextIndex = isDocumentHidden ? index : index + 1; - if (!extras) { - extras = {}; - } - const configuration = this.configuration; - - const silentLoginAsync = (extras: StringMap, state:string, scope:string = null) => { - return _silentLoginAsync(this.configurationName, this.configuration, this.publishEvent.bind(this))(extras, state, scope); - }; - const localsilentLoginAsync = async () => { - try { - let loginParams; - const serviceWorker = await initWorkerAsync(configuration.service_worker_relative_url, this.configurationName); - if (serviceWorker) { - loginParams = serviceWorker.getLoginParams(this.configurationName); - } else { - const session = initSession(this.configurationName, configuration.storage); - loginParams = session.getLoginParams(this.configurationName); - } - const silent_token_response = await silentLoginAsync({ - ...loginParams.extras, - ...extras, - prompt: 'none', - }, loginParams.state); - if (silent_token_response) { - updateTokens(silent_token_response.tokens); - this.publishEvent(Oidc.eventNames.token_renewed, {}); - return { tokens: silent_token_response.tokens, status: 'LOGGED' }; - } - } catch (exceptionSilent: any) { - console.error(exceptionSilent); - this.publishEvent(eventNames.refreshTokensAsync_silent_error, { message: 'exceptionSilent', exception: exceptionSilent.message }); - if (exceptionSilent && exceptionSilent.message && exceptionSilent.message.startsWith('oidc')) { - updateTokens(null); - this.publishEvent(eventNames.refreshTokensAsync_error, { message: 'refresh token silent' }); - return { tokens: null, status: 'SESSION_LOST' }; - } - } - this.publishEvent(eventNames.refreshTokensAsync_error, { message: 'refresh token silent return' }); - return await this.synchroniseTokensAsync(null, nextIndex, forceRefresh, extras, updateTokens); - }; - - if (index > 4) { - updateTokens(null); - this.publishEvent(eventNames.refreshTokensAsync_error, { message: 'refresh token' }); - return { tokens: null, status: 'SESSION_LOST' }; - } - try { - const { status, tokens, nonce } = await this.syncTokensInfoAsync(configuration, this.configurationName, this.tokens, forceRefresh); - switch (status) { - case 'SESSION_LOST': - updateTokens(null); - this.publishEvent(eventNames.refreshTokensAsync_error, { message: 'refresh token session lost' }); - return { tokens: null, status: 'SESSION_LOST' }; - case 'NOT_CONNECTED': - updateTokens(null); - return { tokens: null, status: null }; - case 'TOKENS_VALID': - updateTokens(tokens); - return { tokens, status: 'LOGGED_IN' }; - case 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID': - updateTokens(tokens); - this.publishEvent(Oidc.eventNames.token_renewed, { reason: 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID' }); - return { tokens, status: 'LOGGED_IN' }; - case 'LOGOUT_FROM_ANOTHER_TAB': - updateTokens(null); - this.publishEvent(eventNames.logout_from_another_tab, { status: 'session syncTokensAsync' }); - return { tokens: null, status: 'LOGGED_OUT' }; - case 'REQUIRE_SYNC_TOKENS': - this.publishEvent(eventNames.refreshTokensAsync_begin, { refreshToken, status, tryNumber: index }); - return await localsilentLoginAsync(); - default: { - this.publishEvent(eventNames.refreshTokensAsync_begin, { refreshToken, status, tryNumber: index }); - if (!refreshToken) { - return await localsilentLoginAsync(); - } - - const clientId = configuration.client_id; - const redirectUri = configuration.redirect_uri; - const authority = configuration.authority; - const tokenExtras = configuration.token_request_extras ? configuration.token_request_extras : {}; - const finalExtras = { ...tokenExtras }; - - for (const [key, value] of Object.entries(extras)) { - if (key.endsWith(':token_request')) { - finalExtras[key.replace(':token_request', '')] = value; - } - } - const localFunctionAsync = async () => { - const details = { - client_id: clientId, - redirect_uri: redirectUri, - grant_type: 'refresh_token', - refresh_token: tokens.refreshToken, - }; - const oidcServerConfiguration = await this.initAsync(authority, configuration.authority_configuration); - const timeoutMs = document.hidden ? 10000 : 30000 * 10; - const tokenResponse = await performTokenRequestAsync(this.getFetch())(oidcServerConfiguration.tokenEndpoint, details, finalExtras, tokens, configuration.token_renew_mode, timeoutMs); - if (tokenResponse.success) { - const { isValid, reason } = isTokensOidcValid(tokenResponse.data, nonce.nonce, oidcServerConfiguration); - if (!isValid) { - updateTokens(null); - this.publishEvent(eventNames.refreshTokensAsync_error, { message: `refresh token return not valid tokens, reason: ${reason}` }); - return { tokens: null, status: 'SESSION_LOST' }; - } - updateTokens(tokenResponse.data); - this.publishEvent(eventNames.refreshTokensAsync_end, { success: tokenResponse.success }); - this.publishEvent(Oidc.eventNames.token_renewed, { reason: 'REFRESH_TOKEN' }); - return { tokens: tokenResponse.data, status: 'LOGGED_IN' }; - } else { - this.publishEvent(eventNames.refreshTokensAsync_silent_error, { - message: 'bad request', - tokenResponse, - }); - return await this.synchroniseTokensAsync(refreshToken, nextIndex, forceRefresh, extras, updateTokens); - } - }; - return await localFunctionAsync(); - } - } - } catch (exception: any) { - console.error(exception); - this.publishEvent(eventNames.refreshTokensAsync_silent_error, { message: 'exception', exception: exception.message }); - return this.synchroniseTokensAsync(refreshToken, nextIndex, forceRefresh, extras, updateTokens); - } - } - - async syncTokensInfoAsync(configuration, configurationName, currentTokens, forceRefresh = false) { - // Service Worker can be killed by the browser (when it wants,for example after 10 seconds of inactivity, so we retreieve the session if it happen) - // const configuration = this.configuration; - const nullNonce = { nonce: null }; - if (!currentTokens) { - return { tokens: null, status: 'NOT_CONNECTED', nonce: nullNonce }; - } - let nonce = nullNonce; - const oidcServerConfiguration = await this.initAsync(configuration.authority, configuration.authority_configuration); - const serviceWorker = await initWorkerAsync(configuration.service_worker_relative_url, configurationName); - if (serviceWorker) { - const { status, tokens } = await serviceWorker.initAsync(oidcServerConfiguration, 'syncTokensAsync', configuration); - if (status === 'LOGGED_OUT') { - return { tokens: null, status: 'LOGOUT_FROM_ANOTHER_TAB', nonce: nullNonce }; - } else if (status === 'SESSIONS_LOST') { - return { tokens: null, status: 'SESSIONS_LOST', nonce: nullNonce }; - } else if (!status || !tokens) { - return { tokens: null, status: 'REQUIRE_SYNC_TOKENS', nonce: nullNonce }; - } else if (tokens.issuedAt !== currentTokens.issuedAt) { - const timeLeft = computeTimeLeft(configuration.refresh_time_before_tokens_expiration_in_second, tokens.expiresAt); - const status = (timeLeft > 0) ? 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID' : 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_INVALID'; - const nonce = await serviceWorker.getNonceAsync(); - return { tokens, status, nonce }; - } - nonce = await serviceWorker.getNonceAsync(); - } else { - const session = initSession(configurationName, configuration.storage ?? sessionStorage); - const { tokens, status } = await session.initAsync(); - if (!tokens) { - return { tokens: null, status: 'LOGOUT_FROM_ANOTHER_TAB', nonce: nullNonce }; - } else if (status === 'SESSIONS_LOST') { - return { tokens: null, status: 'SESSIONS_LOST', nonce: nullNonce }; - } else if (tokens.issuedAt !== currentTokens.issuedAt) { - const timeLeft = computeTimeLeft(configuration.refresh_time_before_tokens_expiration_in_second, tokens.expiresAt); - const status = (timeLeft > 0) ? 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID' : 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_INVALID'; - const nonce = await session.getNonceAsync(); - return { tokens, status, nonce }; - } - nonce = await session.getNonceAsync(); - } - - const timeLeft = computeTimeLeft(configuration.refresh_time_before_tokens_expiration_in_second, currentTokens.expiresAt); - const status = (timeLeft > 0) ? 'TOKENS_VALID' : 'TOKENS_INVALID'; - if (forceRefresh) { - return { tokens: currentTokens, status: 'FORCE_REFRESH', nonce }; - } - return { tokens: currentTokens, status, nonce }; + async renewTokensAsync( + extras: StringMap = null, + scope: string = null, + ): Promise { + const result = await this.renewTokensResultAsync(extras, scope); + return result?.tokens; + } + + async renewTokensOrThrowAsync(extras: StringMap = null, scope: string = null): Promise { + const result = await this.renewTokensResultAsync(extras, scope); + if (result?.error) { + throw result.error; + } + if (!result?.tokens) { + throw new OidcError(OidcErrorCode.TOKEN_REQUEST_FAILED, 'Token renewal failed', { + phase: 'refresh', + retryable: false, + }); } + return result.tokens; + } - loginCallbackWithAutoTokensRenewPromise:Promise = null; - loginCallbackWithAutoTokensRenewAsync():Promise { - if (this.loginCallbackWithAutoTokensRenewPromise !== null) { - return this.loginCallbackWithAutoTokensRenewPromise; - } - this.loginCallbackWithAutoTokensRenewPromise = loginCallbackWithAutoTokensRenewAsync(this); - return this.loginCallbackWithAutoTokensRenewPromise.then(result => { - this.loginCallbackWithAutoTokensRenewPromise = null; - return result; - }); - } - - userInfoPromise:Promise = null; - userInfoAsync(noCache = false) { - if (this.userInfoPromise !== null) { - return this.userInfoPromise; - } - this.userInfoPromise = userInfoAsync(this)(noCache); - return this.userInfoPromise.then(result => { - this.userInfoPromise = null; - return result; - }); - } - - renewTokensPromise:Promise = null; - - async renewTokensAsync (extras:StringMap = null) { - if (this.renewTokensPromise !== null) { - return this.renewTokensPromise; - } - if (!this.timeoutId) { - return; - } - timer.clearTimeout(this.timeoutId); - // @ts-ignore - this.renewTokensPromise = renewTokensAndStartTimerAsync(this, this.tokens.refreshToken, true, extras); - return this.renewTokensPromise.then(result => { - this.renewTokensPromise = null; - return result; - }); - } - - async destroyAsync(status) { - return await destroyAsync(this)(status); - } - - async logoutSameTabAsync(clientId: string, sub: any) { - // @ts-ignore - if (this.configuration.monitor_session && this.configuration.client_id === clientId && sub && this.tokens && this.tokens.idTokenPayload && this.tokens.idTokenPayload.sub === sub) { - this.publishEvent(eventNames.logout_from_same_tab, { message: sub }); - await this.destroyAsync('LOGGED_OUT'); - } - } - - async logoutOtherTabAsync(clientId: string, sub: any) { - // @ts-ignore - if (this.configuration.monitor_session && this.configuration.client_id === clientId && sub && this.tokens && this.tokens.idTokenPayload && this.tokens.idTokenPayload.sub === sub) { - await this.destroyAsync('LOGGED_OUT'); - this.publishEvent(eventNames.logout_from_another_tab, { message: 'SessionMonitor', sub }); - } + async destroyAsync(status) { + return await destroyAsync(this)(status); + } + + /** + * Drops the local OIDC session (tokens, user info, service-worker storage) + * and broadcasts `logout_from_same_tab`, without contacting the identity + * provider's `end_session_endpoint` and without revoking tokens. + * + * Use this for SPA-only logouts, service-worker-only flows, or + * error-recovery paths where a full IdP logout is not needed or not + * desirable. For a standard OIDC RP-initiated logout use + * {@link logoutAsync} instead. + */ + clearSessionPromise: Promise = null; + async clearSessionAsync(): Promise { + if (this.clearSessionPromise) { + return this.clearSessionPromise; } + this.clearSessionPromise = clearSessionAsync(this, oidcDatabase)(); + return this.clearSessionPromise.finally(() => { + this.clearSessionPromise = null; + }); + } - logoutPromise:Promise = null; - async logoutAsync(callbackPathOrUrl: string | null | undefined = undefined, extras: StringMap = null) { - if (this.logoutPromise) { - return this.logoutPromise; - } - this.logoutPromise = logoutAsync(this, oidcDatabase, this.getFetch(), window, console)(callbackPathOrUrl, extras); - return this.logoutPromise.then(result => { - this.logoutPromise = null; - return result; - }); + async logoutSameTabAsync(clientId: string, sub: any) { + // @ts-ignore + if ( + this.configuration.monitor_session && + this.configuration.client_id === clientId && + sub && + this.tokens && + this.tokens.idTokenPayload && + this.tokens.idTokenPayload.sub === sub + ) { + await this.destroyAsync('LOGGED_OUT'); + this.publishEvent(eventNames.logout_from_same_tab, { mmessage: 'SessionMonitor', sub }); + } + } + + async logoutOtherTabAsync(clientId: string, sub: any) { + // @ts-ignore + if ( + this.configuration.monitor_session && + this.configuration.client_id === clientId && + sub && + this.tokens && + this.tokens.idTokenPayload && + this.tokens.idTokenPayload.sub === sub + ) { + await this.destroyAsync('LOGGED_OUT'); + this.publishEvent(eventNames.logout_from_another_tab, { message: 'SessionMonitor', sub }); } } - export default Oidc; + logoutPromise: Promise = null; + async logoutAsync( + callbackPathOrUrl: string | null | undefined = undefined, + extras: StringMap = null, + ) { + if (this.logoutPromise) { + return this.logoutPromise; + } + this.logoutPromise = logoutAsync( + this, + oidcDatabase, + this.getFetch(), + console, + this.location, + )(callbackPathOrUrl, extras); + return this.logoutPromise.finally(() => { + this.logoutPromise = null; + }); + } +} + +export default Oidc; diff --git a/packages/oidc-client/src/oidcClient.spec.ts b/packages/oidc-client/src/oidcClient.spec.ts new file mode 100644 index 000000000..5830691bd --- /dev/null +++ b/packages/oidc-client/src/oidcClient.spec.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; + +import { Oidc } from './oidc.js'; +import { OidcClient } from './oidcClient.js'; + +describe('OidcClient.get (issue #1679)', () => { + it('returns null when no configuration has been initialized', () => { + expect(OidcClient.get('unknown-configuration-1679')).toBeNull(); + }); + + it('does not throw when no configuration has been initialized', () => { + expect(() => OidcClient.get('another-unknown-configuration-1679')).not.toThrow(); + }); +}); + +describe('OidcClient.getOrThrow (issue #1679)', () => { + it('throws an explicit error when configuration has not been initialized', () => { + expect(() => OidcClient.getOrThrow('missing-configuration-1679')).toThrow( + /OIDC library does seem initialized/, + ); + }); +}); + +describe('Oidc.get (issue #1679)', () => { + it('returns null when no configuration has been initialized', () => { + expect(Oidc.get('unknown-oidc-1679')).toBeNull(); + }); + + it('Oidc.getOrThrow throws when configuration has not been initialized', () => { + expect(() => Oidc.getOrThrow('missing-oidc-1679')).toThrow( + /OIDC library does seem initialized/, + ); + }); +}); diff --git a/packages/oidc-client/src/oidcClient.ts b/packages/oidc-client/src/oidcClient.ts new file mode 100644 index 000000000..4f0bc306a --- /dev/null +++ b/packages/oidc-client/src/oidcClient.ts @@ -0,0 +1,248 @@ +import { fetchWithTokens } from './fetch'; +import { + ServiceWorkerSignalMessage, + ServiceWorkerSignalOptions, + signalServiceWorkerAsync, +} from './initWorker.js'; +import { ILOidcLocation, OidcLocation } from './location'; +import { LoginCallback, Oidc } from './oidc.js'; +import { getValidTokenAsync, OidcToken, Tokens, ValidToken } from './parseTokens.js'; +import { syncTokensInfoAsync } from './renewTokens'; +import { Fetch, OidcConfiguration, StringMap } from './types.js'; + +export interface EventSubscriber { + (name: string, data: any); +} + +export class OidcClient { + private readonly _oidc: Oidc; + constructor(oidc: Oidc) { + this._oidc = oidc; + } + + subscribeEvents(func: EventSubscriber): string { + return this._oidc.subscribeEvents(func); + } + + removeEventSubscription(id: string): void { + this._oidc.removeEventSubscription(id); + } + + publishEvent(eventName: string, data: any): void { + this._oidc.publishEvent(eventName, data); + } + + static getOrCreate = + (getFetch: () => Fetch, location: ILOidcLocation = new OidcLocation()) => + (configuration: OidcConfiguration, name = 'default'): OidcClient => { + return new OidcClient(Oidc.getOrCreate(getFetch, location)(configuration, name)); + }; + + /** + * Retrieve an existing {@link OidcClient} by configuration name. + * + * Since issue #1679, this method returns `null` when the requested + * configuration has not been initialized, instead of throwing. This + * allows React hooks to be used safely outside of ``. + * + * Use {@link OidcClient.getOrThrow} to preserve the previous fail-fast + * behaviour. + */ + static get(name = 'default'): OidcClient | null { + const oidc = Oidc.get(name); + return oidc ? new OidcClient(oidc) : null; + } + + /** + * Retrieve an existing {@link OidcClient}, throwing if it has not been + * initialized. Equivalent to the pre-#1679 behaviour of + * {@link OidcClient.get}. + */ + static getOrThrow(name = 'default'): OidcClient { + return new OidcClient(Oidc.getOrThrow(name)); + } + + static eventNames = Oidc.eventNames; + tryKeepExistingSessionAsync(): Promise { + return this._oidc.tryKeepExistingSessionAsync(); + } + + loginAsync( + callbackPath: string = undefined, + extras: StringMap = null, + isSilentSignin = false, + scope: string = undefined, + silentLoginOnly = false, + ): Promise { + return this._oidc.loginAsync(callbackPath, extras, isSilentSignin, scope, silentLoginOnly); + } + + logoutAsync( + callbackPathOrUrl: string | null | undefined = undefined, + extras: StringMap = null, + ): Promise { + return this._oidc.logoutAsync(callbackPathOrUrl, extras); + } + + /** + * Drops the local OIDC session (tokens, user info, service-worker storage) + * and notifies same-tab listeners via the `logout_from_same_tab` event, + * without contacting the identity provider's `end_session_endpoint` and + * without revoking tokens. + * + * Use this for SPA-only logouts, service-worker-only flows, or + * error-recovery paths. For a standard OIDC RP-initiated logout (with + * token revocation and navigation to the IdP's end-session endpoint) use + * {@link logoutAsync} instead. + */ + clearSessionAsync(): Promise { + return this._oidc.clearSessionAsync(); + } + + /** + * `true` while a logout flow is in progress: between the moment + * {@link logoutAsync} starts and the moment the browser navigates away to + * the identity provider's end-session endpoint. UI guards and silent-renew + * handlers should check this flag to avoid kicking off a new auth flow + * during that window. + */ + get isLoggingOut(): boolean { + return this._oidc.isLoggingOut === true; + } + + silentLoginCallbackAsync(): Promise { + return this._oidc.silentLoginCallbackAsync(); + } + + renewTokensAsync( + extras: StringMap = null, + scope: string = null, + ): Promise { + return this._oidc.renewTokensAsync(extras, scope); + } + + renewTokensOrThrowAsync(extras: StringMap = null, scope: string = null): Promise { + return this._oidc.renewTokensOrThrowAsync(extras, scope); + } + + loginCallbackAsync(): Promise { + return this._oidc.loginCallbackWithAutoTokensRenewAsync(); + } + + get tokens(): Tokens { + return this._oidc.tokens; + } + + get configuration(): OidcConfiguration { + return this._oidc.configuration; + } + + async generateDemonstrationOfProofOfPossessionAsync( + accessToken: string, + url: string, + method: string, + extras: StringMap = {}, + ): Promise { + return this._oidc.generateDemonstrationOfProofOfPossessionAsync( + accessToken, + url, + method, + extras, + ); + } + + async getValidTokenAsync(waitMs = 200, numberWait = 50): Promise { + const oidc = this._oidc; + const oidcToken: OidcToken = { + getTokens: () => oidc.tokens, + configuration: { + token_automatic_renew_mode: oidc.configuration.token_automatic_renew_mode, + refresh_time_before_tokens_expiration_in_second: + oidc.configuration.refresh_time_before_tokens_expiration_in_second, + }, + syncTokensInfoAsync: async () => { + const { status } = await syncTokensInfoAsync(oidc)( + oidc.configuration, + oidc.configurationName, + oidc.tokens, + false, + ); + return status; + }, + renewTokensAsync: oidc.renewTokensAsync.bind(oidc), + }; + return getValidTokenAsync(oidcToken, waitMs, numberWait); + } + + fetchWithTokens(fetch: Fetch, demonstratingProofOfPossession: boolean = false): Fetch { + return fetchWithTokens(fetch, this._oidc, demonstratingProofOfPossession); + } + + async userInfoAsync( + noCache = false, + demonstratingProofOfPossession: boolean = false, + ): Promise { + return this._oidc.userInfoAsync(noCache, demonstratingProofOfPossession); + } + + userInfo(): T { + return this._oidc.userInfo; + } + + /** + * High-level helper to send a message to the OIDC service worker. + * + * Wraps the low-level `postMessage` + `MessageChannel` plumbing and + * returns the response posted back by the worker. Use the typed message + * symbols exported from `@axa-fr/oidc-client-service-worker/protocol` + * (`ServiceWorkerMessageType`) to build messages. + * + * @throws if no service worker is registered for the current + * configuration, or if the worker does not respond before the timeout + * elapses. + */ + async signalServiceWorker( + message: ServiceWorkerSignalMessage, + options?: ServiceWorkerSignalOptions, + ): Promise { + return signalServiceWorkerAsync( + this._oidc.configuration, + this._oidc.configurationName, + message, + options, + ) as Promise; + } +} + +export interface OidcUserInfo { + sub: string; + name?: string; + given_name?: string; + family_name?: string; + middle_name?: string; + nickname?: string; + preferred_username?: string; + profile?: string; + picture?: string; + website?: string; + email?: string; + email_verified?: boolean; + gender?: string; + birthdate?: string; + zoneinfo?: string; + locale?: string; + phone_number?: string; + phone_number_verified?: boolean; + address?: OidcAddressClaim; + updated_at?: number; + groups?: string[]; +} + +export interface OidcAddressClaim { + formatted?: string; + street_address?: string; + locality?: string; + region?: string; + postal_code?: string; + country?: string; +} diff --git a/packages/oidc-client/src/oidcError.spec.ts b/packages/oidc-client/src/oidcError.spec.ts new file mode 100644 index 000000000..69d55121d --- /dev/null +++ b/packages/oidc-client/src/oidcError.spec.ts @@ -0,0 +1,105 @@ +import { describe, expect, it } from 'vitest'; + +import { + createNetworkError, + createOAuthError, + deserializeOidcError, + isOidcError, + OidcError, + OidcErrorCode, + serializeOidcError, +} from './oidcError'; + +describe('OidcError', () => { + it('preserves all public fields and the Error prototype chain', () => { + const cause = new TypeError('fetch failed'); + const error = new OidcError(OidcErrorCode.TOKEN_REQUEST_FAILED, 'Token request failed', { + phase: 'refresh', + retryable: true, + status: 503, + oauthError: 'temporarily_unavailable', + oauthErrorDescription: 'Try again later', + cause, + }); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(OidcError); + expect(error.name).toBe('OidcError'); + expect(error.message).toBe('Token request failed'); + expect(error).toMatchObject({ + code: OidcErrorCode.TOKEN_REQUEST_FAILED, + phase: 'refresh', + retryable: true, + status: 503, + oauthError: 'temporarily_unavailable', + oauthErrorDescription: 'Try again later', + cause, + }); + expect(isOidcError(error)).toBe(true); + expect(isOidcError(new Error('other'))).toBe(false); + }); + + it.each([ + ['login_required', OidcErrorCode.LOGIN_REQUIRED, false], + ['consent_required', OidcErrorCode.CONSENT_REQUIRED, false], + ['interaction_required', OidcErrorCode.INTERACTION_REQUIRED, false], + ['server_error', OidcErrorCode.OAUTH_ERROR, true], + ['temporarily_unavailable', OidcErrorCode.OAUTH_ERROR, true], + ['access_denied', OidcErrorCode.OAUTH_ERROR, false], + ] as const)('maps OAuth error %s to %s', (oauthError, code, retryable) => { + const error = createOAuthError( + oauthError, + 'description', + `Error from OIDC server: ${oauthError} - description`, + 'callback', + ); + + expect(error).toMatchObject({ + code, + phase: 'callback', + retryable, + oauthError, + oauthErrorDescription: 'description', + }); + }); + + it('wraps a network failure without changing its message', () => { + const cause = new Error('Failed to fetch'); + const error = createNetworkError(cause, 'userinfo'); + + expect(error).toMatchObject({ + code: OidcErrorCode.NETWORK_ERROR, + message: 'Failed to fetch', + phase: 'userinfo', + retryable: true, + cause, + }); + }); + + it('serializes and reconstructs an OidcError without stack or cause', () => { + const original = new OidcError(OidcErrorCode.LOGIN_REQUIRED, 'login required', { + phase: 'refresh', + retryable: false, + oauthError: 'login_required', + oauthErrorDescription: 'Sign in again', + cause: new Error('not cloneable'), + }); + + const serialized = serializeOidcError(original); + expect(serialized).not.toHaveProperty('stack'); + expect(serialized).not.toHaveProperty('cause'); + + const reconstructed = deserializeOidcError(serialized); + expect(reconstructed).toBeInstanceOf(OidcError); + expect(reconstructed).toMatchObject({ + name: 'OidcError', + message: 'login required', + code: OidcErrorCode.LOGIN_REQUIRED, + phase: 'refresh', + retryable: false, + oauthError: 'login_required', + oauthErrorDescription: 'Sign in again', + }); + expect(reconstructed?.cause).toBeUndefined(); + }); +}); diff --git a/packages/oidc-client/src/oidcError.ts b/packages/oidc-client/src/oidcError.ts new file mode 100644 index 000000000..d0115d560 --- /dev/null +++ b/packages/oidc-client/src/oidcError.ts @@ -0,0 +1,197 @@ +export const OidcErrorCode = { + LOGIN_REQUIRED: 'LOGIN_REQUIRED', + CONSENT_REQUIRED: 'CONSENT_REQUIRED', + INTERACTION_REQUIRED: 'INTERACTION_REQUIRED', + OAUTH_ERROR: 'OAUTH_ERROR', + TOKEN_REQUEST_FAILED: 'TOKEN_REQUEST_FAILED', + SILENT_LOGIN_TIMEOUT: 'SILENT_LOGIN_TIMEOUT', + INVALID_STATE: 'INVALID_STATE', + INVALID_NONCE: 'INVALID_NONCE', + DPOP_NONCE_REQUIRED: 'DPOP_NONCE_REQUIRED', + NETWORK_ERROR: 'NETWORK_ERROR', + // Backwards-compatible codes exposed by the existing specialised errors. + STATE_MISSING: 'STATE_MISSING', + STATE_MISMATCH: 'STATE_MISMATCH', + NONCE_MISSING: 'NONCE_MISSING', + ENDPOINT_UNAVAILABLE: 'ENDPOINT_UNAVAILABLE', + REQUEST_FAILED: 'REQUEST_FAILED', + INVALID_RESPONSE: 'INVALID_RESPONSE', +} as const; + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type OidcErrorCode = (typeof OidcErrorCode)[keyof typeof OidcErrorCode]; + +export type OidcErrorPhase = + 'login' | 'callback' | 'refresh' | 'logout' | 'userinfo' | 'api_request'; + +export type OidcErrorOptions = { + phase: OidcErrorPhase; + retryable: boolean; + oauthError?: string; + oauthErrorDescription?: string; + status?: number; + cause?: unknown; +}; + +export type SerializedOidcError = { + name: string; + message: string; + code: OidcErrorCode; + phase: OidcErrorPhase; + retryable: boolean; + oauthError?: string; + oauthErrorDescription?: string; + status?: number; +}; + +const oidcErrorCodes = new Set(Object.values(OidcErrorCode)); +const oidcErrorPhases = new Set([ + 'login', + 'callback', + 'refresh', + 'logout', + 'userinfo', + 'api_request', +]); + +export class OidcError extends Error { + readonly code: OidcErrorCode; + readonly phase: OidcErrorPhase; + readonly oauthError?: string; + readonly oauthErrorDescription?: string; + readonly retryable: boolean; + readonly status?: number; + readonly cause?: unknown; + + constructor(code: OidcErrorCode, message: string, options: OidcErrorOptions) { + super(message); + this.name = 'OidcError'; + this.code = code; + this.phase = options.phase; + this.oauthError = options.oauthError; + this.oauthErrorDescription = options.oauthErrorDescription; + this.retryable = options.retryable; + this.status = options.status; + this.cause = options.cause; + + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export const isOidcError = (value: unknown): value is OidcError => value instanceof OidcError; + +export const isRetryableHttpStatus = (status?: number): boolean => + status === 408 || status === 429 || (status !== undefined && status >= 500); + +export const oidcErrorCodeFromOAuthError = (oauthError?: string): OidcErrorCode => { + switch (oauthError) { + case 'login_required': + return OidcErrorCode.LOGIN_REQUIRED; + case 'consent_required': + return OidcErrorCode.CONSENT_REQUIRED; + case 'interaction_required': + return OidcErrorCode.INTERACTION_REQUIRED; + case 'use_dpop_nonce': + return OidcErrorCode.DPOP_NONCE_REQUIRED; + default: + return OidcErrorCode.OAUTH_ERROR; + } +}; + +export const createOAuthError = ( + oauthError: string | undefined, + oauthErrorDescription: string | undefined, + message: string, + phase: OidcErrorPhase, + status?: number, +): OidcError => { + const code = oidcErrorCodeFromOAuthError(oauthError); + const requiresInteraction = + code === OidcErrorCode.LOGIN_REQUIRED || + code === OidcErrorCode.CONSENT_REQUIRED || + code === OidcErrorCode.INTERACTION_REQUIRED; + const retryable = + !requiresInteraction && + (code === OidcErrorCode.DPOP_NONCE_REQUIRED || + oauthError === 'server_error' || + oauthError === 'temporarily_unavailable' || + isRetryableHttpStatus(status)); + return new OidcError(code, message, { + phase, + retryable, + oauthError, + oauthErrorDescription, + status, + }); +}; + +export const createNetworkError = ( + cause: unknown, + phase: OidcErrorPhase, + fallbackMessage = 'Network request failed', +): OidcError => { + const message = + cause instanceof Error && typeof cause.message === 'string' && cause.message + ? cause.message + : fallbackMessage; + return new OidcError(OidcErrorCode.NETWORK_ERROR, message, { + phase, + retryable: true, + cause, + }); +}; + +export const isNetworkErrorCause = (value: unknown): boolean => { + if (!(value instanceof Error)) { + return false; + } + return ( + value.name === 'AbortError' || + value.name === 'NetworkError' || + value.message === 'Network request failed' || + value.message === 'Failed to fetch' || + value.message === 'fetch failed' + ); +}; + +export const serializeOidcError = (error: OidcError): SerializedOidcError => ({ + name: error.name, + message: error.message, + code: error.code, + phase: error.phase, + retryable: error.retryable, + oauthError: error.oauthError, + oauthErrorDescription: error.oauthErrorDescription, + status: error.status, +}); + +export const deserializeOidcError = (value: unknown): OidcError | null => { + if (!value || typeof value !== 'object') { + return null; + } + const serialized = value as Partial; + if ( + typeof serialized.message !== 'string' || + typeof serialized.code !== 'string' || + !oidcErrorCodes.has(serialized.code) || + typeof serialized.phase !== 'string' || + !oidcErrorPhases.has(serialized.phase as OidcErrorPhase) || + typeof serialized.retryable !== 'boolean' + ) { + return null; + } + const error = new OidcError(serialized.code as OidcErrorCode, serialized.message, { + phase: serialized.phase as OidcErrorPhase, + retryable: serialized.retryable, + oauthError: typeof serialized.oauthError === 'string' ? serialized.oauthError : undefined, + oauthErrorDescription: + typeof serialized.oauthErrorDescription === 'string' + ? serialized.oauthErrorDescription + : undefined, + status: typeof serialized.status === 'number' ? serialized.status : undefined, + }); + if (typeof serialized.name === 'string' && serialized.name) { + error.name = serialized.name; + } + return error; +}; diff --git a/packages/oidc-client/src/oidcStateError.spec.ts b/packages/oidc-client/src/oidcStateError.spec.ts new file mode 100644 index 000000000..22379bdbc --- /dev/null +++ b/packages/oidc-client/src/oidcStateError.spec.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest'; + +import { deserializeOidcError, OidcError, serializeOidcError } from './oidcError'; +import { isOidcStateError, OidcStateError, OidcStateErrorCode } from './oidcStateError'; + +describe('OidcStateError', () => { + it('exposes the well-known state error codes', () => { + expect(OidcStateErrorCode.STATE_MISSING).toBe('STATE_MISSING'); + expect(OidcStateErrorCode.STATE_MISMATCH).toBe('STATE_MISMATCH'); + expect(OidcStateErrorCode.NONCE_MISSING).toBe('NONCE_MISSING'); + }); + + it('is an Error subclass with name "OidcStateError"', () => { + const err = new OidcStateError(OidcStateErrorCode.STATE_MISSING, 'missing'); + expect(err).toBeInstanceOf(Error); + expect(err).toBeInstanceOf(OidcError); + expect(err).toBeInstanceOf(OidcStateError); + expect(err.name).toBe('OidcStateError'); + }); + + it('preserves the code and message passed to the constructor', () => { + const err = new OidcStateError(OidcStateErrorCode.STATE_MISMATCH, 'mismatch happened'); + expect(err.code).toBe('STATE_MISMATCH'); + expect(err.message).toBe('mismatch happened'); + expect(err.phase).toBe('callback'); + expect(err.retryable).toBe(false); + }); + + it('is detectable via isOidcStateError', () => { + const err = new OidcStateError(OidcStateErrorCode.NONCE_MISSING, 'nonce missing'); + expect(isOidcStateError(err)).toBe(true); + expect(isOidcStateError(new Error('boom'))).toBe(false); + expect(isOidcStateError(null)).toBe(false); + expect(isOidcStateError(undefined)).toBe(false); + expect(isOidcStateError({ code: 'STATE_MISSING' })).toBe(false); + }); + + it('stays detectable after serialization and deserialization', () => { + const err = new OidcStateError(OidcStateErrorCode.NONCE_MISSING, 'nonce missing', 'refresh'); + const reconstructed = deserializeOidcError(serializeOidcError(err)); + + expect(reconstructed).toBeInstanceOf(OidcError); + expect(reconstructed).not.toBeInstanceOf(OidcStateError); + expect(isOidcStateError(reconstructed)).toBe(true); + expect(reconstructed).toMatchObject({ + code: OidcStateErrorCode.NONCE_MISSING, + phase: 'refresh', + name: 'OidcStateError', + }); + }); +}); diff --git a/packages/oidc-client/src/oidcStateError.ts b/packages/oidc-client/src/oidcStateError.ts new file mode 100644 index 000000000..98840f3db --- /dev/null +++ b/packages/oidc-client/src/oidcStateError.ts @@ -0,0 +1,71 @@ +import { + isOidcError, + OidcError, + OidcErrorCode as BaseOidcErrorCode, + OidcErrorPhase, +} from './oidcError.js'; + +/** + * Stable, machine-readable codes for OIDC state / nonce failures occurring + * between the authorization redirect and the callback handling. + * + * These codes let consumers react to specific failure modes without having to + * pattern-match against error message strings. + */ +export const OidcStateErrorCode = { + /** No state was found in storage when handling the callback. */ + STATE_MISSING: 'STATE_MISSING', + /** The state returned by the server does not match the stored one. */ + STATE_MISMATCH: 'STATE_MISMATCH', + /** No nonce was found in storage when handling the callback / renewal. */ + NONCE_MISSING: 'NONCE_MISSING', +} as const; + +// Companion type that mirrors the const above. This is the standard TS +// "string-enum-like" pattern; we intentionally reuse the same name so that +// `OidcStateErrorCode` works as both a value namespace and a type for +// consumers. +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type OidcStateErrorCode = (typeof OidcStateErrorCode)[keyof typeof OidcStateErrorCode]; + +const oidcStateErrorCodes = new Set(Object.values(OidcStateErrorCode)); + +export const isOidcStateErrorCode = (value: unknown): value is OidcStateErrorCode => + typeof value === 'string' && oidcStateErrorCodes.has(value); + +/** + * Typed error thrown when the OIDC login state or nonce is missing, + * corrupted, or does not match the value returned by the authorization server. + * + * Consumers should prefer {@link isOidcStateError} and inspect `code` instead + * of relying on the (unstable) error message text, especially when errors may + * cross a postMessage boundary during silent renew. + */ +export class OidcStateError extends OidcError { + declare readonly code: OidcStateErrorCode; + + constructor( + code: OidcStateErrorCode, + message: string, + phase: Extract = 'callback', + ) { + super(code as BaseOidcErrorCode, message, { + phase, + retryable: false, + }); + this.name = 'OidcStateError'; + + // Keep prototype chain intact when transpiled to ES5. + Object.setPrototypeOf(this, OidcStateError.prototype); + } +} + +/** + * Type guard for {@link OidcStateError}. Useful in callers that want to react + * specifically to state/nonce failures. + */ +export const isOidcStateError = (value: unknown): value is OidcStateError => { + return ( + value instanceof OidcStateError || (isOidcError(value) && isOidcStateErrorCode(value.code)) + ); +}; diff --git a/packages/oidc-client/src/par.spec.ts b/packages/oidc-client/src/par.spec.ts new file mode 100644 index 000000000..63debefd4 --- /dev/null +++ b/packages/oidc-client/src/par.spec.ts @@ -0,0 +1,232 @@ +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; + +import { ILOidcLocation } from './location'; +import { defaultLoginAsync } from './login'; +import { OidcAuthorizationServiceConfiguration } from './oidc'; +import { PushedAuthorizationRequestErrorCode } from './pushedAuthorizationRequestError'; +import { Fetch, OidcConfiguration } from './types'; + +const makeStorage = (): Storage => { + const store: Record = {}; + return { + getItem: (key: string) => store[key] ?? null, + setItem: (key: string, value: string) => { + store[key] = value; + }, + removeItem: (key: string) => { + delete store[key]; + }, + clear: () => { + for (const key of Object.keys(store)) { + delete store[key]; + } + }, + get length() { + return Object.keys(store).length; + }, + key: (index: number) => Object.keys(store)[index] ?? null, + [Symbol.iterator]: function* () { + yield* Object.entries(store); + }, + } as unknown as Storage; +}; + +class FakeLocation implements ILOidcLocation { + openedUrl: string = null; + + open(url: string): void { + this.openedUrl = url; + } + + reload(): void {} + + getCurrentHref(): string { + return ''; + } + + getPath(): string { + return '/current'; + } + + getOrigin(): string { + return 'https://client.example.com'; + } +} + +const createConfiguration = (overrides: Partial = {}): OidcConfiguration => { + const storage = makeStorage(); + return { + client_id: 'client', + redirect_uri: 'https://client.example.com/callback', + scope: 'openid profile', + authority: 'https://issuer.example.com', + storage, + login_state_storage: storage, + ...overrides, + }; +}; + +const createMetadata = (overrides: Record = {}) => ({ + authorizationEndpoint: 'https://issuer.example.com/authorize', + issuer: 'https://issuer.example.com', + pushedAuthorizationRequestEndpoint: 'https://issuer.example.com/par', + requirePushedAuthorizationRequests: false, + ...overrides, +}); + +const asFetch = (mock: ReturnType): Fetch => mock as unknown as Fetch; + +const runLogin = async ({ + configuration, + metadata, + fetchMock = vi.fn(), +}: { + configuration: OidcConfiguration; + metadata: Record; + fetchMock?: ReturnType; +}) => { + const location = new FakeLocation(); + const publishEvent = vi.fn(); + const initAsync = vi.fn(async () => metadata); + + const promise = defaultLoginAsync( + 'default', + configuration, + publishEvent, + initAsync, + location, + () => asFetch(fetchMock), + )('/return'); + + return { promise, location, publishEvent, fetchMock }; +}; + +describe('PAR mode selection', () => { + const originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window'); + + beforeAll(() => { + Object.defineProperty(globalThis, 'window', { + configurable: true, + value: { crypto: globalThis.crypto }, + }); + }); + + afterAll(() => { + if (originalWindow) { + Object.defineProperty(globalThis, 'window', originalWindow); + } else { + delete (globalThis as { window?: unknown }).window; + } + }); + + it('maps PAR discovery metadata to the authorization service configuration', () => { + const metadata = new OidcAuthorizationServiceConfiguration({ + authorization_endpoint: 'https://issuer.example.com/authorize', + pushed_authorization_request_endpoint: 'https://issuer.example.com/par', + require_pushed_authorization_requests: true, + issuer: 'https://issuer.example.com', + }) as any; + + expect(metadata.pushedAuthorizationRequestEndpoint).toBe('https://issuer.example.com/par'); + expect(metadata.requirePushedAuthorizationRequests).toBe(true); + }); + + it('keeps PAR disabled by default even when the server advertises it', async () => { + const { promise, location, fetchMock } = await runLogin({ + configuration: createConfiguration(), + metadata: createMetadata(), + }); + + await promise; + + expect(fetchMock).not.toHaveBeenCalled(); + const redirect = new URL(location.openedUrl); + expect(redirect.searchParams.get('request_uri')).toBeNull(); + expect(redirect.searchParams.get('redirect_uri')).toBe('https://client.example.com/callback'); + }); + + it('uses PAR in auto mode when the endpoint is advertised', async () => { + const fetchMock = vi.fn(async () => ({ + status: 201, + json: async () => ({ + request_uri: 'urn:ietf:params:oauth:request_uri:auto', + expires_in: 60, + }), + })); + const { promise, location } = await runLogin({ + configuration: createConfiguration({ par: 'auto' }), + metadata: createMetadata(), + fetchMock, + }); + + await promise; + + expect(fetchMock).toHaveBeenCalledOnce(); + expect(Object.fromEntries(new URL(location.openedUrl).searchParams)).toEqual({ + client_id: 'client', + request_uri: 'urn:ietf:params:oauth:request_uri:auto', + }); + }); + + it('uses the existing front-channel flow in auto mode when PAR is not advertised', async () => { + const { promise, location, fetchMock } = await runLogin({ + configuration: createConfiguration({ par: 'auto' }), + metadata: createMetadata({ pushedAuthorizationRequestEndpoint: undefined }), + }); + + await promise; + + expect(fetchMock).not.toHaveBeenCalled(); + expect(new URL(location.openedUrl).searchParams.get('redirect_uri')).toBe( + 'https://client.example.com/callback', + ); + }); + + it.each([ + ['required', false], + ['auto', true], + ] as const)( + 'fails before navigation in %s mode when no PAR endpoint is available', + async (par, requirePushedAuthorizationRequests) => { + const { promise, location, publishEvent } = await runLogin({ + configuration: createConfiguration({ par }), + metadata: createMetadata({ + pushedAuthorizationRequestEndpoint: undefined, + requirePushedAuthorizationRequests, + }), + }); + + await expect(promise).rejects.toMatchObject({ + code: PushedAuthorizationRequestErrorCode.ENDPOINT_UNAVAILABLE, + }); + expect(location.openedUrl).toBeNull(); + expect(publishEvent).toHaveBeenLastCalledWith( + 'loginAsync_error', + expect.objectContaining({ + code: PushedAuthorizationRequestErrorCode.ENDPOINT_UNAVAILABLE, + }), + ); + }, + ); + + it('does not silently downgrade to a front-channel request after a PAR error', async () => { + const fetchMock = vi.fn(async () => ({ + status: 400, + json: async () => ({ + error: 'invalid_request', + error_description: 'Invalid authorization request', + }), + })); + const { promise, location } = await runLogin({ + configuration: createConfiguration({ par: 'auto' }), + metadata: createMetadata(), + fetchMock, + }); + + await expect(promise).rejects.toMatchObject({ + code: PushedAuthorizationRequestErrorCode.REQUEST_FAILED, + status: 400, + }); + expect(location.openedUrl).toBeNull(); + }); +}); diff --git a/packages/oidc-client/src/parseTokens.spec.ts b/packages/oidc-client/src/parseTokens.spec.ts index a18db08be..00651fbac 100644 --- a/packages/oidc-client/src/parseTokens.spec.ts +++ b/packages/oidc-client/src/parseTokens.spec.ts @@ -1,49 +1,276 @@ -import {getValidTokenAsync, isTokensOidcValid} from "./parseTokens"; -import { describe, it, expect } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { sleepAsync } from './initWorker'; +import { + getValidTokenAsync, + isTokensOidcValid, + parseJwt, + parseOriginalTokens, + setTokens, + TokenRenewMode, +} from './parseTokens'; +import { synchroniseTokensStatus } from './renewTokens'; +import { StringMap, TokenAutomaticRenewMode } from './types'; describe('ParseTokens test Suite', () => { - const currentTimeUnixSecond = new Date().getTime() / 1000; - describe.each([ - [currentTimeUnixSecond + 120, currentTimeUnixSecond - 10, true], - [currentTimeUnixSecond - 20, currentTimeUnixSecond - 50, false], - ])('getValidTokenAsync', (expiresAt, issuedAt, expectIsValidToken) => { - it('should getValidTokenAsync wait and return value', async () => { - const oidc = { - tokens: { - refreshToken: 'youhou', - idTokenPayload: null, - idToken: 'youhou', - accessTokenPayload: null, - accessToken: 'youhou', - expiresAt: expiresAt, - issuedAt: issuedAt, - } - } - const result = await getValidTokenAsync(oidc, 1, 1); - expect(result.isTokensValid).toEqual(expectIsValidToken); - }); + describe.each(['accessToken', 'idToken'])('%s payload extraction', tokenProperty => { + const payload = { sub: 'unit-test', name: 'Test user' }; + const encodedPayload = btoa(JSON.stringify(payload)); + + afterEach(() => { + vi.restoreAllMocks(); }); + it.each([`header.${encodedPayload}.signature`, `.${encodedPayload}.`])( + 'extracts the payload from a three-part token: %s', + token => { + const tokens = setTokens( + { [tokenProperty]: token, issuedAt: 1000, expiresIn: 60 }, + null, + TokenRenewMode.access_token_invalid, + ); + + expect(tokens[`${tokenProperty}Payload`]).toEqual(payload); + expect(tokens[tokenProperty]).toBe(token); + }, + ); + + it.each([ + undefined, + null, + '', + 'opaque-token', + `header.${encodedPayload}`, + `header.${encodedPayload}.signature.extra`, + `header.${encodedPayload}.signature.`, + ])('returns a null payload for a token without exactly three parts: %s', token => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const tokens = setTokens( + { [tokenProperty]: token, issuedAt: 1000, expiresIn: 60 }, + null, + TokenRenewMode.access_token_invalid, + ); + + expect(tokens[`${tokenProperty}Payload`]).toBeNull(); + expect(warn).not.toHaveBeenCalled(); + }); + + it.each(['header..signature', 'header.!.signature', 'header.bm90IGpzb24=.signature'])( + 'warns and returns a null payload when decoding fails: %s', + token => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const tokens = setTokens( + { [tokenProperty]: token, issuedAt: 1000, expiresIn: 60 }, + null, + TokenRenewMode.access_token_invalid, + ); + + expect(tokens[`${tokenProperty}Payload`]).toBeNull(); + expect(warn).toHaveBeenCalledExactlyOnceWith(expect.any(Error)); + }, + ); + }); + + const currentTimeUnixSecond = new Date().getTime() / 1000; + describe.each([ + [currentTimeUnixSecond + 120, currentTimeUnixSecond - 10, true], + [currentTimeUnixSecond - 20, currentTimeUnixSecond - 50, false], + ])('getValidTokenAsync', (expiresAt, issuedAt, expectIsValidToken) => { + it('should getValidTokenAsync wait and return value', async () => { + const oidc = { + getTokens: () => { + return { + refreshToken: 'youhou', + idTokenPayload: null, + idToken: 'youhou', + accessTokenPayload: null, + accessToken: 'youhou', + expiresAt, + issuedAt, + }; + }, + configuration: { + token_automatic_renew_mode: TokenAutomaticRenewMode.AutomaticBeforeTokenExpiration, + refresh_time_before_tokens_expiration_in_second: 0, + }, + syncTokensInfoAsync: async () => synchroniseTokensStatus.TOKENS_VALID, + renewTokensAsync: async (_extras: StringMap): Promise => { + await sleepAsync({ milliseconds: 10 }); + return undefined; + }, + }; + const result = await getValidTokenAsync(oidc, 1, 1); + expect(result.isTokensValid).toEqual(expectIsValidToken); + }); + }); + + describe.each([ + [ + 'eyJzZXNzaW9uX3N0YXRlIjoiNzVjYzVlZDItZGYyZC00NTY5LWJmYzUtMThhOThlNjhiZTExIiwic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoixrTHosOBw6zDhyDlsI_lkI0t44Ob44Or44OYIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidGVzdGluZ2NoYXJhY3RlcnNAaW52ZW50ZWRtYWlsLmNvbSIsImdpdmVuX25hbWUiOiLGtMeiw4HDrMOHIiwiZmFtaWx5X25hbWUiOiLlsI_lkI0t44Ob44Or44OYIn0', + { + session_state: '75cc5ed2-df2d-4569-bfc5-18a98e68be11', + scope: 'openid email profile', + email_verified: true, + name: 'ƴǢÁìÇ 小名-ホルヘ', + preferred_username: 'testingcharacters@inventedmail.com', + given_name: 'ƴǢÁìÇ', + family_name: '小名-ホルヘ', + }, + ], + [ + 'eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCI_IjoiYWE_In0', + { + '?': 'aa?', + iat: 1516239022, + name: 'John Doe', + sub: '1234567890', + }, + ], + ])('parseJwtShouldExtractData', (claimsPart, expectedResult) => { + it('should parseJwtShouldExtractData ', async () => { + const result = parseJwt(claimsPart); + expect(expectedResult).toStrictEqual(result); + }); + }); + + const id_token = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjUwNWZkODljLTM4YzktNGI2Mi04ZjQ3LWI4MGQ0ZTNhYjYxNSJ9.eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3Q6ODA4MCIsInN1YiI6ImFkbWluIiwiYXVkIjoiM2FTbk5XUGxZQWQwOGVES3c1UUNpSWVMcWpIdHkxTTVzSGFzcDJDZWREcWYzbmJkZm8xUFo1cXhmbWoyaFhkUyIsImV4cCI6MTY5MDk4NzQ1NCwiYXV0aF90aW1lIjoxNjkwOTg2NTUxLCJpYXQiOjE2OTA5ODY1NTQsImFjciI6IjAiLCJhenAiOiIzYVNuTldQbFlBZDA4ZURLdzVRQ2lJZUxxakh0eTFNNXNIYXNwMkNlZERxZjNuYmRmbzFQWjVxeGZtajJoWGRTIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWRtaW4iLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGdyb3VwcyBvZmZsaW5lX2FjY2VzcyIsIm5iZiI6MTY5MDk4NjU1NCwianRpIjoiNjMiLCJub25jZSI6ImNpQkVVOTdaVmRWVSIsImdyb3VwcyI6WyJhZG1pbiJdLCJuYW1lIjoiQWRtaW5pc3RyYXRvciIsInVwZGF0ZWRfYXQiOjE2OTA5ODY1NDV9.2MUdtQR_QtzDY9BTMctG8C4uvg92DgMIUUoJed2cI7WTd5_VEPFW87esDQLw4snVdAJM1_Wf3wB88B2MXFDMCnMTNn0TMnzetRDiG3xlr2LL-geL5SNgwD0Y6RPK_aITjrC9uiQCTj3LPEENrBulNRZPURwaVon9WUVNuuBmMTKd7QKEuFN0zYDoRs0HnXo6WKnFy1rldLGh_JpA3PBUuXt4VMjfGQ7yYEuNn7MkFVDX6OnTffR8jTQp74hREvuRLFjYxfgfgu547X7yIcboOl81D0ZQlP-gfvBOeypZolRLScuqAA3fHBYvE0vCtOM6ObekfeeTDfms75csMLUuZtTR07x32xYC8vdoFsY0sRpMByTqlhsae9VX_rETJ7PIWEfruojzcj47WN9dG0K3pdPiJHEwZ1CKgZfU_cY0gtuAGaIcIjKL0txXCevaiIiIsrgSU_HTjNVybp4WHSAs3h6x0XLz4_91luCylsaoMQbwKOQNwAfr2L74jF6DOg-8DIPb-WClRQzaQtrkx_iv6FtqCB3ogFoZwi6xljdYUc2EHUmoAo-LXal-QAgUXGGzfFU2YOpxV3RyAbMGPm7PfkMVzDsDJwORJNhh38QQ6o88GgNnV28BT-d2G0n7okc0QC6o2IW0jpyCrI6v0hWOBUX2EqiJ5Wao-4LYZfCaRgU'; + const refresh_token = + 'DEsqDca7nDGSgT6tJPkCwbPy98B8VOC4AA55lOPs03G3hqhZ8WH08REBcwTZg1s0jZyVoA3iCXzm4PPJ096gjV7ZKYyN8vnFKw6P6KLV3tUI6mWFaSROoh1LipThFrkS'; + const access_token = 'opqavdgHEYx8nhCdc3iByd1HD0jiYN30LevhJy4f5wIavINXKdh4lQ9C3kA49QF0OH0XeA02'; + describe.each([ + [ + { + access_token: access_token, + token_type: 'Bearer', + expires_in: '900', // Here a string instead of a number + refresh_token: refresh_token, + id_token: id_token, + }, + ], + [ + { + access_token: access_token, + token_type: 'Bearer', + expires_in: 900, + refresh_token: refresh_token, + id_token: id_token, + }, + ], + [ + { + access_token: access_token, + token_type: 'Bearer', + expires_in: 900, + expiresAt: 1609987454, // Here expiresAt that come from Service Worker + refresh_token: refresh_token, + id_token: id_token, + }, + ], + ])('getValidTokenAsync', tokens => { + it('should parseOriginalTokens', async () => { + // @ts-ignore + const result = parseOriginalTokens(tokens); + expect(typeof result.issuedAt).toEqual('number'); + }); + }); + + const idTokenPayload = { + iss: 'toto', + exp: currentTimeUnixSecond + 900, + iat: currentTimeUnixSecond - 900, + nonce: 'nonce', + }; + const oidcServerConfiguration = { issuer: 'toto' }; + const idTokenPayloadExpired = { ...idTokenPayload, exp: currentTimeUnixSecond - 20 }; + const idTokenPayloadIssuedTooLongTimeAgo = { + ...idTokenPayload, + iat: currentTimeUnixSecond - 20000000, + }; + + describe.each([ + [idTokenPayload, 'nonce', oidcServerConfiguration, true, 'success'], + [idTokenPayload, 'other_nonce', oidcServerConfiguration, false, 'bad nonce'], + [idTokenPayload, 'nonce', { issuer: 'tutu' }, false, 'different issuer'], + [idTokenPayloadExpired, 'nonce', oidcServerConfiguration, false, 'id token expired issuer'], + [ + idTokenPayloadIssuedTooLongTimeAgo, + 'nonce', + oidcServerConfiguration, + false, + 'id token expired issuer', + ], + ])( + 'isTokensOidcValid', + (idTokenPayload, nonce, oidcServerConfiguration, expectIsValidToken, status) => { + it('should isTokensOidcValid return ' + status, async () => { + const oidc = { + idTokenPayload, + }; + const { isValid } = isTokensOidcValid(oidc, nonce, oidcServerConfiguration); + expect(isValid).toEqual(expectIsValidToken); + }); + }, + ); + + const testTokens = { + id_token: + 'eyJhbGciOiJSUzI1NiIsImtpZCI6IkMyNTJGOUNBQjc3Q0MxNTQwNTBFMTg1NTk5MjJCMTJGIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2RlbW8uZHVlbmRlc29mdHdhcmUuY29tIiwibmJmIjoxNzA2NTQwMjU4LCJpYXQiOjE3MDY1NDAyNTgsImV4cCI6MTcwNjU0MDU1OCwiYXVkIjoiaW50ZXJhY3RpdmUucHVibGljLnNob3J0IiwiYW1yIjpbInB3ZCJdLCJub25jZSI6IlA5dEo5eGxHZE05NiIsImF0X2hhc2giOiJOWnZhR0dZYlhoelRNWlVxUjlNYk5nIiwic2lkIjoiMzQ1QUJDODhFNkU1MEFGMTI3M0VENDE1QTdGRDZBMjMiLCJzdWIiOiIyIiwiYXV0aF90aW1lIjoxNzA2NTMxNjY1LCJpZHAiOiJsb2NhbCJ9.MVtXrCkshJFBplbOw7az3fdWB1Ewqixb2fuHXpx7KbGWUY6qgT9ijlldeD-ZV7JGA958AKqmGwfNjovAJE89pQsCFKkNft6fRO8eM9qKif6eRUqMMPiQrawARpuJOs1NvJ-SyeRs_jSNLwPVzI8NlZyFWHoyQ4DZnFoQLSQMy5UaHaCtWhC_FrWMFLQvbE3RuMlnJGzrsoMewFyVAZctMCTE1MOI3Akvhe1IGc1hmxzwNg3OkxwzHLinsDlDw8UVn8vX5iNI18GFuyTuJlawOq5OHHJH3LdKQD_RbwRF-9BFjKRZfWzGpdpxTD2lIPf1Irc3U_R6xCNuXYUwzrHp6Q', + access_token: 'ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER_default', + expires_in: 75, + token_type: 'Bearer', + refresh_token: 'REFRESH_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER_default', + scope: 'openid profile email api offline_access', + issued_at: 1706540256.465, + accessTokenPayload: { + iss: 'https://demo.duendesoftware.com', + nbf: 1706540258, + iat: 1706540258, + exp: 1706540333, + aud: 'api', + scope: ['openid', 'profile', 'email', 'api', 'offline_access'], + amr: ['pwd'], + client_id: 'interactive.public.short', + sub: '2', + auth_time: 1706531665, + idp: 'local', + name: 'Bob Smith', + email: 'BobSmith@email.com', + sid: '345ABC88E6E50AF1273ED415A7FD6A23', + jti: 'E3CF3853D77AC90ABC774266CD381C43', + }, + idTokenPayload: { + iss: 'https://demo.duendesoftware.com', + nbf: 1706540258, + iat: 1706540258, + exp: 1706540558, + aud: 'interactive.public.short', + amr: ['pwd'], + nonce: 'NONCE_SECURED_BY_OIDC_SERVICE_WORKER_default', + at_hash: 'NZvaGGYbXhzTMZUqR9MbNg', + sid: '345ABC88E6E50AF1273ED415A7FD6A23', + sub: '2', + auth_time: 1706531665, + idp: 'local', + }, + expiresAt: 1706540333, + }; - const idTokenPayload = {iss: "toto", exp: currentTimeUnixSecond +900, iat: currentTimeUnixSecond -900, nonce: "nonce"}; - const oidcServerConfiguration = {issuer:"toto"}; - const idTokenPayloadExpired = {...idTokenPayload, exp: currentTimeUnixSecond-20}; - const idTokenPayloadIssuedTooLongTimeAgo = {...idTokenPayload, iat: currentTimeUnixSecond-20000000}; - - describe.each([ - [idTokenPayload, "nonce", oidcServerConfiguration, true, "success"], - [idTokenPayload, "other_nonce", oidcServerConfiguration, false, "bad nonce"], - [idTokenPayload, "nonce", {issuer:"tutu"}, false, "different issuer"], - [idTokenPayloadExpired, "nonce", oidcServerConfiguration, false, "id token expired issuer"], - [idTokenPayloadIssuedTooLongTimeAgo, "nonce", oidcServerConfiguration, false, "id token expired issuer"], - ])('isTokensOidcValid', (idTokenPayload, nonce, oidcServerConfiguration, expectIsValidToken, status) => { - it('should isTokensOidcValid return ' + status, async () => { - const oidc = { - idTokenPayload - } - const {isValid} = await isTokensOidcValid(oidc, nonce, oidcServerConfiguration); - expect(isValid).toEqual(expectIsValidToken); - }); + describe.each([ + [testTokens, null, TokenRenewMode.access_token_invalid, () => {}], + [ + testTokens, + { testTokens, idTokenPayload: undefined, id_token: undefined }, + TokenRenewMode.access_token_invalid, + (newTokens: any) => { + expect(newTokens.idTokenPayload).toBeDefined(); + expect(newTokens.id_token).toBeDefined(); + }, + ], + ])('setTokens', (tokens, oldTokens, tokenRenewMode, validationFunction) => { + it('should setTokens return updatedTokens', async () => { + const newTokens = setTokens(tokens, oldTokens, tokenRenewMode); + validationFunction(newTokens); }); - + }); }); diff --git a/packages/oidc-client/src/parseTokens.ts b/packages/oidc-client/src/parseTokens.ts index c5ddd8ab9..f79c1f44a 100644 --- a/packages/oidc-client/src/parseTokens.ts +++ b/packages/oidc-client/src/parseTokens.ts @@ -1,194 +1,277 @@ import { sleepAsync } from './initWorker.js'; +import { synchroniseTokensStatus } from './renewTokens'; +import { StringMap, TokenAutomaticRenewMode } from './types'; -const b64DecodeUnicode = (str) => - decodeURIComponent(Array.prototype.map.call(atob(str), (c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')); -const parseJwt = (token) => JSON.parse(b64DecodeUnicode(token.split('.')[1].replace('-', '+').replace('_', '/'))); - -const extractTokenPayload = (token) => { - try { - if (!token) { - return null; - } - if (countLetter(token, '.') === 2) { - return parseJwt(token); - } else { - return null; - } - } catch (e) { - console.warn(e); - } - return null; -}; +const b64DecodeUnicode = str => + decodeURIComponent( + Array.prototype.map + .call(atob(str), c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) + .join(''), + ); +export const parseJwt = (payload: string) => + JSON.parse(b64DecodeUnicode(payload.replaceAll(/-/g, '+').replaceAll(/_/g, '/'))); -const countLetter = (str, find) => { - return (str.split(find)).length - 1; +const extractTokenPayload = (token: string) => { + try { + if (!token) { + return null; + } + const parts = token.split('.'); + return parts.length === 3 ? parseJwt(parts[1]) : null; + } catch (e) { + console.warn(e); + } + return null; }; export type Tokens = { - refreshToken: string; - idTokenPayload:any; - idToken:string; - accessTokenPayload:any; - accessToken:string; - expiresAt: number; - issuedAt: number; + refreshToken: string; + idTokenPayload: any; + idToken: string; + accessTokenPayload: any; + accessToken: string; + expiresAt: number; + issuedAt: number; }; export type TokenRenewModeType = { - access_token_or_id_token_invalid: string; - access_token_invalid:string; - id_token_invalid: string; -} + access_token_or_id_token_invalid: string; + access_token_invalid: string; + id_token_invalid: string; +}; export const TokenRenewMode = { - access_token_or_id_token_invalid: 'access_token_or_id_token_invalid', - access_token_invalid: 'access_token_invalid', - id_token_invalid: 'id_token_invalid', + access_token_or_id_token_invalid: 'access_token_or_id_token_invalid', + access_token_invalid: 'access_token_invalid', + id_token_invalid: 'id_token_invalid', }; -export const setTokens = (tokens, oldTokens = null, tokenRenewMode: string):Tokens => { - if (!tokens) { - return null; +function extractedIssueAt(tokens, accessTokenPayload, _idTokenPayload) { + if (!tokens.issuedAt) { + if (accessTokenPayload && accessTokenPayload.iat) { + return accessTokenPayload.iat; + } else if (_idTokenPayload && _idTokenPayload.iat) { + return _idTokenPayload.iat; + } else { + const currentTimeUnixSecond = new Date().getTime() / 1000; + return currentTimeUnixSecond; } - let accessTokenPayload; + } else if (typeof tokens.issuedAt == 'string') { + return parseInt(tokens.issuedAt, 10); + } + return tokens.issuedAt; +} - if (!tokens.issuedAt) { - const currentTimeUnixSecond = new Date().getTime() / 1000; - tokens.issuedAt = currentTimeUnixSecond; - } +export const setTokens = (tokens, oldTokens = null, tokenRenewMode: string): Tokens => { + if (!tokens) { + return null; + } + let accessTokenPayload; + const expireIn = + typeof tokens.expiresIn == 'string' ? parseInt(tokens.expiresIn, 10) : tokens.expiresIn; - if (tokens.accessTokenPayload !== undefined) { - accessTokenPayload = tokens.accessTokenPayload; - } else { - accessTokenPayload = extractTokenPayload(tokens.accessToken); - } - const _idTokenPayload = tokens.idTokenPayload ? tokens.idTokenPayload : extractTokenPayload(tokens.idToken); + if (tokens.accessTokenPayload !== undefined) { + accessTokenPayload = tokens.accessTokenPayload; + } else { + accessTokenPayload = extractTokenPayload(tokens.accessToken); + } + + // When id_token is not rotated we reuse old id_token + let idToken: string; + if (oldTokens != null && 'idToken' in oldTokens && !('idToken' in tokens)) { + idToken = oldTokens.idToken; + } else { + idToken = tokens.idToken; + } - const idTokenExpireAt = (_idTokenPayload && _idTokenPayload.exp) ? _idTokenPayload.exp : Number.MAX_VALUE; - const accessTokenExpiresAt = (accessTokenPayload && accessTokenPayload.exp) ? accessTokenPayload.exp : tokens.issuedAt + tokens.expiresIn; + const _idTokenPayload = tokens.idTokenPayload + ? tokens.idTokenPayload + : extractTokenPayload(idToken); - let expiresAt; + const idTokenExpireAt = + _idTokenPayload && _idTokenPayload.exp ? _idTokenPayload.exp : Number.MAX_VALUE; + const accessTokenExpiresAt = + accessTokenPayload && accessTokenPayload.exp + ? accessTokenPayload.exp + : tokens.issuedAt + expireIn; + tokens.issuedAt = extractedIssueAt(tokens, accessTokenPayload, _idTokenPayload); + + let expiresAt; + if (tokens.expiresAt) { + expiresAt = tokens.expiresAt; + } else { if (tokenRenewMode === TokenRenewMode.access_token_invalid) { - expiresAt = accessTokenExpiresAt; + expiresAt = accessTokenExpiresAt; } else if (tokenRenewMode === TokenRenewMode.id_token_invalid) { - expiresAt = idTokenExpireAt; + expiresAt = idTokenExpireAt; } else { - expiresAt = idTokenExpireAt < accessTokenExpiresAt ? idTokenExpireAt : accessTokenExpiresAt; + expiresAt = idTokenExpireAt < accessTokenExpiresAt ? idTokenExpireAt : accessTokenExpiresAt; } + } - const newTokens = { ...tokens, idTokenPayload: _idTokenPayload, accessTokenPayload, expiresAt }; - // When refresh_token is not rotated we reuse ald refresh_token - if (oldTokens != null && 'refreshToken' in oldTokens && !('refreshToken' in tokens)) { - const refreshToken = oldTokens.refreshToken; - return { ...newTokens, refreshToken }; - } + const newTokens = { + ...tokens, + idTokenPayload: _idTokenPayload, + accessTokenPayload, + expiresAt, + idToken, + }; + // When refresh_token is not rotated we reuse old refresh_token + if (oldTokens != null && 'refreshToken' in oldTokens && !('refreshToken' in tokens)) { + const refreshToken = oldTokens.refreshToken; + return { ...newTokens, refreshToken }; + } - return newTokens; + return newTokens; }; export const parseOriginalTokens = (tokens, oldTokens, tokenRenewMode: string) => { - if (!tokens) { - return null; - } - if (!tokens.issued_at) { - const currentTimeUnixSecond = new Date().getTime() / 1000; - tokens.issued_at = currentTimeUnixSecond; - } + if (!tokens) { + return null; + } + if (!tokens.issued_at) { + const currentTimeUnixSecond = new Date().getTime() / 1000; + tokens.issued_at = currentTimeUnixSecond; + } - const data = { - accessToken: tokens.access_token, - expiresIn: tokens.expires_in, - idToken: tokens.id_token, - scope: tokens.scope, - tokenType: tokens.token_type, - issuedAt: tokens.issued_at, - }; - - if ('refresh_token' in tokens) { - // @ts-ignore - data.refreshToken = tokens.refresh_token; - } + const data = { + accessToken: tokens.access_token, + expiresIn: tokens.expires_in, + idToken: tokens.id_token, + scope: tokens.scope, + tokenType: tokens.token_type, + issuedAt: tokens.issued_at, + }; - if (tokens.accessTokenPayload !== undefined) { - // @ts-ignore - data.accessTokenPayload = tokens.accessTokenPayload; - } + if ('refresh_token' in tokens) { + // @ts-ignore + data.refreshToken = tokens.refresh_token; + } - if (tokens.idTokenPayload !== undefined) { - // @ts-ignore - data.idTokenPayload = tokens.idTokenPayload; - } + if (tokens.accessTokenPayload !== undefined) { + // @ts-ignore + data.accessTokenPayload = tokens.accessTokenPayload; + } + + if (tokens.idTokenPayload !== undefined) { + // @ts-ignore + data.idTokenPayload = tokens.idTokenPayload; + } - return setTokens(data, oldTokens, tokenRenewMode); + return setTokens(data, oldTokens, tokenRenewMode); }; -export const computeTimeLeft = (refreshTimeBeforeTokensExpirationInSecond, expiresAt) => { - const currentTimeUnixSecond = new Date().getTime() / 1000; - return Math.round(((expiresAt - refreshTimeBeforeTokensExpirationInSecond) - currentTimeUnixSecond)); +export const computeTimeLeft = ( + refreshTimeBeforeTokensExpirationInSecond: number, + expiresAt: number, +) => { + const currentTimeUnixSecond = new Date().getTime() / 1000; + + const timeLeftSecond = expiresAt - currentTimeUnixSecond; + + return Math.round(timeLeftSecond - refreshTimeBeforeTokensExpirationInSecond); }; -export const isTokensValid = (tokens) => { - if (!tokens) { - return false; - } - return computeTimeLeft(0, tokens.expiresAt) > 0; +export const isTokensValid = (tokens, refreshTimeBeforeTokensExpirationInSecond: number = 0) => { + if (!tokens) { + return false; + } + return computeTimeLeft(refreshTimeBeforeTokensExpirationInSecond, tokens.expiresAt) > 0; }; export type ValidToken = { - isTokensValid: boolean; - tokens: Tokens; - numberWaited: number; -} + isTokensValid: boolean; + tokens: Tokens; + numberWaited: number; +}; -export interface OidcToken{ - tokens?: Tokens; +export interface OidcToken { + getTokens: () => Tokens | null; + syncTokensInfoAsync: () => Promise; + configuration: { + token_automatic_renew_mode?: TokenAutomaticRenewMode; + refresh_time_before_tokens_expiration_in_second?: number; + }; + renewTokensAsync: (extras: StringMap) => Promise; } -export const getValidTokenAsync = async (oidc: OidcToken, waitMs = 200, numberWait = 50): Promise => { - let numberWaitTemp = numberWait; - if (!oidc.tokens) { - return null; - } - while (!isTokensValid(oidc.tokens) && numberWaitTemp > 0) { - await sleepAsync(waitMs); - numberWaitTemp = numberWaitTemp - 1; +export const getValidTokenAsync = async ( + oidc: OidcToken, + waitMs = 200, + numberWait = 50, +): Promise => { + let numberWaitTemp = numberWait; + + let status = await oidc.syncTokensInfoAsync(); + while ( + [ + synchroniseTokensStatus.REQUIRE_SYNC_TOKENS, + synchroniseTokensStatus.TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_INVALID, + synchroniseTokensStatus.TOKENS_INVALID, + ].includes(status) && + numberWaitTemp > 0 + ) { + if ( + oidc.configuration.token_automatic_renew_mode == + TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted + ) { + await oidc.renewTokensAsync({}); + break; + } else { + await sleepAsync({ milliseconds: waitMs }); } - const isValid = isTokensValid(oidc.tokens); - return { - isTokensValid: isValid, - tokens: oidc.tokens, - numberWaited: numberWaitTemp - numberWait, - }; + numberWaitTemp = numberWaitTemp - 1; + + status = await oidc.syncTokensInfoAsync(); + } + const isValid = isTokensValid(oidc.getTokens()); + return { + isTokensValid: isValid, + tokens: oidc.getTokens(), + numberWaited: numberWaitTemp - numberWait, + }; }; // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation (excluding rules #1, #4, #5, #7, #8, #12, and #13 which did not apply). // https://github.com/openid/AppAuth-JS/issues/65 export const isTokensOidcValid = (tokens, nonce, oidcServerConfiguration) => { - if (tokens.idTokenPayload) { - const idTokenPayload = tokens.idTokenPayload; - // 2: The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) MUST exactly match the value of the iss (issuer) Claim. - if (oidcServerConfiguration.issuer !== idTokenPayload.iss) { - return { isValid: false, reason: 'Issuer does not match' }; - } - // 3: The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified by the iss (issuer) Claim as an audience. The aud (audience) Claim MAY contain an array with more than one element. The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences not trusted by the Client. - - // 6: If the ID Token is received via direct communication between the Client and the Token Endpoint (which it is in this flow), the TLS server validation MAY be used to validate the issuer in place of checking the token signature. The Client MUST validate the signature of all other ID Tokens according to JWS [JWS] using the algorithm specified in the JWT alg Header Parameter. The Client MUST use the keys provided by the Issuer. - - // 9: The current time MUST be before the time represented by the exp Claim. - const currentTimeUnixSecond = new Date().getTime() / 1000; - if (idTokenPayload.exp && idTokenPayload.exp < currentTimeUnixSecond) { - return { isValid: false, reason: 'Token expired' }; - } - // 10: The iat Claim can be used to reject tokens that were issued too far away from the current time, limiting the amount of time that nonces need to be stored to prevent attacks. The acceptable range is Client specific. - const timeInSevenDays = 60 * 60 * 24 * 7; - if (idTokenPayload.iat && (idTokenPayload.iat + timeInSevenDays) < currentTimeUnixSecond) { - return { isValid: false, reason: 'Token is used from too long time' }; - } - // 11: If a nonce value was sent in the Authentication Request, a nonce Claim MUST be present and its value checked to verify that it is the same value as the one that was sent in the Authentication Request. The Client SHOULD check the nonce value for replay attacks. The precise method for detecting replay attacks is Client specific. - if (idTokenPayload.nonce && idTokenPayload.nonce !== nonce) { - return { isValid: false, reason: 'Nonce does not match' }; - } + if (tokens.idTokenPayload) { + const idTokenPayload = tokens.idTokenPayload; + // 2: The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) MUST exactly match the value of the iss (issuer) Claim. + if (oidcServerConfiguration.issuer !== idTokenPayload.iss) { + return { + isValid: false, + reason: `Issuer does not match (oidcServerConfiguration issuer) ${oidcServerConfiguration.issuer} !== (idTokenPayload issuer) ${idTokenPayload.iss}`, + }; + } + // 3: The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified by the iss (issuer) Claim as an audience. The aud (audience) Claim MAY contain an array with more than one element. The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences not trusted by the Client. + + // 6: If the ID Token is received via direct communication between the Client and the Token Endpoint (which it is in this flow), the TLS server validation MAY be used to validate the issuer in place of checking the token signature. The Client MUST validate the signature of all other ID Tokens according to JWS [JWS] using the algorithm specified in the JWT alg Header Parameter. The Client MUST use the keys provided by the Issuer. + + // 9: The current time MUST be before the time represented by the exp Claim. + const currentTimeUnixSecond = new Date().getTime() / 1000; + if (idTokenPayload.exp && idTokenPayload.exp < currentTimeUnixSecond) { + return { + isValid: false, + reason: `Token expired (idTokenPayload exp) ${idTokenPayload.exp} < (currentTimeUnixSecond) ${currentTimeUnixSecond}`, + }; + } + // 10: The iat Claim can be used to reject tokens that were issued too far away from the current time, limiting the amount of time that nonces need to be stored to prevent attacks. The acceptable range is Client specific. + const timeInSevenDays = 60 * 60 * 24 * 7; + if (idTokenPayload.iat && idTokenPayload.iat + timeInSevenDays < currentTimeUnixSecond) { + return { + isValid: false, + reason: `Token is used from too long time (idTokenPayload iat + timeInSevenDays) ${idTokenPayload.iat + timeInSevenDays} < (currentTimeUnixSecond) ${currentTimeUnixSecond}`, + }; + } + // 11: If a nonce value was sent in the Authentication Request, a nonce Claim MUST be present and its value checked to verify that it is the same value as the one that was sent in the Authentication Request. The Client SHOULD check the nonce value for replay attacks. The precise method for detecting replay attacks is Client specific. + if (idTokenPayload.nonce && idTokenPayload.nonce !== nonce) { + return { + isValid: false, + reason: `Nonce does not match (idTokenPayload nonce) ${idTokenPayload.nonce} !== (nonce) ${nonce}`, + }; } - return { isValid: true, reason: '' }; + } + return { isValid: true, reason: '' }; }; diff --git a/packages/oidc-client/src/protocol.spec.ts b/packages/oidc-client/src/protocol.spec.ts new file mode 100644 index 000000000..cd2c0ed16 --- /dev/null +++ b/packages/oidc-client/src/protocol.spec.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest'; + +import { + buildDpopSecuredPlaceholder, + buildSecuredTokenPlaceholder, + buildStorageKey, + DPOP_TOKEN_PLACEHOLDER_PREFIX, + isServiceWorkerMessageType, + PROTOCOL_VERSION, + ServiceWorkerMessageType, + STORAGE_KEY_PREFIX, + SW_CONTROLLER_CHANGE_RELOAD_COUNT_KEY, + TOKEN_PLACEHOLDERS, +} from './protocol'; + +describe('public oidc-client protocol surface', () => { + it('exposes a stable PROTOCOL_VERSION', () => { + expect(PROTOCOL_VERSION).toMatch(/^\d+\.\d+\.\d+$/); + }); + + it('matches the wire-level message types documented in PROTOCOL.md', () => { + expect(ServiceWorkerMessageType).toEqual({ + SKIP_WAITING: 'SKIP_WAITING', + CLAIM: 'claim', + CLEAR: 'clear', + INIT: 'init', + SET_STATE: 'setState', + GET_STATE: 'getState', + SET_CODE_VERIFIER: 'setCodeVerifier', + GET_CODE_VERIFIER: 'getCodeVerifier', + SET_SESSION_STATE: 'setSessionState', + GET_SESSION_STATE: 'getSessionState', + SET_NONCE: 'setNonce', + GET_NONCE: 'getNonce', + SET_DPOP_NONCE: 'setDemonstratingProofOfPossessionNonce', + GET_DPOP_NONCE: 'getDemonstratingProofOfPossessionNonce', + SET_DPOP_JWK: 'setDemonstratingProofOfPossessionJwk', + GET_DPOP_JWK: 'getDemonstratingProofOfPossessionJwk', + }); + }); + + it('exposes the token placeholders the service worker emits', () => { + expect(TOKEN_PLACEHOLDERS).toEqual({ + ACCESS_TOKEN: 'ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER', + REFRESH_TOKEN: 'REFRESH_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER', + NONCE_TOKEN: 'NONCE_SECURED_BY_OIDC_SERVICE_WORKER', + CODE_VERIFIER: 'CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER', + }); + expect(DPOP_TOKEN_PLACEHOLDER_PREFIX).toBe('DPOP_SECURED_BY_OIDC_SERVICE_WORKER'); + }); +}); + +describe('protocol helpers', () => { + it('builds secured token placeholders that match the SW output', () => { + expect(buildSecuredTokenPlaceholder(TOKEN_PLACEHOLDERS.ACCESS_TOKEN, 'demo', 'tab-1')).toBe( + 'ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER_demo#tabId=tab-1', + ); + expect(buildSecuredTokenPlaceholder(TOKEN_PLACEHOLDERS.NONCE_TOKEN, 'demo')).toBe( + 'NONCE_SECURED_BY_OIDC_SERVICE_WORKER_demo#tabId=default', + ); + }); + + it('builds DPoP placeholders that match the SW output', () => { + expect(buildDpopSecuredPlaceholder('demo', 'tab-2')).toBe( + `${DPOP_TOKEN_PLACEHOLDER_PREFIX}_demo#tabId=tab-2`, + ); + }); + + it.each([ + [STORAGE_KEY_PREFIX.STATE, 'demo', 'oidc.state.demo'], + [STORAGE_KEY_PREFIX.NONCE, 'demo', 'oidc.nonce.demo'], + [STORAGE_KEY_PREFIX.CODE_VERIFIER, 'demo', 'oidc.code_verifier.demo'], + [STORAGE_KEY_PREFIX.LOGIN_PARAMS, 'demo', 'oidc.login.demo'], + [STORAGE_KEY_PREFIX.TAB_ID, 'demo', 'oidc.tabId.demo'], + ])('combines storage prefix %s + %s into %s', (prefix, configurationName, expected) => { + expect(buildStorageKey(prefix, configurationName)).toBe(expected); + }); + + it('exposes the SW controllerchange reload counter key', () => { + expect(SW_CONTROLLER_CHANGE_RELOAD_COUNT_KEY).toBe('oidc.sw.controllerchange_reload_count'); + }); + + it.each(Object.values(ServiceWorkerMessageType))( + 'recognises "%s" as a known message type', + type => { + expect(isServiceWorkerMessageType(type)).toBe(true); + }, + ); + + it.each(['unknown', '', 42, null, undefined, {}])( + 'rejects %p as not a known message type', + value => { + expect(isServiceWorkerMessageType(value)).toBe(false); + }, + ); +}); diff --git a/packages/oidc-client/src/protocol.ts b/packages/oidc-client/src/protocol.ts new file mode 100644 index 000000000..28e7807d5 --- /dev/null +++ b/packages/oidc-client/src/protocol.ts @@ -0,0 +1,106 @@ +/** + * Public, supported entry point for the OIDC service worker `postMessage` + * protocol, available directly from `@axa-fr/oidc-client`. + * + * The same exports are also published from + * `@axa-fr/oidc-client-service-worker/protocol`. The two modules are kept + * deliberately in sync (and verified by a unit test) so applications that + * depend only on `@axa-fr/oidc-client` can interact with the service worker + * without adding a transitive dependency. + * + * See `packages/oidc-client-service-worker/PROTOCOL.md` for the full + * specification. + */ + +/** + * Semver-protected version of the service worker `postMessage` protocol. + */ +export const PROTOCOL_VERSION = '1.0.0' as const; + +/** + * Every supported service worker message type. The values are also the + * literal strings used on the wire as `MessageEventData.type` and must + * remain stable across patch and minor versions of the protocol. + */ +export const ServiceWorkerMessageType = { + SKIP_WAITING: 'SKIP_WAITING', + CLAIM: 'claim', + CLEAR: 'clear', + INIT: 'init', + SET_STATE: 'setState', + GET_STATE: 'getState', + SET_CODE_VERIFIER: 'setCodeVerifier', + GET_CODE_VERIFIER: 'getCodeVerifier', + SET_SESSION_STATE: 'setSessionState', + GET_SESSION_STATE: 'getSessionState', + SET_NONCE: 'setNonce', + GET_NONCE: 'getNonce', + SET_DPOP_NONCE: 'setDemonstratingProofOfPossessionNonce', + GET_DPOP_NONCE: 'getDemonstratingProofOfPossessionNonce', + SET_DPOP_JWK: 'setDemonstratingProofOfPossessionJwk', + GET_DPOP_JWK: 'getDemonstratingProofOfPossessionJwk', +} as const; + +export type ServiceWorkerMessageTypeKey = keyof typeof ServiceWorkerMessageType; +export type ServiceWorkerMessageTypeValue = + (typeof ServiceWorkerMessageType)[ServiceWorkerMessageTypeKey]; + +export interface ServiceWorkerMessage { + type: ServiceWorkerMessageTypeValue | 'SKIP_WAITING' | 'claim'; + configurationName: string; + data: TData; + tabId?: string; +} + +export interface ServiceWorkerResponse { + configurationName?: string; + error?: unknown; + [key: string]: unknown; +} + +/** Stable internal token placeholders – matched by the SW request handler. */ +export const TOKEN_PLACEHOLDERS = { + ACCESS_TOKEN: 'ACCESS_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER', + REFRESH_TOKEN: 'REFRESH_TOKEN_SECURED_BY_OIDC_SERVICE_WORKER', + NONCE_TOKEN: 'NONCE_SECURED_BY_OIDC_SERVICE_WORKER', + CODE_VERIFIER: 'CODE_VERIFIER_SECURED_BY_OIDC_SERVICE_WORKER', +} as const; + +export const DPOP_TOKEN_PLACEHOLDER_PREFIX = 'DPOP_SECURED_BY_OIDC_SERVICE_WORKER' as const; + +export const STORAGE_KEY_PREFIX = { + TAB_ID: 'oidc.tabId.', + STATE: 'oidc.state.', + NONCE: 'oidc.nonce.', + CODE_VERIFIER: 'oidc.code_verifier.', + LOGIN_PARAMS: 'oidc.login.', + SW_VERSION_MISMATCH_RELOAD: 'oidc.sw.version_mismatch_reload.', +} as const; + +export const SW_CONTROLLER_CHANGE_RELOAD_COUNT_KEY = + 'oidc.sw.controllerchange_reload_count' as const; + +export const buildStorageKey = ( + prefix: (typeof STORAGE_KEY_PREFIX)[keyof typeof STORAGE_KEY_PREFIX], + configurationName: string, +): string => `${prefix}${configurationName}`; + +export const buildSecuredTokenPlaceholder = ( + placeholder: (typeof TOKEN_PLACEHOLDERS)[keyof typeof TOKEN_PLACEHOLDERS], + configurationName: string, + tabId: string = 'default', +): string => `${placeholder}_${configurationName}#tabId=${tabId}`; + +export const buildDpopSecuredPlaceholder = ( + configurationName: string, + tabId: string = 'default', +): string => `${DPOP_TOKEN_PLACEHOLDER_PREFIX}_${configurationName}#tabId=${tabId}`; + +export const isServiceWorkerMessageType = ( + value: unknown, +): value is ServiceWorkerMessageTypeValue => { + if (typeof value !== 'string') { + return false; + } + return Object.values(ServiceWorkerMessageType).includes(value as ServiceWorkerMessageTypeValue); +}; diff --git a/packages/oidc-client/src/pushedAuthorizationRequestError.spec.ts b/packages/oidc-client/src/pushedAuthorizationRequestError.spec.ts new file mode 100644 index 000000000..78b197d94 --- /dev/null +++ b/packages/oidc-client/src/pushedAuthorizationRequestError.spec.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; + +import { OidcError } from './oidcError'; +import { + isPushedAuthorizationRequestError, + PushedAuthorizationRequestError, + PushedAuthorizationRequestErrorCode, +} from './pushedAuthorizationRequestError'; + +describe('PushedAuthorizationRequestError', () => { + it('exposes stable error codes', () => { + expect(PushedAuthorizationRequestErrorCode.ENDPOINT_UNAVAILABLE).toBe('ENDPOINT_UNAVAILABLE'); + expect(PushedAuthorizationRequestErrorCode.REQUEST_FAILED).toBe('REQUEST_FAILED'); + expect(PushedAuthorizationRequestErrorCode.INVALID_RESPONSE).toBe('INVALID_RESPONSE'); + }); + + it('preserves PAR and OAuth error details', () => { + const cause = new Error('network failure'); + const error = new PushedAuthorizationRequestError( + PushedAuthorizationRequestErrorCode.REQUEST_FAILED, + 'PAR failed', + { + status: 400, + oauthError: 'invalid_request', + oauthErrorDescription: 'Invalid redirect URI', + cause, + }, + ); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(OidcError); + expect(error.name).toBe('PushedAuthorizationRequestError'); + expect(error.code).toBe('REQUEST_FAILED'); + expect(error.status).toBe(400); + expect(error.oauthError).toBe('invalid_request'); + expect(error.oauthErrorDescription).toBe('Invalid redirect URI'); + expect(error.cause).toBe(cause); + expect(error.phase).toBe('login'); + expect(error.retryable).toBe(true); + expect(isPushedAuthorizationRequestError(error)).toBe(true); + expect(isPushedAuthorizationRequestError(new Error('other'))).toBe(false); + }); +}); diff --git a/packages/oidc-client/src/pushedAuthorizationRequestError.ts b/packages/oidc-client/src/pushedAuthorizationRequestError.ts new file mode 100644 index 000000000..ba8b09955 --- /dev/null +++ b/packages/oidc-client/src/pushedAuthorizationRequestError.ts @@ -0,0 +1,50 @@ +import { + isRetryableHttpStatus, + OidcError, + OidcErrorCode as BaseOidcErrorCode, +} from './oidcError.js'; + +export const PushedAuthorizationRequestErrorCode = { + ENDPOINT_UNAVAILABLE: 'ENDPOINT_UNAVAILABLE', + REQUEST_FAILED: 'REQUEST_FAILED', + INVALID_RESPONSE: 'INVALID_RESPONSE', +} as const; + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type PushedAuthorizationRequestErrorCode = + (typeof PushedAuthorizationRequestErrorCode)[keyof typeof PushedAuthorizationRequestErrorCode]; + +type PushedAuthorizationRequestErrorOptions = { + status?: number; + oauthError?: string; + oauthErrorDescription?: string; + cause?: unknown; +}; + +export class PushedAuthorizationRequestError extends OidcError { + declare readonly code: PushedAuthorizationRequestErrorCode; + + constructor( + code: PushedAuthorizationRequestErrorCode, + message: string, + options: PushedAuthorizationRequestErrorOptions = {}, + ) { + super(code as BaseOidcErrorCode, message, { + phase: 'login', + retryable: options.cause !== undefined || isRetryableHttpStatus(options.status), + status: options.status, + oauthError: options.oauthError, + oauthErrorDescription: options.oauthErrorDescription, + cause: options.cause, + }); + this.name = 'PushedAuthorizationRequestError'; + + Object.setPrototypeOf(this, PushedAuthorizationRequestError.prototype); + } +} + +export const isPushedAuthorizationRequestError = ( + value: unknown, +): value is PushedAuthorizationRequestError => { + return value instanceof PushedAuthorizationRequestError; +}; diff --git a/packages/oidc-client/src/renewTokens.ts b/packages/oidc-client/src/renewTokens.ts index a0e23d0c4..1b887095e 100644 --- a/packages/oidc-client/src/renewTokens.ts +++ b/packages/oidc-client/src/renewTokens.ts @@ -1,37 +1,687 @@ +import { eventNames } from './events'; import { initSession } from './initSession.js'; -import { initWorkerAsync } from './initWorker.js'; +import { initWorkerAsync, sleepAsync } from './initWorker.js'; import Oidc from './oidc.js'; -import { computeTimeLeft } from './parseTokens.js'; +import { + createNetworkError, + isNetworkErrorCause, + isOidcError, + isRetryableHttpStatus, + OidcError, + OidcErrorCode, +} from './oidcError.js'; +import { OidcStateError, OidcStateErrorCode } from './oidcStateError.js'; +import { computeTimeLeft, isTokensOidcValid, setTokens, Tokens } from './parseTokens.js'; +import { performTokenRequestAsync } from './requests'; +import { _silentLoginAsync } from './silentLogin'; import timer from './timer.js'; -import { StringMap } from './types.js'; +import { OidcConfiguration, StringMap, TokenAutomaticRenewMode } from './types.js'; -export async function renewTokensAndStartTimerAsync(oidc, refreshToken, forceRefresh = false, extras:StringMap = null) { - const updateTokens = (tokens) => { oidc.tokens = tokens; }; - const { tokens, status } = await oidc.synchroniseTokensAsync(refreshToken, 0, forceRefresh, extras, updateTokens); +export type RenewTokensResult = { + tokens: Tokens | null; + status: string | null; + error?: OidcError; +}; - const serviceWorker = await initWorkerAsync(oidc.configuration.service_worker_relative_url, oidc.configurationName); - if (!serviceWorker) { - const session = initSession(oidc.configurationName, oidc.configuration.storage); - await session.setTokens(oidc.tokens); - } +const createRefreshTokenError = ( + message: string, + response?: { + status?: number; + oauthError?: string; + oauthErrorDescription?: string; + }, +): OidcError => { + const code = + response?.oauthError === 'use_dpop_nonce' + ? OidcErrorCode.DPOP_NONCE_REQUIRED + : OidcErrorCode.TOKEN_REQUEST_FAILED; + return new OidcError(code, message, { + phase: 'refresh', + retryable: + code === OidcErrorCode.DPOP_NONCE_REQUIRED || isRetryableHttpStatus(response?.status), + status: response?.status, + oauthError: response?.oauthError, + oauthErrorDescription: response?.oauthErrorDescription, + }); +}; - if (!oidc.tokens) { - await oidc.destroyAsync(status); - return; - } +async function syncTokens( + oidc: Oidc, + forceRefresh: boolean, + extras: StringMap, + scope: string = null, +) { + const updateTokens = tokens => { + oidc.tokens = tokens; + }; + const result = await synchroniseTokensAsync(oidc)( + updateTokens, + 0, + 0, + forceRefresh, + extras, + scope, + ); + const { tokens, status } = result; + + const serviceWorker = await initWorkerAsync(oidc.configuration, oidc.configurationName); + if (!serviceWorker) { + const session = initSession( + oidc.configurationName, + oidc.configuration.storage, + oidc.configuration.login_state_storage ?? oidc.configuration.storage, + ); + session.setTokens(oidc.tokens); + } + + if (!oidc.tokens) { + await oidc.destroyAsync(status); + return { ...result, tokens: null } as RenewTokensResult; + } + return { ...result, tokens } as RenewTokensResult; +} + +export async function renewTokensAndStartTimerResultAsync( + oidc, + forceRefresh = false, + extras: StringMap = null, + scope: string = null, +) { + const configuration = oidc.configuration; + const lockResourcesName = `${configuration.client_id}_${oidc.configurationName}_${configuration.authority}`; - if (oidc.timeoutId) { - oidc.timeoutId = autoRenewTokens(oidc, tokens.refreshToken, oidc.tokens.expiresAt, extras); + let result: RenewTokensResult; + const serviceWorker = await initWorkerAsync(oidc.configuration, oidc.configurationName); + if ((configuration?.storage === window?.sessionStorage && !serviceWorker) || !navigator.locks) { + result = await syncTokens(oidc, forceRefresh, extras, scope); + } else { + let lockResult: any = 'retry'; + while (lockResult === 'retry') { + lockResult = await navigator.locks.request( + lockResourcesName, + { ifAvailable: true }, + async lock => { + if (!lock) { + oidc.publishEvent(Oidc.eventNames.syncTokensAsync_lock_not_available, { + lock: 'lock not available', + }); + return 'retry'; + } + return await syncTokens(oidc, forceRefresh, extras, scope); + }, + ); } - return oidc.tokens; + result = lockResult; + } + + if (!result.tokens) { + return result; + } + + if (oidc.timeoutId) { + // @ts-ignore + oidc.timeoutId = autoRenewTokens(oidc, oidc.tokens.expiresAt, extras, scope); + } + + return { ...result, tokens: oidc.tokens }; } -export const autoRenewTokens = (oidc, refreshToken, expiresAt, extras:StringMap = null) => { - const refreshTimeBeforeTokensExpirationInSecond = oidc.configuration.refresh_time_before_tokens_expiration_in_second; - return timer.setTimeout(async () => { - const timeLeft = computeTimeLeft(refreshTimeBeforeTokensExpirationInSecond, expiresAt); - const timeInfo = { timeLeft }; - oidc.publishEvent(Oidc.eventNames.token_timer, timeInfo); - await renewTokensAndStartTimerAsync(oidc, refreshToken, false, extras); - }, 1000); +export async function renewTokensAndStartTimerAsync( + oidc, + forceRefresh = false, + extras: StringMap = null, + scope: string = null, +) { + const result = await renewTokensAndStartTimerResultAsync(oidc, forceRefresh, extras, scope); + return result.tokens; +} + +export const autoRenewTokens = ( + oidc: Oidc, + expiresAt, + extras: StringMap = null, + scope: string = null, +) => { + const refreshTimeBeforeTokensExpirationInSecond = + oidc.configuration.refresh_time_before_tokens_expiration_in_second; + if (oidc.timeoutId) { + timer.clearTimeout(oidc.timeoutId); + } + return timer.setTimeout(async () => { + const timeLeft = computeTimeLeft(refreshTimeBeforeTokensExpirationInSecond, expiresAt); + const timeInfo = { timeLeft }; + oidc.publishEvent(Oidc.eventNames.token_timer, timeInfo); + await renewTokensAndStartTimerAsync(oidc, false, extras, scope); + }, 1000); +}; + +export const synchroniseTokensStatus = { + FORCE_REFRESH: 'FORCE_REFRESH', + SESSION_LOST: 'SESSION_LOST', + NOT_CONNECTED: 'NOT_CONNECTED', + TOKENS_VALID: 'TOKENS_VALID', + TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID: 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID', + TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_INVALID: 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_INVALID', + LOGOUT_FROM_ANOTHER_TAB: 'LOGOUT_FROM_ANOTHER_TAB', + REQUIRE_SYNC_TOKENS: 'REQUIRE_SYNC_TOKENS', + TOKENS_INVALID: 'TOKENS_INVALID', }; + +export const syncTokensInfoAsync = + (oidc: Oidc) => + async ( + configuration: OidcConfiguration, + configurationName: string, + currentTokens: Tokens, + forceRefresh = false, + ) => { + // Service Worker can be killed by the browser (when it wants,for example after 10 seconds of inactivity, so we retreieve the session if it happen) + // const configuration = this.configuration; + const nullNonce = { nonce: null }; + if (!currentTokens) { + return { tokens: null, status: synchroniseTokensStatus.NOT_CONNECTED, nonce: nullNonce }; + } + let nonce; + const oidcServerConfiguration = await oidc.initAsync( + configuration.authority, + configuration.authority_configuration, + ); + const serviceWorker = await initWorkerAsync(configuration, configurationName); + if (serviceWorker) { + const { status, tokens } = await serviceWorker.initAsync( + oidcServerConfiguration, + 'syncTokensAsync', + configuration, + ); + if (status === 'LOGGED_OUT') { + return { + tokens: null, + status: synchroniseTokensStatus.LOGOUT_FROM_ANOTHER_TAB, + nonce: nullNonce, + }; + } else if (status === 'SESSIONS_LOST') { + return { tokens: null, status: synchroniseTokensStatus.SESSION_LOST, nonce: nullNonce }; + } else if (!status || !tokens) { + return { + tokens: null, + status: synchroniseTokensStatus.REQUIRE_SYNC_TOKENS, + nonce: nullNonce, + }; + } else if (tokens.issuedAt !== currentTokens.issuedAt) { + const timeLeft = computeTimeLeft( + configuration.refresh_time_before_tokens_expiration_in_second, + tokens.expiresAt, + ); + const status = + timeLeft > 0 + ? synchroniseTokensStatus.TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID + : synchroniseTokensStatus.TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_INVALID; + const nonce = await serviceWorker.getNonceAsync(); + return { tokens, status, nonce }; + } + nonce = await serviceWorker.getNonceAsync(); + } else { + const session = initSession( + configurationName, + configuration.storage ?? sessionStorage, + configuration.login_state_storage ?? configuration.storage ?? sessionStorage, + ); + const initAsyncResponse = await session.initAsync(); + let { tokens } = initAsyncResponse; + const { status } = initAsyncResponse; + if (tokens) { + tokens = setTokens(tokens, oidc.tokens, configuration.token_renew_mode); + } + if (!tokens) { + return { + tokens: null, + status: synchroniseTokensStatus.LOGOUT_FROM_ANOTHER_TAB, + nonce: nullNonce, + }; + } else if (status === 'SESSIONS_LOST') { + return { tokens: null, status: synchroniseTokensStatus.SESSION_LOST, nonce: nullNonce }; + } else if (tokens.issuedAt !== currentTokens.issuedAt) { + const timeLeft = computeTimeLeft( + configuration.refresh_time_before_tokens_expiration_in_second, + tokens.expiresAt, + ); + const status = + timeLeft > 0 + ? synchroniseTokensStatus.TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID + : synchroniseTokensStatus.TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_INVALID; + const nonce = await session.getNonceAsync(); + return { tokens, status, nonce }; + } + nonce = await session.getNonceAsync(); + } + + const timeLeft = computeTimeLeft( + configuration.refresh_time_before_tokens_expiration_in_second, + currentTokens.expiresAt, + ); + const status = timeLeft > 0 ? 'TOKENS_VALID' : 'TOKENS_INVALID'; + if (forceRefresh) { + return { tokens: currentTokens, status: 'FORCE_REFRESH', nonce }; + } + return { tokens: currentTokens, status, nonce }; + }; + +const synchroniseTokensAsync = + (oidc: Oidc) => + async ( + updateTokens, + tryNumber = 0, + backgroundTry = 0, + forceRefresh = false, + extras: StringMap = null, + scope: string = null, + lastError: OidcError = null, + ) => { + if (!navigator.onLine && document.hidden) { + return { tokens: oidc.tokens, status: 'GIVE_UP' }; + } + let numberTryOnline = 6; + const maxTries = forceRefresh ? 2 : 5; + const maxBackgroundTries = 5; + + while (!navigator.onLine && numberTryOnline > 0) { + await sleepAsync({ milliseconds: 1000 }); + numberTryOnline--; + oidc.publishEvent(eventNames.refreshTokensAsync, { + message: `wait because navigator is offline try ${numberTryOnline}`, + }); + } + const isDocumentHidden = document.hidden; + const nextTry = isDocumentHidden ? tryNumber : tryNumber + 1; + const nextBackgroundTry = isDocumentHidden ? backgroundTry + 1 : backgroundTry; + + if (tryNumber >= maxTries || backgroundTry >= maxBackgroundTries) { + updateTokens(null); + const error = lastError ?? createRefreshTokenError('refresh token'); + oidc.publishEvent(eventNames.refreshTokensAsync_error, { + message: 'refresh token', + error, + }); + return { tokens: null, status: 'SESSION_LOST', error }; + } + + if (!extras) { + extras = {}; + } + const configuration = oidc.configuration; + + const silentLoginAsync = (extras: StringMap, state: string = null, scope: string = null) => { + return _silentLoginAsync( + oidc.configurationName, + oidc.configuration, + oidc.publishEvent.bind(oidc), + 'refresh', + )(extras, state, scope); + }; + const localSilentLoginAsync = async () => { + try { + let loginParams; + const serviceWorker = await initWorkerAsync(configuration, oidc.configurationName); + if (serviceWorker) { + loginParams = serviceWorker.getLoginParams(); + } else { + const session = initSession( + oidc.configurationName, + configuration.storage, + configuration.login_state_storage ?? configuration.storage, + ); + loginParams = session.getLoginParams(); + } + const silentLoginInput = {}; + + if (loginParams && loginParams.extras) { + for (const [key, value] of Object.entries(loginParams.extras)) { + if (value != null) { + silentLoginInput[key] = value; + } + } + } + if (extras) { + for (const [key, value] of Object.entries(extras)) { + if (value != null) { + silentLoginInput[key] = value; + } + } + } + silentLoginInput['prompt'] = 'none'; + if (scope) { + silentLoginInput['scope'] = scope; + } + + const silent_token_response = await silentLoginAsync(silentLoginInput); + if (!silent_token_response) { + updateTokens(null); + const error = createRefreshTokenError('refresh token silent not active'); + oidc.publishEvent(eventNames.refreshTokensAsync_error, { + message: 'refresh token silent not active', + error, + }); + return { tokens: null, status: 'SESSION_LOST', error }; + } + if (silent_token_response.error) { + updateTokens(null); + const error = + silent_token_response.oidcError ?? + new OidcError(OidcErrorCode.OAUTH_ERROR, 'refresh token silent', { + phase: 'refresh', + retryable: false, + oauthError: silent_token_response.error, + }); + oidc.publishEvent(eventNames.refreshTokensAsync_error, { + message: 'refresh token silent', + error, + }); + return { tokens: null, status: 'SESSION_LOST', error }; + } + + updateTokens(silent_token_response.tokens); + oidc.publishEvent(Oidc.eventNames.token_renewed, {}); + return { tokens: silent_token_response.tokens, status: 'LOGGED' }; + } catch (exceptionSilent: any) { + const error = + isOidcError(exceptionSilent) || !isNetworkErrorCause(exceptionSilent) + ? exceptionSilent + : createNetworkError(exceptionSilent, 'refresh'); + console.error(error); + oidc.publishEvent(eventNames.refreshTokensAsync_silent_error, { + message: 'exceptionSilent', + exception: error.message, + error: isOidcError(error) ? error : undefined, + }); + return await synchroniseTokensAsync(oidc)( + updateTokens, + nextTry, + nextBackgroundTry, + forceRefresh, + extras, + scope, + isOidcError(error) ? error : lastError, + ); + } + }; + + try { + const { status, tokens, nonce } = await syncTokensInfoAsync(oidc)( + configuration, + oidc.configurationName, + oidc.tokens, + forceRefresh, + ); + + switch (status) { + case synchroniseTokensStatus.SESSION_LOST: + updateTokens(null); + { + const error = lastError ?? createRefreshTokenError('refresh token session lost'); + oidc.publishEvent(eventNames.refreshTokensAsync_error, { + message: 'refresh token session lost', + error, + }); + return { tokens: null, status: 'SESSION_LOST', error }; + } + case synchroniseTokensStatus.NOT_CONNECTED: + updateTokens(null); + return { tokens: null, status: null }; + case synchroniseTokensStatus.TOKENS_VALID: + updateTokens(tokens); + return { tokens, status: 'LOGGED_IN' }; + case synchroniseTokensStatus.TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID: + updateTokens(tokens); + oidc.publishEvent(Oidc.eventNames.token_renewed, { + reason: 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID', + }); + return { tokens, status: 'LOGGED_IN' }; + case synchroniseTokensStatus.LOGOUT_FROM_ANOTHER_TAB: + updateTokens(null); + oidc.publishEvent(eventNames.logout_from_another_tab, { + status: 'session syncTokensAsync', + }); + return { tokens: null, status: 'LOGGED_OUT' }; + case synchroniseTokensStatus.REQUIRE_SYNC_TOKENS: + if ( + configuration.token_automatic_renew_mode == + TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted && + !forceRefresh + ) { + oidc.publishEvent(eventNames.tokensInvalidAndWaitingActionsToRefresh, {}); + return { tokens: oidc.tokens, status: 'GIVE_UP' }; + } + + oidc.publishEvent(eventNames.refreshTokensAsync_begin, { tryNumber: tryNumber }); + return await localSilentLoginAsync(); + default: { + if ( + configuration.token_automatic_renew_mode == + TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted && + synchroniseTokensStatus.FORCE_REFRESH !== status + ) { + oidc.publishEvent(eventNames.tokensInvalidAndWaitingActionsToRefresh, {}); + return { tokens: oidc.tokens, status: 'GIVE_UP' }; + } + + oidc.publishEvent(eventNames.refreshTokensAsync_begin, { + refreshToken: tokens.refreshToken, + status, + tryNumber: tryNumber, + backgroundTry: backgroundTry, + }); + + if (!tokens.refreshToken) { + return await localSilentLoginAsync(); + } + + const clientId = configuration.client_id; + const redirectUri = configuration.redirect_uri; + const authority = configuration.authority; + const tokenExtras = configuration.token_request_extras + ? configuration.token_request_extras + : {}; + const finalExtras = { ...tokenExtras }; + + for (const [key, value] of Object.entries(extras)) { + if (key.endsWith(':token_request')) { + finalExtras[key.replace(':token_request', '')] = value; + } + } + const localFunctionAsync = async () => { + const details = { + client_id: clientId, + redirect_uri: redirectUri, + grant_type: 'refresh_token', + refresh_token: tokens.refreshToken, + }; + const oidcServerConfiguration = await oidc.initAsync( + authority, + configuration.authority_configuration, + ); + const timeoutMs = document.hidden ? 10000 : 30000 * 10; + const url = oidcServerConfiguration.tokenEndpoint; + const headersExtras = {}; + if (configuration.demonstrating_proof_of_possession) { + headersExtras['DPoP'] = await oidc.generateDemonstrationOfProofOfPossessionAsync( + tokens.accessToken, + url, + 'POST', + ); + } + let tokenResponse = await performTokenRequestAsync(oidc.getFetch())( + url, + details, + finalExtras, + tokens, + headersExtras, + configuration.token_renew_mode, + timeoutMs, + ); + + if ( + !tokenResponse.success && + tokenResponse.oauthError === 'use_dpop_nonce' && + tokenResponse.demonstratingProofOfPossessionNonce && + configuration.demonstrating_proof_of_possession + ) { + const serviceWorker = await initWorkerAsync(configuration, oidc.configurationName); + if (serviceWorker) { + await serviceWorker.setDemonstratingProofOfPossessionNonce( + tokenResponse.demonstratingProofOfPossessionNonce, + ); + } else { + const session = initSession( + oidc.configurationName, + configuration.storage, + configuration.login_state_storage ?? configuration.storage, + ); + await session.setDemonstratingProofOfPossessionNonce( + tokenResponse.demonstratingProofOfPossessionNonce, + ); + } + headersExtras['DPoP'] = await oidc.generateDemonstrationOfProofOfPossessionAsync( + tokens.accessToken, + url, + 'POST', + ); + tokenResponse = await performTokenRequestAsync(oidc.getFetch())( + url, + details, + finalExtras, + tokens, + headersExtras, + configuration.token_renew_mode, + timeoutMs, + ); + } + + if (tokenResponse.success) { + // Guard against a missing/corrupted nonce reaching id_token validation. + // Without this guard, accessing `nonce.nonce` would throw a TypeError + // when the underlying storage has been cleared (private mode, manual + // clearing, browser eviction). We prefer a defined SESSION_LOST result + // so silent renew stays non-throwing for consumers. + // See https://github.com/AxaFrance/oidc-client/issues/1678 + if (!nonce || !nonce.nonce) { + updateTokens(null); + const error = new OidcStateError( + OidcStateErrorCode.NONCE_MISSING, + 'refresh token: nonce missing from storage', + 'refresh', + ); + oidc.publishEvent(eventNames.refreshTokensAsync_error, { + message: 'refresh token: nonce missing from storage', + error, + }); + return { tokens: null, status: 'SESSION_LOST', error }; + } + const { isValid, reason } = isTokensOidcValid( + tokenResponse.data, + nonce.nonce, + oidcServerConfiguration, + ); + if (!isValid) { + updateTokens(null); + const error = new OidcError( + reason.startsWith('Nonce does not match') + ? OidcErrorCode.INVALID_NONCE + : OidcErrorCode.TOKEN_REQUEST_FAILED, + `refresh token return not valid tokens, reason: ${reason}`, + { + phase: 'refresh', + retryable: false, + }, + ); + oidc.publishEvent(eventNames.refreshTokensAsync_error, { + message: `refresh token return not valid tokens, reason: ${reason}`, + error, + }); + return { tokens: null, status: 'SESSION_LOST', error }; + } + updateTokens(tokenResponse.data); + if (tokenResponse.demonstratingProofOfPossessionNonce) { + const serviceWorker = await initWorkerAsync(configuration, oidc.configurationName); + if (serviceWorker) { + await serviceWorker.setDemonstratingProofOfPossessionNonce( + tokenResponse.demonstratingProofOfPossessionNonce, + ); + } else { + const session = initSession( + oidc.configurationName, + configuration.storage, + configuration.login_state_storage ?? configuration.storage, + ); + await session.setDemonstratingProofOfPossessionNonce( + tokenResponse.demonstratingProofOfPossessionNonce, + ); + } + } + oidc.publishEvent(eventNames.refreshTokensAsync_end, { + success: tokenResponse.success, + }); + oidc.publishEvent(Oidc.eventNames.token_renewed, { reason: 'REFRESH_TOKEN' }); + return { tokens: tokenResponse.data, status: 'LOGGED_IN' }; + } else { + const error = createRefreshTokenError('Token request failed', tokenResponse); + oidc.publishEvent(eventNames.refreshTokensAsync_silent_error, { + message: 'bad request', + tokenResponse, + error, + }); + + if (tokenResponse.status >= 400 && tokenResponse.status < 500) { + updateTokens(null); + oidc.publishEvent(eventNames.refreshTokensAsync_error, { + message: `session lost: ${tokenResponse.status}`, + error, + }); + return { tokens: null, status: 'SESSION_LOST', error }; + } + + return await synchroniseTokensAsync(oidc)( + updateTokens, + nextTry, + nextBackgroundTry, + forceRefresh, + extras, + scope, + error, + ); + } + }; + return await localFunctionAsync(); + } + } + } catch (exception: any) { + const error = + isOidcError(exception) || !isNetworkErrorCause(exception) + ? exception + : createNetworkError(exception, 'refresh'); + console.error(error); + + oidc.publishEvent(eventNames.refreshTokensAsync_silent_error, { + message: 'exception', + exception: error.message, + error: isOidcError(error) ? error : undefined, + }); + // we need to break the loop or errors, as direct call of synchroniseTokensAsync + // inside of synchroniseTokensAsync will cause an infinite loop and kill the browser stack + // so we need to brake calls chain and delay next call + return new Promise((resolve, reject) => { + setTimeout(() => { + synchroniseTokensAsync(oidc)( + updateTokens, + nextTry, + nextBackgroundTry, + forceRefresh, + extras, + scope, + isOidcError(error) ? error : lastError, + ) + .then(resolve) + .catch(reject); + }, 1000); + }); + } + }; diff --git a/packages/oidc-client/src/renewTokensError.spec.ts b/packages/oidc-client/src/renewTokensError.spec.ts new file mode 100644 index 000000000..7bbd3bacb --- /dev/null +++ b/packages/oidc-client/src/renewTokensError.spec.ts @@ -0,0 +1,116 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import * as initSessionModule from './initSession'; +import * as initWorkerModule from './initWorker'; +import Oidc from './oidc'; +import { OidcError, OidcErrorCode } from './oidcError'; +import { OidcStateError, OidcStateErrorCode } from './oidcStateError'; +import { Tokens } from './parseTokens'; +import { renewTokensAndStartTimerResultAsync } from './renewTokens'; +import * as requestsModule from './requests'; + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); +}); + +describe('strict token renewal', () => { + it('keeps renewTokensAsync non-throwing while the strict method throws the shared error', async () => { + const error = new OidcError(OidcErrorCode.TOKEN_REQUEST_FAILED, 'Token request failed', { + phase: 'refresh', + retryable: false, + status: 400, + oauthError: 'invalid_grant', + }); + const oidc = Object.create(Oidc.prototype) as Oidc; + const sharedResult = Promise.resolve({ + tokens: null, + status: 'SESSION_LOST', + error, + }); + oidc.renewTokensPromise = sharedResult; + + const nonThrowing = oidc.renewTokensAsync(); + const strict = oidc.renewTokensOrThrowAsync(); + + await expect(nonThrowing).resolves.toBeNull(); + await expect(strict).rejects.toBe(error); + }); + + it('returns renewed tokens from the strict method', async () => { + const tokens = { + accessToken: 'access-token', + expiresAt: Date.now() / 1000 + 3600, + } as Tokens; + const oidc = Object.create(Oidc.prototype) as Oidc; + oidc.renewTokensPromise = Promise.resolve({ + tokens, + status: 'LOGGED_IN', + }); + + await expect(oidc.renewTokensOrThrowAsync()).resolves.toBe(tokens); + }); + + it('returns SESSION_LOST with OidcStateError(NONCE_MISSING) when refresh succeeds but nonce storage is missing', async () => { + vi.stubGlobal('navigator', { locks: undefined, onLine: true }); + vi.stubGlobal('document', { hidden: false }); + vi.stubGlobal('window', { sessionStorage: {} }); + vi.spyOn(initWorkerModule, 'initWorkerAsync').mockResolvedValue(null); + vi.spyOn(initSessionModule, 'initSession').mockReturnValue({ + getNonceAsync: vi.fn().mockResolvedValue({ nonce: undefined }), + initAsync: vi.fn().mockResolvedValue({ + status: null, + tokens: { + accessToken: 'access-token', + expiresAt: 0, + issuedAt: 1, + refreshToken: 'refresh-token', + }, + }), + setTokens: vi.fn(), + } as never); + vi.spyOn(requestsModule, 'performTokenRequestAsync').mockReturnValue( + vi.fn().mockResolvedValue({ + data: { accessToken: 'renewed-token' }, + status: 200, + success: true, + }) as never, + ); + + const oidc = Object.create(Oidc.prototype) as Oidc; + oidc.configurationName = 'default'; + oidc.configuration = { + authority: 'https://issuer.example.com', + client_id: 'client', + redirect_uri: 'https://client.example.com/callback', + refresh_time_before_tokens_expiration_in_second: 0, + scope: 'openid', + storage: {}, + token_renew_mode: 'access_token_or_id_token_invalid', + } as never; + oidc.destroyAsync = vi.fn(); + oidc.getFetch = vi.fn(); + oidc.initAsync = vi.fn().mockResolvedValue({ + tokenEndpoint: 'https://issuer.example.com/token', + }); + oidc.publishEvent = vi.fn(); + oidc.tokens = { + accessToken: 'access-token', + expiresAt: 0, + issuedAt: 1, + refreshToken: 'refresh-token', + } as Tokens; + + const result = await renewTokensAndStartTimerResultAsync(oidc); + + expect(result.status).toBe('SESSION_LOST'); + expect(result.tokens).toBeNull(); + expect(result.error).toBeInstanceOf(OidcStateError); + expect(result.error).toMatchObject({ + code: OidcStateErrorCode.NONCE_MISSING, + phase: 'refresh', + retryable: false, + }); + expect(oidc.tokens).toBeNull(); + }); +}); diff --git a/packages/oidc-client/src/requests.form.spec.ts b/packages/oidc-client/src/requests.form.spec.ts new file mode 100644 index 000000000..36cfe0a29 --- /dev/null +++ b/packages/oidc-client/src/requests.form.spec.ts @@ -0,0 +1,139 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { TokenRenewMode } from './parseTokens'; +import { + performFirstTokenRequestAsync, + performPushedAuthorizationRequestAsync, + performRevocationRequestAsync, + performTokenRequestAsync, +} from './requests'; +import type { Fetch, StringMap } from './types'; + +describe('OAuth request form encoding', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it.each(['revocation', 'token', 'first token', 'PAR'])( + 'preserves encoding and parameters for %s requests', + async requestType => { + const fetch = vi + .fn() + .mockResolvedValue( + new Response( + JSON.stringify( + requestType === 'PAR' + ? { request_uri: 'urn:request:example', expires_in: 60 } + : { access_token: 'opaque', issued_at: 1000, expires_in: 60 }, + ), + { status: requestType === 'PAR' ? 201 : 200 }, + ), + ); + const parameters = { + 'custom key': 'a+b &c=値', + scope: 'openid profile', + empty: '', + }; + const endpoint = 'https://issuer.example.com/endpoint'; + const expectedParameters: StringMap = { ...parameters }; + + if (requestType === 'revocation') { + await performRevocationRequestAsync(fetch)( + endpoint, + 'opaque', + 'access_token', + 'client', + parameters, + ); + Object.assign(expectedParameters, { + token: 'opaque', + token_type_hint: 'access_token', + client_id: 'client', + }); + } else if (requestType === 'token') { + await performTokenRequestAsync(fetch)( + endpoint, + { grant_type: 'refresh_token' }, + parameters, + null, + {}, + TokenRenewMode.access_token_invalid, + ); + expectedParameters.grant_type = 'refresh_token'; + } else if (requestType === 'first token') { + vi.stubGlobal('fetch', fetch); + const storage = { + getCodeVerifierAsync: vi.fn().mockResolvedValue('test-verifier'), + setCodeVerifierAsync: vi.fn(), + setStateAsync: vi.fn(), + }; + await performFirstTokenRequestAsync(storage)( + endpoint, + parameters, + {}, + TokenRenewMode.access_token_invalid, + ); + expectedParameters.code_verifier = 'test-verifier'; + expect(storage.setCodeVerifierAsync).toHaveBeenCalledWith(null); + expect(storage.setStateAsync).toHaveBeenCalledWith(null); + } else { + await performPushedAuthorizationRequestAsync(fetch)(endpoint, parameters); + } + + expect(fetch).toHaveBeenCalledOnce(); + const [url, request] = fetch.mock.calls[0]; + expect(url).toBe(endpoint); + expect(request.method).toBe('POST'); + expect(request.headers).toEqual({ + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + }); + expect(request.body).toContain('custom%20key=a%2Bb%20%26c%3D%E5%80%A4'); + expect(request.body).toContain('scope=openid%20profile'); + expect(Object.fromEntries(new URLSearchParams(request.body as string))).toEqual( + expectedParameters, + ); + expect(parameters).toEqual({ + 'custom key': 'a+b &c=値', + scope: 'openid profile', + empty: '', + }); + }, + ); + + it('does not let revocation extras override existing parameters', async () => { + const fetch = vi.fn().mockResolvedValue(new Response(null, { status: 200 })); + + await performRevocationRequestAsync(fetch)( + 'https://issuer.example.com/revoke', + 'original', + 'access_token', + 'client', + { token: 'replacement', client_id: 'other', token_type_hint: 'refresh_token' }, + ); + + expect(fetch.mock.calls[0][1].body).toBe( + 'token=original&token_type_hint=access_token&client_id=client', + ); + }); + + it('preserves enumerable inherited parameters in token requests', async () => { + const fetch = vi.fn().mockResolvedValue(new Response('{}', { status: 400 })); + const details: StringMap = Object.assign(Object.create({ inherited: 'value' }), { + grant_type: 'refresh_token', + }); + + await performTokenRequestAsync(fetch)( + 'https://issuer.example.com/token', + details, + { grant_type: 'ignored', extra: 'added' }, + null, + {}, + TokenRenewMode.access_token_invalid, + ); + + expect(fetch.mock.calls[0][1].body).toBe( + 'grant_type=refresh_token&extra=added&inherited=value', + ); + expect(details.extra).toBe('added'); + }); +}); diff --git a/packages/oidc-client/src/requests.spec.ts b/packages/oidc-client/src/requests.spec.ts index 7d3271d7d..f4dd09711 100644 --- a/packages/oidc-client/src/requests.spec.ts +++ b/packages/oidc-client/src/requests.spec.ts @@ -1,9 +1,296 @@ - -import { describe, it, expect } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { ILOidcLocation } from './location'; +import { + PushedAuthorizationRequestError, + PushedAuthorizationRequestErrorCode, +} from './pushedAuthorizationRequestError'; +import { + performAuthorizationRequestAsync, + performPushedAuthorizationRequestAsync, + performTokenRequestAsync, +} from './requests'; +import { Fetch } from './types'; -describe('Requests test Suite', () => { - it('performAuthorizationRequestAsync', async () => { - expect(true).toBe(true); +class FakeLocation implements ILOidcLocation { + openedUrl: string = null; + + open(url: string): void { + this.openedUrl = url; + } + + reload(): void {} + + getCurrentHref(): string { + return ''; + } + + getPath(): string { + return '/'; + } + + getOrigin(): string { + return 'https://client.example.com'; + } +} + +const createResponse = (status: number, data: unknown): Response => + ({ + status, + json: vi.fn(async () => data), + }) as unknown as Response; + +const asFetch = (mock: ReturnType): Fetch => mock as unknown as Fetch; + +describe('Pushed Authorization Requests', () => { + it('posts the authorization parameters as form data and accepts a RFC 9126 response', async () => { + const fetchMock = vi.fn(async (_url: string, _request: RequestInit) => + createResponse(201, { + request_uri: 'urn:ietf:params:oauth:request_uri:abc', + expires_in: 90, + }), + ); + + const result = await performPushedAuthorizationRequestAsync(asFetch(fetchMock))( + 'https://issuer.example.com/par', + { + client_id: 'client', + redirect_uri: 'https://client.example.com/callback', + scope: 'openid profile', + state: 'state-value', + }, + ); + + expect(result).toEqual({ + request_uri: 'urn:ietf:params:oauth:request_uri:abc', + expires_in: 90, + }); + expect(fetchMock).toHaveBeenCalledOnce(); + const [url, request] = fetchMock.mock.calls[0]; + expect(url).toBe('https://issuer.example.com/par'); + expect(request.method).toBe('POST'); + expect(request.headers).toEqual({ + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + }); + expect(Object.fromEntries(new URLSearchParams(request.body as string))).toEqual({ + client_id: 'client', + redirect_uri: 'https://client.example.com/callback', + scope: 'openid profile', + state: 'state-value', + }); + }); + + it('surfaces OAuth error details returned by the PAR endpoint', async () => { + const fetchMock = vi.fn(async () => + createResponse(400, { + error: 'invalid_request', + error_description: 'redirect_uri is invalid', + }), + ); + + await expect( + performPushedAuthorizationRequestAsync(asFetch(fetchMock))('https://issuer.example.com/par', { + client_id: 'client', + }), + ).rejects.toMatchObject({ + name: 'PushedAuthorizationRequestError', + code: PushedAuthorizationRequestErrorCode.REQUEST_FAILED, + status: 400, + oauthError: 'invalid_request', + oauthErrorDescription: 'redirect_uri is invalid', + }); + }); + + it.each([ + [{ expires_in: 90 }, 'missing request_uri'], + [{ request_uri: 'urn:request:abc' }, 'missing expires_in'], + [{ request_uri: '', expires_in: 90 }, 'empty request_uri'], + [{ request_uri: 'urn:request:abc', expires_in: 0 }, 'invalid expires_in'], + ])('rejects an invalid successful response (%s: %s)', async (data: any, _description) => { + const fetchMock = vi.fn(async () => createResponse(201, data)); + + await expect( + performPushedAuthorizationRequestAsync(asFetch(fetchMock))('https://issuer.example.com/par', { + client_id: 'client', + }), + ).rejects.toMatchObject({ + code: PushedAuthorizationRequestErrorCode.INVALID_RESPONSE, + status: 201, + }); + }); + + it('rejects request_uri in the pushed parameters before sending a request', async () => { + const fetchMock = vi.fn(); + + await expect( + performPushedAuthorizationRequestAsync(asFetch(fetchMock))('https://issuer.example.com/par', { + client_id: 'client', + request_uri: 'urn:request:not-allowed', + }), + ).rejects.toBeInstanceOf(PushedAuthorizationRequestError); + expect(fetchMock).not.toHaveBeenCalled(); + }); +}); + +describe('performAuthorizationRequestAsync', () => { + const originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window'); + + beforeAll(() => { + Object.defineProperty(globalThis, 'window', { + configurable: true, + value: { crypto: globalThis.crypto }, + }); + }); + + afterAll(() => { + if (originalWindow) { + Object.defineProperty(globalThis, 'window', originalWindow); + } else { + delete (globalThis as { window?: unknown }).window; + } + }); + + it('keeps the existing front-channel authorization request when PAR is not selected', async () => { + const storage = { + setCodeVerifierAsync: vi.fn(async () => undefined), + setStateAsync: vi.fn(async () => undefined), + }; + const location = new FakeLocation(); + + await performAuthorizationRequestAsync(storage, location)( + 'https://issuer.example.com/authorize', + { + client_id: 'client', + redirect_uri: 'https://client.example.com/callback', + response_type: 'code', + scope: 'openid', + state: 'state-value', + }, + ); + + const url = new URL(location.openedUrl); + expect(url.origin + url.pathname).toBe('https://issuer.example.com/authorize'); + expect(Object.fromEntries(url.searchParams)).toMatchObject({ + client_id: 'client', + redirect_uri: 'https://client.example.com/callback', + response_type: 'code', + scope: 'openid', + state: 'state-value', + code_challenge_method: 'S256', + }); + expect(url.searchParams.get('code_challenge')).toBeTruthy(); + expect(storage.setCodeVerifierAsync).toHaveBeenCalledWith(expect.any(String)); + expect(storage.setStateAsync).toHaveBeenCalledWith('state-value'); + }); + + it('pushes the complete PKCE request and redirects with only client_id and request_uri', async () => { + const storage = { + setCodeVerifierAsync: vi.fn(async () => undefined), + setStateAsync: vi.fn(async () => undefined), + }; + const location = new FakeLocation(); + const fetchMock = vi.fn(async (_url: string, _request: RequestInit) => + createResponse(201, { + request_uri: 'urn:ietf:params:oauth:request_uri:abc/123', + expires_in: 60, + }), + ); + + await performAuthorizationRequestAsync(storage, location)( + 'https://issuer.example.com/authorize', + { + client_id: 'client', + redirect_uri: 'https://client.example.com/callback', + response_type: 'code', + scope: 'openid profile', + state: 'state-value', + nonce: 'nonce-value', + }, + { + endpoint: 'https://issuer.example.com/par', + fetch: asFetch(fetchMock), + timeoutMs: 5000, + }, + ); + + const [, request] = fetchMock.mock.calls[0]; + const pushedParameters = Object.fromEntries(new URLSearchParams(request.body as string)); + expect(pushedParameters).toMatchObject({ + client_id: 'client', + redirect_uri: 'https://client.example.com/callback', + response_type: 'code', + scope: 'openid profile', + state: 'state-value', + nonce: 'nonce-value', + code_challenge_method: 'S256', + }); + expect(pushedParameters.code_challenge).toBeTruthy(); + + const redirect = new URL(location.openedUrl); + expect(Object.fromEntries(redirect.searchParams)).toEqual({ + client_id: 'client', + request_uri: 'urn:ietf:params:oauth:request_uri:abc/123', + }); + }); +}); + +describe('token endpoint error details', () => { + it('preserves OAuth details, HTTP status and the DPoP nonce on a non-2xx response', async () => { + const fetchMock = vi.fn(async () => { + return new Response( + JSON.stringify({ + error: 'use_dpop_nonce', + error_description: 'A fresh proof is required', + }), + { + status: 400, + headers: { + 'Content-Type': 'application/json', + 'DPoP-Nonce': 'server-nonce', + }, + }, + ); + }); + + const response = await performTokenRequestAsync(asFetch(fetchMock))( + 'https://issuer.example.com/token', + { grant_type: 'refresh_token' }, + {}, + null, + { DPoP: 'proof' }, + 'access_token_or_id_token_invalid', + ); + + expect(response).toMatchObject({ + success: false, + status: 400, + oauthError: 'use_dpop_nonce', + oauthErrorDescription: 'A fresh proof is required', + demonstratingProofOfPossessionNonce: 'server-nonce', + }); + }); + + it.each([408, 429, 500])('preserves retryable HTTP status %s for the caller', async status => { + const fetchMock = vi.fn(async () => { + return new Response(JSON.stringify({ error: 'temporarily_unavailable' }), { + status, + headers: { 'Content-Type': 'application/json' }, + }); + }); + + const response = await performTokenRequestAsync(asFetch(fetchMock))( + 'https://issuer.example.com/token', + { grant_type: 'refresh_token' }, + {}, + null, + {}, + 'access_token_or_id_token_invalid', + ); + + expect(response).toMatchObject({ + success: false, + status, + oauthError: 'temporarily_unavailable', }); -}); \ No newline at end of file + }); +}); diff --git a/packages/oidc-client/src/requests.ts b/packages/oidc-client/src/requests.ts index 7bf644f00..ae523c34e 100644 --- a/packages/oidc-client/src/requests.ts +++ b/packages/oidc-client/src/requests.ts @@ -1,121 +1,303 @@ import { getFromCache, setCache } from './cache.js'; import { deriveChallengeAsync, generateRandom } from './crypto.js'; +import { ILOidcLocation } from './location'; import { OidcAuthorizationServiceConfiguration } from './oidc.js'; import { parseOriginalTokens } from './parseTokens.js'; +import { + PushedAuthorizationRequestError, + PushedAuthorizationRequestErrorCode, +} from './pushedAuthorizationRequestError.js'; import { Fetch, StringMap } from './types.js'; const oneHourSecond = 60 * 60; -export const fetchFromIssuer = (fetch) => async (openIdIssuerUrl: string, timeCacheSecond = oneHourSecond, storage = window.sessionStorage, timeoutMs = 10000): - Promise => { +export const fetchFromIssuer = + fetch => + async ( + openIdIssuerUrl: string, + timeCacheSecond = oneHourSecond, + storage = window.sessionStorage, + timeoutMs = 10000, + ): Promise => { const fullUrl = `${openIdIssuerUrl}/.well-known/openid-configuration`; const localStorageKey = `oidc.server:${openIdIssuerUrl}`; const data = getFromCache(localStorageKey, storage, timeCacheSecond); if (data) { - return new OidcAuthorizationServiceConfiguration(data); + return new OidcAuthorizationServiceConfiguration(data); } const response = await internalFetch(fetch)(fullUrl, {}, timeoutMs); if (response.status !== 200) { - return null; + return null; } const result = await response.json(); setCache(localStorageKey, result, storage); return new OidcAuthorizationServiceConfiguration(result); -}; + }; -const internalFetch = (fetch) => async (url, headers = {}, timeoutMs = 10000, numberRetry = 0) : Promise => { +const internalFetch = + fetch => + async (url: string, headers = {}, timeoutMs = 10000, numberRetry = 0): Promise => { let response; try { - const controller = new AbortController(); - setTimeout(() => controller.abort(), timeoutMs); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + try { response = await fetch(url, { ...headers, signal: controller.signal }); + } finally { + clearTimeout(timeoutId); + } } catch (e: any) { - if (e.name === 'AbortError' || - e.message === 'Network request failed') { - if (numberRetry <= 1) { - return await internalFetch(fetch)(url, headers, timeoutMs, numberRetry + 1); - } else { - throw e; - } + if (e.name === 'AbortError' || e.message === 'Network request failed') { + if (numberRetry <= 1) { + return await internalFetch(fetch)(url, headers, timeoutMs, numberRetry + 1); } else { - console.error(e.message); - throw e; // rethrow other unexpected errors + throw e; } + } else { + console.error(e.message); + throw e; // rethrow other unexpected errors + } } return response; -}; + }; export const TOKEN_TYPE = { - refresh_token: 'refresh_token', - access_token: 'access_token', + refresh_token: 'refresh_token', + access_token: 'access_token', +}; + +const encodeFormBody = (details: StringMap): string => { + const formBody: string[] = []; + for (const property in details) { + formBody.push(`${encodeURIComponent(property)}=${encodeURIComponent(details[property])}`); + } + return formBody.join('&'); }; -export const performRevocationRequestAsync = (fetch) => async (url, token, token_type = TOKEN_TYPE.refresh_token, client_id, timeoutMs = 10000) => { +export const performRevocationRequestAsync = + fetch => + async ( + url, + token, + token_type = TOKEN_TYPE.refresh_token, + client_id, + extras: StringMap = {}, + timeoutMs = 10000, + ) => { const details = { - token, - token_type_hint: token_type, - client_id, + token, + token_type_hint: token_type, + client_id, }; - - const formBody = []; - for (const property in details) { - const encodedKey = encodeURIComponent(property); - const encodedValue = encodeURIComponent(details[property]); - formBody.push(`${encodedKey}=${encodedValue}`); + for (const [key, value] of Object.entries(extras)) { + if (details[key] === undefined) { + details[key] = value; + } } - const formBodyString = formBody.join('&'); - const response = await internalFetch(fetch)(url, { + const formBodyString = encodeFormBody(details); + + const response = await internalFetch(fetch)( + url, + { method: 'POST', headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', }, body: formBodyString, - }, timeoutMs); + }, + timeoutMs, + ); if (response.status !== 200) { - return { success: false }; + const data = await getResponseJsonAsync(response); + return { + success: false, + status: response.status, + oauthError: typeof data?.error === 'string' ? data.error : undefined, + oauthErrorDescription: + typeof data?.error_description === 'string' ? data.error_description : undefined, + }; } return { - success: true, + success: true, + status: response.status, }; + }; + +type PerformTokenRequestResponse = { + success: boolean; + status?: number; + data?: any; + demonstratingProofOfPossessionNonce?: string; + oauthError?: string; + oauthErrorDescription?: string; }; -export const performTokenRequestAsync = (fetch:Fetch) => async (url, details, extras, oldTokens, tokenRenewMode: string, timeoutMs = 10000) => { - for (const [key, value] of Object.entries(extras)) { - if (details[key] === undefined) { - details[key] = value; - } +export type PushedAuthorizationRequestResponse = { + request_uri: string; + expires_in: number; +}; + +const getResponseJsonAsync = async (response: Response): Promise => { + try { + return await response.json(); + } catch { + return null; + } +}; + +export const performPushedAuthorizationRequestAsync = + (fetch: Fetch) => + async ( + url: string, + details: StringMap, + timeoutMs = 10000, + ): Promise => { + if (details.request_uri !== undefined) { + throw new PushedAuthorizationRequestError( + PushedAuthorizationRequestErrorCode.REQUEST_FAILED, + 'The request_uri parameter must not be included in a pushed authorization request.', + ); + } + + const formBodyString = encodeFormBody(details); + + let response: Response; + try { + response = await internalFetch(fetch)( + url, + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, + body: formBodyString, + }, + timeoutMs, + ); + } catch (cause) { + throw new PushedAuthorizationRequestError( + PushedAuthorizationRequestErrorCode.REQUEST_FAILED, + 'The pushed authorization request could not be sent.', + { cause }, + ); + } + + const data = await getResponseJsonAsync(response); + if (response.status !== 201) { + const oauthError = typeof data?.error === 'string' ? data.error : undefined; + const oauthErrorDescription = + typeof data?.error_description === 'string' ? data.error_description : undefined; + const serverError = [oauthError, oauthErrorDescription].filter(Boolean).join(': '); + throw new PushedAuthorizationRequestError( + PushedAuthorizationRequestErrorCode.REQUEST_FAILED, + `The pushed authorization request failed with HTTP status ${response.status}${ + serverError ? ` (${serverError})` : '' + }.`, + { + status: response.status, + oauthError, + oauthErrorDescription, + }, + ); + } + + if ( + typeof data?.request_uri !== 'string' || + data.request_uri.length === 0 || + !Number.isInteger(data?.expires_in) || + data.expires_in <= 0 + ) { + throw new PushedAuthorizationRequestError( + PushedAuthorizationRequestErrorCode.INVALID_RESPONSE, + 'The pushed authorization response must contain a non-empty request_uri and a positive integer expires_in.', + { status: response.status }, + ); } - const formBody = []; - for (const property in details) { - const encodedKey = encodeURIComponent(property); - const encodedValue = encodeURIComponent(details[property]); - formBody.push(`${encodedKey}=${encodedValue}`); + return { + request_uri: data.request_uri, + expires_in: data.expires_in, + }; + }; + +export const performTokenRequestAsync = + (fetch: Fetch) => + async ( + url: string, + details, + extras, + oldTokens, + headersExtras = {}, + tokenRenewMode: string, + timeoutMs = 10000, + ): Promise => { + for (const [key, value] of Object.entries(extras)) { + if (details[key] === undefined) { + details[key] = value; + } } - const formBodyString = formBody.join('&'); - const response = await internalFetch(fetch)(url, { + const formBodyString = encodeFormBody(details); + + const response = await internalFetch(fetch)( + url, + { method: 'POST', headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + ...headersExtras, }, body: formBodyString, - }, timeoutMs); + }, + timeoutMs, + ); if (response.status !== 200) { - return { success: false, status: response.status }; + const data = await getResponseJsonAsync(response); + return { + success: false, + status: response.status, + data, + oauthError: typeof data?.error === 'string' ? data.error : undefined, + oauthErrorDescription: + typeof data?.error_description === 'string' ? data.error_description : undefined, + demonstratingProofOfPossessionNonce: response.headers?.has( + demonstratingProofOfPossessionNonceResponseHeader, + ) + ? response.headers.get(demonstratingProofOfPossessionNonceResponseHeader) + : null, + }; } const tokens = await response.json(); + + let demonstratingProofOfPossessionNonce = null; + if (response.headers.has(demonstratingProofOfPossessionNonceResponseHeader)) { + demonstratingProofOfPossessionNonce = response.headers.get( + demonstratingProofOfPossessionNonceResponseHeader, + ); + } return { - success: true, - data: parseOriginalTokens(tokens, oldTokens, tokenRenewMode), + success: true, + status: response.status, + data: parseOriginalTokens(tokens, oldTokens, tokenRenewMode), + demonstratingProofOfPossessionNonce: demonstratingProofOfPossessionNonce, }; -}; + }; -export const performAuthorizationRequestAsync = (storage: any) => async (url, extras: StringMap) => { +export const performAuthorizationRequestAsync = + (storage: any, oidcLocation: ILOidcLocation) => + async ( + url, + extras: StringMap, + pushedAuthorizationRequest?: { + endpoint: string; + fetch: Fetch; + timeoutMs?: number; + }, + ) => { extras = extras ? { ...extras } : {}; const codeVerifier = generateRandom(128); const codeChallenge = await deriveChallengeAsync(codeVerifier); @@ -123,47 +305,93 @@ export const performAuthorizationRequestAsync = (storage: any) => async (url, ex await storage.setStateAsync(extras.state); extras.code_challenge = codeChallenge; extras.code_challenge_method = 'S256'; + + if (pushedAuthorizationRequest) { + const response = await performPushedAuthorizationRequestAsync( + pushedAuthorizationRequest.fetch, + )(pushedAuthorizationRequest.endpoint, extras, pushedAuthorizationRequest.timeoutMs); + oidcLocation.open( + `${url}?client_id=${encodeURIComponent(extras.client_id)}&request_uri=${encodeURIComponent( + response.request_uri, + )}`, + ); + return; + } + let queryString = ''; if (extras) { - for (const [key, value] of Object.entries(extras)) { - if (queryString === '') { - queryString += '?'; - } else { - queryString += '&'; - } - queryString += `${key}=${encodeURIComponent(value)}`; + for (const [key, value] of Object.entries(extras)) { + if (queryString === '') { + queryString += '?'; + } else { + queryString += '&'; } + queryString += `${key}=${encodeURIComponent(value)}`; + } } - window.location.href = `${url}${queryString}`; -}; + oidcLocation.open(`${url}${queryString}`); + }; -export const performFirstTokenRequestAsync = (storage:any) => async (url, extras, tokenRenewMode: string, timeoutMs = 10000) => { - extras = extras ? { ...extras } : {}; - extras.code_verifier = await storage.getCodeVerifierAsync(); - const formBody = []; - for (const property in extras) { - const encodedKey = encodeURIComponent(property); - const encodedValue = encodeURIComponent(extras[property]); - formBody.push(`${encodedKey}=${encodedValue}`); - } - const formBodyString = formBody.join('&'); - const response = await internalFetch(fetch)(url, { +const demonstratingProofOfPossessionNonceResponseHeader = 'DPoP-Nonce'; +export const performFirstTokenRequestAsync = + (storage: any) => + async ( + url, + formBodyExtras, + headersExtras, + tokenRenewMode: string, + timeoutMs = 10000, + shouldPreserveLoginStateForDpopRetry = false, + ): Promise => { + formBodyExtras = formBodyExtras ? { ...formBodyExtras } : {}; + formBodyExtras.code_verifier = await storage.getCodeVerifierAsync(); + const formBodyString = encodeFormBody(formBodyExtras); + const response = await internalFetch(fetch)( + url, + { method: 'POST', headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + ...headersExtras, }, body: formBodyString, - }, timeoutMs); - await Promise.all([storage.setCodeVerifierAsync(null), storage.setStateAsync(null)]); + }, + timeoutMs, + ); if (response.status !== 200) { - return { success: false, status: response.status }; + const data = await getResponseJsonAsync(response); + const oauthError = typeof data?.error === 'string' ? data.error : undefined; + if (!(shouldPreserveLoginStateForDpopRetry && oauthError === 'use_dpop_nonce')) { + await Promise.all([storage.setCodeVerifierAsync(null), storage.setStateAsync(null)]); + } + return { + success: false, + status: response.status, + data, + oauthError, + oauthErrorDescription: + typeof data?.error_description === 'string' ? data.error_description : undefined, + demonstratingProofOfPossessionNonce: response.headers?.has( + demonstratingProofOfPossessionNonceResponseHeader, + ) + ? response.headers.get(demonstratingProofOfPossessionNonceResponseHeader) + : null, + }; + } + await Promise.all([storage.setCodeVerifierAsync(null), storage.setStateAsync(null)]); + let demonstratingProofOfPossessionNonce: string = null; + if (response.headers.has(demonstratingProofOfPossessionNonceResponseHeader)) { + demonstratingProofOfPossessionNonce = response.headers.get( + demonstratingProofOfPossessionNonceResponseHeader, + ); } const tokens = await response.json(); return { - success: true, - data: { - state: extras.state, - tokens: parseOriginalTokens(tokens, null, tokenRenewMode), - }, + success: true, + data: { + state: formBodyExtras.state, + tokens: parseOriginalTokens(tokens, null, tokenRenewMode), + demonstratingProofOfPossessionNonce, + }, }; -}; + }; diff --git a/packages/oidc-client/src/route-utils.spec.ts b/packages/oidc-client/src/route-utils.spec.ts index 30a1dc60a..ca4c3e397 100644 --- a/packages/oidc-client/src/route-utils.spec.ts +++ b/packages/oidc-client/src/route-utils.spec.ts @@ -1,23 +1,78 @@ -import { getPath } from './route-utils'; -import { describe, it, expect } from 'vitest'; +import { describe, expect, it } from 'vitest'; + +import { getLocation, getParseQueryStringFromLocation, getPath } from './route-utils'; describe('Route test Suite', () => { - it.each([['http://example.com/pathname', '/pathname'], - ['http://example.com:3000/pathname/?search=test#hash', '/pathname#hash'], - ['http://example.com:3000/pathname/#hash?search=test', '/pathname#hash'], - ['http://example.com:3000/pathname#hash?search=test', '/pathname#hash'], - ['capacitor://localhost/index.html', '/index.html'], - ['capacitor://localhost/pathname#hash?search=test', '/pathname#hash'], - ['http://example.com:3000/', ''],])( - 'getPath should return the full path of an url', - (uri, expected) => { - - const path = getPath(uri); - expect(path).toBe(expected); - }, + it.each([ + ['http://example.com/pathname', '/pathname'], + ['http://example.com:3000/pathname/?search=test#hash', '/pathname#hash'], + ['http://example.com:3000/pathname/#hash?search=test', '/pathname#hash'], + ['http://example.com:3000/pathname#hash?search=test', '/pathname#hash'], + ['capacitor://localhost/index.html', '/index.html'], + ['capacitor://localhost/pathname#hash?search=test', '/pathname#hash'], + ['http://example.com:3000/', ''], + ['https://example.com/authentication/callback?state=abc&code=def#', '/authentication/callback'], + ])('getPath should return the full path of an url', (uri, expected) => { + const path = getPath(uri); + expect(path).toBe(expected); + }); + + describe('URL parsing', () => { + it('preserves the original URL and separates its components', () => { + const href = 'https://example.com:8443/path/?state=abc#section'; + + expect(getLocation(href)).toEqual({ + href, + protocol: 'https:', + host: 'example.com:8443', + hostname: 'example.com', + port: '8443', + path: '/path/', + search: 'state=abc', + hash: '#section', + }); + }); + + it.each([ + ['https://example.com/path?outer=value#route?inner=value', 'inner=value', '#route'], + ['https://example.com/path?outer=value#route?a?b', 'outer=value', '#route?a?b'], + ['https://example.com/path', '', ''], + ])('preserves query and fragment precedence for %s', (href, search, hash) => { + expect(getLocation(href)).toMatchObject({ search, hash }); + }); + + it.each([ + ['https://example.com/path/#_=_', '/path'], + ['https://example.com/path//#', '/path/'], + ['https://example.com/path/#/route/', '/path#/route/'], + ])( + 'normalizes only the existing trailing slash and sentinel fragments for %s', + (href, path) => { + expect(getPath(href)).toBe(path); + }, ); - it('wrong uri format', () => { - expect(() => getPath("urimybad/toto.com")).toThrowError(); + it.each([ + ['?name=Jane%20Doe&scope=openid%2Bprofile', { name: 'Jane Doe', scope: 'openid+profile' }], + ['?scope=openid+profile', { scope: 'openid+profile' }], + ['?key=first&key=last', { key: 'last' }], + ['?empty=&flag', { empty: '', flag: 'undefined' }], + ['?value=a=b', { value: 'a' }], + ['', { '': 'undefined' }], + ['?outer=value#route?inner=value', { inner: 'value' }], + ['?%E5%90%8D=%E5%80%A4', { 名: '値' }], + ])('preserves query decoding behavior for %s', (suffix, expected) => { + expect(getParseQueryStringFromLocation(`https://example.com/${suffix}`)).toEqual(expected); }); -}); \ No newline at end of file + + it.each(['?key=%', '?%ZZ=value'])('preserves malformed escape errors for %s', suffix => { + expect(() => getParseQueryStringFromLocation(`https://example.com/${suffix}`)).toThrow( + URIError, + ); + }); + }); + + it('wrong uri format', () => { + expect(() => getPath('urimybad/toto.com')).toThrowError(); + }); +}); diff --git a/packages/oidc-client/src/route-utils.ts b/packages/oidc-client/src/route-utils.ts index 3d8b0a46c..64d2a0c44 100644 --- a/packages/oidc-client/src/route-utils.ts +++ b/packages/oidc-client/src/route-utils.ts @@ -1,39 +1,37 @@ export const getLocation = (href: string) => { const match = href.match( // eslint-disable-next-line no-useless-escape - /^([a-z][\w-]+\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)([\/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/, + /^([a-z][\w-]+\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)([\/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/, ); if (!match) { - throw new Error('Invalid URL'); + throw new Error('Invalid URL'); } let search = match[6]; let hash = match[7]; - if (hash) { - const splits = hash.split('?'); - if (splits.length === 2) { - hash = splits[0]; - search = splits[1]; - } + if (hash) { + const splits = hash.split('?'); + if (splits.length === 2) { + hash = splits[0]; + search = splits[1]; } + } - if (search.startsWith('?')) { - search = search.slice(1); - } + if (search.startsWith('?')) { + search = search.slice(1); + } - return ( - match && { - href, - protocol: match[1], - host: match[2], - hostname: match[3], - port: match[4], - path: match[5], - search, - hash, - } - ); + return { + href, + protocol: match[1], + host: match[2], + hostname: match[3], + port: match[4], + path: match[5], + search, + hash, + }; }; export const getPath = (href: string) => { @@ -41,12 +39,12 @@ export const getPath = (href: string) => { let { path } = location; if (path.endsWith('/')) { - path = path.slice(0, -1); + path = path.slice(0, -1); } let { hash } = location; - if (hash === '#_=_') { - hash = ''; + if (hash === '#' || hash === '#_=_') { + hash = ''; } if (hash) { @@ -57,23 +55,18 @@ export const getPath = (href: string) => { }; export const getParseQueryStringFromLocation = (href: string) => { - const location = getLocation(href); - const { search } = location; + const location = getLocation(href); + const { search } = location; - return parseQueryString(search); + return parseQueryString(search); }; -const parseQueryString = (queryString:string) => { - const params:any = {}; let temp; let i; let l; - - // Split into key/value pairs - const queries = queryString.split('&'); - - // Convert the array of strings into an object - for (i = 0, l = queries.length; i < l; i++) { - temp = queries[i].split('='); - params[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]); - } +const parseQueryString = (queryString: string) => { + const params: any = {}; + for (const query of queryString.split('&')) { + const [key, value] = query.split('='); + params[decodeURIComponent(key)] = decodeURIComponent(value); + } - return params; + return params; }; diff --git a/packages/oidc-client/src/signalServiceWorker.spec.ts b/packages/oidc-client/src/signalServiceWorker.spec.ts new file mode 100644 index 000000000..b9c6d33be --- /dev/null +++ b/packages/oidc-client/src/signalServiceWorker.spec.ts @@ -0,0 +1,77 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import * as initWorker from './initWorker'; +import { OidcClient } from './oidcClient'; +import { ServiceWorkerMessageType } from './protocol'; + +const buildClient = (overrides: Partial<{ configurationName: string }> = {}) => { + const oidc = { + configuration: { client_id: 'demo-client' }, + configurationName: overrides.configurationName ?? 'demo', + }; + return new OidcClient(oidc as never); +}; + +describe('OidcClient.signalServiceWorker', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('forwards the typed message to signalServiceWorkerAsync with the OIDC config', async () => { + const expected = { configurationName: 'demo', state: 'restored-state' }; + const spy = vi + .spyOn(initWorker, 'signalServiceWorkerAsync') + .mockResolvedValue(expected as never); + + const client = buildClient(); + const response = await client.signalServiceWorker<{ state: string }>({ + type: ServiceWorkerMessageType.GET_STATE, + configurationName: 'demo', + data: null, + }); + + expect(response).toEqual(expected); + expect(spy).toHaveBeenCalledOnce(); + expect(spy).toHaveBeenCalledWith( + { client_id: 'demo-client' }, + 'demo', + expect.objectContaining({ + type: 'getState', + data: null, + }), + undefined, + ); + }); + + it('propagates a custom timeout option', async () => { + const spy = vi.spyOn(initWorker, 'signalServiceWorkerAsync').mockResolvedValue({} as never); + + const client = buildClient(); + await client.signalServiceWorker( + { type: ServiceWorkerMessageType.CLEAR, configurationName: 'demo', data: { status: null } }, + { timeoutMs: 9000 }, + ); + + expect(spy).toHaveBeenCalledWith( + expect.anything(), + 'demo', + expect.objectContaining({ type: 'clear' }), + { timeoutMs: 9000 }, + ); + }); + + it('rejects when the underlying helper rejects', async () => { + const error = new Error('no SW'); + vi.spyOn(initWorker, 'signalServiceWorkerAsync').mockRejectedValue(error); + + const client = buildClient(); + + await expect( + client.signalServiceWorker({ + type: ServiceWorkerMessageType.GET_STATE, + configurationName: 'demo', + data: null, + }), + ).rejects.toBe(error); + }); +}); diff --git a/packages/oidc-client/src/silentLogin.spec.ts b/packages/oidc-client/src/silentLogin.spec.ts new file mode 100644 index 000000000..cd9a31494 --- /dev/null +++ b/packages/oidc-client/src/silentLogin.spec.ts @@ -0,0 +1,158 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { OidcError, OidcErrorCode, serializeOidcError } from './oidcError'; +import { isOidcStateError, OidcStateError, OidcStateErrorCode } from './oidcStateError'; +import { _silentLoginAsync } from './silentLogin'; + +const installDom = () => { + const contentWindow = {}; + const iframe = { + contentWindow, + height: '', + id: '', + remove: vi.fn(), + setAttribute: vi.fn(), + style: { display: '' }, + width: '', + }; + let listener: (event: MessageEvent) => void; + vi.stubGlobal('document', { + body: { appendChild: vi.fn() }, + createElement: vi.fn(() => iframe), + }); + vi.stubGlobal('window', { + addEventListener: vi.fn((_name: string, callback: (event: MessageEvent) => void) => { + listener = callback; + }), + removeEventListener: vi.fn(), + }); + return { contentWindow, iframe, getListener: () => listener }; +}; + +afterEach(() => { + vi.useRealTimers(); + vi.unstubAllGlobals(); +}); + +describe('_silentLoginAsync typed errors', () => { + it('rejects with SILENT_LOGIN_TIMEOUT and publishes the same error', async () => { + vi.useFakeTimers(); + installDom(); + const publishEvent = vi.fn(); + + const promise = _silentLoginAsync( + 'default', + { + authority: 'https://issuer.example.com', + client_id: 'client', + redirect_uri: 'https://client.example.com/callback', + scope: 'openid', + silent_login_uri: 'https://issuer.example.com/silent-login', + silent_redirect_uri: 'https://client.example.com/silent-callback', + silent_login_timeout: 25, + }, + publishEvent, + 'refresh', + )(); + const rejection = expect(promise).rejects.toMatchObject({ + code: OidcErrorCode.SILENT_LOGIN_TIMEOUT, + message: 'timeout', + phase: 'refresh', + retryable: true, + }); + + await vi.advanceTimersByTimeAsync(25); + await rejection; + const publishedError = publishEvent.mock.calls.find( + ([name]) => name === 'silentLoginAsync_error', + )?.[1]; + expect(publishedError).toBeInstanceOf(OidcError); + expect(publishedError.code).toBe(OidcErrorCode.SILENT_LOGIN_TIMEOUT); + }); + + it('reconstructs a serialized OidcError received from the callback iframe', async () => { + const { contentWindow, getListener } = installDom(); + const publishEvent = vi.fn(); + const original = new OidcError(OidcErrorCode.LOGIN_REQUIRED, 'login required', { + phase: 'refresh', + retryable: false, + oauthError: 'login_required', + }); + + const promise = _silentLoginAsync( + 'default', + { + authority: 'https://issuer.example.com', + client_id: 'client', + redirect_uri: 'https://client.example.com/callback', + scope: 'openid', + silent_login_uri: 'https://issuer.example.com/silent-login', + silent_redirect_uri: 'https://client.example.com/silent-callback', + silent_login_timeout: 1000, + }, + publishEvent, + 'refresh', + )(); + getListener()({ + origin: 'https://issuer.example.com', + source: contentWindow, + data: `default_oidc_exception:${JSON.stringify({ + error: original.toString(), + oidcError: serializeOidcError(original), + })}`, + } as unknown as MessageEvent); + + await expect(promise).rejects.toMatchObject({ + code: OidcErrorCode.LOGIN_REQUIRED, + phase: 'refresh', + oauthError: 'login_required', + }); + }); + + it('keeps serialized OidcStateError instances detectable after the iframe round-trip', async () => { + const { contentWindow, getListener } = installDom(); + const publishEvent = vi.fn(); + const original = new OidcStateError( + OidcStateErrorCode.NONCE_MISSING, + 'refresh token: nonce missing from storage', + 'refresh', + ); + + const promise = _silentLoginAsync( + 'default', + { + authority: 'https://issuer.example.com', + client_id: 'client', + redirect_uri: 'https://client.example.com/callback', + scope: 'openid', + silent_login_uri: 'https://issuer.example.com/silent-login', + silent_redirect_uri: 'https://client.example.com/silent-callback', + silent_login_timeout: 1000, + }, + publishEvent, + 'refresh', + )(); + getListener()({ + origin: 'https://issuer.example.com', + source: contentWindow, + data: `default_oidc_exception:${JSON.stringify({ + error: original.toString(), + oidcError: serializeOidcError(original), + })}`, + } as unknown as MessageEvent); + + let thrownError: unknown; + try { + await promise; + } catch (error) { + thrownError = error; + } + + expect(isOidcStateError(thrownError)).toBe(true); + expect(thrownError).toMatchObject({ + code: OidcStateErrorCode.NONCE_MISSING, + phase: 'refresh', + name: 'OidcStateError', + }); + }); +}); diff --git a/packages/oidc-client/src/silentLogin.ts b/packages/oidc-client/src/silentLogin.ts index 70932a974..928113eb8 100644 --- a/packages/oidc-client/src/silentLogin.ts +++ b/packages/oidc-client/src/silentLogin.ts @@ -1,144 +1,213 @@ import { eventNames } from './events.js'; +import { + createOAuthError, + deserializeOidcError, + OidcError, + OidcErrorCode, + OidcErrorPhase, +} from './oidcError.js'; import { Tokens } from './parseTokens.js'; import { autoRenewTokens } from './renewTokens.js'; import timer from './timer.js'; import { OidcConfiguration, StringMap } from './types.js'; export type SilentLoginResponse = { - tokens:Tokens; - sessionState:string; + tokens: Tokens | null; + sessionState: string | null; + error?: string; + oidcError?: OidcError; }; -// eslint-disable-next-line @typescript-eslint/ban-types -export const _silentLoginAsync = (configurationName:string, configuration:OidcConfiguration, publishEvent:Function) => (extras:StringMap = null, state:string = null, scope:string = null):Promise => { +export type PublishEventFunction = (eventName: string, eventData: any) => void; + +export const _silentLoginAsync = + ( + configurationName: string, + configuration: OidcConfiguration, + publishEvent: PublishEventFunction, + phase: OidcErrorPhase = 'login', + ) => + ( + extras: StringMap = null, + state: string = null, + scope: string = null, + ): Promise => { if (!configuration.silent_redirect_uri || !configuration.silent_login_uri) { - return Promise.resolve(null); + return Promise.resolve(null); } - try { - publishEvent(eventNames.silentLoginAsync_begin, {}); - let queries = ''; + publishEvent(eventNames.silentLoginAsync_begin, {}); + let queries = ''; - if (state) { - if (extras == null) { - extras = {}; - } - extras.state = state; + if (state) { + if (extras == null) { + extras = {}; } + extras.state = state; + } - if (scope) { - if (extras == null) { - extras = {}; - } - extras.scope = scope; + if (scope != null) { + if (extras == null) { + extras = {}; + } + extras.scope = scope; + } + + if (extras != null) { + for (const [key, value] of Object.entries(extras)) { + if (value == null) { + continue; + } + if (queries === '') { + queries = `?${encodeURIComponent(key)}=${encodeURIComponent(value)}`; + } else { + queries += `&${encodeURIComponent(key)}=${encodeURIComponent(value)}`; + } } + } + const link = configuration.silent_login_uri + queries; + const idx = link.indexOf('/', link.indexOf('//') + 2); + const iFrameOrigin = link.substring(0, idx); + const iframe = document.createElement('iframe'); + iframe.width = '0px'; + iframe.height = '0px'; - if (extras != null) { - for (const [key, value] of Object.entries(extras)) { - if (queries === '') { - queries = `?${encodeURIComponent(key)}=${encodeURIComponent(value)}`; - } else { - queries += `&${encodeURIComponent(key)}=${encodeURIComponent(value)}`; + iframe.id = `${configurationName}_oidc_iframe`; + iframe.setAttribute('src', link); + iframe.style.display = 'none'; + document.body.appendChild(iframe); + return new Promise((resolve, reject) => { + let isResolved = false; + + const clear = () => { + window.removeEventListener('message', listener); + iframe.remove(); + isResolved = true; + }; + + const listener = (e: MessageEvent) => { + if (e.origin === iFrameOrigin && e.source === iframe.contentWindow) { + const key = `${configurationName}_oidc_tokens:`; + const key_error = `${configurationName}_oidc_error:`; + const key_exception = `${configurationName}_oidc_exception:`; + const data = e.data; + + if (data && typeof data === 'string') { + if (!isResolved) { + if (data.startsWith(key)) { + const result = JSON.parse(e.data.replace(key, '')); + publishEvent(eventNames.silentLoginAsync_end, {}); + resolve(result); + clear(); + } else if (data.startsWith(key_error)) { + const result = JSON.parse(e.data.replace(key_error, '')); + const error = + deserializeOidcError(result.oidcError) ?? + createOAuthError( + result.error, + result.error_description, + `Error from OIDC server: ${result.error} - ${result.error_description}`, + phase, + ); + publishEvent(eventNames.silentLoginAsync_error, error); + resolve({ + error: 'oidc_' + result.error, + tokens: null, + sessionState: null, + oidcError: error, + }); + clear(); + } else if (data.startsWith(key_exception)) { + const result = JSON.parse(e.data.replace(key_exception, '')); + const error = deserializeOidcError(result.oidcError) ?? new Error(result.error); + publishEvent(eventNames.silentLoginAsync_error, error); + reject(error); + clear(); } + } } - } - const link = configuration.silent_login_uri + queries; - const idx = link.indexOf('/', link.indexOf('//') + 2); - const iFrameOrigin = link.substr(0, idx); - const iframe = document.createElement('iframe'); - iframe.width = '0px'; - iframe.height = '0px'; - - iframe.id = `${configurationName}_oidc_iframe`; - iframe.setAttribute('src', link); - document.body.appendChild(iframe); - return new Promise((resolve, reject) => { - try { - let isResolved = false; - window.onmessage = (e: MessageEvent) => { - if (e.origin === iFrameOrigin && - e.source === iframe.contentWindow - ) { - const key = `${configurationName}_oidc_tokens:`; - const key_error = `${configurationName}_oidc_error:`; - const data = e.data; - if (data && typeof (data) === 'string') { - if (!isResolved) { - if (data.startsWith(key)) { - const result = JSON.parse(e.data.replace(key, '')); - publishEvent(eventNames.silentLoginAsync_end, {}); - iframe.remove(); - isResolved = true; - resolve(result); - } else if (data.startsWith(key_error)) { - const result = JSON.parse(e.data.replace(key_error, '')); - publishEvent(eventNames.silentLoginAsync_error, result); - iframe.remove(); - isResolved = true; - reject(new Error('oidc_' + result.error)); - } - } - } - } - }; - const silentSigninTimeout = configuration.silent_login_timeout; - setTimeout(() => { - if (!isResolved) { - publishEvent(eventNames.silentLoginAsync_error, { reason: 'timeout' }); - iframe.remove(); - isResolved = true; - reject(new Error('timeout')); - } - }, silentSigninTimeout); - } catch (e) { - iframe.remove(); - publishEvent(eventNames.silentLoginAsync_error, e); - reject(e); + } + }; + + try { + window.addEventListener('message', listener); + + const silentSigninTimeout = configuration.silent_login_timeout; + setTimeout(() => { + if (!isResolved) { + clear(); + const error = new OidcError(OidcErrorCode.SILENT_LOGIN_TIMEOUT, 'timeout', { + phase, + retryable: true, + }); + publishEvent(eventNames.silentLoginAsync_error, error); + reject(error); } - }); + }, silentSigninTimeout); + } catch (e) { + clear(); + publishEvent(eventNames.silentLoginAsync_error, e); + reject(e); + } + }); } catch (e) { - publishEvent(eventNames.silentLoginAsync_error, e); - throw e; + publishEvent(eventNames.silentLoginAsync_error, e); + throw e; } -}; + }; -// eslint-disable-next-line @typescript-eslint/ban-types -export const defaultSilentLoginAsync = (window, configurationName, configuration:OidcConfiguration, publishEvent :(string, any)=>void, oidc:any) => (extras:StringMap = null, scope:string = undefined) => { +export const defaultSilentLoginAsync = + ( + window, + configurationName, + configuration: OidcConfiguration, + publishEvent: (string, any) => void, + oidc: any, + ) => + (extras: StringMap = null, scope: string = undefined) => { extras = { ...extras }; - const silentLoginAsync = (extras, state, scope) => { - return _silentLoginAsync(configurationName, configuration, publishEvent.bind(oidc))(extras, state, scope); + const silentLoginAsync = (extras, state, scopeInternal) => { + return _silentLoginAsync(configurationName, configuration, publishEvent.bind(oidc))( + extras, + state, + scopeInternal, + ); }; const loginLocalAsync = async () => { - if (oidc.timeoutId) { - timer.clearTimeout(oidc.timeoutId); - } + if (oidc.timeoutId) { + timer.clearTimeout(oidc.timeoutId); + } - let state; - if (extras && 'state' in extras) { - state = extras.state; - delete extras.state; - } + let state; + if (extras && 'state' in extras) { + state = extras.state; + delete extras.state; + } - try { - const extraFinal = !configuration.extras ? extras : { ...configuration.extras, ...extras }; - const silentResult = await silentLoginAsync({ - ...extraFinal, - prompt: 'none', - }, state, scope); - - if (silentResult) { - oidc.tokens = silentResult.tokens; - publishEvent(eventNames.token_aquired, {}); - // @ts-ignore - oidc.timeoutId = autoRenewTokens(oidc, oidc.tokens.refreshToken, oidc.tokens.expiresAt, extras); - return {}; - } - } catch (e) { - return e; + try { + const extraFinal = !configuration.extras ? extras : { ...configuration.extras, ...extras }; + const silentResult = await silentLoginAsync( + { + ...extraFinal, + prompt: 'none', + }, + state, + scope, + ); + + if (silentResult) { + oidc.tokens = silentResult.tokens; + publishEvent(eventNames.token_acquired, {}); + // @ts-ignore + oidc.timeoutId = autoRenewTokens(oidc, oidc.tokens.expiresAt, extras, scope); + return {}; } + } catch (e) { + return e; + } }; return loginLocalAsync(); -}; + }; export default defaultSilentLoginAsync; diff --git a/packages/oidc-client/src/timer.ts b/packages/oidc-client/src/timer.ts index cd088f52b..6f6e2cfec 100644 --- a/packages/oidc-client/src/timer.ts +++ b/packages/oidc-client/src/timer.ts @@ -1,163 +1,13 @@ const timer = (function () { - const workerPort = (function () { - let worker; - let blobURL; - - const workerCode = function () { - const innerIdsByOuterIds = {}; - - const methods = { - setTimeout: function (port, id, timeout) { - innerIdsByOuterIds[id] = setTimeout(function () { - port.postMessage(id); - innerIdsByOuterIds[id] = null; - }, timeout); - }, - - setInterval: function (port, id, timeout) { - innerIdsByOuterIds[id] = setInterval(function () { - port.postMessage(id); - }, timeout); - }, - - clearTimeout: function (port, id) { - clearTimeout(innerIdsByOuterIds[id]); - innerIdsByOuterIds[id] = null; - }, - - clearInterval: function (port, id) { - clearInterval(innerIdsByOuterIds[id]); - innerIdsByOuterIds[id] = null; - }, - }; - - function onMessage(port, event) { - const method = event.data[0]; - const id = event.data[1]; - const option = event.data[2]; - - if (methods[method]) { - methods[method](port, id, option); - } - } - - // For Dedicated Worker - this.onmessage = function (event) { - onMessage(self, event); - }; - - // For Shared Worker - this.onconnect = function (event) { - const port = event.ports[0]; - - port.onmessage = function (event) { - onMessage(port, event); - }; - }; - }.toString(); - - try { - const blob = new Blob(['(', workerCode, ')()'], { type: 'application/javascript' }); - blobURL = URL.createObjectURL(blob); - } catch (error) { - return null; - } - const isInsideBrowser = (typeof process === 'undefined'); - try { - if (SharedWorker) { - worker = new SharedWorker(blobURL); - return worker.port; - } - } catch (error) { - if (isInsideBrowser) { - console.warn('SharedWorker not available'); - } - } - try { - if (Worker) { - worker = new Worker(blobURL); - return worker; - } - } catch (error) { - if (isInsideBrowser) { - console.warn('Worker not available'); - } - } - - return null; - }()); - - if (!workerPort) { - // In NextJS with SSR (Server Side Rendering) during rending in Node JS, the window object is undefined, - // the global object is used instead as it is the closest approximation of a browsers window object. - const bindContext = (typeof window === 'undefined') ? global : window; - - return { - setTimeout: setTimeout.bind(bindContext), - clearTimeout: clearTimeout.bind(bindContext), - setInterval: setInterval.bind(bindContext), - clearInterval: clearInterval.bind(bindContext), - }; - } - - const getId = (function () { - let currentId = 0; - - return function () { - currentId++; - return currentId; - }; - }()); - - const timeoutCallbacksById = {}; - const intervalCallbacksById = {}; - - workerPort.onmessage = function (event) { - const id = event.data; - - const timeoutCallback = timeoutCallbacksById[id]; - if (timeoutCallback) { - timeoutCallback(); - timeoutCallbacksById[id] = null; - return; - } - - const intervalCallback = intervalCallbacksById[id]; - if (intervalCallback) { - intervalCallback(); - } - }; - - function setTimeoutWorker(callback, timeout) { - const id = getId(); - workerPort.postMessage(['setTimeout', id, timeout]); - timeoutCallbacksById[id] = callback; - return id; - } - - function clearTimeoutWorker(id) { - workerPort.postMessage(['clearTimeout', id]); - timeoutCallbacksById[id] = null; - } - - function setIntervalWorker(callback, timeout) { - const id = getId(); - workerPort.postMessage(['setInterval', id, timeout]); - intervalCallbacksById[id] = callback; - return id; - } - - function clearIntervalWorker(id) { - workerPort.postMessage(['clearInterval', id]); - intervalCallbacksById[id] = null; - } - - return { - setTimeout: setTimeoutWorker, - clearTimeout: clearTimeoutWorker, - setInterval: setIntervalWorker, - clearInterval: clearIntervalWorker, - }; -}()); + // In NextJS with SSR (Server Side Rendering) during rending in Node JS, the window object is undefined, + // the global object is used instead as it is the closest approximation of a browsers window object. + const bindContext = typeof window === 'undefined' ? global : window; + return { + setTimeout: setTimeout.bind(bindContext), + clearTimeout: clearTimeout.bind(bindContext), + setInterval: setInterval.bind(bindContext), + clearInterval: clearInterval.bind(bindContext), + }; +})(); export default timer; diff --git a/packages/oidc-client/src/types.ts b/packages/oidc-client/src/types.ts index 7ac7c58ea..ddf7a88d4 100644 --- a/packages/oidc-client/src/types.ts +++ b/packages/oidc-client/src/types.ts @@ -2,40 +2,82 @@ export type Fetch = typeof window.fetch; export type LogoutToken = 'access_token' | 'refresh_token'; +export type PushedAuthorizationRequestMode = 'disabled' | 'auto' | 'required'; + +export type ServiceWorkerUpdateRequireCallback = ( + registration: any, + stopKeepAlive: () => void, +) => Promise; +export type ServiceWorkerRegister = ( + serviceWorkerRelativeUrl: string, +) => Promise; +export type ServiceWorkerActivate = () => boolean; + +export enum TokenAutomaticRenewMode { + AutomaticBeforeTokenExpiration = 'AutomaticBeforeTokensExpiration', + AutomaticOnlyWhenFetchExecuted = 'AutomaticOnlyWhenFetchExecuted', +} + export type OidcConfiguration = { - client_id: string; - redirect_uri: string; - silent_redirect_uri?:string; - silent_login_uri?:string; - silent_login_timeout?:number; - scope: string; - authority: string; - authority_time_cache_wellknowurl_in_second?: number; - authority_timeout_wellknowurl_in_millisecond?: number; - authority_configuration?: AuthorityConfiguration; - refresh_time_before_tokens_expiration_in_second?: number; - token_request_timeout?: number; - service_worker_relative_url?:string; - service_worker_only?:boolean; - service_worker_convert_all_requests_to_cors?:boolean; - extras?:StringMap; - token_request_extras?:StringMap; - storage?: Storage; - monitor_session?: boolean; - token_renew_mode?: string; - logout_tokens_to_invalidate?:Array; + client_id: string; + redirect_uri: string; + silent_redirect_uri?: string; + silent_login_uri?: string; + silent_login_timeout?: number; + scope: string; + authority: string; + authority_time_cache_wellknowurl_in_second?: number; + authority_timeout_wellknowurl_in_millisecond?: number; + authority_configuration?: AuthorityConfiguration; + refresh_time_before_tokens_expiration_in_second?: number; + token_automatic_renew_mode?: TokenAutomaticRenewMode; + token_request_timeout?: number; + par?: PushedAuthorizationRequestMode; + par_request_timeout?: number; + service_worker_relative_url?: string; + service_worker_register?: ServiceWorkerRegister; + service_worker_keep_alive_path?: string; + service_worker_activate?: ServiceWorkerActivate; + service_worker_only?: boolean; + service_worker_convert_all_requests_to_cors?: boolean; + extras?: StringMap; + token_request_extras?: StringMap; + storage?: Storage; + login_state_storage?: Storage; + monitor_session?: boolean; + token_renew_mode?: string; + logout_tokens_to_invalidate?: Array; + demonstrating_proof_of_possession?: boolean; + demonstrating_proof_of_possession_configuration?: DemonstratingProofOfPossessionConfiguration; + preload_user_info?: boolean; + loading_timeout_ms?: number; }; +export interface DemonstratingProofOfPossessionConfiguration { + generateKeyAlgorithm: RsaHashedKeyGenParams | EcKeyGenParams; + digestAlgorithm: AlgorithmIdentifier; + importKeyAlgorithm: + | AlgorithmIdentifier + | RsaHashedImportParams + | EcKeyImportParams + | HmacImportParams + | AesKeyAlgorithm; + signAlgorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams; + jwtHeaderAlgorithm: string; +} + export interface StringMap { - [key: string]: string; + [key: string]: string; } export interface AuthorityConfiguration { - authorization_endpoint: string; - token_endpoint: string; - revocation_endpoint: string; - end_session_endpoint?: string; - userinfo_endpoint?: string; - check_session_iframe?:string; - issuer:string; + authorization_endpoint: string; + token_endpoint: string; + revocation_endpoint: string; + pushed_authorization_request_endpoint?: string; + require_pushed_authorization_requests?: boolean; + end_session_endpoint?: string; + userinfo_endpoint?: string; + check_session_iframe?: string; + issuer: string; } diff --git a/packages/oidc-client/src/user.ts b/packages/oidc-client/src/user.ts index e23f7407c..d6a78cc4e 100644 --- a/packages/oidc-client/src/user.ts +++ b/packages/oidc-client/src/user.ts @@ -1,40 +1,81 @@ -import { sleepAsync } from './initWorker.js'; -import { isTokensValid } from './parseTokens.js'; +import { eventNames } from './events'; +import { fetchWithTokens } from './fetch'; +import Oidc from './oidc'; +import { + createNetworkError, + isNetworkErrorCause, + isOidcError, + isRetryableHttpStatus, + OidcError, + OidcErrorCode, +} from './oidcError'; -export const userInfoAsync = (oidc) => async (noCache = false) => { - if (oidc.userInfo != null && !noCache) { +export const userInfoAsync = + (oidc: Oidc) => + async (noCache = false, demonstrating_proof_of_possession = false) => { + try { + if (oidc.userInfo != null && !noCache) { return oidc.userInfo; - } - - // We wait the synchronisation before making a request - while (oidc.tokens && !isTokensValid(oidc.tokens)) { - await sleepAsync(200); - } - - if (!oidc.tokens) { - return null; - } - const accessToken = oidc.tokens.accessToken; - if (!accessToken) { - return null; - } - - const oidcServerConfiguration = await oidc.initAsync(oidc.configuration.authority, oidc.configuration.authority_configuration); - const url = oidcServerConfiguration.userInfoEndpoint; - const fetchUserInfo = async (accessToken) => { - const res = await fetch(url, { - headers: { - authorization: `Bearer ${accessToken}`, + } + // Check storage cache + const stored = + !noCache && oidc.configuration.storage?.getItem(`oidc.${oidc.configurationName}.userInfo`); + if (stored) { + oidc.userInfo = JSON.parse(stored); + return oidc.userInfo; + } + const configuration = oidc.configuration; + const oidcServerConfiguration = await oidc.initAsync( + configuration.authority, + configuration.authority_configuration, + ); + const url = oidcServerConfiguration.userInfoEndpoint; + const fetchUserInfo = async () => { + const oidcFetch = fetchWithTokens( + fetch, + oidc, + demonstrating_proof_of_possession, + 'userinfo', + ); + const response = await oidcFetch(url); + if (response.status !== 200) { + const isDpopNonceRequired = + response.headers?.has('DPoP-Nonce') && + (response.status === 400 || + response.status === 401 || + response.headers.get('WWW-Authenticate')?.includes('use_dpop_nonce')); + const error = new OidcError( + isDpopNonceRequired ? OidcErrorCode.DPOP_NONCE_REQUIRED : OidcErrorCode.REQUEST_FAILED, + 'UserInfo request failed', + { + phase: 'userinfo', + retryable: isDpopNonceRequired || isRetryableHttpStatus(response.status), + status: response.status, + oauthError: isDpopNonceRequired ? 'use_dpop_nonce' : undefined, }, - }); - - if (res.status !== 200) { - return null; + ); + oidc.publishEvent(eventNames.userInfoAsync_error, error); + return null; } - - return res.json(); - }; - const userInfo = await fetchUserInfo(accessToken); - oidc.userInfo = userInfo; - return userInfo; -}; + return response.json(); + }; + const userInfo = await fetchUserInfo(); + oidc.userInfo = userInfo; + // Store in cache + if (userInfo) { + oidc.configuration.storage?.setItem( + `oidc.${oidc.configurationName}.userInfo`, + JSON.stringify(userInfo), + ); + } + return userInfo; + } catch (cause) { + const error = isOidcError(cause) + ? cause + : isNetworkErrorCause(cause) + ? createNetworkError(cause, 'userinfo') + : cause; + oidc.publishEvent(eventNames.userInfoAsync_error, error); + throw error; + } + }; diff --git a/packages/oidc-client/src/vanillaOidc.ts b/packages/oidc-client/src/vanillaOidc.ts deleted file mode 100644 index e2f3c7477..000000000 --- a/packages/oidc-client/src/vanillaOidc.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { LoginCallback, Oidc } from './oidc.js'; -import { getValidTokenAsync, Tokens, ValidToken } from './parseTokens.js'; -import { Fetch, OidcConfiguration, StringMap } from './types.js'; - -export interface EventSubscriber { - (name: string, data:any); -} - -export class VanillaOidc { - private _oidc: Oidc; - constructor(oidc: Oidc) { - this._oidc = oidc; - } - - subscribeEvents(func:EventSubscriber):string { - return this._oidc.subscribeEvents(func); - } - - removeEventSubscription(id:string):void { - this._oidc.removeEventSubscription(id); - } - - publishEvent(eventName:string, data:any) : void { - this._oidc.publishEvent(eventName, data); - } - - static getOrCreate = (getFetch : () => Fetch) => (configuration:OidcConfiguration, name = 'default'): VanillaOidc => { - return new VanillaOidc(Oidc.getOrCreate(getFetch)(configuration, name)); - }; - - static get(name = 'default'):VanillaOidc { - return new VanillaOidc(Oidc.get(name)); - } - - static eventNames = Oidc.eventNames; - tryKeepExistingSessionAsync():Promise { - return this._oidc.tryKeepExistingSessionAsync(); - } - - loginAsync(callbackPath:string = undefined, extras:StringMap = null, isSilentSignin = false, scope:string = undefined, silentLoginOnly = false):Promise { - return this._oidc.loginAsync(callbackPath, extras, isSilentSignin, scope, silentLoginOnly); - } - - logoutAsync(callbackPathOrUrl: string | null | undefined = undefined, extras: StringMap = null):Promise { - return this._oidc.logoutAsync(callbackPathOrUrl, extras); - } - - silentLoginCallbackAsync():Promise { - return this._oidc.silentLoginCallbackAsync(); - } - - renewTokensAsync(extras:StringMap = null):Promise { - return this._oidc.renewTokensAsync(extras); - } - - loginCallbackAsync():Promise { - return this._oidc.loginCallbackWithAutoTokensRenewAsync(); - } - - get tokens():Tokens { - return this._oidc.tokens; - } - - get configuration():OidcConfiguration { - return this._oidc.configuration; - } - - async getValidTokenAsync(waitMs = 200, numberWait = 50): Promise { - return getValidTokenAsync(this._oidc, waitMs, numberWait); - } - - async userInfoAsync(noCache = false):Promise { - return this._oidc.userInfoAsync(noCache); - } -} - -export interface OidcUserInfo { - sub: string; - name?: string; - given_name?: string; - family_name?: string; - middle_name?: string; - nickname?: string; - preferred_username?: string; - profile?: string; - picture?: string; - website?: string; - email?: string; - email_verified?: boolean; - gender?: string; - birthdate?: string; - zoneinfo?: string; - locale?: string; - phone_number?: string; - phone_number_verified?: boolean; - address?: OidcAddressClaim; - updated_at?: number; - groups?: string[]; -} - -export interface OidcAddressClaim { - formatted?: string; - street_address?: string; - locality?: string; - region?: string; - postal_code?: string; - country?: string; -} diff --git a/packages/oidc-client/src/version.ts b/packages/oidc-client/src/version.ts new file mode 100644 index 000000000..bcc50f8bf --- /dev/null +++ b/packages/oidc-client/src/version.ts @@ -0,0 +1 @@ +export default '7.29.6'; diff --git a/packages/oidc-client/tests/setup.js b/packages/oidc-client/tests/setup.js index e022aaa60..8476fd748 100644 --- a/packages/oidc-client/tests/setup.js +++ b/packages/oidc-client/tests/setup.js @@ -1,11 +1,8 @@ -import { expect, afterEach } from 'vitest'; -import { cleanup } from '@testing-library/react'; -import matchers from '@testing-library/jest-dom/matchers'; +import { defineConfig } from 'vite'; +import { configDefaults } from 'vitest/config'; -// extends Vitest's expect method with methods from react-testing-library -expect.extend(matchers); - -// runs a cleanup after each test case (e.g. clearing jsdom) -afterEach(() => { - cleanup(); -}); \ No newline at end of file +export default defineConfig({ + test: { + exclude: [...configDefaults.exclude, 'public/*'], + }, +}); diff --git a/packages/oidc-client/tsconfig.eslint.json b/packages/oidc-client/tsconfig.eslint.json index e9041fd6b..b90fc83e0 100644 --- a/packages/oidc-client/tsconfig.eslint.json +++ b/packages/oidc-client/tsconfig.eslint.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "include": ["src"] -} \ No newline at end of file +} diff --git a/packages/oidc-client/tsconfig.json b/packages/oidc-client/tsconfig.json index 32493f492..b07a6c47c 100644 --- a/packages/oidc-client/tsconfig.json +++ b/packages/oidc-client/tsconfig.json @@ -1,11 +1,11 @@ -{ +{ "compilerOptions": { "target": "ES2019", "lib": ["ES2021", "DOM"], + "types": ["node"], "outDir": "dist", - "baseUrl": ".", "skipLibCheck": true, - "module": "CommonJS", + "module": "ESNext", "declaration": true, "declarationMap": true, "sourceMap": true, @@ -15,24 +15,18 @@ "strictFunctionTypes": false, "strictPropertyInitialization": false, "noImplicitThis": false, - "alwaysStrict": false, "noUnusedLocals": false, "noUnusedParameters": false, "noImplicitReturns": false, "noFallthroughCasesInSwitch": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "allowJs": true, "rootDir": "src" }, - "exclude": [ - "node_modules", - "**/*.spec.tsx", - ], - "include": [ - "src/**/*" - ], + "exclude": ["node_modules", "**/*.spec.tsx"], + "include": ["src/**/*"] // "files": ["./src/index.ts"] } diff --git a/packages/oidc-client/vite.config.ts b/packages/oidc-client/vite.config.ts index 07867ece3..ad02950d4 100644 --- a/packages/oidc-client/vite.config.ts +++ b/packages/oidc-client/vite.config.ts @@ -1,10 +1,6 @@ -import { defineConfig } from 'vite'; -import { resolve } from 'path'; +import { resolve } from 'path'; +import { defineConfig } from 'vite'; import dts from 'vite-plugin-dts'; -import pkg from './package.json'; - -const dependencies = externalDependencies(); -console.log('external dependencies:', dependencies); export default defineConfig({ build: { @@ -14,26 +10,12 @@ export default defineConfig({ formats: ['es', 'umd'], fileName: 'index', }, - rollupOptions: { - external: [...dependencies], - }, }, plugins: [ - dts(), //generate typescript typedefs + dts(), // generate typescript typedefs ], resolve: { - preserveSymlinks: true, //https://github.com/vitejs/vite/issues/11657 - }, - test: { - globals: true, - environment: 'jsdom', - setupFiles: './tests/setup.js', + preserveSymlinks: true, // https://github.com/vitejs/vite/issues/11657 }, }); - -function externalDependencies(): Array { - const deps = Object.keys(pkg.dependencies || {}); - const peerDeps = Object.keys(pkg.peerDependencies || {}); - return [...deps, ...peerDeps]; -} diff --git a/packages/react-oidc/.eslintrc.cjs b/packages/react-oidc/.eslintrc.cjs deleted file mode 100644 index 968c0320f..000000000 --- a/packages/react-oidc/.eslintrc.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - extends: [__dirname+'/config/defaultEslintConfig.cjs'], - parserOptions: { - project: './tsconfig.eslint.json', - tsconfigRootDir: __dirname, - }, - rules: { - '@typescript-eslint/naming-convention': [ - 'error', - { - 'selector': 'variable', - 'types': ['boolean'], - 'format': ['PascalCase'], - 'prefix': ['is', 'with', 'should', 'has', 'can', 'did', 'will'] - } - ] - } - } \ No newline at end of file diff --git a/packages/react-oidc/README.md b/packages/react-oidc/README.md index 102b48cfa..f469334c9 100644 --- a/packages/react-oidc/README.md +++ b/packages/react-oidc/README.md @@ -1,635 +1,374 @@ # @axa-fr/react-oidc -[![Continuous Integration](https://github.com/AxaGuilDEv/react-oidc/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/AxaGuilDEv/react-oidc/actions/workflows/npm-publish.yml) -[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=AxaGuilDEv_react-oidc&metric=alert_status)](https://sonarcloud.io/dashboard?id=AxaGuilDEv_react-oidc) [![Reliability](https://sonarcloud.io/api/project_badges/measure?project=AxaGuilDEv_react-oidc&metric=reliability_rating)](https://sonarcloud.io/component_measures?id=AxaGuilDEv_react-oidc&metric=reliability_rating) [![Security](https://sonarcloud.io/api/project_badges/measure?project=AxaGuilDEv_react-oidc&metric=security_rating)](https://sonarcloud.io/component_measures?id=AxaGuilDEv_react-oidc&metric=security_rating) [![Code Corevage](https://sonarcloud.io/api/project_badges/measure?project=AxaGuilDEv_react-oidc&metric=coverage)](https://sonarcloud.io/component_measures?id=AxaGuilDEv_react-oidc&metric=Coverage) [![Twitter](https://img.shields.io/twitter/follow/GuildDEvOpen?style=social)](https://twitter.com/intent/follow?screen_name=GuildDEvOpen) +React components and hooks for OpenID Connect (OIDC), built on [`@axa-fr/oidc-client`](../oidc-client/README.md). The provider handles browser authentication callbacks, session restoration, and token renewal; your components use hooks to sign in, read user information, and call protected APIs. -Try the demo at https://black-rock-0dc6b0d03.1.azurestaticapps.net/ +The library supports Authorization Code Flow with PKCE, multiple named configurations, optional service-worker token isolation, PAR, and DPoP. It is a browser-side authentication library, not a server-side session or authorization system. -![Sample React OIDC](https://github.com/AxaGuilDEv/react-oidc/blob/master/docs/img/introduction.gif?raw=true) +- [Quick start](#quick-start) +- [Protect components](#protect-components) +- [Call protected APIs](#call-protected-apis) +- [User information and token hooks](#user-information-and-token-hooks) +- [Service worker](#service-worker) +- [Configuration, renewal, PAR, and DPoP](#configuration-renewal-par-and-dpop) +- [Custom components and provider options](#custom-components-and-provider-options) +- [Routing and Next.js](#routing-and-nextjs) +- [Named configurations](#named-configurations) +- [Errors and missing providers](#errors-and-missing-providers) +- [Examples and further reading](#examples-and-further-reading) -A set of react components to make OIDC (OpenID Connect) client easy. It aim to simplify OAuth authentication between multiples providers. + -- [About](#about) -- [Getting Started](#getting-started) -- [Run The Demo](#run-the-demo) -- [Examples](#examples) -- [How It Works](#how-it-works) -- [NextJS](#NextJS) -- [Hash route](#Hash-route) -- [Service Worker Support](#service-worker-support) - -## About - -Easy set up of OIDC for react. -It is a real alternative to existing oidc-client libraries. - -- **Secure** : - - With the use of Service Worker, your tokens (refresh_token and access_token) are not accessible to the JavaScript client code (big protection against XSRF attacks) - - OIDC using client side Code Credential Grant with PKCE only -- **Lightweight** -- **Simple** : - - refresh_token and access_token are auto refreshed in background - - with the use of the Service Worker, you do not need to inject the access_token in every fetch, you have only to configure `OidcTrustedDomains.js` file -- **No cookies problem** : You can disable silent signin (that internally use an iframe). For your information, your OIDC server should be in the same domain of your website in order to be able to send OIDC server cookies from your website via an internal IFRAME, else, you may encounter COOKIES problem. -- **Multiple Authentication** : - - You can authenticate many times to the same provider with different scope (for example you can acquire a new 'payment' scope for a payment) - - You can authenticate to multiple different providers inside the same SPA (single page application) website -- **Flexible** : - - Work with Service Worker (more secure) and without for older browser (less secure) - -![](https://github.com/AxaGuilDEv/react-oidc/blob/master/docs/img/schema_pcke_client_side_with_service_worker.png?raw=true) - -The service worker catch **access_token** and **refresh_token** that will never be accessible to the client. - -## Getting Started +## Quick start ```sh -npm install @axa-fr/react-oidc --save - -# If you have a "public" folder, the 2 files will be created : -# ./public/OidcServiceWorker.js <-- will be updated at each "npm install" -# ./public/OidcTrustedDomains.js <-- won't be updated if already exist +npm install @axa-fr/react-oidc ``` -If you need a very secure mode where refresh_token and access_token will be hide behind a service worker that will proxify requests. -The only file you should edit is "OidcTrustedDomains.js". +Register a public browser client with your identity provider using Authorization Code Flow with PKCE. Register the exact callback URL and post-logout URL, and allow the application's origin through CORS. Never embed a client secret in a browser application. -```javascript -// OidcTrustedDomains.js +Replace the issuer and client ID below with your own settings. This example assumes a client-rendered React application with an HTML element named `root`. Your web server must serve the application on `/authentication/callback` as well as `/`. -// Add bellow trusted domains, access tokens will automatically injected to be send to -// trusted domain can also be a path like https://www.myapi.com/users, -// then all subroute like https://www.myapi.com/useers/1 will be authorized to send access_token to. +```tsx +import { useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import { OidcProvider, useOidc, type OidcConfiguration } from '@axa-fr/react-oidc'; -// Domains used by OIDC server must be also declared here -const trustedDomains = { - default: ["https://demo.duendesoftware.com", "https://www.myapi.com/users"], +const configuration: OidcConfiguration = { + client_id: 'your-public-client', + authority: 'https://issuer.example.com', + redirect_uri: `${window.location.origin}/authentication/callback`, + scope: 'openid profile', }; -// Service worker will continue to give access token to the JavaScript client -// Ideal to hide refresh token from client JavaScript, but to retrieve access_token for some -// scenarios which require it. For example, to send it via websocket connection. -trustedDomains.config_show_access_token = { domains : ["https://demo.duendesoftware.com"], showAccessToken: true }; - -``` - -## Run The Demo - -```sh -git clone https://github.com/AxaGuilDEv/react-oidc.git -cd react-oidc/packages/react -npm install -npm start -# then navigate to http://localhost:4200 -``` - -## Examples - -### Application startup - -The library is router agnostic and use native History API. - -The default routes used internally : - -- www.your-app.fr/authentication/callback +function Account(): React.JSX.Element { + const { login, logout, isAuthenticated } = useOidc(); + const [hasError, setHasError] = useState(false); + + const changeSession = async (): Promise => { + setHasError(false); + try { + if (isAuthenticated) { + await logout('/'); + } else { + await login('/'); + } + } catch { + setHasError(true); + } + }; -```javascript -import React from "react"; -import { render } from "react-dom"; -import { BrowserRouter as Router } from "react-router-dom"; -import { OidcProvider } from "@axa-fr/react-oidc"; -import Header from "./Layout/Header"; -import Routes from "./Router"; + return ( +
+

{isAuthenticated ? 'Signed in' : 'Not signed in'}

+ + {hasError &&

Authentication failed. Please try again.

} +
+ ); +} -// This configuration use hybrid mode -// ServiceWorker are used if available (more secure) else tokens are given to the client -// You need to give inside your code the "access_token" when using fetch -const configuration = { - client_id: "interactive.public.short", - redirect_uri: window.location.origin + "/authentication/callback", - silent_redirect_uri: - window.location.origin + "/authentication/silent-callback", - scope: "openid profile email api offline_access", // offline_access scope allow your client to retrieve the refresh_token - authority: "https://demo.duendesoftware.com", - service_worker_relative_url: "/OidcServiceWorker.js", - service_worker_only: false, -}; +const root = document.getElementById('root'); +if (!root) throw new Error('Missing root element'); -const App = () => ( +createRoot(root).render( - -
- - - + + , ); - -render(, document.getElementById("root")); ``` -```javascript -const propTypes = { - loadingComponent: PropTypes.elementType, // you can inject your own loading component - sessionLostComponent: PropTypes.elementType, // you can inject your own session lost component - authenticating: PropTypes.elementType, // you can inject your own authenticationg component - authenticatingErrorComponent: PropTypes.elementType, - callbackSuccessComponent: PropTypes.elementType, // you can inject your own call back success component - serviceWorkerNotSupportedComponent: PropTypes.elementType, // you can inject your page that explain your require a more modern browser - onSessionLost: PropTypes.function, // If set "sessionLostComponent" is not displayed and onSessionLost callback is called instead - configuration: PropTypes.shape({ - client_id: PropTypes.string.isRequired, // oidc client id - redirect_uri: PropTypes.string.isRequired, // oidc redirect url - silent_redirect_uri: PropTypes.string, // Optional activate silent-signin that use cookies between OIDC server and client javascript to restore sessions - silent_login_uri: PropTypes.string, // Optional, route that trigger the signin - silent_login_timeout: PropTypes.number, // Optional default is 12000 milliseconds - scope: PropTypes.string.isRequired, // oidc scope (you need to set "offline_access") - authority: PropTypes.string.isRequired, - storage: Storage, // Default sessionStorage, you can set localStorage but it is less secure to XSS attacks - authority_configuration: PropTypes.shape({ - // Optional for providers that does not implement OIDC server auto discovery via a .wellknowurl - authorization_endpoint: PropTypes.string, - token_endpoint: PropTypes.string, - userinfo_endpoint: PropTypes.string, - end_session_endpoint: PropTypes.string, - revocation_endpoint: PropTypes.string, - check_session_iframe: PropTypes.string, - issuer: PropTypes.string, - }), - refresh_time_before_tokens_expiration_in_second: PropTypes.number, // default is 120 seconds - service_worker_relative_url: PropTypes.string, - service_worker_only: PropTypes.boolean, // default false - service_worker_convert_all_requests_to_cors: PropTypes.boolean, // force all requests that servie worker upgrades to have 'cors' mode. This allows setting authentication token on requests initialted by html parsing(e.g. img tags, download links etc). - extras: StringMap | undefined, // ex: {'prompt': 'consent', 'access_type': 'offline'} list of key/value that are send to the oidc server (more info: https://github.com/openid/AppAuth-JS) - token_request_extras: StringMap | undefined, // ex: {'prompt': 'consent', 'access_type': 'offline'} list of key/value that are send to the oidc server during token request (more info: https://github.com/openid/AppAuth-JS) - withCustomHistory: PropTypes.function, // Override history modification, return instance with replaceState(url, stateHistory) implemented (like History.replaceState()) - authority_time_cache_wellknowurl_in_second: 60 * 60, // Time to cache in second of openid wellknowurl, default is 1 hour - authority_timeout_wellknowurl_in_millisecond: 10000, // Timeout in millisecond of openid wellknowurl, default is 10 seconds, then error is throwed - monitor_session: PropTypes.boolean, // Add OpenId monitor session, default is false (more information https://openid.net/specs/openid-connect-session-1_0.html), if you need to set it to true consider https://infi.nl/nieuws/spa-necromancy/ - onLogoutFromAnotherTab: Function, // Optional, can be set to override the default behavior, this function is triggered when user with the same subject is logged out from another tab when session_monitor is active - onLogoutFromSameTab: Function, // Optional, can be set to override the default behavior, this function is triggered when user is logged out from same tab when session_monitor is active - token_renew_mode: PropTypes.string, // Optional, update tokens base on the selected token(s) lifetime: "access_token_or_id_token_invalid" (default), "access_token_invalid" , "id_token_invalid" - logout_tokens_to_invalidate : Array // Optional tokens to invalidate during logout, default: ['access_token', 'refresh_token'] - }).isRequired, -}; -``` +`OidcProvider` processes the callback route; do not call `loginCallbackAsync()` yourself inside this React setup. `login('/')` selects the application destination after authentication, not the registered callback URL. -## How to consume +This example uses browser storage. For optional token isolation, follow the [service-worker setup](#service-worker). -"useOidc" returns all props from the Hook : +```mermaid +flowchart LR + UI["React components and hooks"] --> Provider["OidcProvider"] + Provider --> Client["@axa-fr/oidc-client"] + Client --> IdP["OIDC identity provider"] + Client --> SW["Optional service worker"] + SW --> API["Trusted API"] + Client --> Fetch["OIDC fetch without worker"] + Fetch --> API +``` -```javascript -import React from "react"; -import { useOidc } from "./oidc"; +## Protect components -export const Home = () => { - const { login, logout, renewTokens, isAuthenticated } = useOidc(); +`OidcSecure` starts login when there is no authenticated session and renders its children only after authentication: +```tsx +import { OidcSecure } from '@axa-fr/react-oidc'; + +export function PrivatePage(): React.JSX.Element { return ( -
-
-
-
Welcome !!!
-

- React Demo Application protected by OpenId Connect -

- {!isAuthenticated && ( - - )} - {isAuthenticated && ( - - )} - {isAuthenticated && ( - - )} -
-
-
+ +

Account

+
); -}; +} ``` -The Hook method exposes : +Place it beneath `OidcProvider`. You can protect the whole application, a route element, or a smaller component. Optional props are `callbackPath`, authorization `extras`, and `configurationName`. -- isAuthenticated : if the user is logged in or not -- logout: logout function (return a promise) -- login: login function 'return a promise' -- renewTokens: renew tokens function 'return a promise' +The higher-order component (HOC) equivalent is: -## How to secure a component +```tsx +import { withOidcSecure } from '@axa-fr/react-oidc'; -`OidcSecure` component trigger authentication in case user is not authenticated. So, the children of that component can be accessible only once you are connected. +function AccountDetails(): React.JSX.Element { + return

Account details

; +} -```javascript -import React from "react"; -import { OidcSecure } from "@axa-fr/react-oidc"; +export const ProtectedAccountDetails = withOidcSecure(AccountDetails, '/account'); +``` -const AdminSecure = () => ( - -

My sub component

} -
-); +Its signature is `withOidcSecure(Component, callbackPath?, extras?, configurationName?)`. -// adding the oidc user in the props -export default AdminSecure; -``` +These components gate the UI; your APIs must independently validate tokens and enforce authorization. -## How to secure a component : HOC method - -"withOidcSecure" act the same as "OidcSecure" it also trigger authentication in case user is not authenticated. - -```javascript -import React from "react"; -import { Switch, Route } from "react-router-dom"; -import { withOidcSecure } from "@axa-fr/react-oidc"; -import Home from "../Pages/Home"; -import Dashboard from "../Pages/Dashboard"; -import Admin from "../Pages/Admin"; - -const Routes = () => ( - - - - - - -); +## Call protected APIs -export default Routes; -``` +`useOidcFetch()` returns a fetch wrapper that attaches the access token (or a service-worker placeholder) and integrates with renewal. Use it only for trusted API URLs, not arbitrary user-supplied destinations. -## How to get "Access Token" : Hook method +```tsx +import { useState } from 'react'; +import { useOidcFetch } from '@axa-fr/react-oidc'; -```javascript -import { useOidcAccessToken } from "@axa-fr/react-oidc"; +export function ProfileRequest(): React.JSX.Element { + const { fetch: oidcFetch } = useOidcFetch(); + const [status, setStatus] = useState('Ready'); -const DisplayAccessToken = () => { - const { accessToken, accessTokenPayload } = useOidcAccessToken(); + const loadProfile = async (): Promise => { + setStatus('Loading…'); + try { + const response = await oidcFetch('https://api.example.com/profile'); + if (!response.ok) { + throw new Error(`API request failed with status ${response.status}`); + } + setStatus('Profile request succeeded'); + } catch { + setStatus('Could not load the profile'); + } + }; - if (!accessToken) { - return

you are not authentified

; - } return ( -
-
-
Access Token
-

- Please consider to configure the ServiceWorker in order to protect - your application from XSRF attacks. ""access_token" and - "refresh_token" will never be accessible from your client side - javascript. -

- {

{JSON.stringify(accessToken)}

} - {accessTokenPayload != null && ( -

{JSON.stringify(accessTokenPayload)}

- )} -
-
+ <> + +

{status}

+ ); -}; +} ``` -## How to get IDToken : Hook method +Render protected API consumers beneath `OidcSecure`, or wait until `useOidc().isAuthenticated` is true. Check `response.ok`: ordinary HTTP error responses do not automatically reject. -```javascript -import { useOidcIdToken } from "@axa-fr/react-oidc"; +| API | Arguments / result | +| --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | +| `useOidcFetch(fetch?, configurationName?, demonstratingProofOfPossession?)` | Returns `{ fetch }`; defaults to browser fetch and configuration `'default'`. | +| `withOidcFetch(fetch?, configurationName?, demonstratingProofOfPossession?)(Component)` | Injects a `fetch` prop into a component. | -const DisplayIdToken = () => { - const { idToken, idTokenPayload } = useOidcIdToken(); +For example, `withOidcFetch()(ProfileComponent)` supplies the same wrapper through props instead of a hook. - if (!idToken) { - return

you are not authentified

; - } +## User information and token hooks - return ( -
-
-
ID Token
- {

{JSON.stringify(idToken)}

} - {idTokenPayload != null && ( -

{JSON.stringify(idTokenPayload)}

- )} -
-
- ); -}; -``` +### User information -## How to get User Information : Hook method +`useOidcUser()` reads the provider's user-info endpoint and exposes loading state. The exported enum is **`OidcUserStatus`**: -```javascript -import { useOidcUser, UserStatus } from "@axa-fr/react-oidc"; +```tsx +import { OidcUserStatus, useOidcUser } from '@axa-fr/react-oidc'; -const DisplayUserInfo = () => { +export function UserGreeting(): React.JSX.Element { const { oidcUser, oidcUserLoadingState } = useOidcUser(); switch (oidcUserLoadingState) { - case UserStatus.Loading: - return

User Information are loading

; - case UserStatus.Unauthenticated: - return

you are not authenticated

; - case UserStatus.LoadingError: - return

Fail to load user information

; + case OidcUserStatus.Loading: + return

Loading profile…

; + case OidcUserStatus.Unauthenticated: + return

Please sign in.

; + case OidcUserStatus.LoadingError: + return

Could not load your profile.

; default: - return ( -
-
-
User information
-

{JSON.stringify(oidcUser)}

-
-
- ); + return

Hello, {oidcUser?.name ?? 'there'}.

; } -}; +} ``` -## How to get a fetch that inject Access_Token : Hook method +The hook also returns `reloadOidcUser()`. Use `useOidcUser(configurationName?, demonstratingProofOfPossession?)` for custom claims, where `MyUserInfo` extends `OidcUserInfo`. -If your are not using the service worker. Fetch function need to send AccessToken. -This Hook give you a wrapped fetch that add the access token for you. +### Hook reference -```javascript -import React, { useEffect, useState } from "react"; -import { useOidcFetch, OidcSecure } from "@axa-fr/react-oidc"; +| Hook | Return values | +| --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| `useOidc(configurationName?)` | `isAuthenticated`, `login`, `logout`, `renewTokens`. | +| `useOidcUser(configurationName?, demonstratingProofOfPossession?)` | `oidcUser`, `oidcUserLoadingState`, `reloadOidcUser`. | +| `useOidcAccessToken(configurationName?)` | `accessToken`, `accessTokenPayload`, and, when enabled, `generateDemonstrationOfProofOfPossessionAsync`. | +| `useOidcIdToken(configurationName?)` | `idToken`, `idTokenPayload`. | -const DisplayUserInfo = ({ fetch }) => { - const [oidcUser, setOidcUser] = useState(null); - const [isLoading, setLoading] = useState(true); +`login(callbackPath?, extras?, silentLoginOnly?, scope?)`, `logout(callbackPath?, extras?)`, and `renewTokens(extras?)` return promises. - useEffect(() => { - const fetchUserInfoAsync = async () => { - const res = await fetch( - "https://demo.duendesoftware.com/connect/userinfo", - ); - if (res.status != 200) { - return null; - } - return res.json(); - }; - let isMounted = true; - fetchUserInfoAsync().then((userInfo) => { - if (isMounted) { - setLoading(false); - setOidcUser(userInfo); - } - }); - return () => { - isMounted = false; - }; - }, []); - - if (isLoading) { - return <>Loading; - } +Prefer `useOidcFetch` over manually building authorization headers. When worker token hiding is enabled, `accessToken` is a placeholder rather than the real token. Do not render or log raw tokens, and treat user claims as personal data. An ID token identifies the user to the client; it is not a replacement for an API access token. - return ( -
-
-
-
User information
- {oidcUser != null && ( -

{JSON.stringify(oidcUser)}

- )} -
-
-
- ); -}; +## Service worker -export const FetchUserHook = () => { - const { fetch } = useOidcFetch(); - return ( - - - - ); -}; +Ensure your application's static-assets directory exists (`public` below), then install the worker assets: + +```sh +node ./node_modules/@axa-fr/react-oidc/bin/copy-service-worker-files.mjs public ``` -## How to get a fetch that inject Access_Token : HOC method +Keep the generated `OidcServiceWorker.js` synchronized with package updates. For example, merge this into your application's `package.json`: -If your are not using the service worker. Fetch function need to send AccessToken. -This HOC give you a wrapped fetch that add the access token for you. +```json +{ + "scripts": { + "postinstall": "node ./node_modules/@axa-fr/react-oidc/bin/copy-service-worker-files.mjs public" + } +} +``` -```javascript -import React, { useEffect, useState } from "react"; -import { useOidcFetch, OidcSecure } from "@axa-fr/react-oidc"; +Then: -const DisplayUserInfo = ({ fetch }) => { - const [oidcUser, setOidcUser] = useState(null); - const [isLoading, setLoading] = useState(true); +1. Configure `public/OidcTrustedDomains.js` with your provider and API destinations. +2. Add `service_worker_relative_url: '/OidcServiceWorker.js'` to the OIDC configuration. +3. Set `service_worker_only: true` if login must not fall back to browser token storage. +4. Serve the worker over HTTPS (or localhost) with a scope that covers the application. - useEffect(() => { - const fetchUserInfoAsync = async () => { - const res = await fetch( - "https://demo.duendesoftware.com/connect/userinfo", - ); - if (res.status != 200) { - return null; - } - return res.json(); - }; - let isMounted = true; - fetchUserInfoAsync().then((userInfo) => { - if (isMounted) { - setLoading(false); - setOidcUser(userInfo); - } - }); - return () => { - isMounted = false; - }; - }, []); - - if (isLoading) { - return <>Loading; - } +Follow the [core service-worker guide](../oidc-client/README.md#service-worker) for a trusted-domain example, all options, token-exposure choices, and fallback behavior. - return ( -
-
-
-
User information
- {oidcUser != null && ( -

{JSON.stringify(oidcUser)}

- )} -
-
-
- ); -}; +**Multi-tab login:** when `allowMultiTabLogin: true` is set in a trusted-domain entry, use `useOidcFetch()` or `withOidcFetch()` for protected API calls. Their tab-specific placeholder identifies the session to the worker. Plain fetch or a default Axios request cannot supply that marker and may result in HTTP 401. -const UserInfoWithFetchHoc = withOidcFetch(fetch)(DisplayUserInfo); -export const FetchUserHoc = () => ( - - - -); -``` +The worker can hide access and refresh tokens from application JavaScript, but it does **not** prevent XSS or stop injected code from making requests through your application. It does not hide all identity information. Keep normal XSS defenses and avoid broad trusted-domain rules. -## Components override - -You can inject your own components. -All components definition receive props `configurationName`. Please checkout the demo for more complete example. - -```javascript -import React from "react"; -import { render } from "react-dom"; -import { BrowserRouter as Router } from "react-router-dom"; -import { OidcProvider } from "@axa-fr/react-oidc"; -import Header from "./Layout/Header"; -import Routes from "./Router"; - -// This configuration use hybrid mode -// ServiceWorker are used if available (more secure) else tokens are given to the client -// You need to give inside your code the "access_token" when using fetch -const configuration = { - client_id: "interactive.public.short", - redirect_uri: "http://localhost:4200/authentication/callback", - silent_redirect_uri: "http://localhost:4200/authentication/silent-callback", - scope: "openid profile email api offline_access", - authority: "https://demo.identityserver.io", - service_worker_relative_url: "/OidcServiceWorker.js", - service_worker_only: false, -}; +## Configuration, renewal, PAR, and DPoP -const Loading = () =>

Loading

; -const AuthenticatingError = () =>

Authenticating error

; -const Authenticating = () =>

Authenticating

; -const SessionLost = () =>

Session Lost

; -const ServiceWorkerNotSupported = () =>

Not supported

; -const CallBackSuccess = () =>

Success

; - -//const [isSessionLost, setIsSessionLost] = useState(false); - -//const onSessionLost = ()=>{ -// setIsSessionLost(true); -//} - -const App = () => ( - - {/* isSessionLost && */} - -
- - - +`OidcProvider` accepts the same `OidcConfiguration` as the vanilla client. Keep provider props, such as custom components and routing callbacks, outside that configuration object. + +- **Configuration:** see the [core configuration reference](../oidc-client/README.md#configuration) for required fields, storage, discovery, timeouts, logout, and session monitoring. +- **Renewal:** automatic renewal is enabled by default. For `TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted`, use `useOidcFetch`/`withOidcFetch` so requests trigger renewal. See [renewal behavior and strict renewal](../oidc-client/README.md#token-renewal). +- **Silent login:** add a distinct `silent_redirect_uri` if required. The provider handles its silent-login routes. Provider and browser cookie policies can prevent iframe login. +- **PAR:** set `par: 'auto'` or `'required'` in `configuration`; the default is `'disabled'`. `auto` uses PAR when an endpoint is advertised; missing required endpoints fail before navigation. Once selected, PAR errors never silently downgrade. See [full PAR semantics, errors, and CORS requirements](../oidc-client/README.md#pushed-authorization-requests-par). +- **DPoP:** enable `demonstrating_proof_of_possession` and use `useOidcFetch(undefined, 'default', true)` or `withOidcFetch(undefined, 'default', true)`. For user info, use `useOidcUser('default', true)`. See [DPoP and worker-specific settings](../oidc-client/README.md#dpop). Both the identity provider and resource server must support it. + +## Custom components and provider options + +Replace built-in status screens with your own components. These are **provider props**, not fields inside `configuration`: + +```tsx +const Loading = (): React.JSX.Element =>

Restoring your session…

; +const AuthenticationError = (): React.JSX.Element => ( +

Sign-in failed. Please return to the sign-in page and try again.

); -render(, document.getElementById("root")); + + +; ``` -## How It Works +| Provider prop | Purpose | +| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `configuration`, `configurationName` | Client configuration and its name (`'default'` by default). | +| `loadingComponent` | Session restoration/loading screen. | +| `loadingTimeoutComponent` | Screen shown when the loading watchdog expires. | +| `authenticatingComponent` | Screen shown while starting login. | +| `authenticatingErrorComponent` | Login or callback failure screen. | +| `callbackSuccessComponent` | Screen shown after a successful callback. | +| `sessionLostComponent` | Session-loss screen. | +| `serviceWorkerNotSupportedComponent` | Unavailable-worker screen when worker-only mode is required. | +| `onSessionLost` | Handles session loss instead of displaying the built-in session-loss flow. | +| `onLogoutFromAnotherTab`, `onLogoutFromSameTab` | React to logout events. | +| `onEvent(configurationName, eventName, data)` | Observe client events; avoid logging complete payloads. | +| `withCustomHistory` | Returns a history adapter with `replaceState(url, stateHistory?)`. | +| `navigateAfterCallback(callbackPath)` | Async callback to perform post-login navigation; takes precedence over the history adapter for this navigation. | +| `getFetch` | Supplies a custom fetch implementation for the underlying client. | +| `location` | Custom `ILOidcLocation` adapter. | -These components encapsulate the use of "@axa-fr/vanilla-oidc" in order to hide workflow complexity. -Internally, native History API is used to be router library agnostic. +Custom status components receive `configurationName`. Set `configuration.loading_timeout_ms` to change the default 30-second loading watchdog, or a nonpositive value to disable it. See [`OidcProviderProps`](./src/OidcProvider.tsx) for the full type. -More information about OIDC +## Routing and Next.js -- [French : Augmentez la sécurité et la simplicité de votre Système d’Information OpenID Connect](https://medium.com/just-tech-it-now/augmentez-la-s%C3%A9curit%C3%A9-et-la-simplicit%C3%A9-de-votre-syst%C3%A8me-dinformation-avec-oauth-2-0-cf0732d71284) -- [English : Increase the security and simplicity of your information system with openid connect](https://medium.com/just-tech-it-now/increase-the-security-and-simplicity-of-your-information-system-with-openid-connect-fa8c26b99d6d) +### Client-side routers -## NextJS +The default navigation uses the browser History API and dispatches `popstate`. Keep `OidcProvider` mounted on callback URLs. Protect route elements with `OidcSecure`; no particular router package is required. -To work with NextJS you need to inject your own history surcharge like the sample below. +For a router-specific integration, provide `navigateAfterCallback` to perform and await the router's navigation, or `withCustomHistory` to replace the default history adapter. These options belong on `OidcProvider`. -**component/layout.js** +The library retains hash-route callback matching for legacy integrations, but OAuth redirect URIs must not contain a fragment. Prefer path-based callback URLs for new deployments, even if the rest of the application uses a hash router. Existing hash-callback setups depend on provider-specific behavior. Interactive and silent callback URLs must be different. -```javascript -import { OidcProvider } from "@axa-fr/react-oidc"; -import { useRouter } from "next/router"; +### Next.js -const configuration = { - client_id: "interactive.public.short", - redirect_uri: "http://localhost:3001/#authentication/callback", - silent_redirect_uri: "http://localhost:3001/#authentication/silent-callback", // Optional activate silent-login that use cookies between OIDC server and client javascript to restore the session - scope: "openid profile email api offline_access", - authority: "https://demo.duendesoftware.com", -}; +Keep this browser library behind a client-only boundary: do not access `window` or initialize the OIDC client while rendering on the server. In the App Router, `'use client'` alone does not disable prerendering; use an appropriate client-only mounting or dynamic-import strategy. -const onEvent = (configurationName, eventName, data) => { - console.log(`oidc:${configurationName}:${eventName}`, data); -}; +The repository's [Next.js demo](../../examples/nextjs-demo/README.md) uses the **Pages Router** and a custom history adapter. For an initialized client-only provider using `next/router`, a post-login navigation adapter can look like this: + +```tsx +import { useRouter } from 'next/router'; +import { OidcProvider } from '@axa-fr/react-oidc'; -export default function Layout({ children }) { +function ClientAuth({ children }: React.PropsWithChildren): React.JSX.Element { const router = useRouter(); - const withCustomHistory = () => { - return { - replaceState: (url) => { - router - .replace({ - pathname: url, - }) - .then(() => { - window.dispatchEvent(new Event("popstate")); - }); - }, - }; - }; return ( - <> - -
{children}
-
- + => { + await router.replace(path); + window.dispatchEvent(new Event('popstate')); + }} + > + {children} + ); } ``` -For more information checkout the [NextJS React OIDC demo](https://github.com/AxaGuilDEv/react-oidc/tree/master/packages/nextjs-demo) +Here `configuration` is your browser-side OIDC configuration. Do not copy `next/router` into an App Router application; adapt to that router's APIs and navigation lifecycle. This package does not provide server-side route protection or a server session. -## Hash route +## Named configurations -`react-oidc` work also with hash router. +Use names to separate identity providers or sessions with different scopes. Give each configuration distinct callback URLs and, for worker mode, a matching trusted-domain entry. -```javascript -export const configurationIdentityServerWithHash = { - client_id: "interactive.public.short", - redirect_uri: window.location.origin + "#authentication-callback", - silent_redirect_uri: - window.location.origin + "#authentication-silent-callback", - scope: "openid profile email api offline_access", - authority: "https://demo.duendesoftware.com", - refresh_time_before_tokens_expiration_in_second: 70, - service_worker_relative_url: "/OidcServiceWorker.js", - service_worker_only: false, -}; +```tsx + + + + + +``` + +Select the name explicitly; nesting does not change the hooks' default name: + +```tsx +const { login, isAuthenticated } = useOidc('payments'); +const { fetch: paymentsFetch } = useOidcFetch(undefined, 'payments'); ``` -## Service Worker Support +Use `` for that session's protected UI. Token and user hooks also accept a configuration name. + +## Errors and missing providers + +`OidcError`, `OidcErrorCode`, `isOidcError`, `OidcStateError`, `OidcStateErrorCode`, `isOidcStateError`, and the PAR error classes/guards are re-exported from this package. Use stable codes and phases, not string matching, for error handling. + +See the [core errors and events guide](../oidc-client/README.md#errors-and-events) for renewal errors, missing/mismatched state, missing nonces, retryability, and recovery. The provider's custom error/session-loss screens and `onEvent` callback are the React integration points. For strict renewal outside a hook, use `OidcClient.getOrThrow(name).renewTokensOrThrowAsync()`. + +When no client has been initialized for a name, `useOidc`, `useOidcUser`, `useOidcAccessToken`, and `useOidcIdToken` warn once per name and return unauthenticated/null defaults. This is useful in tests and Storybook, but does not initialize authentication. + +`OidcClient.get(name)` returns `null` for a missing client; `getOrThrow(name)` fails explicitly. `OidcSecure` and requests made through `useOidcFetch` require initialization and remain fail-fast. Mount a matching provider before using them. + +## Examples and further reading + +- [React demo](../../examples/react-oidc-demo/README.md) — routes, hooks, worker options, and named configurations. +- [Next.js demo](../../examples/nextjs-demo/README.md) — Pages Router integration. +- [Core client guide and API](../oidc-client/README.md). +- [Service-worker protocol](../oidc-client-service-worker/PROTOCOL.md). +- [Service-worker package guide](../oidc-client-service-worker/README.md). +- [FAQ and deployment guidance](../../FAQ.md). -- Firefox : tested on Firefox 98.0.2 -- Chrome/Edge : tested on version upper to 90 -- Opera : tested on version upper to 80 -- Safari : tested on Safari/605.1.15 +The demos use the repository's pnpm workspace. Follow their READMEs for setup rather than installing each package separately. diff --git a/packages/react-oidc/bin/copy-service-worker-files.mjs b/packages/react-oidc/bin/copy-service-worker-files.mjs new file mode 100644 index 000000000..d9fdbf575 --- /dev/null +++ b/packages/react-oidc/bin/copy-service-worker-files.mjs @@ -0,0 +1,76 @@ +/* global console, process */ +/* eslint no-console: "off" */ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +try { + /** + * Script to run after npm install + * + * Copy selected files to user's directory + */ + const script_prefix = 'react-oidc'; + + const copyFile = async (src, dest, overwrite) => { + if (!fileExists(src)) { + console.log(`[${script_prefix}:skip] file does not exist ${src}`); + return false; + } + if (!overwrite) { + if (fileExists(dest)) { + console.log(`[${script_prefix}:skip] file exists not overwriting ${dest}`); + return true; + } + } + await fs.promises.copyFile(src, dest); + console.log(`[${script_prefix}:copy] ${dest}`); + return true; + }; + + const fileExists = path => { + return !!fs.existsSync(path); + }; + + const initPath = process.cwd(); + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const srcDir = path.join(__dirname, '..', '..', 'oidc-client-service-worker', 'dist'); + const srcDirFallback = path.join( + __dirname, + '..', + 'node_modules', + '@axa-fr', + 'oidc-client-service-worker', + 'dist', + ); + const destinationFolder = process.argv.length >= 3 ? process.argv[2] : 'public'; + const destinationDir = path.join(initPath, destinationFolder); + + const files = [ + { + fileName: 'OidcServiceWorker.js', + overwrite: true, + }, + { + fileName: 'OidcTrustedDomains.js', + overwrite: false, + }, + ]; + + for await (const file of files) { + const success = await copyFile( + path.join(srcDir, file.fileName), + path.join(destinationDir, file.fileName), + file.overwrite, + ); + if (!success) { + await copyFile( + path.join(srcDirFallback, file.fileName), + path.join(destinationDir, file.fileName), + file.overwrite, + ); + } + } +} catch (err) { + console.warn(err); +} diff --git a/packages/react-oidc/bin/post-install.js b/packages/react-oidc/bin/post-install.js deleted file mode 100644 index 49f138d49..000000000 --- a/packages/react-oidc/bin/post-install.js +++ /dev/null @@ -1,35 +0,0 @@ -import cpy from 'cpy'; -import path from 'path'; - -/** - * Script to run after npm install - * - * Copy selected files to user's directory - */ - -const initPath = process.env.INIT_CWD; -// console.log('currentdir:', process.cwd()); -// console.log('userPath:', initPath); - -function copyProgress(progress) { - console.log('✓ [react-oidc:copy] ', progress.destinationPath); -} - -const srcDir = '../oidc-client-service-worker/dist/'; -const destinationDir = path.join(initPath, 'public'); - -await cpy([path.join(srcDir,'OidcServiceWorker.js')], destinationDir, { - overwrite: true, -}).on('progress', copyProgress); - -try { - await cpy([path.join(srcDir,'OidcTrustedDomains.js')], destinationDir, { - overwrite: false, - }).on('progress', copyProgress); -} catch (e) { - if (e.code === 'EEXIST') { //file exists - console.log( - `✗ [react-oidc:skip] OidcTrustedDomains.js not copied, already exists in ${destinationDir}` - ); - } else throw e; -} diff --git a/packages/react-oidc/config/defaultEslintConfig.cjs b/packages/react-oidc/config/defaultEslintConfig.cjs deleted file mode 100644 index c3fa61f5b..000000000 --- a/packages/react-oidc/config/defaultEslintConfig.cjs +++ /dev/null @@ -1,148 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - extends: [ - 'standard', - 'plugin:react/recommended', - 'plugin:react-hooks/recommended', - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:import/typescript', - 'plugin:jsx-a11y/recommended', - ], - plugins: ['simple-import-sort', 'testing-library'], - env: { - node: true, - es6: true, - browser: true, - }, - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - // typescript-eslint specific options - warnOnUnsupportedTypeScriptVersion: true, - }, - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': [ - 'error', - { - argsIgnorePattern: '^_|req|res|next|err|ctx|args|context|info', - ignoreRestSiblings: true, - }, - ], - 'no-array-constructor': 'off', - '@typescript-eslint/no-array-constructor': 'warn', - 'no-redeclare': 'off', - '@typescript-eslint/no-redeclare': 'warn', - 'no-use-before-define': 'off', - '@typescript-eslint/no-use-before-define': [ - 'warn', - { - functions: false, - classes: false, - variables: false, - typedefs: false, - }, - ], - 'no-unused-expressions': 'off', - '@typescript-eslint/no-unused-expressions': [ - 'error', - { - allowShortCircuit: true, - allowTernary: true, - allowTaggedTemplates: true, - }, - ], - '@typescript-eslint/triple-slash-reference': 'off', - '@typescript-eslint/member-delimiter-style': [ - 'error', - { - multiline: { - delimiter: 'semi', - requireLast: true, - }, - singleline: { - delimiter: 'semi', - requireLast: false, - }, - }, - ], - camelcase: 'off', - 'comma-dangle': [ - 'error', - { - arrays: 'always-multiline', - objects: 'always-multiline', - imports: 'always-multiline', - exports: 'always-multiline', - functions: 'always-multiline', - }, - ], - 'array-callback-return': 'warn', - 'jsx-quotes': ['error', 'prefer-double'], - // 'max-len': ['error', { code: 120 }], - indent: 'off', - // quotes: ['error', 'single'], - semi: ['error', 'always'], - 'space-before-function-paren': 'off', - - 'import/no-named-as-default': 'off', - 'import/no-named-as-default-member': 'off', - 'import/default': 'off', - 'import/named': 'off', - 'import/namespace': 'off', - 'import/no-unresolved': 'off', - 'simple-import-sort/imports': 'error', - 'simple-import-sort/exports': 'error', - 'react/prop-types': 'off', - 'react/jsx-wrap-multilines': 'error', - 'react/react-in-jsx-scope': 'off', - 'react/display-name': 'off', - // https://github.com/facebook/react/tree/master/packages/eslint-plugin-react-hooks - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'off', - }, - - overrides: [ - { - files: ['*.js', '*.jsx'], - rules: { - '@typescript-eslint/no-var-requires': 'off', - }, - }, - { - // 3) Now we enable eslint-plugin-testing-library rules or preset only for matching files! - files: ['**/?(*.)+(spec|test).[jt]s?(x)'], - extends: ['plugin:testing-library/react'], - rules: { - 'testing-library/await-async-query': 'error', - 'testing-library/no-await-sync-query': 'error', - 'testing-library/no-debugging-utils': 'warn', - 'testing-library/no-dom-import': 'off', - 'testing-library/no-unnecessary-act': 'off', - }, - }, - ], - - settings: { - react: { - version: 'detect', - }, - 'import/parsers': { - '@typescript-eslint/parser': ['.ts', '.tsx'], - }, - 'import/resolver': { - typescript: { - alwaysTryTypes: true, - }, - }, - }, - }; \ No newline at end of file diff --git a/packages/react-oidc/package.json b/packages/react-oidc/package.json index b2d851439..3e2abb445 100644 --- a/packages/react-oidc/package.json +++ b/packages/react-oidc/package.json @@ -1,6 +1,6 @@ { "name": "@axa-fr/react-oidc", - "version": "6.24.1", + "version": "7.29.6", "private": false, "type": "module", "main": "./dist/index.umd.cjs", @@ -9,7 +9,7 @@ "description": "OpenID Connect & OAuth authentication using react", "repository": { "type": "git", - "url": "https://github.com/AxaGuilDEv/react-oidc.git" + "url": "https://github.com/AxaFrance/oidc-client.git" }, "files": [ "dist", @@ -35,46 +35,36 @@ "serve": "vite preview", "test": "vitest --root . --coverage", "clean": "rimraf dist", - "postinstall": "node ./bin/post-install.js", + "postinstall": "echo 'WARNING keep sink OidcServiceWorker.js version file'", "prepare": "pnpm run clean && pnpm run copy-service-worker && pnpm run build", "lint": "eslint src" }, "dependencies": { - "@axa-fr/oidc-client-service-worker": "workspace:*", - "@axa-fr/vanilla-oidc": "workspace:*" + "@axa-fr/oidc-client": "workspace:*", + "@axa-fr/oidc-client-service-worker": "workspace:*" }, "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0", - "react-router-dom": "^6.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "devDependencies": { - "@testing-library/jest-dom": "5.16.5", - "@testing-library/react": "13.3.0", - "@testing-library/user-event": "14.4.3", - "@types/react": "^18.2.15", - "@typescript-eslint/eslint-plugin": "^5.50.0", - "@typescript-eslint/parser": "^5.50.0", - "@vitejs/plugin-react": "4.0.3", - "@vitest/coverage-v8": "^0.33.0", - "cpy": "^10.1.0", - "cpy-cli": "^5.0.0", - "cross-env": "^7.0.3", - "eslint": "^8.26.0", - "eslint-config-standard": "^17.1.0", - "eslint-config-standard-with-typescript": "^36.1.0", - "eslint-import-resolver-typescript": "^3.5.5", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-simple-import-sort": "^10.0.0", - "jsdom": "22.1.0", - "msw": "1.2.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "rimraf": "5.0.1", - "typescript": "5.1.6", - "vite": "^4.4.4", - "vite-plugin-dts": "^3.3.0", - "vitest": "^0.33.0" + "@testing-library/jest-dom": "7.0.1", + "@testing-library/react": "16.3.3", + "@testing-library/user-event": "14.6.7", + "@types/react": "19.3.0", + "@vitejs/plugin-react": "6.1.1", + "@vitest/coverage-v8": "5.0.0", + "cpy": "13.2.3", + "cpy-cli": "^7.0.0", + "cross-env": "^10.1.0", + "jsdom": "30.0.1", + "msw": "2.15.0", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "rimraf": "6.1.3", + "typescript": "npm:@typescript/typescript6@6.0.2", + "vite": "8.3.0", + "vite-plugin-dts": "5.1.0", + "vitest": "5.0.0" }, "license": "MIT", "publishConfig": { diff --git a/packages/react-oidc/public/OidcTrustedDomains.js b/packages/react-oidc/public/OidcTrustedDomains.js index 1aea5cd1a..e72a3d143 100644 --- a/packages/react-oidc/public/OidcTrustedDomains.js +++ b/packages/react-oidc/public/OidcTrustedDomains.js @@ -5,22 +5,25 @@ // Domains used by OIDC server must be also declared here // eslint-disable-next-line @typescript-eslint/no-unused-vars const trustedDomains = { - default: ['https://demo.duendesoftware.com', 'https://kdhttps.auth0.com'], - config_classic: ['https://demo.duendesoftware.com'], - config_without_silent_login: ['https://demo.duendesoftware.com'], - config_without_refresh_token: ['https://demo.duendesoftware.com'], - config_without_refresh_token_silent_login: ['https://demo.duendesoftware.com'], - config_google: ['https://oauth2.googleapis.com', 'https://openidconnect.googleapis.com'], - config_with_hash: ['https://demo.duendesoftware.com'], + default: ['https://demo.duendesoftware.com', 'https://kdhttps.auth0.com'], + config_classic: ['https://demo.duendesoftware.com'], + config_without_silent_login: ['https://demo.duendesoftware.com'], + config_without_refresh_token: ['https://demo.duendesoftware.com'], + config_without_refresh_token_silent_login: ['https://demo.duendesoftware.com'], + config_google: ['https://oauth2.googleapis.com', 'https://openidconnect.googleapis.com'], + config_with_hash: ['https://demo.duendesoftware.com'], }; // Service worker will continue to give access token to the JavaScript client // Ideal to hide refresh token from client JavaScript, but to retrieve access_token for some // scenarios which require it. For example, to send it via websocket connection. -trustedDomains.config_show_access_token = { domains: ['https://demo.duendesoftware.com'], showAccessToken: true }; +trustedDomains.config_show_access_token = { + domains: ['https://demo.duendesoftware.com'], + showAccessToken: true, +}; // This example defines domains used by OIDC server separately from domains to which access tokens will be injected. trustedDomains.config_separate_oidc_access_token_domains = { - oidcDomains: ['https://demo.duendesoftware.com'], - accessTokenDomains: ['https://myapi'], + oidcDomains: ['https://demo.duendesoftware.com'], + accessTokenDomains: ['https://myapi'], }; diff --git a/packages/react-oidc/src/FetchToken.spec.tsx b/packages/react-oidc/src/FetchToken.spec.tsx new file mode 100644 index 000000000..997805679 --- /dev/null +++ b/packages/react-oidc/src/FetchToken.spec.tsx @@ -0,0 +1,135 @@ +import type { Fetch } from '@axa-fr/oidc-client'; +import { renderHook } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { useOidcFetch } from './FetchToken'; + +const { getOrThrow, fetchWithTokens } = vi.hoisted(() => ({ + getOrThrow: vi.fn(), + fetchWithTokens: vi.fn(), +})); + +vi.mock('@axa-fr/oidc-client', () => ({ + OidcClient: { getOrThrow }, +})); + +describe('useOidcFetch', () => { + const originalFetch = vi.fn(); + const authenticatedFetch = vi.fn(); + const response = new Response('response'); + + beforeEach(() => { + vi.resetAllMocks(); + getOrThrow.mockReturnValue({ fetchWithTokens }); + fetchWithTokens.mockReturnValue(authenticatedFetch); + authenticatedFetch.mockResolvedValue(response); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('looks up the default configuration only when a request is made', async () => { + const { result } = renderHook(() => useOidcFetch(originalFetch)); + + expect(getOrThrow).not.toHaveBeenCalled(); + await expect(result.current.fetch('/resource')).resolves.toBe(response); + expect(getOrThrow).toHaveBeenCalledExactlyOnceWith('default'); + expect(fetchWithTokens).toHaveBeenCalledExactlyOnceWith(originalFetch, false); + expect(authenticatedFetch).toHaveBeenCalledExactlyOnceWith('/resource', undefined); + }); + + it('defaults to the browser fetch implementation', async () => { + vi.stubGlobal('fetch', originalFetch); + const { result } = renderHook(() => useOidcFetch()); + + await result.current.fetch('/resource'); + + expect(fetchWithTokens).toHaveBeenCalledExactlyOnceWith(originalFetch, false); + }); + + it.each([new URL('https://example.com/resource'), new Request('https://example.com/resource')])( + 'forwards request objects and options unchanged: %s', + async input => { + const init: RequestInit = { method: 'POST', headers: { 'X-Custom': 'value' }, body: 'body' }; + const { result } = renderHook(() => useOidcFetch(originalFetch, 'custom', true)); + + await expect(result.current.fetch(input, init)).resolves.toBe(response); + + expect(getOrThrow).toHaveBeenCalledWith('custom'); + expect(fetchWithTokens).toHaveBeenCalledWith(originalFetch, true); + expect(authenticatedFetch.mock.calls[0][0]).toBe(input); + expect(authenticatedFetch.mock.calls[0][1]).toBe(init); + }, + ); + + it('uses the current client for each request rather than caching it', async () => { + const nextFetchWithTokens = vi.fn().mockReturnValue(authenticatedFetch); + const { result } = renderHook(() => useOidcFetch(originalFetch)); + await result.current.fetch('/first'); + + getOrThrow.mockReturnValue({ fetchWithTokens: nextFetchWithTokens }); + await result.current.fetch('/second'); + + expect(getOrThrow).toHaveBeenCalledTimes(2); + expect(fetchWithTokens).toHaveBeenCalledTimes(1); + expect(nextFetchWithTokens).toHaveBeenCalledExactlyOnceWith(originalFetch, false); + }); + + it('keeps the callback stable when its dependencies are unchanged', () => { + const { result, rerender } = renderHook(() => useOidcFetch(originalFetch)); + const callback = result.current.fetch; + + rerender(); + + expect(result.current.fetch).toBe(callback); + }); + + it.each([ + { fetch: vi.fn(), configurationName: 'default', isDpop: false }, + { fetch: originalFetch, configurationName: 'custom', isDpop: false }, + { fetch: originalFetch, configurationName: 'default', isDpop: true }, + ])('updates the callback when a dependency changes: %s', async nextProps => { + const { result, rerender } = renderHook( + ({ fetch, configurationName, isDpop }) => useOidcFetch(fetch, configurationName, isDpop), + { initialProps: { fetch: originalFetch, configurationName: 'default', isDpop: false } }, + ); + const callback = result.current.fetch; + + rerender(nextProps); + expect(result.current.fetch).not.toBe(callback); + await result.current.fetch('/resource'); + + expect(getOrThrow).toHaveBeenCalledWith(nextProps.configurationName); + expect(fetchWithTokens).toHaveBeenCalledWith(nextProps.fetch, nextProps.isDpop); + }); + + it('rejects the request rather than throwing synchronously when configuration is missing', async () => { + const error = new Error('Missing configuration'); + getOrThrow.mockImplementation(() => { + throw error; + }); + const { result } = renderHook(() => useOidcFetch(originalFetch)); + + await expect(result.current.fetch('/resource')).rejects.toBe(error); + expect(fetchWithTokens).not.toHaveBeenCalled(); + }); + + it('preserves errors thrown while constructing the authenticated fetch', async () => { + const error = new Error('Cannot construct fetch'); + fetchWithTokens.mockImplementation(() => { + throw error; + }); + const { result } = renderHook(() => useOidcFetch(originalFetch)); + + await expect(result.current.fetch('/resource')).rejects.toBe(error); + }); + + it('preserves rejected fetch errors', async () => { + const error = new Error('Network failure'); + authenticatedFetch.mockRejectedValue(error); + const { result } = renderHook(() => useOidcFetch(originalFetch)); + + await expect(result.current.fetch('/resource')).rejects.toBe(error); + }); +}); diff --git a/packages/react-oidc/src/FetchToken.tsx b/packages/react-oidc/src/FetchToken.tsx index aea6d68e7..204f35eac 100644 --- a/packages/react-oidc/src/FetchToken.tsx +++ b/packages/react-oidc/src/FetchToken.tsx @@ -1,4 +1,4 @@ -import { Fetch, VanillaOidc } from '@axa-fr/vanilla-oidc'; +import { Fetch, OidcClient } from '@axa-fr/oidc-client'; import { useCallback } from 'react'; export interface ComponentWithOidcFetchProps { @@ -7,53 +7,42 @@ export interface ComponentWithOidcFetchProps { const defaultConfigurationName = 'default'; -const fetchWithToken = (fetch: Fetch, getOidcWithConfigurationName: () => VanillaOidc | null) => async (...params: Parameters) => { - const [url, options, ...rest] = params; - const optionTmp = options ? { ...options } : { method: 'GET' }; - - let headers = new Headers(); - if (optionTmp.headers) { - headers = !(optionTmp.headers instanceof Headers) - ? new Headers(optionTmp.headers) - : optionTmp.headers; - } - const oidc = getOidcWithConfigurationName(); - - // @ts-ignore - const getValidToken = await oidc.getValidTokenAsync(); - const accessToken = getValidToken?.tokens?.accessToken; - - if (!headers.has('Accept')) { - headers.set('Accept', 'application/json'); - } - if (accessToken) { - headers.set('Authorization', `Bearer ${accessToken}`); - if (!optionTmp.credentials) { - optionTmp.credentials = 'same-origin'; - } - } - const newOptions = { ...optionTmp, headers }; - return await fetch(url, newOptions, ...rest); -}; - -export const withOidcFetch = (fetch: Fetch = null, configurationName = defaultConfigurationName) => ( - WrappedComponent, -) => (props: ComponentWithOidcFetchProps) => { - const { fetch: newFetch } = useOidcFetch(fetch || props.fetch, configurationName); - return ; -}; - -export const useOidcFetch = (fetch: Fetch = null, configurationName = defaultConfigurationName) => { +export const withOidcFetch = + ( + fetch: Fetch = null, + configurationName = defaultConfigurationName, + demonstratingProofOfPossession: boolean = false, + ) => + WrappedComponent => + (props: ComponentWithOidcFetchProps) => { + const { fetch: newFetch } = useOidcFetch( + fetch || props.fetch, + configurationName, + demonstratingProofOfPossession, + ); + return ; + }; + +export const useOidcFetch = ( + fetch: Fetch = null, + configurationName = defaultConfigurationName, + demonstratingProofOfPossession: boolean = false, +) => { const previousFetch = fetch || window.fetch; - const getOidc = VanillaOidc.get; + // useOidcFetch relies on an initialized OIDC configuration to attach + // the bearer token, so we keep the original fail-fast behaviour. + const getOidc = OidcClient.getOrThrow; const memoizedFetchCallback = useCallback( - (input: RequestInfo | URL, init?: RequestInit) => { - const getOidcWithConfigurationName = () => getOidc(configurationName); - const newFetch = fetchWithToken(previousFetch, getOidcWithConfigurationName); - return newFetch(input, init); + async (input: RequestInfo | URL, init?: RequestInit): Promise => { + const oidc = getOidc(configurationName); + const authenticatedFetch = oidc.fetchWithTokens( + previousFetch, + demonstratingProofOfPossession, + ); + return await authenticatedFetch(input, init); }, - [previousFetch, configurationName], + [previousFetch, configurationName, demonstratingProofOfPossession], ); return { fetch: memoizedFetchCallback }; }; diff --git a/packages/react-oidc/src/OidcProvider.spec.tsx b/packages/react-oidc/src/OidcProvider.spec.tsx new file mode 100644 index 000000000..2aecef6a9 --- /dev/null +++ b/packages/react-oidc/src/OidcProvider.spec.tsx @@ -0,0 +1,454 @@ +import { act, render, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +let mockEventSubscribers: Array<{ id: string; func: (name: string, data: any) => void }> = []; +const mockPublishEvent = vi.fn((eventName, data) => { + mockEventSubscribers.forEach(sub => sub.func(eventName, data)); +}); +const mockSubscribeEvents = vi.fn(func => { + const id = Math.random().toString(); + mockEventSubscribers.push({ id, func }); + return id; +}); +const mockRemoveEventSubscription = vi.fn(id => { + mockEventSubscribers = mockEventSubscribers.filter(e => e.id !== id); +}); + +const mockOidcInstance: { + subscribeEvents: typeof mockSubscribeEvents; + removeEventSubscription: typeof mockRemoveEventSubscription; + publishEvent: typeof mockPublishEvent; + configuration: Record; + tryKeepExistingSessionAsync: ReturnType; + tokens: unknown; +} = { + subscribeEvents: mockSubscribeEvents, + removeEventSubscription: mockRemoveEventSubscription, + publishEvent: mockPublishEvent, + configuration: { + redirect_uri: 'http://localhost/callback', + silent_redirect_uri: 'http://localhost/silent-callback', + silent_login_uri: 'http://localhost/silent-login', + }, + tryKeepExistingSessionAsync: vi.fn().mockResolvedValue(true), + tokens: null, +}; + +vi.mock('@axa-fr/oidc-client', () => ({ + OidcClient: { + getOrCreate: vi.fn(() => () => mockOidcInstance), + get: vi.fn(() => mockOidcInstance), + eventNames: { + service_worker_not_supported_by_browser: 'service_worker_not_supported_by_browser', + token_acquired: 'token_acquired', + logout_from_another_tab: 'logout_from_another_tab', + logout_from_same_tab: 'logout_from_same_tab', + token_renewed: 'token_renewed', + token_timer: 'token_timer', + loginAsync_begin: 'loginAsync_begin', + loginAsync_error: 'loginAsync_error', + loginCallbackAsync_begin: 'loginCallbackAsync_begin', + loginCallbackAsync_end: 'loginCallbackAsync_end', + loginCallbackAsync_error: 'loginCallbackAsync_error', + refreshTokensAsync_begin: 'refreshTokensAsync_begin', + refreshTokensAsync: 'refreshTokensAsync', + refreshTokensAsync_end: 'refreshTokensAsync_end', + refreshTokensAsync_error: 'refreshTokensAsync_error', + refreshTokensAsync_silent_error: 'refreshTokensAsync_silent_error', + tryKeepExistingSessionAsync_begin: 'tryKeepExistingSessionAsync_begin', + tryKeepExistingSessionAsync_end: 'tryKeepExistingSessionAsync_end', + tryKeepExistingSessionAsync_error: 'tryKeepExistingSessionAsync_error', + silentLoginAsync_begin: 'silentLoginAsync_begin', + silentLoginAsync: 'silentLoginAsync', + silentLoginAsync_end: 'silentLoginAsync_end', + silentLoginAsync_error: 'silentLoginAsync_error', + syncTokensAsync_begin: 'syncTokensAsync_begin', + syncTokensAsync_lock_not_available: 'syncTokensAsync_lock_not_available', + syncTokensAsync_end: 'syncTokensAsync_end', + syncTokensAsync_error: 'syncTokensAsync_error', + tokensInvalidAndWaitingActionsToRefresh: 'tokensInvalidAndWaitingActionsToRefresh', + loadingTimeout_error: 'loadingTimeout_error', + }, + }, + OidcLocation: class { + getCurrentHref() { + return 'http://localhost/'; + } + getPath() { + return '/'; + } + open() {} + reload() {} + getOrigin() { + return 'http://localhost'; + } + }, + getFetchDefault: vi.fn(() => fetch), +})); + +vi.mock('./core/routes/OidcRoutes.js', () => ({ + default: ({ children }: any) =>
{children}
, +})); + +import { OidcProvider } from './OidcProvider'; + +describe('OidcProvider loading timeout', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.clearAllMocks(); + mockEventSubscribers = []; + mockOidcInstance.tokens = null; + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + const baseConfiguration = { + client_id: 'test-client', + redirect_uri: 'http://localhost/callback', + silent_redirect_uri: 'http://localhost/silent-callback', + scope: 'openid', + authority: 'http://localhost/authority', + }; + + it('should fire loadingTimeout_error when stuck in initial loading state', async () => { + render( + +
App
+
, + ); + + act(() => { + vi.advanceTimersByTime(100); + }); + + expect(mockPublishEvent).toHaveBeenCalledWith('loadingTimeout_error', { timeoutMs: 100 }); + }); + + it('should fire loadingTimeout_error when stuck in loginAsync_begin state', async () => { + render( + +
App
+
, + ); + + // Simulate loginAsync_begin event + act(() => { + mockEventSubscribers.forEach(sub => sub.func('loginAsync_begin', {})); + }); + + // Reset to track timeout event specifically + mockPublishEvent.mockClear(); + + act(() => { + vi.advanceTimersByTime(200); + }); + + expect(mockPublishEvent).toHaveBeenCalledWith('loadingTimeout_error', { timeoutMs: 200 }); + }); + + it('should render loadingTimeoutComponent when loadingTimeout_error is triggered', async () => { + const CustomTimeoutComponent = () =>
Timeout!
; + + render( + +
App
+
, + ); + + await act(async () => { + vi.advanceTimersByTime(50); + }); + + expect(screen.getByTestId('custom-timeout')).toBeTruthy(); + }); + + it('should NOT fire loadingTimeout_error when loading_timeout_ms is 0 (disabled)', async () => { + render( + +
App
+
, + ); + + act(() => { + vi.advanceTimersByTime(60_000); + }); + + expect(mockPublishEvent).not.toHaveBeenCalledWith('loadingTimeout_error', expect.anything()); + }); + + it('should NOT fire loadingTimeout_error when loading_timeout_ms is negative (disabled)', async () => { + render( + +
App
+
, + ); + + act(() => { + vi.advanceTimersByTime(60_000); + }); + + expect(mockPublishEvent).not.toHaveBeenCalledWith('loadingTimeout_error', expect.anything()); + }); + + it('should NOT fire loadingTimeout_error if provider leaves loading state before deadline', async () => { + render( + +
App
+
, + ); + + // Advance partway + act(() => { + vi.advanceTimersByTime(200); + }); + + // Simulate successful callback (leaving loading state) + act(() => { + mockEventSubscribers.forEach(sub => sub.func('loginCallbackAsync_end', {})); + }); + + mockPublishEvent.mockClear(); + + // Advance past original timeout + act(() => { + vi.advanceTimersByTime(500); + }); + + expect(mockPublishEvent).not.toHaveBeenCalledWith('loadingTimeout_error', expect.anything()); + }); + + it('should use default timeout of 30000ms when loading_timeout_ms is not configured', async () => { + render( + +
App
+
, + ); + + act(() => { + vi.advanceTimersByTime(29_999); + }); + + expect(mockPublishEvent).not.toHaveBeenCalledWith('loadingTimeout_error', expect.anything()); + + act(() => { + vi.advanceTimersByTime(1); + }); + + expect(mockPublishEvent).toHaveBeenCalledWith('loadingTimeout_error', { timeoutMs: 30_000 }); + }); + + it('should NOT fire loadingTimeout_error after a silent session restore when authenticated', async () => { + render( + +
App
+
, + ); + + act(() => { + vi.advanceTimersByTime(100); + }); + + // Simulate a successful silent session restore (no loginCallbackAsync_end is emitted). + act(() => { + mockEventSubscribers.forEach(sub => + sub.func('tryKeepExistingSessionAsync_end', { success: true }), + ); + }); + + mockPublishEvent.mockClear(); + + act(() => { + vi.advanceTimersByTime(10_000); + }); + + expect(mockPublishEvent).not.toHaveBeenCalledWith('loadingTimeout_error', expect.anything()); + }); + + it('should NOT fire loadingTimeout_error on an anonymous route when no session exists', async () => { + render( + +
App
+
, + ); + + act(() => { + mockEventSubscribers.forEach(sub => + sub.func('tryKeepExistingSessionAsync_end', { success: false }), + ); + }); + + mockPublishEvent.mockClear(); + + act(() => { + vi.advanceTimersByTime(300); + }); + + expect(mockPublishEvent).not.toHaveBeenCalledWith('loadingTimeout_error', expect.anything()); + }); + + it('should fire loadingTimeout_error when a protected route starts login after no session was found', async () => { + render( + +
App
+
, + ); + + act(() => { + mockEventSubscribers.forEach(sub => + sub.func('tryKeepExistingSessionAsync_end', { success: false }), + ); + }); + + act(() => { + mockEventSubscribers.forEach(sub => sub.func('loginAsync_begin', {})); + }); + + mockPublishEvent.mockClear(); + + act(() => { + vi.advanceTimersByTime(300); + }); + + expect(mockPublishEvent).toHaveBeenCalledWith('loadingTimeout_error', { timeoutMs: 300 }); + }); + + it('should NOT fire loadingTimeout_error on an anonymous route when session restore fails', async () => { + render( + +
App
+
, + ); + + act(() => { + mockEventSubscribers.forEach(sub => + sub.func('tryKeepExistingSessionAsync_error', { message: 'session restore failed' }), + ); + }); + + mockPublishEvent.mockClear(); + + act(() => { + vi.advanceTimersByTime(300); + }); + + expect(mockPublishEvent).not.toHaveBeenCalledWith('loadingTimeout_error', expect.anything()); + }); + + it('should NOT fire loadingTimeout_error when token_acquired fires before the deadline', async () => { + render( + +
App
+
, + ); + + act(() => { + mockEventSubscribers.forEach(sub => sub.func('token_acquired', {})); + }); + + mockPublishEvent.mockClear(); + + act(() => { + vi.advanceTimersByTime(10_000); + }); + + expect(mockPublishEvent).not.toHaveBeenCalledWith('loadingTimeout_error', expect.anything()); + }); + + it('should NOT fire loadingTimeout_error when token_renewed fires before the deadline', async () => { + render( + +
App
+
, + ); + + act(() => { + mockEventSubscribers.forEach(sub => sub.func('token_renewed', {})); + }); + + mockPublishEvent.mockClear(); + + act(() => { + vi.advanceTimersByTime(10_000); + }); + + expect(mockPublishEvent).not.toHaveBeenCalledWith('loadingTimeout_error', expect.anything()); + }); + + it('should NOT arm the watchdog when the OIDC client mounts already authenticated', async () => { + mockOidcInstance.tokens = { accessToken: 'existing-token' }; + + render( + +
App
+
, + ); + + act(() => { + vi.advanceTimersByTime(60_000); + }); + + expect(mockPublishEvent).not.toHaveBeenCalledWith('loadingTimeout_error', expect.anything()); + }); + + it('should propagate loadingTimeout_error through onEvent callback', async () => { + const onEvent = vi.fn(); + + render( + +
App
+
, + ); + + act(() => { + vi.advanceTimersByTime(100); + }); + + expect(onEvent).toHaveBeenCalledWith('default', 'loadingTimeout_error', { timeoutMs: 100 }); + }); +}); diff --git a/packages/react-oidc/src/OidcProvider.tsx b/packages/react-oidc/src/OidcProvider.tsx index b7ca22e55..4cadba9a4 100644 --- a/packages/react-oidc/src/OidcProvider.tsx +++ b/packages/react-oidc/src/OidcProvider.tsx @@ -1,207 +1,329 @@ -import { Fetch, getFetchDefault, OidcConfiguration, VanillaOidc } from '@axa-fr/vanilla-oidc'; +import { + Fetch, + getFetchDefault, + ILOidcLocation, + OidcClient, + OidcConfiguration, + OidcLocation, +} from '@axa-fr/oidc-client'; import { ComponentType, FC, PropsWithChildren, useEffect, useState } from 'react'; import AuthenticatingError from './core/default-component/AuthenticateError.component.js'; -import { Authenticating, CallBackSuccess, Loading, SessionLost } from './core/default-component/index.js'; +import { + Authenticating, + CallBackSuccess, + Loading, + LoadingTimeout, + SessionLost, +} from './core/default-component/index.js'; import ServiceWorkerNotSupported from './core/default-component/ServiceWorkerNotSupported.component.js'; import OidcRoutes from './core/routes/OidcRoutes.js'; import { CustomHistory } from './core/routes/withRouter.js'; export type oidcContext = { - (name?: string): VanillaOidc; + (name?: string): OidcClient; }; const defaultEventState = { name: '', data: null }; export type OidcProviderProps = { - callbackSuccessComponent?: ComponentType; - sessionLostComponent?: ComponentType; - authenticatingComponent?: ComponentType; - authenticatingErrorComponent?: ComponentType; - loadingComponent?: ComponentType; - serviceWorkerNotSupportedComponent?: ComponentType; - configurationName?: string; - configuration?: OidcConfiguration; - children: any; - onSessionLost?: () => void; - onLogoutFromAnotherTab?: () => void; - onLogoutFromSameTab?: () => void; - withCustomHistory?: () => CustomHistory; - onEvent?: (configuration: string, name: string, data: any) => void; - getFetch?: () => Fetch; + callbackSuccessComponent?: ComponentType; + sessionLostComponent?: ComponentType; + authenticatingComponent?: ComponentType; + authenticatingErrorComponent?: ComponentType; + loadingComponent?: ComponentType; + loadingTimeoutComponent?: ComponentType; + serviceWorkerNotSupportedComponent?: ComponentType; + configurationName?: string; + configuration?: OidcConfiguration; + children: any; + onSessionLost?: () => void; + onLogoutFromAnotherTab?: () => void; + onLogoutFromSameTab?: () => void; + withCustomHistory?: () => CustomHistory; + navigateAfterCallback?: (callbackPath: string) => Promise; + onEvent?: (configuration: string, name: string, data: any) => void; + getFetch?: () => Fetch; + location?: ILOidcLocation; }; export type OidcSessionProps = { - configurationName: string; - loadingComponent: PropsWithChildren; + configurationName: string; + loadingComponent: PropsWithChildren; }; -const OidcSession: FC> = ({ loadingComponent, children, configurationName }) => { - const [isLoading, setIsLoading] = useState(true); - const getOidc = VanillaOidc.get; - const oidc = getOidc(configurationName); - useEffect(() => { - let isMounted = true; - if (oidc) { - oidc.tryKeepExistingSessionAsync().then(() => { - if (isMounted) { - setIsLoading(false); - } - }); +const OidcSession: FC> = ({ + loadingComponent, + children, + configurationName, +}) => { + const [isLoading, setIsLoading] = useState(true); + const getOidc = OidcClient.get; + const oidc = getOidc(configurationName); + useEffect(() => { + let isMounted = true; + if (oidc) { + oidc.tryKeepExistingSessionAsync().then(() => { + if (isMounted) { + setIsLoading(false); } - return () => { - isMounted = false; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [configurationName]); - const LoadingComponent = loadingComponent; - return ( - <> - {isLoading - ? ( - - ) - : ( - <>{children} - )} - - ); + }); + } + return () => { + isMounted = false; + }; + }, [configurationName]); + const LoadingComponent = loadingComponent; + return ( + <>{isLoading ? : <>{children}} + ); }; const Switch = ({ isLoading, loadingComponent, children, configurationName }) => { - const LoadingComponent = loadingComponent; - if (isLoading) { - return {children}; - } - return <>{children}; + const LoadingComponent = loadingComponent; + if (isLoading) { + return {children}; + } + return <>{children}; }; +const DEFAULT_LOADING_TIMEOUT_MS = 30_000; + +/** + * Returns true when an event signals that the OIDC client is no longer + * loading. Restoring the existing session is complete even when no session was + * found: an OidcSecure child will emit `loginAsync_begin` if the current route + * actually requires authentication. + */ +const isLoadingEndSignal = (name: string): boolean => + name === OidcClient.eventNames.token_acquired || + name === OidcClient.eventNames.token_renewed || + name === OidcClient.eventNames.loginCallbackAsync_end || + name === OidcClient.eventNames.tryKeepExistingSessionAsync_end || + name === OidcClient.eventNames.tryKeepExistingSessionAsync_error; + export const OidcProvider: FC> = ({ - children, - configuration, - configurationName = 'default', - callbackSuccessComponent = CallBackSuccess, - authenticatingComponent = Authenticating, - loadingComponent = Loading, - serviceWorkerNotSupportedComponent = ServiceWorkerNotSupported, - authenticatingErrorComponent = AuthenticatingError, - sessionLostComponent = SessionLost, - onSessionLost = null, - onLogoutFromAnotherTab = null, - onLogoutFromSameTab = null, - withCustomHistory = null, - onEvent = null, - getFetch = null, + children, + configuration, + configurationName = 'default', + callbackSuccessComponent = CallBackSuccess, + authenticatingComponent = Authenticating, + loadingComponent = Loading, + loadingTimeoutComponent = LoadingTimeout, + serviceWorkerNotSupportedComponent = ServiceWorkerNotSupported, + authenticatingErrorComponent = AuthenticatingError, + sessionLostComponent = SessionLost, + onSessionLost = null, + onLogoutFromAnotherTab = null, + onLogoutFromSameTab = null, + withCustomHistory = null, + navigateAfterCallback = null, + onEvent = null, + getFetch = null, + location = null, }) => { - const getOidc = (configurationName = 'default') => { - return VanillaOidc.getOrCreate(getFetch ?? getFetchDefault)(configuration, configurationName); + if (configuration && configuration.redirect_uri && configuration.silent_redirect_uri) { + if (configuration.redirect_uri === configuration.silent_redirect_uri) { + throw new Error('redirect_uri and silent_redirect_uri must be different'); + } + } + + const getOidc = (configurationName = 'default') => { + return OidcClient.getOrCreate(getFetch ?? getFetchDefault, location ?? new OidcLocation())( + configuration, + configurationName, + ); + }; + + const loading = false; + const [event, setEvent] = useState(defaultEventState); + const [isLoadingWatchdogActive, setIsLoadingWatchdogActive] = useState(true); + const [currentConfigurationName, setConfigurationName] = useState(configurationName); + + useEffect(() => { + const oidc = getOidc(configurationName); + const newSubscriptionId = oidc.subscribeEvents((name, data) => { + if (onEvent) { + onEvent(configurationName, name, data); + } + }); + return () => { + const previousOidc = getOidc(configurationName); + previousOidc.removeEventSubscription(newSubscriptionId); }; - // eslint-disable-next-line @typescript-eslint/naming-convention - const [loading, setLoading] = useState(true); - const [event, setEvent] = useState(defaultEventState); - const [currentConfigurationName, setConfigurationName] = useState('default'); - - useEffect(() => { - const oidc = getOidc(configurationName); - const newSubscriptionId = oidc.subscribeEvents((name, data) => { - if (onEvent) { - onEvent(configurationName, name, data); - } - }); - return () => { - const previousOidc = getOidc(configurationName); - previousOidc.removeEventSubscription(newSubscriptionId); - }; - }, [configurationName, onEvent]); - - useEffect(() => { - const oidc = getOidc(configurationName); - const newSubscriptionId = oidc.subscribeEvents((name, data) => { - if (name === VanillaOidc.eventNames.refreshTokensAsync_error || name === VanillaOidc.eventNames.syncTokensAsync_error) { - if (onSessionLost != null) { - onSessionLost(); - return; - } - setEvent({ name, data }); - } else if (name === VanillaOidc.eventNames.logout_from_another_tab) { - if (onLogoutFromAnotherTab != null) { - onLogoutFromAnotherTab(); - return; - } - setEvent({ name, data }); - } else if (name === VanillaOidc.eventNames.logout_from_same_tab) { - if (onLogoutFromSameTab != null) { - onLogoutFromSameTab(); - } - // setEvent({name, data}); - } else if (name === VanillaOidc.eventNames.loginAsync_begin || - name === VanillaOidc.eventNames.loginCallbackAsync_end || - name === VanillaOidc.eventNames.loginAsync_error || - name === VanillaOidc.eventNames.loginCallbackAsync_error - ) { - setEvent({ name, data }); - } else if (name === VanillaOidc.eventNames.service_worker_not_supported_by_browser && configuration.service_worker_only === true) { - setEvent({ name, data }); - } - }); - - setConfigurationName(configurationName); - setLoading(false); - return () => { - const previousOidc = getOidc(configurationName); - previousOidc.removeEventSubscription(newSubscriptionId); - setEvent(defaultEventState); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [configuration, configurationName]); - - const SessionLostComponent = sessionLostComponent; - const AuthenticatingComponent = authenticatingComponent; - const LoadingComponent = loadingComponent; - const ServiceWorkerNotSupportedComponent = serviceWorkerNotSupportedComponent; - const AuthenticatingErrorComponent = authenticatingErrorComponent; - - const isLoading = (loading || (currentConfigurationName !== configurationName)); + }, [configurationName, onEvent]); + + useEffect(() => { const oidc = getOidc(configurationName); - const eventName = event.name; - switch (eventName) { - case VanillaOidc.eventNames.service_worker_not_supported_by_browser: - return ( - - ); - case VanillaOidc.eventNames.loginAsync_begin: - return ( - - ); - case VanillaOidc.eventNames.loginAsync_error: - case VanillaOidc.eventNames.loginCallbackAsync_error: - return ( - - ); - case VanillaOidc.eventNames.refreshTokensAsync_error: - case VanillaOidc.eventNames.syncTokensAsync_error: - case VanillaOidc.eventNames.logout_from_another_tab: - return ( - - ); - default: - return ( - - - - {children} - - - - ); + const newSubscriptionId = oidc.subscribeEvents((name, data) => { + if (name === OidcClient.eventNames.loginAsync_begin) { + setIsLoadingWatchdogActive(true); + } else if (isLoadingEndSignal(name)) { + setIsLoadingWatchdogActive(false); + } + if ( + name === OidcClient.eventNames.refreshTokensAsync_error || + name === OidcClient.eventNames.syncTokensAsync_error + ) { + if (onSessionLost != null) { + onSessionLost(); + return; + } + setEvent({ name, data }); + } else if (name === OidcClient.eventNames.logout_from_another_tab) { + if (onLogoutFromAnotherTab != null) { + onLogoutFromAnotherTab(); + return; + } + setEvent({ name, data }); + } else if (name === OidcClient.eventNames.logout_from_same_tab) { + if (onLogoutFromSameTab != null) { + onLogoutFromSameTab(); + } + // setEvent({name, data}); + } else if (name === OidcClient.eventNames.loadingTimeout_error) { + setEvent({ name, data }); + } else if ( + name === OidcClient.eventNames.loginAsync_begin || + name === OidcClient.eventNames.loginCallbackAsync_end || + name === OidcClient.eventNames.loginAsync_error || + name === OidcClient.eventNames.loginCallbackAsync_error + ) { + setEvent({ name, data }); + } else if ( + name === OidcClient.eventNames.service_worker_not_supported_by_browser && + configuration.service_worker_only === true + ) { + setEvent({ name, data }); + } + }); + + queueMicrotask(() => { + setConfigurationName(configurationName); + }); + + return () => { + const previousOidc = getOidc(configurationName); + previousOidc.removeEventSubscription(newSubscriptionId); + setEvent(defaultEventState); + setIsLoadingWatchdogActive(true); + }; + }, [configuration, configurationName]); + + useEffect(() => { + const timeoutMs = configuration?.loading_timeout_ms ?? DEFAULT_LOADING_TIMEOUT_MS; + if (timeoutMs <= 0) { + return; + } + if (!isLoadingWatchdogActive) { + return; + } + const oidcInstance = getOidc(configurationName); + if (oidcInstance?.tokens != null) { + return; } + const isStuck = event.name === '' || event.name === OidcClient.eventNames.loginAsync_begin; + if (!isStuck) { + return; + } + const timeoutId = setTimeout(() => { + getOidc(configurationName).publishEvent(OidcClient.eventNames.loadingTimeout_error, { + timeoutMs, + }); + }, timeoutMs); + return () => clearTimeout(timeoutId); + }, [event.name, isLoadingWatchdogActive, configurationName, configuration]); + + const SessionLostComponent = sessionLostComponent; + const AuthenticatingComponent = authenticatingComponent; + const LoadingComponent = loadingComponent; + const LoadingTimeoutComponent = loadingTimeoutComponent; + const ServiceWorkerNotSupportedComponent = serviceWorkerNotSupportedComponent; + const AuthenticatingErrorComponent = authenticatingErrorComponent; + + const isLoading = loading || currentConfigurationName !== configurationName; + const oidc = getOidc(configurationName); + const eventName = event.name; + switch (eventName) { + case OidcClient.eventNames.service_worker_not_supported_by_browser: + return ( + + + + ); + case OidcClient.eventNames.loginAsync_begin: + return ( + + + + ); + case OidcClient.eventNames.loadingTimeout_error: + return ( + + + + ); + case OidcClient.eventNames.loginAsync_error: + case OidcClient.eventNames.loginCallbackAsync_error: + return ( + + + + ); + case OidcClient.eventNames.refreshTokensAsync_error: + case OidcClient.eventNames.syncTokensAsync_error: + case OidcClient.eventNames.logout_from_another_tab: + return ( + + + + ); + default: + return ( + + + + {children} + + + + ); + } }; export default OidcProvider; diff --git a/packages/react-oidc/src/OidcSecure.spec.tsx b/packages/react-oidc/src/OidcSecure.spec.tsx new file mode 100644 index 000000000..653c57775 --- /dev/null +++ b/packages/react-oidc/src/OidcSecure.spec.tsx @@ -0,0 +1,84 @@ +import { render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { OidcSecure, withOidcSecure } from './OidcSecure'; + +const { getOrThrow, loginAsync } = vi.hoisted(() => ({ + getOrThrow: vi.fn(), + loginAsync: vi.fn(), +})); + +vi.mock('@axa-fr/oidc-client', () => ({ OidcClient: { getOrThrow } })); + +describe('OidcSecure', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('renders children without logging in when tokens are present', () => { + getOrThrow.mockReturnValue({ tokens: {}, loginAsync }); + + render(Protected content); + + expect(screen.getByText('Protected content')).toBeDefined(); + expect(getOrThrow).toHaveBeenCalledWith('default'); + expect(loginAsync).not.toHaveBeenCalled(); + }); + + it('hides children and starts login with the supplied configuration and arguments', () => { + getOrThrow.mockReturnValue({ tokens: null, loginAsync }); + const extras = { prompt: 'login' }; + + render( + + Protected content + , + ); + + expect(screen.queryByText('Protected content')).toBeNull(); + expect(getOrThrow).toHaveBeenCalledWith('custom'); + expect(loginAsync).toHaveBeenCalledExactlyOnceWith('/private', extras); + }); + + it('does not restart login while logout is in progress', () => { + getOrThrow.mockReturnValue({ tokens: null, isLoggingOut: true, loginAsync }); + + render(Protected content); + + expect(screen.queryByText('Protected content')).toBeNull(); + expect(loginAsync).not.toHaveBeenCalled(); + }); + + it('does not repeat login on an unchanged render', () => { + getOrThrow.mockReturnValue({ tokens: null, loginAsync }); + const { rerender } = render(Protected content); + + rerender(Protected content); + + expect(loginAsync).toHaveBeenCalledExactlyOnceWith(null, null); + }); + + it('preserves the fail-fast behavior for a missing configuration', () => { + const error = new Error('Missing configuration'); + getOrThrow.mockImplementation(() => { + throw error; + }); + + expect(() => render(Protected content)).toThrow(error); + }); + + it('passes props through the higher-order component', () => { + getOrThrow.mockReturnValue({ tokens: {}, loginAsync }); + const Component = withOidcSecure( + ({ children }) => {children}, + '/callback', + null, + 'custom', + ); + + render(Wrapped content); + + expect(screen.getByText('Wrapped content')).toBeDefined(); + expect(getOrThrow).toHaveBeenCalledWith('custom'); + }); +}); diff --git a/packages/react-oidc/src/OidcSecure.tsx b/packages/react-oidc/src/OidcSecure.tsx index 0635e2f66..776bf7b6b 100644 --- a/packages/react-oidc/src/OidcSecure.tsx +++ b/packages/react-oidc/src/OidcSecure.tsx @@ -1,32 +1,49 @@ -import { StringMap, VanillaOidc } from '@axa-fr/vanilla-oidc'; +import { OidcClient, StringMap } from '@axa-fr/oidc-client'; import { FC, PropsWithChildren, useEffect } from 'react'; export type OidcSecureProps = { - callbackPath?:string; - extras?:StringMap; - configurationName?: string; + callbackPath?: string; + extras?: StringMap; + configurationName?: string; }; -export const OidcSecure: FC> = ({ children, callbackPath = null, extras = null, configurationName = 'default' }) => { - const getOidc = VanillaOidc.get; - const oidc = getOidc(configurationName); - useEffect(() => { - if (!oidc.tokens) { - oidc.loginAsync(callbackPath, extras); - } - }, [configurationName, callbackPath, extras]); - - if (!oidc.tokens) { - return null; - } - return <>{children}; -}; - -export const withOidcSecure = ( - WrappedComponent: FC>, +export const OidcSecure: FC> = ({ + children, callbackPath = null, extras = null, configurationName = 'default', -) => (props) => { - return ; +}) => { + // Keep the previous fail-fast behaviour: this component cannot + // meaningfully render without an initialized OIDC configuration. + const getOidc = OidcClient.getOrThrow; + const oidc = getOidc(configurationName); + useEffect(() => { + // Skip auto re-login while a logout flow is in progress: in that window + // `tokens` has just been cleared but the browser has not yet navigated to + // the identity provider's end-session endpoint, and starting a new auth + // flow here would race the logout navigation (see issue #1677). + if (!oidc.tokens && !oidc.isLoggingOut) { + oidc.loginAsync(callbackPath, extras); + } + }, [configurationName, callbackPath, extras]); + + if (!oidc.tokens) { + return null; + } + return <>{children}; }; + +export const withOidcSecure = + ( + WrappedComponent: FC>, + callbackPath = null, + extras = null, + configurationName = 'default', + ) => + props => { + return ( + + + + ); + }; diff --git a/packages/react-oidc/src/ReactOidc.spec.tsx b/packages/react-oidc/src/ReactOidc.spec.tsx new file mode 100644 index 000000000..08b9eb876 --- /dev/null +++ b/packages/react-oidc/src/ReactOidc.spec.tsx @@ -0,0 +1,222 @@ +import type { Tokens } from '@axa-fr/oidc-client'; +import { act, renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { useOidc, useOidcAccessToken, useOidcIdToken } from './ReactOidc'; + +const { get, subscribeEvents, removeEventSubscription, generateProof } = vi.hoisted(() => ({ + get: vi.fn(), + subscribeEvents: vi.fn<(listener: (name: string) => void) => string>(), + removeEventSubscription: vi.fn(), + generateProof: vi.fn(), +})); + +vi.mock('@axa-fr/oidc-client', () => ({ + OidcClient: { + get, + eventNames: { + token_renewed: 'token_renewed', + token_acquired: 'token_acquired', + logout_from_another_tab: 'logout_from_another_tab', + logout_from_same_tab: 'logout_from_same_tab', + refreshTokensAsync_error: 'refreshTokensAsync_error', + syncTokensAsync_error: 'syncTokensAsync_error', + }, + }, +})); + +const initialTokens = { + accessToken: 'access-example', + accessTokenPayload: { sub: 'user' }, + idToken: 'id-example', + idTokenPayload: { sub: 'user' }, +}; + +const client = { + tokens: null as Pick< + Tokens, + 'accessToken' | 'accessTokenPayload' | 'idToken' | 'idTokenPayload' + > | null, + configuration: { demonstrating_proof_of_possession: false }, + subscribeEvents, + removeEventSubscription, + generateDemonstrationOfProofOfPossessionAsync: generateProof, + loginAsync: vi.fn(), + logoutAsync: vi.fn(), + renewTokensAsync: vi.fn(), +}; + +beforeEach(() => { + vi.resetAllMocks(); + client.tokens = { ...initialTokens }; + client.configuration.demonstrating_proof_of_possession = false; + get.mockReturnValue(client); + subscribeEvents.mockReturnValue('subscription'); +}); + +describe.each([ + { + name: 'access token', + useToken: useOidcAccessToken, + initial: { + accessToken: initialTokens.accessToken, + accessTokenPayload: initialTokens.accessTokenPayload, + generateDemonstrationOfProofOfPossessionAsync: null, + }, + empty: { accessToken: null, accessTokenPayload: null }, + }, + { + name: 'ID token', + useToken: useOidcIdToken, + initial: { + idToken: initialTokens.idToken, + idTokenPayload: initialTokens.idTokenPayload, + }, + empty: { idToken: null, idTokenPayload: null }, + }, +])('$name hook', ({ useToken, initial, empty }) => { + it('initializes from the configured client', () => { + const { result } = renderHook(() => useToken('custom')); + + expect(result.current).toEqual(initial); + expect(get).toHaveBeenCalledWith('custom'); + }); + + it('uses the empty state when the client has no tokens', () => { + client.tokens = null; + const { result } = renderHook(() => useToken()); + + expect(result.current).toEqual(empty); + }); + + it.each([ + 'token_renewed', + 'token_acquired', + 'logout_from_another_tab', + 'logout_from_same_tab', + 'refreshTokensAsync_error', + 'syncTokensAsync_error', + ])('refreshes token state on %s', eventName => { + const { result } = renderHook(() => useToken()); + const listener = subscribeEvents.mock.calls[0][0]; + + client.tokens = null; + act(() => listener(eventName)); + expect(result.current).toEqual(empty); + + client.tokens = { ...initialTokens }; + act(() => listener(eventName)); + expect(result.current).toEqual(initial); + }); + + it('ignores unrelated events', () => { + const { result } = renderHook(() => useToken()); + client.tokens = null; + + act(() => subscribeEvents.mock.calls[0][0]('unrelated')); + + expect(result.current).toEqual(initial); + }); + + it('cleans up subscriptions on configuration changes and unmount', () => { + subscribeEvents.mockReturnValueOnce('first').mockReturnValueOnce('second'); + const { rerender, unmount } = renderHook(({ name }) => useToken(name), { + initialProps: { name: 'first' }, + }); + + rerender({ name: 'second' }); + expect(removeEventSubscription).toHaveBeenCalledExactlyOnceWith('first'); + expect(subscribeEvents).toHaveBeenCalledTimes(2); + expect(get).toHaveBeenLastCalledWith('second'); + + unmount(); + expect(removeEventSubscription.mock.calls).toEqual([['first'], ['second']]); + }); +}); + +describe('access token proof generation', () => { + it('uses the initial access token when generating a proof', async () => { + client.configuration.demonstrating_proof_of_possession = true; + generateProof.mockResolvedValue('proof'); + const { result } = renderHook(() => useOidcAccessToken()); + + await expect( + result.current.generateDemonstrationOfProofOfPossessionAsync( + 'https://api.example.com', + 'GET', + ), + ).resolves.toBe('proof'); + expect(generateProof).toHaveBeenCalledExactlyOnceWith( + 'access-example', + 'https://api.example.com', + 'GET', + ); + }); + + it('uses refreshed tokens and forwards proof extras after a token event', async () => { + client.configuration.demonstrating_proof_of_possession = true; + const { result } = renderHook(() => useOidcAccessToken()); + client.tokens = { ...initialTokens, accessToken: 'renewed-example' }; + act(() => subscribeEvents.mock.calls[0][0]('token_renewed')); + const extras = { nonce: 'example-nonce' }; + + await result.current.generateDemonstrationOfProofOfPossessionAsync( + 'https://api.example.com', + 'POST', + extras, + ); + + expect(generateProof).toHaveBeenCalledExactlyOnceWith( + 'renewed-example', + 'https://api.example.com', + 'POST', + extras, + ); + }); +}); + +describe('useOidc', () => { + it('tracks authentication on login and logout events', () => { + client.tokens = null; + const { result } = renderHook(() => useOidc('custom')); + const listener = subscribeEvents.mock.calls[0][0]; + expect(result.current.isAuthenticated).toBe(false); + + client.tokens = { ...initialTokens }; + act(() => listener('token_acquired')); + expect(result.current.isAuthenticated).toBe(true); + + client.tokens = null; + act(() => listener('logout_from_same_tab')); + expect(result.current.isAuthenticated).toBe(false); + }); + + it('forwards login and logout arguments and returns their promises', () => { + const loginPromise = Promise.resolve(); + const logoutPromise = Promise.resolve(); + client.loginAsync.mockReturnValue(loginPromise); + client.logoutAsync.mockReturnValue(logoutPromise); + const { result } = renderHook(() => useOidc('custom')); + const extras = { prompt: 'login' }; + + expect(result.current.login('/callback', extras, true, 'openid')).toBe(loginPromise); + expect(client.loginAsync).toHaveBeenCalledExactlyOnceWith( + '/callback', + extras, + false, + 'openid', + true, + ); + expect(result.current.logout('/logout', extras)).toBe(logoutPromise); + expect(client.logoutAsync).toHaveBeenCalledExactlyOnceWith('/logout', extras); + }); + + it('returns renewed token data without adding internal fields', async () => { + client.renewTokensAsync.mockResolvedValue({ ...initialTokens, internal: 'not exposed' }); + const { result } = renderHook(() => useOidc()); + const extras = { scope: 'openid' }; + + await expect(result.current.renewTokens(extras)).resolves.toEqual(initialTokens); + expect(client.renewTokensAsync).toHaveBeenCalledExactlyOnceWith(extras); + }); +}); diff --git a/packages/react-oidc/src/ReactOidc.tsx b/packages/react-oidc/src/ReactOidc.tsx index 367c4d085..12a7e0f92 100644 --- a/packages/react-oidc/src/ReactOidc.tsx +++ b/packages/react-oidc/src/ReactOidc.tsx @@ -1,168 +1,232 @@ -import { StringMap, VanillaOidc } from '@axa-fr/vanilla-oidc'; +import { OidcClient, StringMap, Tokens } from '@axa-fr/oidc-client'; import { useEffect, useState } from 'react'; +import { warnMissingConfigurationOnce } from './warnMissingConfiguration.js'; + const defaultConfigurationName = 'default'; type GetOidcFn = { - (configurationName?: string): any; -} + (configurationName?: string): any; +}; -const defaultIsAuthenticated = (getOidc: GetOidcFn, configurationName: string) => { - let isAuthenticated = false; - const oidc = getOidc(configurationName); - if (oidc) { - isAuthenticated = getOidc(configurationName).tokens != null; - } - return isAuthenticated; +const defaultIsAuthenticated = (getOidc: GetOidcFn, configurationName: string): boolean => { + const oidc = getOidc(configurationName); + return oidc ? oidc.tokens != null : false; }; +const isTokenStateEvent = (name: string): boolean => + name === OidcClient.eventNames.token_renewed || + name === OidcClient.eventNames.token_acquired || + name === OidcClient.eventNames.logout_from_another_tab || + name === OidcClient.eventNames.logout_from_same_tab || + name === OidcClient.eventNames.refreshTokensAsync_error || + name === OidcClient.eventNames.syncTokensAsync_error; + export const useOidc = (configurationName = defaultConfigurationName) => { - const getOidc = VanillaOidc.get; - const [isAuthenticated, setIsAuthenticated] = useState(defaultIsAuthenticated(getOidc, configurationName)); - - useEffect(() => { - let isMounted = true; - const oidc = getOidc(configurationName); - setIsAuthenticated(defaultIsAuthenticated(getOidc, configurationName)); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const newSubscriptionId = oidc.subscribeEvents((name: string, data: any) => { - if (name === VanillaOidc.eventNames.logout_from_another_tab || name === VanillaOidc.eventNames.logout_from_same_tab || name === VanillaOidc.eventNames.token_aquired) { - if (isMounted) { - setIsAuthenticated(defaultIsAuthenticated(getOidc, configurationName)); - } - } - }); - return () => { - isMounted = false; - oidc.removeEventSubscription(newSubscriptionId); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [configurationName]); - - const login = (callbackPath:string | undefined = undefined, extras:StringMap = null, silentLoginOnly = false) => { - return getOidc(configurationName).loginAsync(callbackPath, extras, false, undefined, silentLoginOnly); - }; - const logout = (callbackPath: string | null | undefined = undefined, extras:StringMap = null) => { - return getOidc(configurationName).logoutAsync(callbackPath, extras); + const getOidc = OidcClient.get; + const [isAuthenticated, setIsAuthenticated] = useState(() => + defaultIsAuthenticated(getOidc, configurationName), + ); + + useEffect(() => { + let isMounted = true; + const oidc = getOidc(configurationName); + // Hooks may be rendered outside of an (issue #1679): + // in that case `oidc` is null and we simply skip event subscription. + if (!oidc) { + warnMissingConfigurationOnce(configurationName); + return undefined; + } + + const newSubscriptionId = oidc.subscribeEvents((name: string, data: any) => { + if ( + name === OidcClient.eventNames.logout_from_another_tab || + name === OidcClient.eventNames.logout_from_same_tab || + name === OidcClient.eventNames.token_acquired + ) { + if (isMounted) { + setIsAuthenticated(defaultIsAuthenticated(getOidc, configurationName)); + } + } + }); + return () => { + isMounted = false; + oidc.removeEventSubscription(newSubscriptionId); }; - const renewTokens = async (extras: StringMap = null) : Promise => { - const tokens = await getOidc(configurationName).renewTokensAsync(extras); - - return { - // @ts-ignore - accessToken: tokens.accessToken, - // @ts-ignore - accessTokenPayload: tokens.accessTokenPayload, - // @ts-ignore - idToken: tokens.idToken, - // @ts-ignore - idTokenPayload: tokens.idTokenPayload, - }; + }, [configurationName]); + + const login = ( + callbackPath: string | undefined = undefined, + extras: StringMap | undefined = undefined, + silentLoginOnly = false, + scope: string = undefined, + ): Promise => { + const oidc = getOidc(configurationName); + if (!oidc) { + warnMissingConfigurationOnce(configurationName); + return Promise.resolve(); + } + return oidc.loginAsync(callbackPath, extras, false, scope, silentLoginOnly); + }; + const logout = ( + callbackPath: string | null | undefined = undefined, + extras: StringMap | undefined = undefined, + ): Promise => { + const oidc = getOidc(configurationName); + if (!oidc) { + warnMissingConfigurationOnce(configurationName); + return Promise.resolve(); + } + return oidc.logoutAsync(callbackPath, extras); + }; + const renewTokens = async ( + extras: StringMap | undefined = undefined, + ): Promise => { + const oidc = getOidc(configurationName); + if (!oidc) { + warnMissingConfigurationOnce(configurationName); + return { accessToken: null, accessTokenPayload: null, idToken: null, idTokenPayload: null }; + } + const tokens = await oidc.renewTokensAsync(extras); + + return { + // @ts-ignore + accessToken: tokens.accessToken, + // @ts-ignore + accessTokenPayload: tokens.accessTokenPayload, + // @ts-ignore + idToken: tokens.idToken, + // @ts-ignore + idTokenPayload: tokens.idTokenPayload, }; - return { login, logout, renewTokens, isAuthenticated }; + }; + return { login, logout, renewTokens, isAuthenticated }; }; const accessTokenInitialState = { accessToken: null, accessTokenPayload: null }; const initTokens = (configurationName: string) => { - const getOidc = VanillaOidc.get; - const oidc = getOidc(configurationName); - if (oidc.tokens) { - const tokens = oidc.tokens; - return { - accessToken: tokens.accessToken, - accessTokenPayload: tokens.accessTokenPayload, - }; - } + const getOidc = OidcClient.get; + const oidc = getOidc(configurationName); + if (!oidc) { return accessTokenInitialState; + } + if (oidc.tokens) { + const tokens = oidc.tokens; + return { + accessToken: tokens.accessToken, + accessTokenPayload: tokens.accessTokenPayload, + generateDemonstrationOfProofOfPossessionAsync: oidc.configuration + .demonstrating_proof_of_possession + ? (url: string, method: string) => + oidc.generateDemonstrationOfProofOfPossessionAsync(tokens.accessToken, url, method) + : null, + }; + } + return accessTokenInitialState; }; export type OidcAccessToken = { - accessToken?: any; - accessTokenPayload?: any; + accessToken?: any; + accessTokenPayload?: any; + generateDemonstrationOfProofOfPossessionAsync?: any; +}; + +function getGenerateDemonstrationOfProofOfPossessionAsync(oidc: OidcClient, tokens: Tokens) { + return oidc.configuration.demonstrating_proof_of_possession + ? (url: string, method: string, extras: StringMap = {}) => + oidc.generateDemonstrationOfProofOfPossessionAsync(tokens.accessToken, url, method, extras) + : null; } export const useOidcAccessToken = (configurationName = defaultConfigurationName) => { - const getOidc = VanillaOidc.get; - const [state, setAccessToken] = useState(initTokens(configurationName)); - - useEffect(() => { - let isMounted = true; - const oidc = getOidc(configurationName); - if (oidc.tokens) { - const tokens = oidc.tokens; - setAccessToken({ accessToken: tokens.accessToken, accessTokenPayload: tokens.accessTokenPayload }); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const newSubscriptionId = oidc.subscribeEvents((name: string, data: any) => { - if (name === VanillaOidc.eventNames.token_renewed || - name === VanillaOidc.eventNames.token_aquired || - name === VanillaOidc.eventNames.logout_from_another_tab || - name === VanillaOidc.eventNames.logout_from_same_tab || - name === VanillaOidc.eventNames.refreshTokensAsync_error || - name === VanillaOidc.eventNames.syncTokensAsync_error) { - if (isMounted) { - const tokens = oidc.tokens; - setAccessToken(tokens != null ? { accessToken: tokens.accessToken, accessTokenPayload: tokens.accessTokenPayload } : accessTokenInitialState); + const getOidc = OidcClient.get; + const [state, setAccessToken] = useState(() => initTokens(configurationName)); + + useEffect(() => { + let isMounted = true; + const oidc = getOidc(configurationName); + // No provider in the tree (issue #1679): keep the default token state. + if (!oidc) { + warnMissingConfigurationOnce(configurationName); + return undefined; + } + + const newSubscriptionId = oidc.subscribeEvents((name: string, data: any) => { + if (isTokenStateEvent(name)) { + if (isMounted) { + const tokens = oidc.tokens; + setAccessToken( + tokens != null + ? { + accessToken: tokens.accessToken, + accessTokenPayload: tokens.accessTokenPayload, + generateDemonstrationOfProofOfPossessionAsync: + getGenerateDemonstrationOfProofOfPossessionAsync(oidc, tokens), } - } - }); - return () => { - isMounted = false; - oidc.removeEventSubscription(newSubscriptionId); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [configurationName]); - return state; + : accessTokenInitialState, + ); + } + } + }); + return () => { + isMounted = false; + oidc.removeEventSubscription(newSubscriptionId); + }; + }, [configurationName]); + return state; }; const idTokenInitialState = { idToken: null, idTokenPayload: null }; const initIdToken = (configurationName: string) => { - const getOidc = VanillaOidc.get; - const oidc = getOidc(configurationName); - if (oidc.tokens) { - const tokens = oidc.tokens; - return { idToken: tokens.idToken, idTokenPayload: tokens.idTokenPayload }; - } + const getOidc = OidcClient.get; + const oidc = getOidc(configurationName); + + if (!oidc) { return idTokenInitialState; + } + if (oidc.tokens) { + const tokens = oidc.tokens; + return { idToken: tokens.idToken, idTokenPayload: tokens.idTokenPayload }; + } + return idTokenInitialState; }; export type OidcIdToken = { - idToken?: any; - idTokenPayload?: any; -} + idToken?: any; + idTokenPayload?: any; +}; export const useOidcIdToken = (configurationName = defaultConfigurationName) => { - const getOidc = VanillaOidc.get; - const [state, setIDToken] = useState(initIdToken(configurationName)); - - useEffect(() => { - let isMounted = true; - const oidc = getOidc(configurationName); - if (oidc.tokens) { - const tokens = oidc.tokens; - setIDToken({ idToken: tokens.idToken, idTokenPayload: tokens.idTokenPayload }); + const getOidc = OidcClient.get; + const [state, setIDToken] = useState(() => initIdToken(configurationName)); + + useEffect(() => { + let isMounted = true; + const oidc = getOidc(configurationName); + // No provider in the tree (issue #1679): keep the default id-token state. + if (!oidc) { + warnMissingConfigurationOnce(configurationName); + return undefined; + } + + const newSubscriptionId = oidc.subscribeEvents((name: string, data: any) => { + if (isTokenStateEvent(name)) { + if (isMounted) { + const tokens = oidc.tokens; + setIDToken( + tokens != null + ? { idToken: tokens.idToken, idTokenPayload: tokens.idTokenPayload } + : idTokenInitialState, + ); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const newSubscriptionId = oidc.subscribeEvents((name: string, data: any) => { - if (name === VanillaOidc.eventNames.token_renewed || - name === VanillaOidc.eventNames.token_aquired || - name === VanillaOidc.eventNames.logout_from_another_tab || - name === VanillaOidc.eventNames.logout_from_same_tab || - name === VanillaOidc.eventNames.refreshTokensAsync_error || - name === VanillaOidc.eventNames.syncTokensAsync_error) { - if (isMounted) { - const tokens = oidc.tokens; - setIDToken(tokens != null ? { idToken: tokens.idToken, idTokenPayload: tokens.idTokenPayload } : idTokenInitialState); - } - } - }); - return () => { - isMounted = false; - oidc.removeEventSubscription(newSubscriptionId); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [configurationName]); - return state; + } + }); + return () => { + isMounted = false; + oidc.removeEventSubscription(newSubscriptionId); + }; + }, [configurationName]); + return state; }; diff --git a/packages/react-oidc/src/User.spec.tsx b/packages/react-oidc/src/User.spec.tsx new file mode 100644 index 000000000..c174b1b7d --- /dev/null +++ b/packages/react-oidc/src/User.spec.tsx @@ -0,0 +1,90 @@ +import { act, renderHook, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { OidcUserStatus, useOidcUser } from './User'; + +const mockSubscribeEvents = vi.fn().mockReturnValue('subscription-id'); +const mockRemoveEventSubscription = vi.fn(); +const mockUserInfo = vi.fn(); +const mockUserInfoAsync = vi.fn(); + +vi.mock('@axa-fr/oidc-client', () => ({ + OidcClient: { + get: vi.fn(() => ({ + tokens: { access_token: 'test-token' }, + userInfo: mockUserInfo, + userInfoAsync: mockUserInfoAsync, + subscribeEvents: mockSubscribeEvents, + removeEventSubscription: mockRemoveEventSubscription, + })), + eventNames: { + logout_from_another_tab: 'logout_from_another_tab', + logout_from_same_tab: 'logout_from_same_tab', + }, + }, +})); + +describe('useOidcUser', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockSubscribeEvents.mockReturnValue('subscription-id'); + mockRemoveEventSubscription.mockReset(); + }); + + it('should load user information and update status correctly on reload', async () => { + const userInfo = { sub: 'user1', name: 'Test User' }; + // Initial state: no cached user + mockUserInfo.mockReturnValue(null); + // userInfoAsync resolves with user info + mockUserInfoAsync.mockResolvedValue(userInfo); + + const { result } = renderHook(() => useOidcUser()); + + // Initially unauthenticated since userInfo returns null + await waitFor(() => { + expect(result.current.oidcUserLoadingState).toBe(OidcUserStatus.Unauthenticated); + }); + + // Simulate the user being authenticated (tokens present) + mockUserInfo.mockReturnValue(userInfo); + + // Trigger reload + act(() => { + result.current.reloadOidcUser(); + }); + + // After reload, should eventually show loaded state (not stay stuck at loading) + await waitFor(() => { + expect(result.current.oidcUserLoadingState).toBe(OidcUserStatus.Loaded); + }); + + expect(result.current.oidcUser).toEqual(userInfo); + }); + + it('should not get stuck at Loading state after reloadOidcUser is called', async () => { + const initialUser = { sub: 'user1', name: 'Initial User' }; + const reloadedUser = { sub: 'user1', name: 'Reloaded User' }; + + // User is already authenticated + mockUserInfo.mockReturnValue(initialUser); + // userInfoAsync resolves with new data + mockUserInfoAsync.mockResolvedValue(reloadedUser); + + const { result } = renderHook(() => useOidcUser()); + + // Initial state: user loaded from cache + expect(result.current.oidcUserLoadingState).toBe(OidcUserStatus.Loaded); + + // Trigger reload + act(() => { + result.current.reloadOidcUser(); + }); + + // Should complete the reload and show the reloaded user (not stuck at Loading) + await waitFor(() => { + expect(result.current.oidcUser).toEqual(reloadedUser); + }); + + expect(result.current.oidcUserLoadingState).toBe(OidcUserStatus.Loaded); + }); +}); diff --git a/packages/react-oidc/src/User.ts b/packages/react-oidc/src/User.ts index 4311e0c58..00cd25cc8 100644 --- a/packages/react-oidc/src/User.ts +++ b/packages/react-oidc/src/User.ts @@ -1,45 +1,89 @@ -import { type OidcUserInfo, VanillaOidc } from '@axa-fr/vanilla-oidc'; -import { useEffect, useState } from 'react'; +import { OidcClient, type OidcUserInfo } from '@axa-fr/oidc-client'; +import { useEffect, useRef, useState } from 'react'; + +import { warnMissingConfigurationOnce } from './warnMissingConfiguration.js'; export enum OidcUserStatus { - Unauthenticated= 'Unauthenticated', - Loading = 'Loading user', - Loaded = 'User loaded', - LoadingError = 'Error loading user' + Unauthenticated = 'Unauthenticated', + Loading = 'Loading user', + Loaded = 'User loaded', + LoadingError = 'Error loading user', } export type OidcUser = { - user: T; - status: OidcUserStatus; -} + user: T | null; + status: OidcUserStatus; +}; -export const useOidcUser = (configurationName = 'default') => { - const [oidcUser, setOidcUser] = useState>({ user: null, status: OidcUserStatus.Unauthenticated }); - const [oidcUserId, setOidcUserId] = useState(''); +export const useOidcUser = ( + configurationName = 'default', + demonstrating_proof_of_possession = false, +) => { + const oidc = OidcClient.get(configurationName); + // When the hook is used outside of an (issue #1679), + // `OidcClient.get` returns null instead of throwing. + const user = oidc ? oidc.userInfo() : null; + const [oidcUser, setOidcUser] = useState>({ + user: user, + status: user ? OidcUserStatus.Loaded : OidcUserStatus.Unauthenticated, + }); + const [oidcUserId, setOidcUserId] = useState(user ? 1 : 0); + const oidcPreviousUserIdRef = useRef(user ? 1 : 0); - const oidc = VanillaOidc.get(configurationName); - useEffect(() => { - let isMounted = true; - if (oidc && oidc.tokens) { - setOidcUser({ ...oidcUser, status: OidcUserStatus.Loading }); - const isNoCache = oidcUserId !== ''; - oidc.userInfoAsync(isNoCache) - .then((info) => { - if (isMounted) { - // @ts-ignore - setOidcUser({ user: info, status: OidcUserStatus.Loaded }); - } - }) - .catch(() => setOidcUser({ ...oidcUser, status: OidcUserStatus.LoadingError })); - } else { - setOidcUser({ user: null, status: OidcUserStatus.Unauthenticated }); + useEffect(() => { + const oidc = OidcClient.get(configurationName); + let isMounted = true; + if (!oidc) { + warnMissingConfigurationOnce(configurationName); + return undefined; + } + if (oidc.tokens) { + const isCache = oidcUserId === oidcPreviousUserIdRef.current; + if (isCache && oidc.userInfo()) { + return; + } + oidcPreviousUserIdRef.current = oidcUserId; + // Use queueMicrotask to defer setState to avoid synchronous call in effect + queueMicrotask(() => { + if (isMounted) { + setOidcUser({ ...oidcUser, status: OidcUserStatus.Loading }); } - return () => { isMounted = false; }; - }, [oidcUserId]); - - const reloadOidcUser = () => { - setOidcUserId(oidcUserId + ' '); + }); + oidc + .userInfoAsync(!isCache, demonstrating_proof_of_possession) + .then(info => { + if (isMounted) { + // @ts-ignore + setOidcUser({ user: info, status: OidcUserStatus.Loaded }); + } + }) + .catch(() => setOidcUser({ ...oidcUser, status: OidcUserStatus.LoadingError })); + } else { + queueMicrotask(() => { + if (isMounted) { + setOidcUser({ user: null, status: OidcUserStatus.Unauthenticated }); + } + }); + } + const newSubscriptionId = oidc.subscribeEvents((name: string) => { + if ( + name === OidcClient.eventNames.logout_from_another_tab || + name === OidcClient.eventNames.logout_from_same_tab + ) { + if (isMounted) { + setOidcUser({ user: null, status: OidcUserStatus.Unauthenticated }); + } + } + }); + return () => { + isMounted = false; + oidc.removeEventSubscription(newSubscriptionId); }; + }, [oidcUserId, configurationName, demonstrating_proof_of_possession]); + + const reloadOidcUser = () => { + setOidcUserId(oidcUserId + 1); + }; - return { oidcUser: oidcUser.user, oidcUserLoadingState: oidcUser.status, reloadOidcUser }; + return { oidcUser: oidcUser.user, oidcUserLoadingState: oidcUser.status, reloadOidcUser }; }; diff --git a/packages/react-oidc/src/core/default-component/Authenticating.component.tsx b/packages/react-oidc/src/core/default-component/Authenticating.component.tsx index 5b286a95b..772d4f483 100644 --- a/packages/react-oidc/src/core/default-component/Authenticating.component.tsx +++ b/packages/react-oidc/src/core/default-component/Authenticating.component.tsx @@ -1,6 +1,6 @@ import { ComponentType } from 'react'; -const Authenticating : ComponentType = () => ( +const Authenticating: ComponentType = () => (

Authentication in progress

diff --git a/packages/react-oidc/src/core/default-component/Callback.component.spec.tsx b/packages/react-oidc/src/core/default-component/Callback.component.spec.tsx new file mode 100644 index 000000000..372289aec --- /dev/null +++ b/packages/react-oidc/src/core/default-component/Callback.component.spec.tsx @@ -0,0 +1,249 @@ +import { act, render, waitFor } from '@testing-library/react'; +import React from 'react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import CallbackManager, { CallBackSuccess, verifyNavigationCommitted } from './Callback.component'; + +vi.mock('@axa-fr/oidc-client', () => ({ + OidcClient: { + get: vi.fn(), + getOrThrow: vi.fn(), + eventNames: { + loginCallbackAsync_navigated: 'loginCallbackAsync_navigated', + loginCallbackAsync_navigation_error: 'loginCallbackAsync_navigation_error', + }, + }, +})); + +vi.mock('../routes/withRouter.js', () => ({ + getCustomHistory: vi.fn(), +})); + +import { OidcClient } from '@axa-fr/oidc-client'; + +import { getCustomHistory } from '../routes/withRouter.js'; + +describe('verifyNavigationCommitted', () => { + it('should return true when current path matches target path', () => { + const windowMock = { location: { pathname: '/dashboard' } } as Window; + expect(verifyNavigationCommitted('/dashboard', windowMock)).toBe(true); + }); + + it('should return false when current path does not match target path', () => { + const windowMock = { location: { pathname: '/callback' } } as Window; + expect(verifyNavigationCommitted('/dashboard', windowMock)).toBe(false); + }); + + it('should return true when target path is root', () => { + const windowMock = { location: { pathname: '/anything' } } as Window; + expect(verifyNavigationCommitted('/', windowMock)).toBe(true); + }); +}); + +describe('CallBackSuccess', () => { + it('renders the success message', () => { + const { getByText } = render(); + expect(getByText('Authentication complete')).toBeTruthy(); + expect(getByText('You will be redirected to your application.')).toBeTruthy(); + }); +}); + +describe('CallbackManager', () => { + let mockPublishEvent: ReturnType; + let mockLoginCallbackAsync: ReturnType; + let mockReplaceState: ReturnType; + + beforeEach(() => { + vi.useFakeTimers(); + mockPublishEvent = vi.fn(); + mockLoginCallbackAsync = vi.fn(); + mockReplaceState = vi.fn(); + + (OidcClient.getOrThrow as ReturnType).mockReturnValue({ + loginCallbackAsync: mockLoginCallbackAsync, + publishEvent: mockPublishEvent, + }); + + (getCustomHistory as ReturnType).mockReturnValue({ + replaceState: mockReplaceState, + }); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.clearAllMocks(); + }); + + it('should use navigateAfterCallback when provided and emit navigated event on success', async () => { + mockLoginCallbackAsync.mockResolvedValue({ callbackPath: '/dashboard' }); + const navigateAfterCallback = vi.fn().mockResolvedValue(undefined); + + await act(async () => { + render( + , + ); + }); + + expect(navigateAfterCallback).toHaveBeenCalledWith('/dashboard'); + expect(mockPublishEvent).toHaveBeenCalledWith('loginCallbackAsync_navigated', { + configurationName: 'default', + callbackPath: '/dashboard', + }); + }); + + it('should emit navigation_error event when navigateAfterCallback rejects', async () => { + mockLoginCallbackAsync.mockResolvedValue({ callbackPath: '/dashboard' }); + const navError = new Error('Navigation failed'); + const navigateAfterCallback = vi.fn().mockRejectedValue(navError); + + await act(async () => { + render( + , + ); + }); + + expect(mockPublishEvent).toHaveBeenCalledWith('loginCallbackAsync_navigation_error', { + configurationName: 'default', + callbackPath: '/dashboard', + error: navError, + }); + }); + + it('should render error component when navigateAfterCallback fails', async () => { + mockLoginCallbackAsync.mockResolvedValue({ callbackPath: '/dashboard' }); + const navigateAfterCallback = vi.fn().mockRejectedValue(new Error('fail')); + + const ErrorComponent = () =>
Error occurred
; + + let container; + await act(async () => { + const result = render( + , + ); + container = result.container; + }); + + expect(container.textContent).toContain('Error occurred'); + }); + + it('should use default history navigation when navigateAfterCallback is not provided', async () => { + vi.useRealTimers(); + mockLoginCallbackAsync.mockResolvedValue({ callbackPath: '/dashboard' }); + + // Mock window.location to simulate successful navigation + Object.defineProperty(window, 'location', { + value: { pathname: '/dashboard' }, + writable: true, + }); + + await act(async () => { + render(); + // Wait for the verification delay to pass + await new Promise(resolve => setTimeout(resolve, 300)); + }); + + expect(mockReplaceState).toHaveBeenCalledWith('/dashboard'); + + await waitFor(() => { + expect(mockPublishEvent).toHaveBeenCalledWith('loginCallbackAsync_navigated', { + configurationName: 'default', + callbackPath: '/dashboard', + }); + }); + }); + + it('should emit navigation_error when default navigation does not commit', async () => { + vi.useRealTimers(); + mockLoginCallbackAsync.mockResolvedValue({ callbackPath: '/dashboard' }); + + // Mock window.location to simulate failed navigation + Object.defineProperty(window, 'location', { + value: { pathname: '/callback' }, + writable: true, + }); + + await act(async () => { + render(); + // Wait for the verification delay to pass + await new Promise(resolve => setTimeout(resolve, 300)); + }); + + await waitFor(() => { + expect(mockPublishEvent).toHaveBeenCalledWith( + 'loginCallbackAsync_navigation_error', + expect.objectContaining({ + configurationName: 'default', + callbackPath: '/dashboard', + }), + ); + }); + }); + + it('should use "/" as fallback when callbackPath is empty', async () => { + mockLoginCallbackAsync.mockResolvedValue({ callbackPath: '' }); + const navigateAfterCallback = vi.fn().mockResolvedValue(undefined); + + await act(async () => { + render( + , + ); + }); + + expect(navigateAfterCallback).toHaveBeenCalledWith('/'); + }); + + it('should use withCustomHistory when provided and no navigateAfterCallback', async () => { + mockLoginCallbackAsync.mockResolvedValue({ callbackPath: '/profile' }); + const customReplaceState = vi.fn(); + const withCustomHistory = vi.fn().mockReturnValue({ replaceState: customReplaceState }); + + Object.defineProperty(window, 'location', { + value: { pathname: '/' }, + writable: true, + }); + + await act(async () => { + render(); + }); + + expect(withCustomHistory).toHaveBeenCalled(); + expect(customReplaceState).toHaveBeenCalledWith('/profile'); + }); + + it('should set error state when loginCallbackAsync throws', async () => { + mockLoginCallbackAsync.mockRejectedValue(new Error('login error')); + + const ErrorComponent = () =>
Login Error
; + + let container; + await act(async () => { + const result = render( + , + ); + container = result.container; + }); + + expect(container.textContent).toContain('Login Error'); + }); + + it('should render success component by default', () => { + mockLoginCallbackAsync.mockReturnValue(new Promise(() => {})); // never resolves + + const { getByText } = render(); + + expect(getByText('Authentication complete')).toBeTruthy(); + }); +}); diff --git a/packages/react-oidc/src/core/default-component/Callback.component.tsx b/packages/react-oidc/src/core/default-component/Callback.component.tsx index 612d326b8..dc40c13e4 100644 --- a/packages/react-oidc/src/core/default-component/Callback.component.tsx +++ b/packages/react-oidc/src/core/default-component/Callback.component.tsx @@ -1,31 +1,97 @@ -import { VanillaOidc } from '@axa-fr/vanilla-oidc'; +import { OidcClient } from '@axa-fr/oidc-client'; import { ComponentType, useEffect, useState } from 'react'; import { getCustomHistory } from '../routes/withRouter.js'; import AuthenticatingError from './AuthenticateError.component.js'; -export const CallBackSuccess: ComponentType = () => (
-
-

Authentication complete

-

You will be redirected to your application.

+export const CallBackSuccess: ComponentType = () => ( +
+
+

Authentication complete

+

You will be redirected to your application.

+
-
); +); -const CallbackManager: ComponentType = ({ callBackError, callBackSuccess, configurationName, withCustomHistory }) => { +const NAVIGATION_VERIFICATION_DELAY_MS = 200; + +export const verifyNavigationCommitted = ( + targetPath: string, + windowInternal: Window = window, +): boolean => { + const currentPath = windowInternal.location.pathname; + return currentPath === targetPath || targetPath === '/'; +}; + +const CallbackManager: ComponentType = ({ + callBackError, + callBackSuccess, + configurationName, + withCustomHistory, + navigateAfterCallback, +}) => { const [isError, setIsError] = useState(false); useEffect(() => { let isMounted = true; const playCallbackAsync = async () => { - const getOidc = VanillaOidc.get; + const getOidc = OidcClient.getOrThrow; try { - const { callbackPath } = await getOidc(configurationName).loginCallbackAsync(); - const history = (withCustomHistory) ? withCustomHistory() : getCustomHistory(); - history.replaceState(callbackPath || '/'); - } catch (error) { + const oidcClient = getOidc(configurationName); + const { callbackPath } = await oidcClient.loginCallbackAsync(); + const targetPath = callbackPath || '/'; + + if (navigateAfterCallback) { + try { + await navigateAfterCallback(targetPath); + oidcClient.publishEvent(OidcClient.eventNames.loginCallbackAsync_navigated, { + configurationName, + callbackPath: targetPath, + }); + } catch (navigationError) { + oidcClient.publishEvent(OidcClient.eventNames.loginCallbackAsync_navigation_error, { + configurationName, + callbackPath: targetPath, + error: navigationError, + }); + if (isMounted) { + console.warn(navigationError); + setIsError(true); + } + } + } else { + const history = withCustomHistory ? withCustomHistory() : getCustomHistory(); + history.replaceState(targetPath); + + await new Promise(resolve => { + setTimeout(() => { + resolve(); + }, NAVIGATION_VERIFICATION_DELAY_MS); + }); + if (isMounted) { - console.warn(error); - setIsError(true); + const committed = verifyNavigationCommitted(targetPath); + if (committed) { + oidcClient.publishEvent(OidcClient.eventNames.loginCallbackAsync_navigated, { + configurationName, + callbackPath: targetPath, + }); + } else { + oidcClient.publishEvent(OidcClient.eventNames.loginCallbackAsync_navigation_error, { + configurationName, + callbackPath: targetPath, + error: new Error( + `Navigation did not commit: expected "${targetPath}" but found "${window.location.pathname}"`, + ), + }); + setIsError(true); + } } + } + } catch (error) { + if (isMounted) { + console.warn(error); + setIsError(true); + } } }; playCallbackAsync(); diff --git a/packages/react-oidc/src/core/default-component/Loading.component.tsx b/packages/react-oidc/src/core/default-component/Loading.component.tsx index e404ade19..a86b68a89 100644 --- a/packages/react-oidc/src/core/default-component/Loading.component.tsx +++ b/packages/react-oidc/src/core/default-component/Loading.component.tsx @@ -1,9 +1,5 @@ import { ComponentType } from 'react'; -const Loading : ComponentType = () => ( - - Loading - -); +const Loading: ComponentType = () => Loading; export default Loading; diff --git a/packages/react-oidc/src/core/default-component/LoadingTimeout.component.tsx b/packages/react-oidc/src/core/default-component/LoadingTimeout.component.tsx new file mode 100644 index 000000000..97e04f481 --- /dev/null +++ b/packages/react-oidc/src/core/default-component/LoadingTimeout.component.tsx @@ -0,0 +1,14 @@ +import { ComponentType } from 'react'; + +const LoadingTimeout: ComponentType = () => ( +
+
+

Loading timeout

+

+ Authentication is taking longer than expected. Please try refreshing the page. +

+
+
+); + +export default LoadingTimeout; diff --git a/packages/react-oidc/src/core/default-component/ServiceWorkerNotSupported.component.tsx b/packages/react-oidc/src/core/default-component/ServiceWorkerNotSupported.component.tsx index 49c499f1b..649a97237 100644 --- a/packages/react-oidc/src/core/default-component/ServiceWorkerNotSupported.component.tsx +++ b/packages/react-oidc/src/core/default-component/ServiceWorkerNotSupported.component.tsx @@ -1,10 +1,13 @@ import { ComponentType } from 'react'; -const ServiceWorkerNotSupported : ComponentType = () => ( +const ServiceWorkerNotSupported: ComponentType = () => (

Unable to authenticate on this browser

-

Your browser is not secure enough to make authentication work. Try updating your browser or use a newer browser.

+

+ Your browser is not secure enough to make authentication work. Try updating your browser or + use a newer browser. +

); diff --git a/packages/react-oidc/src/core/default-component/SessionLost.component.tsx b/packages/react-oidc/src/core/default-component/SessionLost.component.tsx index 25e4c889f..c2f4ee348 100644 --- a/packages/react-oidc/src/core/default-component/SessionLost.component.tsx +++ b/packages/react-oidc/src/core/default-component/SessionLost.component.tsx @@ -5,7 +5,7 @@ export const SessionLost: ComponentType = () => (

Session timed out

- Your session has expired. Please re-authenticate. + Your session has expired. Please re-authenticate.

diff --git a/packages/react-oidc/src/core/default-component/SilentCallback.component.tsx b/packages/react-oidc/src/core/default-component/SilentCallback.component.tsx index dcef5baa4..f19b9a261 100644 --- a/packages/react-oidc/src/core/default-component/SilentCallback.component.tsx +++ b/packages/react-oidc/src/core/default-component/SilentCallback.component.tsx @@ -1,17 +1,23 @@ -import { VanillaOidc } from '@axa-fr/vanilla-oidc'; -import { ComponentType, useEffect } from 'react'; +import { OidcClient } from '@axa-fr/oidc-client'; +import { FC, useEffect } from 'react'; -const SilentCallbackManager: ComponentType = ({ configurationName }) => { - useEffect(() => { - const playCallbackAsync = async () => { - const getOidc = VanillaOidc.get; - const oidc = getOidc(configurationName); - oidc.silentLoginCallbackAsync(); - }; - playCallbackAsync(); - }, []); +export interface SilentCallbackProps { + configurationName: string; +} - return <>; +const SilentCallbackManager: FC = ({ configurationName }) => { + useEffect(() => { + const playCallbackAsync = async () => { + const oidc = OidcClient.getOrThrow(configurationName); + oidc.silentLoginCallbackAsync(); + }; + + playCallbackAsync().catch(error => { + console.error('Error during silent login callback:', error); + }); + }, [configurationName]); + + return null; }; export default SilentCallbackManager; diff --git a/packages/react-oidc/src/core/default-component/SilentLogin.component.spec.tsx b/packages/react-oidc/src/core/default-component/SilentLogin.component.spec.tsx new file mode 100644 index 000000000..17bb10da8 --- /dev/null +++ b/packages/react-oidc/src/core/default-component/SilentLogin.component.spec.tsx @@ -0,0 +1,71 @@ +import { render } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import SilentLogin from './SilentLogin.component'; + +const { getOrThrow, getParseQueryStringFromLocation, loginAsync } = vi.hoisted(() => ({ + getOrThrow: vi.fn(), + getParseQueryStringFromLocation: vi.fn(), + loginAsync: vi.fn(), +})); + +vi.mock('@axa-fr/oidc-client', () => ({ + OidcClient: { getOrThrow }, + getParseQueryStringFromLocation, +})); + +describe('silent login', () => { + beforeEach(() => { + vi.resetAllMocks(); + getOrThrow.mockReturnValue({ tokens: null, loginAsync }); + }); + + it.each([{}, { state: 'state' }, { state: 'state', scope: 'openid' }])( + 'passes null extras when the query contains only reserved parameters: %s', + query => { + getParseQueryStringFromLocation.mockReturnValue(query); + + render(); + + expect(getOrThrow).toHaveBeenCalledWith('custom'); + expect(loginAsync).toHaveBeenCalledExactlyOnceWith(null, null, true, query.scope); + }, + ); + + it('forwards non-reserved parameters without changing the query data', () => { + const query = Object.freeze({ + state: 'state', + scope: 'openid profile', + prompt: 'none', + login_hint: 'example', + }); + getParseQueryStringFromLocation.mockReturnValue(query); + + render(); + + expect(loginAsync).toHaveBeenCalledExactlyOnceWith( + null, + { prompt: 'none', login_hint: 'example' }, + true, + 'openid profile', + ); + }); + + it('does not start login when already authenticated', () => { + getParseQueryStringFromLocation.mockReturnValue({}); + getOrThrow.mockReturnValue({ tokens: {}, loginAsync }); + + render(); + + expect(loginAsync).not.toHaveBeenCalled(); + }); + + it('only starts silent login on the initial mount', () => { + getParseQueryStringFromLocation.mockReturnValue({ prompt: 'none' }); + const { rerender } = render(); + + rerender(); + + expect(loginAsync).toHaveBeenCalledOnce(); + }); +}); diff --git a/packages/react-oidc/src/core/default-component/SilentLogin.component.tsx b/packages/react-oidc/src/core/default-component/SilentLogin.component.tsx index f12290bbd..8baebbd97 100644 --- a/packages/react-oidc/src/core/default-component/SilentLogin.component.tsx +++ b/packages/react-oidc/src/core/default-component/SilentLogin.component.tsx @@ -1,31 +1,29 @@ -import { getParseQueryStringFromLocation, VanillaOidc } from '@axa-fr/vanilla-oidc'; +import { getParseQueryStringFromLocation, OidcClient } from '@axa-fr/oidc-client'; import { ComponentType, useEffect } from 'react'; const SilentLogin: ComponentType = ({ configurationName }) => { - const queryParams = getParseQueryStringFromLocation(window.location.href); + const queryParams = getParseQueryStringFromLocation(window.location.href); - const getOidc = VanillaOidc.get; - const oidc = getOidc(configurationName); + const getOidc = OidcClient.getOrThrow; + const oidc = getOidc(configurationName); - let extras = null; + let extras = null; - for (const [key, value] of Object.entries(queryParams)) { - if (key === 'state' || key === 'scope') { - continue; - } - if (extras === null) { - extras = {}; - } - extras[key] = value; + for (const [key, value] of Object.entries(queryParams)) { + if (key === 'state' || key === 'scope') { + continue; } + extras ??= {}; + extras[key] = value; + } - useEffect(() => { - if (!oidc.tokens) { - oidc.loginAsync(null, extras, true, queryParams.scope); - } - }, []); + useEffect(() => { + if (!oidc.tokens) { + oidc.loginAsync(null, extras, true, queryParams.scope); + } + }, []); - return <>; + return <>; }; export default SilentLogin; diff --git a/packages/react-oidc/src/core/default-component/index.ts b/packages/react-oidc/src/core/default-component/index.ts index 7a441ad7c..e42898e17 100644 --- a/packages/react-oidc/src/core/default-component/index.ts +++ b/packages/react-oidc/src/core/default-component/index.ts @@ -2,5 +2,6 @@ export { default as AuthenticateError } from './AuthenticateError.component.js'; export { default as Authenticating } from './Authenticating.component.js'; export { default as Callback, CallBackSuccess } from './Callback.component.js'; export { default as Loading } from './Loading.component.js'; +export { default as LoadingTimeout } from './LoadingTimeout.component.js'; export { default as ServiceWorkerNotSupported } from './ServiceWorkerNotSupported.component.js'; export { default as SessionLost } from './SessionLost.component.js'; diff --git a/packages/react-oidc/src/core/routes/OidcRoutes.behavior.spec.tsx b/packages/react-oidc/src/core/routes/OidcRoutes.behavior.spec.tsx new file mode 100644 index 000000000..22084db43 --- /dev/null +++ b/packages/react-oidc/src/core/routes/OidcRoutes.behavior.spec.tsx @@ -0,0 +1,123 @@ +import { act, render, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import Callback from '../default-component/Callback.component'; +import SilentCallback from '../default-component/SilentCallback.component'; +import SilentLogin from '../default-component/SilentLogin.component'; +import OidcRoutes from './OidcRoutes'; + +vi.mock('../default-component/Callback.component', () => ({ + default: vi.fn(() => Callback), +})); +vi.mock('../default-component/SilentCallback.component', () => ({ + default: vi.fn(() => Silent callback), +})); +vi.mock('../default-component/SilentLogin.component', () => ({ + default: vi.fn(() => Silent login), +})); + +const props = { + configurationName: 'custom', + redirect_uri: new URL('/callback', window.location.href).href, + silent_redirect_uri: new URL('/silent-callback', window.location.href).href, + silent_login_uri: new URL('/silent-login', window.location.href).href, + location: { + getCurrentHref: () => window.location.href, + getPath: () => window.location.pathname, + getOrigin: () => window.location.origin, + open: vi.fn(), + reload: vi.fn(), + }, +}; + +describe('OIDC route selection', () => { + beforeEach(() => { + vi.clearAllMocks(); + window.history.replaceState(null, '', '/'); + }); + + afterEach(() => { + window.history.replaceState(null, '', '/'); + vi.restoreAllMocks(); + }); + + it('renders application content on a non-OIDC route', () => { + render(Application); + + expect(screen.getByText('Application')).toBeDefined(); + expect(Callback).not.toHaveBeenCalled(); + expect(SilentCallback).not.toHaveBeenCalled(); + expect(SilentLogin).not.toHaveBeenCalled(); + }); + + it.each([ + ['/callback?code=example#', 'Callback', Callback], + ['/silent-callback', 'Silent callback', SilentCallback], + ['/silent-login', 'Silent login', SilentLogin], + ])('renders the matching handler for %s', (path, text, handler) => { + window.history.replaceState(null, '', path); + + render(Application); + + expect(screen.getByText(text)).toBeDefined(); + expect(screen.queryByText('Application')).toBeNull(); + expect(vi.mocked(handler).mock.calls[0][0]).toMatchObject({ configurationName: 'custom' }); + }); + + it('gives the silent callback priority when configured paths overlap', () => { + window.history.replaceState(null, '', '/callback'); + + render( + , + ); + + expect(screen.getByText('Silent callback')).toBeDefined(); + expect(Callback).not.toHaveBeenCalled(); + expect(SilentLogin).not.toHaveBeenCalled(); + }); + + it('forwards custom callback components and navigation callbacks unchanged', () => { + window.history.replaceState(null, '', '/callback'); + const success = (): null => null; + const error = (): null => null; + const history = vi.fn(); + const navigate = vi.fn(); + + render( + , + ); + + expect(vi.mocked(Callback).mock.calls[0][0]).toMatchObject({ + callBackSuccess: success, + callBackError: error, + withCustomHistory: history, + navigateAfterCallback: navigate, + }); + }); + + it('updates the route on popstate and removes its listener on unmount', () => { + const addEventListener = vi.spyOn(window, 'addEventListener'); + const removeEventListener = vi.spyOn(window, 'removeEventListener'); + const { unmount } = render(Application); + const listener = addEventListener.mock.calls.find(([name]) => name === 'popstate')[1]; + + act(() => { + window.history.replaceState(null, '', '/silent-login'); + window.dispatchEvent(new PopStateEvent('popstate')); + }); + expect(screen.getByText('Silent login')).toBeDefined(); + + unmount(); + expect(removeEventListener).toHaveBeenCalledWith('popstate', listener, false); + }); +}); diff --git a/packages/react-oidc/src/core/routes/OidcRoutes.spec.tsx b/packages/react-oidc/src/core/routes/OidcRoutes.spec.tsx index 79a94b540..d5e063ddc 100644 --- a/packages/react-oidc/src/core/routes/OidcRoutes.spec.tsx +++ b/packages/react-oidc/src/core/routes/OidcRoutes.spec.tsx @@ -1,15 +1,30 @@ +import { render } from '@testing-library/react'; import React from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + import OidcRoutes from './OidcRoutes'; -import { render } from "@testing-library/react"; -import { describe, it, expect } from 'vitest'; describe('Authenticating test suite', () => { + beforeEach(() => { + // Mock window.location + delete (window as any).location; + window.location = { href: 'http://example.com:3000/' } as any; + + // Mock window event listeners + window.addEventListener = vi.fn(); + window.removeEventListener = vi.fn(); + }); + it('renders correctly', () => { const props = { children: 'http://url.com', - callbackComponent: () =>
tcallback component
, redirect_uri: 'http://example.com:3000/authentication/callback', - configurationName: '' + configurationName: '', + location: { + pathname: '/', + search: '', + hash: '', + }, }; const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); diff --git a/packages/react-oidc/src/core/routes/OidcRoutes.tsx b/packages/react-oidc/src/core/routes/OidcRoutes.tsx index 7440d5e19..9bb20d185 100644 --- a/packages/react-oidc/src/core/routes/OidcRoutes.tsx +++ b/packages/react-oidc/src/core/routes/OidcRoutes.tsx @@ -1,4 +1,4 @@ -import { getPath } from '@axa-fr/vanilla-oidc'; +import { getPath, ILOidcLocation } from '@axa-fr/oidc-client'; import React, { ComponentType, FC, PropsWithChildren, useEffect, useState } from 'react'; import CallbackComponent from '../default-component/Callback.component.js'; @@ -15,6 +15,8 @@ type OidcRoutesProps = { silent_redirect_uri?: string; silent_login_uri?: string; withCustomHistory?: () => CustomHistory; + navigateAfterCallback?: (callbackPath: string) => Promise; + location: ILOidcLocation; }; const OidcRoutes: FC> = ({ @@ -23,8 +25,10 @@ const OidcRoutes: FC> = ({ redirect_uri, silent_redirect_uri, silent_login_uri, - children, configurationName, + children, + configurationName, withCustomHistory = null, + navigateAfterCallback = null, }) => { // This exist because in next.js window outside useEffect is null const pathname = window ? getPath(window.location.href) : ''; @@ -40,24 +44,26 @@ const OidcRoutes: FC> = ({ const callbackPath = getPath(redirect_uri); - if (silent_redirect_uri) { - if (path === getPath(silent_redirect_uri)) { - return ; - } + if (silent_redirect_uri && path === getPath(silent_redirect_uri)) { + return ; } - if (silent_login_uri) { - if (path === getPath(silent_login_uri)) { - return ; - } + if (silent_login_uri && path === getPath(silent_login_uri)) { + return ; } - switch (path) { - case callbackPath: - return ; - default: - return <>{children}; + if (path === callbackPath) { + return ( + + ); } + return <>{children}; }; export default React.memo(OidcRoutes); diff --git a/packages/react-oidc/src/core/routes/__snapshots__/OidcRoutes.spec.tsx.snap b/packages/react-oidc/src/core/routes/__snapshots__/OidcRoutes.spec.tsx.snap index 7d29518f5..f78094328 100644 --- a/packages/react-oidc/src/core/routes/__snapshots__/OidcRoutes.spec.tsx.snap +++ b/packages/react-oidc/src/core/routes/__snapshots__/OidcRoutes.spec.tsx.snap @@ -5,9 +5,3 @@ exports[`Authenticating test suite > renders correctly 1`] = ` http://url.com `; - -exports[`Authenticating test suite renders correctly 1`] = ` - - http://url.com - -`; diff --git a/packages/react-oidc/src/core/routes/withRouter.spec.tsx b/packages/react-oidc/src/core/routes/withRouter.spec.tsx index 3cea580a0..1efc87c00 100644 --- a/packages/react-oidc/src/core/routes/withRouter.spec.tsx +++ b/packages/react-oidc/src/core/routes/withRouter.spec.tsx @@ -1,19 +1,20 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + import { CreateEvent, WindowInternal } from './withRouter'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; describe('WithRouter test Suite', () => { const paramsMock = { bubbles: false, cancelable: false, detail: 'detail' }; - beforeEach(() => { }); + beforeEach(() => {}); it('should CreateEvent return correct Event if not on IE', () => { const windowMock = { - CustomEvent: vi.fn().mockImplementation((event, params) => { + CustomEvent: vi.fn(function (event, params) { return { event, params }; }), }; const documentMock = {} as Document; - const res = CreateEvent((windowMock as unknown) as WindowInternal, documentMock)( + const res = CreateEvent(windowMock as unknown as WindowInternal, documentMock)( 'event test', - paramsMock + paramsMock, ); expect(res).toEqual({ event: 'event test', @@ -33,14 +34,13 @@ describe('WithRouter test Suite', () => { const documentMock = { createEvent: vi.fn(() => evtMock), }; - const typedDocumentMock = (documentMock as unknown) as Document; - const res = CreateEvent((windowMock as unknown) as WindowInternal, typedDocumentMock)( + const typedDocumentMock = documentMock as unknown as Document; + const res = CreateEvent(windowMock as unknown as WindowInternal, typedDocumentMock)( 'event test', - paramsMock + paramsMock, ); expect(res).toEqual({ ...evtMock }); expect(documentMock.createEvent).toHaveBeenCalledWith('CustomEvent'); expect(evtMock.initCustomEvent).toHaveBeenCalledWith('event test', false, false, 'detail'); }); - }); diff --git a/packages/react-oidc/src/core/routes/withRouter.tsx b/packages/react-oidc/src/core/routes/withRouter.tsx index dd87edb1c..5382d001c 100644 --- a/packages/react-oidc/src/core/routes/withRouter.tsx +++ b/packages/react-oidc/src/core/routes/withRouter.tsx @@ -1,7 +1,4 @@ -const generateKey = () => - Math.random() - .toString(36) - .substr(2, 6); +const generateKey = () => Math.random().toString(36).slice(2, 8); // Exported only for test export type WindowInternal = Window & { @@ -20,19 +17,23 @@ type InitCustomEventParams = { }; // IE Polyfill for CustomEvent -export const CreateEvent = (windowInternal: WindowInternal, documentInternal: Document) => ( - event: string, - params: InitCustomEventParams, -): CustomEvent => { - if (typeof windowInternal.CustomEvent === 'function') { - return new windowInternal.CustomEvent(event, params); - } - const paramsToFunction = params || { bubbles: false, cancelable: false, detail: undefined }; - const evt: CustomEvent = documentInternal.createEvent('CustomEvent'); - evt.initCustomEvent(event, paramsToFunction.bubbles, paramsToFunction.cancelable, paramsToFunction.detail); - (evt as CustomEvent & IPrototype).prototype = windowInternal.Event.prototype; - return evt; -}; +export const CreateEvent = + (windowInternal: WindowInternal, documentInternal: Document) => + (event: string, params: InitCustomEventParams): CustomEvent => { + if (typeof windowInternal.CustomEvent === 'function') { + return new windowInternal.CustomEvent(event, params); + } + const paramsToFunction = params || { bubbles: false, cancelable: false, detail: undefined }; + const evt: CustomEvent = documentInternal.createEvent('CustomEvent'); + evt.initCustomEvent( + event, + paramsToFunction.bubbles, + paramsToFunction.cancelable, + paramsToFunction.detail, + ); + (evt as CustomEvent & IPrototype).prototype = windowInternal.Event.prototype; + return evt; + }; type WindowHistoryState = typeof window.history.state; @@ -42,7 +43,7 @@ export interface ReactOidcHistory { export type CustomHistory = { replaceState(url?: string | null, stateHistory?: WindowHistoryState): void; -} +}; const getHistory = ( windowInternal: WindowInternal, @@ -59,4 +60,5 @@ const getHistory = ( }; }; -export const getCustomHistory = () => getHistory(window, CreateEvent(window, document), generateKey); +export const getCustomHistory = () => + getHistory(window, CreateEvent(window, document), generateKey); diff --git a/packages/react-oidc/src/hooksWithoutProvider.spec.tsx b/packages/react-oidc/src/hooksWithoutProvider.spec.tsx new file mode 100644 index 000000000..efd77e946 --- /dev/null +++ b/packages/react-oidc/src/hooksWithoutProvider.spec.tsx @@ -0,0 +1,89 @@ +import { renderHook } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { useOidc, useOidcAccessToken, useOidcIdToken } from './ReactOidc'; +import { OidcUserStatus, useOidcUser } from './User'; +import { resetWarnedConfigurations } from './warnMissingConfiguration'; + +vi.mock('@axa-fr/oidc-client', () => ({ + OidcClient: { + get: vi.fn(() => null), + getOrThrow: vi.fn(() => { + throw new Error('OIDC library does seem initialized.'); + }), + eventNames: { + logout_from_another_tab: 'logout_from_another_tab', + logout_from_same_tab: 'logout_from_same_tab', + token_acquired: 'token_acquired', + token_renewed: 'token_renewed', + refreshTokensAsync_error: 'refreshTokensAsync_error', + syncTokensAsync_error: 'syncTokensAsync_error', + }, + }, +})); + +describe('hooks used outside (issue #1679)', () => { + let warnSpy: ReturnType; + + beforeEach(() => { + resetWarnedConfigurations(); + warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + }); + + afterEach(() => { + warnSpy.mockRestore(); + }); + + describe('useOidc', () => { + it('does not throw and returns safe defaults', () => { + const { result } = renderHook(() => useOidc()); + expect(result.current.isAuthenticated).toBe(false); + expect(typeof result.current.login).toBe('function'); + expect(typeof result.current.logout).toBe('function'); + expect(typeof result.current.renewTokens).toBe('function'); + }); + + it('login / logout / renewTokens resolve without throwing', async () => { + const { result } = renderHook(() => useOidc()); + await expect(result.current.login()).resolves.toBeUndefined(); + await expect(result.current.logout()).resolves.toBeUndefined(); + await expect(result.current.renewTokens()).resolves.toMatchObject({ + accessToken: null, + idToken: null, + }); + }); + + it('warns once per configuration name', () => { + renderHook(() => useOidc('cfg-a')); + renderHook(() => useOidc('cfg-a')); + renderHook(() => useOidc('cfg-b')); + const messagesForA = warnSpy.mock.calls.filter(args => String(args[0]).includes('"cfg-a"')); + const messagesForB = warnSpy.mock.calls.filter(args => String(args[0]).includes('"cfg-b"')); + expect(messagesForA.length).toBe(1); + expect(messagesForB.length).toBe(1); + }); + }); + + describe('useOidcAccessToken', () => { + it('returns the default access-token state', () => { + const { result } = renderHook(() => useOidcAccessToken()); + expect(result.current).toEqual({ accessToken: null, accessTokenPayload: null }); + }); + }); + + describe('useOidcIdToken', () => { + it('returns the default id-token state', () => { + const { result } = renderHook(() => useOidcIdToken()); + expect(result.current).toEqual({ idToken: null, idTokenPayload: null }); + }); + }); + + describe('useOidcUser', () => { + it('returns the default user state without throwing', () => { + const { result } = renderHook(() => useOidcUser()); + expect(result.current.oidcUser).toBeNull(); + expect(result.current.oidcUserLoadingState).toBe(OidcUserStatus.Unauthenticated); + expect(typeof result.current.reloadOidcUser).toBe('function'); + }); + }); +}); diff --git a/packages/react-oidc/src/index.ts b/packages/react-oidc/src/index.ts index 44537a8a1..b74f16805 100644 --- a/packages/react-oidc/src/index.ts +++ b/packages/react-oidc/src/index.ts @@ -1,4 +1,5 @@ export { useOidcFetch, withOidcFetch } from './FetchToken.js'; +export type { OidcProviderProps } from './OidcProvider.js'; export { OidcProvider } from './OidcProvider.js'; export { OidcSecure, withOidcSecure } from './OidcSecure.js'; export { useOidc, useOidcAccessToken, useOidcIdToken } from './ReactOidc.js'; @@ -6,7 +7,26 @@ export { OidcUserStatus, useOidcUser } from './User.js'; export type { AuthorityConfiguration, Fetch, + ILOidcLocation, OidcConfiguration, + OidcErrorOptions, + OidcErrorPhase, + PushedAuthorizationRequestMode, StringMap, -} from '@axa-fr/vanilla-oidc'; -export { type OidcUserInfo, TokenRenewMode } from '@axa-fr/vanilla-oidc'; +} from '@axa-fr/oidc-client'; +export type { OidcUserInfo } from '@axa-fr/oidc-client'; +export { + isOidcError, + isOidcStateError, + isPushedAuthorizationRequestError, + OidcClient, + OidcError, + OidcErrorCode, + OidcLocation, + OidcStateError, + OidcStateErrorCode, + PushedAuthorizationRequestError, + PushedAuthorizationRequestErrorCode, + TokenAutomaticRenewMode, + TokenRenewMode, +} from '@axa-fr/oidc-client'; diff --git a/packages/react-oidc/src/warnMissingConfiguration.ts b/packages/react-oidc/src/warnMissingConfiguration.ts new file mode 100644 index 000000000..4fb21a87a --- /dev/null +++ b/packages/react-oidc/src/warnMissingConfiguration.ts @@ -0,0 +1,28 @@ +const warnedConfigurations = new Set(); + +/** + * Emits a `console.warn` once per `configurationName` when an OIDC hook is + * used without a matching ``. Introduced for issue #1679 so + * that consumers get a clear, non-fatal signal in tests and Storybook. + * + * Exported for testing purposes. + */ +export const warnMissingConfigurationOnce = (configurationName: string): void => { + if (warnedConfigurations.has(configurationName)) { + return; + } + warnedConfigurations.add(configurationName); + console.warn( + `@axa-fr/react-oidc: no OIDC configuration found for "${configurationName}". ` + + `Make sure to wrap your component tree with . ` + + `Hooks are returning safe default values (issue #1679).`, + ); +}; + +/** + * Resets the internal set of already-warned configuration names. Intended + * for tests only. + */ +export const resetWarnedConfigurations = (): void => { + warnedConfigurations.clear(); +}; diff --git a/packages/react-oidc/tests/setup.js b/packages/react-oidc/tests/setup.js index e022aaa60..ebbe461d0 100644 --- a/packages/react-oidc/tests/setup.js +++ b/packages/react-oidc/tests/setup.js @@ -1,11 +1,7 @@ -import { expect, afterEach } from 'vitest'; -import { cleanup } from '@testing-library/react'; -import matchers from '@testing-library/jest-dom/matchers'; - -// extends Vitest's expect method with methods from react-testing-library -expect.extend(matchers); +import { cleanup } from '@testing-library/react'; +import { afterEach } from 'vitest'; // runs a cleanup after each test case (e.g. clearing jsdom) afterEach(() => { - cleanup(); -}); \ No newline at end of file + cleanup(); +}); diff --git a/packages/react-oidc/tsconfig.eslint.json b/packages/react-oidc/tsconfig.eslint.json index e9041fd6b..b90fc83e0 100644 --- a/packages/react-oidc/tsconfig.eslint.json +++ b/packages/react-oidc/tsconfig.eslint.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", "include": ["src"] -} \ No newline at end of file +} diff --git a/packages/react-oidc/tsconfig.json b/packages/react-oidc/tsconfig.json index 76f36a849..334dcc95f 100644 --- a/packages/react-oidc/tsconfig.json +++ b/packages/react-oidc/tsconfig.json @@ -1,13 +1,12 @@ -{ +{ "compilerOptions": { "target": "ES2019", "lib": ["ES2021", "DOM"], "outDir": "dist", - "baseUrl": ".", "jsx": "react-jsx", - "forceConsistentCasingInFileNames":true, - "skipLibCheck":true, - "module": "CommonJS", + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "module": "ESNext", "declaration": true, "declarationMap": true, "sourceMap": true, @@ -17,18 +16,16 @@ "strictFunctionTypes": false, "strictPropertyInitialization": false, "noImplicitThis": false, - "alwaysStrict": false, "noUnusedLocals": false, "noUnusedParameters": false, "noImplicitReturns": false, "noFallthroughCasesInSwitch": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "allowJs": true, - "rootDir": "src", - + "rootDir": "src" }, "exclude": [ "node_modules", @@ -39,7 +36,5 @@ "**/*.stories.tsx", "./src/setupTests.ts" ], - "include": [ - "src" - ] -} \ No newline at end of file + "include": ["src"] +} diff --git a/packages/react-oidc/vite.config.ts b/packages/react-oidc/vite.config.ts index 247d9617a..3e63539c4 100644 --- a/packages/react-oidc/vite.config.ts +++ b/packages/react-oidc/vite.config.ts @@ -1,7 +1,8 @@ -import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; import { resolve } from 'path'; -import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; import dts from 'vite-plugin-dts'; + import pkg from './package.json'; const dependencies = externalDependencies(); @@ -16,7 +17,7 @@ export default defineConfig({ fileName: 'index', }, rollupOptions: { - external: [...dependencies], + external: [...dependencies, 'react/jsx-runtime'], output: { globals: { react: 'React', @@ -31,7 +32,7 @@ export default defineConfig({ react(), ], resolve: { - preserveSymlinks: true, //https://github.com/vitejs/vite/issues/11657 + preserveSymlinks: true, // https://github.com/vitejs/vite/issues/11657 }, test: { globals: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23941c819..621245cea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,45 +1,82 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false importers: .: devDependencies: + '@eslint/compat': + specifier: ^2.1.1 + version: 2.1.1(eslint@10.10.0) + '@eslint/eslintrc': + specifier: ^3.3.7 + version: 3.3.7 + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.10.0) + '@typescript-eslint/eslint-plugin': + specifier: ^8.70.0 + version: 8.70.0(@typescript-eslint/parser@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0))(@typescript/typescript6@6.0.2)(eslint@10.10.0) + '@typescript-eslint/parser': + specifier: ^8.70.0 + version: 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0) + '@typescript/native': + specifier: npm:typescript@7.0.2 + version: typescript@7.0.2 eslint: - specifier: ^8.45.0 - version: 8.45.0 - eslint-define-config: - specifier: ^1.21.0 - version: 1.21.0 + specifier: ^10.10.0 + version: 10.10.0 + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@10.10.0) eslint-plugin-import: - specifier: ^2.27.5 - version: 2.27.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.45.0) + specifier: ^2.32.0 + version: 2.32.0(@typescript-eslint/parser@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0))(eslint@10.10.0) eslint-plugin-jsx-a11y: - specifier: ^6.7.1 - version: 6.7.1(eslint@8.45.0) + specifier: ^6.10.2 + version: 6.10.2(eslint@10.10.0) eslint-plugin-n: - specifier: ^16.0.1 - version: 16.0.1(eslint@8.45.0) + specifier: ^18.3.0 + version: 18.3.0(@typescript/typescript6@6.0.2)(eslint@10.10.0) + eslint-plugin-no-only-tests: + specifier: ^3.4.0 + version: 3.4.0 + eslint-plugin-prettier: + specifier: ^5.5.6 + version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0))(eslint@10.10.0)(prettier@3.9.6) + eslint-plugin-react: + specifier: ^7.37.5 + version: 7.37.5(eslint@10.10.0) eslint-plugin-react-hooks: - specifier: ^4.6.0 - version: 4.6.0(eslint@8.45.0) + specifier: ^7.1.1 + version: 7.1.1(eslint@10.10.0) eslint-plugin-regexp: - specifier: ^1.15.0 - version: 1.15.0(eslint@8.45.0) + specifier: ^3.3.0 + version: 3.3.0(eslint@10.10.0) + eslint-plugin-simple-import-sort: + specifier: ^14.0.0 + version: 14.0.0(eslint@10.10.0) eslint-plugin-testing-library: - specifier: ^5.11.0 - version: 5.11.0(eslint@8.45.0)(typescript@5.1.6) + specifier: ^7.16.2 + version: 7.16.2(@typescript/typescript6@6.0.2)(eslint@10.10.0) + prettier: + specifier: ^3.9.6 + version: 3.9.6 tslib: - specifier: ^2.6.0 - version: 2.6.0 + specifier: ^2.8.1 + version: 2.8.1 tsx: - specifier: ^3.12.7 - version: 3.12.7 + specifier: 4.23.13 + version: 4.23.13 typescript: - specifier: ^5.1.6 - version: 5.1.6 + specifier: npm:@typescript/typescript6@6.0.2 + version: '@typescript/typescript6@6.0.2' vitest: - specifier: ^0.33.0 - version: 0.33.0(jsdom@22.1.0) + specifier: 5.0.0 + version: 5.0.0(@types/node@26.5.1)(@vitest/coverage-v8@5.0.0)(jsdom@30.0.1)(msw@2.15.0(@types/node@26.5.1)(@typescript/typescript6@6.0.2))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)) examples/nextjs-demo: dependencies: @@ -47,5098 +84,5879 @@ importers: specifier: workspace:* version: link:../../packages/react-oidc next: - specifier: latest - version: 13.4.10(react-dom@18.2.0)(react@18.2.0) + specifier: ^16.3.5 + version: 16.3.5(@babel/core@7.29.7)(@types/node@26.5.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) react: - specifier: latest - version: 18.2.0 + specifier: ^19.3.0 + version: 19.3.0 react-dom: - specifier: latest - version: 18.2.0(react@18.2.0) + specifier: ^19.3.0 + version: 19.3.0(react@19.3.0) + styled-jsx: + specifier: ^5.1.7 + version: 5.1.7(@babel/core@7.29.7)(react@19.3.0) examples/oidc-client-demo: dependencies: - '@axa-fr/vanilla-oidc': + '@axa-fr/oidc-client': specifier: workspace:~ version: link:../../packages/oidc-client - '@testing-library/jest-dom': - specifier: ^5.16.5 - version: 5.16.5 - '@testing-library/user-event': - specifier: ^13.5.0 - version: 13.5.0(@testing-library/dom@9.3.1) - '@types/jest': - specifier: ^27.5.2 - version: 27.5.2 typescript: - specifier: ^5.1.6 - version: 5.1.6 + specifier: npm:@typescript/typescript6@6.0.2 + version: '@typescript/typescript6@6.0.2' web-vitals: - specifier: ^3.4.0 - version: 3.4.0 + specifier: 6.2.1 + version: 6.2.1 devDependencies: '@types/node': - specifier: ^18.11.9 - version: 18.11.9 + specifier: 26.5.1 + version: 26.5.1 cross-env: - specifier: ^7.0.3 - version: 7.0.3 + specifier: ^10.1.0 + version: 10.1.0 vite: - specifier: ^4.4.4 - version: 4.4.4(@types/node@18.11.9) + specifier: 8.3.0 + version: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13) examples/react-oidc-demo: - devDependencies: - '@axa-fr/oidc-client-service-worker': - specifier: workspace:* - version: link:../../packages/oidc-client-service-worker + dependencies: '@axa-fr/react-oidc': specifier: workspace:* version: link:../../packages/react-oidc - '@axa-fr/vanilla-oidc': - specifier: workspace:* - version: link:../../packages/oidc-client + react: + specifier: ^19.3.0 + version: 19.3.0 + react-dom: + specifier: ^19.3.0 + version: 19.3.0(react@19.3.0) + react-router-dom: + specifier: ^7.18.3 + version: 7.18.3(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + devDependencies: '@testing-library/jest-dom': - specifier: 5.16.5 - version: 5.16.5 + specifier: 7.0.1 + version: 7.0.1(@testing-library/dom@10.4.2)(vitest@5.0.0) '@testing-library/react': - specifier: 13.3.0 - version: 13.3.0(react-dom@18.2.0)(react@18.2.0) + specifier: 16.3.3 + version: 16.3.3(@testing-library/dom@10.4.2)(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@testing-library/user-event': - specifier: 14.4.3 - version: 14.4.3(@testing-library/dom@9.3.1) + specifier: 14.6.7 + version: 14.6.7(@testing-library/dom@10.4.2) '@types/react': - specifier: ^18.2.15 - version: 18.2.15 - '@typescript-eslint/eslint-plugin': - specifier: ^5.50.0 - version: 5.50.0(@typescript-eslint/parser@5.62.0)(eslint@8.45.0)(typescript@5.1.6) - '@typescript-eslint/parser': - specifier: ^5.50.0 - version: 5.62.0(eslint@8.45.0)(typescript@5.1.6) + specifier: 19.3.0 + version: 19.3.0 '@vitejs/plugin-react': - specifier: 4.0.3 - version: 4.0.3(vite@4.4.4) - '@vitest/coverage-c8': - specifier: ^0.33.0 - version: 0.33.0(vitest@0.33.0) + specifier: 6.1.1 + version: 6.1.1(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)) bootstrap: - specifier: ^4.6.2 - version: 4.6.2(jquery@3.7.0)(popper.js@1.16.1) + specifier: ^5.3.8 + version: 5.3.8(@popperjs/core@2.11.8) copyfiles: specifier: 2.4.1 version: 2.4.1 cross-env: - specifier: ^7.0.3 - version: 7.0.3 - eslint: - specifier: ^8.26.0 - version: 8.45.0 - eslint-config-standard: - specifier: ^17.1.0 - version: 17.1.0(eslint-plugin-import@2.27.5)(eslint-plugin-n@16.0.1)(eslint-plugin-promise@6.1.1)(eslint@8.45.0) - eslint-config-standard-with-typescript: - specifier: ^36.1.0 - version: 36.1.0(@typescript-eslint/eslint-plugin@5.50.0)(eslint-plugin-import@2.27.5)(eslint-plugin-n@16.0.1)(eslint-plugin-promise@6.1.1)(eslint@8.45.0)(typescript@5.1.6) - eslint-import-resolver-typescript: - specifier: ^3.5.5 - version: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.27.5)(eslint@8.45.0) - eslint-plugin-react: - specifier: ^7.32.2 - version: 7.32.2(eslint@8.45.0) - eslint-plugin-simple-import-sort: - specifier: ^10.0.0 - version: 10.0.0(eslint@8.45.0) + specifier: ^10.1.0 + version: 10.1.0 jsdom: - specifier: 22.1.0 - version: 22.1.0 - msw: - specifier: 1.2.2 - version: 1.2.2(typescript@5.1.6) - react: - specifier: ^18.2.0 - version: 18.2.0 - react-dom: - specifier: ^18.2.0 - version: 18.2.0(react@18.2.0) - react-router-dom: - specifier: ^6.14.1 - version: 6.14.1(react-dom@18.2.0)(react@18.2.0) + specifier: 30.0.1 + version: 30.0.1 typescript: - specifier: 5.1.6 - version: 5.1.6 + specifier: npm:@typescript/typescript6@6.0.2 + version: '@typescript/typescript6@6.0.2' vite: - specifier: ^4.4.4 - version: 4.4.4(@types/node@18.11.9) + specifier: 8.3.0 + version: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13) vite-plugin-dts: - specifier: ^3.3.0 - version: 3.3.0(typescript@5.1.6)(vite@4.4.4) + specifier: 5.1.0 + version: 5.1.0(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(rolldown@1.2.8)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)) vitest: - specifier: ^0.33.0 - version: 0.33.0(jsdom@22.1.0) + specifier: 5.0.0 + version: 5.0.0(@types/node@26.5.1)(@vitest/coverage-v8@5.0.0)(jsdom@30.0.1)(msw@2.15.0(@types/node@26.5.1)(@typescript/typescript6@6.0.2))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)) packages/oidc-client: - devDependencies: + dependencies: '@axa-fr/oidc-client-service-worker': specifier: workspace:* version: link:../oidc-client-service-worker + devDependencies: '@testing-library/dom': - specifier: ^9.3.1 - version: 9.3.1 + specifier: 10.4.2 + version: 10.4.2 '@testing-library/jest-dom': - specifier: ^5.16.5 - version: 5.16.5 + specifier: 7.0.1 + version: 7.0.1(@testing-library/dom@10.4.2)(vitest@5.0.0) '@testing-library/react': - specifier: 13.3.0 - version: 13.3.0(react-dom@18.2.0)(react@18.2.0) + specifier: 16.3.3 + version: 16.3.3(@testing-library/dom@10.4.2)(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@types/node': + specifier: ^26.5.1 + version: 26.5.1 '@vitest/coverage-v8': - specifier: ^0.33.0 - version: 0.33.0(vitest@0.33.0) - base64-js: - specifier: ^1.5.1 - version: 1.5.1 + specifier: 5.0.0 + version: 5.0.0(vitest@5.0.0) cpy: - specifier: ^10.1.0 - version: 10.1.0 + specifier: 13.2.3 + version: 13.2.3 cpy-cli: - specifier: ^5.0.0 - version: 5.0.0 - eslint: - specifier: ^8.26.0 - version: 8.45.0 - eslint-config-standard: - specifier: ^17.1.0 - version: 17.1.0(eslint-plugin-import@2.27.5)(eslint-plugin-n@16.0.1)(eslint-plugin-promise@6.1.1)(eslint@8.45.0) - eslint-config-standard-with-typescript: - specifier: ^36.1.0 - version: 36.1.0(@typescript-eslint/eslint-plugin@5.50.0)(eslint-plugin-import@2.27.5)(eslint-plugin-n@16.0.1)(eslint-plugin-promise@6.1.1)(eslint@8.45.0)(typescript@5.1.6) - eslint-import-resolver-typescript: - specifier: ^3.5.5 - version: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.27.5)(eslint@8.45.0) - eslint-plugin-react: - specifier: ^7.32.2 - version: 7.32.2(eslint@8.45.0) - eslint-plugin-simple-import-sort: - specifier: ^10.0.0 - version: 10.0.0(eslint@8.45.0) + specifier: ^7.0.0 + version: 7.0.0 rimraf: - specifier: 5.0.1 - version: 5.0.1 + specifier: 6.1.3 + version: 6.1.3 typescript: - specifier: 5.1.6 - version: 5.1.6 + specifier: npm:@typescript/typescript6@6.0.2 + version: '@typescript/typescript6@6.0.2' vite: - specifier: ^4.4.4 - version: 4.4.4(@types/node@18.11.9) + specifier: 8.3.0 + version: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13) vite-plugin-dts: - specifier: ^3.3.0 - version: 3.3.0(typescript@5.1.6)(vite@4.4.4) + specifier: 5.1.0 + version: 5.1.0(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(rolldown@1.2.8)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)) vitest: - specifier: ^0.33.0 - version: 0.33.0(jsdom@22.1.0) + specifier: 5.0.0 + version: 5.0.0(@types/node@26.5.1)(@vitest/coverage-v8@5.0.0)(jsdom@30.0.1)(msw@2.15.0(@types/node@26.5.1)(@typescript/typescript6@6.0.2))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)) packages/oidc-client-service-worker: devDependencies: - '@typescript-eslint/eslint-plugin': - specifier: ^5.50.0 - version: 5.50.0(@typescript-eslint/parser@5.62.0)(eslint@8.45.0)(typescript@5.1.6) - '@typescript-eslint/parser': - specifier: ^5.50.0 - version: 5.62.0(eslint@8.45.0)(typescript@5.1.6) - '@vitest/coverage-c8': - specifier: ^0.33.0 - version: 0.33.0(vitest@0.33.0) + '@vitest/coverage-v8': + specifier: 5.0.0 + version: 5.0.0(vitest@5.0.0) cpy: - specifier: ^10.1.0 - version: 10.1.0 + specifier: 13.2.3 + version: 13.2.3 cpy-cli: - specifier: ^5.0.0 - version: 5.0.0 - eslint: - specifier: ^8.26.0 - version: 8.45.0 - eslint-config-standard: - specifier: ^17.1.0 - version: 17.1.0(eslint-plugin-import@2.27.5)(eslint-plugin-n@16.0.1)(eslint-plugin-promise@6.1.1)(eslint@8.45.0) - eslint-config-standard-with-typescript: - specifier: ^36.1.0 - version: 36.1.0(@typescript-eslint/eslint-plugin@5.50.0)(eslint-plugin-import@2.27.5)(eslint-plugin-n@16.0.1)(eslint-plugin-promise@6.1.1)(eslint@8.45.0)(typescript@5.1.6) - eslint-import-resolver-typescript: - specifier: ^3.5.5 - version: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.27.5)(eslint@8.45.0) - eslint-plugin-react: - specifier: ^7.32.2 - version: 7.32.2(eslint@8.45.0) - eslint-plugin-simple-import-sort: - specifier: ^10.0.0 - version: 10.0.0(eslint@8.45.0) - msw: - specifier: 1.2.2 - version: 1.2.2(typescript@5.1.6) + specifier: ^7.0.0 + version: 7.0.0 rimraf: - specifier: 5.0.1 - version: 5.0.1 + specifier: 6.1.3 + version: 6.1.3 typescript: - specifier: 5.1.6 - version: 5.1.6 + specifier: npm:@typescript/typescript6@6.0.2 + version: '@typescript/typescript6@6.0.2' vite: - specifier: ^4.4.4 - version: 4.4.4(@types/node@18.11.9) + specifier: 8.3.0 + version: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13) vite-plugin-dts: - specifier: ^3.3.0 - version: 3.3.0(typescript@5.1.6)(vite@4.4.4) + specifier: 5.1.0 + version: 5.1.0(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(rolldown@1.2.8)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)) vitest: - specifier: ^0.33.0 - version: 0.33.0(jsdom@22.1.0) + specifier: 5.0.0 + version: 5.0.0(@types/node@26.5.1)(@vitest/coverage-v8@5.0.0)(jsdom@30.0.1)(msw@2.15.0(@types/node@26.5.1)(@typescript/typescript6@6.0.2))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)) packages/react-oidc: dependencies: + '@axa-fr/oidc-client': + specifier: workspace:* + version: link:../oidc-client '@axa-fr/oidc-client-service-worker': specifier: workspace:* version: link:../oidc-client-service-worker - '@axa-fr/vanilla-oidc': - specifier: workspace:* - version: link:../oidc-client - react-router-dom: - specifier: ^6.0.0 - version: 6.14.1(react-dom@18.2.0)(react@18.2.0) devDependencies: '@testing-library/jest-dom': - specifier: 5.16.5 - version: 5.16.5 + specifier: 7.0.1 + version: 7.0.1(@testing-library/dom@10.4.2)(vitest@5.0.0) '@testing-library/react': - specifier: 13.3.0 - version: 13.3.0(react-dom@18.2.0)(react@18.2.0) + specifier: 16.3.3 + version: 16.3.3(@testing-library/dom@10.4.2)(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@testing-library/user-event': - specifier: 14.4.3 - version: 14.4.3(@testing-library/dom@9.3.1) + specifier: 14.6.7 + version: 14.6.7(@testing-library/dom@10.4.2) '@types/react': - specifier: ^18.2.15 - version: 18.2.15 - '@typescript-eslint/eslint-plugin': - specifier: ^5.50.0 - version: 5.50.0(@typescript-eslint/parser@5.62.0)(eslint@8.45.0)(typescript@5.1.6) - '@typescript-eslint/parser': - specifier: ^5.50.0 - version: 5.62.0(eslint@8.45.0)(typescript@5.1.6) + specifier: 19.3.0 + version: 19.3.0 '@vitejs/plugin-react': - specifier: 4.0.3 - version: 4.0.3(vite@4.4.4) + specifier: 6.1.1 + version: 6.1.1(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)) '@vitest/coverage-v8': - specifier: ^0.33.0 - version: 0.33.0(vitest@0.33.0) + specifier: 5.0.0 + version: 5.0.0(vitest@5.0.0) cpy: - specifier: ^10.1.0 - version: 10.1.0 + specifier: 13.2.3 + version: 13.2.3 cpy-cli: - specifier: ^5.0.0 - version: 5.0.0 + specifier: ^7.0.0 + version: 7.0.0 cross-env: - specifier: ^7.0.3 - version: 7.0.3 - eslint: - specifier: ^8.26.0 - version: 8.45.0 - eslint-config-standard: - specifier: ^17.1.0 - version: 17.1.0(eslint-plugin-import@2.27.5)(eslint-plugin-n@16.0.1)(eslint-plugin-promise@6.1.1)(eslint@8.45.0) - eslint-config-standard-with-typescript: - specifier: ^36.1.0 - version: 36.1.0(@typescript-eslint/eslint-plugin@5.50.0)(eslint-plugin-import@2.27.5)(eslint-plugin-n@16.0.1)(eslint-plugin-promise@6.1.1)(eslint@8.45.0)(typescript@5.1.6) - eslint-import-resolver-typescript: - specifier: ^3.5.5 - version: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.27.5)(eslint@8.45.0) - eslint-plugin-react: - specifier: ^7.32.2 - version: 7.32.2(eslint@8.45.0) - eslint-plugin-simple-import-sort: - specifier: ^10.0.0 - version: 10.0.0(eslint@8.45.0) + specifier: ^10.1.0 + version: 10.1.0 jsdom: - specifier: 22.1.0 - version: 22.1.0 + specifier: 30.0.1 + version: 30.0.1 msw: - specifier: 1.2.2 - version: 1.2.2(typescript@5.1.6) + specifier: 2.15.0 + version: 2.15.0(@types/node@26.5.1)(@typescript/typescript6@6.0.2) react: - specifier: ^18.2.0 - version: 18.2.0 + specifier: ^19.3.0 + version: 19.3.0 react-dom: - specifier: ^18.2.0 - version: 18.2.0(react@18.2.0) + specifier: ^19.3.0 + version: 19.3.0(react@19.3.0) rimraf: - specifier: 5.0.1 - version: 5.0.1 + specifier: 6.1.3 + version: 6.1.3 typescript: - specifier: 5.1.6 - version: 5.1.6 + specifier: npm:@typescript/typescript6@6.0.2 + version: '@typescript/typescript6@6.0.2' vite: - specifier: ^4.4.4 - version: 4.4.4(@types/node@18.11.9) + specifier: 8.3.0 + version: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13) vite-plugin-dts: - specifier: ^3.3.0 - version: 3.3.0(typescript@5.1.6)(vite@4.4.4) + specifier: 5.1.0 + version: 5.1.0(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(rolldown@1.2.8)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)) vitest: - specifier: ^0.33.0 - version: 0.33.0(jsdom@22.1.0) + specifier: 5.0.0 + version: 5.0.0(@types/node@26.5.1)(@vitest/coverage-v8@5.0.0)(jsdom@30.0.1)(msw@2.15.0(@types/node@26.5.1)(@typescript/typescript6@6.0.2))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)) packages: - /@aashutoshrathi/word-wrap@1.2.6: - resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} - engines: {node: '>=0.10.0'} - dev: true - - /@adobe/css-tools@4.2.0: - resolution: {integrity: sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==} - - /@ampproject/remapping@2.2.1: - resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.18 - dev: true + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} - /@babel/code-frame@7.22.5: - resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/highlight': 7.22.5 + '@asamuzakjp/css-color@6.0.7': + resolution: {integrity: sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==} + engines: {node: ^22.13.0 || >=24.0.0} - /@babel/compat-data@7.22.9: - resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==} - engines: {node: '>=6.9.0'} - dev: true + '@asamuzakjp/dom-selector@8.3.2': + resolution: {integrity: sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==} + engines: {node: ^22.13.0 || >=24.0.0} - /@babel/core@7.22.9: - resolution: {integrity: sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} - dependencies: - '@ampproject/remapping': 2.2.1 - '@babel/code-frame': 7.22.5 - '@babel/generator': 7.22.9 - '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.22.9) - '@babel/helper-module-transforms': 7.22.9(@babel/core@7.22.9) - '@babel/helpers': 7.22.6 - '@babel/parser': 7.22.7 - '@babel/template': 7.22.5 - '@babel/traverse': 7.22.8 - '@babel/types': 7.22.5 - convert-source-map: 1.9.0 - debug: 4.3.4 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/generator@7.22.9: - resolution: {integrity: sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==} + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.22.5 - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.18 - jsesc: 2.5.2 - dev: true - /@babel/helper-compilation-targets@7.22.9(@babel/core@7.22.9): - resolution: {integrity: sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==} + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/compat-data': 7.22.9 - '@babel/core': 7.22.9 - '@babel/helper-validator-option': 7.22.5 - browserslist: 4.21.9 - lru-cache: 5.1.1 - semver: 6.3.1 - dev: true - /@babel/helper-environment-visitor@7.22.5: - resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==} + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-function-name@7.22.5: - resolution: {integrity: sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==} + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.22.5 - '@babel/types': 7.22.5 - dev: true - /@babel/helper-hoist-variables@7.22.5: - resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.22.5 - dev: true - /@babel/helper-module-imports@7.22.5: - resolution: {integrity: sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==} + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.22.5 - dev: true - /@babel/helper-module-transforms@7.22.9(@babel/core@7.22.9): - resolution: {integrity: sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==} + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.22.9 - '@babel/helper-environment-visitor': 7.22.5 - '@babel/helper-module-imports': 7.22.5 - '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.22.5 - dev: true - /@babel/helper-plugin-utils@7.22.5: - resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-simple-access@7.22.5: - resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.22.5 - dev: true - /@babel/helper-split-export-declaration@7.22.6: - resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.22.5 - dev: true - /@babel/helper-string-parser@7.22.5: - resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} - dev: true - /@babel/helper-validator-identifier@7.22.5: - resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-option@7.22.5: - resolution: {integrity: sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==} + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - dev: true - /@babel/helpers@7.22.6: - resolution: {integrity: sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==} + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.22.5 - '@babel/traverse': 7.22.8 - '@babel/types': 7.22.5 - transitivePeerDependencies: - - supports-color - dev: true - /@babel/highlight@7.22.5: - resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-validator-identifier': 7.22.5 - chalk: 2.4.2 - js-tokens: 4.0.0 - /@babel/parser@7.22.7: - resolution: {integrity: sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==} - engines: {node: '>=6.0.0'} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true - dependencies: - '@babel/types': 7.22.5 - dev: true - /@babel/plugin-transform-react-jsx-self@7.22.5(@babel/core@7.22.9): - resolution: {integrity: sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.9 - '@babel/helper-plugin-utils': 7.22.5 - dev: true + '@cacheable/memory@2.2.0': + resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} - /@babel/plugin-transform-react-jsx-source@7.22.5(@babel/core@7.22.9): - resolution: {integrity: sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.9 - '@babel/helper-plugin-utils': 7.22.5 - dev: true + '@cacheable/utils@2.5.0': + resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} - /@babel/runtime@7.22.6: - resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==} - engines: {node: '>=6.9.0'} - dependencies: - regenerator-runtime: 0.13.11 + '@csstools/color-helpers@6.1.1': + resolution: {integrity: sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==} + engines: {node: '>=20.19.0'} - /@babel/template@7.22.5: - resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.22.5 - '@babel/parser': 7.22.7 - '@babel/types': 7.22.5 - dev: true + '@csstools/css-calc@3.4.0': + resolution: {integrity: sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 - /@babel/traverse@7.22.8: - resolution: {integrity: sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.22.5 - '@babel/generator': 7.22.9 - '@babel/helper-environment-visitor': 7.22.5 - '@babel/helper-function-name': 7.22.5 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.22.7 - '@babel/types': 7.22.5 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: true + '@csstools/css-color-parser@4.2.3': + resolution: {integrity: sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 - /@babel/types@7.22.5: - resolution: {integrity: sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-string-parser': 7.22.5 - '@babel/helper-validator-identifier': 7.22.5 - to-fast-properties: 2.0.0 - dev: true + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 - /@bcoe/v8-coverage@0.2.3: - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - dev: true + '@csstools/css-syntax-patches-for-csstree@1.1.13': + resolution: {integrity: sha512-i9ZylF5QNhmNfPA9l0vHAWK4kPrbIp6g9lKgaiIFsIBz2F/WNB7OLrzlNNcCOm+h42bkaSD2v1PG+IBPHhc3ZA==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true - /@esbuild-kit/cjs-loader@2.4.2: - resolution: {integrity: sha512-BDXFbYOJzT/NBEtp71cvsrGPwGAMGRB/349rwKuoxNSiKjPraNNnlK6MIIabViCjqZugu6j+xeMDlEkWdHHJSg==} - dependencies: - '@esbuild-kit/core-utils': 3.1.0 - get-tsconfig: 4.6.2 - dev: true + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} - /@esbuild-kit/core-utils@3.1.0: - resolution: {integrity: sha512-Uuk8RpCg/7fdHSceR1M6XbSZFSuMrxcePFuGgyvsBn+u339dk5OeL4jv2EojwTN2st/unJGsVm4qHWjWNmJ/tw==} - dependencies: - esbuild: 0.17.19 - source-map-support: 0.5.21 - dev: true + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} - /@esbuild-kit/esm-loader@2.5.5: - resolution: {integrity: sha512-Qwfvj/qoPbClxCRNuac1Du01r9gvNOT+pMYtJDapfB1eoGN1YlJ1BixLyL9WVENRx5RXgNLdfYdx/CuswlGhMw==} - dependencies: - '@esbuild-kit/core-utils': 3.1.0 - get-tsconfig: 4.6.2 - dev: true + '@epic-web/invariant@1.0.0': + resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} - /@esbuild/android-arm64@0.17.19: - resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] - /@esbuild/android-arm64@0.18.13: - resolution: {integrity: sha512-j7NhycJUoUAG5kAzGf4fPWfd17N6SM3o1X6MlXVqfHvs2buFraCJzos9vbeWjLxOyBKHyPOnuCuipbhvbYtTAg==} - engines: {node: '>=12'} + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} cpu: [arm64] os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.17.19: - resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-arm@0.18.13: - resolution: {integrity: sha512-KwqFhxRFMKZINHzCqf8eKxE0XqWlAVPRxwy6rc7CbVFxzUWB2sA/s3hbMZeemPdhN3fKBkqOaFhTbS8xJXYIWQ==} - engines: {node: '>=12'} + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} cpu: [arm] os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-x64@0.17.19: - resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - /@esbuild/android-x64@0.18.13: - resolution: {integrity: sha512-M2eZkRxR6WnWfVELHmv6MUoHbOqnzoTVSIxgtsyhm/NsgmL+uTmag/VVzdXvmahak1I6sOb1K/2movco5ikDJg==} - engines: {node: '>=12'} + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} cpu: [x64] os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-arm64@0.17.19: - resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - /@esbuild/darwin-arm64@0.18.13: - resolution: {integrity: sha512-f5goG30YgR1GU+fxtaBRdSW3SBG9pZW834Mmhxa6terzcboz7P2R0k4lDxlkP7NYRIIdBbWp+VgwQbmMH4yV7w==} - engines: {node: '>=12'} + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} cpu: [arm64] os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-x64@0.17.19: - resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - /@esbuild/darwin-x64@0.18.13: - resolution: {integrity: sha512-RIrxoKH5Eo+yE5BtaAIMZaiKutPhZjw+j0OCh8WdvKEKJQteacq0myZvBDLU+hOzQOZWJeDnuQ2xgSScKf1Ovw==} - engines: {node: '>=12'} + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} cpu: [x64] os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-arm64@0.17.19: - resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/freebsd-arm64@0.18.13: - resolution: {integrity: sha512-AfRPhHWmj9jGyLgW/2FkYERKmYR+IjYxf2rtSLmhOrPGFh0KCETFzSjx/JX/HJnvIqHt/DRQD/KAaVsUKoI3Xg==} - engines: {node: '>=12'} + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-x64@0.17.19: - resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/freebsd-x64@0.18.13: - resolution: {integrity: sha512-pGzWWZJBInhIgdEwzn8VHUBang8UvFKsvjDkeJ2oyY5gZtAM6BaxK0QLCuZY+qoj/nx/lIaItH425rm/hloETA==} - engines: {node: '>=12'} + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} cpu: [x64] os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm64@0.17.19: - resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-arm64@0.18.13: - resolution: {integrity: sha512-hCzZbVJEHV7QM77fHPv2qgBcWxgglGFGCxk6KfQx6PsVIdi1u09X7IvgE9QKqm38OpkzaAkPnnPqwRsltvLkIQ==} - engines: {node: '>=12'} + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} cpu: [arm64] os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm@0.17.19: - resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-arm@0.18.13: - resolution: {integrity: sha512-4iMxLRMCxGyk7lEvkkvrxw4aJeC93YIIrfbBlUJ062kilUUnAiMb81eEkVvCVoh3ON283ans7+OQkuy1uHW+Hw==} - engines: {node: '>=12'} + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} cpu: [arm] os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ia32@0.17.19: - resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-ia32@0.18.13: - resolution: {integrity: sha512-I3OKGbynl3AAIO6onXNrup/ttToE6Rv2XYfFgLK/wnr2J+1g+7k4asLrE+n7VMhaqX+BUnyWkCu27rl+62Adug==} - engines: {node: '>=12'} + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} cpu: [ia32] os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.17.19: - resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-loong64@0.18.13: - resolution: {integrity: sha512-8pcKDApAsKc6WW51ZEVidSGwGbebYw2qKnO1VyD8xd6JN0RN6EUXfhXmDk9Vc4/U3Y4AoFTexQewQDJGsBXBpg==} - engines: {node: '>=12'} + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} cpu: [loong64] os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-mips64el@0.17.19: - resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-mips64el@0.18.13: - resolution: {integrity: sha512-6GU+J1PLiVqWx8yoCK4Z0GnfKyCGIH5L2KQipxOtbNPBs+qNDcMJr9euxnyJ6FkRPyMwaSkjejzPSISD9hb+gg==} - engines: {node: '>=12'} + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} cpu: [mips64el] os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ppc64@0.17.19: - resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-ppc64@0.18.13: - resolution: {integrity: sha512-pfn/OGZ8tyR8YCV7MlLl5hAit2cmS+j/ZZg9DdH0uxdCoJpV7+5DbuXrR+es4ayRVKIcfS9TTMCs60vqQDmh+w==} - engines: {node: '>=12'} + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} cpu: [ppc64] os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-riscv64@0.17.19: - resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-riscv64@0.18.13: - resolution: {integrity: sha512-aIbhU3LPg0lOSCfVeGHbmGYIqOtW6+yzO+Nfv57YblEK01oj0mFMtvDJlOaeAZ6z0FZ9D13oahi5aIl9JFphGg==} - engines: {node: '>=12'} + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} cpu: [riscv64] os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-s390x@0.17.19: - resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-s390x@0.18.13: - resolution: {integrity: sha512-Pct1QwF2sp+5LVi4Iu5Y+6JsGaV2Z2vm4O9Dd7XZ5tKYxEHjFtb140fiMcl5HM1iuv6xXO8O1Vrb1iJxHlv8UA==} - engines: {node: '>=12'} + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} cpu: [s390x] os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-x64@0.17.19: - resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/linux-x64@0.18.13: - resolution: {integrity: sha512-zTrIP0KzYP7O0+3ZnmzvUKgGtUvf4+piY8PIO3V8/GfmVd3ZyHJGz7Ht0np3P1wz+I8qJ4rjwJKqqEAbIEPngA==} - engines: {node: '>=12'} + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} cpu: [x64] os: [linux] - requiresBuild: true - dev: true - optional: true - /@esbuild/netbsd-x64@0.17.19: - resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] os: [netbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/netbsd-x64@0.18.13: - resolution: {integrity: sha512-I6zs10TZeaHDYoGxENuksxE1sxqZpCp+agYeW039yqFwh3MgVvdmXL5NMveImOC6AtpLvE4xG5ujVic4NWFIDQ==} - engines: {node: '>=12'} + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} cpu: [x64] os: [netbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/openbsd-x64@0.17.19: - resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] os: [openbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/openbsd-x64@0.18.13: - resolution: {integrity: sha512-W5C5nczhrt1y1xPG5bV+0M12p2vetOGlvs43LH8SopQ3z2AseIROu09VgRqydx5qFN7y9qCbpgHLx0kb0TcW7g==} - engines: {node: '>=12'} + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} cpu: [x64] os: [openbsd] - requiresBuild: true - dev: true - optional: true - /@esbuild/sunos-x64@0.17.19: - resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] - /@esbuild/sunos-x64@0.18.13: - resolution: {integrity: sha512-X/xzuw4Hzpo/yq3YsfBbIsipNgmsm8mE/QeWbdGdTTeZ77fjxI2K0KP3AlhZ6gU3zKTw1bKoZTuKLnqcJ537qw==} - engines: {node: '>=12'} + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} cpu: [x64] os: [sunos] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-arm64@0.17.19: - resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-arm64@0.18.13: - resolution: {integrity: sha512-4CGYdRQT/ILd+yLLE5i4VApMPfGE0RPc/wFQhlluDQCK09+b4JDbxzzjpgQqTPrdnP7r5KUtGVGZYclYiPuHrw==} - engines: {node: '>=12'} + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} cpu: [arm64] os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-ia32@0.17.19: - resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-ia32@0.18.13: - resolution: {integrity: sha512-D+wKZaRhQI+MUGMH+DbEr4owC2D7XnF+uyGiZk38QbgzLcofFqIOwFs7ELmIeU45CQgfHNy9Q+LKW3cE8g37Kg==} - engines: {node: '>=12'} + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} cpu: [ia32] os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-x64@0.17.19: - resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - /@esbuild/win32-x64@0.18.13: - resolution: {integrity: sha512-iVl6lehAfJS+VmpF3exKpNQ8b0eucf5VWfzR8S7xFve64NBNz2jPUgx1X93/kfnkfgP737O+i1k54SVQS7uVZA==} - engines: {node: '>=12'} + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} cpu: [x64] os: [win32] - requiresBuild: true - dev: true - optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.45.0): - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - dependencies: - eslint: 8.45.0 - eslint-visitor-keys: 3.4.1 - dev: true - /@eslint-community/regexpp@4.5.1: - resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - dev: true - /@eslint/eslintrc@2.1.0: - resolution: {integrity: sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 9.6.1 - globals: 13.20.0 - ignore: 5.2.4 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true + '@eslint/compat@2.1.1': + resolution: {integrity: sha512-rMcy8GSrwNzcISX/BlTDY/GLB4eCopEuy9woIls3To+15OLxykZrxxq+WUcylCPCQ6F4MujjBM1DX5V1aqI3Vw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^8.40 || 9 || 10 + peerDependenciesMeta: + eslint: + optional: true - /@eslint/js@8.44.0: - resolution: {integrity: sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - /@humanwhocodes/config-array@0.11.10: - resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} - engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - /@humanwhocodes/module-importer@1.0.1: + '@eslint/eslintrc@3.3.7': + resolution: {integrity: sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.3': + resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - dev: true - /@humanwhocodes/object-schema@1.2.1: - resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} - dev: true + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} - /@isaacs/cliui@8.0.2: - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - dependencies: - string-width: 5.1.2 - string-width-cjs: /string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: /strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: /wrap-ansi@7.0.0 - dev: true + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} - /@istanbuljs/schema@0.1.3: - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - dev: true + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] - /@jest/schemas@29.6.0: - resolution: {integrity: sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@sinclair/typebox': 0.27.8 - dev: true + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] - /@jridgewell/gen-mapping@0.3.3: - resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.18 - dev: true + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} + engines: {node: '>=20.9.0'} + os: [freebsd] - /@jridgewell/resolve-uri@3.1.0: - resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} - engines: {node: '>=6.0.0'} - dev: true + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} + cpu: [arm64] + os: [darwin] - /@jridgewell/set-array@1.1.2: - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} - dev: true + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} + cpu: [x64] + os: [darwin] - /@jridgewell/sourcemap-codec@1.4.14: - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} - dev: true + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} + cpu: [arm64] + os: [linux] + libc: [glibc] - /@jridgewell/sourcemap-codec@1.4.15: - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: true + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} + cpu: [arm] + os: [linux] + libc: [glibc] - /@jridgewell/trace-mapping@0.3.18: - resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 - dev: true + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} + cpu: [ppc64] + os: [linux] + libc: [glibc] - /@microsoft/api-extractor-model@7.27.4: - resolution: {integrity: sha512-HjqQFmuGPOS20rtnu+9Jj0QrqZyR59E+piUWXPMZTTn4jaZI+4UmsHSf3Id8vyueAhOBH2cgwBuRTE5R+MfSMw==} - dependencies: - '@microsoft/tsdoc': 0.14.2 - '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 3.59.5 - transitivePeerDependencies: - - '@types/node' - dev: true + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] - /@microsoft/api-extractor@7.36.2: - resolution: {integrity: sha512-ONe/jOmTZtR3OjTkWKHmeSV1P5ozbHDxHr6FV3KoWyIl1AcPk2B3dmvVBM5eOlZB5bgM66nxcWQTZ6msQo2hHg==} - hasBin: true - dependencies: - '@microsoft/api-extractor-model': 7.27.4 - '@microsoft/tsdoc': 0.14.2 - '@microsoft/tsdoc-config': 0.16.2 - '@rushstack/node-core-library': 3.59.5 - '@rushstack/rig-package': 0.4.0 - '@rushstack/ts-command-line': 4.15.1 - colors: 1.2.5 - lodash: 4.17.21 - resolve: 1.22.2 - semver: 7.3.8 - source-map: 0.6.1 - typescript: 5.0.4 - transitivePeerDependencies: - - '@types/node' - dev: true + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} + cpu: [s390x] + os: [linux] + libc: [glibc] - /@microsoft/tsdoc-config@0.16.2: - resolution: {integrity: sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==} - dependencies: - '@microsoft/tsdoc': 0.14.2 - ajv: 6.12.6 - jju: 1.4.0 - resolve: 1.19.0 - dev: true + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} + cpu: [x64] + os: [linux] + libc: [glibc] - /@microsoft/tsdoc@0.14.2: - resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} - dev: true + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} + cpu: [arm64] + os: [linux] + libc: [musl] - /@mswjs/cookies@0.2.2: - resolution: {integrity: sha512-mlN83YSrcFgk7Dm1Mys40DLssI1KdJji2CMKN8eOlBqsTADYzj2+jWzsANsUTFbxDMWPD5e9bfA1RGqBpS3O1g==} - engines: {node: '>=14'} - dependencies: - '@types/set-cookie-parser': 2.4.2 - set-cookie-parser: 2.6.0 - dev: true + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} + cpu: [x64] + os: [linux] + libc: [musl] - /@mswjs/interceptors@0.17.9: - resolution: {integrity: sha512-4LVGt03RobMH/7ZrbHqRxQrS9cc2uh+iNKSj8UWr8M26A2i793ju+csaB5zaqYltqJmA2jUq4VeYfKmVqvsXQg==} - engines: {node: '>=14'} - dependencies: - '@open-draft/until': 1.0.3 - '@types/debug': 4.1.8 - '@xmldom/xmldom': 0.8.9 - debug: 4.3.4 - headers-polyfill: 3.1.2 - outvariant: 1.4.0 - strict-event-emitter: 0.2.8 - web-encoding: 1.1.5 - transitivePeerDependencies: - - supports-color - dev: true + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] - /@next/env@13.4.10: - resolution: {integrity: sha512-3G1yD/XKTSLdihyDSa8JEsaWOELY+OWe08o0LUYzfuHp1zHDA8SObQlzKt+v+wrkkPcnPweoLH1ImZeUa0A1NQ==} - dev: false + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@inquirer/ansi@2.0.7': + resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/confirm@6.1.1': + resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@11.2.1': + resolution: {integrity: sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@2.0.7': + resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + + '@inquirer/type@4.0.7': + resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@mswjs/interceptors@0.41.9': + resolution: {integrity: sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==} + engines: {node: '>=18'} + + '@next/env@16.3.5': + resolution: {integrity: sha512-NWEXVDMqoEo0ktmU6u0sE2Vg0LOcsD7NnOTJNo3/fEaTfsg+F1bMIxuDmQbda4e3yTIQwVdUREF2yIuMOusKtg==} - /@next/swc-darwin-arm64@13.4.10: - resolution: {integrity: sha512-4bsdfKmmg7mgFGph0UorD1xWfZ5jZEw4kKRHYEeTK9bT1QnMbPVPlVXQRIiFPrhoDQnZUoa6duuPUJIEGLV1Jg==} + '@next/swc-darwin-arm64@16.3.5': + resolution: {integrity: sha512-pMmGgETfKvElucLHtVaeiMRbp2zUbvKx7b1yGko0liBz3cw1mKSggWN/Rp/wPz8z+E1O82u3r4L1Co+ZS5hokQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - requiresBuild: true - dev: false - optional: true - /@next/swc-darwin-x64@13.4.10: - resolution: {integrity: sha512-ngXhUBbcZIWZWqNbQSNxQrB9T1V+wgfCzAor2olYuo/YpaL6mUYNUEgeBMhr8qwV0ARSgKaOp35lRvB7EmCRBg==} + '@next/swc-darwin-x64@16.3.5': + resolution: {integrity: sha512-76VaGYvf6HPa5/w12yLkE3dXTn9AfdEviI79oEL3aZoAmRLc9rWitjWqyjViVysK/ht/y9YKzFkBrUdi/wGkow==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - requiresBuild: true - dev: false - optional: true - /@next/swc-linux-arm64-gnu@13.4.10: - resolution: {integrity: sha512-SjCZZCOmHD4uyM75MVArSAmF5Y+IJSGroPRj2v9/jnBT36SYFTORN8Ag/lhw81W9EeexKY/CUg2e9mdebZOwsg==} + '@next/swc-linux-arm64-gnu@16.3.5': + resolution: {integrity: sha512-zKDELJ5jSQMHeO/hmXUQsAzagX4bQD4OiMi3pQ5FbUj+yK506oLVHnKA2YXMlbg1EHHqJYtyePOgByIDXD1lqw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - requiresBuild: true - dev: false - optional: true + libc: [glibc] - /@next/swc-linux-arm64-musl@13.4.10: - resolution: {integrity: sha512-F+VlcWijX5qteoYIOxNiBbNE8ruaWuRlcYyIRK10CugqI/BIeCDzEDyrHIHY8AWwbkTwe6GRHabMdE688Rqq4Q==} + '@next/swc-linux-arm64-musl@16.3.5': + resolution: {integrity: sha512-7Vql0pgzCoHagv6+FNOZoqmJqA52c6zeVbhtS/47qFozO1MSx4ms7x7GHiciY8R5CDsSMKMQjJEryoJLcsBIbA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - requiresBuild: true - dev: false - optional: true + libc: [musl] - /@next/swc-linux-x64-gnu@13.4.10: - resolution: {integrity: sha512-WDv1YtAV07nhfy3i1visr5p/tjiH6CeXp4wX78lzP1jI07t4PnHHG1WEDFOduXh3WT4hG6yN82EQBQHDi7hBrQ==} + '@next/swc-linux-x64-gnu@16.3.5': + resolution: {integrity: sha512-NH/xzehyHEFWE2nlcZon7TB/0+H4shfWCi7S1zka815XCOhJDYZhoeJtOYy0dh0WVRWACVXSyGNFFytoMxUhRg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - requiresBuild: true - dev: false - optional: true + libc: [glibc] - /@next/swc-linux-x64-musl@13.4.10: - resolution: {integrity: sha512-zFkzqc737xr6qoBgDa3AwC7jPQzGLjDlkNmt/ljvQJ/Veri5ECdHjZCUuiTUfVjshNIIpki6FuP0RaQYK9iCRg==} + '@next/swc-linux-x64-musl@16.3.5': + resolution: {integrity: sha512-lV4+EhWMfS8jcC+EH2nn/Cm5cn6XsgbE07bU9tMH8fCo0tNAqhyzi1b5wQ/Tn6NGFTvKDY65w3ZH95EjwBRAnQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - requiresBuild: true - dev: false - optional: true + libc: [musl] - /@next/swc-win32-arm64-msvc@13.4.10: - resolution: {integrity: sha512-IboRS8IWz5mWfnjAdCekkl8s0B7ijpWeDwK2O8CdgZkoCDY0ZQHBSGiJ2KViAG6+BJVfLvcP+a2fh6cdyBr9QQ==} + '@next/swc-win32-arm64-msvc@16.3.5': + resolution: {integrity: sha512-/wKzAREX2RF++MhicjDbg8tGn2AiBIM0+EFeTFKoUEUbW5D6amCJehd5Z5G1H5/gxNdgnwoXMcHz24H/c2tGkQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - requiresBuild: true - dev: false - optional: true - - /@next/swc-win32-ia32-msvc@13.4.10: - resolution: {integrity: sha512-bSA+4j8jY4EEiwD/M2bol4uVEu1lBlgsGdvM+mmBm/BbqofNBfaZ2qwSbwE2OwbAmzNdVJRFRXQZ0dkjopTRaQ==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: false - optional: true - /@next/swc-win32-x64-msvc@13.4.10: - resolution: {integrity: sha512-g2+tU63yTWmcVQKDGY0MV1PjjqgZtwM4rB1oVVi/v0brdZAcrcTV+04agKzWtvWroyFz6IqtT0MoZJA7PNyLVw==} + '@next/swc-win32-x64-msvc@16.3.5': + resolution: {integrity: sha512-LNdCHzgLFc+UeqMS84LzXPaeBRKyqDN9OMyFAr1OrB0XrNw78IRrEVtZvvA7245W/HsaoeVOQX9jPjPk8jojwA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - requiresBuild: true - dev: false - optional: true - /@nodelib/fs.scandir@2.1.5: + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: true - /@nodelib/fs.stat@2.0.5: + '@nodelib/fs.stat@2.0.5': resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} engines: {node: '>= 8'} - dev: true - /@nodelib/fs.walk@1.2.8: + '@nodelib/fs.walk@1.2.8': resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.15.0 - dev: true - /@open-draft/until@1.0.3: - resolution: {integrity: sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==} - dev: true + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} - /@pkgjs/parseargs@0.11.0: - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - requiresBuild: true - dev: true - optional: true + '@open-draft/deferred-promise@3.0.0': + resolution: {integrity: sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==} - /@pkgr/utils@2.4.2: - resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - dependencies: - cross-spawn: 7.0.3 - fast-glob: 3.3.0 - is-glob: 4.0.3 - open: 9.1.0 - picocolors: 1.0.0 - tslib: 2.6.0 - dev: true + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} - /@remix-run/router@1.7.1: - resolution: {integrity: sha512-bgVQM4ZJ2u2CM8k1ey70o1ePFXsEzYVZoWghh6WjM8p59jQ7HxzbHW4SbnWFG7V9ig9chLawQxDTZ3xzOF8MkQ==} - engines: {node: '>=14'} + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@oxc-project/types@0.149.0': + resolution: {integrity: sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==} + + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@rolldown/binding-android-arm-eabi@1.2.8': + resolution: {integrity: sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.8': + resolution: {integrity: sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.8': + resolution: {integrity: sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.8': + resolution: {integrity: sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.8': + resolution: {integrity: sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.8': + resolution: {integrity: sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.8': + resolution: {integrity: sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.8': + resolution: {integrity: sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.8': + resolution: {integrity: sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.8': + resolution: {integrity: sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.8': + resolution: {integrity: sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.8': + resolution: {integrity: sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.8': + resolution: {integrity: sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.8': + resolution: {integrity: sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] - /@rollup/pluginutils@5.0.2: - resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} + '@rolldown/binding-win32-x64-msvc@1.2.8': + resolution: {integrity: sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 peerDependenciesMeta: rollup: optional: true - dependencies: - '@types/estree': 1.0.1 - estree-walker: 2.0.2 - picomatch: 2.3.1 - dev: true - /@rushstack/node-core-library@3.59.5: - resolution: {integrity: sha512-1IpV7LufrI1EoVO8hYsb3t6L8L+yp40Sa0OaOV2CIu1zx4e6ZeVNaVIEXFgMXBKdGXkAh21MnCaIzlDNpG6ZQw==} + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@testing-library/dom@10.4.2': + resolution: {integrity: sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@7.0.1': + resolution: {integrity: sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} peerDependencies: - '@types/node': '*' + '@testing-library/dom': '>=10 <11' + vitest: '>= 0.32' peerDependenciesMeta: - '@types/node': + vitest: optional: true - dependencies: - colors: 1.2.5 - fs-extra: 7.0.1 - import-lazy: 4.0.0 - jju: 1.4.0 - resolve: 1.22.2 - semver: 7.3.8 - z-schema: 5.0.5 - dev: true - /@rushstack/rig-package@0.4.0: - resolution: {integrity: sha512-FnM1TQLJYwSiurP6aYSnansprK5l8WUK8VG38CmAaZs29ZeL1msjK0AP1VS4ejD33G0kE/2cpsPsS9jDenBMxw==} + '@testing-library/react@16.3.3': + resolution: {integrity: sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/user-event@14.6.7': + resolution: {integrity: sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@26.5.1': + resolution: {integrity: sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==} + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@types/set-cookie-parser@2.4.10': + resolution: {integrity: sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==} + + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + + '@typescript-eslint/eslint-plugin@8.70.0': + resolution: {integrity: sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.70.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.70.0': + resolution: {integrity: sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.70.0': + resolution: {integrity: sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.70.0': + resolution: {integrity: sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.70.0': + resolution: {integrity: sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.70.0': + resolution: {integrity: sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.70.0': + resolution: {integrity: sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.70.0': + resolution: {integrity: sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.70.0': + resolution: {integrity: sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.70.0': + resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + + '@typescript/typescript6@6.0.2': + resolution: {integrity: sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==} + hasBin: true + + '@vitejs/plugin-react@6.1.1': + resolution: {integrity: sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + oxc-transform-react: ^0.145.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + oxc-transform-react: + optional: true + + '@vitest/coverage-v8@5.0.0': + resolution: {integrity: sha512-toMg6PZGCIa/lQNCDoASrfb1ly4hsUKXFtFYC9kD4t78o5Y6LyNJU7AENt8eHPr3quYdxaxK7hj2mnbFfUk9NA==} + peerDependencies: + '@vitest/browser': 5.0.0 + vitest: 5.0.0 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/istanbul-lib-coverage@1.0.1': + resolution: {integrity: sha512-k3DJZ8LhMBK9NS4SclF1ASD3OgXEWDorbIcPTRDK0/Zae6fRvu+fJRxtFdLfHsa9Y24beCdPnoNZ4LviTNstfA==} + engines: {node: '>=22'} + + '@vitest/istanbul-lib-report@1.0.1': + resolution: {integrity: sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==} + engines: {node: '>=22'} + + '@vitest/mocker@5.0.0': + resolution: {integrity: sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/spy@5.0.0': + resolution: {integrity: sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==} + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.2.0: + resolution: {integrity: sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + ast-v8-to-istanbul@1.0.6: + resolution: {integrity: sha512-fvpl29helSO2w/z7utIbrkNXILdrLwDwAMH2I/zPKlGf5244+gf+B4cyS1sANcrPY2h+hWCGSgC8N61s/+AF9A==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.13.0: + resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.11.23: + resolution: {integrity: sha512-le521dGVfxM7yRX0EikCoSz+rOK+hHzdDt/E7mG1jOJB/6WAAUuwVroLwaB7ApaUsz5Q0kFlDXLSA9MheUIfRQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + bootstrap@5.3.8: + resolution: {integrity: sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==} + peerDependencies: + '@popperjs/core': ^2.11.8 + + brace-expansion@1.1.18: + resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.9: + resolution: {integrity: sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + cacheable@2.5.0: + resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + comment-parser@1.4.9: + resolution: {integrity: sha512-+2AvZKjJaNq9GQljm3tr0utwrig5BL+jgJGvz5rDpGKNIp+ojcRXWUwBbh/sGIi1QCprR6PsKFakub+w1v+4hQ==} + engines: {node: '>= 12.0.0'} + + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.3.1: + resolution: {integrity: sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + copy-file@11.1.0: + resolution: {integrity: sha512-X8XDzyvYaA6msMyAM575CUoygY5b44QzLcGRKsK3MFmXcOvQa518dNPLsKYwkYsn72g3EiW+LE0ytd/FlqWmyw==} + engines: {node: '>=18'} + + copyfiles@2.4.1: + resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==} + hasBin: true + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cpy-cli@7.0.0: + resolution: {integrity: sha512-uGCdhIxGfZcPXidCuT8w1jBknVXFx0un7NLjzqBZcdnkIWtLUnWMPk5TC37ceoVjwASLSNsRtTXXNTuFIyE2ng==} + engines: {node: '>=20'} + hasBin: true + + cpy@13.2.3: + resolution: {integrity: sha512-N+jPR8ODTJIzAKmCtUglRSCQzMuDkB/saH1ZJfY1MDfXYMZbfsuih3GhpUEYeuTCDQLBr4GvKyHDwV0Y2Sd0gQ==} + engines: {node: '>=20'} + + cross-env@10.1.0: + resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} + engines: {node: '>=20'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.427: + resolution: {integrity: sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enhanced-resolve@5.25.1: + resolution: {integrity: sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==} + engines: {node: '>=10.13.0'} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.4.0: + resolution: {integrity: sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} + engines: {node: '>= 0.4'} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-compat-utils@0.5.1: + resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} + + eslint-module-utils@2.14.0: + resolution: {integrity: sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-es-x@7.8.0: + resolution: {integrity: sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '>=8' + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-n@18.3.0: + resolution: {integrity: sha512-cPVguuDe6DrIPb/qUXHf8P89MaVTUmiYWwpt5gX5AILsvRIiZAxMFXcFR6QHYBksqKJpjfUBlL/RleCJUWcD7w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: '>=8.57.1' + ts-declaration-location: ^1.0.6 + typescript: '>=5.0.0' + peerDependenciesMeta: + ts-declaration-location: + optional: true + typescript: + optional: true + + eslint-plugin-no-only-tests@3.4.0: + resolution: {integrity: sha512-4S3/9Nb7A2tiMcpzEQE9bQSlpeOz6WJkgryBuou/SA8W2x2c8Zf4j0NvTKBjv6qNhF9T79tmkecm/0CHqV0UGg==} + engines: {node: '>=5.0.0'} + + eslint-plugin-prettier@5.5.6: + resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-plugin-regexp@3.3.0: + resolution: {integrity: sha512-TT0JTQbW7CRKohntpGAsMdQ1K3MsmJK4gdAhKJtIwlJ0JvuYxauymFgJdvqxIY4lqNj0EuRZWVrymQtEdJYEFg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: '>=9.38.0' + + eslint-plugin-simple-import-sort@14.0.0: + resolution: {integrity: sha512-NUJO0+XFCkk+o5EsAJruTgnfMEpeWrPWeJS15UVF60GgXmqz1BJ9/3hzlvG7lkL8Bubzos5cCLptThbFfPnSMQ==} + peerDependencies: + eslint: '>=5.0.0' + + eslint-plugin-testing-library@7.16.2: + resolution: {integrity: sha512-8gleGnQXK2ZA3hHwjCwpYTZvM+9VsrJ+/9kDI8CjqAQGAdMQOdn/rJNu7ZySENuiWlGKQWyZJ4ZjEg2zamaRHw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.10.0: + resolution: {integrity: sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + exsolve@1.1.1: + resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@11.1.5: + resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@6.1.23: + resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@16.2.4: + resolution: {integrity: sha512-c8B/VNLmxRcmqqenRA9t+9IyOjf9+V6lTxPaUJLqOCONdQkWZ0ETYgX0qbtJqPsgCNusT9MZ5Jeidw8Eb9tn2g==} + engines: {node: '>=20'} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphql@16.14.2: + resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + headers-polyfill@5.0.1: + resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.2.0: + resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.9: + resolution: {integrity: sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {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. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + jsdoc-type-pratt-parser@9.2.1: + resolution: {integrity: sha512-V4Ww4EHnTcTLSOMoB0FsF72JhQvcAsriCm/LWnxJeGWoxIjEL2l9na11abQok5SYShq8m0Gl02el/xAbTCulvQ==} + engines: {node: ^22.22.2 || >=24.15.0} + + jsdom@30.0.1: + resolution: {integrity: sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + peerDependencies: + canvas: ^3.2.3 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + junk@4.0.1: + resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==} + engines: {node: '>=12.20'} + + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + local-pkg@1.2.1: + resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} + engines: {node: '>=14'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magic-string@1.3.1: + resolution: {integrity: sha512-rm91zr2Ou+XueDTohjQQjdQEcYM6zVi8KVUCG8Ec3vHwUEKrhSdCNyfuIywkA6hcCAteIn0ZOtAHA6eGpiX+Pg==} + + magicast@0.5.5: + resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + meow@14.1.0: + resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==} + engines: {node: '>=20'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msw@2.15.0: + resolution: {integrity: sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@3.0.0: + resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} + engines: {node: ^20.17.0 || >=22.9.0} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next@16.3.5: + resolution: {integrity: sha512-MdtsTgzyfCPRLC6uJ1mN8ao7lyJ4BB0U6Inhnx3gta1UcCIdHK3yxLG0E8OWQteWD8/Q0qb8A5o7wJaL8M9y2w==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-exports-info@1.6.2: + resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} + engines: {node: '>= 0.4'} + + node-releases@2.0.55: + resolution: {integrity: sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==} + engines: {node: '>=18'} + + noms@0.0.0: + resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + own-keys@1.0.2: + resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==} + engines: {node: '>= 0.4'} + + p-event@6.0.1: + resolution: {integrity: sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==} + engines: {node: '>=16.17'} + + p-filter@4.1.0: + resolution: {integrity: sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==} + engines: {node: '>=18'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map@7.0.8: + resolution: {integrity: sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw==} + engines: {node: '>=18'} + + p-timeout@6.1.4: + resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} + engines: {node: '>=14.16'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.3: + resolution: {integrity: sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qified@0.10.1: + resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} + engines: {node: '>=20'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.3.0: + resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==} + peerDependencies: + react: ^19.3.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-router-dom@7.18.3: + resolution: {integrity: sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.18.3: + resolution: {integrity: sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.3.0: + resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} + engines: {node: '>=0.10.0'} + + readable-stream@1.0.34: + resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + refa@0.12.1: + resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp-ast-analysis@0.7.1: + resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true + + rettime@0.11.11: + resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} + engines: {node: 20 || >=22} + hasBin: true + + rolldown@1.2.8: + resolution: {integrity: sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + scslre@0.3.0: + resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} + engines: {node: ^14.0.0 || >=16.0.0} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + set-cookie-parser@3.1.1: + resolution: {integrity: sha512-vM9SUhjsUYs6UeJUmygc5Ofm5eQGe85riob5ju6XCgFGJI5PLV4nrDAQpQjd+LkFBpAkADn5BQQpZ9EUNkyLuA==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.1.0: + resolution: {integrity: sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@0.10.31: + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + 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 + + styled-jsx@5.1.7: + resolution: {integrity: sha512-HPLmEIYprxCeWDMLYiaaAhsV3yGfIlCqzuVOybE6fjF3SUJmH67nCoMDO+nAvHNHo46OfvpCNu4Rcue82dMNFg==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + 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 + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} + engines: {node: ^14.18.0 || >=16.0.0} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + + tinybench@6.1.4: + resolution: {integrity: sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==} + engines: {node: '>=20.0.0'} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + tldts-core@7.4.7: + resolution: {integrity: sha512-rNlAI8fKn/JckBMUSbNL/ES2kmDiurWaE49l+ikwEc9A6lFR7gMx9AhgQMQKBK4H5w4pKLH64JzZfB99uRsGNQ==} + + tldts@7.4.7: + resolution: {integrity: sha512-56L0/9HELHSsG1bFCzay8UoLxzRL7kpFf7Wl5q/kSYwiSJGACvro61xnKzPNM+SadxllzdtXsKDSXE7HPeqIAw==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.23.13: + resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@5.8.0: + resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} + engines: {node: '>=20'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} + + undici@8.10.2: + resolution: {integrity: sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==} + engines: {node: '>=22.19.0'} + + unicorn-magic@0.4.0: + resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} + engines: {node: '>=20'} + + unplugin-dts@1.1.0: + resolution: {integrity: sha512-KZJ+qk+lmd9dJY/PJvDRFL9BUtVubKDiX7Ai/X6mlkCArcQNzwmupKjVqHbRA42uqO5ZfFRFc+o8/OCbQ1GonQ==} + peerDependencies: + '@microsoft/api-extractor': '>=7' + '@rspack/core': ^1 + '@vue/language-core': ^3.1.5 + esbuild: '*' + rolldown: '*' + rollup: '>=3' + typescript: '>=4' + vite: '>=3' + webpack: ^4 || ^5 + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@rspack/core': + optional: true + '@vue/language-core': + optional: true + esbuild: + optional: true + rolldown: + optional: true + rollup: + optional: true + vite: + optional: true + webpack: + optional: true + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + + untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + + update-browserslist-db@1.3.3: + resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-plugin-dts@5.1.0: + resolution: {integrity: sha512-MfLc2G+mXPUDGGxpHQeYbdunavFP9UpT9yxiAsTgUXRxfEd3HqysSAM4E72jhz5eIz3OOexCHkqiVlQq8ok9/A==} + peerDependencies: + '@microsoft/api-extractor': '>=7' + rollup: '>=3' + vite: '>=3' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + rollup: + optional: true + vite: + optional: true + + vite@8.3.0: + resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@5.0.0: + resolution: {integrity: sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.0 + '@vitest/browser-preview': 5.0.0 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.0 + '@vitest/coverage-v8': 5.0.0 + '@vitest/ui': 5.0.0 + happy-dom: '*' + jsdom: '*' + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-uri@3.2.0: + resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + web-vitals@6.2.1: + resolution: {integrity: sha512-rLcLXA2sx6+9dE88NHFubwTtGxpK4yYBLj6qHPdFoCaLr0cXGb4efOqtKLlm4loGA4OEKHIQKMKzZkKyOh5ctw==} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + whatwg-url@17.1.1: + resolution: {integrity: sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==} + engines: {node: ^22.14.0 || >=24.0.0} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@16.2.2: + resolution: {integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==} + engines: {node: '>=10'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.6.5: + resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} + +snapshots: + + '@adobe/css-tools@4.5.0': {} + + '@asamuzakjp/css-color@6.0.7': + dependencies: + '@csstools/css-calc': 3.4.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.2.3(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.5.2 + + '@asamuzakjp/dom-selector@8.3.2': + dependencies: + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.9 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@cacheable/memory@2.2.0': + dependencies: + '@cacheable/utils': 2.5.0 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.5.0': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 + + '@csstools/color-helpers@6.1.1': {} + + '@csstools/css-calc@3.4.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.2.3(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.1 + '@csstools/css-calc': 3.4.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.13(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@epic-web/invariant@1.0.0': {} + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@10.10.0)': + dependencies: + eslint: 10.10.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/compat@2.1.1(eslint@10.10.0)': + dependencies: + '@eslint/core': 1.2.1 + optionalDependencies: + eslint: 10.10.0 + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.6 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.7.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.7': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.2 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@10.0.1(eslint@10.10.0)': + optionalDependencies: + eslint: 10.10.0 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.3': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@exodus/bytes@1.15.1': {} + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.3 + optional: true + + '@img/sharp-darwin-x64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.3 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.4': + dependencies: + '@img/sharp-wasm32': 0.35.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.3': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.3': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.3': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.3': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.3': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + optional: true + + '@img/sharp-linux-arm64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.3 + optional: true + + '@img/sharp-linux-arm@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.3 + optional: true + + '@img/sharp-linux-ppc64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.3 + optional: true + + '@img/sharp-linux-riscv64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.3 + optional: true + + '@img/sharp-linux-s390x@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.3 + optional: true + + '@img/sharp-linux-x64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.3 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + optional: true + + '@img/sharp-wasm32@0.35.4': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.4': + dependencies: + '@img/sharp-wasm32': 0.35.4 + optional: true + + '@img/sharp-win32-arm64@0.35.4': + optional: true + + '@img/sharp-win32-ia32@0.35.4': + optional: true + + '@img/sharp-win32-x64@0.35.4': + optional: true + + '@inquirer/ansi@2.0.7': {} + + '@inquirer/confirm@6.1.1(@types/node@26.5.1)': + dependencies: + '@inquirer/core': 11.2.1(@types/node@26.5.1) + '@inquirer/type': 4.0.7(@types/node@26.5.1) + optionalDependencies: + '@types/node': 26.5.1 + + '@inquirer/core@11.2.1(@types/node@26.5.1)': + dependencies: + '@inquirer/ansi': 2.0.7 + '@inquirer/figures': 2.0.7 + '@inquirer/type': 4.0.7(@types/node@26.5.1) + cli-width: 4.1.0 + fast-wrap-ansi: 0.2.2 + mute-stream: 3.0.0 + signal-exit: 4.1.0 + optionalDependencies: + '@types/node': 26.5.1 + + '@inquirer/figures@2.0.7': {} + + '@inquirer/type@4.0.7(@types/node@26.5.1)': + optionalDependencies: + '@types/node': 26.5.1 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': dependencies: - resolve: 1.22.2 - strip-json-comments: 3.1.1 - dev: true + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 - /@rushstack/ts-command-line@4.15.1: - resolution: {integrity: sha512-EL4jxZe5fhb1uVL/P/wQO+Z8Rc8FMiWJ1G7VgnPDvdIt5GVjRfK7vwzder1CZQiX3x0PY6uxENYLNGTFd1InRQ==} + '@keyv/bigmap@1.3.1(keyv@5.6.0)': dependencies: - '@types/argparse': 1.0.38 - argparse: 1.0.10 - colors: 1.2.5 - string-argv: 0.3.2 - dev: true + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 - /@sinclair/typebox@0.27.8: - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - dev: true + '@keyv/serialize@1.1.1': {} - /@swc/helpers@0.5.1: - resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==} + '@mswjs/interceptors@0.41.9': dependencies: - tslib: 2.6.0 - dev: false + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 - /@testing-library/dom@8.20.1: - resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==} - engines: {node: '>=12'} - dependencies: - '@babel/code-frame': 7.22.5 - '@babel/runtime': 7.22.6 - '@types/aria-query': 5.0.1 - aria-query: 5.1.3 - chalk: 4.1.2 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - pretty-format: 27.5.1 - dev: true + '@next/env@16.3.5': {} - /@testing-library/dom@9.3.1: - resolution: {integrity: sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==} - engines: {node: '>=14'} - dependencies: - '@babel/code-frame': 7.22.5 - '@babel/runtime': 7.22.6 - '@types/aria-query': 5.0.1 - aria-query: 5.1.3 - chalk: 4.1.2 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - pretty-format: 27.5.1 + '@next/swc-darwin-arm64@16.3.5': + optional: true + + '@next/swc-darwin-x64@16.3.5': + optional: true + + '@next/swc-linux-arm64-gnu@16.3.5': + optional: true + + '@next/swc-linux-arm64-musl@16.3.5': + optional: true + + '@next/swc-linux-x64-gnu@16.3.5': + optional: true + + '@next/swc-linux-x64-musl@16.3.5': + optional: true + + '@next/swc-win32-arm64-msvc@16.3.5': + optional: true + + '@next/swc-win32-x64-msvc@16.3.5': + optional: true - /@testing-library/jest-dom@5.16.5: - resolution: {integrity: sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==} - engines: {node: '>=8', npm: '>=6', yarn: '>=1'} + '@nodelib/fs.scandir@2.1.5': dependencies: - '@adobe/css-tools': 4.2.0 - '@babel/runtime': 7.22.6 - '@types/testing-library__jest-dom': 5.14.8 - aria-query: 5.3.0 - chalk: 3.0.0 - css.escape: 1.5.1 - dom-accessibility-api: 0.5.16 - lodash: 4.17.21 - redent: 3.0.0 + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 - /@testing-library/react@13.3.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-DB79aA426+deFgGSjnf5grczDPiL4taK3hFaa+M5q7q20Kcve9eQottOG5kZ74KEr55v0tU2CQormSSDK87zYQ==} - engines: {node: '>=12'} - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - dependencies: - '@babel/runtime': 7.22.6 - '@testing-library/dom': 8.20.1 - '@types/react-dom': 18.2.7 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: true - - /@testing-library/user-event@13.5.0(@testing-library/dom@9.3.1): - resolution: {integrity: sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==} - engines: {node: '>=10', npm: '>=6'} - peerDependencies: - '@testing-library/dom': '>=7.21.4' + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': dependencies: - '@babel/runtime': 7.22.6 - '@testing-library/dom': 9.3.1 - dev: false + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.3 - /@testing-library/user-event@14.4.3(@testing-library/dom@9.3.1): - resolution: {integrity: sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - '@testing-library/dom': '>=7.21.4' + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/deferred-promise@3.0.0': {} + + '@open-draft/logger@0.3.0': dependencies: - '@testing-library/dom': 9.3.1 - dev: true + is-node-process: 1.2.0 + outvariant: 1.4.3 - /@tootallnate/once@2.0.0: - resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} - engines: {node: '>= 10'} - dev: true + '@open-draft/until@2.1.0': {} - /@types/argparse@1.0.38: - resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} - dev: true + '@oxc-project/types@0.149.0': {} - /@types/aria-query@5.0.1: - resolution: {integrity: sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==} + '@pkgr/core@0.3.6': {} - /@types/chai-subset@1.3.3: - resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} - dependencies: - '@types/chai': 4.3.5 - dev: true + '@popperjs/core@2.11.8': {} + + '@rolldown/binding-android-arm-eabi@1.2.8': + optional: true - /@types/chai@4.3.5: - resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==} - dev: true + '@rolldown/binding-android-arm64@1.2.8': + optional: true - /@types/cookie@0.4.1: - resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} - dev: true + '@rolldown/binding-darwin-arm64@1.2.8': + optional: true - /@types/debug@4.1.8: - resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==} - dependencies: - '@types/ms': 0.7.31 - dev: true + '@rolldown/binding-darwin-x64@1.2.8': + optional: true - /@types/estree@1.0.1: - resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} - dev: true + '@rolldown/binding-freebsd-x64@1.2.8': + optional: true - /@types/istanbul-lib-coverage@2.0.4: - resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} - dev: true + '@rolldown/binding-linux-arm-gnueabihf@1.2.8': + optional: true - /@types/jest@27.5.2: - resolution: {integrity: sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA==} - dependencies: - jest-matcher-utils: 27.5.1 - pretty-format: 27.5.1 + '@rolldown/binding-linux-arm64-gnu@1.2.8': + optional: true - /@types/js-levenshtein@1.1.1: - resolution: {integrity: sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g==} - dev: true + '@rolldown/binding-linux-arm64-musl@1.2.8': + optional: true - /@types/json-schema@7.0.12: - resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} - dev: true + '@rolldown/binding-linux-ppc64-gnu@1.2.8': + optional: true - /@types/json5@0.0.29: - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - dev: true + '@rolldown/binding-linux-s390x-gnu@1.2.8': + optional: true - /@types/minimist@1.2.2: - resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} - dev: true + '@rolldown/binding-linux-x64-gnu@1.2.8': + optional: true - /@types/ms@0.7.31: - resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} - dev: true + '@rolldown/binding-linux-x64-musl@1.2.8': + optional: true - /@types/node@18.11.9: - resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==} - dev: true + '@rolldown/binding-openharmony-arm64@1.2.8': + optional: true - /@types/normalize-package-data@2.4.1: - resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} - dev: true + '@rolldown/binding-win32-arm64-msvc@1.2.8': + optional: true - /@types/prop-types@15.7.5: - resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} - dev: true + '@rolldown/binding-win32-x64-msvc@1.2.8': + optional: true - /@types/react-dom@18.2.7: - resolution: {integrity: sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==} - dependencies: - '@types/react': 18.2.15 - dev: true + '@rolldown/pluginutils@1.0.1': {} - /@types/react@18.2.15: - resolution: {integrity: sha512-oEjE7TQt1fFTFSbf8kkNuc798ahTUzn3Le67/PWjE8MAfYAD/qB7O8hSTcromLFqHCt9bcdOg5GXMokzTjJ5SA==} + '@rollup/pluginutils@5.4.0': dependencies: - '@types/prop-types': 15.7.5 - '@types/scheduler': 0.16.3 - csstype: 3.1.2 - dev: true + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.7 - /@types/scheduler@0.16.3: - resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} - dev: true + '@rtsao/scc@1.1.0': {} - /@types/semver@7.5.0: - resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} - dev: true + '@sindresorhus/merge-streams@4.0.0': {} - /@types/set-cookie-parser@2.4.2: - resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==} + '@swc/helpers@0.5.23': dependencies: - '@types/node': 18.11.9 - dev: true + tslib: 2.8.1 - /@types/testing-library__jest-dom@5.14.8: - resolution: {integrity: sha512-NRfJE9Cgpmu4fx716q9SYmU4jxxhYRU1BQo239Txt/9N3EC745XZX1Yl7h/SBIDlo1ANVOCRB4YDXjaQdoKCHQ==} + '@testing-library/dom@10.4.2': dependencies: - '@types/jest': 27.5.2 + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 - /@typescript-eslint/eslint-plugin@5.50.0(@typescript-eslint/parser@5.62.0)(eslint@8.45.0)(typescript@5.1.6): - resolution: {integrity: sha512-vwksQWSFZiUhgq3Kv7o1Jcj0DUNylwnIlGvKvLLYsq8pAWha6/WCnXUeaSoNNha/K7QSf2+jvmkxggC1u3pIwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/parser': ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@testing-library/jest-dom@7.0.1(@testing-library/dom@10.4.2)(vitest@5.0.0)': dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.45.0)(typescript@5.1.6) - '@typescript-eslint/scope-manager': 5.50.0 - '@typescript-eslint/type-utils': 5.50.0(eslint@8.45.0)(typescript@5.1.6) - '@typescript-eslint/utils': 5.50.0(eslint@8.45.0)(typescript@5.1.6) - debug: 4.3.4 - eslint: 8.45.0 - grapheme-splitter: 1.0.4 - ignore: 5.2.4 - natural-compare-lite: 1.4.0 - regexpp: 3.2.0 - semver: 7.5.4 - tsutils: 3.21.0(typescript@5.1.6) - typescript: 5.1.6 - transitivePeerDependencies: - - supports-color - dev: true + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.2 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + optionalDependencies: + vitest: 5.0.0(@types/node@26.5.1)(@vitest/coverage-v8@5.0.0)(jsdom@30.0.1)(msw@2.15.0(@types/node@26.5.1)(@typescript/typescript6@6.0.2))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)) - /@typescript-eslint/parser@5.62.0(eslint@8.45.0)(typescript@5.1.6): - resolution: {integrity: sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@testing-library/react@16.3.3(@testing-library/dom@10.4.2)(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': dependencies: - '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.1.6) - debug: 4.3.4 - eslint: 8.45.0 - typescript: 5.1.6 - transitivePeerDependencies: - - supports-color - dev: true + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.2 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + optionalDependencies: + '@types/react': 19.3.0 - /@typescript-eslint/scope-manager@5.50.0: - resolution: {integrity: sha512-rt03kaX+iZrhssaT974BCmoUikYtZI24Vp/kwTSy841XhiYShlqoshRFDvN1FKKvU2S3gK+kcBW1EA7kNUrogg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@testing-library/user-event@14.6.7(@testing-library/dom@10.4.2)': dependencies: - '@typescript-eslint/types': 5.50.0 - '@typescript-eslint/visitor-keys': 5.50.0 - dev: true + '@testing-library/dom': 10.4.2 - /@typescript-eslint/scope-manager@5.62.0: - resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/visitor-keys': 5.62.0 - dev: true + '@types/aria-query@5.0.4': {} - /@typescript-eslint/type-utils@5.50.0(eslint@8.45.0)(typescript@5.1.6): - resolution: {integrity: sha512-dcnXfZ6OGrNCO7E5UY/i0ktHb7Yx1fV6fnQGGrlnfDhilcs6n19eIRcvLBqx6OQkrPaFlDPk3OJ0WlzQfrV0bQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '*' - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@types/chai@5.2.3': dependencies: - '@typescript-eslint/typescript-estree': 5.50.0(typescript@5.1.6) - '@typescript-eslint/utils': 5.50.0(eslint@8.45.0)(typescript@5.1.6) - debug: 4.3.4 - eslint: 8.45.0 - tsutils: 3.21.0(typescript@5.1.6) - typescript: 5.1.6 - transitivePeerDependencies: - - supports-color - dev: true + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 - /@typescript-eslint/types@5.50.0: - resolution: {integrity: sha512-atruOuJpir4OtyNdKahiHZobPKFvZnBnfDiyEaBf6d9vy9visE7gDjlmhl+y29uxZ2ZDgvXijcungGFjGGex7w==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true + '@types/deep-eql@4.0.2': {} - /@typescript-eslint/types@5.62.0: - resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true + '@types/esrecurse@4.3.1': {} - /@typescript-eslint/typescript-estree@5.50.0(typescript@5.1.6): - resolution: {integrity: sha512-Gq4zapso+OtIZlv8YNAStFtT6d05zyVCK7Fx3h5inlLBx2hWuc/0465C2mg/EQDDU2LKe52+/jN4f0g9bd+kow==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 5.50.0 - '@typescript-eslint/visitor-keys': 5.50.0 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.5.4 - tsutils: 3.21.0(typescript@5.1.6) - typescript: 5.1.6 - transitivePeerDependencies: - - supports-color - dev: true + '@types/estree@1.0.9': {} - /@typescript-eslint/typescript-estree@5.62.0(typescript@5.1.6): - resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: 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.5.4 - tsutils: 3.21.0(typescript@5.1.6) - typescript: 5.1.6 - transitivePeerDependencies: - - supports-color - dev: true + '@types/json-schema@7.0.15': {} - /@typescript-eslint/utils@5.50.0(eslint@8.45.0)(typescript@5.1.6): - resolution: {integrity: sha512-v/AnUFImmh8G4PH0NDkf6wA8hujNNcrwtecqW4vtQ1UOSNBaZl49zP1SHoZ/06e+UiwzHpgb5zP5+hwlYYWYAw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@types/json-schema': 7.0.12 - '@types/semver': 7.5.0 - '@typescript-eslint/scope-manager': 5.50.0 - '@typescript-eslint/types': 5.50.0 - '@typescript-eslint/typescript-estree': 5.50.0(typescript@5.1.6) - eslint: 8.45.0 - eslint-scope: 5.1.1 - eslint-utils: 3.0.0(eslint@8.45.0) - semver: 7.5.4 - transitivePeerDependencies: - - supports-color - - typescript - dev: true + '@types/json5@0.0.29': {} - /@typescript-eslint/utils@5.62.0(eslint@8.45.0)(typescript@5.1.6): - resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.45.0) - '@types/json-schema': 7.0.12 - '@types/semver': 7.5.0 - '@typescript-eslint/scope-manager': 5.62.0 - '@typescript-eslint/types': 5.62.0 - '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.1.6) - eslint: 8.45.0 - eslint-scope: 5.1.1 - semver: 7.5.4 - transitivePeerDependencies: - - supports-color - - typescript - dev: true + '@types/node@26.5.1': + dependencies: + undici-types: 8.9.0 - /@typescript-eslint/visitor-keys@5.50.0: - resolution: {integrity: sha512-cdMeD9HGu6EXIeGOh2yVW6oGf9wq8asBgZx7nsR/D36gTfQ0odE5kcRYe5M81vjEFAcPeugXrHg78Imu55F6gg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@types/react@19.3.0': dependencies: - '@typescript-eslint/types': 5.50.0 - eslint-visitor-keys: 3.4.1 - dev: true + csstype: 3.2.3 - /@typescript-eslint/visitor-keys@5.62.0: - resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@types/set-cookie-parser@2.4.10': dependencies: - '@typescript-eslint/types': 5.62.0 - eslint-visitor-keys: 3.4.1 - dev: true + '@types/node': 26.5.1 - /@vitejs/plugin-react@4.0.3(vite@4.4.4): - resolution: {integrity: sha512-pwXDog5nwwvSIzwrvYYmA2Ljcd/ZNlcsSG2Q9CNDBwnsd55UGAyr2doXtB5j+2uymRCnCfExlznzzSFbBRcoCg==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - vite: ^4.2.0 + '@types/statuses@2.0.6': {} + + '@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0))(@typescript/typescript6@6.0.2)(eslint@10.10.0)': dependencies: - '@babel/core': 7.22.9 - '@babel/plugin-transform-react-jsx-self': 7.22.5(@babel/core@7.22.9) - '@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.22.9) - react-refresh: 0.14.0 - vite: 4.4.4(@types/node@18.11.9) + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0) + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/type-utils': 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0) + '@typescript-eslint/utils': 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0) + '@typescript-eslint/visitor-keys': 8.70.0 + eslint: 10.10.0 + ignore: 7.0.9 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - dev: true - /@vitest/coverage-c8@0.33.0(vitest@0.33.0): - resolution: {integrity: sha512-DaF1zJz4dcOZS4k/neiQJokmOWqsGXwhthfmUdPGorXIQHjdPvV6JQSYhQDI41MyI8c+IieQUdIDs5XAMHtDDw==} - peerDependencies: - vitest: '>=0.30.0 <1' - dependencies: - '@ampproject/remapping': 2.2.1 - c8: 7.14.0 - magic-string: 0.30.1 - picocolors: 1.0.0 - std-env: 3.3.3 - vitest: 0.33.0(jsdom@22.1.0) - dev: true - - /@vitest/coverage-v8@0.33.0(vitest@0.33.0): - resolution: {integrity: sha512-Rj5IzoLF7FLj6yR7TmqsfRDSeaFki6NAJ/cQexqhbWkHEV2htlVGrmuOde3xzvFsCbLCagf4omhcIaVmfU8Okg==} - peerDependencies: - vitest: '>=0.32.0 <1' - dependencies: - '@ampproject/remapping': 2.2.1 - '@bcoe/v8-coverage': 0.2.3 - istanbul-lib-coverage: 3.2.0 - istanbul-lib-report: 3.0.0 - istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.1.5 - magic-string: 0.30.1 - picocolors: 1.0.0 - std-env: 3.3.3 - test-exclude: 6.0.0 - v8-to-istanbul: 9.1.0 - vitest: 0.33.0(jsdom@22.1.0) + '@typescript-eslint/parser@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0)': + dependencies: + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/typescript-estree': 8.70.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/visitor-keys': 8.70.0 + debug: 4.4.3 + eslint: 10.10.0 + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - dev: true - - /@vitest/expect@0.33.0: - resolution: {integrity: sha512-sVNf+Gla3mhTCxNJx+wJLDPp/WcstOe0Ksqz4Vec51MmgMth/ia0MGFEkIZmVGeTL5HtjYR4Wl/ZxBxBXZJTzQ==} - dependencies: - '@vitest/spy': 0.33.0 - '@vitest/utils': 0.33.0 - chai: 4.3.7 - dev: true - /@vitest/runner@0.33.0: - resolution: {integrity: sha512-UPfACnmCB6HKRHTlcgCoBh6ppl6fDn+J/xR8dTufWiKt/74Y9bHci5CKB8tESSV82zKYtkBJo9whU3mNvfaisg==} + '@typescript-eslint/project-service@8.70.0(@typescript/typescript6@6.0.2)': dependencies: - '@vitest/utils': 0.33.0 - p-limit: 4.0.0 - pathe: 1.1.1 - dev: true - - /@vitest/snapshot@0.33.0: - resolution: {integrity: sha512-tJjrl//qAHbyHajpFvr8Wsk8DIOODEebTu7pgBrP07iOepR5jYkLFiqLq2Ltxv+r0uptUb4izv1J8XBOwKkVYA==} - dependencies: - magic-string: 0.30.1 - pathe: 1.1.1 - pretty-format: 29.6.1 - dev: true + '@typescript-eslint/tsconfig-utils': 8.70.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.70.0 + debug: 4.4.3 + typescript: '@typescript/typescript6@6.0.2' + transitivePeerDependencies: + - supports-color - /@vitest/spy@0.33.0: - resolution: {integrity: sha512-Kv+yZ4hnH1WdiAkPUQTpRxW8kGtH8VRTnus7ZTGovFYM1ZezJpvGtb9nPIjPnptHbsyIAxYZsEpVPYgtpjGnrg==} + '@typescript-eslint/scope-manager@8.70.0': dependencies: - tinyspy: 2.1.1 - dev: true + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/visitor-keys': 8.70.0 - /@vitest/utils@0.33.0: - resolution: {integrity: sha512-pF1w22ic965sv+EN6uoePkAOTkAPWM03Ri/jXNyMIKBb/XHLDPfhLvf/Fa9g0YECevAIz56oVYXhodLvLQ/awA==} + '@typescript-eslint/tsconfig-utils@8.70.0(@typescript/typescript6@6.0.2)': dependencies: - diff-sequences: 29.4.3 - loupe: 2.3.6 - pretty-format: 29.6.1 - dev: true + typescript: '@typescript/typescript6@6.0.2' - /@volar/language-core@1.9.0: - resolution: {integrity: sha512-+PTRrGanAD2PxqMty0ZC46xhgW5BWzb67RLHhZyB3Im4+eMXsKlYjFUt7Z8ZCwTWQQOnj8NQ6gSgUEoOTwAHrQ==} + '@typescript-eslint/type-utils@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0)': dependencies: - '@volar/source-map': 1.9.0 - dev: true + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/typescript-estree': 8.70.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/utils': 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0) + debug: 4.4.3 + eslint: 10.10.0 + ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) + typescript: '@typescript/typescript6@6.0.2' + transitivePeerDependencies: + - supports-color - /@volar/source-map@1.9.0: - resolution: {integrity: sha512-TQWLY8ozUOHBHTMC2pHZsNbtM25Q9QCEwAL8JFR/gmR9Yv0d9qup/gQdd5sDI7RmoPYKD+gqjLrbM4Ib41QSJQ==} - dependencies: - muggle-string: 0.3.1 - dev: true + '@typescript-eslint/types@8.70.0': {} - /@volar/typescript@1.9.0: - resolution: {integrity: sha512-B8X4/H6V93uD7zu5VCw05eB0Ukcc39SFKsZoeylkAk2sJ50oaJLpajnQ8Ov4c+FnVQ6iPA6Xy1qdWoWJjh6xEg==} + '@typescript-eslint/typescript-estree@8.70.0(@typescript/typescript6@6.0.2)': dependencies: - '@volar/language-core': 1.9.0 - dev: true + '@typescript-eslint/project-service': 8.70.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.70.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/visitor-keys': 8.70.0 + debug: 4.4.3 + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) + typescript: '@typescript/typescript6@6.0.2' + transitivePeerDependencies: + - supports-color - /@vue/compiler-core@3.3.4: - resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==} + '@typescript-eslint/utils@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0)': dependencies: - '@babel/parser': 7.22.7 - '@vue/shared': 3.3.4 - estree-walker: 2.0.2 - source-map-js: 1.0.2 - dev: true + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0) + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/typescript-estree': 8.70.0(@typescript/typescript6@6.0.2) + eslint: 10.10.0 + typescript: '@typescript/typescript6@6.0.2' + transitivePeerDependencies: + - supports-color - /@vue/compiler-dom@3.3.4: - resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==} + '@typescript-eslint/visitor-keys@8.70.0': dependencies: - '@vue/compiler-core': 3.3.4 - '@vue/shared': 3.3.4 - dev: true + '@typescript-eslint/types': 8.70.0 + eslint-visitor-keys: 5.0.1 - /@vue/language-core@1.8.5(typescript@5.1.6): - resolution: {integrity: sha512-DKQNiNQzNV7nrkZQujvjfX73zqKdj2+KoM4YeKl+ft3f+crO3JB4ycPnmgaRMNX/ULJootdQPGHKFRl5cXxwaw==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@volar/language-core': 1.9.0 - '@volar/source-map': 1.9.0 - '@vue/compiler-dom': 3.3.4 - '@vue/reactivity': 3.3.4 - '@vue/shared': 3.3.4 - minimatch: 9.0.3 - muggle-string: 0.3.1 - typescript: 5.1.6 - vue-template-compiler: 2.7.14 - dev: true + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true - /@vue/reactivity@3.3.4: - resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==} - dependencies: - '@vue/shared': 3.3.4 - dev: true + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true - /@vue/shared@3.3.4: - resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==} - dev: true + '@typescript/typescript-darwin-x64@7.0.2': + optional: true - /@vue/typescript@1.8.5(typescript@5.1.6): - resolution: {integrity: sha512-domFBbNr3PEcjGBeB+cmgUM3cI6pJsJezguIUKZ1rphkfIkICyoMjCd3TitoP32yo2KABLiaXcGFzgFfQf6B3w==} - dependencies: - '@volar/typescript': 1.9.0 - '@vue/language-core': 1.8.5(typescript@5.1.6) - transitivePeerDependencies: - - typescript - dev: true + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true - /@xmldom/xmldom@0.8.9: - resolution: {integrity: sha512-4VSbbcMoxc4KLjb1gs96SRmi7w4h1SF+fCoiK0XaQX62buCc1G5d0DC5bJ9xJBNPDSVCmIrcl8BiYxzjrqaaJA==} - engines: {node: '>=10.0.0'} - dev: true + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true - /@zxing/text-encoding@0.9.0: - resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} - requiresBuild: true - dev: true + '@typescript/typescript-linux-arm64@7.0.2': optional: true - /abab@2.0.6: - resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} - dev: true + '@typescript/typescript-linux-arm@7.0.2': + optional: true - /acorn-jsx@5.3.2(acorn@8.10.0): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - acorn: 8.10.0 - dev: true + '@typescript/typescript-linux-loong64@7.0.2': + optional: true - /acorn-walk@8.2.0: - resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} - engines: {node: '>=0.4.0'} - dev: true + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true - /acorn@8.10.0: - resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true - /agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - dependencies: - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: true + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true - /aggregate-error@4.0.1: - resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==} - engines: {node: '>=12'} - dependencies: - clean-stack: 4.2.0 - indent-string: 5.0.0 - dev: true + '@typescript/typescript-linux-s390x@7.0.2': + optional: true - /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - dev: true + '@typescript/typescript-linux-x64@7.0.2': + optional: true - /ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} - dependencies: - type-fest: 0.21.3 - dev: true + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true - /ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - dev: true + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true - /ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - dependencies: - color-convert: 1.9.3 + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 + '@typescript/typescript-sunos-x64@7.0.2': + optional: true - /ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} + '@typescript/typescript-win32-arm64@7.0.2': + optional: true - /ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - dev: true + '@typescript/typescript-win32-x64@7.0.2': + optional: true - /anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} + '@typescript/typescript6@6.0.2': dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - dev: true + '@typescript/old': typescript@6.0.3 - /argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + '@vitejs/plugin-react@6.1.1(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13))': dependencies: - sprintf-js: 1.0.3 - dev: true + '@rolldown/pluginutils': 1.0.1 + vite: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13) - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true - - /aria-query@5.1.3: - resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + '@vitest/coverage-v8@5.0.0(vitest@5.0.0)': dependencies: - deep-equal: 2.2.2 + '@bcoe/v8-coverage': 1.0.2 + '@vitest/istanbul-lib-coverage': 1.0.1 + '@vitest/istanbul-lib-report': 1.0.1 + ast-v8-to-istanbul: 1.0.6 + magicast: 0.5.5 + obug: 2.2.1 + std-env: 4.2.0 + tinyrainbow: 3.1.1 + vitest: 5.0.0(@types/node@26.5.1)(@vitest/coverage-v8@5.0.0)(jsdom@30.0.1)(msw@2.15.0(@types/node@26.5.1)(@typescript/typescript6@6.0.2))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)) - /aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - dependencies: - dequal: 2.0.3 + '@vitest/istanbul-lib-coverage@1.0.1': {} - /array-buffer-byte-length@1.0.0: - resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + '@vitest/istanbul-lib-report@1.0.1': dependencies: - call-bind: 1.0.2 - is-array-buffer: 3.0.2 + '@vitest/istanbul-lib-coverage': 1.0.1 - /array-includes@3.1.6: - resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} - engines: {node: '>= 0.4'} + '@vitest/mocker@5.0.0(msw@2.15.0(@types/node@26.5.1)(@typescript/typescript6@6.0.2))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13))': dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.3 - get-intrinsic: 1.2.1 - is-string: 1.0.7 - dev: true + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.0 + estree-walker: 3.0.3 + magic-string: 1.3.1 + optionalDependencies: + msw: 2.15.0(@types/node@26.5.1)(@typescript/typescript6@6.0.2) + vite: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13) - /array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - dev: true + '@vitest/spy@5.0.0': {} - /array.prototype.flat@1.3.1: - resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} - engines: {node: '>= 0.4'} + '@volar/language-core@2.4.28': dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.3 - es-shim-unscopables: 1.0.0 - dev: true + '@volar/source-map': 2.4.28 - /array.prototype.flatmap@1.3.1: - resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.3 - es-shim-unscopables: 1.0.0 - dev: true + '@volar/source-map@2.4.28': {} - /array.prototype.tosorted@1.1.1: - resolution: {integrity: sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==} + '@volar/typescript@2.4.28': dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.3 - es-shim-unscopables: 1.0.0 - get-intrinsic: 1.2.1 - dev: true - - /arrify@1.0.1: - resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} - engines: {node: '>=0.10.0'} - dev: true - - /arrify@3.0.0: - resolution: {integrity: sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==} - engines: {node: '>=12'} - dev: true - - /assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - dev: true + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.2.0 - /ast-types-flow@0.0.7: - resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==} - dev: true - - /asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: true - - /available-typed-arrays@1.0.5: - resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} - engines: {node: '>= 0.4'} - - /axe-core@4.7.2: - resolution: {integrity: sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==} - engines: {node: '>=4'} - dev: true - - /axobject-query@3.2.1: - resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: - dequal: 2.0.3 - dev: true - - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true + acorn: 8.18.0 - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: true + acorn@8.18.0: {} - /big-integer@1.6.51: - resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} - engines: {node: '>=0.6'} - dev: true + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 - /binary-extensions@2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} - engines: {node: '>=8'} - dev: true + ansi-regex@5.0.1: {} - /bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + ansi-styles@4.3.0: dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - dev: true + color-convert: 2.0.1 - /bootstrap@4.6.2(jquery@3.7.0)(popper.js@1.16.1): - resolution: {integrity: sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==} - peerDependencies: - jquery: 1.9.1 - 3 - popper.js: ^1.16.1 - dependencies: - jquery: 3.7.0 - popper.js: 1.16.1 - dev: true + ansi-styles@5.2.0: {} - /bplist-parser@0.2.0: - resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==} - engines: {node: '>= 5.10.0'} - dependencies: - big-integer: 1.6.51 - dev: true + argparse@2.0.1: {} - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + aria-query@5.3.0: dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - dev: true + dequal: 2.0.3 + + aria-query@5.3.2: {} - /brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + array-buffer-byte-length@1.0.2: dependencies: - balanced-match: 1.0.2 - dev: true + call-bound: 1.0.4 + is-array-buffer: 3.0.5 - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} + array-includes@3.2.0: dependencies: - fill-range: 7.0.1 - dev: true + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 - /browserslist@4.21.9: - resolution: {integrity: sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true + array.prototype.findlast@1.2.5: dependencies: - caniuse-lite: 1.0.30001516 - electron-to-chromium: 1.4.461 - node-releases: 2.0.13 - update-browserslist-db: 1.0.11(browserslist@4.21.9) - dev: true + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 - /buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - dev: true - - /buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + array.prototype.findlastindex@1.2.6: dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - dev: true + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 - /builtins@5.0.1: - resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} + array.prototype.flat@1.3.3: dependencies: - semver: 7.5.4 - dev: true + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 - /bundle-name@3.0.0: - resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==} - engines: {node: '>=12'} + array.prototype.flatmap@1.3.3: dependencies: - run-applescript: 5.0.0 - dev: true + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 - /busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} + array.prototype.tosorted@1.1.4: dependencies: - streamsearch: 1.1.0 - dev: false + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 - /c8@7.14.0: - resolution: {integrity: sha512-i04rtkkcNcCf7zsQcSv/T9EbUn4RXQ6mropeMcjFOsQXQ0iGLAr/xT6TImQg4+U9hmNpN9XdvPkjUL1IzbgxJw==} - engines: {node: '>=10.12.0'} - hasBin: true + arraybuffer.prototype.slice@1.0.4: dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@istanbuljs/schema': 0.1.3 - find-up: 5.0.0 - foreground-child: 2.0.0 - istanbul-lib-coverage: 3.2.0 - istanbul-lib-report: 3.0.0 - istanbul-reports: 3.1.5 - rimraf: 3.0.2 - test-exclude: 6.0.0 - v8-to-istanbul: 9.1.0 - yargs: 16.2.0 - yargs-parser: 20.2.9 - dev: true + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 - /cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - dev: true + assertion-error@2.0.1: {} - /call-bind@1.0.2: - resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + ast-types-flow@0.0.8: {} + + ast-v8-to-istanbul@1.0.6: dependencies: - function-bind: 1.1.1 - get-intrinsic: 1.2.1 + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - dev: true + async-function@1.0.0: {} - /camelcase-keys@8.0.2: - resolution: {integrity: sha512-qMKdlOfsjlezMqxkUGGMaWWs17i2HoL15tM+wtx8ld4nLrUwU58TFdvyGOz/piNP842KeO8yXvggVQSdQ828NA==} - engines: {node: '>=14.16'} + available-typed-arrays@1.0.7: dependencies: - camelcase: 7.0.1 - map-obj: 4.3.0 - quick-lru: 6.1.1 - type-fest: 2.19.0 - dev: true + possible-typed-array-names: 1.1.0 - /camelcase@7.0.1: - resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} - engines: {node: '>=14.16'} - dev: true + axe-core@4.13.0: {} - /caniuse-lite@1.0.30001516: - resolution: {integrity: sha512-Wmec9pCBY8CWbmI4HsjBeQLqDTqV91nFVR83DnZpYyRnPI1wePDsTg0bGLPC5VU/3OIZV1fmxEea1b+tFKe86g==} + axobject-query@4.1.0: {} - /chai@4.3.7: - resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} - engines: {node: '>=4'} + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.11.23: {} + + bidi-js@1.0.3: dependencies: - assertion-error: 1.1.0 - check-error: 1.0.2 - deep-eql: 4.1.3 - get-func-name: 2.0.0 - loupe: 2.3.6 - pathval: 1.1.1 - type-detect: 4.0.8 - dev: true - - /chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} + require-from-string: 2.0.2 + + bootstrap@5.3.8(@popperjs/core@2.11.8): dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 + '@popperjs/core': 2.11.8 - /chalk@3.0.0: - resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} - engines: {node: '>=8'} + brace-expansion@1.1.18: dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 + balanced-match: 1.0.2 + concat-map: 0.0.1 - /chalk@4.1.1: - resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==} - engines: {node: '>=10'} + brace-expansion@5.0.9: dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: true + balanced-match: 4.0.4 - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} + braces@3.0.3: dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 + fill-range: 7.1.1 - /chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - dev: true + browserslist@4.28.9: + dependencies: + baseline-browser-mapping: 2.11.23 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.427 + node-releases: 2.0.55 + update-browserslist-db: 1.3.3(browserslist@4.28.9) - /check-error@1.0.2: - resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} - dev: true + cacheable@2.5.0: + dependencies: + '@cacheable/memory': 2.2.0 + '@cacheable/utils': 2.5.0 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.10.1 - /chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} + call-bind-apply-helpers@1.0.2: dependencies: - anymatch: 3.1.3 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.2 - dev: true + es-errors: 1.3.0 + function-bind: 1.1.2 - /clean-stack@4.2.0: - resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==} - engines: {node: '>=12'} + call-bind@1.0.9: dependencies: - escape-string-regexp: 5.0.0 - dev: true + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 - /cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} + call-bound@1.0.4: dependencies: - restore-cursor: 3.1.0 - dev: true + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 - /cli-spinners@2.9.0: - resolution: {integrity: sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==} - engines: {node: '>=6'} - dev: true + callsites@3.1.0: {} - /cli-width@3.0.0: - resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} - engines: {node: '>= 10'} - dev: true + caniuse-lite@1.0.30001810: {} - /client-only@0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - dev: false + chai@6.2.2: {} - /cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cli-width@4.1.0: {} + + client-only@0.0.1: {} + + cliui@7.0.4: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - dev: true - /cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} + cliui@8.0.1: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - dev: true - /clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - dev: true - - /color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - dependencies: - color-name: 1.1.3 - - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + color-convert@2.0.1: dependencies: color-name: 1.1.4 - /color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + color-name@1.1.4: {} - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comment-parser@1.4.9: {} - /colors@1.2.5: - resolution: {integrity: sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==} - engines: {node: '>=0.1.90'} - dev: true + compare-versions@6.1.1: {} - /combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - dependencies: - delayed-stream: 1.0.0 - dev: true + concat-map@0.0.1: {} - /commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} - requiresBuild: true - dev: true - optional: true + confbox@0.1.8: {} - /comment-parser@1.3.1: - resolution: {integrity: sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==} - engines: {node: '>= 12.0.0'} - dev: true + confbox@0.3.1: {} - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true + convert-source-map@2.0.0: {} - /convert-source-map@1.9.0: - resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} - dev: true + cookie@1.1.1: {} - /cookie@0.4.2: - resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} - engines: {node: '>= 0.6'} - dev: true + copy-file@11.1.0: + dependencies: + graceful-fs: 4.2.11 + p-event: 6.0.1 - /copyfiles@2.4.1: - resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==} - hasBin: true + copyfiles@2.4.1: dependencies: glob: 7.2.3 - minimatch: 3.1.2 + minimatch: 3.1.5 mkdirp: 1.0.4 noms: 0.0.0 through2: 2.0.5 untildify: 4.0.0 - yargs: 16.2.0 - dev: true - - /core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - dev: true + yargs: 16.2.2 - /cp-file@10.0.0: - resolution: {integrity: sha512-vy2Vi1r2epK5WqxOLnskeKeZkdZvTKfFZQCplE3XWsP+SUJyd5XAUFC9lFgTjjXJF2GMne/UML14iEmkAaDfFg==} - engines: {node: '>=14.16'} - dependencies: - graceful-fs: 4.2.11 - nested-error-stacks: 2.1.1 - p-event: 5.0.1 - dev: true + core-util-is@1.0.3: {} - /cpy-cli@5.0.0: - resolution: {integrity: sha512-fb+DZYbL9KHc0BC4NYqGRrDIJZPXUmjjtqdw4XRRg8iV8dIfghUX/WiL+q4/B/KFTy3sK6jsbUhBaz0/Hxg7IQ==} - engines: {node: '>=16'} - hasBin: true + cpy-cli@7.0.0: dependencies: - cpy: 10.1.0 - meow: 12.0.1 - dev: true + cpy: 13.2.3 + globby: 16.2.4 + meow: 14.1.0 - /cpy@10.1.0: - resolution: {integrity: sha512-VC2Gs20JcTyeQob6UViBLnyP0bYHkBh6EiKzot9vi2DmeGlFT9Wd7VG3NBrkNx/jYvFBeyDOMMHdHQhbtKLgHQ==} - engines: {node: '>=16'} + cpy@13.2.3: dependencies: - arrify: 3.0.0 - cp-file: 10.0.0 - globby: 13.2.2 + copy-file: 11.1.0 + globby: 16.2.4 junk: 4.0.1 - micromatch: 4.0.5 - nested-error-stacks: 2.1.1 - p-filter: 3.0.0 - p-map: 6.0.0 - dev: true - - /cross-env@7.0.3: - resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} - engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} - hasBin: true + micromatch: 4.0.8 + p-filter: 4.1.0 + p-map: 7.0.8 + + cross-env@10.1.0: dependencies: - cross-spawn: 7.0.3 - dev: true + '@epic-web/invariant': 1.0.0 + cross-spawn: 7.0.6 - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - dev: true - - /css.escape@1.5.1: - resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - /cssstyle@3.0.0: - resolution: {integrity: sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==} - engines: {node: '>=14'} + css-tree@3.2.1: dependencies: - rrweb-cssom: 0.6.0 - dev: true - - /csstype@3.1.2: - resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} - dev: true + mdn-data: 2.27.1 + source-map-js: 1.2.1 - /damerau-levenshtein@1.0.8: - resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - dev: true + css.escape@1.5.1: {} - /data-urls@4.0.0: - resolution: {integrity: sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==} - engines: {node: '>=14'} - dependencies: - abab: 2.0.6 - whatwg-mimetype: 3.0.0 - whatwg-url: 12.0.1 - dev: true + csstype@3.2.3: {} - /de-indent@1.0.2: - resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} - dev: true + damerau-levenshtein@1.0.8: {} - /debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + data-urls@7.0.0: dependencies: - ms: 2.1.3 - dev: true + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + data-view-buffer@1.0.2: dependencies: - ms: 2.1.2 - dev: true + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 - /decamelize-keys@2.0.1: - resolution: {integrity: sha512-nrNeSCtU2gV3Apcmn/EZ+aR20zKDuNDStV67jPiupokD3sOAFeMzslLMCFdKv1sPqzwoe5ZUhsSW9IAVgKSL/Q==} - engines: {node: '>=14.16'} + data-view-byte-length@1.0.2: dependencies: - decamelize: 6.0.0 - map-obj: 4.3.0 - quick-lru: 6.1.1 - type-fest: 3.13.1 - dev: true + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 - /decamelize@6.0.0: - resolution: {integrity: sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - - /decimal.js@10.4.3: - resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} - dev: true - - /deep-eql@4.1.3: - resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} - engines: {node: '>=6'} + data-view-byte-offset@1.0.1: dependencies: - type-detect: 4.0.8 - dev: true - - /deep-equal@2.2.2: - resolution: {integrity: sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==} - dependencies: - array-buffer-byte-length: 1.0.0 - call-bind: 1.0.2 - es-get-iterator: 1.1.3 - get-intrinsic: 1.2.1 - is-arguments: 1.1.1 - is-array-buffer: 3.0.2 - is-date-object: 1.0.5 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.2 - isarray: 2.0.5 - object-is: 1.1.5 - object-keys: 1.1.1 - object.assign: 4.1.4 - regexp.prototype.flags: 1.5.0 - side-channel: 1.0.4 - which-boxed-primitive: 1.0.2 - which-collection: 1.0.1 - which-typed-array: 1.1.10 - - /deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dev: true + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 - /default-browser-id@3.0.0: - resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} - engines: {node: '>=12'} + debug@3.2.7: dependencies: - bplist-parser: 0.2.0 - untildify: 4.0.0 - dev: true + ms: 2.1.3 - /default-browser@4.0.0: - resolution: {integrity: sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==} - engines: {node: '>=14.16'} + debug@4.4.3: dependencies: - bundle-name: 3.0.0 - default-browser-id: 3.0.0 - execa: 7.1.1 - titleize: 3.0.0 - dev: true + ms: 2.1.3 - /defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - dependencies: - clone: 1.0.4 - dev: true + decimal.js@10.6.0: {} - /define-lazy-prop@3.0.0: - resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} - engines: {node: '>=12'} - dev: true + deep-is@0.1.4: {} - /define-properties@1.2.0: - resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} - engines: {node: '>= 0.4'} + define-data-property@1.1.4: dependencies: - has-property-descriptors: 1.0.0 - object-keys: 1.1.1 - - /delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - dev: true - - /dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 - /diff-sequences@27.5.1: - resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 - /diff-sequences@29.4.3: - resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true + dequal@2.0.3: {} - /dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - dependencies: - path-type: 4.0.0 - dev: true + detect-libc@2.1.2: {} - /doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} + doctrine@2.1.0: dependencies: esutils: 2.0.3 - dev: true - /doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dependencies: - esutils: 2.0.3 - dev: true + dom-accessibility-api@0.5.16: {} - /dom-accessibility-api@0.5.16: - resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dom-accessibility-api@0.6.3: {} - /domexception@4.0.0: - resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} - engines: {node: '>=12'} + dunder-proto@1.0.1: dependencies: - webidl-conversions: 7.0.0 - dev: true + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 - /eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - dev: true + electron-to-chromium@1.5.427: {} - /electron-to-chromium@1.4.461: - resolution: {integrity: sha512-1JkvV2sgEGTDXjdsaQCeSwYYuhLRphRpc+g6EHTFELJXEiznLt3/0pZ9JuAOQ5p2rI3YxKTbivtvajirIfhrEQ==} - dev: true + emoji-regex@8.0.0: {} - /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true - - /emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - dev: true + emoji-regex@9.2.2: {} - /enhanced-resolve@5.15.0: - resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} - engines: {node: '>=10.13.0'} + enhanced-resolve@5.25.1: dependencies: graceful-fs: 4.2.11 - tapable: 2.2.1 - dev: true + tapable: 2.3.3 - /entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - dev: true + entities@8.0.0: {} - /error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + es-abstract-get@1.0.0: dependencies: - is-arrayish: 0.2.1 - dev: true - - /es-abstract@1.21.3: - resolution: {integrity: sha512-ZU4miiY1j3sGPFLJ34VJXEqhpmL+HGByCinGHv4HC+Fxl2fI2Z4yR6tl0mORnDr6PA8eihWo4LmSWDbvhALckg==} - engines: {node: '>= 0.4'} - dependencies: - array-buffer-byte-length: 1.0.0 - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - es-set-tostringtag: 2.0.1 - es-to-primitive: 1.2.1 - function.prototype.name: 1.1.5 - get-intrinsic: 1.2.1 - get-symbol-description: 1.0.0 - globalthis: 1.0.3 - gopd: 1.0.1 - has: 1.0.3 - has-property-descriptors: 1.0.0 - has-proto: 1.0.1 - has-symbols: 1.0.3 - internal-slot: 1.0.5 - is-array-buffer: 3.0.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.4 + function.prototype.name: 1.2.0 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 is-callable: 1.2.7 - is-negative-zero: 2.0.2 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.2 - is-string: 1.0.7 - is-typed-array: 1.1.10 - is-weakref: 1.0.2 - object-inspect: 1.12.3 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 object-keys: 1.1.1 - object.assign: 4.1.4 - regexp.prototype.flags: 1.5.0 - safe-regex-test: 1.0.0 - string.prototype.trim: 1.2.7 - string.prototype.trimend: 1.0.6 - string.prototype.trimstart: 1.0.6 - typed-array-byte-offset: 1.0.0 - typed-array-length: 1.0.4 - unbox-primitive: 1.0.2 - which-typed-array: 1.1.10 - dev: true - - /es-get-iterator@1.1.3: - resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - has-symbols: 1.0.3 - is-arguments: 1.1.1 - is-map: 2.0.2 - is-set: 2.0.2 - is-string: 1.0.7 - isarray: 2.0.5 - stop-iteration-iterator: 1.0.0 - - /es-set-tostringtag@2.0.1: - resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} - engines: {node: '>= 0.4'} - dependencies: - get-intrinsic: 1.2.1 - has: 1.0.3 - has-tostringtag: 1.0.0 - dev: true - - /es-shim-unscopables@1.0.0: - resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} - dependencies: - has: 1.0.3 - dev: true - - /es-to-primitive@1.2.1: - resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} - engines: {node: '>= 0.4'} - dependencies: + object.assign: 4.1.7 + own-keys: 1.0.2 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.22 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.4.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + + es-module-lexer@2.3.2: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.4 + + es-to-primitive@1.3.4: + dependencies: + es-abstract-get: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 is-callable: 1.2.7 - is-date-object: 1.0.5 - is-symbol: 1.0.4 - dev: true + is-date-object: 1.1.0 + is-symbol: 1.1.1 - /esbuild@0.17.19: - resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/android-arm': 0.17.19 - '@esbuild/android-arm64': 0.17.19 - '@esbuild/android-x64': 0.17.19 - '@esbuild/darwin-arm64': 0.17.19 - '@esbuild/darwin-x64': 0.17.19 - '@esbuild/freebsd-arm64': 0.17.19 - '@esbuild/freebsd-x64': 0.17.19 - '@esbuild/linux-arm': 0.17.19 - '@esbuild/linux-arm64': 0.17.19 - '@esbuild/linux-ia32': 0.17.19 - '@esbuild/linux-loong64': 0.17.19 - '@esbuild/linux-mips64el': 0.17.19 - '@esbuild/linux-ppc64': 0.17.19 - '@esbuild/linux-riscv64': 0.17.19 - '@esbuild/linux-s390x': 0.17.19 - '@esbuild/linux-x64': 0.17.19 - '@esbuild/netbsd-x64': 0.17.19 - '@esbuild/openbsd-x64': 0.17.19 - '@esbuild/sunos-x64': 0.17.19 - '@esbuild/win32-arm64': 0.17.19 - '@esbuild/win32-ia32': 0.17.19 - '@esbuild/win32-x64': 0.17.19 - dev: true - - /esbuild@0.18.13: - resolution: {integrity: sha512-vhg/WR/Oiu4oUIkVhmfcc23G6/zWuEQKFS+yiosSHe4aN6+DQRXIfeloYGibIfVhkr4wyfuVsGNLr+sQU1rWWw==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true + esbuild@0.28.2: optionalDependencies: - '@esbuild/android-arm': 0.18.13 - '@esbuild/android-arm64': 0.18.13 - '@esbuild/android-x64': 0.18.13 - '@esbuild/darwin-arm64': 0.18.13 - '@esbuild/darwin-x64': 0.18.13 - '@esbuild/freebsd-arm64': 0.18.13 - '@esbuild/freebsd-x64': 0.18.13 - '@esbuild/linux-arm': 0.18.13 - '@esbuild/linux-arm64': 0.18.13 - '@esbuild/linux-ia32': 0.18.13 - '@esbuild/linux-loong64': 0.18.13 - '@esbuild/linux-mips64el': 0.18.13 - '@esbuild/linux-ppc64': 0.18.13 - '@esbuild/linux-riscv64': 0.18.13 - '@esbuild/linux-s390x': 0.18.13 - '@esbuild/linux-x64': 0.18.13 - '@esbuild/netbsd-x64': 0.18.13 - '@esbuild/openbsd-x64': 0.18.13 - '@esbuild/sunos-x64': 0.18.13 - '@esbuild/win32-arm64': 0.18.13 - '@esbuild/win32-ia32': 0.18.13 - '@esbuild/win32-x64': 0.18.13 - dev: true - - /escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} - dev: true - - /escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - dev: true - - /escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - dev: true - - /eslint-config-standard-with-typescript@36.1.0(@typescript-eslint/eslint-plugin@5.50.0)(eslint-plugin-import@2.27.5)(eslint-plugin-n@16.0.1)(eslint-plugin-promise@6.1.1)(eslint@8.45.0)(typescript@5.1.6): - resolution: {integrity: sha512-Gpk+7Q3EHqIzTnqYs/LpfOwVb8+kocvlFLYhBdCmUy+EUpsC7067PaHhGSp8P4N+lC2KNGBZ7e2tiGyoRNSVHA==} - peerDependencies: - '@typescript-eslint/eslint-plugin': ^5.50.0 - eslint: ^8.0.1 - eslint-plugin-import: ^2.25.2 - eslint-plugin-n: '^15.0.0 || ^16.0.0 ' - eslint-plugin-promise: ^6.0.0 - typescript: '*' - dependencies: - '@typescript-eslint/eslint-plugin': 5.50.0(@typescript-eslint/parser@5.62.0)(eslint@8.45.0)(typescript@5.1.6) - '@typescript-eslint/parser': 5.62.0(eslint@8.45.0)(typescript@5.1.6) - eslint: 8.45.0 - eslint-config-standard: 17.1.0(eslint-plugin-import@2.27.5)(eslint-plugin-n@16.0.1)(eslint-plugin-promise@6.1.1)(eslint@8.45.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.45.0) - eslint-plugin-n: 16.0.1(eslint@8.45.0) - eslint-plugin-promise: 6.1.1(eslint@8.45.0) - typescript: 5.1.6 - transitivePeerDependencies: - - supports-color - dev: true - - /eslint-config-standard@17.1.0(eslint-plugin-import@2.27.5)(eslint-plugin-n@16.0.1)(eslint-plugin-promise@6.1.1)(eslint@8.45.0): - resolution: {integrity: sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==} - engines: {node: '>=12.0.0'} - peerDependencies: - eslint: ^8.0.1 - eslint-plugin-import: ^2.25.2 - eslint-plugin-n: '^15.0.0 || ^16.0.0 ' - eslint-plugin-promise: ^6.0.0 - dependencies: - eslint: 8.45.0 - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.45.0) - eslint-plugin-n: 16.0.1(eslint@8.45.0) - eslint-plugin-promise: 6.1.1(eslint@8.45.0) - dev: true - - /eslint-define-config@1.21.0: - resolution: {integrity: sha512-OKfreV19Nw4yK4UX1CDkv5FXWdzeF+VSROsO28DVi1BrzqOD4a3U71LJqEhcupK65MoLXxARQ0pSg8bDvNPONA==} - engines: {node: ^16.13.0 || >=18.0.0, npm: '>=7.0.0', pnpm: '>= 8.6.0'} - dev: true - - /eslint-import-resolver-node@0.3.7: - resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-compat-utils@0.5.1(eslint@10.10.0): + dependencies: + eslint: 10.10.0 + semver: 7.8.5 + + eslint-config-prettier@10.1.8(eslint@10.10.0): + dependencies: + eslint: 10.10.0 + + eslint-import-resolver-node@0.3.10: dependencies: debug: 3.2.7 - is-core-module: 2.12.1 - resolve: 1.22.2 - transitivePeerDependencies: - - supports-color - dev: true - - /eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.27.5)(eslint@8.45.0): - resolution: {integrity: sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - eslint: '*' - eslint-plugin-import: '*' - dependencies: - debug: 4.3.4 - enhanced-resolve: 5.15.0 - eslint: 8.45.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.45.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.45.0) - get-tsconfig: 4.6.2 - globby: 13.2.2 - is-core-module: 2.12.1 - is-glob: 4.0.3 - synckit: 0.8.5 + is-core-module: 2.16.2 + resolve: 2.0.0-next.7 transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-node - - eslint-import-resolver-webpack - supports-color - dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.45.0): - resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0))(eslint-import-resolver-node@0.3.10)(eslint@10.10.0): dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.45.0)(typescript@5.1.6) debug: 3.2.7 - eslint: 8.45.0 - eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0)(eslint-plugin-import@2.27.5)(eslint@8.45.0) + optionalDependencies: + '@typescript-eslint/parser': 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0) + eslint: 10.10.0 + eslint-import-resolver-node: 0.3.10 transitivePeerDependencies: - supports-color - dev: true - /eslint-plugin-es-x@7.1.0(eslint@8.45.0): - resolution: {integrity: sha512-AhiaF31syh4CCQ+C5ccJA0VG6+kJK8+5mXKKE7Qs1xcPRg02CDPOj3mWlQxuWS/AYtg7kxrDNgW9YW3vc0Q+Mw==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - eslint: '>=8' + eslint-plugin-es-x@7.8.0(eslint@10.10.0): dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.45.0) - '@eslint-community/regexpp': 4.5.1 - eslint: 8.45.0 - dev: true + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0) + '@eslint-community/regexpp': 4.12.2 + eslint: 10.10.0 + eslint-compat-utils: 0.5.1(eslint@10.10.0) - /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.45.0): - resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0))(eslint@10.10.0): dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.45.0)(typescript@5.1.6) - array-includes: 3.1.6 - array.prototype.flat: 1.3.1 - array.prototype.flatmap: 1.3.1 + '@rtsao/scc': 1.1.0 + array-includes: 3.2.0 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 8.45.0 - eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.45.0) - has: 1.0.3 - is-core-module: 2.12.1 + eslint: 10.10.0 + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0))(eslint-import-resolver-node@0.3.10)(eslint@10.10.0) + hasown: 2.0.4 + is-core-module: 2.16.2 is-glob: 4.0.3 - minimatch: 3.1.2 - object.values: 1.1.6 - resolve: 1.22.2 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 semver: 6.3.1 - tsconfig-paths: 3.14.2 + string.prototype.trimend: 1.0.10 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - dev: true - /eslint-plugin-jsx-a11y@6.7.1(eslint@8.45.0): - resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==} - engines: {node: '>=4.0'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + eslint-plugin-jsx-a11y@6.10.2(eslint@10.10.0): dependencies: - '@babel/runtime': 7.22.6 - aria-query: 5.3.0 - array-includes: 3.1.6 - array.prototype.flatmap: 1.3.1 - ast-types-flow: 0.0.7 - axe-core: 4.7.2 - axobject-query: 3.2.1 + aria-query: 5.3.2 + array-includes: 3.2.0 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.13.0 + axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 8.45.0 - has: 1.0.3 - jsx-ast-utils: 3.3.4 - language-tags: 1.0.5 - minimatch: 3.1.2 - object.entries: 1.1.6 - object.fromentries: 2.0.6 - semver: 6.3.1 - dev: true + eslint: 10.10.0 + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-n@18.3.0(@typescript/typescript6@6.0.2)(eslint@10.10.0): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0) + enhanced-resolve: 5.25.1 + eslint: 10.10.0 + eslint-plugin-es-x: 7.8.0(eslint@10.10.0) + get-tsconfig: 4.14.3 + globals: 15.15.0 + globrex: 0.1.2 + ignore: 5.3.2 + semver: 7.8.5 + optionalDependencies: + typescript: '@typescript/typescript6@6.0.2' - /eslint-plugin-n@16.0.1(eslint@8.45.0): - resolution: {integrity: sha512-CDmHegJN0OF3L5cz5tATH84RPQm9kG+Yx39wIqIwPR2C0uhBGMWfbbOtetR83PQjjidA5aXMu+LEFw1jaSwvTA==} - engines: {node: '>=16.0.0'} - peerDependencies: - eslint: '>=7.0.0' - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.45.0) - builtins: 5.0.1 - eslint: 8.45.0 - eslint-plugin-es-x: 7.1.0(eslint@8.45.0) - ignore: 5.2.4 - is-core-module: 2.12.1 - minimatch: 3.1.2 - resolve: 1.22.2 - semver: 7.5.4 - dev: true - - /eslint-plugin-promise@6.1.1(eslint@8.45.0): - resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + eslint-plugin-no-only-tests@3.4.0: {} + + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.10.0))(eslint@10.10.0)(prettier@3.9.6): dependencies: - eslint: 8.45.0 - dev: true + eslint: 10.10.0 + prettier: 3.9.6 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.13 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@10.10.0) - /eslint-plugin-react-hooks@4.6.0(eslint@8.45.0): - resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} - engines: {node: '>=10'} - peerDependencies: - eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + eslint-plugin-react-hooks@7.1.1(eslint@10.10.0): dependencies: - eslint: 8.45.0 - dev: true + '@babel/core': 7.29.7 + '@babel/parser': 7.29.8 + eslint: 10.10.0 + hermes-parser: 0.25.1 + zod: 4.6.5 + zod-validation-error: 4.0.2(zod@4.6.5) + transitivePeerDependencies: + - supports-color - /eslint-plugin-react@7.32.2(eslint@8.45.0): - resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + eslint-plugin-react@7.37.5(eslint@10.10.0): dependencies: - array-includes: 3.1.6 - array.prototype.flatmap: 1.3.1 - array.prototype.tosorted: 1.1.1 + array-includes: 3.2.0 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 - eslint: 8.45.0 + es-iterator-helpers: 1.4.0 + eslint: 10.10.0 estraverse: 5.3.0 - jsx-ast-utils: 3.3.4 - minimatch: 3.1.2 - object.entries: 1.1.6 - object.fromentries: 2.0.6 - object.hasown: 1.1.2 - object.values: 1.1.6 + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 prop-types: 15.8.1 - resolve: 2.0.0-next.4 + resolve: 2.0.0-next.7 semver: 6.3.1 - string.prototype.matchall: 4.0.8 - dev: true + string.prototype.matchall: 4.1.0 + string.prototype.repeat: 1.0.0 - /eslint-plugin-regexp@1.15.0(eslint@8.45.0): - resolution: {integrity: sha512-YEtQPfdudafU7RBIFci81R/Q1yErm0mVh3BkGnXD2Dk8DLwTFdc2ITYH1wCnHKim2gnHfPFgrkh+b2ozyyU7ag==} - engines: {node: ^12 || >=14} - peerDependencies: - eslint: '>=6.0.0' + eslint-plugin-regexp@3.3.0(eslint@10.10.0): dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.45.0) - '@eslint-community/regexpp': 4.5.1 - comment-parser: 1.3.1 - eslint: 8.45.0 - grapheme-splitter: 1.0.4 - jsdoctypeparser: 9.0.0 - refa: 0.11.0 - regexp-ast-analysis: 0.6.0 - scslre: 0.2.0 - dev: true - - /eslint-plugin-simple-import-sort@10.0.0(eslint@8.45.0): - resolution: {integrity: sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw==} - peerDependencies: - eslint: '>=5.0.0' + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0) + '@eslint-community/regexpp': 4.12.2 + comment-parser: 1.4.9 + eslint: 10.10.0 + jsdoc-type-pratt-parser: 9.2.1 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + scslre: 0.3.0 + + eslint-plugin-simple-import-sort@14.0.0(eslint@10.10.0): dependencies: - eslint: 8.45.0 - dev: true + eslint: 10.10.0 - /eslint-plugin-testing-library@5.11.0(eslint@8.45.0)(typescript@5.1.6): - resolution: {integrity: sha512-ELY7Gefo+61OfXKlQeXNIDVVLPcvKTeiQOoMZG9TeuWa7Ln4dUNRv8JdRWBQI9Mbb427XGlVB1aa1QPZxBJM8Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} - peerDependencies: - eslint: ^7.5.0 || ^8.0.0 + eslint-plugin-testing-library@7.16.2(@typescript/typescript6@6.0.2)(eslint@10.10.0): dependencies: - '@typescript-eslint/utils': 5.62.0(eslint@8.45.0)(typescript@5.1.6) - eslint: 8.45.0 + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/utils': 8.70.0(@typescript/typescript6@6.0.2)(eslint@10.10.0) + eslint: 10.10.0 transitivePeerDependencies: - supports-color - typescript - dev: true - /eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} - dependencies: - esrecurse: 4.3.0 - estraverse: 4.3.0 - dev: true - - /eslint-scope@7.2.1: - resolution: {integrity: sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@9.1.2: dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 esrecurse: 4.3.0 estraverse: 5.3.0 - dev: true - /eslint-utils@3.0.0(eslint@8.45.0): - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' - dependencies: - eslint: 8.45.0 - eslint-visitor-keys: 2.1.0 - dev: true + eslint-visitor-keys@3.4.3: {} - /eslint-visitor-keys@2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} - dev: true + eslint-visitor-keys@4.2.1: {} - /eslint-visitor-keys@3.4.1: - resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true + eslint-visitor-keys@5.0.1: {} - /eslint@8.45.0: - resolution: {integrity: sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true + eslint@10.10.0: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.45.0) - '@eslint-community/regexpp': 4.5.1 - '@eslint/eslintrc': 2.1.0 - '@eslint/js': 8.44.0 - '@humanwhocodes/config-array': 0.11.10 + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.7.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.3 + '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 - doctrine: 3.0.0 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.1 - eslint-visitor-keys: 3.4.1 - espree: 9.6.1 - esquery: 1.5.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 + file-entry-cache: 11.1.5 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.20.0 - graphemer: 1.4.0 - ignore: 5.2.4 + ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - 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 + minimatch: 10.2.6 natural-compare: 1.4.0 - optionator: 0.9.3 - strip-ansi: 6.0.1 - text-table: 0.2.0 + optionator: 0.9.4 transitivePeerDependencies: - supports-color - dev: true - /espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + espree@10.4.0: dependencies: - acorn: 8.10.0 - acorn-jsx: 5.3.2(acorn@8.10.0) - eslint-visitor-keys: 3.4.1 - dev: true + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 4.2.1 - /esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} - engines: {node: '>=0.10'} + espree@11.2.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: dependencies: estraverse: 5.3.0 - dev: true - /esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 - dev: true - /estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - dev: true + estraverse@5.3.0: {} - /estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - dev: true + estree-walker@2.0.2: {} - /estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - dev: true + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - dev: true + esutils@2.0.3: {} - /events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - dev: true + expect-type@1.4.0: {} - /execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - dev: true - - /execa@7.1.1: - resolution: {integrity: sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==} - engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} - dependencies: - cross-spawn: 7.0.3 - get-stream: 6.0.1 - human-signals: 4.3.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 - dev: true - - /external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} - dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.0.33 - dev: true + exsolve@1.1.1: {} - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} - /fast-glob@3.3.0: - resolution: {integrity: sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==} - engines: {node: '>=8.6.0'} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 - micromatch: 4.0.5 - dev: true - - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true + micromatch: 4.0.8 - /fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - dev: true + fast-json-stable-stringify@2.1.0: {} - /fastq@1.15.0: - resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} - dependencies: - reusify: 1.0.4 - dev: true + fast-levenshtein@2.0.6: {} - /figures@3.2.0: - resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} - engines: {node: '>=8'} - dependencies: - escape-string-regexp: 1.0.5 - dev: true + fast-string-truncated-width@3.0.3: {} - /file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + fast-string-width@3.0.2: dependencies: - flat-cache: 3.0.4 - dev: true + fast-string-truncated-width: 3.0.3 - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} + fast-wrap-ansi@0.2.2: dependencies: - to-regex-range: 5.0.1 - dev: true + fast-string-width: 3.0.2 - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} + fastq@1.20.3: dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - dev: true + reusify: 1.1.0 - /find-up@6.3.0: - resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - locate-path: 7.2.0 - path-exists: 5.0.0 - dev: true + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 - /flat-cache@3.0.4: - resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} - engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@11.1.5: dependencies: - flatted: 3.2.7 - rimraf: 3.0.2 - dev: true - - /flatted@3.2.7: - resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} - dev: true + flat-cache: 6.1.23 - /for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + fill-range@7.1.1: dependencies: - is-callable: 1.2.7 + to-regex-range: 5.0.1 - /foreground-child@2.0.0: - resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} - engines: {node: '>=8.0.0'} + find-up@5.0.0: dependencies: - cross-spawn: 7.0.3 - signal-exit: 3.0.7 - dev: true + locate-path: 6.0.0 + path-exists: 4.0.0 - /foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} - engines: {node: '>=14'} + flat-cache@6.1.23: dependencies: - cross-spawn: 7.0.3 - signal-exit: 4.0.2 - dev: true + cacheable: 2.5.0 + flatted: 3.4.4 + hookified: 1.15.1 - /form-data@4.0.0: - resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} - engines: {node: '>= 6'} - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - dev: true + flatted@3.4.4: {} - /fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} + for-each@0.3.5: dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - dev: true + is-callable: 1.2.7 - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true + fs.realpath@1.0.0: {} - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true + fsevents@2.3.3: optional: true - /function-bind@1.1.1: - resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + function-bind@1.1.2: {} - /function.prototype.name@1.1.5: - resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} - engines: {node: '>= 0.4'} + function.prototype.name@1.2.0: dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.3 + call-bind: 1.0.9 + call-bound: 1.0.4 + es-define-property: 1.0.1 + es-errors: 1.3.0 functions-have-names: 1.2.3 - dev: true + has-property-descriptors: 1.0.2 + hasown: 2.0.4 + is-callable: 1.2.7 + is-document.all: 1.0.0 - /functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + functions-have-names@1.2.3: {} - /gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - dev: true + generator-function@2.0.1: {} - /get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - dev: true + gensync@1.0.0-beta.2: {} - /get-func-name@2.0.0: - resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} - dev: true + get-caller-file@2.0.5: {} - /get-intrinsic@1.2.1: - resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + get-intrinsic@1.3.0: dependencies: - function-bind: 1.1.1 - has: 1.0.3 - has-proto: 1.0.1 - has-symbols: 1.0.3 + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 - /get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - dev: true + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 - /get-symbol-description@1.0.0: - resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} - engines: {node: '>= 0.4'} + get-symbol-description@1.1.0: dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - dev: true + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 - /get-tsconfig@4.6.2: - resolution: {integrity: sha512-E5XrT4CbbXcXWy+1jChlZmrmCwd5KGx502kDCXJJ7y898TtWW9FwoG5HfOLVRKmlmDGkWN2HM9Ho+/Y8F0sJDg==} + get-tsconfig@4.14.3: dependencies: resolve-pkg-maps: 1.0.0 - dev: true - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 - dev: true - /glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 - dev: true - /glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - dev: false + glob-to-regexp@0.4.1: {} - /glob@10.3.3: - resolution: {integrity: sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true + glob@13.0.6: dependencies: - foreground-child: 3.1.1 - jackspeak: 2.2.1 - minimatch: 9.0.3 - minipass: 7.0.2 - path-scurry: 1.10.1 - dev: true + minimatch: 10.2.6 + minipass: 7.1.3 + path-scurry: 2.0.2 - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + glob@7.2.3: dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 3.1.5 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true - - /globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - dev: true - /globals@13.20.0: - resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==} - engines: {node: '>=8'} - dependencies: - type-fest: 0.20.2 - dev: true - - /globalthis@1.0.3: - resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} - engines: {node: '>= 0.4'} - dependencies: - define-properties: 1.2.0 - dev: true + globals@14.0.0: {} - /globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.0 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 3.0.0 - dev: true + globals@15.15.0: {} - /globby@13.2.2: - resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + globalthis@1.0.4: dependencies: - dir-glob: 3.0.1 - fast-glob: 3.3.0 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 4.0.0 - dev: true + define-properties: 1.2.1 + gopd: 1.2.0 - /gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + globby@16.2.4: dependencies: - get-intrinsic: 1.2.1 - - /graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - /grapheme-splitter@1.0.4: - resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} - dev: true - - /graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - dev: true - - /graphql@16.7.1: - resolution: {integrity: sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==} - engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - dev: true - - /hard-rejection@2.1.0: - resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} - engines: {node: '>=6'} - dev: true - - /has-bigints@1.0.2: - resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + '@sindresorhus/merge-streams': 4.0.0 + fast-glob: 3.3.3 + ignore: 7.0.9 + is-path-inside: 4.0.0 + micromatch: 4.0.8 + slash: 5.1.0 + unicorn-magic: 0.4.0 - /has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} + globrex@0.1.2: {} - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} + gopd@1.2.0: {} - /has-property-descriptors@1.0.0: - resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} - dependencies: - get-intrinsic: 1.2.1 + graceful-fs@4.2.11: {} - /has-proto@1.0.1: - resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} - engines: {node: '>= 0.4'} + graphql@16.14.2: {} - /has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} + has-bigints@1.1.0: {} - /has-tostringtag@1.0.0: - resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} - engines: {node: '>= 0.4'} + has-property-descriptors@1.0.2: dependencies: - has-symbols: 1.0.3 + es-define-property: 1.0.1 - /has@1.0.3: - resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} - engines: {node: '>= 0.4.0'} + has-proto@1.2.0: dependencies: - function-bind: 1.1.1 + dunder-proto: 1.0.1 - /he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - dev: true - - /headers-polyfill@3.1.2: - resolution: {integrity: sha512-tWCK4biJ6hcLqTviLXVR9DTRfYGQMXEIUj3gwJ2rZ5wO/at3XtkI4g8mCvFdUF9l1KMBNCfmNAdnahm1cgavQA==} - dev: true + has-symbols@1.1.0: {} - /hosted-git-info@4.1.0: - resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} - engines: {node: '>=10'} + has-tostringtag@1.0.2: dependencies: - lru-cache: 6.0.0 - dev: true + has-symbols: 1.1.0 - /hosted-git-info@6.1.1: - resolution: {integrity: sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hashery@1.5.1: dependencies: - lru-cache: 7.18.3 - dev: true + hookified: 1.15.1 - /html-encoding-sniffer@3.0.0: - resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} - engines: {node: '>=12'} + hasown@2.0.4: dependencies: - whatwg-encoding: 2.0.0 - dev: true + function-bind: 1.1.2 - /html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - dev: true - - /http-proxy-agent@5.0.0: - resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} - engines: {node: '>= 6'} + headers-polyfill@5.0.1: dependencies: - '@tootallnate/once': 2.0.0 - agent-base: 6.0.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: true + '@types/set-cookie-parser': 2.4.10 + set-cookie-parser: 3.1.1 - /https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - dependencies: - agent-base: 6.0.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: true + hermes-estree@0.25.1: {} - /human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - dev: true + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 - /human-signals@4.3.1: - resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} - engines: {node: '>=14.18.0'} - dev: true + hookified@1.15.1: {} - /iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - dev: true + hookified@2.2.0: {} - /iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} + html-encoding-sniffer@6.0.0: dependencies: - safer-buffer: 2.1.2 - dev: true + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' - /ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: true + ignore@5.3.2: {} - /ignore@5.2.4: - resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} - engines: {node: '>= 4'} - dev: true + ignore@7.0.9: {} - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - dev: true - - /import-lazy@4.0.0: - resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} - engines: {node: '>=8'} - dev: true - - /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - dev: true - /indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} + imurmurhash@0.1.4: {} - /indent-string@5.0.0: - resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} - engines: {node: '>=12'} - dev: true + indent-string@4.0.0: {} - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + inflight@1.0.6: dependencies: once: 1.4.0 wrappy: 1.0.2 - dev: true - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true + inherits@2.0.4: {} - /inquirer@8.2.5: - resolution: {integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==} - engines: {node: '>=12.0.0'} + internal-slot@1.1.0: dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.17.21 - mute-stream: 0.0.8 - ora: 5.4.1 - run-async: 2.4.1 - rxjs: 7.8.1 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 - wrap-ansi: 7.0.0 - dev: true + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 - /internal-slot@1.0.5: - resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} - engines: {node: '>= 0.4'} + is-array-buffer@3.0.5: dependencies: - get-intrinsic: 1.2.1 - has: 1.0.3 - side-channel: 1.0.4 + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 - /is-arguments@1.1.1: - resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} - engines: {node: '>= 0.4'} + is-async-function@2.1.1: dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 - /is-array-buffer@3.0.2: - resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + is-bigint@1.1.0: dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - is-typed-array: 1.1.10 - - /is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - dev: true + has-bigints: 1.1.0 - /is-bigint@1.0.4: - resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + is-boolean-object@1.2.2: dependencies: - has-bigints: 1.0.2 + call-bound: 1.0.4 + has-tostringtag: 1.0.2 - /is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - dependencies: - binary-extensions: 2.2.0 - dev: true + is-callable@1.2.7: {} - /is-boolean-object@1.1.2: - resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} - engines: {node: '>= 0.4'} + is-core-module@2.16.2: dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 - - /is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} + hasown: 2.0.4 - /is-core-module@2.12.1: - resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} + is-data-view@1.0.2: dependencies: - has: 1.0.3 - dev: true + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 - /is-date-object@1.0.5: - resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} - engines: {node: '>= 0.4'} + is-date-object@1.1.0: dependencies: - has-tostringtag: 1.0.0 - - /is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - dev: true - - /is-docker@3.0.0: - resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true - dev: true + call-bound: 1.0.4 + has-tostringtag: 1.0.2 - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: true - - /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - dev: true - - /is-generator-function@1.0.10: - resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} - engines: {node: '>= 0.4'} + is-document.all@1.0.0: dependencies: - has-tostringtag: 1.0.0 - dev: true + call-bound: 1.0.4 - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - dependencies: - is-extglob: 2.1.1 - dev: true + is-extglob@2.1.1: {} - /is-inside-container@1.0.0: - resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} - engines: {node: '>=14.16'} - hasBin: true + is-finalizationregistry@1.1.1: dependencies: - is-docker: 3.0.0 - dev: true - - /is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - dev: true - - /is-map@2.0.2: - resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + call-bound: 1.0.4 - /is-negative-zero@2.0.2: - resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} - engines: {node: '>= 0.4'} - dev: true - - /is-node-process@1.2.0: - resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} - dev: true + is-fullwidth-code-point@3.0.0: {} - /is-number-object@1.0.7: - resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} - engines: {node: '>= 0.4'} + is-generator-function@1.1.2: dependencies: - has-tostringtag: 1.0.0 + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: true + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 - /is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - dev: true + is-map@2.0.3: {} - /is-plain-obj@1.1.0: - resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} - engines: {node: '>=0.10.0'} - dev: true + is-negative-zero@2.0.3: {} - /is-potential-custom-element-name@1.0.1: - resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - dev: true + is-node-process@1.2.0: {} - /is-regex@1.1.4: - resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} - engines: {node: '>= 0.4'} + is-number-object@1.1.1: dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 + call-bound: 1.0.4 + has-tostringtag: 1.0.2 - /is-set@2.0.2: - resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} + is-number@7.0.0: {} - /is-shared-array-buffer@1.0.2: - resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} - dependencies: - call-bind: 1.0.2 - - /is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - dev: true + is-path-inside@4.0.0: {} - /is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true + is-potential-custom-element-name@1.0.1: {} - /is-string@1.0.7: - resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} - engines: {node: '>= 0.4'} + is-regex@1.2.1: dependencies: - has-tostringtag: 1.0.0 + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 - /is-symbol@1.0.4: - resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} - engines: {node: '>= 0.4'} - dependencies: - has-symbols: 1.0.3 + is-set@2.0.3: {} - /is-typed-array@1.1.10: - resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} - engines: {node: '>= 0.4'} + is-shared-array-buffer@1.0.4: dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.0 - - /is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - dev: true - - /is-weakmap@2.0.1: - resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} + call-bound: 1.0.4 - /is-weakref@1.0.2: - resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + is-string@1.1.1: dependencies: - call-bind: 1.0.2 - dev: true + call-bound: 1.0.4 + has-tostringtag: 1.0.2 - /is-weakset@2.0.2: - resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==} + is-symbol@1.1.1: dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 - /is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} + is-typed-array@1.1.15: dependencies: - is-docker: 2.2.1 - dev: true - - /isarray@0.0.1: - resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} - dev: true - - /isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - dev: true - - /isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true + which-typed-array: 1.1.22 - /istanbul-lib-coverage@3.2.0: - resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} - engines: {node: '>=8'} - dev: true + is-weakmap@2.0.2: {} - /istanbul-lib-report@3.0.0: - resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} - engines: {node: '>=8'} + is-weakref@1.1.1: dependencies: - istanbul-lib-coverage: 3.2.0 - make-dir: 3.1.0 - supports-color: 7.2.0 - dev: true + call-bound: 1.0.4 - /istanbul-lib-source-maps@4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} - engines: {node: '>=10'} + is-weakset@2.0.4: dependencies: - debug: 4.3.4 - istanbul-lib-coverage: 3.2.0 - source-map: 0.6.1 - transitivePeerDependencies: - - supports-color - dev: true + call-bound: 1.0.4 + get-intrinsic: 1.3.0 - /istanbul-reports@3.1.5: - resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==} - engines: {node: '>=8'} - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.0 - dev: true + isarray@0.0.1: {} - /jackspeak@2.2.1: - resolution: {integrity: sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw==} - engines: {node: '>=14'} - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - dev: true + isarray@1.0.0: {} - /jest-diff@27.5.1: - resolution: {integrity: sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - dependencies: - chalk: 4.1.2 - diff-sequences: 27.5.1 - jest-get-type: 27.5.1 - pretty-format: 27.5.1 + isarray@2.0.5: {} - /jest-get-type@27.5.1: - resolution: {integrity: sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + isexe@2.0.0: {} - /jest-matcher-utils@27.5.1: - resolution: {integrity: sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + iterator.prototype@1.1.5: dependencies: - chalk: 4.1.2 - jest-diff: 27.5.1 - jest-get-type: 27.5.1 - pretty-format: 27.5.1 - - /jju@1.4.0: - resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} - dev: true + define-data-property: 1.1.4 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 - /jquery@3.7.0: - resolution: {integrity: sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==} - dev: true + js-tokens@10.0.0: {} - /js-levenshtein@1.1.6: - resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} - engines: {node: '>=0.10.0'} - dev: true - - /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@4.0.0: {} - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true + js-yaml@4.3.2: dependencies: argparse: 2.0.1 - dev: true - /jsdoctypeparser@9.0.0: - resolution: {integrity: sha512-jrTA2jJIL6/DAEILBEh2/w9QxCuwmvNXIry39Ay/HVfhE3o2yVV0U44blYkqdHA/OKloJEqvJy0xU+GSdE2SIw==} - engines: {node: '>=10'} - hasBin: true - dev: true + jsdoc-type-pratt-parser@9.2.1: + dependencies: + '@types/estree': 1.0.9 + '@types/node': 26.5.1 - /jsdom@22.1.0: - resolution: {integrity: sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==} - engines: {node: '>=16'} - peerDependencies: - canvas: ^2.5.0 - peerDependenciesMeta: - canvas: - optional: true + jsdom@30.0.1: dependencies: - abab: 2.0.6 - cssstyle: 3.0.0 - data-urls: 4.0.0 - decimal.js: 10.4.3 - domexception: 4.0.0 - form-data: 4.0.0 - html-encoding-sniffer: 3.0.0 - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 + '@asamuzakjp/css-color': 6.0.7 + '@asamuzakjp/dom-selector': 8.3.2 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.13(css-tree@3.2.1) + '@exodus/bytes': 1.15.1 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.7 - parse5: 7.1.2 - rrweb-cssom: 0.6.0 + lru-cache: 11.5.2 + parse5: 8.0.1 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 4.1.3 - w3c-xmlserializer: 4.0.0 - webidl-conversions: 7.0.0 - whatwg-encoding: 2.0.0 - whatwg-mimetype: 3.0.0 - whatwg-url: 12.0.1 - ws: 8.13.0 - xml-name-validator: 4.0.0 + tough-cookie: 6.0.2 + undici: 8.10.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 17.1.1 + xml-name-validator: 5.0.0 transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: true - - /jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true - dev: true + - '@noble/hashes' - /json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - dev: true + jsesc@3.1.0: {} - /json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true + json-schema-traverse@0.4.1: {} - /json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - dev: true + json-stable-stringify-without-jsonify@1.0.1: {} - /json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true + json5@1.0.2: dependencies: minimist: 1.2.8 - dev: true - - /json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - dev: true - /jsonc-parser@3.2.0: - resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} - dev: true + json5@2.2.3: {} - /jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - optionalDependencies: - graceful-fs: 4.2.11 - dev: true - - /jsx-ast-utils@3.3.4: - resolution: {integrity: sha512-fX2TVdCViod6HwKEtSWGHs57oFhVfCMwieb9PuRDgjDPh5XeqJiHFFFJCHxU5cnTc3Bu/GRL+kPiFmw8XWOfKw==} - engines: {node: '>=4.0'} + jsx-ast-utils@3.3.5: dependencies: - array-includes: 3.1.6 - array.prototype.flat: 1.3.1 - object.assign: 4.1.4 - object.values: 1.1.6 - dev: true + array-includes: 3.2.0 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 - /junk@4.0.1: - resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==} - engines: {node: '>=12.20'} - dev: true + junk@4.0.1: {} - /kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - dev: true + keyv@5.6.0: + dependencies: + '@keyv/serialize': 1.1.1 - /kolorist@1.8.0: - resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} - dev: true + kolorist@1.8.0: {} - /language-subtag-registry@0.3.22: - resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} - dev: true + language-subtag-registry@0.3.23: {} - /language-tags@1.0.5: - resolution: {integrity: sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==} + language-tags@1.0.9: dependencies: - language-subtag-registry: 0.3.22 - dev: true + language-subtag-registry: 0.3.23 - /levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 - dev: true - - /lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - dev: true - /local-pkg@0.4.3: - resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} - engines: {node: '>=14'} - dev: true - - /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - dependencies: - p-locate: 5.0.0 - dev: true + lightningcss-android-arm64@1.33.0: + optional: true - /locate-path@7.2.0: - resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - p-locate: 6.0.0 - dev: true + lightningcss-darwin-arm64@1.33.0: + optional: true - /lodash.get@4.4.2: - resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - dev: true + lightningcss-darwin-x64@1.33.0: + optional: true - /lodash.isequal@4.5.0: - resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - dev: true + lightningcss-freebsd-x64@1.33.0: + optional: true - /lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - dev: true + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lightningcss-linux-arm64-gnu@1.33.0: + optional: true - /log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} - dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 - dev: true + lightningcss-linux-arm64-musl@1.33.0: + optional: true - /loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - dependencies: - js-tokens: 4.0.0 + lightningcss-linux-x64-gnu@1.33.0: + optional: true - /loupe@2.3.6: - resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} - dependencies: - get-func-name: 2.0.0 - dev: true + lightningcss-linux-x64-musl@1.33.0: + optional: true - /lru-cache@10.0.0: - resolution: {integrity: sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==} - engines: {node: 14 || >=16.14} - dev: true + lightningcss-win32-arm64-msvc@1.33.0: + optional: true - /lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - dependencies: - yallist: 3.1.1 - dev: true + lightningcss-win32-x64-msvc@1.33.0: + optional: true - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} + lightningcss@1.33.0: dependencies: - yallist: 4.0.0 - dev: true - - /lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - dev: true - - /lz-string@1.5.0: - resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true - - /magic-string@0.30.1: - resolution: {integrity: sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==} - engines: {node: '>=12'} + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + local-pkg@1.2.1: + dependencies: + mlly: 1.8.2 + pkg-types: 2.3.3 + quansync: 0.2.11 + + locate-path@6.0.0: dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true + p-locate: 5.0.0 - /make-dir@3.1.0: - resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} - engines: {node: '>=8'} + loose-envify@1.4.0: dependencies: - semver: 6.3.1 - dev: true + js-tokens: 4.0.0 - /map-obj@4.3.0: - resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} - engines: {node: '>=8'} - dev: true - - /meow@12.0.1: - resolution: {integrity: sha512-/QOqMALNoKQcJAOOdIXjNLtfcCdLXbMFyB1fOOPdm6RzfBTlsuodOCTBDjVbeUSmgDQb8UI2oONqYGtq1PKKKA==} - engines: {node: '>=16.10'} - dependencies: - '@types/minimist': 1.2.2 - camelcase-keys: 8.0.2 - decamelize: 6.0.0 - decamelize-keys: 2.0.1 - hard-rejection: 2.1.0 - minimist-options: 4.1.0 - normalize-package-data: 5.0.0 - read-pkg-up: 9.1.0 - redent: 4.0.0 - trim-newlines: 5.0.0 - type-fest: 3.13.1 - yargs-parser: 21.1.1 - dev: true + lru-cache@11.5.2: {} - /merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - dev: true + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 - /merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - dev: true + lz-string@1.5.0: {} - /micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} + magic-string@0.30.21: dependencies: - braces: 3.0.2 - picomatch: 2.3.1 - dev: true + '@jridgewell/sourcemap-codec': 1.6.0 - /mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - dev: true + magic-string@1.3.1: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 - /mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} + magicast@0.5.5: dependencies: - mime-db: 1.52.0 - dev: true + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + source-map-js: 1.2.1 - /mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - dev: true + math-intrinsics@1.1.0: {} - /mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - dev: true + mdn-data@2.27.1: {} - /min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} + meow@14.1.0: {} + + merge2@1.4.1: {} - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + micromatch@4.0.8: dependencies: - brace-expansion: 1.1.11 - dev: true + braces: 3.0.3 + picomatch: 2.3.2 - /minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} + min-indent@1.0.1: {} + + minimatch@10.2.6: dependencies: - brace-expansion: 2.0.1 - dev: true + brace-expansion: 5.0.9 - /minimist-options@4.1.0: - resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} - engines: {node: '>= 6'} + minimatch@3.1.5: dependencies: - arrify: 1.0.1 - is-plain-obj: 1.1.0 - kind-of: 6.0.3 - dev: true + brace-expansion: 1.1.18 - /minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - dev: true + minimist@1.2.8: {} - /minipass@7.0.2: - resolution: {integrity: sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA==} - engines: {node: '>=16 || 14 >=14.17'} - dev: true + minipass@7.1.3: {} - /mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - dev: true + mkdirp@1.0.4: {} - /mlly@1.4.0: - resolution: {integrity: sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==} + mlly@1.8.2: dependencies: - acorn: 8.10.0 - pathe: 1.1.1 - pkg-types: 1.0.3 - ufo: 1.1.2 - dev: true + acorn: 8.18.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true - - /ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true + ms@2.1.3: {} - /msw@1.2.2(typescript@5.1.6): - resolution: {integrity: sha512-GsW3PE/Es/a1tYThXcM8YHOZ1S1MtivcS3He/LQbbTCx3rbWJYCtWD5XXyJ53KlNPT7O1VI9sCW3xMtgFe8XpQ==} - engines: {node: '>=14'} - hasBin: true - requiresBuild: true - peerDependencies: - typescript: '>= 4.4.x <= 5.1.x' - peerDependenciesMeta: - typescript: - optional: true + msw@2.15.0(@types/node@26.5.1)(@typescript/typescript6@6.0.2): dependencies: - '@mswjs/cookies': 0.2.2 - '@mswjs/interceptors': 0.17.9 - '@open-draft/until': 1.0.3 - '@types/cookie': 0.4.1 - '@types/js-levenshtein': 1.1.1 - chalk: 4.1.1 - chokidar: 3.5.3 - cookie: 0.4.2 - graphql: 16.7.1 - headers-polyfill: 3.1.2 - inquirer: 8.2.5 + '@inquirer/confirm': 6.1.1(@types/node@26.5.1) + '@mswjs/interceptors': 0.41.9 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.14.2 + headers-polyfill: 5.0.1 is-node-process: 1.2.0 - js-levenshtein: 1.1.6 - node-fetch: 2.6.12 - outvariant: 1.4.0 - path-to-regexp: 6.2.1 - strict-event-emitter: 0.4.6 - type-fest: 2.19.0 - typescript: 5.1.6 - yargs: 17.7.2 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.11 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.2 + type-fest: 5.8.0 + until-async: 3.0.2 + yargs: 17.7.3 + optionalDependencies: + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - - encoding - - supports-color - dev: true - - /muggle-string@0.3.1: - resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==} - dev: true - - /mute-stream@0.0.8: - resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - dev: true - - /nanoid@3.3.6: - resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true + - '@types/node' - /natural-compare-lite@1.4.0: - resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} - dev: true + mute-stream@3.0.0: {} - /natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - dev: true + nanoid@3.3.19: {} - /nested-error-stacks@2.1.1: - resolution: {integrity: sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==} - dev: true + natural-compare@1.4.0: {} - /next@13.4.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-4ep6aKxVTQ7rkUW2fBLhpBr/5oceCuf4KmlUpvG/aXuDTIf9mexNSpabUD6RWPspu6wiJJvozZREhXhueYO36A==} - engines: {node: '>=16.8.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - fibers: '>= 3.1.0' - react: ^18.2.0 - react-dom: ^18.2.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - fibers: - optional: true - sass: - optional: true + next@16.3.5(@babel/core@7.29.7)(@types/node@26.5.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: - '@next/env': 13.4.10 - '@swc/helpers': 0.5.1 - busboy: 1.6.0 - caniuse-lite: 1.0.30001516 - postcss: 8.4.14 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.1.1(react@18.2.0) - watchpack: 2.4.0 - zod: 3.21.4 + '@next/env': 16.3.5 + '@swc/helpers': 0.5.23 + baseline-browser-mapping: 2.11.23 + caniuse-lite: 1.0.30001810 + postcss: 8.5.23 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.3.0) optionalDependencies: - '@next/swc-darwin-arm64': 13.4.10 - '@next/swc-darwin-x64': 13.4.10 - '@next/swc-linux-arm64-gnu': 13.4.10 - '@next/swc-linux-arm64-musl': 13.4.10 - '@next/swc-linux-x64-gnu': 13.4.10 - '@next/swc-linux-x64-musl': 13.4.10 - '@next/swc-win32-arm64-msvc': 13.4.10 - '@next/swc-win32-ia32-msvc': 13.4.10 - '@next/swc-win32-x64-msvc': 13.4.10 + '@next/swc-darwin-arm64': 16.3.5 + '@next/swc-darwin-x64': 16.3.5 + '@next/swc-linux-arm64-gnu': 16.3.5 + '@next/swc-linux-arm64-musl': 16.3.5 + '@next/swc-linux-x64-gnu': 16.3.5 + '@next/swc-linux-x64-musl': 16.3.5 + '@next/swc-win32-arm64-msvc': 16.3.5 + '@next/swc-win32-x64-msvc': 16.3.5 + sharp: 0.35.4(@types/node@26.5.1) transitivePeerDependencies: - '@babel/core' + - '@types/node' - babel-plugin-macros - dev: false - /node-fetch@2.6.12: - resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true + node-exports-info@1.6.2: dependencies: - whatwg-url: 5.0.0 - dev: true + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 - /node-releases@2.0.13: - resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} - dev: true + node-releases@2.0.55: {} - /noms@0.0.0: - resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==} + noms@0.0.0: dependencies: inherits: 2.0.4 readable-stream: 1.0.34 - dev: true - - /normalize-package-data@3.0.3: - resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} - engines: {node: '>=10'} - dependencies: - hosted-git-info: 4.1.0 - is-core-module: 2.12.1 - semver: 7.5.4 - validate-npm-package-license: 3.0.4 - dev: true - - /normalize-package-data@5.0.0: - resolution: {integrity: sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - hosted-git-info: 6.1.1 - is-core-module: 2.12.1 - semver: 7.5.4 - validate-npm-package-license: 3.0.4 - dev: true - /normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - dev: true - - /npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - dependencies: - path-key: 3.1.1 - dev: true - - /npm-run-path@5.1.0: - resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - path-key: 4.0.0 - dev: true - - /nwsapi@2.2.7: - resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} - dev: true + object-assign@4.1.1: {} - /object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - dev: true - - /object-inspect@1.12.3: - resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} - - /object-is@1.1.5: - resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 + object-inspect@1.13.4: {} - /object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} + object-keys@1.1.1: {} - /object.assign@4.1.4: - resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} - engines: {node: '>= 0.4'} + object.assign@4.1.7: dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - has-symbols: 1.0.3 + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 object-keys: 1.1.1 - /object.entries@1.1.6: - resolution: {integrity: sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.3 - dev: true - - /object.fromentries@2.0.6: - resolution: {integrity: sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.3 - dev: true - - /object.hasown@1.1.2: - resolution: {integrity: sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==} + object.entries@1.1.9: dependencies: - define-properties: 1.2.0 - es-abstract: 1.21.3 - dev: true + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 - /object.values@1.1.6: - resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} - engines: {node: '>= 0.4'} + object.fromentries@2.0.8: dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.3 - dev: true + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + object.groupby@1.0.3: dependencies: - wrappy: 1.0.2 - dev: true + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 - /onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} + object.values@1.2.1: dependencies: - mimic-fn: 2.1.0 - dev: true + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 - /onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} - dependencies: - mimic-fn: 4.0.0 - dev: true + obug@2.2.1: {} - /open@9.1.0: - resolution: {integrity: sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==} - engines: {node: '>=14.16'} + once@1.4.0: dependencies: - default-browser: 4.0.0 - define-lazy-prop: 3.0.0 - is-inside-container: 1.0.0 - is-wsl: 2.2.0 - dev: true + wrappy: 1.0.2 - /optionator@0.9.3: - resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} - engines: {node: '>= 0.8.0'} + optionator@0.9.4: dependencies: - '@aashutoshrathi/word-wrap': 1.2.6 deep-is: 0.1.4 fast-levenshtein: 2.0.6 levn: 0.4.1 prelude-ls: 1.2.1 type-check: 0.4.0 - dev: true + word-wrap: 1.2.5 - /ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} - dependencies: - bl: 4.1.0 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.9.0 - is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 - log-symbols: 4.1.0 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - dev: true - - /os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - dev: true - - /outvariant@1.4.0: - resolution: {integrity: sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==} - dev: true + outvariant@1.4.3: {} - /p-event@5.0.1: - resolution: {integrity: sha512-dd589iCQ7m1L0bmC5NLlVYfy3TbBEsMUfWx9PyAgPeIcFZ/E2yaTZ4Rz4MiBmmJShviiftHVXOqfnfzJ6kyMrQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + own-keys@1.0.2: dependencies: - p-timeout: 5.1.0 - dev: true + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 - /p-filter@3.0.0: - resolution: {integrity: sha512-QtoWLjXAW++uTX67HZQz1dbTpqBfiidsB6VtQUC9iR85S120+s0T5sO6s+B5MLzFcZkrEd/DGMmCjR+f2Qpxwg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-event@6.0.1: dependencies: - p-map: 5.5.0 - dev: true + p-timeout: 6.1.4 - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} + p-filter@4.1.0: dependencies: - yocto-queue: 0.1.0 - dev: true + p-map: 7.0.8 - /p-limit@4.0.0: - resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-limit@3.1.0: dependencies: - yocto-queue: 1.0.0 - dev: true + yocto-queue: 0.1.0 - /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} + p-locate@5.0.0: dependencies: p-limit: 3.1.0 - dev: true - - /p-locate@6.0.0: - resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - p-limit: 4.0.0 - dev: true - /p-map@5.5.0: - resolution: {integrity: sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==} - engines: {node: '>=12'} - dependencies: - aggregate-error: 4.0.1 - dev: true + p-map@7.0.8: {} - /p-map@6.0.0: - resolution: {integrity: sha512-T8BatKGY+k5rU+Q/GTYgrEf2r4xRMevAN5mtXc2aPc4rS1j3s+vWTaO2Wag94neXuCAUAs8cxBL9EeB5EA6diw==} - engines: {node: '>=16'} - dev: true + p-timeout@6.1.4: {} - /p-timeout@5.1.0: - resolution: {integrity: sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==} - engines: {node: '>=12'} - dev: true + package-json-from-dist@1.0.1: {} - /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} + parent-module@1.0.1: dependencies: callsites: 3.1.0 - dev: true - - /parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - dependencies: - '@babel/code-frame': 7.22.5 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - dev: true - /parse5@7.1.2: - resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + parse5@8.0.1: dependencies: - entities: 4.5.0 - dev: true + entities: 8.0.0 - /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - dev: true + path-browserify@1.0.1: {} - /path-exists@5.0.0: - resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - - /path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - dev: true + path-exists@4.0.0: {} - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - dev: true + path-is-absolute@1.0.1: {} - /path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - dev: true + path-key@3.1.1: {} - /path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true + path-parse@1.0.7: {} - /path-scurry@1.10.1: - resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} - engines: {node: '>=16 || 14 >=14.17'} + path-scurry@2.0.2: dependencies: - lru-cache: 10.0.0 - minipass: 7.0.2 - dev: true + lru-cache: 11.5.2 + minipass: 7.1.3 - /path-to-regexp@6.2.1: - resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} - dev: true + path-to-regexp@6.3.0: {} - /path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - dev: true + pathe@2.0.3: {} - /pathe@1.1.1: - resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} - dev: true + picocolors@1.1.1: {} - /pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - dev: true + picomatch@2.3.2: {} - /picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + picomatch@4.0.7: {} - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: true + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 - /pkg-types@1.0.3: - resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + pkg-types@2.3.3: dependencies: - jsonc-parser: 3.2.0 - mlly: 1.4.0 - pathe: 1.1.1 - dev: true + confbox: 0.3.1 + exsolve: 1.1.1 + pathe: 2.0.3 - /popper.js@1.16.1: - resolution: {integrity: sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==} - deprecated: You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1 - dev: true + possible-typed-array-names@1.1.0: {} - /postcss@8.4.14: - resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==} - engines: {node: ^10 || ^12 || >=14} + postcss@8.5.23: dependencies: - nanoid: 3.3.6 - picocolors: 1.0.0 - source-map-js: 1.0.2 - dev: false + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 - /postcss@8.4.26: - resolution: {integrity: sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==} - engines: {node: ^10 || ^12 || >=14} + postcss@8.5.28: dependencies: - nanoid: 3.3.6 - picocolors: 1.0.0 - source-map-js: 1.0.2 - dev: true + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 - /prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - dev: true + prelude-ls@1.2.1: {} - /pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + prettier-linter-helpers@1.0.1: dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 + fast-diff: 1.3.0 - /pretty-format@29.6.1: - resolution: {integrity: sha512-7jRj+yXO0W7e4/tSJKoR7HRIHLPPjtNaUGG2xxKQnGvPNRkgWcQ0AZX6P4KBRJN4FcTBWb3sa7DVUJmocYuoog==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + prettier@3.9.6: {} + + pretty-format@27.5.1: dependencies: - '@jest/schemas': 29.6.0 + ansi-regex: 5.0.1 ansi-styles: 5.2.0 - react-is: 18.2.0 - dev: true + react-is: 17.0.2 - /process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - dev: true + process-nextick-args@2.0.1: {} - /prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 - dev: true - - /psl@1.9.0: - resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} - dev: true - - /punycode@2.3.0: - resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} - engines: {node: '>=6'} - dev: true - - /querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - dev: true - - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true - /quick-lru@6.1.1: - resolution: {integrity: sha512-S27GBT+F0NTRiehtbrgaSE1idUAJ5bX8dPAQTdylEyNlrdcH5X4Lz7Edz3DYzecbsCluD5zO8ZNEe04z3D3u6Q==} - engines: {node: '>=12'} - dev: true + punycode@2.3.1: {} - /react-dom@18.2.0(react@18.2.0): - resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} - peerDependencies: - react: ^18.2.0 + qified@0.10.1: dependencies: - loose-envify: 1.4.0 - react: 18.2.0 - scheduler: 0.23.0 - - /react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: true - - /react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + hookified: 2.2.0 - /react-is@18.2.0: - resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} - dev: true + quansync@0.2.11: {} - /react-refresh@0.14.0: - resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} - engines: {node: '>=0.10.0'} - dev: true + queue-microtask@1.2.3: {} - /react-router-dom@6.14.1(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-ssF6M5UkQjHK70fgukCJyjlda0Dgono2QGwqGvuk7D+EDGHdacEN3Yke2LTMjkrpHuFwBfDFsEjGVXBDmL+bWw==} - engines: {node: '>=14'} - peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' + react-dom@19.3.0(react@19.3.0): dependencies: - '@remix-run/router': 1.7.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-router: 6.14.1(react@18.2.0) + react: 19.3.0 + scheduler: 0.28.0 - /react-router@6.14.1(react@18.2.0): - resolution: {integrity: sha512-U4PfgvG55LdvbQjg5Y9QRWyVxIdO1LlpYT7x+tMAxd9/vmiPuJhIwdxZuIQLN/9e3O4KFDHYfR9gzGeYMasW8g==} - engines: {node: '>=14'} - peerDependencies: - react: '>=16.8' - dependencies: - '@remix-run/router': 1.7.1 - react: 18.2.0 + react-is@16.13.1: {} - /react@18.2.0: - resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} - engines: {node: '>=0.10.0'} - dependencies: - loose-envify: 1.4.0 + react-is@17.0.2: {} - /read-pkg-up@9.1.0: - resolution: {integrity: sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + react-router-dom@7.18.3(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: - find-up: 6.3.0 - read-pkg: 7.1.0 - type-fest: 2.19.0 - dev: true + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + react-router: 7.18.3(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - /read-pkg@7.1.0: - resolution: {integrity: sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg==} - engines: {node: '>=12.20'} + react-router@7.18.3(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: - '@types/normalize-package-data': 2.4.1 - normalize-package-data: 3.0.3 - parse-json: 5.2.0 - type-fest: 2.19.0 - dev: true + cookie: 1.1.1 + react: 19.3.0 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.3.0(react@19.3.0) - /readable-stream@1.0.34: - resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} + react@19.3.0: {} + + readable-stream@1.0.34: dependencies: core-util-is: 1.0.3 inherits: 2.0.4 isarray: 0.0.1 string_decoder: 0.10.31 - dev: true - /readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 inherits: 2.0.4 @@ -5147,1211 +5965,699 @@ packages: safe-buffer: 5.1.2 string_decoder: 1.1.1 util-deprecate: 1.0.2 - dev: true - - /readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - dev: true - - /readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - dependencies: - picomatch: 2.3.1 - dev: true - /redent@3.0.0: - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} - engines: {node: '>=8'} + redent@3.0.0: dependencies: indent-string: 4.0.0 strip-indent: 3.0.0 - /redent@4.0.0: - resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} - engines: {node: '>=12'} + refa@0.12.1: dependencies: - indent-string: 5.0.0 - strip-indent: 4.0.0 - dev: true + '@eslint-community/regexpp': 4.12.2 - /refa@0.11.0: - resolution: {integrity: sha512-486O8/pQXwj9jV0mVvUnTsxq0uknpBnNJ0eCUhkZqJRQ8KutrT1PhzmumdCeM1hSBF2eMlFPmwECRER4IbKXlQ==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + reflect.getprototypeof@1.0.10: dependencies: - '@eslint-community/regexpp': 4.5.1 - dev: true - - /regenerator-runtime@0.13.11: - resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 - /regexp-ast-analysis@0.6.0: - resolution: {integrity: sha512-OLxjyjPkVH+rQlBLb1I/P/VTmamSjGkvN5PTV5BXP432k3uVz727J7H29GA5IFiY0m7e1xBN7049Wn59FY3DEQ==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + regexp-ast-analysis@0.7.1: dependencies: - '@eslint-community/regexpp': 4.5.1 - refa: 0.11.0 - dev: true + '@eslint-community/regexpp': 4.12.2 + refa: 0.12.1 - /regexp.prototype.flags@1.5.0: - resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} - engines: {node: '>= 0.4'} + regexp.prototype.flags@1.5.4: dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - functions-have-names: 1.2.3 - - /regexpp@3.2.0: - resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} - engines: {node: '>=8'} - dev: true - - /require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - dev: true + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 - /requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - dev: true - - /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - dev: true + require-directory@2.1.1: {} - /resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - dev: true + require-from-string@2.0.2: {} - /resolve@1.19.0: - resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==} - dependencies: - is-core-module: 2.12.1 - path-parse: 1.0.7 - dev: true + resolve-from@4.0.0: {} - /resolve@1.22.2: - resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} - hasBin: true - dependencies: - is-core-module: 2.12.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - dev: true + resolve-pkg-maps@1.0.0: {} - /resolve@2.0.0-next.4: - resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} - hasBin: true + resolve@2.0.0-next.7: dependencies: - is-core-module: 2.12.1 + es-errors: 1.3.0 + is-core-module: 2.16.2 + node-exports-info: 1.6.2 + object-keys: 1.1.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: true - /restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - dev: true + rettime@0.11.11: {} - /reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true + reusify@1.1.0: {} - /rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true + rimraf@6.1.3: dependencies: - glob: 7.2.3 - dev: true + glob: 13.0.6 + package-json-from-dist: 1.0.1 - /rimraf@5.0.1: - resolution: {integrity: sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==} - engines: {node: '>=14'} - hasBin: true + rolldown@1.2.8: dependencies: - glob: 10.3.3 - dev: true - - /rollup@3.26.2: - resolution: {integrity: sha512-6umBIGVz93er97pMgQO08LuH3m6PUb3jlDUUGFsNJB6VgTCUaDFpupf5JfU30529m/UKOgmiX+uY6Sx8cOYpLA==} - engines: {node: '>=14.18.0', npm: '>=8.0.0'} - hasBin: true + '@oxc-project/types': 0.149.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - fsevents: 2.3.2 - dev: true - - /rrweb-cssom@0.6.0: - resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} - dev: true - - /run-applescript@5.0.0: - resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} - engines: {node: '>=12'} - dependencies: - execa: 5.1.1 - dev: true - - /run-async@2.4.1: - resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} - engines: {node: '>=0.12.0'} - dev: true - - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + '@rolldown/binding-android-arm-eabi': 1.2.8 + '@rolldown/binding-android-arm64': 1.2.8 + '@rolldown/binding-darwin-arm64': 1.2.8 + '@rolldown/binding-darwin-x64': 1.2.8 + '@rolldown/binding-freebsd-x64': 1.2.8 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.8 + '@rolldown/binding-linux-arm64-gnu': 1.2.8 + '@rolldown/binding-linux-arm64-musl': 1.2.8 + '@rolldown/binding-linux-ppc64-gnu': 1.2.8 + '@rolldown/binding-linux-s390x-gnu': 1.2.8 + '@rolldown/binding-linux-x64-gnu': 1.2.8 + '@rolldown/binding-linux-x64-musl': 1.2.8 + '@rolldown/binding-openharmony-arm64': 1.2.8 + '@rolldown/binding-win32-arm64-msvc': 1.2.8 + '@rolldown/binding-win32-x64-msvc': 1.2.8 + + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 - dev: true - - /rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - dependencies: - tslib: 2.6.0 - dev: true - - /safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - dev: true - - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: true - - /safe-regex-test@1.0.0: - resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - is-regex: 1.1.4 - dev: true - - /safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: true - - /saxes@6.0.0: - resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} - engines: {node: '>=v12.22.7'} - dependencies: - xmlchars: 2.2.0 - dev: true - /scheduler@0.23.0: - resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + safe-array-concat@1.1.4: dependencies: - loose-envify: 1.4.0 - - /scslre@0.2.0: - resolution: {integrity: sha512-4hc49fUMmX3jM0XdFUAPBrs1xwEcdHa0KyjEsjFs+Zfc66mpFpq5YmRgDtl+Ffo6AtJIilfei+yKw8fUn3N88w==} - dependencies: - '@eslint-community/regexpp': 4.5.1 - refa: 0.11.0 - regexp-ast-analysis: 0.6.0 - dev: true - - /semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - dev: true + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 - /semver@7.3.8: - resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} - engines: {node: '>=10'} - hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true + safe-buffer@5.1.2: {} - /semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} - engines: {node: '>=10'} - hasBin: true + safe-push-apply@1.0.0: dependencies: - lru-cache: 6.0.0 - dev: true - - /set-cookie-parser@2.6.0: - resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} - dev: true + es-errors: 1.3.0 + isarray: 2.0.5 - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} + safe-regex-test@1.1.0: dependencies: - shebang-regex: 3.0.0 - dev: true - - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - dev: true + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 - /side-channel@1.0.4: - resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + saxes@6.0.0: dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - object-inspect: 1.12.3 + xmlchars: 2.2.0 - /siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - dev: true + scheduler@0.28.0: {} - /signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: true + scslre@0.3.0: + dependencies: + '@eslint-community/regexpp': 4.12.2 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 - /signal-exit@4.0.2: - resolution: {integrity: sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==} - engines: {node: '>=14'} - dev: true + semver@6.3.1: {} - /slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - dev: true + semver@7.8.5: {} - /slash@4.0.0: - resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} - engines: {node: '>=12'} - dev: true + set-cookie-parser@2.7.2: {} - /source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} + set-cookie-parser@3.1.1: {} - /source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + set-function-length@1.2.2: dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - dev: true + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 - /source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - dev: true + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 - /spdx-correct@3.2.0: - resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + set-proto@1.0.0: dependencies: - spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.13 - dev: true + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 - /spdx-exceptions@2.3.0: - resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} - dev: true + sharp@0.35.4(@types/node@26.5.1): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.4 + '@img/sharp-darwin-x64': 0.35.4 + '@img/sharp-freebsd-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-linux-arm': 0.35.4 + '@img/sharp-linux-arm64': 0.35.4 + '@img/sharp-linux-ppc64': 0.35.4 + '@img/sharp-linux-riscv64': 0.35.4 + '@img/sharp-linux-s390x': 0.35.4 + '@img/sharp-linux-x64': 0.35.4 + '@img/sharp-linuxmusl-arm64': 0.35.4 + '@img/sharp-linuxmusl-x64': 0.35.4 + '@img/sharp-webcontainers-wasm32': 0.35.4 + '@img/sharp-win32-arm64': 0.35.4 + '@img/sharp-win32-ia32': 0.35.4 + '@img/sharp-win32-x64': 0.35.4 + '@types/node': 26.5.1 + optional: true - /spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + shebang-command@2.0.0: dependencies: - spdx-exceptions: 2.3.0 - spdx-license-ids: 3.0.13 - dev: true + shebang-regex: 3.0.0 - /spdx-license-ids@3.0.13: - resolution: {integrity: sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==} - dev: true + shebang-regex@3.0.0: {} - /sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - dev: true + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 - /stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - dev: true + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 - /std-env@3.3.3: - resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==} - dev: true + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 - /stop-iteration-iterator@1.0.0: - resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} - engines: {node: '>= 0.4'} + side-channel@1.1.1: dependencies: - internal-slot: 1.0.5 + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 - /streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - dev: false + siginfo@2.0.0: {} - /strict-event-emitter@0.2.8: - resolution: {integrity: sha512-KDf/ujU8Zud3YaLtMCcTI4xkZlZVIYxTLr+XIULexP+77EEVWixeXroLUXQXiVtH4XH2W7jr/3PT1v3zBuvc3A==} - dependencies: - events: 3.3.0 - dev: true + signal-exit@4.1.0: {} - /strict-event-emitter@0.4.6: - resolution: {integrity: sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==} - dev: true + slash@5.1.0: {} - /string-argv@0.3.2: - resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} - engines: {node: '>=0.6.19'} - dev: true + source-map-js@1.2.1: {} - /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} + stackback@0.0.2: {} + + statuses@2.0.2: {} + + std-env@4.2.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + strict-event-emitter@0.5.1: {} + + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - dev: true - - /string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 - dev: true - - /string.prototype.matchall@4.0.8: - resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.3 - get-intrinsic: 1.2.1 - has-symbols: 1.0.3 - internal-slot: 1.0.5 - regexp.prototype.flags: 1.5.0 - side-channel: 1.0.4 - dev: true - - /string.prototype.trim@1.2.7: - resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.3 - dev: true - /string.prototype.trimend@1.0.6: - resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} + string.prototype.includes@2.0.1: dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.3 - dev: true + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 - /string.prototype.trimstart@1.0.6: - resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} + string.prototype.matchall@4.1.0: dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.21.3 - dev: true - - /string_decoder@0.10.31: - resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} - dev: true + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.1 - /string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string.prototype.repeat@1.0.0: dependencies: - safe-buffer: 5.1.2 - dev: true + define-properties: 1.2.1 + es-abstract: 1.24.2 - /string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + string.prototype.trim@1.2.11: dependencies: - safe-buffer: 5.2.1 - dev: true + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} + string.prototype.trimend@1.0.10: dependencies: - ansi-regex: 5.0.1 - dev: true + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 - /strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} + string.prototype.trimstart@1.0.8: dependencies: - ansi-regex: 6.0.1 - dev: true - - /strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - dev: true - - /strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - dev: true + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 - /strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - dev: true + string_decoder@0.10.31: {} - /strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} + string_decoder@1.1.1: dependencies: - min-indent: 1.0.1 + safe-buffer: 5.1.2 - /strip-indent@4.0.0: - resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} - engines: {node: '>=12'} + strip-ansi@6.0.1: dependencies: - min-indent: 1.0.1 - dev: true + ansi-regex: 5.0.1 - /strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - dev: true + strip-bom@3.0.0: {} - /strip-literal@1.0.1: - resolution: {integrity: sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==} + strip-indent@3.0.0: dependencies: - acorn: 8.10.0 - dev: true + min-indent: 1.0.1 - /styled-jsx@5.1.1(react@18.2.0): - resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@babel/core': '*' - babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' - peerDependenciesMeta: - '@babel/core': - optional: true - babel-plugin-macros: - optional: true - dependencies: - client-only: 0.0.1 - react: 18.2.0 - dev: false + strip-json-comments@3.1.1: {} - /supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} + styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.3.0): dependencies: - has-flag: 3.0.0 + client-only: 0.0.1 + react: 19.3.0 + optionalDependencies: + '@babel/core': 7.29.7 - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} + styled-jsx@5.1.7(@babel/core@7.29.7)(react@19.3.0): dependencies: - has-flag: 4.0.0 + client-only: 0.0.1 + react: 19.3.0 + optionalDependencies: + '@babel/core': 7.29.7 - /supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - dev: true + supports-preserve-symlinks-flag@1.0.0: {} - /symbol-tree@3.2.4: - resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - dev: true + symbol-tree@3.2.4: {} - /synckit@0.8.5: - resolution: {integrity: sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==} - engines: {node: ^14.18.0 || >=16.0.0} + synckit@0.11.13: dependencies: - '@pkgr/utils': 2.4.2 - tslib: 2.6.0 - dev: true - - /tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} - engines: {node: '>=6'} - dev: true + '@pkgr/core': 0.3.6 - /test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.1.2 - dev: true + tagged-tag@1.0.0: {} - /text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true + tapable@2.3.3: {} - /through2@2.0.5: - resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + through2@2.0.5: dependencies: readable-stream: 2.3.8 xtend: 4.0.2 - dev: true - /through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - dev: true + tinybench@6.1.4: {} - /tinybench@2.5.0: - resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} - dev: true + tinyexec@1.3.0: {} - /tinypool@0.6.0: - resolution: {integrity: sha512-FdswUUo5SxRizcBc6b1GSuLpLjisa8N8qMyYoP3rl+bym+QauhtJP5bvZY1ytt8krKGmMLYIRl36HBZfeAoqhQ==} - engines: {node: '>=14.0.0'} - dev: true + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 - /tinyspy@2.1.1: - resolution: {integrity: sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==} - engines: {node: '>=14.0.0'} - dev: true + tinyrainbow@3.1.1: {} - /titleize@3.0.0: - resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} - engines: {node: '>=12'} - dev: true + tldts-core@7.4.7: {} - /tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} + tldts@7.4.7: dependencies: - os-tmpdir: 1.0.2 - dev: true - - /to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - dev: true + tldts-core: 7.4.7 - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - dev: true - /tough-cookie@4.1.3: - resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} - engines: {node: '>=6'} + tough-cookie@6.0.2: dependencies: - psl: 1.9.0 - punycode: 2.3.0 - universalify: 0.2.0 - url-parse: 1.5.10 - dev: true + tldts: 7.4.7 - /tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - dev: true - - /tr46@4.1.1: - resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} - engines: {node: '>=14'} + tr46@6.0.0: dependencies: - punycode: 2.3.0 - dev: true + punycode: 2.3.1 - /trim-newlines@5.0.0: - resolution: {integrity: sha512-kstfs+hgwmdsOadN3KgA+C68wPJwnZq4DN6WMDCvZapDWEF34W2TyPKN2v2+BJnZgIz5QOfxFeldLyYvdgRAwg==} - engines: {node: '>=14.16'} - dev: true + ts-api-utils@2.5.0(@typescript/typescript6@6.0.2): + dependencies: + typescript: '@typescript/typescript6@6.0.2' - /tsconfig-paths@3.14.2: - resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 json5: 1.0.2 minimist: 1.2.8 strip-bom: 3.0.0 - dev: true - - /tslib@1.14.1: - resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - dev: true - - /tslib@2.6.0: - resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==} - /tsutils@3.21.0(typescript@5.1.6): - resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} - 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' - dependencies: - tslib: 1.14.1 - typescript: 5.1.6 - dev: true + tslib@2.8.1: {} - /tsx@3.12.7: - resolution: {integrity: sha512-C2Ip+jPmqKd1GWVQDvz/Eyc6QJbGfE7NrR3fx5BpEHMZsEHoIxHL1j+lKdGobr8ovEyqeNkPLSKp6SCSOt7gmw==} - hasBin: true + tsx@4.23.13: dependencies: - '@esbuild-kit/cjs-loader': 2.4.2 - '@esbuild-kit/core-utils': 3.1.0 - '@esbuild-kit/esm-loader': 2.5.5 + esbuild: 0.28.2 optionalDependencies: - fsevents: 2.3.2 - dev: true + fsevents: 2.3.3 - /type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - dev: true - - /type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - dev: true - - /type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - dev: true - - /type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - dev: true - /type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} - dev: true - - /type-fest@3.13.1: - resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} - engines: {node: '>=14.16'} - dev: true - - /typed-array-byte-offset@1.0.0: - resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - for-each: 0.3.3 - has-proto: 1.0.1 - is-typed-array: 1.1.10 - dev: true - - /typed-array-length@1.0.4: - resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + type-fest@5.8.0: dependencies: - call-bind: 1.0.2 - for-each: 0.3.3 - is-typed-array: 1.1.10 - dev: true - - /typescript@5.0.4: - resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} - engines: {node: '>=12.20'} - hasBin: true - dev: true - - /typescript@5.1.6: - resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} - engines: {node: '>=14.17'} - hasBin: true - - /ufo@1.1.2: - resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==} - dev: true + tagged-tag: 1.0.0 - /unbox-primitive@1.0.2: - resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + typed-array-buffer@1.0.3: dependencies: - call-bind: 1.0.2 - has-bigints: 1.0.2 - has-symbols: 1.0.3 - which-boxed-primitive: 1.0.2 - dev: true - - /universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - dev: true - - /universalify@0.2.0: - resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} - engines: {node: '>= 4.0.0'} - dev: true + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 - /untildify@4.0.0: - resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} - engines: {node: '>=8'} - dev: true - - /update-browserslist-db@1.0.11(browserslist@4.21.9): - resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' + typed-array-byte-length@1.0.3: dependencies: - browserslist: 4.21.9 - escalade: 3.1.1 - picocolors: 1.0.0 - dev: true + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 - /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + typed-array-byte-offset@1.0.4: dependencies: - punycode: 2.3.0 - dev: true + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 - /url-parse@1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + typed-array-length@1.0.8: dependencies: - querystringify: 2.2.0 - requires-port: 1.0.0 - dev: true - - /util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: true + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 - /util@0.12.5: - resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - dependencies: - inherits: 2.0.4 - is-arguments: 1.1.1 - is-generator-function: 1.0.10 - is-typed-array: 1.1.10 - which-typed-array: 1.1.10 - dev: true - - /v8-to-istanbul@9.1.0: - resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} - engines: {node: '>=10.12.0'} - dependencies: - '@jridgewell/trace-mapping': 0.3.18 - '@types/istanbul-lib-coverage': 2.0.4 - convert-source-map: 1.9.0 - dev: true - - /validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - dependencies: - spdx-correct: 3.2.0 - spdx-expression-parse: 3.0.1 - dev: true - - /validator@13.9.0: - resolution: {integrity: sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==} - engines: {node: '>= 0.10'} - dev: true - - /vite-node@0.33.0(@types/node@18.11.9): - resolution: {integrity: sha512-19FpHYbwWWxDr73ruNahC+vtEdza52kA90Qb3La98yZ0xULqV8A5JLNPUff0f5zID4984tW7l3DH2przTJUZSw==} - engines: {node: '>=v14.18.0'} - hasBin: true - dependencies: - cac: 6.7.14 - debug: 4.3.4 - mlly: 1.4.0 - pathe: 1.1.1 - picocolors: 1.0.0 - vite: 4.4.4(@types/node@18.11.9) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - stylus - - sugarss - - supports-color - - terser - dev: true + typescript@6.0.3: {} - /vite-plugin-dts@3.3.0(typescript@5.1.6)(vite@4.4.4): - resolution: {integrity: sha512-9jm7wV8fkA4JaKmZdeg/X71dMi8l9SbdmzQRafW4ea1fOfd/LHBDKuwFuxKpK8h1h8O7abKycXS087EP7EL8Hw==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - typescript: '*' - vite: '*' - peerDependenciesMeta: - vite: - optional: true - dependencies: - '@microsoft/api-extractor': 7.36.2 - '@rollup/pluginutils': 5.0.2 - '@vue/language-core': 1.8.5(typescript@5.1.6) - debug: 4.3.4 + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + + ufo@1.6.4: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@8.9.0: {} + + undici@8.10.2: {} + + unicorn-magic@0.4.0: {} + + unplugin-dts@1.1.0(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(rolldown@1.2.8)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)): + dependencies: + '@rollup/pluginutils': 5.4.0 + '@volar/typescript': 2.4.28 + compare-versions: 6.1.1 + debug: 4.4.3 + glob-to-regexp: 0.4.1 kolorist: 1.8.0 - typescript: 5.1.6 - vite: 4.4.4(@types/node@18.11.9) - vue-tsc: 1.8.5(typescript@5.1.6) + local-pkg: 1.2.1 + magic-string: 0.30.21 + typescript: '@typescript/typescript6@6.0.2' + unplugin: 2.3.11 + optionalDependencies: + esbuild: 0.28.2 + rolldown: 1.2.8 + vite: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13) transitivePeerDependencies: - - '@types/node' - - rollup - supports-color - dev: true - /vite@4.4.4(@types/node@18.11.9): - resolution: {integrity: sha512-4mvsTxjkveWrKDJI70QmelfVqTm+ihFAb6+xf4sjEU2TmUCTlVX87tmg/QooPEMQb/lM9qGHT99ebqPziEd3wg==} - engines: {node: ^14.18.0 || >=16.0.0} - hasBin: true - 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 + unplugin@2.3.11: dependencies: - '@types/node': 18.11.9 - esbuild: 0.18.13 - postcss: 8.4.26 - rollup: 3.26.2 - optionalDependencies: - fsevents: 2.3.2 - dev: true + '@jridgewell/remapping': 2.3.5 + acorn: 8.18.0 + picomatch: 4.0.7 + webpack-virtual-modules: 0.6.2 - /vitest@0.33.0(jsdom@22.1.0): - resolution: {integrity: sha512-1CxaugJ50xskkQ0e969R/hW47za4YXDUfWJDxip1hwbnhUjYolpfUn2AMOulqG/Dtd9WYAtkHmM/m3yKVrEejQ==} - engines: {node: '>=v14.18.0'} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@vitest/browser': '*' - '@vitest/ui': '*' - happy-dom: '*' - jsdom: '*' - playwright: '*' - safaridriver: '*' - webdriverio: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - playwright: - optional: true - safaridriver: - optional: true - webdriverio: - optional: true - dependencies: - '@types/chai': 4.3.5 - '@types/chai-subset': 1.3.3 - '@types/node': 18.11.9 - '@vitest/expect': 0.33.0 - '@vitest/runner': 0.33.0 - '@vitest/snapshot': 0.33.0 - '@vitest/spy': 0.33.0 - '@vitest/utils': 0.33.0 - acorn: 8.10.0 - acorn-walk: 8.2.0 - cac: 6.7.14 - chai: 4.3.7 - debug: 4.3.4 - jsdom: 22.1.0 - local-pkg: 0.4.3 - magic-string: 0.30.1 - pathe: 1.1.1 - picocolors: 1.0.0 - std-env: 3.3.3 - strip-literal: 1.0.1 - tinybench: 2.5.0 - tinypool: 0.6.0 - vite: 4.4.4(@types/node@18.11.9) - vite-node: 0.33.0(@types/node@18.11.9) - why-is-node-running: 2.2.2 - transitivePeerDependencies: - - less - - lightningcss - - sass - - stylus - - sugarss - - supports-color - - terser - dev: true + until-async@3.0.2: {} - /vue-template-compiler@2.7.14: - resolution: {integrity: sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==} - dependencies: - de-indent: 1.0.2 - he: 1.2.0 - dev: true + untildify@4.0.0: {} - /vue-tsc@1.8.5(typescript@5.1.6): - resolution: {integrity: sha512-Jr8PTghJIwp69MFsEZoADDcv2l+lXA8juyN/5AYA5zxyZNvIHjSbgKgkYIYc1qnihrOyIG1VOnfk4ZE0jqn8bw==} - hasBin: true - peerDependencies: - typescript: '*' + update-browserslist-db@1.3.3(browserslist@4.28.9): dependencies: - '@vue/language-core': 1.8.5(typescript@5.1.6) - '@vue/typescript': 1.8.5(typescript@5.1.6) - semver: 7.5.4 - typescript: 5.1.6 - dev: true + browserslist: 4.28.9 + escalade: 3.2.0 + picocolors: 1.1.1 - /w3c-xmlserializer@4.0.0: - resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} - engines: {node: '>=14'} + uri-js@4.4.1: dependencies: - xml-name-validator: 4.0.0 - dev: true + punycode: 2.3.1 - /watchpack@2.4.0: - resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} - engines: {node: '>=10.13.0'} - dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - dev: false + util-deprecate@1.0.2: {} - /wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + vite-plugin-dts@5.1.0(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(rolldown@1.2.8)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)): dependencies: - defaults: 1.0.4 - dev: true + unplugin-dts: 1.1.0(@typescript/typescript6@6.0.2)(esbuild@0.28.2)(rolldown@1.2.8)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)) + optionalDependencies: + vite: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13) + transitivePeerDependencies: + - '@rspack/core' + - '@vue/language-core' + - esbuild + - rolldown + - supports-color + - typescript + - webpack - /web-encoding@1.1.5: - resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13): dependencies: - util: 0.12.5 + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.8 + tinyglobby: 0.2.17 optionalDependencies: - '@zxing/text-encoding': 0.9.0 - dev: true + '@types/node': 26.5.1 + esbuild: 0.28.2 + fsevents: 2.3.3 + tsx: 4.23.13 + + vitest@5.0.0(@types/node@26.5.1)(@vitest/coverage-v8@5.0.0)(jsdom@30.0.1)(msw@2.15.0(@types/node@26.5.1)(@typescript/typescript6@6.0.2))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.0(msw@2.15.0(@types/node@26.5.1)(@typescript/typescript6@6.0.2))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13)) + chai: 6.2.2 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.3.1 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.1.4 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + vite: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(tsx@4.23.13) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.5.1 + '@vitest/coverage-v8': 5.0.0(vitest@5.0.0) + jsdom: 30.0.1 + transitivePeerDependencies: + - msw - /web-vitals@3.4.0: - resolution: {integrity: sha512-n9fZ5/bG1oeDkyxLWyep0eahrNcPDF6bFqoyispt7xkW0xhDzpUBTgyDKqWDi1twT0MgH4HvvqzpUyh0ZxZV4A==} - dev: false + vscode-uri@3.2.0: {} - /webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - dev: true + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 - /webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} - dev: true + web-vitals@6.2.1: {} - /whatwg-encoding@2.0.0: - resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} - engines: {node: '>=12'} - dependencies: - iconv-lite: 0.6.3 - dev: true + webidl-conversions@8.0.1: {} - /whatwg-mimetype@3.0.0: - resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} - engines: {node: '>=12'} - dev: true + webpack-virtual-modules@0.6.2: {} - /whatwg-url@12.0.1: - resolution: {integrity: sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==} - engines: {node: '>=14'} - dependencies: - tr46: 4.1.1 - webidl-conversions: 7.0.0 - dev: true + whatwg-mimetype@5.0.0: {} - /whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@16.0.1: dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - dev: true + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' - /which-boxed-primitive@1.0.2: - resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + whatwg-url@17.1.1: dependencies: - is-bigint: 1.0.4 - is-boolean-object: 1.1.2 - is-number-object: 1.0.7 - is-string: 1.0.7 - is-symbol: 1.0.4 + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.2.0 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 - /which-collection@1.0.1: - resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==} + which-collection@1.0.2: dependencies: - is-map: 2.0.2 - is-set: 2.0.2 - is-weakmap: 2.0.1 - is-weakset: 2.0.2 + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 - /which-typed-array@1.1.10: - resolution: {integrity: sha512-uxoA5vLUfRPdjCuJ1h5LlYdmTLbYfums398v3WLkM+i/Wltl2/XyZpQWKbN++ck5L64SR/grOHqtXCUKmlZPNA==} - engines: {node: '>= 0.4'} + which-typed-array@1.1.22: dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.0 - is-typed-array: 1.1.10 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true + which@2.0.2: dependencies: isexe: 2.0.0 - dev: true - /why-is-node-running@2.2.2: - resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} - engines: {node: '>=8'} - hasBin: true + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 - dev: true - /wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true - - /wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 - dev: true - - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true - /ws@8.13.0: - resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} - 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 - dev: true + wrappy@1.0.2: {} - /xml-name-validator@4.0.0: - resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} - engines: {node: '>=12'} - dev: true - - /xmlchars@2.2.0: - resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - dev: true + xml-name-validator@5.0.0: {} - /xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - dev: true + xmlchars@2.2.0: {} - /y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - dev: true + xtend@4.0.2: {} - /yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - dev: true + y18n@5.0.8: {} - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true + yallist@3.1.1: {} - /yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - dev: true + yargs-parser@20.2.9: {} - /yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - dev: true + yargs-parser@21.1.1: {} - /yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} + yargs@16.2.2: dependencies: cliui: 7.0.4 - escalade: 3.1.1 + escalade: 3.2.0 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 20.2.9 - dev: true - /yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} + yargs@17.7.3: dependencies: cliui: 8.0.1 - escalade: 3.1.1 + escalade: 3.2.0 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 - dev: true - - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: true - /yocto-queue@1.0.0: - resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} - engines: {node: '>=12.20'} - dev: true + yocto-queue@0.1.0: {} - /z-schema@5.0.5: - resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} - engines: {node: '>=8.0.0'} - hasBin: true + zod-validation-error@4.0.2(zod@4.6.5): dependencies: - lodash.get: 4.4.2 - lodash.isequal: 4.5.0 - validator: 13.9.0 - optionalDependencies: - commander: 9.5.0 - dev: true + zod: 4.6.5 - /zod@3.21.4: - resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} - dev: false + zod@4.6.5: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ed54ecd09..9c1c025b1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,7 @@ packages: - 'packages/*' - - 'examples/*' \ No newline at end of file + - 'examples/*' +allowBuilds: + esbuild: true + msw: true + sharp: true diff --git a/readme.md b/readme.md deleted file mode 100644 index fc9af3e02..000000000 --- a/readme.md +++ /dev/null @@ -1,342 +0,0 @@ -# @axa-fr/react-oidc - -[![Continuous Integration](https://github.com/AxaGuilDEv/react-oidc/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/AxaGuilDEv/react-oidc/actions/workflows/npm-publish.yml) -[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=AxaGuilDEv_react-oidc&metric=alert_status)](https://sonarcloud.io/dashboard?id=AxaGuilDEv_react-oidc) [![Reliability](https://sonarcloud.io/api/project_badges/measure?project=AxaGuilDEv_react-oidc&metric=reliability_rating)](https://sonarcloud.io/component_measures?id=AxaGuilDEv_react-oidc&metric=reliability_rating) [![Security](https://sonarcloud.io/api/project_badges/measure?project=AxaGuilDEv_react-oidc&metric=security_rating)](https://sonarcloud.io/component_measures?id=AxaGuilDEv_react-oidc&metric=security_rating) [![Code Coverage](https://sonarcloud.io/api/project_badges/measure?project=AxaGuilDEv_react-oidc&metric=coverage)](https://sonarcloud.io/component_measures?id=AxaGuilDEv_react-oidc&metric=Coverage) [![Twitter](https://img.shields.io/twitter/follow/GuildDEvOpen?style=social)](https://twitter.com/intent/follow?screen_name=GuildDEvOpen) - -- Try the demo react at https://black-rock-0dc6b0d03.1.azurestaticapps.net/ -- Try the demo vanilla at https://icy-glacier-004ab4303.2.azurestaticapps.net/ - -

- Sample React Oicd -

- -A set of react components to make Oidc (OpenID Connect) client easy. It aim to simplify OAuth authentication between multiples providers. It is compatible with NextJS. - -- [About](#about) -- [Getting Started](#getting-started) -- [Run The Demo](#run-the-demo) -- [Run The NextJS Demo](#run-the-nextjs-demo) -- [How It Works](#how-it-works) -- Packages - - [`@axa-fr/react-oidc`](./packages/react#readme.md) [![npm version](https://badge.fury.io/js/%40axa-fr%2Freact-oidc.svg)](https://badge.fury.io/js/%40axa-fr%2Freact-oidc) - - [`@axa-fr/vanilla-oidc`](./packages/vanilla#readme.md) [![npm version](https://badge.fury.io/js/%40axa-fr%2Fvanilla-oidc.svg)](https://badge.fury.io/js/%40axa-fr%2Fvanilla-oidc) - - [`@axa-fr/react-oidc-context`](./packages/context#readme.md) [![npm version](https://badge.fury.io/js/%40axa-fr%2Freact-oidc-context.svg)](https://badge.fury.io/js/%40axa-fr%2Freact-oidc-context) **Will be deprecated in v6: has been renamed to @axa-fr/react-oidc** - - [`@axa-fr/react-oidc-context-fetch`](./packages/context-fetch#readme.md) [![npm version](https://badge.fury.io/js/%40axa-fr%2Freact-oidc-context-fetch.svg)](https://badge.fury.io/js/%40axa-fr%2Freact-oidc-context-fetch) **Deprecated in v4** - - [`@axa-fr/react-oidc-redux`](./packages/redux#readme.md) [![npm version](https://badge.fury.io/js/%40axa-fr%2Freact-oidc-redux.svg)](https://badge.fury.io/js/%40axa-fr%2Freact-oidc-redux) **Deprecated in v4 : use react-oidc-context which works with redux and in fact does not use any react context** - - [`@axa-fr/react-oidc-redux-fetch`](./packages/redux-fetch#readme.md) [![npm version](https://badge.fury.io/js/%40axa-fr%2Freact-oidc-redux-fetch.svg)](https://badge.fury.io/js/%40axa-fr%2Freact-oidc-redux-fetch) **Deprecated in v4** - - [`@axa-fr/react-oidc-fetch-observable`](./packages/fetch-observable#readme.md) [![npm version](https://badge.fury.io/js/%40axa-fr%2Freact-oidc-fetch-observable.svg)](https://badge.fury.io/js/%40axa-fr%2Freact-oidc-fetch-observable) **Deprecated in v4** -- [Migrations](#migrations) -- [Contribute](#contribute) - -## About - -These libraries is used to manage client authentication. - -- **Secure** : - - With the use of Service Worker, your tokens (refresh_token and access_token) are not accessible to the JavaScript client code (big protection against XSRF attacks) - - OIDC using client side Code Credential Grant with pkce only -- **Lightweight** -- **Simple** - - refresh_token and access_token are auto refreshed in background - - with the use of the Service Worker, you do not need to inject the access_token in every fetch, you have only to configure OidcTrustedDomains.js file -- **No cookies problem** : You can disable silent signin (that internally use an iFrame). For your information, your OIDC server should be in the same domain of your website in order to be able to send OIDC server cookies from your website via an internal IFRAME, else, you may encounter COOKIES problem. -- **Multiple Authentication** : - - You can authenticate many times to the same provider with different scope (for example you can acquire a new 'payment' scope for a payment) - - You can authenticate to multiple different providers inside the same SPA (single page application) website -- **Flexible** : - - Work with Service Worker (more secure) and without for older browser (less secure) - -

- Schema Authorization Code Grant with pcke flow on the using service worker -
- The service worker catch access_token and refresh_token that will never be accessible to the client. -

- -Works perfectly well with: - -- [Auth0](https://auth0.com/) -- [Duende Identity Server](https://duendesoftware.com/) -- Google -- AWS -- [Keycloak](https://www.keycloak.org/) -- all OIDC compatible providers - -

- @axa-fr/react-oidc is one of the securest way to Authenticate. -
- @axa-fr/react-oidc is one of the securest way to Authenticate. -

- -

- Service Worker lifetime drawback. -
- Service Worker lifetime drawback. -

- -

- Silent-Signing constraints. -
- Silent-Signing constraints. -

- -

- @axa-fr/react-oidc is the simplest and cheapest. -
- @axa-fr/react-oidc is the simplest and cheapest. -

- -## Getting Started - -### Getting Started React using create-react-app - -```sh -npm install @axa-fr/react-oidc --save - -# If you have a "public" folder, the 2 files will be created : -# ./public/OidcServiceWorker.js <-- will be updated at each "npm install" -# ./public/OidcTrustedDomains.js <-- won't be updated if already exist -``` - -If you require a very secure mode where `refresh_token` and `access_token` will be hidden behind a service worker that will proxify requests. -The only file you should edit is `OidcTrustedDomains.js`. - -```javascript -import React from 'react'; -import { render } from 'react-dom'; -import { BrowserRouter as Router } from 'react-router-dom'; -import { OidcProvider } from '@axa-fr/react-oidc'; -import Header from './Layout/Header'; -import Routes from './Router'; - -// This configuration use the ServiceWorker mode only -// "access_token" will be provided automaticaly to the urls and domains configured inside "OidcTrustedDomains.js" -const configuration = { - client_id: 'interactive.public.short', - redirect_uri: window.location.origin + '/authentication/callback', - silent_redirect_uri: window.location.origin + '/authentication/silent-callback', // Optional activate silent-signin that use cookies between OIDC server and client javascript to restore the session - scope: 'openid profile email api offline_access', - authority: 'https://demo.duendesoftware.com', - service_worker_relative_url:'/OidcServiceWorker.js', - service_worker_only:true, -}; - -const App = () => ( - - -
- - - -); - -render(, document.getElementById('root')); -``` - -#### Trusted Domains - -Any domain that has a match in the `OidcTrustedDomains.js` will have the access token automatically injected on requests. - -The `OidcTrustedDomains.js` format is key (`default` being the default key) that has an array of string URLs or RegExp to match trusted domain URLs. - -> **Warning** -> You could use a wildcard for the value, by either providing the wildcard string value `'*'` or a RegExp such as `RegExp('^http.*')` , but you are reducing the security of your application by doing so and it is strongly discouraged. - -```javascript -// OidcTrustedDomains.js - -// Add below trusted domains, access tokens will automatically injected to be send to -// trusted domain can also be a path like https://www.myapi.com/users, -// then all subroute like https://www.myapi.com/useers/1 will be authorized to send access_token to. - -// Domains used by OIDC server must be also declared here -const trustedDomains = { - default:[ - "https://demo.duendesoftware.com", - "https://www.myapi.com/users", - new RegExp('^(https://[a-zA-Z0-9-]+.domain.com/api/)')] -}; - -// Service worker will continue to give access token to the JavaScript client -// Ideal to hide refresh token from client JavaScript, but to retrieve access_token for some -// scenarios which require it. For example, to send it via websocket connection. -trustedDomains.config_show_access_token = { domains : ["https://demo.duendesoftware.com"], showAccessToken: true }; - -// This example defines domains used by OIDC server separately from domains to which access tokens will be injected. -trustedDomains.config_separate_oidc_access_token_domains = { - oidcDomains: ["https://demo.duendesoftware.com"], - accessTokenDomains: ["https://myapi"] -}; - -``` - -#### How to consume - -`useOidc` returns all props from the Hook : - -```javascript -import React from 'react'; -import {useOidc} from "./oidc"; - -export const Home = () => { - - const { login, logout, isAuthenticated} = useOidc(); - - return ( -
-
-
-
Welcome!
-

React Demo Application protected by OpenID Connect

- {!isAuthenticated && - } - {isAuthenticated && - } -
-
-
- ) -}; - -``` - -The Hook method exposes : - -- isAuthenticated : if the user is logged in or not -- logout: logout function (return a promise) -- login: login function 'return a promise' - -"OidcSecure" component trigger authentication in case user is not authenticated. So, the children of that component can be accessible only once you are connected. - -```javascript -import React from 'react'; -import { OidcSecure } from '@axa-fr/react-oidc'; - -const AdminSecure = () => ( - -

My sub component

-
-); - -export default AdminSecure; -``` - -How to get IDToken - -```javascript -import { useOidcIdToken } from '@axa-fr/react-oidc'; - -const DisplayIdToken =() => { - const{ idToken, idTokenPayload } = useOidcIdToken(); - - if(!idToken){ - return

you are not authentified

- } - - return ( -
-
-
ID Token
- {

{JSON.stringify(idToken)}

} - {idTokenPayload != null && -

{JSON.stringify(idTokenPayload)}

} -
-
- ); -} -``` - -#### How to get User Information - -```javascript -import {useOidcUser} from '@axa-fr/react-oidc'; - -const DisplayUserInfo = () => { - const {oidcUser, oidcUserLoadingState} = useOidcUser(); - - switch (oidcUserLoadingState) { - case OidcUserStatus.Loading: - return

User Information are loading

; - case OidcUserStatus.Unauthenticated: - return

you are not authenticated

; - case OidcUserStatus.LoadingError: - return

Fail to load user information

; - default: - return ( -
-
-
User information
-

{JSON.stringify(oidcUser)}

-
-
- ); - } -}; -``` - -More documentation : - -- [`@axa-fr/react-oidc`](./packages/react#readme) - -### Getting Started Vanilla - -More documentation : - -- [`@axa-fr/vanilla-oidc`](./packages/vanilla#readme) - -## Run The Demo - -```sh -git clone https://github.com/AxaGuilDEv/react-oidc.git -cd react-oidc/packages/react -pnpm install -pnpm start -# then navigate to http://localhost:4200 -``` - -## Run The NextJS Demo - -```sh -git clone https://github.com/AxaGuilDEv/react-oidc.git -cd react-oidc/packages/nextjs-demo -pnpm install -pnpm run dev -# then navigate to http://localhost:3001 -``` - -## How It Works - -These components encapsulate the use of "`@axa-fr/vanilla-oidc`" in order to hide workflow complexity. -Internally, native History API is used to be router library agnostic. - -More information about OIDC - -- [French : Augmentez la sécurité et la simplicité de votre Système d’Information OpenID Connect](https://medium.com/just-tech-it-now/augmentez-la-s%C3%A9curit%C3%A9-et-la-simplicit%C3%A9-de-votre-syst%C3%A8me-dinformation-avec-oauth-2-0-cf0732d71284) -- [English : Increase the security and simplicity of your information system with OpenID Connect](https://medium.com/just-tech-it-now/increase-the-security-and-simplicity-of-your-information-system-with-openid-connect-fa8c26b99d6d) - -## Migrations - -v4 is a complete rewrite. It uses the libraries ["App-AuthJS"](https://github.com/openid/AppAuth-JS) instead of oidc-client. -In v4 we have chosen to remove a lot of the surface API in order to simplify usage and enforce security. -In this version you can use a ServiceWorker that will hide the `refresh_token` and `access_token` (more secure). - -- Migrating from v3 to v4 [`guide`](./MIGRATION_GUIDE_V3_TO_V4.md) -- Migrating from v3 to v5 [`guide`](./MIGRATION_GUIDE_V3_TO_V5.md) -- Migrating from v4 to v5 [`guide`](./MIGRATION_GUIDE_V4_TO_V5.md) -- Migrating from v5 to v6 [`guide`](./MIGRATION_GUIDE_V5_TO_V6.md) - -## Contribute - -- [How to run the solution and to contribute](./CONTRIBUTING.md) -- [Please respect our code of conduct](./CODE_OF_CONDUCT.md) diff --git a/scripts/publish-changelog.js b/scripts/publish-changelog.js deleted file mode 100644 index bee28d60e..000000000 --- a/scripts/publish-changelog.js +++ /dev/null @@ -1,16 +0,0 @@ -const fs = require('fs-extra'); -const VERSION = require('../packages/react/package.json').version; - -try { - const execSync = require('child_process').execSync; - child = execSync( - `npm run changelog && git add . && git commit -m "docs(changelog) update to new ${VERSION}" && git push` - ); - console.log('error', child.error); - console.log('stdout ', child.stdout); - console.log('stderr ', child.stderr); - - console.log('success!'); -} catch (err) { - console.error(err); -} diff --git a/scripts/twitter.js b/scripts/twitter.js deleted file mode 100644 index d53a261c6..000000000 --- a/scripts/twitter.js +++ /dev/null @@ -1,30 +0,0 @@ -const Twit = require("twit"); // eslint-disable-line - -/* should be ran with "node twitter consumer_key consumer_secret access_token access_token_secret" - in this order -*/ -const argv = process.argv; - -const T = new Twit({ - consumer_key: argv[2], - consumer_secret: argv[3], - access_token: argv[4], - access_token_secret: argv[5], - timeout_ms: 60 * 1000, - strictSSL: true -}); - -const json = require("../packages/context/package.json"); - -const message = `Hey a new version (${ - json.version -}) of the @axa-fr/react-oidc is available on @github and @npm! -check out the new changelog https://github.com/AxaGuilDEv/react-oidc/blob/master/CHANGELOG.md`; - -T.post("statuses/update", { status: message }, err => { - if (!err) { - console.log("Greate job"); // eslint-disable-line - } else { - console.error(err); - } -}); diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 000000000..34e9295c3 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "strict": true, + "allowJs": true, + "noEmit": true, + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true + } +} diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 000000000..ffcbb9477 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig.base.json" +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..7704e13bc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,5 @@ +// Yes this file is intentionally empty! +// --- +// Having a blank `tsconfig.json` file prevents TypeScript from crawling up your directory tree +// and possibly picking up a parent `tsconfig.json` (which, unsurprisingly, is very hard to debug) +{}