[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: awinogrodzki"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Main\n\non:\n  pull_request:\n    paths:\n      - \"**\"\n      - \"!*.md\"\n      - \"!**/*.md\"\n      - \"!.gitignore\"\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\nenv:\n  FIREBASE_API_KEY: AIzaSyAXYgJha6lO_L4qfWpnhf3KijeKYDhuFzQ\n  FIREBASE_PROJECT_ID: next-firebase-auth-edge-demo\n  FIREBASE_ADMIN_CLIENT_EMAIL: ${{ secrets.FIREBASE_ADMIN_CLIENT_EMAIL }}\n  FIREBASE_ADMIN_PRIVATE_KEY: ${{ secrets.FIREBASE_ADMIN_PRIVATE_KEY }}\n  FIREBASE_AUTH_TENANT_ID: ${{ secrets.FIREBASE_AUTH_TENANT_ID }}\n  FIREBASE_APP_CHECK_KEY: ${{ secrets.FIREBASE_APP_CHECK_KEY }}\n  FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }}\njobs:\n  install:\n    name: Install dependencies\n    timeout-minutes: 10\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Use Node.js 22\n        uses: actions/setup-node@v4\n        with:\n          cache: \"yarn\"\n          cache-dependency-path: yarn.lock\n          node-version: 22\n      - name: Cache node_modules\n        uses: actions/cache@v4\n        with:\n          path: |\n            node_modules\n          key: ${{ runner.os }}-node_modules-cache-v2-${{ hashFiles('./yarn.lock') }}-${{github.sha}}\n          restore-keys: |\n            ${{ runner.os }}-node_modules-cache-v2-${{ hashFiles('./yarn.lock') }}-\n      - run: yarn install\n\n  build:\n    name: Build packages\n    timeout-minutes: 15\n    needs: install\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Use Node.js 22\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22\n      - name: Cache node_modules\n        uses: actions/cache@v4\n        with:\n          path: |\n            node_modules\n          key: ${{ runner.os }}-node_modules-cache-v2-${{ hashFiles('./yarn.lock') }}-${{github.sha}}\n      - name: Cache build\n        uses: actions/cache@v4\n        with:\n          path: |\n            lib\n          key: ${{ runner.os }}-build-cache-v2-${{ github.ref_name }}-${{github.sha}}\n          restore-keys: |\n            ${{ runner.os }}-build-cache-v2-${{ github.ref_name }}-\n      - run: yarn build\n\n  lint:\n    name: Lint packages\n    timeout-minutes: 10\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Use Node.js 22\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22\n      - name: Cache node_modules\n        uses: actions/cache@v4\n        with:\n          path: |\n            node_modules\n          key: ${{ runner.os }}-node_modules-cache-v2-${{ hashFiles('./yarn.lock') }}-${{github.sha}}\n      - run: yarn lint\n\n  tests:\n    name: Tests\n    timeout-minutes: 15\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - uses: actions/checkout@v4\n      - name: Use Node.js 22\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22\n      - name: Cache node_modules\n        uses: actions/cache@v4\n        with:\n          path: |\n            node_modules\n          key: ${{ runner.os }}-node_modules-cache-v2-${{ hashFiles('./yarn.lock') }}-${{github.sha}}\n      - name: Cache build\n        uses: actions/cache@v4\n        with:\n          path: |\n            lib\n          key: ${{ runner.os }}-build-cache-v2-${{ github.ref_name }}-${{github.sha}}\n      - run: yarn test\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\nenv:\n  FIREBASE_API_KEY: AIzaSyAXYgJha6lO_L4qfWpnhf3KijeKYDhuFzQ\n  FIREBASE_PROJECT_ID: next-firebase-auth-edge-demo\n  FIREBASE_ADMIN_CLIENT_EMAIL: ${{ secrets.FIREBASE_ADMIN_CLIENT_EMAIL }}\n  FIREBASE_ADMIN_PRIVATE_KEY: ${{ secrets.FIREBASE_ADMIN_PRIVATE_KEY }}\n  FIREBASE_AUTH_TENANT_ID: ${{ secrets.FIREBASE_AUTH_TENANT_ID }}\n  FIREBASE_APP_CHECK_KEY: ${{ secrets.FIREBASE_APP_CHECK_KEY }}\n  FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }}\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  push:\n    branches: [main, canary]\n    paths:\n      - \"**\"\n      - \"!*.md\"\n      - \"!**/*.md\"\n      - \"!.gitignore\"\n\njobs:\n  install:\n    name: Install dependencies\n    timeout-minutes: 10\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Use Node.js 22\n        uses: actions/setup-node@v4\n        with:\n          cache: \"yarn\"\n          cache-dependency-path: yarn.lock\n          node-version: 22\n      - name: Cache node_modules\n        uses: actions/cache@v4\n        with:\n          path: |\n            node_modules\n          key: ${{ runner.os }}-node_modules-cache-v2-${{ hashFiles('./yarn.lock') }}-${{github.sha}}\n          restore-keys: |\n            ${{ runner.os }}-node_modules-cache-v2-${{ hashFiles('./yarn.lock') }}-\n      - run: yarn install\n\n  build:\n    name: Build packages\n    timeout-minutes: 15\n    needs: install\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Use Node.js 22\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22\n      - name: Cache node_modules\n        uses: actions/cache@v4\n        with:\n          path: |\n            node_modules\n          key: ${{ runner.os }}-node_modules-cache-v2-${{ hashFiles('./yarn.lock') }}-${{github.sha}}\n      - name: Cache build\n        uses: actions/cache@v4\n        with:\n          path: |\n            lib\n            browser\n            esm\n          key: ${{ runner.os }}-build-cache-v2-${{ github.ref_name }}-${{github.sha}}\n          restore-keys: |\n            ${{ runner.os }}-build-cache-v2-${{ github.ref_name }}-\n      - run: yarn build\n\n  semantic-release:\n    if: \"!contains(github.event.head_commit.message, '[skip ci]')\"\n\n    name: Semantic Release\n    runs-on: ubuntu-latest\n    needs:\n      - build\n    permissions:\n      contents: write\n      id-token: write\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Use Node.js 22\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22\n      - name: Cache node_modules\n        uses: actions/cache@v4\n        with:\n          path: |\n            node_modules\n          key: ${{ runner.os }}-node_modules-cache-v2-${{ hashFiles('./yarn.lock') }}-${{github.sha}}\n      - name: Cache build\n        uses: actions/cache@v4\n        with:\n          path: |\n            lib\n            browser\n            esm\n          key: ${{ runner.os }}-build-cache-v2-${{ github.ref_name }}-${{github.sha}}\n          restore-keys: |\n            ${{ runner.os }}-build-cache-v2-${{ github.ref_name }}-\n\n      - name: Release\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: npx semantic-release\n\n\n\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n.vscode\nyarn-error.log\n.yarn\n.next\n.env\n\n\ntmp\npackage-lock.json\ncoverage\nwebpack-stats.json\n.turbo\n.DS_Store\n.idea\n*.log\ndist\n.fleet\n.history\n\n#dist\nlib\nbrowser\nesm\ncjs\n"
  },
  {
    "path": ".husky/commit-msg",
    "content": "npx --no -- commitlint --edit \"${1}\"\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "yarn test\nyarn lint --fix\nyarn check-circular-imports\ngit add --all\n"
  },
  {
    "path": ".npmignore",
    "content": ".env\n.env.dist\nexamples\nsrc\n!lib\n!browser\n!esm"
  },
  {
    "path": ".releaserc.yaml",
    "content": "plugins:\n  - \"@semantic-release/commit-analyzer\"\n  - \"@semantic-release/release-notes-generator\"\n  - \"@semantic-release/changelog\"\n  - \"@semantic-release/npm\"\n  - \"@semantic-release/git\"\n  - - \"@semantic-release/github\"\n    - successComment: false\n      failTitle: false\n\nbranches:\n  - main\n  - name: canary\n    prerelease: true\n"
  },
  {
    "path": ".swcrc",
    "content": "{\n  \"jsc\": {\n     \"parser\": {\n        \"syntax\": \"typescript\",\n        \"decorators\": true\n     },\n     \"transform\": {\n        \"legacyDecorator\": true,\n        \"decoratorMetadata\": true\n     }\n  }\n}"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# [1.12.0](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.11.5...v1.12.0) (2026-02-26)\n\n\n### Bug Fixes\n\n* **release:** update canary release after rebase ([f80caf1](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/f80caf15dde88da8164bcef3be643a60c4380065))\n\n\n### Features\n\n* **next:** support Next.js 16 ([7f69f89](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/7f69f893223fdd2297563d408601999890b079b6))\n\n# [1.12.0-canary.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.11.5...v1.12.0-canary.1) (2026-02-26)\n\n\n### Bug Fixes\n\n* **release:** update canary release after rebase ([f80caf1](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/f80caf15dde88da8164bcef3be643a60c4380065))\n\n\n### Features\n\n* **next:** support Next.js 16 ([7f69f89](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/7f69f893223fdd2297563d408601999890b079b6))\n\n## [1.11.5](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.11.4...v1.11.5) (2026-02-16)\n\n\n### Bug Fixes\n\n* update node version to 22 in github workflows ([ce45f99](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/ce45f99daa167e1871ba3794937ad9f10aafa850))\n\n## [1.11.4](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.11.3...v1.11.4) (2026-02-16)\n\n\n### Bug Fixes\n\n* another attempt to fix semantic release ([1397c7b](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/1397c7b880a0bc158fcbd79fb034f63c9ac2c1dd))\n\n## [1.11.3](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.11.2...v1.11.3) (2026-02-16)\n\n\n### Bug Fixes\n\n* insignificant change to trigger and test release flow ([d90764d](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d90764d956cf7c0d8a727afaefb27610cae76859))\n* trigger release to test semantic release integration ([e5dd2f0](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/e5dd2f0909920262d53b0ae27765fbe1f336b7ff))\n\n## [1.11.2](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.11.1...v1.11.2) (2026-02-16)\n\n\n### Bug Fixes\n\n* upgrade semantic release version to support provenance mode ([a37975b](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/a37975b403791671f10a96d8508fa3d72b7ef903))\n* use provenance with semantic release ([d2031e5](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d2031e5875606a8c73201174cd52af0546fe612e))\n\n## [1.11.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.11.0...v1.11.1) (2025-09-14)\n\n\n### Bug Fixes\n\n* remove cookie verification warning ([910808e](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/910808ea03dcf0da986218ae56182bc1f265d17d))\n\n# [1.11.0](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.10.1...v1.11.0) (2025-08-21)\n\n\n### Bug Fixes\n\n* **refresh-token:** respond with 401: Unauthorized when verify fails with InvalidTokenError ([cf84cec](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/cf84cecf77ae14bbadf5c1441abe74a2e0c65b58))\n\n\n### Features\n\n* **enableTokenRefreshOnExpiredKidHeader:** token refresh on expired kid is no longer experimental ([6800605](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/6800605360aae0d45680c8252ad432f453d1c4f5))\n* **refresh-token:** handle 401: Unauthorized in getValidIdToken and getValidCustomToken ([d96d89f](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d96d89f3be6edbcd0d10174017e62aea886fbd33))\n\n## [1.10.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.10.0...v1.10.1) (2025-08-18)\n\n\n### Bug Fixes\n\n* pass dynamicCustomClaimsKeys in refreshCookiesWithIdToken ([cbae23f](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/cbae23f461b84fc1642805f1602bb81f99b4dc63))\n\n# [1.10.0](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.9.1...v1.10.0) (2025-08-11)\n\n\n### Bug Fixes\n\n* allow to use `/` as private path ([00eaeca](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/00eaeca9b2ea73a736fab5d2ba60f9f22a764f2b))\n* encode redirect param ([2afeef3](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/2afeef38e0fab0214b0c06da2b258871c909414f))\n\n\n### Features\n\n* **middleware:** redirectToLogin supports privatePaths ([1a5450c](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/1a5450cade9266275c036b23752b66f0327d4669))\n* exposing token-verifier for public use ([d4a3796](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d4a379692f41376608ecf4b865ed249f07daffd1))\n* update firebase, firebase-admin and react dependencies ([41b94f7](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/41b94f7b16a9012b3707bf6942dee75eea1ea5bc))\n* **metadata:** added metadata support ([9dda6dc](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/9dda6dc8ea88291b286cb6a6be47899f0e647d90))\n* **metadata:** clear metadata cookies after logout ([d148629](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d148629daa226314fcd994ffafe094fc226b890a))\n* **metadata:** improved getTokens warning readability ([6945e55](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/6945e55f4b6157d5d46799a07d0bb437954b594a))\n* **metadata:** integrate metadata with starter example ([5ed8cf3](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/5ed8cf3880bf15fd153dcd3c00cbe44faeeb7145))\n* **metadata:** update types with getMetadata method ([5d9dbaa](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/5d9dbaa4f692dc76b667197e3f20ed7a83d10310))\n\n# [1.10.0-canary.7](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.10.0-canary.6...v1.10.0-canary.7) (2025-08-11)\n\n\n### Bug Fixes\n\n* allow to use `/` as private path ([00eaeca](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/00eaeca9b2ea73a736fab5d2ba60f9f22a764f2b))\n\n# [1.10.0-canary.6](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.10.0-canary.5...v1.10.0-canary.6) (2025-08-11)\n\n\n### Features\n\n* **middleware:** redirectToLogin supports privatePaths ([1a5450c](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/1a5450cade9266275c036b23752b66f0327d4669))\n\n# [1.10.0-canary.5](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.10.0-canary.4...v1.10.0-canary.5) (2025-08-11)\n\n\n### Features\n\n* update firebase, firebase-admin and react dependencies ([41b94f7](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/41b94f7b16a9012b3707bf6942dee75eea1ea5bc))\n\n# [1.10.0-canary.4](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.10.0-canary.3...v1.10.0-canary.4) (2025-05-29)\n\n\n### Bug Fixes\n\n* encode redirect param ([2afeef3](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/2afeef38e0fab0214b0c06da2b258871c909414f))\n\n# [1.10.0-canary.3](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.10.0-canary.2...v1.10.0-canary.3) (2025-04-08)\n\n\n### Features\n\n* exposing token-verifier for public use ([d4a3796](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d4a379692f41376608ecf4b865ed249f07daffd1))\n\n# [1.10.0-canary.2](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.10.0-canary.1...v1.10.0-canary.2) (2025-03-11)\n\n\n### Features\n\n* **metadata:** clear metadata cookies after logout ([d148629](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d148629daa226314fcd994ffafe094fc226b890a))\n\n# [1.10.0-canary.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.9.1...v1.10.0-canary.1) (2025-03-11)\n\n\n### Features\n\n* **metadata:** added metadata support ([9dda6dc](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/9dda6dc8ea88291b286cb6a6be47899f0e647d90))\n* **metadata:** improved getTokens warning readability ([6945e55](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/6945e55f4b6157d5d46799a07d0bb437954b594a))\n* **metadata:** integrate metadata with starter example ([5ed8cf3](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/5ed8cf3880bf15fd153dcd3c00cbe44faeeb7145))\n* **metadata:** update types with getMetadata method ([5d9dbaa](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/5d9dbaa4f692dc76b667197e3f20ed7a83d10310))\n\n## [1.9.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.9.0...v1.9.1) (2025-02-18)\n\n\n### Bug Fixes\n\n* **dynamic-custom-claims:** allow to update claims after token refresh ([475e767](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/475e76709ece48294746b334522c0c16dbc5ce6d))\n\n# [1.9.0](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.2...v1.9.0) (2025-02-18)\n\n\n### Bug Fixes\n\n* **#297:** propagate custom claims when exchanging id token for custom, id and refresh tokens ([55254b8](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/55254b87196c9fb7f16c36785131b34edd3b219e)), closes [#297](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/297)\n* **#303:** support npm 11 ([88328e5](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/88328e51abfebf2eef63895b37d91784a0e982ce)), closes [#303](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/303)\n* **#306:** support Node.js 23 ([f27d210](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/f27d21023b8f0120fd7ddfd17e9c9d42b2a28f31)), closes [#306](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/306)\n* return cached token or server token ([c1a04a9](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/c1a04a96f12aea574aa5c44b2d41a053bf746f6c))\n* return cached valid token ([a73f9ec](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/a73f9ecd04860c72613664c3ab857fe4efd46954))\n\n\n### Features\n\n* **#300:** added removeServerCookies method to logout from Server Actions ([cab2d23](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/cab2d238012ed7fb20cdbb09da7e69eab3867c14)), closes [#300](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/300)\n* full firebase emulator support ([9dcf5e9](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/9dcf5e94be548b0f3bb0277bee6abce43592a7d2))\n\n# [1.9.0-canary.6](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.9.0-canary.5...v1.9.0-canary.6) (2025-01-28)\n\n\n### Bug Fixes\n\n* **#306:** support Node.js 23 ([f27d210](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/f27d21023b8f0120fd7ddfd17e9c9d42b2a28f31)), closes [#306](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/306)\n\n# [1.9.0-canary.5](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.9.0-canary.4...v1.9.0-canary.5) (2025-01-23)\n\n\n### Bug Fixes\n\n* **#303:** support npm 11 ([88328e5](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/88328e51abfebf2eef63895b37d91784a0e982ce)), closes [#303](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/303)\n\n# [1.9.0-canary.4](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.9.0-canary.3...v1.9.0-canary.4) (2025-01-22)\n\n\n### Features\n\n* **#300:** added removeServerCookies method to logout from Server Actions ([cab2d23](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/cab2d238012ed7fb20cdbb09da7e69eab3867c14)), closes [#300](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/300)\n\n# [1.9.0-canary.3](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.9.0-canary.2...v1.9.0-canary.3) (2025-01-21)\n\n\n### Bug Fixes\n\n* **#297:** propagate custom claims when exchanging id token for custom, id and refresh tokens ([55254b8](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/55254b87196c9fb7f16c36785131b34edd3b219e)), closes [#297](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/297)\n\n# [1.9.0-canary.2](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.9.0-canary.1...v1.9.0-canary.2) (2024-12-16)\n\n\n### Bug Fixes\n\n* return cached token or server token ([c1a04a9](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/c1a04a96f12aea574aa5c44b2d41a053bf746f6c))\n* return cached valid token ([a73f9ec](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/a73f9ecd04860c72613664c3ab857fe4efd46954))\n\n# [1.9.0-canary.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.2...v1.9.0-canary.1) (2024-11-15)\n\n\n### Features\n\n* full firebase emulator support ([9dcf5e9](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/9dcf5e94be548b0f3bb0277bee6abce43592a7d2))\n\n## [1.8.2](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.1...v1.8.2) (2024-11-07)\n\n\n### Bug Fixes\n\n* **docs:** added `await` before calling `cookies` and `headers` due to change in Next.js 15 ([d14c9df](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d14c9df55aa3476ea30d56b884599f4e8af9e3ff))\n* add logs to invalid token comparator func ([11eaede](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/11eaedee236b4cf93f5b2542ae10eebbe0c86884))\n* added additional logs around cookie parser ([1550c80](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/1550c80e504e7e49ff44a89dd20c59c2878b6dbc))\n* added additional logs to debug a failed verification in auth middleware ([30ddc5e](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/30ddc5e6f4cb7c7d713cc210d77648d8722d924c))\n* await on parse cookie result to work around [#271](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/271) ([f6b5106](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/f6b51062b4308e32d1ef1d123912d21dc93d3f85))\n* debug Vercel logging by removing inheritance from Error ([46ca356](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/46ca35632f46d92dc4f0229552c09e4f455fee58))\n* export error module explicitly ([575281c](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/575281c45e037eccdf48c833ae605cd373388896))\n* remove console.log and improve debug logs around token fetching ([31dfbd2](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/31dfbd2226dcd775bd4a76cdb4a5c5f04f72954e))\n* remove debug logs from cookie parser ([2ce3190](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/2ce3190cbffa476fd228f6cc1bf578cae6c8591f))\n* remove unnecessary async in get tokens functions ([c0f530c](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/c0f530c3ecfe9c0e120f20d4a9e3bcab264a28db))\n* work around [#271](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/271) in getCookiesTokens ([5fef799](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/5fef799648daa2a0fcf20829dd82b443b9f511e7))\n* **#271:** use runtime flag to identify invalid token error ([d7220b0](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d7220b0e1dd642385d3320efd812b2e08117e51e)), closes [#271](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/271)\n\n## [1.8.2-canary.11](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.2-canary.10...v1.8.2-canary.11) (2024-11-07)\n\n\n### Bug Fixes\n\n* **docs:** added `await` before calling `cookies` and `headers` due to change in Next.js 15 ([d14c9df](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d14c9df55aa3476ea30d56b884599f4e8af9e3ff))\n\n## [1.8.2-canary.10](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.2-canary.9...v1.8.2-canary.10) (2024-11-06)\n\n\n### Bug Fixes\n\n* remove unnecessary async in get tokens functions ([c0f530c](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/c0f530c3ecfe9c0e120f20d4a9e3bcab264a28db))\n* work around [#271](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/271) in getCookiesTokens ([5fef799](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/5fef799648daa2a0fcf20829dd82b443b9f511e7))\n\n## [1.8.2-canary.9](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.2-canary.8...v1.8.2-canary.9) (2024-11-06)\n\n\n### Bug Fixes\n\n* await on parse cookie result to work around [#271](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/271) ([f6b5106](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/f6b51062b4308e32d1ef1d123912d21dc93d3f85))\n\n## [1.8.2-canary.8](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.2-canary.7...v1.8.2-canary.8) (2024-11-06)\n\n\n### Bug Fixes\n\n* remove debug logs from cookie parser ([2ce3190](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/2ce3190cbffa476fd228f6cc1bf578cae6c8591f))\n\n## [1.8.2-canary.7](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.2-canary.6...v1.8.2-canary.7) (2024-11-06)\n\n\n### Bug Fixes\n\n* added additional logs around cookie parser ([1550c80](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/1550c80e504e7e49ff44a89dd20c59c2878b6dbc))\n\n## [1.8.2-canary.6](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.2-canary.5...v1.8.2-canary.6) (2024-11-06)\n\n\n### Bug Fixes\n\n* debug Vercel logging by removing inheritance from Error ([46ca356](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/46ca35632f46d92dc4f0229552c09e4f455fee58))\n\n## [1.8.2-canary.5](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.2-canary.4...v1.8.2-canary.5) (2024-11-06)\n\n\n### Bug Fixes\n\n* remove console.log and improve debug logs around token fetching ([31dfbd2](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/31dfbd2226dcd775bd4a76cdb4a5c5f04f72954e))\n\n## [1.8.2-canary.4](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.2-canary.3...v1.8.2-canary.4) (2024-11-06)\n\n\n### Bug Fixes\n\n* add logs to invalid token comparator func ([11eaede](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/11eaedee236b4cf93f5b2542ae10eebbe0c86884))\n\n## [1.8.2-canary.3](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.2-canary.2...v1.8.2-canary.3) (2024-11-06)\n\n\n### Bug Fixes\n\n* **#271:** use runtime flag to identify invalid token error ([d7220b0](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d7220b0e1dd642385d3320efd812b2e08117e51e)), closes [#271](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/271)\n\n## [1.8.2-canary.2](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.2-canary.1...v1.8.2-canary.2) (2024-11-06)\n\n\n### Bug Fixes\n\n* export error module explicitly ([575281c](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/575281c45e037eccdf48c833ae605cd373388896))\n\n## [1.8.2-canary.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.1...v1.8.2-canary.1) (2024-11-06)\n\n\n### Bug Fixes\n\n* added additional logs to debug a failed verification in auth middleware ([30ddc5e](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/30ddc5e6f4cb7c7d713cc210d77648d8722d924c))\n\n## [1.8.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.0...v1.8.1) (2024-11-05)\n\n\n### Bug Fixes\n\n* update cookie library to avoid vulnerability in cookie < 0.7.0 ([0940e28](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/0940e2875a3f8d0b9769317914df074d70caa741))\n\n# [1.8.0](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.1...v1.8.0) (2024-10-28)\n\n\n### Bug Fixes\n\n* added circular import validation ([deaa2e3](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/deaa2e393b4e7630090d0dce96ed9e7e6b7fa8e0))\n* automated release build cache ([b6abf5a](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/b6abf5aea77efdfb3827b18864fe0d08c8a2f7e6))\n* create request cookies provider from cloned headers ([d17c376](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d17c3762807ccba180116612c0463dec4357b0e9))\n* include missing directories in package.json exports ([668ae8b](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/668ae8bc4dc55b05623de71e97a2faf1eb417928))\n* remove declarations from esm build ([025e4c8](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/025e4c8a501201618df4fb77a6532d22aed9ddaf))\n\n\n### Features\n\n* make custom token optional ([4a18cb7](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/4a18cb7ed6aca3e8e72819437939fae1bc9eeffc))\n* refactor cookies to separate multiple from single type ([9aba786](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/9aba786f04ab20c8e57e1daeacab670d59c770f0))\n* support esm, commonjs and browser build targets ([93a17bd](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/93a17bde917817d7191cfaf5bce4f17a836c454b))\n* validate tenantId when verifying id token ([798d0f1](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/798d0f1037b387a691284b22f6a092dfbfd0d156))\n\n# [1.8.0-canary.9](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.0-canary.8...v1.8.0-canary.9) (2024-10-09)\n\n\n### Features\n\n* make custom token optional ([4a18cb7](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/4a18cb7ed6aca3e8e72819437939fae1bc9eeffc))\n\n# [1.8.0-canary.8](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.0-canary.7...v1.8.0-canary.8) (2024-09-30)\n\n\n### Bug Fixes\n\n* create request cookies provider from cloned headers ([d17c376](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d17c3762807ccba180116612c0463dec4357b0e9))\n\n# [1.8.0-canary.7](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.0-canary.6...v1.8.0-canary.7) (2024-09-30)\n\n\n### Bug Fixes\n\n* added circular import validation ([deaa2e3](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/deaa2e393b4e7630090d0dce96ed9e7e6b7fa8e0))\n\n# [1.8.0-canary.6](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.0-canary.5...v1.8.0-canary.6) (2024-09-29)\n\n\n### Features\n\n* refactor cookies to separate multiple from single type ([9aba786](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/9aba786f04ab20c8e57e1daeacab670d59c770f0))\n\n# [1.8.0-canary.5](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.0-canary.4...v1.8.0-canary.5) (2024-09-22)\n\n\n### Bug Fixes\n\n* include missing directories in package.json exports ([668ae8b](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/668ae8bc4dc55b05623de71e97a2faf1eb417928))\n\n# [1.8.0-canary.4](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.0-canary.3...v1.8.0-canary.4) (2024-09-22)\n\n\n### Bug Fixes\n\n* automated release build cache ([b6abf5a](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/b6abf5aea77efdfb3827b18864fe0d08c8a2f7e6))\n\n# [1.8.0-canary.3](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.0-canary.2...v1.8.0-canary.3) (2024-09-22)\n\n\n### Bug Fixes\n\n* remove declarations from esm build ([025e4c8](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/025e4c8a501201618df4fb77a6532d22aed9ddaf))\n\n# [1.8.0-canary.2](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.8.0-canary.1...v1.8.0-canary.2) (2024-09-22)\n\n\n### Features\n\n* support esm, commonjs and browser build targets ([93a17bd](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/93a17bde917817d7191cfaf5bce4f17a836c454b))\n\n# [1.8.0-canary.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.1...v1.8.0-canary.1) (2024-09-21)\n\n\n### Features\n\n* validate tenantId when verifying id token ([798d0f1](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/798d0f1037b387a691284b22f6a092dfbfd0d156))\n\n## [1.7.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.0...v1.7.1) (2024-09-13)\n\n\n### Bug Fixes\n\n* handle switch from multiple to single cookie ([9b18bd5](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/9b18bd58ed0b765c19727d8aaf8f1f45299623d0))\n\n# [1.7.0](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.6.2...v1.7.0) (2024-09-09)\n\n\n### Bug Fixes\n\n* add debug logs for experimental feature ([41ef1df](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/41ef1dfcf6fe23a7dabfa4e8d3cc5e2c1172b31e))\n* **#242:** use TextEncoder when mapping token to UInt8Array ([23b04dc](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/23b04dcd8867fd7c6b108c41496cb19930e5cc16)), closes [#242](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/242)\n* **#246:** re-throw invalid PKCS8 error as AuthError with user-friendly message ([a7d7a22](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/a7d7a228733e67525b001cff70a523880d858e01)), closes [#246](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/246)\n* **#249:** merge error stack trace in token verifier to improve visibility on fetch errors ([6bce756](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/6bce7564216dff60fe736ef85e8508d2df686eaf)), closes [#249](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/249)\n* add missing name property to decoded id token type ([39b086d](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/39b086db222f619a8b4cf0365895f33c6832e3fc))\n* pass cookie serialization options to cookie setter ([b28ce7a](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/b28ce7a866318f958e58b14e4adfcc85a47e5bef))\n* recreate canary tags after force push ([c9b7c18](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/c9b7c18e5cb4f8a31e5388e0bfd23665e8b5674e))\n* semantic-release rate exceeded error ([676b602](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/676b6021a013c0afdddd75a0cea71b2a8b4786e2))\n* semantic-version git history issue ([d514f57](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d514f5713883e1713f265b07a4670518af646a6b))\n* update next.js peer dependency to rc ([f2953fd](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/f2953fd38bdd6df9b4b535a21abb47793249752b))\n\n\n### Features\n\n* **middleware:** introduced `redirectToPath` method and RegExp support in `redirectToLogin` method ([21024bb](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/21024bb02f6f0300301e7822751e047caef745c0))\n* added `path` option to `redirectToHome` helper function ([54f07f4](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/54f07f4a09fad3e46fc089e5d762afa4df5eb1f5))\n* allow setAuthCookies to accept custom auth headers or fall back ([b1d169b](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/b1d169b13d1c6132799aed23ef1c6da3698ba080))\n* experimental option to refresh token on expired kid header ([2869531](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/28695315164fffee7b3a08879e95033c44b8a197))\n* introduced `refreshCookiesWithIdToken` function to enable login using Server Actions ([#212](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/212)) ([6cd0b13](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/6cd0b138036ff0f4fcfa91d786fca5255cfa2654))\n* next.js 15 rc support ([a994dd0](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/a994dd07bce5420049573b2651b08ecb1a82b63c))\n* pass custom auth header from authMiddleware ([71286af](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/71286afe6c7faebf2cdcd568e507a5e0739720f0))\n* **getTokens:** introduced optional `cookieSerializeOptions` option ([e041542](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/e041542c6b2f4380fcc7f803f7e1c8d5c14bc6e1))\n* replaced no matching kid auth error with invalid token error ([9d2d0fc](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/9d2d0fcb49374d0bb6b260c43d8a2409377b0144))\n* support Node.js 22 ([6c7f435](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/6c7f435485391a4d987f0bc3d0653536d4ef93ff))\n\n# [1.7.0-canary.17](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.0-canary.16...v1.7.0-canary.17) (2024-09-07)\n\n\n### Features\n\n* **middleware:** introduced `redirectToPath` method and RegExp support in `redirectToLogin` method ([21024bb](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/21024bb02f6f0300301e7822751e047caef745c0))\n\n# [1.7.0-canary.16](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.0-canary.15...v1.7.0-canary.16) (2024-09-06)\n\n\n### Features\n\n* allow setAuthCookies to accept custom auth headers or fall back ([b1d169b](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/b1d169b13d1c6132799aed23ef1c6da3698ba080))\n* pass custom auth header from authMiddleware ([71286af](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/71286afe6c7faebf2cdcd568e507a5e0739720f0))\n\n# [1.7.0-canary.15](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.0-canary.14...v1.7.0-canary.15) (2024-09-06)\n\n\n### Bug Fixes\n\n* add debug logs for experimental feature ([41ef1df](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/41ef1dfcf6fe23a7dabfa4e8d3cc5e2c1172b31e))\n\n# [1.7.0-canary.14](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.0-canary.13...v1.7.0-canary.14) (2024-09-06)\n\n\n### Features\n\n* experimental option to refresh token on expired kid header ([2869531](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/28695315164fffee7b3a08879e95033c44b8a197))\n\n# [1.7.0-canary.13](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.0-canary.12...v1.7.0-canary.13) (2024-09-03)\n\n\n### Bug Fixes\n\n* **#249:** merge error stack trace in token verifier to improve visibility on fetch errors ([6bce756](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/6bce7564216dff60fe736ef85e8508d2df686eaf)), closes [#249](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/249)\n\n# [1.7.0-canary.12](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.0-canary.11...v1.7.0-canary.12) (2024-09-03)\n\n\n### Bug Fixes\n\n* **#242:** use TextEncoder when mapping token to UInt8Array ([23b04dc](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/23b04dcd8867fd7c6b108c41496cb19930e5cc16)), closes [#242](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/242)\n\n# [1.7.0-canary.11](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.0-canary.10...v1.7.0-canary.11) (2024-08-30)\n\n\n### Bug Fixes\n\n* **#246:** re-throw invalid PKCS8 error as AuthError with user-friendly message ([a7d7a22](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/a7d7a228733e67525b001cff70a523880d858e01)), closes [#246](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/246)\n\n# [1.7.0-canary.10](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.0-canary.9...v1.7.0-canary.10) (2024-08-22)\n\n\n### Features\n\n* **getTokens:** introduced optional `cookieSerializeOptions` option ([e041542](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/e041542c6b2f4380fcc7f803f7e1c8d5c14bc6e1))\n\n# [1.7.0-canary.9](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.0-canary.8...v1.7.0-canary.9) (2024-08-21)\n\n\n### Bug Fixes\n\n* pass cookie serialization options to cookie setter ([b28ce7a](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/b28ce7a866318f958e58b14e4adfcc85a47e5bef))\n\n# [1.7.0-canary.8](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.0-canary.7...v1.7.0-canary.8) (2024-08-21)\n\n\n### Features\n\n* replaced no matching kid auth error with invalid token error ([9d2d0fc](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/9d2d0fcb49374d0bb6b260c43d8a2409377b0144))\n\n# [1.7.0-canary.7](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.0-canary.6...v1.7.0-canary.7) (2024-08-21)\n\n\n### Features\n\n* support Node.js 22 ([6c7f435](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/6c7f435485391a4d987f0bc3d0653536d4ef93ff))\n\n# [1.7.0-canary.6](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.0-canary.5...v1.7.0-canary.6) (2024-08-10)\n\n\n### Bug Fixes\n\n* semantic-release rate exceeded error ([676b602](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/676b6021a013c0afdddd75a0cea71b2a8b4786e2))\n\n# [1.7.0-canary.5](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.0-canary.4...v1.7.0-canary.5) (2024-08-10)\n\n\n### Bug Fixes\n\n* update next.js peer dependency to rc ([f2953fd](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/f2953fd38bdd6df9b4b535a21abb47793249752b))\n\n# [1.7.0-canary.4](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.0-canary.3...v1.7.0-canary.4) (2024-08-10)\n\n\n### Bug Fixes\n\n* add missing name property to decoded id token type ([39b086d](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/39b086db222f619a8b4cf0365895f33c6832e3fc))\n\n\n### Features\n\n* next.js 15 rc support ([a994dd0](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/a994dd07bce5420049573b2651b08ecb1a82b63c))\n\n# [1.7.0-canary.3](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.7.0-canary.2...v1.7.0-canary.3) (2024-08-08)\n\n\n### Bug Fixes\n\n* recreate canary tags after force push ([c9b7c18](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/c9b7c18e5cb4f8a31e5388e0bfd23665e8b5674e))\n* semantic-version git history issue ([d514f57](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d514f5713883e1713f265b07a4670518af646a6b))\n\n# [1.7.0-canary.2](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.6.2...v1.7.0-canary.2) (2024-07-25)\n\n\n### Features\n\n* added `path` option to `redirectToHome` helper function ([54f07f4](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/54f07f4a09fad3e46fc089e5d762afa4df5eb1f5))\n\n\n# [1.7.0-canary.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.6.1...v1.7.0-canary.1) (2024-07-16)\n\n\n### Features\n\n* introduced `refreshCookiesWithIdToken` function to enable login using Server Actions ([#212](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/212)) ([fd6b193](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/fd6b193d345af85e7cca502640b98e2c93aebadc))\n\n## [1.6.2](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.6.1...v1.6.2) (2024-07-16)\n\n\n### Bug Fixes\n\n* fix `JWSInvalid: Invalid Compact JWS` error when migrating between token formats ([#214](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/214)) ([5b6b0c3](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/5b6b0c3c0eeb62e1f28c7e48c73ad93bee3c0bbc))\n\n## [1.6.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.6.0...v1.6.1) (2024-07-15)\n\n\n### Bug Fixes\n\n* rename appendEmptyResponseHeaders to removeCookies ([498d044](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/498d0443b7981776cc7091049ac83a92a4d8d81b))\n\n# [1.6.0](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.5.3...v1.6.0) (2024-07-15)\n\n\n### Bug Fixes\n\n* enable refresh token route ([d081c22](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d081c22f67bdde49211ac6053011901c616f99d6))\n* fix \"process is not defined\" error in cloudflare worker [#192](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/192) ([6a94587](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/6a9458774da1ec8a026a223ffd9204eb5c11915f))\n* return null from getValidIdToken if provided server token is empty ([613f230](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/613f230504e30e8329eb1c1be008fadbf4347c96))\n* store latest valid id token on client ([5764a33](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/5764a33ae8cadff6e48f5e7cb6d31e977e4d8ab9))\n* suppress unknown headers property error ([1459ba9](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/1459ba99703ba7a6b3e9f10f59304d0974ccc652))\n\n\n### Features\n\n* added `getValidCustomToken` method and documented client-side SDK usage ([2261ef9](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/2261ef9321a0e3974456af2db11915a128d69421))\n* exposed customToken in handleValidToken, getTokens and getFirebaseAuth methods ([f95c34c](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/f95c34cafb5f87b3afe60130a1631e3c337f2d34))\n* introduced `enableMultipleCookies` auth middleware option to increase token capacity ([23ee02f](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/23ee02f2160faee133127dfb8808b1977dba4593))\n* introduced refreshTokenPath middleware option and getValidIdToken client method ([56e07c5](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/56e07c59cc9b6da45fd818c0600638bb9258bafa))\n* introduced removeCookie method ([f108984](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/f108984a9c74ed8cf2cf26133a8f3f8f65c905f9))\n* support for async response factory in refreshCredentials method ([25bf5c4](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/25bf5c46f68bc0f8cdd6cfd480802f3d23922a4d))\n\n# [1.6.0-canary.9](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.6.0-canary.8...v1.6.0-canary.9) (2024-07-14)\n\n\n### Features\n\n* introduced `enableMultipleCookies` auth middleware option to increase token capacity ([23ee02f](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/23ee02f2160faee133127dfb8808b1977dba4593))\n\n# [1.6.0-canary.8](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.6.0-canary.7...v1.6.0-canary.8) (2024-07-14)\n\n\n### Features\n\n* added `getValidCustomToken` method and documented client-side SDK usage ([2261ef9](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/2261ef9321a0e3974456af2db11915a128d69421))\n\n# [1.6.0-canary.7](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.6.0-canary.6...v1.6.0-canary.7) (2024-07-07)\n\n\n### Bug Fixes\n\n* suppress unknown headers property error ([1459ba9](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/1459ba99703ba7a6b3e9f10f59304d0974ccc652))\n\n\n### Features\n\n* exposed customToken in handleValidToken, getTokens and getFirebaseAuth methods ([f95c34c](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/f95c34cafb5f87b3afe60130a1631e3c337f2d34))\n\n# [1.6.0-canary.6](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.6.0-canary.5...v1.6.0-canary.6) (2024-06-17)\n\n\n### Bug Fixes\n\n* return null from getValidIdToken if provided server token is empty ([613f230](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/613f230504e30e8329eb1c1be008fadbf4347c96))\n\n# [1.6.0-canary.5](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.6.0-canary.4...v1.6.0-canary.5) (2024-06-15)\n\n\n### Bug Fixes\n\n* store latest valid id token on client ([5764a33](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/5764a33ae8cadff6e48f5e7cb6d31e977e4d8ab9))\n\n# [1.6.0-canary.4](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.6.0-canary.3...v1.6.0-canary.4) (2024-06-15)\n\n\n### Bug Fixes\n\n* enable refresh token route ([d081c22](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/d081c22f67bdde49211ac6053011901c616f99d6))\n\n# [1.6.0-canary.3](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.6.0-canary.2...v1.6.0-canary.3) (2024-06-15)\n\n\n### Features\n\n* introduced refreshTokenPath middleware option and getValidIdToken client method ([56e07c5](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/56e07c59cc9b6da45fd818c0600638bb9258bafa))\n\n# [1.6.0-canary.2](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.6.0-canary.1...v1.6.0-canary.2) (2024-06-05)\n\n\n### Features\n\n* introduced removeCookie method ([f108984](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/f108984a9c74ed8cf2cf26133a8f3f8f65c905f9))\n\n# [1.6.0-canary.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.5.4-canary.1...v1.6.0-canary.1) (2024-06-05)\n\n\n### Features\n\n* support for async response factory in refreshCredentials method ([25bf5c4](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/25bf5c46f68bc0f8cdd6cfd480802f3d23922a4d))\n\n## [1.5.4-canary.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.5.3...v1.5.4-canary.1) (2024-06-01)\n\n\n### Bug Fixes\n\n* fix \"process is not defined\" error in cloudflare worker [#192](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/192) ([6a94587](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/6a9458774da1ec8a026a223ffd9204eb5c11915f))\n\n## [1.5.3](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.5.2...v1.5.3) (2024-05-31)\n\n\n### Bug Fixes\n\n* referer is now based on caller host ([2f75386](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/2f75386de3d91aea42345771c006221eff819104))\n\n## [1.5.2](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.5.1...v1.5.2) (2024-05-30)\n\n\n### Bug Fixes\n\n* expose tokens in refreshCredentials response factory callback ([644b8a2](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/644b8a272cb48e830d21344f12bae9e3082ae1f4))\n\n## [1.5.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.5.0...v1.5.1) (2024-05-30)\n\n\n### Bug Fixes\n\n* reintroduce refreshAuthCookies as refreshNextResponseCookiesWithToken ([620f986](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/620f98682b9002837bfca287d32ea0371f2b2017))\n\n# [1.5.0](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.4.5...v1.5.0) (2024-05-30)\n\n\n### Bug Fixes\n\n* remove fetch `cache: no-store` due to https://github.com/awinogrodzki/next-firebase-auth-edge/issues/173 ([6fb8143](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/6fb81430b580b586f5a27c5b36624a441aa68e82))\n\n\n### Features\n\n* added refreshCredentials method that allows to pass modified request headers to NextResponse constructor ([2bf2877](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/2bf2877f5b12456c5e8125d5fa1babfc0074edaf))\n* extract referer from Next.js request headers ([bc666fa](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/bc666fa887b81adbf91681faa7d1974417b20988))\n* introduced Firebase API Key domain restriction support. Introduced changes to advanced methods and removed APIs deprecated in 1.0 ([67dbb9a](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/67dbb9a2908d62d90fb40a5a154cd2a7d8b14626))\n\n\n### Performance Improvements\n\n* **refreshCredentials:** slightly improve performance by generating signed tokens only once ([da2fc3e](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/da2fc3e164da0d5015e4d484813cafce2f033ea2))\n\n# [1.5.0-canary.5](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.5.0-canary.4...v1.5.0-canary.5) (2024-05-30)\n\n\n### Features\n\n* extract referer from Next.js request headers ([bc666fa](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/bc666fa887b81adbf91681faa7d1974417b20988))\n\n# [1.5.0-canary.4](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.5.0-canary.3...v1.5.0-canary.4) (2024-05-27)\n\n\n### Performance Improvements\n\n* **refreshCredentials:** slightly improve performance by generating signed tokens only once ([da2fc3e](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/da2fc3e164da0d5015e4d484813cafce2f033ea2))\n\n# [1.5.0-canary.3](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.5.0-canary.2...v1.5.0-canary.3) (2024-05-27)\n\n\n### Features\n\n* added refreshCredentials method that allows to pass modified request headers to NextResponse constructor ([2bf2877](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/2bf2877f5b12456c5e8125d5fa1babfc0074edaf))\n\n# [1.5.0-canary.2](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.5.0-canary.1...v1.5.0-canary.2) (2024-05-26)\n\n\n### Bug Fixes\n\n* remove fetch `cache: no-store` due to https://github.com/awinogrodzki/next-firebase-auth-edge/issues/173 ([6fb8143](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/6fb81430b580b586f5a27c5b36624a441aa68e82))\n\n# [1.5.0-canary.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.4.5...v1.5.0-canary.1) (2024-05-26)\n\n\n### Features\n\n* introduced Firebase API Key domain restriction support. Introduced changes to advanced methods and removed APIs deprecated in 1.0 ([67dbb9a](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/67dbb9a2908d62d90fb40a5a154cd2a7d8b14626))\n\n## [1.4.5](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.4.4...v1.4.5) (2024-05-26)\n\n\n### Bug Fixes\n\n* /api/login endpoint now fails with 400: Missing Token error when called without credentials ([2997fc5](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/2997fc51c503400fb9068750374797993f4a61d8))\n* exclude lib folder from npmignore file ([f7ef2d5](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/f7ef2d5249d7183f3f1204a34c540e03392943a4))\n* fix build cache path in github workflows ([df4c98d](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/df4c98dfe7176029743a04513aa5b67c60a453a3))\n* remove .env.dist from npm package ([5c136f9](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/5c136f9e2e7d2f3bc0427f21a91c7ff36a87d0d0))\n* remove tests and lint steps from semantic release pipeline ([160662d](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/160662d53077e7cfdd69f194eb1d89e31a7e8d55))\n* semantic release npm publish initialization ([3ed6ef5](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/3ed6ef591ced2613d3936aea7dd28140605ca167))\n* semantic release package configuration ([ec93cc6](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/ec93cc67ed5a8a2a624cef526de88d7601829aec))\n* set correct pkgRoot in semantic releases configuration ([9c36948](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/9c3694839088fee50e1362537cc7ad3e345d7763))\n\n## [1.4.5-canary.7](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.4.5-canary.6...v1.4.5-canary.7) (2024-05-26)\n\n\n### Bug Fixes\n\n* fix build cache path in github workflows ([df4c98d](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/df4c98dfe7176029743a04513aa5b67c60a453a3))\n\n## [1.4.5-canary.6](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.4.5-canary.5...v1.4.5-canary.6) (2024-05-26)\n\n\n### Bug Fixes\n\n* exclude lib folder from npmignore file ([f7ef2d5](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/f7ef2d5249d7183f3f1204a34c540e03392943a4))\n\n## [1.4.5-canary.5](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.4.5-canary.4...v1.4.5-canary.5) (2024-05-26)\n\n\n### Bug Fixes\n\n* remove tests and lint steps from semantic release pipeline ([160662d](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/160662d53077e7cfdd69f194eb1d89e31a7e8d55))\n\n## [1.4.5-canary.4](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.4.5-canary.3...v1.4.5-canary.4) (2024-05-26)\n\n\n### Bug Fixes\n\n* set correct pkgRoot in semantic releases configuration ([9c36948](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/9c3694839088fee50e1362537cc7ad3e345d7763))\n\n## [1.4.5-canary.2](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.4.5-canary.1...v1.4.5-canary.2) (2024-05-26)\n\n\n### Bug Fixes\n\n* remove .env.dist from npm package ([5c136f9](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/5c136f9e2e7d2f3bc0427f21a91c7ff36a87d0d0))\n\n## [1.4.5-canary.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.4.4...v1.4.5-canary.1) (2024-05-26)\n\n\n### Bug Fixes\n\n* /api/login endpoint now fails with 400: Missing Token error when called without credentials ([2997fc5](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/2997fc51c503400fb9068750374797993f4a61d8))\n* semantic release npm publish initialization ([3ed6ef5](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/3ed6ef591ced2613d3936aea7dd28140605ca167))\n\n## [1.4.4](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.4.3...v1.4.4) (2024-05-26)\n\n\n### Bug Fixes\n\n* disable default tag behavior in yarn publish ([1661468](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/1661468a501ad759ac55ce66d3eb0c8bab496b13))\n* lint ([c703cfb](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/c703cfb9a4c5afc67165366fd1bcaa3651c67a73))\n* semantic release publish step authorization ([232f624](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/232f6244e0126b0112cc4a0255780b070049910d))\n* semantic release publish step git author ([c917de4](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/c917de4227f432e0aeefdcdc1fd6b38a0d79d7bf))\n\n## [1.4.4-canary.1](https://github.com/awinogrodzki/next-firebase-auth-edge/compare/v1.4.3...v1.4.4-canary.1) (2024-05-26)\n\n\n### Bug Fixes\n\n* disable default tag behavior in yarn publish ([1661468](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/1661468a501ad759ac55ce66d3eb0c8bab496b13))\n* lint ([c703cfb](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/c703cfb9a4c5afc67165366fd1bcaa3651c67a73))\n* semantic release publish step authorization ([232f624](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/232f6244e0126b0112cc4a0255780b070049910d))\n* semantic release publish step git author ([c917de4](https://github.com/awinogrodzki/next-firebase-auth-edge/commit/c917de4227f432e0aeefdcdc1fd6b38a0d79d7bf))\n\n## 1.4.3\n\n### Patch Changes\n\n- Remove digest from debug logs\n\n## 1.4.2\n\n### Patch Changes\n\n- Fetch Google public keys with cache: \"no-store\" to fix #159\n\n## 1.4.1\n\n### Patch Changes\n\n- Improve cookieSignatureKeys input validation\n\n## 1.4.0\n\n### Minor Changes\n\n- `handleInvalidToken` is now called with `InvalidTokenReason` as the first argument. It gives developers more inslight and control over authentication flow\n\n## 1.3.0\n\n### Minor Changes\n\n- The library now stores tokens and signature in a single cookie, allowing to run in Firebase Hosting environment\n- Use the library without service account in authenticated Google Cloud Run environment\n- Added debug mode option\n\n## 1.2.0\n\n### Minor Changes\n\n- Introduced refreshServerCookies method to refresh credentials from inside Server Actions\n\n## 1.1.0\n\n### Minor Changes\n\n- Deprecated refreshAuthCookies methods in favor of refreshNextResponseCookies and refreshApiResponseCookies\n\n## 1.0.1\n\n### Patch Changes\n\n- Update middleware token verification caching doc link\n\n## 1.0.0\n\n### Major Changes\n\n- Reworked APIs\n\n## 0.11.2\n\n### Patch Changes\n\n- Added getUserByEmail method\n\n## 0.11.1\n\n### Patch Changes\n\n- Added Node.js 20 support\n\n## 0.11.0\n\n### Patch Changes\n\n- Added App Check support\n\n## 0.10.2\n\n### Patch Changes\n\n- Stop displaying middleware verification cache warning on prefetched routes\n\n## 0.10.1\n\n### Patch Changes\n\n- Remove internal verification cookie on middleware request instead throwing an error\n- Remove internal verification cookie on middleware request instead of throwing an error\n\n## 0.10.0\n\n### Minor Changes\n\n- Next.js 14 support\n\n## 0.9.5\n\n### Patch Changes\n\n- Skip response headers validation on redirect\n\n## 0.9.4\n\n### Patch Changes\n\n- Add list users function support\n\n## 0.9.3\n\n### Patch Changes\n\n- 964c04c: Check if the FIREBASE_AUTH_EMULATOR_HOST has already http:// added to it, otherwise you will get a cryptic fetch failed error.\n\n## 0.9.2\n\n### Patch Changes\n\n- Support tenantId in refreshAuthCookies\n\n## 0.9.1\n\n### Patch Changes\n\n- Return null if user was deleted from Firebase\n\n## 0.9.0\n\n### Minor Changes\n\n- Added middleware token verification caching\n\n## 0.8.8\n\n### Patch Changes\n\n- Add support for specifying tenantId in middleware\n\n## 0.8.7\n\n### Patch Changes\n\n- Convert signature key to UInt8Array directly instead using base64url.decode due to #92\n\n## 0.8.6\n\n### Patch Changes\n\n- Throw user friendly error on no matching kid in public keys response\n\n## 0.8.5\n\n### Patch Changes\n\n- Revalidate token against all public keys if kid is missing\n\n## 0.8.4\n\n### Patch Changes\n\n- Fix https://github.com/awinogrodzki/next-firebase-auth-edge/issues/90 by validating token against all returned public keys in case of not matching kid header\n\n## 0.8.3\n\n### Patch Changes\n\n- Fix no \"kid\" claim in idToken error when using emulator\n\n## 0.8.2\n\n### Patch Changes\n\n- Added createUser and updateUser methods\n\n## 0.8.1\n\n### Patch Changes\n\n- Remove 'cache: no-store' header from refreshExpiredIdToken\n\n## 0.8.0\n\n### Minor Changes\n\n- Refactor: remove custom JSON Web Token and Signature implementation in favor of jose\n\n## 0.7.7\n\n### Patch Changes\n\n- Fix Node.js 18.17 native WebCrypto ArrayBuffer compatibility issue\n\n## 0.7.6\n\n### Patch Changes\n\n- Import Next.js request cookie interfaces as type\n\n## 0.7.5\n\n### Patch Changes\n\n- Make caches optional due to Vercel Edge middleware error https://github.com/vercel/next.js/issues/50102\n\n## 0.7.4\n\n### Patch Changes\n\n- Set global cache before using ResponseCache\n\n## 0.7.3\n\n### Patch Changes\n\n- Use polyfill only if runtime is defined\n\n## 0.7.2\n\n### Patch Changes\n\n- Fix \"body already used\" error by cloning response upon rewriting\n\n## 0.7.1\n\n### Patch Changes\n\n- Added @edge-runtime/primitives to dependencies\n\n## 0.7.0\n\n### Minor Changes\n\n- Updated Next.js to 13.4 with stable app directory. Integrated edge-runtime and removed direct dependency to @peculiar/web-crypto. Integrated ServiceAccountCredential and PublicKeySignatureVerifier with Web APIs CacheStorage.\n\n## 0.6.2\n\n### Patch Changes\n\n- Update engines to support Node 19\n\n## 0.6.1\n\n### Patch Changes\n\n- Fix ReadonlyRequestCookies imports after update to Next.js 13.3.0\n\n## 0.6.0\n\n### Minor Changes\n\n- Added setCustomUserClaims, getUser and refreshAuthCookies Edge-runtime compatible methods\n\n## 0.5.1\n\n### Patch Changes\n\n- Handle refresh token error using handleError function\n- Updated dependencies\n  - next-firebase-auth-edge@0.5.1\n\n## 0.5.0\n\n### Minor Changes\n\n- Rename methods from getAuthenticatedResponse, getUnauthenticatedResponse and getErrorResponse to more readable handleValidToken, handleInvalidToken and handleError functions\n\n## 0.4.4\n\n### Patch Changes\n\n- Added refreshAuthCookies method to refresh cookie headers in api middleware\n\n## 0.4.3\n\n### Patch Changes\n\n- Introduced getUnauthenticatedResponse middleware option to handle redirects for unauthenticated users\n\n## 0.4.2\n\n### Patch Changes\n\n- getAuthenticatedResponse and getErrorResponse options are now async\n\n## 0.4.1\n\n### Patch Changes\n\n- Optional redirectOptions for use-cases where authentication happens in more than one contexts\n\n## 0.4.0\n\n### Minor Changes\n\n- Added authentication middleware to automatically handle redirection and authentication cookie refresh\n\n## 0.3.1\n\n### Patch Changes\n\n- Re-throw INVALID_CREDENTIALS FirebaseAuthError with error details on token refresh error\n\n## 0.3.0\n\n### Minor Changes\n\n- Updated peer next peer dependency to ^13.1.1 and removed allowMiddlewareResponseBody flag'\n\n## 0.2.15\n\n### Patch Changes\n\n- Handle \"USER_NOT_FOUND\" error during token refresh\n\n## 0.2.14\n\n### Patch Changes\n\n- Added Firebase Authentication Emulator support\n\n## 0.2.13\n\n### Patch Changes\n\n- Fix incorrect HMAC algorithm key buffer size\n\n## 0.2.12\n\n### Patch Changes\n\n- Update rotating credential HMAC key algorithm to SHA-512\n\n## 0.2.11\n\n### Patch Changes\n\n- Update rotating credential HMAC key algorithm to SHA-256\n\n## 0.2.10\n\n### Patch Changes\n\n- Support Next.js 18 LTS\n\n## 0.2.9\n\n### Patch Changes\n\n- Update Next.js peerDependency version to ^13.0.5 to allow future minor/patch versions\n\n## 0.2.8\n\n### Patch Changes\n\n- Integrated with changesets and eslint to improve transparency and legibility\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Amadeusz Winogrodzki\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"logo-white.svg\">\n  <source media=\"(prefers-color-scheme: light)\" srcset=\"logo.svg\">\n  <img alt=\"next-firebase-auth-edge\" src=\"logo.svg\" width=\"320\">\n</picture>\n\n---\n\nNext.js Firebase Authentication for Edge and Node.js runtimes. Use Firebase Authentication with latest Next.js features.\n\n[![npm version](https://badge.fury.io/js/next-firebase-auth-edge.svg)](https://badge.fury.io/js/next-firebase-auth-edge)\n\n## Example\n\nCheck out a working demo here: [next-firebase-auth-edge-starter.vercel.app](https://next-firebase-auth-edge-starter.vercel.app/)\n\nYou can find the source code for this demo at [examples/next-typescript-starter](https://github.com/ensite-in/next-firebase-auth-edge/tree/main/examples/next-typescript-starter)\n\n## Guide\n\nNew to Firebase or Next.js? No worries! Follow this easy, step-by-step guide to set up Firebase Authentication in Next.js app using the **next-firebase-auth-edge** library:\n\nhttps://hackernoon.com/using-firebase-authentication-with-the-latest-nextjs-features\n\n## Docs\n\nThe official documentation is available here: https://next-firebase-auth-edge-docs.vercel.app\n\n## Why?\n\nThe official `firebase-admin` library depends heavily on Node.js’s internal `crypto` library, which isn’t available in [Next.js Edge Runtime](https://nextjs.org/docs/api-reference/edge-runtime).\n\nThis library solves that problem by handling the creation and verification of [Custom ID Tokens](https://firebase.google.com/docs/auth/admin/verify-id-tokens) using the Web Crypto API, which works in Edge runtimes.\n\n## Features\n\n`next-firebase-auth-edge` supports all the latest Next.js features, like the [App Router](https://nextjs.org/docs/app) and [Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components).\n\nTo make adopting the newest Next.js features easier, this library works seamlessly with both [getServerSideProps](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props) and legacy [API Routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes).\n\n### Key Features:\n* **Supports Next.js's latest features**\n* **Zero bundle size**\n* **Minimal setup**: Unlike other libraries, you won’t need to create your own API routes or modify your `next.config.js`. Everything’s handled by [middleware](https://next-firebase-auth-edge-docs.vercel.app/docs/usage/middleware).\n* **Secure**: Uses [jose](https://github.com/panva/jose) for JWT validation, and signs user cookies with rotating keys to prevent cryptanalysis attacks.\n\n### What's New\n\nKey updates in latest release include:\n\n* New `enableTokenRefreshOnExpiredKidHeader` option in `authMiddleware`, which refreshes user tokens when Google’s public certificates expire (instead of throwing an error)\n* Added `privatePaths` option to [redirectToLogin](https://next-firebase-auth-edge-docs.vercel.app/docs/usage/redirect-functions#redirecttologin) helper function \n* Added [Metadata](https://next-firebase-auth-edge-docs.vercel.app/docs/usage/middleware#metadata) feature that allows to store custom data inside session cookies\n* Added `removeServerCookies` method to handle logout from inside Server Action \n* Added `experimental_createAnonymousUserIfUserNotFound` option to create anonymous user if no user was found \n* Full Firebase Emulator Support.\nThe library now fully supports the Firebase Emulator, enabling you to run your development app without needing to create a Firebase Project. Follow [starter example README](https://github.com/awinogrodzki/next-firebase-auth-edge/tree/main/examples/next-typescript-starter#emulator-support) for details\n* Custom token is now optional. To enable custom token support use [enableCustomToken](https://next-firebase-auth-edge-docs.vercel.app/docs/usage/middleware#custom-token) option\n* Support ESM, Browser and Node.js imports for better tree-shaking features\n* Support for **Node.js 24+** and **NPM 11**\n* Support for **Next.js 16**\n* Support for **React 19**\n\n## Installation\n\nTo install, run one of the following:\n\nWith **npm**\n\n```shell\nnpm install next-firebase-auth-edge\n```\n\nWith **yarn**\n\n```shell\nyarn add next-firebase-auth-edge\n```\n\nWith **pnpm**\n\n```shell\npnpm add next-firebase-auth-edge\n```\n\n## [→ Read the docs](https://next-firebase-auth-edge-docs.vercel.app/)\n"
  },
  {
    "path": "commitlint.config.js",
    "content": "module.exports = { extends: ['@commitlint/config-conventional'] };\n"
  },
  {
    "path": "docs/.eslintrc.js",
    "content": "module.exports = {\n  extends: ['prettier'],\n  parser: '@typescript-eslint/parser',\n  parserOptions: {\n    ecmaVersion: 2018,\n    sourceType: 'module',\n  },\n  plugins: ['@typescript-eslint', 'prettier'],\n  root: true,\n  rules: {\n    'prettier/prettier': 'error',\n  },\n};\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "/node_modules\n/.next/\n.DS_Store\n.vercel\npublic/.nextra\ntsconfig.tsbuildinfo\n.env\npublic/robots.txt\npublic/sitemap.xml\npublic/sitemap-0.xml\n"
  },
  {
    "path": "docs/README.md",
    "content": "# docs\n\nWebsite for `next-firebase-auth-edge`\n\nYou can run it locally like this:\n\n```\nyarn install\nyarn dev\n```\n"
  },
  {
    "path": "docs/components/Chip.tsx",
    "content": "import clsx from 'clsx';\nimport {ReactNode} from 'react';\n\ntype Props = {\n  children: ReactNode;\n  className?: string;\n  color?: 'green' | 'yellow';\n};\n\nexport default function Chip({children, className, color = 'green'}: Props) {\n  return (\n    <span\n      className={clsx(\n        className,\n        'inline-block rounded-md px-[6px] py-[2px] text-xs font-semibold uppercase tracking-wider',\n        {\n          green:\n            'bg-green-100 text-green-800 dark:bg-green-700/50 dark:text-green-100',\n          yellow:\n            'bg-yellow-100 text-orange-800 dark:bg-yellow-700/50 dark:text-yellow-100'\n        }[color]\n      )}\n    >\n      {children}\n    </span>\n  );\n}\n"
  },
  {
    "path": "docs/components/CommunityLink.tsx",
    "content": "import Link from 'next/link';\nimport {ComponentProps} from 'react';\nimport Chip from './Chip';\n\ntype Props = Omit<ComponentProps<typeof Link>, 'children'> & {\n  date: string;\n  author: string;\n  title: string;\n  type?: 'article' | 'video';\n};\n\nexport default function CommunityLink({\n  author,\n  date,\n  title,\n  type,\n  ...rest\n}: Props) {\n  return (\n    <div>\n      <Link className=\"inline-block py-2\" {...rest}>\n        <p className=\"text-xl font-semibold\">{title}</p>\n        <div className=\"mt-2\">\n          {type && (\n            <Chip\n              className=\"mr-2 translate-y-[-1px]\"\n              color={({article: 'green', video: 'yellow'} as const)[type]}\n            >\n              {{article: 'Article', video: 'Video'}[type]}\n            </Chip>\n          )}\n          <p className=\"inline-block text-base text-slate-500\">{date}</p>\n          <p className=\"inline-block text-base text-slate-500\">{' ・ '}</p>\n          <p className=\"inline-block text-base text-slate-500\">{author}</p>\n        </div>\n      </Link>\n    </div>\n  );\n}\n"
  },
  {
    "path": "docs/components/Example.tsx",
    "content": "import Chip from './Chip';\n\ntype Props = {\n  demoLink?: string;\n  sourceLink: string;\n  hash: string;\n  name: string;\n  description: string;\n  featured?: boolean;\n};\n\nexport default function Example({\n  demoLink,\n  description,\n  featured,\n  hash,\n  name,\n  sourceLink\n}: Props) {\n  return (\n    <div className=\"py-2\">\n      <h2\n        className=\"flex scroll-mt-8 items-center text-xl font-semibold\"\n        id={hash}\n      >\n        {name}\n        {featured && <Chip className=\"ml-3\">Featured</Chip>}\n      </h2>\n      <div className=\"mt-2\">\n        <p className=\"mt-2 max-w-lg text-base text-slate-600 dark:text-slate-400\">\n          {description}\n        </p>\n        <div className=\"mt-2\">\n          <a\n            className=\"nx-text-primary-600 inline-block text-base underline\"\n            href={sourceLink}\n            rel=\"noreferrer\"\n            target=\"_blank\"\n          >\n            Source\n          </a>\n          {demoLink && (\n            <>\n              <span className=\"inline-block text-base text-slate-500\">\n                {' ・ '}\n              </span>\n              <a\n                className=\"nx-text-primary-600 inline-block text-base underline\"\n                href={demoLink}\n                rel=\"noreferrer\"\n                target=\"_blank\"\n              >\n                Demo\n              </a>\n            </>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "docs/components/FeaturePanel.tsx",
    "content": "type Props = {\n  code?: string;\n  description: string;\n  title: string;\n};\n\nexport default function FeaturePanel({code, description, title}: Props) {\n  return (\n    <div className=\"-mx-4 flex flex-col overflow-hidden border-slate-200 lg:mx-0\">\n      {code && (\n        <div className=\"grow rounded-sm bg-white dark:bg-slate-800\">\n          <pre className=\"-ml-4 overflow-x-auto !p-4 lg:!p-6\">{code}</pre>\n        </div>\n      )}\n      <div className=\"p-4 lg:p-6\">\n        <h3 className=\"font-semibold text-slate-900 dark:text-white\">\n          {title}\n        </h3>\n        <p className=\"mt-2 max-w-md text-slate-600 dark:text-slate-400\">\n          {description}\n        </p>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "docs/components/Footer.tsx",
    "content": "import config from 'config';\nimport {useRouter} from 'next/router';\nimport FooterLink from './FooterLink';\nimport FooterSeparator from './FooterSeparator';\n\nexport default function Footer() {\n  const router = useRouter();\n\n  // Unfortunately, Nextra renders the footer incorrectly here\n  const isHidden = router.pathname.startsWith('/examples');\n  if (isHidden) return null;\n\n  return (\n    <div className=\"border-t border-slate-200 bg-slate-100 dark:border-t-slate-800 dark:bg-transparent\">\n      <div className=\"mx-auto max-w-[90rem] px-4 py-2 md:flex md:justify-between \">\n        <div>\n          <FooterLink href=\"/docs\">Docs</FooterLink>\n          <FooterSeparator />\n          <FooterLink href=\"/examples\">Examples</FooterLink>\n        </div>\n        <div>\n          <FooterLink href={config.githubUrl} target=\"_blank\">\n            GitHub\n          </FooterLink>\n          <FooterSeparator />\n          <FooterLink\n            href=\"https://github.com/sponsors/awinogrodzki\"\n            target=\"_blank\"\n          >\n            &#9829; Sponsor\n          </FooterLink>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "docs/components/FooterLink.tsx",
    "content": "import Link from 'next/link';\nimport {ComponentProps} from 'react';\n\ntype Props = ComponentProps<typeof Link>;\n\nexport default function FooterLink({children, ...rest}: Props) {\n  return (\n    <Link\n      className=\"inline-block py-3 text-slate-500 transition-colors hover:text-slate-900 dark:text-slate-400 dark:hover:text-white\"\n      {...rest}\n    >\n      <p className=\"inline-flex items-center text-xs\">{children}</p>\n    </Link>\n  );\n}\n"
  },
  {
    "path": "docs/components/FooterSeparator.tsx",
    "content": "export default function FooterSeparator() {\n  return (\n    <span className=\"mx-1 text-slate-400 dark:text-slate-400\">\n      &nbsp;&middot;&nbsp;\n    </span>\n  );\n}\n"
  },
  {
    "path": "docs/components/Hero.tsx",
    "content": "import HeroCode from './HeroCode';\nimport LinkButton from './LinkButton';\nimport Wrapper from './Wrapper';\n\ntype Props = {\n  description: string;\n  getStarted: string;\n  titleRegular: string;\n  titleStrong: string;\n  viewExample: string;\n};\n\nexport default function Hero({\n  description,\n  getStarted,\n  titleRegular,\n  titleStrong,\n  viewExample\n}: Props) {\n  return (\n    <div className=\"dark overflow-hidden\">\n      <div className=\"relative max-w-full overflow-hidden bg-slate-800 py-16 sm:px-2 lg:px-0\">\n        <div className=\"absolute left-0 top-0 h-[20500px] w-[20500px] translate-x-[-47.5%] rounded-full bg-gradient-to-b from-slate-900 via-cyan-500 md:top-1\" />\n        <Wrapper>\n          <div className=\"flex flex-col gap-8 xl:flex-row xl:items-center xl:justify-between\">\n            <div className=\"max-w-3xl\">\n              <h1 className=\"inline bg-gradient-to-r from-white via-sky-100 to-primary bg-clip-text text-3xl leading-tight tracking-tight text-transparent lg:text-5xl\">\n                <strong className=\"font-semibold\">{titleStrong}</strong>{' '}\n                <span className=\"font-light\">{titleRegular}</span>\n              </h1>\n\n              <p className=\"mt-3 max-w-xl text-lg leading-normal tracking-tight text-sky-100/70 lg:mt-4 lg:text-2xl lg:leading-normal\">\n                {description}\n              </p>\n              <div className=\"mt-8 flex gap-4 lg:mt-10\">\n                <LinkButton href=\"/docs\">{getStarted}</LinkButton>\n                <LinkButton\n                  href=\"https://next-firebase-auth-edge-starter.vercel.app\"\n                  target=\"_blank\"\n                  variant=\"secondary\"\n                >\n                  {viewExample}\n                </LinkButton>\n              </div>\n            </div>\n            <div className=\"max-w-[44rem] xl:-mr-8 2xl:-mr-16\">\n              <HeroCode />\n            </div>\n          </div>\n        </Wrapper>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "docs/components/HeroCode.tsx",
    "content": "import clsx from 'clsx';\nimport {ReactNode, useState} from 'react';\n\nfunction Tab({\n  active,\n  children,\n  onClick\n}: {\n  active: boolean;\n  children: ReactNode;\n  onClick(): void;\n}) {\n  return (\n    <button\n      className={clsx(\n        'flex items-center rounded-md px-4 py-2 text-sm font-medium transition-colors',\n        active\n          ? 'bg-slate-800 text-white'\n          : 'bg-slate-800/40 text-slate-500 hover:bg-slate-800'\n      )}\n      onClick={onClick}\n      type=\"button\"\n    >\n      {children}\n    </button>\n  );\n}\n\nconst files = [\n  {\n    name: 'proxy.ts',\n    code: (\n      <code\n        className=\"nx-border-black nx-border-opacity-[0.04] nx-bg-opacity-[0.03] nx-bg-black nx-break-words nx-rounded-md nx-border nx-py-0.5 nx-px-[.25em] nx-text-[.9em] dark:nx-border-white/10 dark:nx-bg-white/10\"\n        dir=\"ltr\"\n        data-language=\"tsx\"\n        data-theme=\"default\"\n      >\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-token-keyword)'}}>import</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>type</span>\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {' '}\n            {'{'} NextRequest {'}'}{' '}\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>from</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"next/server\"\n          </span>\n          <span style={{color: 'var(--shiki-color-text)'}}>;</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-token-keyword)'}}>import</span>\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {' '}\n            {'{'} authMiddleware {'}'}{' '}\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>from</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"next-firebase-auth-edge\"\n          </span>\n          <span style={{color: 'var(--shiki-color-text)'}}>;</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\"> </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-token-keyword)'}}>export</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>async</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>function</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-function)'}}>proxy</span>\n          <span style={{color: 'var(--shiki-color-text)'}}>(request</span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-function)'}}>\n            NextRequest\n          </span>\n          <span style={{color: 'var(--shiki-color-text)'}}>) {'{'}</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'  '}</span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>return</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-function)'}}>\n            authMiddleware\n          </span>\n          <span style={{color: 'var(--shiki-color-text)'}}>(request</span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> {'{'}</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'    '}loginPath\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"/api/login\"\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'    '}logoutPath\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"/api/logout\"\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'    '}apiKey</span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX\"\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'    '}cookieName\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"AuthToken\"\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'    '}cookieSignatureKeys\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> [</span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"Key-Should-Be-at-least-32-bytes-in-length\"\n          </span>\n          <span style={{color: 'var(--shiki-color-text)'}}>]</span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'    '}cookieSerializeOptions\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> {'{'}</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'      '}path</span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"/\"\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'      '}httpOnly\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-constant)'}}>true</span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'      '}secure\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-constant)'}}>false</span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-comment)'}}>\n            // Set this to true on HTTPS environments\n          </span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'      '}sameSite\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"lax\"\n          </span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>as</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>const</span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'      '}maxAge\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-constant)'}}>12</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>*</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-constant)'}}>60</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>*</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-constant)'}}>60</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>*</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-constant)'}}>24</span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-comment)'}}>\n            // Twelve days\n          </span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'    '}\n            {'}'}\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'    '}serviceAccount\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> {'{'}</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'      '}projectId\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"your-firebase-project-id\"\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'      '}clientEmail\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com\"\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'      '}privateKey\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n\"\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'    '}\n            {'}'}\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'  '}\n            {'}'});\n          </span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'}'}</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\"> </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-token-keyword)'}}>export</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>const</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-constant)'}}>config</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>=</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> {'{'}</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'  '}matcher</span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> [</span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"/api/login\"\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"/api/logout\"\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"/\"\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"/((?!_next|favicon.ico|api|.*\\\\.).*)\"\n          </span>\n          <span style={{color: 'var(--shiki-color-text)'}}>]</span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'}'};</span>\n        </span>\n      </code>\n    )\n  },\n  {\n    name: 'app/layout.tsx',\n    code: (\n      <code\n        className=\"nx-border-black nx-border-opacity-[0.04] nx-bg-opacity-[0.03] nx-bg-black nx-break-words nx-rounded-md nx-border nx-py-0.5 nx-px-[.25em] nx-text-[.9em] dark:nx-border-white/10 dark:nx-bg-white/10\"\n        data-language=\"tsx\"\n        data-theme=\"default\"\n        dir=\"ltr\"\n      >\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-token-keyword)'}}>import</span>\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {' '}\n            {'{'} cookies {'}'}{' '}\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>from</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"next/headers\"\n          </span>\n          <span style={{color: 'var(--shiki-color-text)'}}>;</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-token-keyword)'}}>import</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> {'{'}</span>\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {' '}\n            getTokens {'}'}{' '}\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>from</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"next-firebase-auth-edge\"\n          </span>\n          <span style={{color: 'var(--shiki-color-text)'}}>;</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\"> </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-token-keyword)'}}>export</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>default</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>async</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>function</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-function)'}}>RootLayout</span>\n          <span style={{color: 'var(--shiki-color-text)'}}>({'{'}</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'  '}children</span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'}'}</span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> {'{'}</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'  '}children</span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-function)'}}>JSX</span>\n          <span style={{color: 'var(--shiki-color-text)'}}>.</span>\n          <span style={{color: 'var(--shiki-token-function)'}}>Element</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'}'}) {'{'}\n          </span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'  '}</span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>const</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-constant)'}}>tokens</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>=</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>await</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-function)'}}>getTokens</span>\n          <span style={{color: 'var(--shiki-color-text)'}}>(</span>\n          <span style={{color: 'var(--shiki-token-function)'}}>cookies</span>\n          <span style={{color: 'var(--shiki-color-text)'}}>()</span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> {'{'}</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'    '}apiKey</span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            'XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX'\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'    '}cookieName\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            'AuthToken'\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'    '}cookieSignatureKeys\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> [</span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            'Key-Should-Be-at-least-32-bytes-in-length'\n          </span>\n          <span style={{color: 'var(--shiki-color-text)'}}>]</span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'    '}serviceAccount\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> {'{'}</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'      '}projectId\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            'your-firebase-project-id'\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'      '}clientEmail\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            'firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com'\n          </span>\n          <span style={{color: 'var(--shiki-token-punctuation)'}}>,</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'      '}privateKey\n          </span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>:</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            '-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n'\n          </span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'    '}\n            {'}'}\n          </span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'  '}\n            {'}'});\n          </span>\n        </span>\n        {'\\n'}\n        <span className=\"line\"> </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'  '}</span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>return</span>\n          <span style={{color: 'var(--shiki-color-text)'}}> (</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'    '}&lt;</span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            html\n          </span>\n          <span style={{color: 'var(--shiki-color-text)'}}> </span>\n          <span style={{color: 'var(--shiki-token-function)'}}>lang</span>\n          <span style={{color: 'var(--shiki-token-keyword)'}}>=</span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            \"en\"\n          </span>\n          <span style={{color: 'var(--shiki-color-text)'}}>&gt;</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'      '}&lt;</span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            head\n          </span>\n          <span style={{color: 'var(--shiki-color-text)'}}> /&gt;</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'      '}&lt;</span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            body\n          </span>\n          <span style={{color: 'var(--shiki-color-text)'}}>&gt;</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'        '}\n            {'{'}\n          </span>\n          <span style={{color: 'var(--shiki-token-comment)'}}>/* ... */</span>\n          <span style={{color: 'var(--shiki-color-text)'}}>{'}'}</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>\n            {'      '}&lt;/\n          </span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            body\n          </span>\n          <span style={{color: 'var(--shiki-color-text)'}}>&gt;</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'    '}&lt;/</span>\n          <span style={{color: 'var(--shiki-token-string-expression)'}}>\n            html\n          </span>\n          <span style={{color: 'var(--shiki-color-text)'}}>&gt;</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'  '});</span>\n        </span>\n        {'\\n'}\n        <span className=\"line\">\n          <span style={{color: 'var(--shiki-color-text)'}}>{'}'}</span>\n        </span>\n      </code>\n    )\n  }\n];\n\nexport default function HeroCode() {\n  const [fileIndex, setFileIndex] = useState(0);\n\n  return (\n    <div className=\"relative\">\n      <div className=\"absolute inset-0 rounded-md bg-gradient-to-tr from-sky-300 via-sky-300/70 to-blue-300 opacity-10 blur-lg\" />\n      <div className=\"absolute inset-0 rounded-md bg-gradient-to-tr from-sky-300 via-sky-300/70 to-blue-300 opacity-10\" />\n      <div className=\"relative rounded-md bg-[#0A101F]/80 ring-1 ring-white/10 backdrop-blur\">\n        <div className=\"absolute -top-px right-10 h-px w-1/2 bg-gradient-to-r from-sky-300/0 via-sky-300/40 to-sky-300/0\" />\n        <div className=\"p-4\">\n          <svg\n            aria-hidden=\"true\"\n            className=\"h-2.5 w-auto\"\n            fill=\"none\"\n            viewBox=\"0 0 42 10\"\n          >\n            <circle className=\"fill-slate-800\" cx={5} cy={5} r=\"4.5\" />\n            <circle className=\"fill-slate-800\" cx={21} cy={5} r=\"4.5\" />\n            <circle className=\"fill-slate-800\" cx={37} cy={5} r=\"4.5\" />\n          </svg>\n          <div className=\"mt-4 flex space-x-2 overflow-x-auto\">\n            {files.map((file) => (\n              <Tab\n                key={file.name}\n                active={fileIndex === files.indexOf(file)}\n                onClick={() => setFileIndex(files.indexOf(file))}\n              >\n                {file.name}\n              </Tab>\n            ))}\n          </div>\n          <div className=\"mt-6 flex items-start lg:min-h-[260px] lg:w-[684px]\">\n            <pre className=\"ml-[-16px] flex overflow-x-auto px-0\" data-theme>\n              {files[fileIndex].code}\n            </pre>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "docs/components/Link.tsx",
    "content": "import NextLink from 'next/link';\nimport {ComponentProps} from 'react';\n\ntype Props = Omit<ComponentProps<typeof NextLink>, 'className'>;\n\nexport default function Link(props: Props) {\n  return (\n    <NextLink\n      className=\"text-sky-600 underline underline-offset-2 transition-colors hover:text-sky-700\"\n      {...props}\n    />\n  );\n}\n"
  },
  {
    "path": "docs/components/LinkButton.tsx",
    "content": "import clsx from 'clsx';\nimport Link from 'next/link';\nimport {ComponentProps} from 'react';\n\ntype Props = {\n  variant?: 'primary' | 'secondary';\n} & Omit<ComponentProps<typeof Link>, 'className'>;\n\nexport default function LinkButton({\n  children,\n  variant = 'primary',\n  ...rest\n}: Props) {\n  return (\n    <Link\n      className={clsx(\n        'group inline-block rounded-full px-4 py-2 text-base font-semibold transition-colors lg:px-8 lg:py-4',\n        variant === 'primary'\n          ? 'bg-slate-800 text-white hover:bg-slate-700 dark:bg-primary dark:text-slate-900 dark:hover:bg-sky-200'\n          : 'bg-slate-200 text-slate-700 dark:bg-slate-800 dark:text-white/90 dark:hover:bg-slate-700'\n      )}\n      {...rest}\n    >\n      {children}\n      <span className=\"ml-2 inline-block transition-transform group-hover:translate-x-1\">\n        →\n      </span>\n    </Link>\n  );\n}\n"
  },
  {
    "path": "docs/components/Section.tsx",
    "content": "import {ReactNode} from 'react';\nimport Wrapper from './Wrapper';\n\ntype Props = {\n  children: ReactNode;\n  description: string;\n  title: string;\n};\n\nexport default function Section({children, description, title}: Props) {\n  return (\n    <section className=\"py-20 lg:py-40\">\n      <Wrapper>\n        <div>\n          <h2 className=\"text-2xl font-bold tracking-tight text-slate-900 lg:text-4xl dark:text-white\">\n            {title}\n          </h2>\n          <div className=\"mt-6 max-w-[42rem] text-base text-slate-600 lg:text-lg dark:text-slate-400\">\n            {description}\n          </div>\n        </div>\n        <div className=\"mt-10 lg:mt-24\">{children}</div>\n      </Wrapper>\n    </section>\n  );\n}\n"
  },
  {
    "path": "docs/components/Steps.module.css",
    "content": ".root {\n  @apply ml-4 border-l border-slate-200 pl-8;\n  counter-reset: step;\n}\n\n.root h3 {\n  counter-increment: step;\n  @apply text-lg;\n}\n.root h3:before {\n  content: counter(step);\n  @apply absolute mt-[-6px] ml-[-52px] inline-block h-10 w-10 rounded-full border-4 border-white bg-slate-100 pt-[4px] text-center text-base font-bold text-slate-500;\n}\n\n:global(.dark) .root {\n  @apply border-slate-800;\n}\n:global(.dark) .root h3:before {\n  @apply bg-slate-800 text-white/75;\n  border-color: rgba(17, 17, 17, var(--tw-bg-opacity)); /* nx-bg-dark */\n}\n"
  },
  {
    "path": "docs/components/Steps.tsx",
    "content": "import {ReactNode} from 'react';\nimport styles from './Steps.module.css';\n\ntype Props = {\n  children: ReactNode;\n};\n\nexport default function Steps({children}: Props) {\n  return <div className={styles.root}>{children}</div>;\n}\n"
  },
  {
    "path": "docs/components/Wrapper.tsx",
    "content": "import clsx from 'clsx';\nimport {ReactNode} from 'react';\n\ntype Props = {\n  children: ReactNode;\n  className?: string;\n};\n\nexport default function Wrapper({children, className}: Props) {\n  return (\n    <div className={clsx(className, 'relative mx-auto max-w-[95rem] px-4')}>\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "docs/config.js",
    "content": "module.exports = {\n  baseUrl: 'https://next-firebase-auth-edge-docs.vercel.app',\n  githubUrl: 'https://github.com/awinogrodzki/next-firebase-auth-edge'\n};\n"
  },
  {
    "path": "docs/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/basic-features/typescript for more information.\n"
  },
  {
    "path": "docs/next-sitemap.config.js",
    "content": "const config = require('./config');\n\n/** @type {import('next-sitemap').IConfig} */\nmodule.exports = {\n  siteUrl: config.baseUrl,\n  generateRobotsTxt: true\n};\n"
  },
  {
    "path": "docs/next.config.js",
    "content": "const withNextra = require('nextra')({\n  theme: 'nextra-theme-docs',\n  themeConfig: './theme.config.tsx',\n  staticImage: true,\n  defaultShowCopyCode: true,\n  flexsearch: {\n    codeblocks: false\n  }\n});\n\nmodule.exports = withNextra({\n  redirects: () => [\n    // Index pages\n    {\n      source: '/docs',\n      destination: '/docs/getting-started',\n      permanent: false\n    },\n    // Legacy pages\n    {\n      source: '/docs/usage/refresh-auth-cookies',\n      destination: '/docs/usage/refresh-credentials',\n      permanent: true\n    },\n  ],\n});\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"docs\",\n  \"version\": \"2.14.3\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"lint\": \"eslint ./pages ./components --ext .ts,.tsx\",\n    \"test\": \"echo 'No tests yet'\",\n    \"build\": \"next build\",\n    \"sitemap\": \"next-sitemap\",\n    \"start\": \"next start\"\n  },\n  \"dependencies\": {\n    \"@heroicons/react\": \"^2.0.17\",\n    \"@tailwindcss/typography\": \"^0.5.9\",\n    \"@vercel/analytics\": \"1.1.0\",\n    \"clsx\": \"^1.2.1\",\n    \"http-status-codes\": \"^2.2.0\",\n    \"next\": \"^14.0.4\",\n    \"nextra\": \"^2.13.2\",\n    \"nextra-theme-docs\": \"^2.13.2\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"tailwindcss\": \"^3.3.2\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^20.1.2\",\n    \"@types/react\": \"^18.2.29\",\n    \"autoprefixer\": \"^10.4.0\",\n    \"eslint\": \"^8.54.0\",\n    \"eslint-config-molindo\": \"^7.0.0\",\n    \"eslint-config-next\": \"^14.0.3\",\n    \"eslint-config-prettier\": \"^9.1.0\",\n    \"next-sitemap\": \"^4.0.7\",\n    \"typescript\": \"^5.2.2\"\n  },\n  \"resolutions\": {\n    \"strip-ansi\": \"6.0.1\",\n    \"next-mdx-remote\": \"6.0.0\"\n  },\n  \"funding\": \"https://github.com/awinogrodzki/next-firebase-auth-edge?sponsor=1\"\n}\n"
  },
  {
    "path": "docs/pages/_app.tsx",
    "content": "import {AppProps} from 'next/app';\nimport {Inter} from 'next/font/google';\nimport {ReactNode} from 'react';\nimport 'nextra-theme-docs/style.css';\nimport '../styles.css';\n\nconst inter = Inter({subsets: ['latin']});\n\ntype Props = AppProps & {\n  Component: {getLayout?(page: ReactNode): ReactNode};\n};\n\nexport default function App({Component, pageProps}: Props) {\n  const getLayout = Component.getLayout || ((page: ReactNode) => page);\n  return (\n    <div className={inter.className}>\n      {getLayout(<Component {...pageProps} />)}\n    </div>\n  );\n}\n"
  },
  {
    "path": "docs/pages/_document.tsx",
    "content": "import {Html, Head, Main, NextScript} from 'next/document';\nimport {SkipNavLink} from 'nextra-theme-docs';\nimport React from 'react';\n\nexport default function Document() {\n  return (\n    <Html lang=\"en\">\n      <Head />\n      <body>\n        <SkipNavLink styled />\n        <Main />\n        <NextScript />\n      </body>\n    </Html>\n  );\n}\n"
  },
  {
    "path": "docs/pages/_meta.json",
    "content": "{\n  \"index\": {\n    \"title\": \"Introduction\",\n    \"type\": \"page\",\n    \"display\": \"hidden\",\n    \"theme\": {\"layout\": \"raw\"}\n  },\n  \"docs\": {\n    \"title\": \"Docs\",\n    \"type\": \"page\"\n  },\n  \"examples\": {\n    \"title\": \"Examples\",\n    \"type\": \"page\",\n    \"theme\": {\n      \"sidebar\": false,\n      \"toc\": false\n    }\n  }\n}"
  },
  {
    "path": "docs/pages/docs/_meta.json",
    "content": "{\n  \"getting-started\": \"Getting started\",\n  \"usage\": \"Usage guide\",\n  \"emulator\": \"Emulator\",\n  \"app-check\": \"App Check\",\n  \"errors\": \"Handling errors\",\n  \"faq\": \"FAQ\"\n}"
  },
  {
    "path": "docs/pages/docs/app-check.mdx",
    "content": "# App Check Support\n\nThis library provides support for [Firebase App Check](https://firebase.google.com/docs/app-check). To learn how to integrate App Check into your app, follow the instructions in the [starter example README](https://github.com/awinogrodzki/next-firebase-auth-edge/tree/main/examples/next-typescript-starter).\n\nTo use `next-firebase-auth-edge` with App Check, you need to include the `X-Firebase-AppCheck` header with the App Check token when making a call to the `/api/login` endpoint. You can see how this works in [this example](https://github.com/awinogrodzki/next-firebase-auth-edge/blob/main/examples/next-typescript-starter/api/index.ts#L10-L14).\n\n```tsx\nimport { getToken } from \"@firebase/app-check\";\nimport { getAppCheck } from \"../app-check\";\n\nconst appCheckTokenResponse = await getToken(getAppCheck(), false);\n\nawait fetch(\"/api/login\", {\n  method: \"GET\",\n  headers: {\n    Authorization: `Bearer ${token}`,\n    \"X-Firebase-AppCheck\": appCheckTokenResponse.token,\n  },\n});\n```\n\n## Advanced Usage\n\nIf you need to explicitly create or verify an App Check token, you can use the `getAppCheck` function from `next-firebase-auth-edge/app-check`. You can see an example of how to do this below:\n\n```tsx\nimport { getAppCheck } from \"next-firebase-auth-edge/app-check\";\n\n// Optional in authenticated Google Cloud Run environment. Otherwise required.\nconst serviceAccount = {\n  projectId: \"firebase-project-id\",\n  privateKey: \"firebase service account private key\",\n  clientEmail: \"firebase service account client email\",\n};\n\n// Optional. Specify if your project supports multi-tenancy\n// https://cloud.google.com/identity-platform/docs/multi-tenancy-authentication\nconst tenantId = \"You tenant id\";\n\nconst { createToken, verifyToken } = getAppCheck({ serviceAccount, tenantId });\n```\n\n```tsx\nconst appId = \"your-app-id\";\n\n// Optional\nconst createTokenOptions = {\n  ttlMillis: 3600 * 1000,\n};\n\nconst token = await createToken(appId, createTokenOptions);\n\n// Optional\nconst verifyTokenOptions = {\n  currentDate: new Date(),\n};\n\nconst response = await verifyToken(token, verifyTokenOptions);\n```\n"
  },
  {
    "path": "docs/pages/docs/emulator.mdx",
    "content": "# Emulator Support\n\nThis library supports the Firebase Authentication Emulator. For more details on how to set it up, check out the [starter example README](https://github.com/awinogrodzki/next-firebase-auth-edge/tree/main/examples/next-typescript-starter).\n"
  },
  {
    "path": "docs/pages/docs/errors.mdx",
    "content": "# Handling Errors\n\n## handleInvalidToken\n\nThe [Auth middleware](/docs/usage/middleware) provides a `handleInvalidToken` function, which is called with an `InvalidTokenReason` as the first argument.\n\nThe `InvalidTokenReason` is primarily for informational purposes. The `handleInvalidToken` function is typically called when something **expected** happens, allowing the user to be safely redirected to the login page. One common **expected** event is when a user visits your app for the first time.\n\n### InvalidTokenReason\n\nThe table below describes the different types of `InvalidTokenReason`:\n\n| Name                    | Description                                                                                                                                                                                                                      |\n| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `MISSING_CREDENTIALS`   | The request does not contain an authentication cookie                                                                                                                                                                            |\n| `MISSING_REFRESH_TOKEN` | Credentials have expired, and no refresh token is available                                                                                                                                                                      |\n| `MALFORMED_CREDENTIALS` | The cookies cannot be parsed or the structure has changed                                                                                                                                                                        |\n| `INVALID_SIGNATURE`     | The cookie signature cannot be verified or the signature keys have changed                                                                                                                                                       |\n| `INVALID_CREDENTIALS`   | The cookies have a valid structure, but the `idToken` cannot be verified                                                                                                                                                         |\n| `INVALID_KID`           | This error usually means the certificate used to sign the token has expired, which is **expected**. Google periodically refreshes certificates as part of [key rotation](https://developer.okta.com/docs/concepts/key-rotation/) |\n\n## handleError\n\nUnlike `handleInvalidToken`, which handles **expected** issues, `handleError` is called when something **unexpected** occurs that a developer should investigate.\n\nThe `handleError` function receives an `AuthError` object as the first argument. This object includes `code` and `message` properties, which describe the type and meaning of the error.\n\n### AuthError\n\nThe error codes are divided as follows:\n\n| Code | Description |\n| ---- | ----------- |\n| `USER_NOT_FOUND` | The user cannot be found, possibly because they were removed after generating or refreshing the custom token |\n| `INVALID_CREDENTIAL` | The token could not be refreshed due to an invalid refresh token or service account credentials |\n| `TOKEN_EXPIRED` | Handled internally to refresh the token. Occurs when the custom `idToken` has expired |\n| `USER_DISABLED` | Thrown when `authMiddleware` is called with `checkRevoked: true` and the user has been disabled |\n| `TOKEN_REVOKED` | Thrown when `authMiddleware` is called with `checkRevoked: true` and the token has been revoked |\n| `INVALID_ARGUMENT` | The token has an incorrect structure or the certificate used to sign it has expired |\n| `INTERNAL_ERROR` | An internal error occurred. Check the error message for more details |\n| `NO_KID_IN_HEADER` | Handled internally to verify the token against all public certificates. Re-throws `INVALID_SIGNATURE` if none of the public keys match the token's signature |\n| `INVALID_SIGNATURE` | The token signature cannot be verified |\n| `MISMATCHING_TENANT_ID` | Provided tenant ID does not match Firebase tenant ID from the token |\n"
  },
  {
    "path": "docs/pages/docs/faq.mdx",
    "content": "# FAQ\n\n## Where are the login and logout API routes defined?\n\nUnlike the [next-firebase-auth](https://github.com/gladly-team/next-firebase-auth?tab=readme-ov-file#get-started) library, `next-firebase-auth-edge` does not require you to manually define your own `/api/login` or `/api/logout` routes. These routes are automatically handled by the [authMiddleware](/docs/getting-started/middleware). For more details, check out this [explanation](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/34#issuecomment-1588032612).\n"
  },
  {
    "path": "docs/pages/docs/getting-started/_meta.json",
    "content": "{\n  \"index\": \"Welcome!\",\n  \"middleware\": \"Middleware\",\n  \"auth-context\": \"AuthContext\",\n  \"auth-provider\": \"AuthProvider\",\n  \"layout\": \"Layout\",\n  \"login-page\": \"Usage with Firebase Auth\",\n  \"login-with-server-action\": \"Sign in with Server Action\",\n  \"logout-with-server-action\": \"Sign out with Server Action\"\n}\n"
  },
  {
    "path": "docs/pages/docs/getting-started/auth-context.mdx",
    "content": "# AuthContext\n\nThe library doesn't include any client-side code or built-in authentication state context. It's up to the developer to decide how to manage user data throughout the application.\n\nCheck out the example of an `AuthContext` below:\n\n## Example AuthContext\n\nThe following is an example implementation of custom `AuthContext` using React's [createContext](https://react.dev/reference/react/createContext).\n\n```tsx filename=\"AuthContext.ts\"\nimport {createContext, useContext} from 'react';\nimport {UserInfo} from 'firebase/auth';\nimport {Claims} from 'next-firebase-auth-edge/auth/claims';\n\nexport interface User extends UserInfo {\n  emailVerified: boolean;\n  customClaims: Claims;\n}\n\nexport interface AuthContextValue {\n  user: User | null;\n}\n\nexport const AuthContext = createContext<AuthContextValue>({\n  user: null\n});\n\nexport const useAuth = () => useContext(AuthContext);\n```\n"
  },
  {
    "path": "docs/pages/docs/getting-started/auth-provider.mdx",
    "content": "# AuthProvider\n\nTo share user data between the server and client components, we can use the custom `AuthContext` we created in the [previous step](/docs/getting-started/auth-context).\n\n## Example AuthProvider\n\nBelow is an example of how to implement a custom `AuthProvider` component that uses `AuthContext` to pass user data between the server and client components.\n\n```tsx filename=\"AuthProvider.tsx\"\n'use client';\n\nimport * as React from 'react';\nimport {AuthContext, User} from './AuthContext';\n\nexport interface AuthProviderProps {\n  user: User | null;\n  children: React.ReactNode;\n}\n\nexport const AuthProvider: React.FunctionComponent<AuthProviderProps> = ({\n  user,\n  children\n}) => {\n  return (\n    <AuthContext.Provider\n      value={{\n        user\n      }}\n    >\n      {children}\n    </AuthContext.Provider>\n  );\n};\n```\n"
  },
  {
    "path": "docs/pages/docs/getting-started/index.mdx",
    "content": "import {Card} from 'nextra-theme-docs';\nimport {ChevronRightIcon} from '@heroicons/react/24/outline';\n\n# Next.js Firebase Authentication\n\nWelcome to the `next-firebase-auth-edge` docs!\n\nIn this guide you will learn how to set up Firebase Authentication in your Next.js app.\n\n<div className=\"mt-8 flex flex-col gap-4 md:w-2/3\">\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Setup Next.js Middleware\"\n    href=\"/docs/getting-started/middleware\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Setup custom AuthContext\"\n    href=\"/docs/getting-started/auth-context\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Setup custom AuthProvider\"\n    href=\"/docs/getting-started/auth-provider\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Setup App Router Layout\"\n    href=\"/docs/getting-started/layout\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Usage with Firebase Auth\"\n    href=\"/docs/getting-started/login-page\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Sign in with Server Action\"\n    href=\"/docs/getting-started/login-with-server-action\"\n  />\n</div>\n"
  },
  {
    "path": "docs/pages/docs/getting-started/layout.mdx",
    "content": "# Layout\n\nWe can use the `getTokens` function from `next-firebase-auth-edge` to pull user information from request cookies.\n\nOnce we have the token details, we can map them to a `User` object and pass it to the `AuthProvider` we created in the [previous step](/docs/getting-started/auth-provider).\n\nYou can use `getTokens` in any React Server Component, whether it's `page.tsx` or `layout.tsx`.\n\nLearn more about the Next.js App Router in the [official docs](https://nextjs.org/docs/app).\n\n## Example RootLayout\n\nHere’s an example of how to implement the `RootLayout` React Server Component. It uses the `getTokens` function to create a user object from cookies and passes it to the `AuthProvider` client component.\n\n```tsx filename=\"app/layout.tsx\"\nimport { filterStandardClaims } from \"next-firebase-auth-edge/auth/claims\";\nimport { Tokens, getTokens } from \"next-firebase-auth-edge\";\nimport { cookies } from \"next/headers\";\nimport { User } from \"./AuthContext\";\nimport { AuthProvider } from \"./AuthProvider\";\n\nconst toUser = ({ decodedToken }: Tokens): User => {\n  const {\n    uid,\n    email,\n    picture: photoURL,\n    email_verified: emailVerified,\n    phone_number: phoneNumber,\n    name: displayName,\n    source_sign_in_provider: signInProvider,\n  } = decodedToken;\n\n  const customClaims = filterStandardClaims(decodedToken);\n\n  return {\n    uid,\n    email: email ?? null,\n    displayName: displayName ?? null,\n    photoURL: photoURL ?? null,\n    phoneNumber: phoneNumber ?? null,\n    emailVerified: emailVerified ?? false,\n    providerId: signInProvider,\n    customClaims,\n  };\n};\n\nexport default async function RootLayout({\n  children,\n}: {\n  children: JSX.Element\n}) {\n  // Since Next.js 15, `cookies()` returns a Promise and must be preceded with `await`.\n  // In Next.js 14, `cookies()` is synchronous — use `getTokens(cookies(), ...)` without `await` on `cookies()`.\n  const tokens = await getTokens(await cookies(), {\n    apiKey: 'XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX',\n    cookieName: 'AuthToken',\n    cookieSignatureKeys: [\n      'Key-Should-Be-at-least-32-bytes-in-length'\n    ],\n    serviceAccount: {\n      projectId: 'your-firebase-project-id',\n      clientEmail: 'firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com',\n      privateKey: '-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n'\n    }\n  });\n  const user = tokens ? toUser(tokens) : null;\n\n  return (\n    <html lang=\"en\">\n      <head />\n      <body>\n        <main>\n          <AuthProvider user={user}>{children}</AuthProvider>\n        </main>\n      </body>\n    </html>\n  );\n}\n```\n"
  },
  {
    "path": "docs/pages/docs/getting-started/login-page.mdx",
    "content": "# Using Firebase Auth\n\nFollow the official `firebase/auth` [guide](https://firebase.google.com/docs/auth/web/password-auth) to handle different operations like `signInWithEmailAndPassword` or `signOut`.\n\nCheck out the [starter example](/examples#starter) for more complete examples, including login, registration, and password reset flows.\n\n## Example LoginPage\n\nHere’s a simple login page client component that lets users sign in with their `email` and `password`:\n\n```tsx filename=\"page.tsx\"\n'use client';\n\nimport * as React from 'react';\nimport {getAuth, signInWithEmailAndPassword} from 'firebase/auth';\nimport {useRouter} from 'next/navigation';\n\nexport default function LoginPage() {\n  const [email, setEmail] = React.useState('');\n  const [password, setPassword] = React.useState('');\n  const router = useRouter();\n\n  async function handleSubmit(event: React.FormEvent) {\n    event.preventDefault();\n    event.stopPropagation();\n\n    const credential = await signInWithEmailAndPassword(\n      getAuth(),\n      email,\n      password\n    );\n    const idToken = await credential.user.getIdToken();\n\n    // Sets authenticated browser cookies\n    await fetch('/api/login', {\n      headers: {\n        Authorization: `Bearer ${idToken}`\n      }\n    });\n\n    // Refresh page after updating browser cookies\n    router.refresh();\n  }\n\n  return (\n    <div>\n      <h1>Login</h1>\n      <form onSubmit={handleSubmit}>\n        <input\n          required\n          value={email}\n          onChange={(e) => setEmail(e.target.value)}\n          name=\"email\"\n          type=\"email\"\n          placeholder=\"Email address\"\n        />\n        <br />\n        <input\n          required\n          name=\"password\"\n          value={password}\n          onChange={(e) => setPassword(e.target.value)}\n          type=\"password\"\n          placeholder=\"Password\"\n          minLength={8}\n        />\n        <button type=\"submit\">Submit</button>\n      </form>\n    </div>\n  );\n}\n```\n\nLet's focus on the form submission handler:\n```tsx\nasync function handleSubmit(event: React.FormEvent) {\n  event.preventDefault();\n  event.stopPropagation();\n\n  const credential = await signInWithEmailAndPassword(\n    getAuth(),\n    email,\n    password\n  );\n  const idToken = await credential.user.getIdToken();\n\n  // Sets authenticated browser cookies\n  await fetch('/api/login', {\n    headers: {\n      Authorization: `Bearer ${idToken}`\n    }\n  });\n\n  // Refresh page after updating browser cookies\n  router.refresh();\n}\n```\n\nIt can be broken down to following actions:\n1. We sign user in using email and password using `signInWithEmailAndPassword` from Firebase Client SDK\n2. We extract `idToken` using `credential` returned by `signInWithEmailAndPassword`\n3. We call `/api/login` endpoint exposed by the middleware to update browser cookies with authentication token\n4. We call `router.refresh()` to re-render server components with updated credentials\n"
  },
  {
    "path": "docs/pages/docs/getting-started/login-with-server-action.mdx",
    "content": "# Sign in with Server Action\n\nYou can follow the official `firebase/auth` [guide](https://firebase.google.com/docs/auth/web/password-auth) to handle operations like `signInWithEmailAndPassword` or `signOut`.\n\n## refreshCookiesWithIdToken\n\nThe `refreshCookiesWithIdToken` method updates browser cookies with the latest authenticated credentials based on the `idToken`. This method works with **Server Actions** and **Middleware**. After performing Firebase operations in a Server Action, you can use this method to refresh the browser cookies with updated credentials.\n\n## Example loginAction and LoginPage\n\n**Note for Vercel users:** The `firebase/auth` library isn't fully compatible with **Vercel** environments when used inside Server Actions.\n\nIf you get a `ReferenceError: document is not defined`, move the `firebase/auth` import to a client component.\n\nBelow is a simple example of a login page client component that lets users sign in using a Server Action and the `refreshCookiesWithIdToken` method.\n\nFirst, let’s define our Server Action:\n\n```tsx filename=\"login.tsx\"\n'use server';\n\nimport {refreshCookiesWithIdToken} from 'next-firebase-auth-edge/next/cookies';\nimport {signInWithEmailAndPassword} from 'firebase/auth';\nimport {cookies, headers} from 'next/headers';\nimport {redirect} from 'next/navigation';\n\n// See starter example for implementation: https://github.com/awinogrodzki/next-firebase-auth-edge/tree/main/examples/next-typescript-starter\nimport {getFirebaseAuth} from '@/app/auth/firebase';\nimport {authConfig} from '@/config/server-config';\n\nexport async function loginAction(username: string, password: string) {\n  const credential = await signInWithEmailAndPassword(\n    getFirebaseAuth(),\n    username,\n    password\n  );\n\n  const idToken = await credential.user.getIdToken();\n\n  // Since Next.js 15, `headers()` and `cookies()` return a Promise and must be preceded with `await`.\n  // In Next.js 14, these functions are synchronous — call them without `await`.\n  await refreshCookiesWithIdToken(\n    idToken,\n    await headers(),\n    await cookies(),\n    authConfig\n  );\n  redirect('/');\n}\n```\n\nNext, create LoginPage client component that will call login action:\n\n```tsx filename=\"LoginPage.tsx\"\n'use client';\n\nimport * as React from 'react';\n\ninterface LoginPageProps {\n  loginAction: (email: string, password: string) => void;\n}\n\nexport default function LoginPage({loginAction}: LoginPageProps) {\n  const [email, setEmail] = React.useState('');\n  const [password, setPassword] = React.useState('');\n  let [isLoginActionPending, startTransition] = React.useTransition();\n\n  async function handleSubmit(event: React.FormEvent) {\n    event.preventDefault();\n    event.stopPropagation();\n\n    startTransition(() => loginAction(email, password));\n  }\n\n  return (\n    <div>\n      <h1>Login</h1>\n      <form onSubmit={handleSubmit}>\n        <input\n          required\n          value={email}\n          onChange={(e) => setEmail(e.target.value)}\n          name=\"email\"\n          type=\"email\"\n          placeholder=\"Email address\"\n        />\n        <br />\n        <input\n          required\n          name=\"password\"\n          value={password}\n          onChange={(e) => setPassword(e.target.value)}\n          type=\"password\"\n          placeholder=\"Password\"\n          minLength={8}\n        />\n        <button disabled={isLoginActionPending} type=\"submit\">\n          Submit\n        </button>\n      </form>\n    </div>\n  );\n}\n```\n\nLastly, let's use LoginPage inside `page.tsx` Server Component:\n\n```tsx filename=\"page.tsx\"\nimport LoginPage from './LoginPage';\nimport {loginAction} from './login';\n\nexport default function Page() {\n  return <LoginPage loginAction={loginAction} />;\n}\n```\n"
  },
  {
    "path": "docs/pages/docs/getting-started/logout-with-server-action.mdx",
    "content": "# Sign out with Server Action\n\n## removeServerCookies [v1.9.0]\n\nThe `removeServerCookies` method removes server cookies from the browser. This method works with **Server Actions** and **Middleware**. \nAfter logging out with Firebase in a Server Action, you can use this method to remove the auth cookies from the browser.\n\nPrior to version `1.9.0`, the `removeServerCookies` method was not available. Instead, you had to manually remove the cookies from the browser.\n\nWith multiple cookies enabled:\n\n```tsx\ncookies.delete(authConfig.cookieName + \".id\");\ncookies.delete(authConfig.cookieName + \".refresh\");\ncookies.delete(authConfig.cookieName + \".sig\");\n\n// Optionally, if you enabled custom token:\ncookies.delete(authConfig.cookieName + \".custom\");\n```\n\nWithout multiple cookies enabled:\n\n```tsx\ncookies.delete(authConfig.cookieName);\n```\n\n\n## Example logoutAction and LogoutPage\n\nBelow is a simple example of a logout page component that lets users sign out using a Server Action and the `removeServerCookies` method.\n\nFirst, let’s define our Server Action:\n\n```tsx filename=\"logout.tsx\"\n'use server';\n\nimport {removeServerCookies} from \"next-firebase-auth-edge/next/cookies\";\nimport {signOut} from 'firebase/auth';\nimport {cookies} from 'next/headers';\nimport {redirect} from 'next/navigation';\nimport {getFirebaseAuth} from '@/app/auth/firebase';\nimport {authConfig} from '@/config/server-config';\n\nexport async function logoutAction() {\n  await signOut(getFirebaseAuth());\n\n  // Since Next.js 15, `cookies()` returns a Promise and must be preceded with `await`.\n  // In Next.js 14, `cookies()` is synchronous — call it without `await`.\n  removeServerCookies(await cookies(), { cookieName: authConfig.cookieName });\n  redirect('/');\n}\n```\n\nNext, create LogoutPage component that will call logout action:\n\n```tsx filename=\"LogoutPage.tsx\"\ninterface LogoutPageProps {\n  logoutAction: () => void;\n}\n\nexport default function LogoutPage({logoutAction}: LogoutPageProps) {\n    return (\n        <div>\n            <h1>Logout</h1>\n            <form action={logoutAction}>\n                <button type=\"submit\">Sign out</button>\n            </form>\n        </div>\n    );\n}\n```"
  },
  {
    "path": "docs/pages/docs/getting-started/middleware.mdx",
    "content": "# Authentication Middleware\n\nThe library offers an `authMiddleware` function that's meant to be used with [Next.js Proxy](https://nextjs.org/docs/app/getting-started/proxy) (or [Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware) in Next.js 14-15).\n\nFor more details, check out the [Authentication Middleware usage docs](/docs/usage/middleware).\n\nHere's a basic example of how to use `authMiddleware` in `proxy.ts`:\n\n> **Next.js 14-15:** Use `middleware.ts` with `export async function middleware(...)` instead. See the [Next.js 16 migration note](/docs/usage/middleware#nextjs-14-15-compatibility).\n\n```tsx filename=\"proxy.ts\"\nimport type { NextRequest } from \"next/server\";\nimport { authMiddleware } from \"next-firebase-auth-edge\";\n\nexport async function proxy(request: NextRequest) {\n  return authMiddleware(request, {\n    loginPath: \"/api/login\",\n    logoutPath: \"/api/logout\",\n    apiKey: \"XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX\",\n    cookieName: \"AuthToken\",\n    cookieSignatureKeys: [\"Key-Should-Be-at-least-32-bytes-in-length\"],\n    cookieSerializeOptions: {\n      path: \"/\",\n      httpOnly: true,\n      secure: false, // Set this to true on HTTPS environments\n      sameSite: \"lax\" as const,\n      maxAge: 12 * 60 * 60 * 24, // Twelve days\n    },\n    serviceAccount: {\n      projectId: \"your-firebase-project-id\",\n      clientEmail: \"firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com\",\n      privateKey: \"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n\",\n    },\n  });\n}\n\nexport const config = {\n  matcher: [\"/api/login\", \"/api/logout\", \"/\", \"/((?!_next|favicon.ico|api|.*\\\\.).*)\"],\n};\n```\n"
  },
  {
    "path": "docs/pages/docs/usage/_meta.json",
    "content": "{\n  \"index\": \"Start\",\n  \"middleware\": \"Middleware\",\n  \"server-components\": \"Server Components\",\n  \"redirect-functions\": \"Redirect Helper Functions\",\n  \"app-router-api-routes\": \"App Router API Route Handlers\",\n  \"pages-router-api-routes\": \"Pages Router API Routes\",\n  \"get-server-side-props\": \"Usage in getServerSideProps\",\n  \"refresh-credentials\": \"Refreshing credentials\",\n  \"remove-credentials\": \"Removing credentials\",\n  \"client-side-apis\": \"Using Client-Side APIs\",\n  \"domain-restriction\": \"Firebase API Key domain restriction\",\n  \"advanced-usage\": \"Advanced usage\",\n  \"cloud-run\": \"Usage in Google Cloud Run\",\n  \"firebase-hosting\": \"Usage in Firebase Hosting\",\n  \"debug-mode\": \"Debug mode\"\n}\n"
  },
  {
    "path": "docs/pages/docs/usage/advanced-usage.mdx",
    "content": "# Advanced Usage\n\nThe authentication middleware may not cover every use case. To support more complex authentication flows, `next-firebase-auth-edge` offers a set of low-level tools:\n\n## getFirebaseAuth\n\nThe `getFirebaseAuth` function provides several server-side methods to manage more advanced authentication scenarios.\n\n```tsx\nimport {getFirebaseAuth} from 'next-firebase-auth-edge';\n\nconst {\n  getCustomIdAndRefreshTokens,\n  verifyIdToken,\n  createCustomToken,\n  handleTokenRefresh,\n  getUser,\n  getUserByEmail,\n  createUser,\n  updateUser,\n  deleteUser,\n  verifyAndRefreshExpiredIdToken,\n  setCustomUserClaims\n} = getFirebaseAuth({\n  apiKey: 'YOUR FIREBASE API KEY',\n  serviceAccount: {\n    projectId: 'your-firebase-project-id',\n    clientEmail:\n      'firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com',\n    privateKey: '-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n'\n  }\n});\n```\n\n### Options\n\n| Name             | Type                                                             | Required?                                                                                  | Description                                                                                                                                                    |\n| ---------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| apiKey           | `string`                                                         | **Required**                                                                               | Firebase Web API Key from the Firebase Project settings page. This key becomes visible only after you enable Firebase Authentication in your Firebase project. |\n| serviceAccount   | `{ projectId: string; clientEmail: string; privateKey: string }` | Optional (required unless in [Google Cloud Run](https://cloud.google.com/run) environment) | Firebase Service Account credentials.                                                                                                                          |\n| tenantId         | `string`                                                         | Optional                                                                                   | Specify this if your project supports [multi-tenancy](https://cloud.google.com/identity-platform/docs/multi-tenancy-authentication).                           |\n| serviceAccountId | `string`                                                         | Optional                                                                                   | Used to specify a service account ID in a [Google Cloud Run](https://cloud.google.com/run) environment.                                                        |\n\n### Methods\n\n| Name                           | Type                                                                                                                                                                           | Description                                                                                                                                                                                                                                                                                                                                                                      |\n| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| getCustomIdAndRefreshTokens    | `(idToken: string, options?: {appCheckToken?: string, referer?: string}) => Promise<CustomTokens>`                                                                             | Generates new ID and refresh tokens for the user identified by the given `idToken`. Optionally accepts an `appCheckToken` if your app supports [App Check](https://firebase.google.com/docs/app-check). Can also accept a `referer` if your API key is domain-restricted.                                                                                                        |\n| verifyIdToken                  | `(idToken: string, options?: {checkRevoked?: boolean, referer?: string, currentDate?: Date}) => Promise<DecodedIdToken>`                                                       | Verifies the given `idToken` and throws an `AuthError` if verification fails. You can check for revoked tokens by passing `checkRevoked`. The `referer` is used for domain-restricted API keys. Optionally set a `currentDate` to control when the token is validated against.                                                                                                   |\n| verifyAndRefreshExpiredIdToken | `(tokens: {idToken: string, refreshToken: string, customToken?: string}, options?: {checkRevoked?: boolean, referer?: string, currentDate?: Date}) => Promise<VerifiedCookies>` | Verifies the `idToken`, and if it's expired, uses the `refreshToken` to revalidate it. Throws `InvalidTokenError` if the credentials are invalid. The options are the same as in `verifyIdToken`. Returns `VerifiedCookies` which includes the decoded token and the new tokens. Custom token can be enabled by setting `enableCustomToken` option to `true` in `authMiddleware`. |\n| createCustomToken              | `(uid: string, developerClaims?: object) => Promise<string>`                                                                                                                   | Creates a custom token for the specified Firebase user. You can also pass optional `developerClaims` to include additional data.                                                                                                                                                                                                                                                 |\n| handleTokenRefresh             | `(refreshToken: string, options?: {referer?: string, enableCustomToken?: boolean}) => Promise<VerifiedCookies>`                                                                                              | Returns a new ID token and its decoded form using the given `refreshToken`. The `referer` option is used if the API key is domain-restricted. `enableCustomToken` should match the value you pass to `authMiddleware`.                                                                                                                                                                                                                                 |\n| getUser                        | `(uid: string) => Promise<UserRecord>`                                                                                                                                         | Retrieves a Firebase `UserRecord` by the user's `uid`.                                                                                                                                                                                                                                                                                                                           |\n| getUserByEmail                 | `(email: string) => Promise<UserRecord>`                                                                                                                                       | Retrieves a Firebase `UserRecord` by the user's email address.                                                                                                                                                                                                                                                                                                                   |\n| createUser                     | `(request: CreateRequest) => Promise<UserRecord>`                                                                                                                              | Creates a new user and returns the `UserRecord`. Refer to Firebase’s [Create a user](https://firebase.google.com/docs/auth/admin/manage-users#create_a_user) documentation for details on the request structure.                                                                                                                                                                 |\n| updateUser                     | `(uid: string, request: UpdateRequest) => Promise<UserRecord>`                                                                                                                 | Updates an existing user by `uid` and returns the updated `UserRecord`. See Firebase’s [Update a user](https://firebase.google.com/docs/auth/admin/manage-users#update_a_user) documentation for request examples.                                                                                                                                                               |\n| deleteUser                     | `(uid: string) => Promise<void>`                                                                                                                                               | Deletes the user associated with the provided `uid`.                                                                                                                                                                                                                                                                                                                             |\n| setCustomUserClaims            | `(uid: string, customClaims: object ∣ null) => Promise<void>`                                                                                                                  | Sets custom claims for the specified user, overwriting existing values. Use `getUser` to retrieve the current claims.                                                                                                                                                                                                                                                            |\n"
  },
  {
    "path": "docs/pages/docs/usage/app-router-api-routes.mdx",
    "content": "# App Router API Route Handlers\n\nHere’s an example of how to use the [getTokens](/docs/usage/server-components) function in [API Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers).\n\n```tsx\nimport { NextRequest, NextResponse } from \"next/server\";\nimport { getTokens } from \"next-firebase-auth-edge\";\n\nexport async function GET(request: NextRequest) {\n  const tokens = await getTokens(request.cookies, {\n    apiKey: 'XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX',\n    cookieName: 'AuthToken',\n    cookieSignatureKeys: ['Key-Should-Be-at-least-32-bytes-in-length'],\n    serviceAccount: {\n      projectId: 'your-firebase-project-id',\n      clientEmail: 'firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com',\n      privateKey: '-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n'\n    }\n  });\n\n  if (!tokens) {\n    throw new Error(\"Unauthenticated\");\n  }\n\n  const headers: Record<string, string> = {\n    \"Content-Type\": \"application/json\",\n  };\n\n  const response = new NextResponse(\n    JSON.stringify({\n      tokens,\n    }),\n    {\n      status: 200,\n      headers,\n    }\n  );\n\n  return response;\n}\n```\n"
  },
  {
    "path": "docs/pages/docs/usage/client-side-apis.mdx",
    "content": "# Using Client-Side APIs\n\nThe starter example uses the [inMemoryPersistence](https://github.com/awinogrodzki/next-firebase-auth-edge/blob/main/examples/next-typescript-starter/app/auth/firebase.ts#L28-L30) strategy, relying entirely on server-side tokens. This avoids consistency issues on the client side.\n\nWhile this approach is recommended, it can lead to a few challenges:\n\n1. **Stale tokens:** In long-running client sessions, server-side tokens may expire, requiring the user to refresh the page to get a valid token. This typically happens if the user reopens a tab after about an hour.\n2. **Unauthenticated Firebase Client SDK environment:** With `inMemoryPersistence`, `currentUser` will often be `null` when using [client-side APIs](https://firebase.google.com/docs/auth/web/manage-users), preventing the use of Firebase’s client-side SDKs.\n\nHowever, `next-firebase-auth-edge` includes several features that address these issues:\n\n### Enable Refresh Token API Endpoint in Auth Middleware\n\nIn long-running client sessions (e.g., when a user reopens a tab after an hour), the server-side token may expire. This can cause problems when validating external API calls or when using the `customToken` with Firebase's `signInWithCustomToken`.\n\nTo resolve this, you can expose an endpoint via `authMiddleware` to refresh client-side tokens when the server-side token has expired.\n\nTo enable this endpoint, define the `refreshTokenPath` option in proxy/middleware.\n\n> **Next.js 14-15:** Use `middleware.ts` with `export async function middleware(...)` instead of `proxy.ts`. See the [compatibility note](/docs/usage/middleware#nextjs-14-15-compatibility).\n\n```tsx filename=\"proxy.ts\"\nexport async function proxy(request: NextRequest) {\n  return authMiddleware(request, {\n    loginPath: '/api/login',\n    logoutPath: '/api/logout',\n    refreshTokenPath: '/api/refresh-token'\n    // other options...\n  });\n}\n\nexport const config = {\n  // Make sure to include the path in `matcher`\n  matcher: [\n    '/api/login',\n    '/api/logout',\n    '/api/refresh-token',\n    '/',\n    '/((?!_next|favicon.ico|api|.*\\\\.).*)'\n  ]\n};\n```\n\nCalling `/api/refresh-token` will:\n\n1. Check if the current token has expired. If it has, it regenerates the token and updates the cookies with a `Set-Cookie` header containing the fresh token.\n2. Return JSON with a valid `idToken`. It can also return `customToken`, if `enableCustomToken` is set to `true` in `authMiddleware`.\n\n### getValidIdToken\n\nThe `getValidIdToken` function works in conjunction with the [refresh token endpoint](/docs/usage/client-side-apis#enable-refresh-token-api-endpoint-in-auth-middleware) to ensure you have the latest valid ID token. This is helpful if you use tokens to authorize external API calls.\n\nIt requires `serverIdToken`, which is the `token` returned by the [getTokens](/docs/usage/server-components#gettokens) function in server components.\n\nThe function is optimized to be fast and safe for repeated calls. The `/api/refresh-token` endpoint will only be called if the token has expired.\n\nExample usage:\n\n```ts\nimport {getValidIdToken} from 'next-firebase-auth-edge/next/client';\n\nexport async function fetchSomethingFromExternalApi(serverIdToken: string) {\n  const idToken = await getValidIdToken({\n    serverIdToken,\n    refreshTokenUrl: '/api/refresh-token'\n  });\n\n  return fetch('https://some-external-api.com/api/example', {\n    method: 'GET',\n    headers: {\n      Authorization: `Bearer ${idToken}`\n    }\n  });\n}\n```\n\n### getValidCustomToken\n\nPlease note that since v1.8 custom token is disabled by default. In order to enable custom cookies, pass `enableCustomToken: true` option to `authMiddleware`.\n\nCustom token introduces significant footprint on authentication cookie and is not required for most use-cases.\n\nIf you want to avoid cookie size issues, learn how to [split session into multiple cookies](/docs/usage/middleware#multiple-cookies)\n\nSimilar to `getValidIdToken`, the `getValidCustomToken` function works with the [refresh token endpoint](/docs/usage/client-side-apis#enable-refresh-token-api-endpoint-in-auth-middleware) to provide a valid custom token. This is useful when using the custom token with Firebase’s [signInWithCustomToken](https://firebase.google.com/docs/auth/web/custom-auth#authenticate-with-firebase) method.\n\nIt requires `serverCustomToken`, which is the `customToken` returned by the [getTokens](/docs/usage/server-components#gettokens) function in server components.\n\nLike `getValidIdToken`, this function is designed to be efficient and only calls the `/api/refresh-token` endpoint if necessary.\n\nExample usage:\n\n```ts\nexport async function signInWithServerCustomToken(serverCustomToken: string) {\n  const auth = getAuth(getFirebaseApp());\n\n  const customToken = await getValidCustomToken({\n    serverCustomToken,\n    refreshTokenUrl: '/api/refresh-token'\n  });\n\n  if (!customToken) {\n    throw new Error('Invalid custom token');\n  }\n\n  return signInWithCustomToken(auth, customToken);\n}\n```\n\n## Handling errors\n\n`getValidIdToken` and `getValidCustomToken` can fail with [AuthError](/docs/errors#autherror).\n\nWhenever the error happens, it usually means that the credentials have expired due to various reasons, and **user should be redirected to the sign in page**.\n\n## Using Firebase Client SDKs\n\nThe Firebase Client SDK exposes the [signInWithCustomToken](https://firebase.google.com/docs/auth/web/custom-auth#authenticate-with-firebase) method, which allows you to access the current user using a custom token.\n\nYou can obtain a custom token by calling the [getTokens](/docs/usage/server-components#gettokens) function in server components.\n\n```tsx\nimport {signInWithCustomToken} from 'firebase/auth';\nimport {getValidCustomToken} from 'next-firebase-auth-edge/next/client';\n\nimport {doc, getDoc, getFirestore, updateDoc, setDoc} from 'firebase/firestore';\n\nexport async function doSomethingWithFirestoreClient(\n  serverCustomToken: string\n) {\n  const auth = getAuth(getFirebaseApp());\n\n  // See https://next-firebase-auth-edge-docs.vercel.app/docs/usage/client-side-apis#getvalidcustomtoken\n  const customToken = await getValidCustomToken({\n    serverCustomToken,\n    refreshTokenUrl: '/api/refresh-token'\n  });\n\n  if (!customToken) {\n    throw new Error('Invalid custom token');\n  }\n\n  const {user: firebaseUser} = await signInWithCustomToken(auth, customToken);\n\n  // Use client-side firestore instance\n  const db = getFirestore(getApp());\n}\n```\n"
  },
  {
    "path": "docs/pages/docs/usage/cloud-run.mdx",
    "content": "# Usage in Google Cloud Run Environment\n\nBefore running `next-firebase-auth-edge` in a Google Cloud Run environment, make sure to:\n\n1. [Enable the IAM Service Account Credentials API](https://console.cloud.google.com/apis/api/iamcredentials.googleapis.com).\n2. Assign the `iam.serviceAccounts.signBlob` permission to the IAM role attached to the [default compute service account](https://console.cloud.google.com/iam-admin/iam).\n\nOnce this is done, you can omit the `serviceAccount` option in `authMiddleware`, `getTokens`, and other functions. If `serviceAccount` is `undefined`, `next-firebase-auth-edge` will automatically extract credentials from the authenticated [Google Cloud Run](https://cloud.google.com/run) environment.\n\nKeep in mind that you still need to provide the Firebase `apiKey`.\n\nExample [authMiddleware](/docs/usage/middleware) usage:\n\n> **Next.js 14-15:** Use `middleware.ts` with `export async function middleware(...)` instead of `proxy.ts`. See the [compatibility note](/docs/usage/middleware#nextjs-14-15-compatibility).\n\n```tsx filename=\"proxy.ts\"\nimport { NextRequest, NextResponse } from \"next/server\";\nimport { authMiddleware } from \"next-firebase-auth-edge\";\n\nexport async function proxy(request: NextRequest) {\n  return authMiddleware(request, {\n    loginPath: \"/api/login\",\n    logoutPath: \"/api/logout\",\n    apiKey: \"XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX\",\n    cookieName: \"AuthToken\",\n    cookieSignatureKeys: [\"Key-Should-Be-at-least-32-bytes-in-length\"],\n    cookieSerializeOptions: {\n      path: \"/\",\n      httpOnly: true,\n      secure: false,\n      sameSite: \"lax\" as const,\n      maxAge: 12 * 60 * 60 * 24,\n    },\n  });\n}\n```\n\nExample [getTokens](/docs/usage/server-components) usage:\n\n```tsx\nimport { getTokens } from \"next-firebase-auth-edge\";\n\nconst tokens = await getTokens(context.req.cookies, {\n    apiKey: 'XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX',\n    cookieName: 'AuthToken',\n    cookieSignatureKeys: ['Key-Should-Be-at-least-32-bytes-in-length'],\n});\n```\n"
  },
  {
    "path": "docs/pages/docs/usage/debug-mode.mdx",
    "content": "# Debug Mode\n\nYou can enable `debug mode` by setting `debug: true` in the options for `authMiddleware` and `getTokens`.\n\nWhen debug mode is active, the middleware will log additional details about the authentication process to the console.\n\n> **Next.js 14-15:** Use `middleware.ts` with `export async function middleware(...)` instead of `proxy.ts`. See the [compatibility note](/docs/usage/middleware#nextjs-14-15-compatibility).\n\n```tsx filename=\"proxy.ts\"\nimport { NextRequest, NextResponse } from \"next/server\";\nimport { authMiddleware } from \"next-firebase-auth-edge\";\n\nexport async function proxy(request: NextRequest) {\n  return authMiddleware(request, {\n    debug: true, // Enable debug mode\n\n    loginPath: \"/api/login\",\n    logoutPath: \"/api/logout\",\n    apiKey: \"XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX\",\n    cookieName: \"AuthToken\",\n    cookieSignatureKeys: [\"Key-Should-Be-at-least-32-bytes-in-length\"],\n    cookieSerializeOptions: {\n      path: \"/\",\n      httpOnly: true,\n      secure: false,\n      sameSite: \"lax\" as const,\n      maxAge: 12 * 60 * 60 * 24,\n    },\n    serviceAccount: {\n      projectId: \"your-firebase-project-id\",\n      clientEmail: \"firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com\",\n      privateKey: \"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n\",\n    },\n  });\n}\n\nexport const config = {\n  matcher: [\"/api/login\", \"/api/logout\", \"/\", \"/((?!_next|favicon.ico|api|.*\\\\.).*)\"],\n};\n```\n\n\n\n```tsx\nimport { getTokens } from \"next-firebase-auth-edge\";\nimport { cookies } from \"next/headers\";\n```\n\n```tsx\n// Since Next.js 15, `cookies()` returns a Promise and must be preceded with `await`.\n// In Next.js 14, `cookies()` is synchronous — use `getTokens(cookies(), ...)` without `await` on `cookies()`.\nconst tokens = await getTokens(await cookies(), {\n  debug: true,\n  apiKey: 'XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX',\n  cookieName: 'AuthToken',\n  cookieSignatureKeys: ['Key-Should-Be-at-least-32-bytes-in-length'],\n  serviceAccount: {\n    projectId: 'your-firebase-project-id',\n    clientEmail:\n      'firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com',\n    privateKey:\n      '-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n'\n  }\n});\n```"
  },
  {
    "path": "docs/pages/docs/usage/domain-restriction.mdx",
    "content": "# Firebase API Key Domain Restriction\n\nIn a production-ready application, it's important to restrict your Firebase API key by domain for security purposes.\n\nYou can update your API key restrictions in the [Google Cloud Console](https://console.cloud.google.com/apis/credentials).\n\n## Enable Referer Validation\n\nTo support API key domain restrictions, you need to inform Google APIs about the referer of your requests.\n\nIf you are using any of the advanced methods like `getCustomIdAndRefreshTokens`, `verifyIdToken`, `handleTokenRefresh`, or `verifyAndRefreshExpiredIdToken` from the [advanced usage](/docs/usage/advanced-usage) section, make sure to pass the `referer` option. The `referer` should be the authorized domain, derived from the request headers. You can use the `getReferer` function (imported from `next-firebase-auth-edge/next/utils`) to extract the referer from the headers of `NextRequest`.\n\n```ts\nimport {getFirebaseAuth} from 'next-firebase-auth-edge/auth';\nimport {getReferer} from 'next-firebase-auth-edge/next/utils';\nimport type {NextRequest} from 'next/server';\n\nconst {verifyIdToken} = getFirebaseAuth(/*{...}*/);\n\nexport async function POST(request: NextRequest) {\n  const token = request.headers.get('Authorization')?.split(' ')[1] ?? '';\n\n  if (!token) {\n    throw new Error('Unauthenticated');\n  }\n\n  await verifyIdToken(token, {\n    referer: getReferer(request.headers)\n  });\n\n  //...\n}\n```\n"
  },
  {
    "path": "docs/pages/docs/usage/firebase-hosting.mdx",
    "content": "# Usage in Firebase Hosting Environment\n\nBy default, the Firebase Hosting environment strips all cookies except for `__session`. (See [this StackOverflow thread](https://stackoverflow.com/questions/44929653/firebase-cloud-function-wont-store-cookie-named-other-than-session) for more details.)\n\nTo use `next-firebase-auth-edge` in Firebase Hosting, you need to set a custom `cookieName` with the value `__session`, as shown in the examples below:\n\n> **Next.js 14-15:** Use `middleware.ts` with `export async function middleware(...)` instead of `proxy.ts`. See the [compatibility note](/docs/usage/middleware#nextjs-14-15-compatibility).\n\n```tsx filename=\"proxy.ts\"\nimport { NextRequest, NextResponse } from \"next/server\";\nimport { authMiddleware } from \"next-firebase-auth-edge\";\n\nexport async function proxy(request: NextRequest) {\n  return authMiddleware(request, {\n    cookieName: \"__session\", // This needs to be \"__session\" to work inside Firebase Hosting\n\n    loginPath: \"/api/login\",\n    logoutPath: \"/api/logout\",\n    apiKey: \"XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX\",\n    cookieSignatureKeys: [\"Key-Should-Be-at-least-32-bytes-in-length\"],\n    cookieSerializeOptions: {\n      path: \"/\",\n      httpOnly: true,\n      secure: false,\n      sameSite: \"lax\" as const,\n      maxAge: 12 * 60 * 60 * 24,\n    },\n    serviceAccount: {\n      projectId: \"your-firebase-project-id\",\n      clientEmail: \"firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com\",\n      privateKey: \"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n\",\n    },\n  });\n}\n\nexport const config = {\n  matcher: [\"/api/login\", \"/api/logout\", \"/\", \"/((?!_next|favicon.ico|api|.*\\\\.).*)\"],\n};\n```\n\nExample [getTokens](/docs/usage/server-components) usage:\n\n```tsx\nimport { getTokens } from \"next-firebase-auth-edge\";\n\nconst tokens = await getTokens(context.req.cookies, {\n  apiKey: 'XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX',\n  cookieName: '__session',\n  cookieSignatureKeys: ['Key-Should-Be-at-least-32-bytes-in-length'],\n  serviceAccount: {\n    projectId: \"your-firebase-project-id\",\n    clientEmail: \"firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com\",\n    privateKey: \"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n\",\n  },\n});\n```\n"
  },
  {
    "path": "docs/pages/docs/usage/get-server-side-props.mdx",
    "content": "# Usage in getServerSideProps\n\nExample usage of [getApiRequestTokens](/docs/usage/pages-router-api-routes) function in [getServerSideProps](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props)\n\n```tsx\nimport { GetServerSidePropsContext } from \"next\";\nimport { getApiRequestTokens } from \"next-firebase-auth-edge\";\n\nexport async function getServerSideProps(context: GetServerSidePropsContext) {\n  const tokens = await getApiRequestTokens(context.req, {\n    apiKey: 'XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX',\n    cookieName: 'AuthToken',\n    cookieSignatureKeys: ['Key-Should-Be-at-least-32-bytes-in-length'],\n    serviceAccount: {\n      projectId: 'your-firebase-project-id',\n      clientEmail: 'firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com',\n      privateKey: '-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n'\n    },\n    // Optional\n    tenantId: 'your-tenant-id',\n  });\n\n  return { props: { tokens } };\n}\n```\n"
  },
  {
    "path": "docs/pages/docs/usage/index.mdx",
    "content": "import {Card, Cards} from 'nextra-theme-docs';\nimport {ChevronRightIcon} from '@heroicons/react/24/outline';\n\n# Usage Guide\n\nThis page provides comprehensive documentation for the `next-firebase-auth-edge` functions and their use cases.\n\nIf you prefer a more hands-on learning experience, you can explore these example applications instead:\n\n- [Starter Example](/examples#starter)\n- [Minimal Example](/examples#minimal)\n\n## Sections\n\n<Cards num={2}>\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Authentication Middleware\"\n    href=\"/docs/usage/middleware\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Server Components\"\n    href=\"/docs/usage/server-components\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Redirect Helper Functions\"\n    href=\"/docs/usage/redirect-functions\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"App Router API Route Handlers\"\n    href=\"/docs/usage/app-router-api-routes\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Pages Router API Routes\"\n    href=\"/docs/usage/pages-router-api-routes\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Usage in getServerSideProps\"\n    href=\"/docs/usage/get-server-side-props\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Refreshing credentials\"\n    href=\"/docs/usage/refresh-credentials\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Removing credentials\"\n    href=\"/docs/usage/remove-credentials\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Using Client-Side APIs\"\n    href=\"/docs/usage/client-side-apis\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Advanced usage\"\n    href=\"/docs/usage/advanced-usage\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Usage in Google Cloud Run\"\n    href=\"/docs/usage/cloud-run\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Usage in Firebase Hosting\"\n    href=\"/docs/usage/firebase-hosting\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Debug mode\"\n    href=\"/docs/usage/debug-mode\"\n  />\n  <Card\n    icon={<ChevronRightIcon />}\n    title=\"Firebase API Key domain restriction\"\n    href=\"/docs/usage/domain-restriction\"\n  />\n</Cards>\n"
  },
  {
    "path": "docs/pages/docs/usage/middleware.mdx",
    "content": "# Authentication Middleware\n\nThe `authMiddleware` works with the [getTokens](/docs/usage/server-components) function to share valid user credentials between [Next.js Proxy](https://nextjs.org/docs/app/getting-started/proxy) (or [Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware) in Next.js 14-15) and [Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components).\n\n## Key Features\n\n1. Sets up `/api/login` and `/api/logout` endpoints for managing browser authentication cookies. You don't need to create these API routes yourself—the middleware handles it for you. You can rename these endpoints by adjusting the `loginPath` and `logoutPath` options in the middleware settings.\n2. Automatically refreshes browser authentication cookies when the token expires, allowing developers to handle it accordingly.\n3. Signs user cookies with rotating keys to reduce the risk of cryptanalysis attacks.\n4. Validates user cookies on every request.\n5. Provides flexibility by allowing you to define custom behavior using the `handleValidToken`, `handleInvalidToken`, and `handleError` callbacks.\n\n## Next.js 14-15 Compatibility\n\nStarting from Next.js 16, there are two key API changes:\n\n1. **`middleware.ts` has been renamed to `proxy.ts`** and the exported function should be named `proxy` instead of `middleware`. The proxy runs on the Node.js runtime instead of the Edge runtime.\n2. **Async `cookies()` and `headers()` are now mandatory.** Next.js 15 introduced async versions of `cookies()` and `headers()` with temporary synchronous backward compatibility. In Next.js 16, the synchronous fallback is removed — you must use `await cookies()` and `await headers()`.\n\nAll code examples in the documentation use the **Next.js 15-16** convention with `await cookies()` and `await headers()`.\n\nIf you are using **Next.js 14 or 15**, replace `proxy.ts` with `middleware.ts` and `export async function proxy(...)` with `export async function middleware(...)`. If you are using **Next.js 14**, you should also remove the `await` before `cookies()` and `headers()` calls, as these functions are synchronous in Next.js 14.\n\nThe `authMiddleware` function and its options remain the same across all versions.\n\n## Advanced usage\n\nAdvanced usage of `authMiddleware` in `proxy.ts`, based on [starter example](/examples#starter):\n\n```tsx filename=\"proxy.ts\"\nimport {NextResponse} from 'next/server';\nimport type {NextRequest} from 'next/server';\nimport {\n  authMiddleware,\n  redirectToHome,\n  redirectToLogin\n} from 'next-firebase-auth-edge';\n\nconst PUBLIC_PATHS = ['/register', '/login', '/reset-password'];\n\nexport async function proxy(request: NextRequest) {\n  return authMiddleware(request, {\n    loginPath: '/api/login',\n    logoutPath: '/api/logout',\n    apiKey: 'XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX',\n    cookieName: 'AuthToken',\n    cookieSignatureKeys: ['Key-Should-Be-at-least-32-bytes-in-length'],\n    cookieSerializeOptions: {\n      path: '/',\n      httpOnly: true,\n      secure: false, // Set this to true on HTTPS environments\n      sameSite: 'lax' as const,\n      maxAge: 12 * 60 * 60 * 24 // twelve days\n    },\n    serviceAccount: {\n      projectId: 'your-firebase-project-id',\n      clientEmail:\n        'firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com',\n      privateKey:\n        '-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n'\n    },\n    enableMultipleCookies: true,\n    enableCustomToken: false,\n    debug: true,\n    tenantId: 'your-tenant-id',\n    checkRevoked: true,\n    authorizationHeaderName: 'Authorization',\n    dynamicCustomClaimsKeys: ['someCustomClaim'],\n    handleValidToken: async ({token, decodedToken}, headers) => {\n      // Authenticated user should not be able to access /login, /register and /reset-password routes\n      if (PUBLIC_PATHS.includes(request.nextUrl.pathname)) {\n        return redirectToHome(request);\n      }\n\n      return NextResponse.next({\n        request: {\n          headers\n        }\n      });\n    },\n    handleInvalidToken: async (reason) => {\n      console.info('Missing or malformed credentials', {reason});\n\n      return redirectToLogin(request, {\n        path: '/login',\n        publicPaths: PUBLIC_PATHS\n      });\n    },\n    handleError: async (error) => {\n      console.error('Unhandled authentication error', {error});\n\n      return redirectToLogin(request, {\n        path: '/login',\n        publicPaths: PUBLIC_PATHS\n      });\n    },\n    getMetadata: async (tokens: TokenSet) => {\n      // Here you can load any data related to the user\n      // The data will be saved in cookies and can be accessed using `getTokens` function.\n      // Note: The cookie size is limited, so keep the data compact\n      return {uid: tokens.decodedIdToken.uid, timestamp: new Date().getTime()};\n    },\n    enableTokenRefreshOnExpiredKidHeader: true\n  });\n}\n\nexport const config = {\n  matcher: [\n    '/api/login',\n    '/api/logout',\n    '/',\n    '/((?!_next|favicon.ico|api|.*\\\\.)*)'\n  ]\n};\n```\n\n## Metadata\n\nStarting from v1.10.0, Authentication Middleware allows **you** to store custom data within cookies. The data is signed and verified together with the token, so it's safe to use it to, for example, load user permissions from the database.\n\nExample usage:\n\n```tsx filename=\"proxy.ts\"\nexport async function proxy(request: NextRequest) {\n  return authMiddleware(request, {\n    // Store additional data in the cookies\n    getMetadata: async (tokens: TokenSet) => {\n      const roles = await loadRolesFromDb();\n      return {roles};\n    }\n    // ...other options\n  });\n}\n```\n\nAccess the data using [getTokens](/docs/usage/server-components):\n\n```tsx\nconst {\n  metadata: {roles}\n} = await getTokens(await cookies(), authConfig);\n```\n\n## Custom Token\n\nStarting from v1.8.0, **custom token is no longer enabled by default**. If you wish to enable custom token, set `enableCustomToken` option to `true` in `authMiddleware`.\n\nCustom token introduces a significant footprint on the size of authentication cookie and is not required for most use-cases.\n\nIt's recommended to use `enableCustomToken` together with `enableMultipleCookies`. `enableMultipleCookies` would split session into multiple cookies, eliminating issues that can come from cookie size, as explained below.\n\n## Multiple Cookies\n\nStarting from v1.6.0, the `authMiddleware` supports the `enableMultipleCookies` option.\n\nBy default, the session data is stored in a single cookie. This works for most cases, but it limits the size of custom claims you can add to a token. Most browsers won't store a cookie larger than [4096 bytes](https://support.convert.com/hc/en-us/articles/4511582623117-Cookie-size-limits-and-the-impact-on-the-use-of-Convert-goals).\n\nTo prevent cookie size issues, it's recommended to set `enableMultipleCookies` to `true`.\n\nWhen enabled, the session will be split into four cookies:\n\n- `${cookieName}.id` – stores the `idToken`\n- `${cookieName}.refresh` – stores the `refreshToken`\n- `${cookieName}.custom` – stores the `customToken`\n- `${cookieName}.sig` – stores the `signature` used to validate the tokens\n\n### Firebase Hosting and Multiple Cookies\n\nIf you're using [Firebase Hosting](/docs/usage/firebase-hosting), set `enableMultipleCookies` to `false`.\n\nDue to an issue with Firebase Hosting ([details here](https://stackoverflow.com/questions/44929653/firebase-cloud-function-wont-store-cookie-named-other-than-session)), multiple cookies are not supported.\n\nIf you run into cookie size problems on Firebase Hosting, you may want to either reduce the size of custom claims in your tokens or consider switching to a different hosting provider.\n\n## Middleware Token Verification Caching\n\nSince v0.9.0, the `handleValidToken` function is called with modified request `headers` as a second parameter.\n\nYou can pass this headers object to `NextResponse.next({ request: { headers } })` to enable token verification caching.\n\nFor more details on modifying request headers in middleware, check out [Modifying Request Headers in Middleware](https://vercel.com/templates/next.js/edge-functions-modify-request-header).\n\nThe example below shows a simplified version of how you can combine other middleware with `next-firebase-auth-edge`.\n\n```tsx\nhandleValidToken: async ({token, decodedToken, customToken}, headers) => {\n  return NextResponse.next({\n    request: {\n      headers // Pass modified request headers to skip token verification in subsequent getTokens and getApiRequestTokens calls\n    }\n  });\n};\n```\n\n## Usage with `next-intl` and other middlewares\n\n```tsx filename=\"proxy.ts\"\nimport type {NextRequest} from 'next/server';\nimport createIntlMiddleware from 'next-intl/middleware';\nimport {authMiddleware} from 'next-firebase-auth-edge';\n\nconst intlMiddleware = createIntlMiddleware({\n  locales: ['en', 'pl'],\n  defaultLocale: 'en'\n});\n\nexport async function proxy(request: NextRequest) {\n  return authMiddleware(request, {\n    // ...\n    handleValidToken: async (tokens) => {\n      return intlMiddleware(request);\n    },\n    handleInvalidToken: async (reason) => {\n      return intlMiddleware(request);\n    },\n    handleError: async (error) => {\n      return intlMiddleware(request);\n    }\n  });\n}\n```\n\n**Note:** When using `next-intl` middleware, you don't need to pass `headers` like you would in [middleware token verification caching](https://next-firebase-auth-edge-docs.vercel.app/docs/usage/middleware#middleware-token-verification-caching). By the time we call `intlMiddleware`, the `request` already has the updated headers. `next-intl` will handle passing the modified headers for you.\n\nIf you're experiencing issues with redirects while using `next-intl` middleware, check out [this comment on GitHub](https://github.com/awinogrodzki/next-firebase-auth-edge/issues/169#issuecomment-2076683598) for code examples.\n\n## Options\n\n| Name                                 | Required?                                                                                                       | Type/Default                                                                                                                                                    | Description                                                                                                                                                                                                                                                                                                                                                                                                       |\n| ------------------------------------ | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| loginPath                            | **Required**                                                                                                    |                                                                                                                                                                 | Defines the API login endpoint. When called with a Firebase auth token from the client, it responds with `Set-Cookie` headers containing signed ID and refresh tokens.                                                                                                                                                                                                                                            |\n| logoutPath                           | **Required**                                                                                                    |                                                                                                                                                                 | Defines the API logout endpoint. When called from the client, it returns empty `Set-Cookie` headers that clear any previously set credentials.                                                                                                                                                                                                                                                                    |\n| apiKey                               | **Required**                                                                                                    |                                                                                                                                                                 | The Firebase project API key, used to fetch Firebase ID and refresh tokens.                                                                                                                                                                                                                                                                                                                                       |\n| cookieName                           | **Required**                                                                                                    |                                                                                                                                                                 | The name of the cookie set by the `loginPath` API route.                                                                                                                                                                                                                                                                                                                                                          |\n| cookieSignatureKeys                  | **Required**                                                                                                    |                                                                                                                                                                 | [Rotating keys](https://developer.okta.com/docs/concepts/key-rotation/#:~:text=Key%20rotation%20is%20when%20a,and%20follows%20cryptographic%20best%20practices.) used to validate the cookie.                                                                                                                                                                                                                     |\n| cookieSerializeOptions               | **Required**                                                                                                    |                                                                                                                                                                 | Defines additional options for the `Set-Cookie` headers.                                                                                                                                                                                                                                                                                                                                                          |\n| serviceAccount                       | Optional in authenticated [Google Cloud Run](https://cloud.google.com/run) environments. Otherwise **required** |                                                                                                                                                                 | The Firebase project service account.                                                                                                                                                                                                                                                                                                                                                                             |\n| enableMultipleCookies                | Optional                                                                                                        | `boolean`, defaults to `false`                                                                                                                                  | Splits session tokens into multiple cookies to increase token claims capacity. Recommended, but defaults to `false` for backwards compatibility. Set to `false` on Firebase Hosting due to limitations like [this](https://stackoverflow.com/questions/44929653/firebase-cloud-function-wont-store-cookie-named-other-than-session).                                                                              |\n| enableCustomToken                    | Optional                                                                                                        | `boolean`, defaults to `false`                                                                                                                                  | If enabled, authentication cookie would contain custom token. This is helpful if you want to use [signInWithCustomToken](https://firebase.google.com/docs/auth/web/custom-auth) Firebase Client SDK method                                                                                                                                                                                                        |\n| tenantId                             | Optional                                                                                                        | `string`, defaults to `undefined`                                                                                                                               | The Google Cloud Platform tenant identifier. Specify if your project supports [multi-tenancy](https://cloud.google.com/identity-platform/docs/multi-tenancy-authentication).                                                                                                                                                                                                                                      |\n| authorizationHeaderName              | Optional                                                                                                        | `string`, defaults to `Authorization`                                                                                                                           | The name of the authorization header expected by the login endpoint.                                                                                                                                                                                                                                                                                                                                              |\n| checkRevoked                         | Optional                                                                                                        | `boolean`, defaults to `false`                                                                                                                                  | If `true`, validates the token against the Firebase server on every request. Unless there's a specific need, it's usually better not to use this.                                                                                                                                                                                                                                                                 |\n| handleValidToken                     | Optional                                                                                                        | `(tokens: { token: string, decodedToken: DecodedIdToken, customToken?: string }, headers: Headers) => Promise<NextResponse>`, defaults to `NextResponse.next()` | Called when a valid token is received. Should return a promise resolving with `NextResponse`. It's passed modified request `headers` as a second parameter, which, if forwarded with `NextResponse.next({ request: { headers } })`, prevents re-verification of the token in subsequent calls, improving response times.                                                                                          |\n| handleInvalidToken                   | Optional                                                                                                        | `(reason: InvalidTokenReason) => Promise<NextResponse>`, defaults to `NextResponse.next()`                                                                      | Called when a request is unauthenticated (either missing credentials or has invalid credentials). Can be used to redirect users to a specific page. The `reason` argument can be one of `MISSING_CREDENTIALS`, `MISSING_REFRESH_TOKEN`, `MALFORMED_CREDENTIALS`, `INVALID_SIGNATURE`, `INVALID_KID` or `INVALID_CREDENTIALS`. See the [handleInvalidToken section](/docs/errors#handleinvalidtoken) for details on each reason. |\n| handleError                          | Optional                                                                                                        | `(error: AuthError) => Promise<NextResponse>`, defaults to `NextResponse.next()`                                                                                | Called when an unhandled error occurs during authentication. By default, the app will render, but you can customize error handling here. See the [handleError section](/docs/errors#handleerror) for more information on possible errors.                                                                                                                                                                         |\n| getMetadata                          | Optional                                                                                                        | `(tokens: TokenSet) => Promise<object>`                                                                                                                         | Called when user signs in or the credentials are refreshed. Can be used to load user data from external sources and save it inside the cookies. Metadata can be accessed using `metadata` property returned by [getTokens](/docs/usage/server-components)                                                                                                                                                         |\n| debug                                | Optional                                                                                                        | `boolean`, defaults to `false`                                                                                                                                  | Enables helpful logs for better understanding and debugging of the authentication flow.                                                                                                                                                                                                                                                                                                                           |\n| dynamicCustomClaimsKeys              | Optional                                                                                                        | `string[]`, defaults to `undefined`                                                                                                                             | By default, when you update custom user claims using `setCustomUserClaims`, the changes will only take effect after the user logs in again. If you want the updated claim to be available immediately after refreshing the token (e.g., after handling `refreshNextResponseCookies`), you need to include the claim key in `dynamicCustomClaimsKeys`.                                                             |\n| enableTokenRefreshOnExpiredKidHeader | Optional                                                                                                        | `boolean`, defaults to `true`                                                                                                                                   | By default, [when Google public keys expire](https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_a_third-party_jwt_library) the token is refreshed automatically. Set `false` to opt-out from automatic refresh. When opted-out, `authMiddleware` will call [handleInvalidToken](/docs/errors#handleinvalidtoken) with `INVALID_KID` reason, allowing user to be safely redirected to sign in page.                |\n"
  },
  {
    "path": "docs/pages/docs/usage/pages-router-api-routes.mdx",
    "content": "# Pages Router API Routes\n\nTo support gradual adoption of the latest Next.js features, `next-firebase-auth-edge` offers the `getApiRequestTokens` function. This function is designed to work with [getServerSideProps](/docs/usage/get-server-side-props) and [API Routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes).\n\nThe `getApiRequestTokens` function works similarly to `getTokens`, but it's specifically for extracting cookie information from the `req` object.\n\n```tsx\nimport { NextApiRequest, NextApiResponse } from \"next\";\nimport { getApiRequestTokens } from \"next-firebase-auth-edge\";\n\nexport default async function handler(\n  req: NextApiRequest,\n  res: NextApiResponse\n) {\n  const tokens = await getApiRequestTokens(req, {\n    apiKey: 'XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX',\n    cookieName: 'AuthToken',\n    cookieSignatureKeys: ['Key-Should-Be-at-least-32-bytes-in-length'],\n    serviceAccount: {\n      projectId: 'your-firebase-project-id',\n      clientEmail: 'firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com',\n      privateKey: '-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n'\n    }\n  });\n\n  if (!tokens) {\n    throw new Error('Unauthenticated');\n  }\n\n  return res.status(200).json({ tokens });\n}\n```\n"
  },
  {
    "path": "docs/pages/docs/usage/redirect-functions.mdx",
    "content": "# Redirect Helper Functions\n\nThis library provides a set of helper functions to simplify handling common redirect scenarios.\n\nThese redirect functions make it easier to create a [redirect response in Next.js Middleware](https://nextjs.org/docs/app/building-your-application/routing/redirecting#nextresponseredirect-in-middleware).\n\nFor example, here’s how the `redirectToPath` helper function works:\n\n```ts\nexport function redirectToPath(\n  request: NextRequest,\n  path: string,\n  options: RedirectToPathOptions = {shouldClearSearchParams: false}\n) {\n  const url = request.nextUrl.clone();\n  url.pathname = path;\n\n  if (options.shouldClearSearchParams) {\n    url.search = '';\n  }\n\n  return NextResponse.redirect(url);\n}\n```\n\nIt’s a straightforward function that builds a `NextResponse` object for handling redirects.\n\n**Note:** You don’t have to use the library's redirect functions—you can always implement your own redirect logic if it better suits your needs.\n\n## redirectToPath\n\nExample usage\n\n```ts\nimport {redirectToPath} from 'next-firebase-auth-edge';\n```\n\n```ts\nredirectToPath(request, '/dashboard', {shouldClearSearchParams: true});\n```\n\n## redirectToHome\n\n`redirectToHome` is a simplified version of `redirectToPath` that redirects the user to the home page (`/`).\n\n```ts\nimport {redirectToHome} from 'next-firebase-auth-edge';\n```\n\n```ts\nredirectToHome(request);\n```\n\n## redirectToLogin\n\n`redirectToLogin` redirects unauthenticated users to a public login page. \n\n### Public paths\n\n`redirectToLogin` will skip the redirect if the request matches one of the paths listed in `publicPaths`.\n\n```ts\nimport {redirectToLogin} from 'next-firebase-auth-edge';\n```\n\n```ts\nredirectToLogin(request, {\n  path: '/sign-in',\n  publicPaths: ['/sign-in', '/register', /^\\/post\\/(\\w+)/]\n});\n```\n\n### Private paths\n\n`redirectToLogin` will skip the redirect if the request does not match one of the paths listed in `privatePaths`.\n\n```ts\nimport {redirectToLogin} from 'next-firebase-auth-edge';\n```\n\n```ts\nredirectToLogin(request, {\n  path: '/sign-in',\n  privatePaths: ['/dashboard', /^\\/settings\\/(\\w+)/]\n});\n```"
  },
  {
    "path": "docs/pages/docs/usage/refresh-credentials.mdx",
    "content": "# Refreshing Credentials\n\n`next-firebase-auth-edge` provides three different functions for updating user credentials after changes to the user token structure (e.g., adding new user claims).\n\n## Refresh Credentials in Proxy/Middleware\n\nTo refresh credentials in [Next.js Proxy](https://nextjs.org/docs/app/getting-started/proxy) (or [Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware) in Next.js 14-15) after updating the user token, use the `refreshCredentials` function from `next-firebase-auth-edge/next/cookies`.\n\n> **Next.js 14-15:** Use `middleware.ts` with `export async function middleware(...)` instead of `proxy.ts`. See the [compatibility note](/docs/usage/middleware#nextjs-14-15-compatibility).\n\n```tsx filename=\"proxy.ts\"\nimport type {NextRequest} from 'next/server';\nimport {authMiddleware} from 'next-firebase-auth-edge';\nimport {refreshCredentials} from 'next-firebase-auth-edge/next/cookies';\n\nconst commonOptions = {\n  apiKey: 'XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX',\n  cookieName: 'AuthToken',\n  cookieSignatureKeys: ['Key-Should-Be-at-least-32-bytes-in-length'],\n  cookieSerializeOptions: {\n    path: '/',\n    httpOnly: true,\n    secure: false, // Set this to true on HTTPS environments\n    sameSite: 'strict' as const,\n    maxAge: 12 * 60 * 60 * 24 // twelve days\n  },\n  serviceAccount: {\n    projectId: 'your-firebase-project-id',\n    clientEmail:\n      'firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com',\n    privateKey: '-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n'\n  }\n};\n\nexport async function proxy(request: NextRequest) {\n  return authMiddleware(request, {\n    handleValidToken: async ({decodedToken}, headers) => {\n      const shouldRefreshCredentials =\n        await makeSomeComputationsToDeduceIfUserCredentialsShouldBeUpdated(\n          decodedToken.uid\n        );\n\n      if (shouldRefreshCredentials) {\n        return refreshCredentials(\n          request,\n          commonOptions,\n          ({headers, tokens}) => {\n            // Optionally perform additional verification on refreshed `tokens`...\n\n            return NextResponse.next({\n              request: {\n                headers\n              }\n            });\n          }\n        );\n      }\n\n      return NextResponse.next({\n        request: {\n          headers\n        }\n      });\n    },\n    ...commonOptions\n  });\n}\n\nexport const config = {\n  matcher: [\n    '/',\n    '/((?!_next|favicon.ico|api|.*\\\\.).*)',\n    '/api/login',\n    '/api/logout'\n  ]\n};\n```\n\n### refreshCredentials\n\nThe `refreshCredentials` function is useful when you need to update user credentials after some asynchronous action that affects the user token structure (e.g., a cron job or event queue that updates custom claims).\n\nIn this example, `makeSomeComputationsToDeduceIfUserCredentialsShouldBeUpdated` is a fictional function used to quickly check if user credentials need updating. For example, it might check a distributed cache.\n\nWhen you call `refreshCredentials`, it performs three actions:\n\n1. It generates a new token based on the existing credentials, including any updated claims.\n2. It allows the developer to create a new `NextResponse` with [Modified Request Headers](https://vercel.com/templates/next.js/edge-functions-modify-request-header). Passing the modified request headers ensures that `getTokens` will return the fresh token within **a single request**.\n3. It updates the `NextResponse` with `Set-Cookie` headers that contain the latest credentials.\n\nHere’s the function signature:\n\n```tsx\nasync function refreshCredentials<Metadata extends object>(\n  request: NextRequest,\n  options: SetAuthCookiesOptions<Metadata>,\n  responseFactory: (options: {\n    headers: Headers;\n    tokens: VerifiedCookies;\n    metadata: Metadata;\n  }) => NextResponse | Promise<NextResponse>\n): Promise<NextResponse>;\n```\n\n#### Response Factory Options\n\n| Name     | Description                                                                                                                                                                                                               |\n| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| headers  | [Modified Request Headers](https://vercel.com/templates/next.js/edge-functions-modify-request-header). Passing these modified headers ensures that `getTokens` will return the updated token within **a single request**. |\n| tokens   | A `VerifiedCookies` object, containing the values for `idToken`, `refreshToken`, and `decodedIdToken`. It also returns `customToken` if you passed `enableCustomToken` flag to `authMiddleware`.                          |\n| metadata | A `Metadata` object. `Metadata` is whatever `getMetadata` callback provided to `authMiddleware` resolves with.                                                                                                            |\n\n## Refresh Auth Cookies in API Route Handlers\n\nTo refresh authentication cookies after updating a user token in [API Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers), use `refreshNextResponseCookies` from `next-firebase-auth-edge/next/cookies`.\n\n```tsx\nimport {NextResponse} from 'next/server';\nimport type {NextRequest} from 'next/server';\nimport {refreshNextResponseCookies} from 'next-firebase-auth-edge/next/cookies';\nimport {getFirebaseAuth, getTokens} from 'next-firebase-auth-edge';\n\nconst commonOptions = {\n  apiKey: 'XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX',\n  cookieName: 'AuthToken',\n  cookieSignatureKeys: ['Key-Should-Be-at-least-32-bytes-in-length'],\n  cookieSerializeOptions: {\n    path: '/',\n    httpOnly: true,\n    secure: false, // Set this to true on HTTPS environments\n    sameSite: 'strict' as const,\n    maxAge: 12 * 60 * 60 * 24 // twelve days\n  },\n  serviceAccount: {\n    projectId: 'your-firebase-project-id',\n    clientEmail:\n      'firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com',\n    privateKey: '-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n'\n  }\n};\n\nconst {setCustomUserClaims, getUser} = getFirebaseAuth({\n  serviceAccount: commonOptions.serviceAccount,\n  apiKey: commonOptions.apiKey\n});\n\nexport async function POST(request: NextRequest) {\n  const tokens = await getTokens(request.cookies, commonOptions);\n\n  if (!tokens) {\n    throw new Error('Cannot update custom claims of unauthenticated user');\n  }\n\n  await setCustomUserClaims(tokens.decodedToken.uid, {\n    someCustomClaim: {\n      updatedAt: Date.now()\n    }\n  });\n\n  const user = await getUser(tokens.decodedToken.uid);\n  const response = new NextResponse(\n    JSON.stringify({\n      customClaims: user.customClaims\n    }),\n    {\n      status: 200,\n      headers: {'content-type': 'application/json'}\n    }\n  );\n\n  return refreshNextResponseCookies(request, response, commonOptions);\n}\n```\n\n## Refresh Auth Cookies in API Route Handlers with an ID Token Extracted from the Authorization Header\n\nTo refresh authentication cookies using the token string extracted from the Authorization header, use `refreshNextResponseCookiesWithToken` from `next-firebase-auth-edge/next/cookies`.\n\n```tsx\nimport type {NextRequest} from 'next/server';\nimport {refreshNextResponseCookiesWithToken} from 'next-firebase-auth-edge/next/cookies';\n\nconst commonOptions = {\n  apiKey: 'XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX',\n  cookieName: 'AuthToken',\n  cookieSignatureKeys: ['Key-Should-Be-at-least-32-bytes-in-length'],\n  cookieSerializeOptions: {\n    path: '/',\n    httpOnly: true,\n    secure: false, // Set this to true on HTTPS environments\n    sameSite: 'strict' as const,\n    maxAge: 12 * 60 * 60 * 24 // twelve days\n  },\n  serviceAccount: {\n    projectId: 'your-firebase-project-id',\n    clientEmail:\n      'firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com',\n    privateKey: '-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n'\n  }\n};\n\nexport async function POST(request: NextRequest) {\n  const token = request.headers.get('Authorization')?.split(' ')[1] ?? '';\n\n  if (!token) {\n    throw new Error('Unauthenticated');\n  }\n\n  return refreshNextResponseCookiesWithToken(\n    token,\n    request,\n    response,\n    commonOptions\n  );\n}\n```\n\n## Refresh Auth Cookies in Pages Router API Routes\n\nTo refresh authentication cookies after updating a user token in [API Routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes), use `refreshApiResponseCookies` from `next-firebase-auth-edge/next/api`.\n\n```tsx\nimport {NextApiRequest, NextApiResponse} from 'next';\nimport {refreshApiResponseCookies} from 'next-firebase-auth-edge/next/api';\n\nexport default async function handler(\n  req: NextApiRequest,\n  res: NextApiResponse\n) {\n  await refreshApiResponseCookies(req, res, {\n    serviceAccount: {\n      projectId: 'your-firebase-project-id',\n      clientEmail:\n        'firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com',\n      privateKey:\n        '-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n'\n    },\n    apiKey: 'XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX',\n    cookieName: 'AuthToken',\n    cookieSignatureKeys: ['Key-Should-Be-at-least-32-bytes-in-length'],\n    cookieSerializeOptions: {\n      path: '/',\n      httpOnly: true,\n      secure: false, // Set this to true on HTTPS environments\n      sameSite: 'strict' as const,\n      maxAge: 12 * 60 * 60 * 24 // twelve days\n    }\n  });\n\n  res.status(200).json({example: true});\n}\n```\n\n## Refresh Auth Cookies in Server Actions\n\nTo refresh authentication cookies after updating user credentials in [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations), use `refreshServerCookies` from `next-firebase-auth-edge/next/cookies`.\n\n```tsx\n'use server';\n\nimport {cookies, headers} from 'next/headers';\nimport {getTokens} from 'next-firebase-auth-edge';\nimport {refreshServerCookies} from 'next-firebase-auth-edge/next/cookies';\n\nconst commonOptions = {\n  apiKey: 'XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX',\n  cookieName: 'AuthToken',\n  cookieSignatureKeys: ['Key-Should-Be-at-least-32-bytes-in-length'],\n  cookieSerializeOptions: {\n    path: '/',\n    httpOnly: true,\n    secure: false, // Set this to true on HTTPS environments\n    sameSite: 'strict' as const,\n    maxAge: 12 * 60 * 60 * 24 // twelve days\n  },\n  serviceAccount: {\n    projectId: 'your-firebase-project-id',\n    clientEmail:\n      'firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com',\n    privateKey: '-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n'\n  }\n};\n\nexport async function performServerAction() {\n  const tokens = await getTokens(await cookies(), commonOptions);\n\n  if (!tokens) {\n    throw new Error('Unauthenticated');\n  }\n\n  // headers() needs to be wrapped with new Headers() to work in Server Actions\n  await refreshServerCookies(\n    await cookies(),\n    new Headers(await headers()),\n    commonOptions\n  );\n}\n```\n"
  },
  {
    "path": "docs/pages/docs/usage/remove-credentials.mdx",
    "content": "# Removing Credentials\n\nThe `next-firebase-auth-edge` library provides a `removeCookies` method to remove authenticated cookies within Middleware. This is useful for situations where you want to explicitly log a user out of the app.\n\n## Removing Credentials in Middleware or API routes\n\nTo remove authenticated cookies in Middleware or API routes, use the `removeCookies` function from `next-firebase-auth-edge/next/cookies`. This will attach expired `Set-Cookie` headers to the response, prompting the browser to delete the authenticated cookies.\n\n```tsx\nimport {NextRequest, NextResponse} from 'next/server';\nimport {removeCookies} from 'next-firebase-auth-edge/next/cookies';\n\n//...\nfunction forceLogout(request: NextRequest) {\n  const response = NextResponse.redirect(new URL('/login', request.url));\n\n  removeCookies(request.headers, response, {\n    cookieName: 'AuthToken',\n    cookieSerializeOptions: {\n      path: '/',\n      httpOnly: true,\n      secure: false,\n      sameSite: 'lax' as const,\n      maxAge: 12 * 60 * 60 * 24\n    }\n  });\n\n  return response;\n}\n```\n\n## Removing Credentials in Server Actions\n\nTo remove authenticated cookies in Server Actions, use the `removeServerCookies` function from `next-firebase-auth-edge/next/cookies`. This will remove authentication cookies using `cookies.delete` method on Next.js cookies object\n\n```tsx\nimport {NextRequest, NextResponse} from 'next/server';\nimport {cookies} from 'next/headers';\nimport {removeServerCookies} from 'next-firebase-auth-edge/next/cookies';\n\n// Since Next.js 15, `cookies()` returns a Promise and must be preceded with `await`.\n// In Next.js 14, `cookies()` is synchronous — call it without `await`.\nremoveServerCookies(await cookies(), {\n  cookieName: 'AuthToken',\n});\n```\n"
  },
  {
    "path": "docs/pages/docs/usage/server-components.mdx",
    "content": "# Usage in Server Components\n\nThe library provides a `getTokens` function to extract and validate user credentials. This function can only be used in `Server Components` or [API Route Handlers](/docs/usage/app-router-api-routes). It returns `null` if there are no authentication cookies or if the credentials have expired. If the request contains valid credentials, the function returns an object with `token`, `decodedToken`. The object can contain `customToken`, if you passed `enableCustomToken` flag to `authMiddleware`. The `token` is a JWT-encoded string, while `decodedToken` is the decoded object representation of that token.\n\n## getTokens\n\nHere’s an example of how to use the `getTokens` function from `next-firebase-auth-edge`:\n\n```tsx\nimport {getTokens} from 'next-firebase-auth-edge';\nimport {cookies, headers} from 'next/headers';\nimport {notFound} from 'next/navigation';\n\nexport default async function ServerComponentExample() {\n  // Since Next.js 15, `cookies()` returns a Promise and must be preceded with `await`.\n  // In Next.js 14, `cookies()` is synchronous — use `getTokens(cookies(), ...)` without `await` on `cookies()`.\n  const tokens = await getTokens(await cookies(), {\n    apiKey: 'XXxxXxXXXxXxxxxx_XxxxXxxxxxXxxxXXXxxXxX',\n    cookieName: 'AuthToken',\n    cookieSignatureKeys: ['Key-Should-Be-at-least-32-bytes-in-length'],\n    serviceAccount: {\n      projectId: 'your-firebase-project-id',\n      clientEmail:\n        'firebase-adminsdk-nnw48@your-firebase-project-id.iam.gserviceaccount.com',\n      privateKey:\n        '-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n'\n    },\n    tenantId: 'your-tenant-id'\n  });\n\n  if (!tokens) {\n    return notFound();\n  }\n\n  const {token, decodedToken, customToken, metadata} = tokens;\n\n  return (\n    <div style={{wordBreak: 'break-word', width: '600px'}}>\n      <p>\n        Valid token: {token}\n        <br />\n        User email: {decodedToken.email}\n        <br />\n        Custom token, if you enabled custom token support by passing `enableCustomToken` flag to `authMiddleware`: {customToken}\n        <br />\n        Metadata:\n        <pre>\n          {JSON.stringify(metadata, undefined, 2)}\n        </pre>\n      </p>\n    </div>\n  );\n}\n```\n\n### Required Options\n\n| Name                | Description                                                                                                                                                                                                          |\n| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| apiKey              | **Required**. The Firebase Web API Key, which you can find on the Firebase Project settings overview page. Keep in mind, this API key will only be visible once you enable Firebase Authentication.                  |\n| serviceAccount      | Optional in authenticated [Google Cloud Run](https://cloud.google.com/run) environments. Otherwise, **required**. This refers to the Firebase Service Account credentials.                                           |\n| cookieName          | **Required**. The name of the cookie set by the `loginPath` API route.                                                                                                                                               |\n| cookieSignatureKeys | **Required**. These are [rotating keys](https://developer.okta.com/docs/concepts/key-rotation/#:~:text=Key%20rotation%20is%20when%20a,and%20follows%20cryptographic%20best%20practices) used to validate the cookie. |\n\n### Optional Options\n\n| Name     | Description                                                                                                                                                                                                                                                                         |\n| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| tenantId | **Optional** `string`. Specify this if your project supports [multi-tenancy](https://cloud.google.com/identity-platform/docs/multi-tenancy-authentication).                                                                                                                         |\n\n### Metadata\n\n`getTokens` can return `metadata` property, which is a custom data that can be saved within the cookies using `getMetadata` property passed to [Authentication Middleware](/docs/usage/middleware#metadata).\n\n`getMetadata` is called when user logs in or the credential are refreshed. The resulting object is then saved within user cookies."
  },
  {
    "path": "docs/pages/examples/index.mdx",
    "content": "import Example from 'components/Example';\n\n# Examples\n\n<div className=\"mt-8 flex flex-col gap-4\">\n\n<Example\n  featured\n  name=\"Starter\"\n  hash=\"starter\"\n  description=\"An example that showcases usage of next-firebase-auth-edge with the Next.js 16\"\n  demoLink=\"https://next-firebase-auth-edge-starter.vercel.app\"\n  sourceLink=\"https://github.com/awinogrodzki/next-firebase-auth-edge/tree/main/examples/next-typescript-starter\"\n/>\n\n<Example\n  name=\"Minimal\"\n  hash=\"minimal\"\n  description=\"A minimal example that showcases usage of next-firebase-auth-edge with the Next.js 14\"\n  demoLink=\"https://next-typescript-minimal-xi.vercel.app/\"\n  sourceLink=\"https://github.com/awinogrodzki/next-firebase-auth-edge/tree/main/examples/next-typescript-minimal\"\n/>\n\n</div>\n"
  },
  {
    "path": "docs/pages/index.mdx",
    "content": "---\ntitle: next-firebase-auth-edge\n---\n\nimport Hero from 'components/Hero';\n\n<div className=\"bg-slate-50 dark:bg-gray-900\">\n\n<Hero\n  titleStrong=\"Next.js Firebase Authentication\"\n  titleRegular=\"for Node.js and Edge runtimes\"\n  description=\"Use Firebase Auth with latest Next.js features\"\n  getStarted=\"Get started\"\n  viewExample=\"View an example\"\n/>\n\n</div>\n"
  },
  {
    "path": "docs/postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {}\n  }\n};\n"
  },
  {
    "path": "docs/prettier.config.js",
    "content": "module.exports = {\n  singleQuote: true,\n  trailingComma: 'none',\n  bracketSpacing: false\n};\n"
  },
  {
    "path": "docs/public/favicon/site.webmanifest",
    "content": "{\n    \"name\": \"\",\n    \"short_name\": \"\",\n    \"icons\": [\n        {\n            \"src\": \"/android-chrome-192x192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"/android-chrome-512x512.png\",\n            \"sizes\": \"512x512\",\n            \"type\": \"image/png\"\n        }\n    ],\n    \"theme_color\": \"#ffffff\",\n    \"background_color\": \"#ffffff\",\n    \"display\": \"standalone\"\n}\n"
  },
  {
    "path": "docs/services/BrowserTracker.tsx",
    "content": "import * as vercel from '@vercel/analytics/react';\n\ntype Event = {\n  name: 'partner-referral';\n  data: {href: string; name: string};\n};\n\nexport default class BrowserTracker {\n  public static trackEvent({data, name}: Event) {\n    const promises = [];\n\n    if (typeof window !== 'undefined') {\n      const umami = (window as any).umami;\n      if (umami) {\n        promises.push(umami.track(name, data));\n      } else {\n        console.warn('umami not loaded');\n      }\n    }\n\n    promises.push(vercel.track(name, data));\n\n    return Promise.all(promises);\n  }\n}\n"
  },
  {
    "path": "docs/services/ServerTracker.tsx",
    "content": "import * as vercel from '@vercel/analytics/server';\n\nexport default class ServerTracker {\n  private static postToCollect({\n    auth,\n    body,\n    request\n  }: {\n    body: {\n      type: 'pageview' | 'event';\n      payload?: any;\n    };\n    auth?: string;\n    request: Request;\n  }) {\n    const referer = request.headers.get('referer');\n    const requestUrl = new URL(request.url);\n\n    const language = request.headers\n      .get('accept-language')\n      ?.split(',')\n      .at(0)\n      ?.replace(/;.*/, '');\n\n    const headers = new Headers();\n    headers.set('content-type', 'application/json');\n    if (referer) {\n      headers.set('referer', referer);\n    }\n    if (auth) {\n      headers.set('x-umami-auth', auth);\n    }\n\n    return fetch(process.env.UMAMI_URL + '/api/collect', {\n      method: 'POST',\n      headers,\n      body: JSON.stringify({\n        ...body,\n        payload: {\n          language,\n          website: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,\n          hostname: requestUrl.hostname,\n          url: requestUrl.pathname,\n          ...body.payload\n        }\n      })\n    }).then((res) =>\n      res.text().then((nextAuth) => {\n        if (!nextAuth || !res.ok) {\n          throw new Error(\n            'Failed to track umami event: ' + res.status + nextAuth + ' '\n          );\n        }\n        return nextAuth;\n      })\n    );\n  }\n\n  private static createAuth(request: Request) {\n    return ServerTracker.postToCollect({\n      request,\n      body: {\n        type: 'pageview',\n        payload: {url: request.url}\n      }\n    });\n  }\n\n  public static async trackEvent({\n    data,\n    name,\n    request\n  }: {\n    data?: Record<string, string>;\n    name: string;\n    request: Request;\n  }) {\n    const promises = [];\n\n    promises.push(\n      vercel.track(name, data).catch((error) => {\n        throw new Error('Vercel tracking failed', {cause: error});\n      })\n    );\n\n    promises.push(\n      ServerTracker.postToCollect({\n        request,\n        auth: await ServerTracker.createAuth(request),\n        body: {\n          type: 'event',\n          payload: {\n            event_name: name,\n            event_data: data\n          }\n        }\n      }).catch((error) => {\n        throw new Error('Umami tracking failed', {cause: error});\n      })\n    );\n\n    return Promise.all(promises);\n  }\n}\n"
  },
  {
    "path": "docs/styles.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root {\n  --shiki-token-punctuation: rgba(0, 0, 0, 0.5);\n  --shiki-token-comment: rgba(100, 116, 139, 0.8);\n}\n\n.dark {\n  --shiki-token-string-expression: hsl(160, 75%, 45%);\n  --shiki-token-punctuation: rgba(255, 255, 255, 0.5);\n}\n\n/**\n * Navbar on home\n */\n.navbar-home {\n  @apply bg-slate-800 text-white dark:bg-slate-800;\n}\n\n/* Hamburger menu */\n.navbar-home svg.open {\n  @apply !text-slate-900 dark:!text-white;\n}\n\n.navbar-home nav > a {\n  @apply !text-white/80 transition-opacity hover:opacity-70;\n}\n.navbar-home *:not(.nextra-scrollbar) a,\n.navbar-home *:not(.nextra-scrollbar) button {\n  @apply !text-inherit;\n}\n\nhtml:not(.dark) .navbar-home .nextra-scrollbar {\n  @apply !bg-white;\n}\n\n.navbar-home input::placeholder {\n  @apply !text-white/50;\n}\n.navbar-home input {\n  @apply !bg-slate-900 !text-white;\n}\n\n.navbar-home .nextra-nav-container-blur {\n  display: none;\n}\n\n/**\n * Typography\n */\n\nfigure {\n  @apply my-8 flex flex-col items-center;\n}\n\nfigure .nextra-code-block {\n  @apply w-full;\n}\nfigure .nextra-code-block > pre {\n  @apply mb-0;\n}\n\nfigcaption {\n  @apply mt-4 text-center text-sm text-slate-500;\n}\n\nsummary {\n  @apply font-bold;\n}\n\ndetails > div > div {\n  @apply pt-4 pl-7 pr-3;\n}\ndetails > div > div:last-child {\n  @apply mb-4;\n}\n"
  },
  {
    "path": "docs/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  darkMode: 'class',\n  content: [\n    './components/**/*.{js,tsx}',\n    './pages/**/*.{md,mdx}',\n    './theme.config.{js,tsx}'\n  ],\n  theme: {\n    extend: {\n      fontSize: {\n        '2xs': ['0.69rem', { lineHeight: '1' }],\n        '5xl': ['3rem', { lineHeight: '1.2' }]\n      },\n      colors: {\n        slate: {\n          50: \"#E1F8F9\",\n          100: \"#C3F2F4\",\n          200: \"#87E5E8\",\n          300: \"#50D9DD\",\n          400: \"#25BBC1\",\n          500: \"#198185\",\n          600: \"#0E4749\",\n          700: \"#0B3638\",\n          800: \"#072527\",\n          900: \"#031111\",\n          950: \"#020809\"\n        },\n        gray: {\n          50: \"#E1F8F9\",\n          100: \"#C3F2F4\",\n          200: \"#87E5E8\",\n          300: \"#50D9DD\",\n          400: \"#25BBC1\",\n          500: \"#198185\",\n          600: \"#0E4749\",\n          700: \"#0B3638\",\n          800: \"#072527\",\n          900: \"#031111\",\n          950: \"#020809\"\n        },\n        primary: '#B3F2DD'\n      }\n    },\n    fontFamily: {\n      sans: ['Inter', 'sans-serif'],\n      mono: [\n        'Monaco',\n        'ui-monospace',\n        'SFMono-Regular',\n        'Menlo',\n        'Consolas',\n        'Liberation Mono',\n        'Courier New',\n        'monospace'\n      ]\n    }\n  }\n};\n"
  },
  {
    "path": "docs/theme.config.tsx",
    "content": "import Footer from 'components/Footer';\nimport { useRouter } from 'next/router';\nimport { ThemeConfig } from 'nextra';\nimport { Navbar, ThemeSwitch, useConfig } from 'nextra-theme-docs';\nimport { ComponentProps } from 'react';\nimport config from './config';\n\nconst logo = (\n  <svg\n    version=\"1.1\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    xmlnsXlink=\"http://www.w3.org/1999/xlink\"\n    x=\"0px\"\n    y=\"0px\"\n    viewBox=\"0 0 710.3 127\"\n    xmlSpace=\"preserve\"\n    width=\"240px\"\n    fill=\"currentColor\"\n  >\n    <g>\n      <path\n        d=\"M157.9,80.1c-0.3,0-0.6-0.1-0.8-0.3s-0.3-0.5-0.3-0.8V54.7c0-0.3,0.1-0.6,0.3-0.8c0.2-0.2,0.5-0.3,0.8-0.3h2.4\n\t\tc0.3,0,0.6,0.1,0.8,0.3s0.3,0.5,0.3,0.8V57c0.9-1.2,2-2.1,3.3-2.8s3.1-1.1,5.2-1.1c2.2,0,4.1,0.5,5.6,1.5c1.6,1,2.7,2.3,3.5,4\n\t\ts1.2,3.7,1.2,6v14.4c0,0.3-0.1,0.6-0.3,0.8c-0.2,0.2-0.5,0.3-0.8,0.3h-2.6c-0.3,0-0.6-0.1-0.8-0.3s-0.3-0.5-0.3-0.8V64.8\n\t\tc0-2.4-0.6-4.2-1.7-5.6s-2.9-2-5.1-2c-2.1,0-3.8,0.7-5.1,2s-1.9,3.2-1.9,5.6v14.1c0,0.3-0.1,0.6-0.3,0.8c-0.2,0.2-0.5,0.3-0.8,0.3\n\t\tL157.9,80.1L157.9,80.1z\"\n      />\n      <path\n        d=\"M198.3,80.6c-3.5,0-6.3-1.1-8.4-3.2c-2.1-2.2-3.2-5.1-3.4-8.9c0-0.4-0.1-1-0.1-1.7s0-1.3,0.1-1.7c0.1-2.4,0.7-4.5,1.7-6.4\n\t\tc1-1.8,2.3-3.2,4.1-4.2s3.7-1.5,6.1-1.5c2.6,0,4.8,0.5,6.5,1.6s3.1,2.6,4,4.7s1.4,4.4,1.4,7v0.9c0,0.4-0.1,0.7-0.3,0.9\n\t\tc-0.2,0.2-0.5,0.3-0.8,0.3h-17.7c0,0,0,0.1,0,0.2c0,0.1,0,0.2,0,0.2c0.1,1.4,0.4,2.7,0.9,3.9c0.5,1.2,1.3,2.2,2.4,2.9\n\t\ts2.2,1.1,3.7,1.1c1.2,0,2.2-0.2,3.1-0.6c0.8-0.4,1.5-0.8,2-1.2c0.5-0.5,0.9-0.8,1-1.1c0.3-0.4,0.5-0.7,0.7-0.8\n\t\tc0.2-0.1,0.4-0.1,0.8-0.1h2.4c0.3,0,0.6,0.1,0.8,0.3s0.3,0.4,0.3,0.8c0,0.5-0.3,1.1-0.8,1.9c-0.5,0.7-1.2,1.5-2.2,2.2\n\t\tc-0.9,0.7-2.1,1.3-3.5,1.8C201.6,80.4,200,80.6,198.3,80.6z M191.3,64.9h14v-0.2c0-1.5-0.3-2.9-0.8-4.1s-1.4-2.1-2.4-2.8\n\t\tc-1.1-0.7-2.3-1.1-3.8-1.1c-1.5,0-2.7,0.3-3.8,1.1c-1,0.7-1.8,1.6-2.4,2.8s-0.8,2.6-0.8,4.1L191.3,64.9L191.3,64.9z\"\n      />\n      <path\n        d=\"M214.5,80.1c-0.3,0-0.5-0.1-0.7-0.3c-0.2-0.2-0.3-0.5-0.3-0.8c0-0.1,0-0.2,0.1-0.4c0.1-0.2,0.1-0.3,0.3-0.5l8.9-11.6\n\t\tl-8.3-11c-0.1-0.2-0.2-0.4-0.3-0.5c-0.1-0.1-0.1-0.3-0.1-0.4c0-0.3,0.1-0.6,0.3-0.8c0.2-0.2,0.5-0.3,0.8-0.3h2.6\n\t\tc0.4,0,0.7,0.1,0.8,0.3c0.2,0.2,0.4,0.4,0.5,0.5l6.8,8.8l6.8-8.8c0.1-0.2,0.3-0.4,0.5-0.6s0.5-0.3,0.8-0.3h2.5\n\t\tc0.3,0,0.6,0.1,0.8,0.3c0.2,0.2,0.3,0.5,0.3,0.8c0,0.1,0,0.3-0.1,0.4c0,0.1-0.1,0.3-0.3,0.5l-8.4,11.1l8.9,11.5\n\t\tc0.1,0.2,0.2,0.4,0.3,0.5c0,0.1,0.1,0.3,0.1,0.4c0,0.3-0.1,0.6-0.3,0.8c-0.2,0.2-0.5,0.3-0.8,0.3h-2.7c-0.3,0-0.6-0.1-0.8-0.2\n\t\ts-0.4-0.3-0.5-0.5l-7.2-9.3l-7.2,9.3c-0.1,0.1-0.3,0.3-0.5,0.5c-0.2,0.2-0.5,0.3-0.8,0.3L214.5,80.1L214.5,80.1z\"\n      />\n      <path\n        d=\"M253.6,80.1c-1.9,0-3.5-0.4-4.6-1.1c-1.2-0.7-2.1-1.8-2.6-3.1c-0.5-1.3-0.8-2.9-0.8-4.8V57.6h-4c-0.3,0-0.6-0.1-0.8-0.3\n\t\ts-0.3-0.5-0.3-0.8v-1.7c0-0.3,0.1-0.6,0.3-0.8c0.2-0.2,0.5-0.3,0.8-0.3h4V45c0-0.3,0.1-0.6,0.3-0.8s0.5-0.3,0.8-0.3h2.4\n\t\tc0.3,0,0.6,0.1,0.8,0.3s0.3,0.5,0.3,0.8v8.5h6.3c0.3,0,0.6,0.1,0.8,0.3s0.3,0.5,0.3,0.8v1.7c0,0.3-0.1,0.6-0.3,0.8\n\t\tc-0.2,0.2-0.5,0.3-0.8,0.3h-6.3v13.1c0,1.6,0.3,2.9,0.8,3.8c0.5,0.9,1.5,1.4,2.9,1.4h3.1c0.3,0,0.6,0.1,0.8,0.3s0.3,0.5,0.3,0.8\n\t\tv1.8c0,0.3-0.1,0.6-0.3,0.8c-0.2,0.2-0.5,0.3-0.8,0.3C257.1,80.1,253.6,80.1,253.6,80.1z\"\n      />\n      <path\n        d=\"M278.6,80.1c-0.3,0-0.6-0.1-0.8-0.3s-0.3-0.5-0.3-0.8V57.6h-4.3c-0.3,0-0.6-0.1-0.8-0.3s-0.3-0.5-0.3-0.8v-1.7\n\t\tc0-0.3,0.1-0.6,0.3-0.8c0.2-0.2,0.5-0.3,0.8-0.3h4.3V51c0-1.7,0.3-3.3,0.9-4.6c0.6-1.3,1.5-2.3,2.8-3s3-1.1,5.1-1.1h7.9\n\t\tc0.3,0,0.6,0.1,0.8,0.3s0.3,0.5,0.3,0.8v1.7c0,0.3-0.1,0.6-0.3,0.8c-0.2,0.2-0.5,0.3-0.8,0.3h-7.8c-1.6,0-2.7,0.4-3.3,1.2\n\t\ts-0.9,2-0.9,3.6v2.3H294c0.3,0,0.6,0.1,0.8,0.3s0.3,0.5,0.3,0.8v24.2c0,0.3-0.1,0.6-0.3,0.8c-0.2,0.2-0.5,0.3-0.8,0.3h-2.4\n\t\tc-0.4,0-0.7-0.1-0.9-0.3c-0.2-0.2-0.3-0.5-0.3-0.8V57.6h-8.4v21.3c0,0.3-0.1,0.6-0.3,0.8c-0.2,0.2-0.5,0.3-0.8,0.3H278.6z\"\n      />\n      <path\n        d=\"M303.9,80.1c-0.3,0-0.6-0.1-0.8-0.3s-0.3-0.5-0.3-0.8V54.8c0-0.3,0.1-0.6,0.3-0.9c0.2-0.2,0.5-0.4,0.8-0.4h2.4\n\t\tc0.3,0,0.6,0.1,0.9,0.4s0.4,0.5,0.4,0.9V57c0.7-1.2,1.6-2,2.8-2.6c1.2-0.6,2.6-0.9,4.3-0.9h2c0.3,0,0.6,0.1,0.8,0.3\n\t\tc0.2,0.2,0.3,0.5,0.3,0.8v2.1c0,0.3-0.1,0.6-0.3,0.8c-0.2,0.2-0.5,0.3-0.8,0.3h-3.1c-1.8,0-3.3,0.5-4.3,1.6s-1.6,2.5-1.6,4.4v15\n\t\tc0,0.3-0.1,0.6-0.4,0.8c-0.2,0.2-0.5,0.3-0.9,0.3L303.9,80.1L303.9,80.1z\"\n      />\n      <path\n        d=\"M332.2,80.6c-3.5,0-6.3-1.1-8.4-3.2c-2.1-2.2-3.2-5.1-3.4-8.9c0-0.4,0-1,0-1.7s0-1.3,0-1.7c0.1-2.4,0.7-4.5,1.7-6.4\n\t\tc1-1.8,2.3-3.2,4.1-4.2s3.7-1.5,6-1.5c2.6,0,4.8,0.5,6.5,1.6s3.1,2.6,4,4.7s1.4,4.4,1.4,7v0.9c0,0.4-0.1,0.7-0.3,0.9\n\t\tc-0.2,0.2-0.5,0.3-0.8,0.3h-17.7c0,0,0,0.1,0,0.2c0,0.1,0,0.2,0,0.2c0.1,1.4,0.4,2.7,0.9,3.9c0.5,1.2,1.3,2.2,2.4,2.9\n\t\tc1,0.8,2.2,1.1,3.7,1.1c1.2,0,2.2-0.2,3.1-0.6c0.8-0.4,1.5-0.8,2-1.2c0.5-0.5,0.9-0.8,1-1.1c0.3-0.4,0.5-0.7,0.7-0.8\n\t\tc0.2-0.1,0.4-0.1,0.8-0.1h2.5c0.3,0,0.6,0.1,0.8,0.3s0.3,0.4,0.3,0.8c0,0.5-0.3,1.1-0.8,1.9s-1.2,1.5-2.2,2.2\n\t\tc-1,0.7-2.1,1.3-3.5,1.8C335.6,80.4,334,80.6,332.2,80.6z M325.3,64.9h14v-0.2c0-1.5-0.3-2.9-0.8-4.1s-1.4-2.1-2.4-2.8\n\t\tc-1-0.7-2.3-1.1-3.8-1.1c-1.5,0-2.7,0.3-3.8,1.1c-1,0.7-1.8,1.6-2.4,2.8s-0.8,2.6-0.8,4.1V64.9z\"\n      />\n      <path\n        d=\"M363.4,80.6c-2,0-3.7-0.4-5.1-1.1s-2.5-1.6-3.3-2.7v2.1c0,0.3-0.1,0.6-0.3,0.8c-0.2,0.2-0.5,0.3-0.8,0.3h-2.4\n\t\tc-0.3,0-0.6-0.1-0.8-0.3s-0.3-0.5-0.3-0.8V45c0-0.3,0.1-0.6,0.3-0.8s0.5-0.3,0.8-0.3h2.5c0.4,0,0.7,0.1,0.9,0.3\n\t\tc0.2,0.2,0.3,0.5,0.3,0.8v11.7c0.9-1.1,2-1.9,3.3-2.7s3-1.1,5-1.1c1.9,0,3.5,0.3,4.9,1c1.3,0.7,2.5,1.6,3.3,2.7\n\t\tc0.9,1.2,1.5,2.5,2,3.9c0.4,1.5,0.7,3,0.7,4.6c0,0.5,0,1.1,0,1.5s0,1,0,1.5c0,1.6-0.3,3.2-0.7,4.7c-0.4,1.5-1.1,2.8-2,3.9\n\t\tc-0.9,1.1-2,2-3.3,2.7C366.9,80.3,365.3,80.6,363.4,80.6z M362.3,76.5c1.8,0,3.2-0.4,4.2-1.2c1-0.8,1.7-1.8,2.2-3s0.7-2.6,0.8-4\n\t\tc0-1,0-2,0-3c-0.1-1.4-0.3-2.8-0.8-4c-0.4-1.2-1.2-2.2-2.2-3c-1-0.8-2.4-1.2-4.2-1.2c-1.6,0-2.9,0.4-4,1.1s-1.9,1.7-2.4,2.9\n\t\tc-0.5,1.2-0.8,2.3-0.8,3.5c0,0.5,0,1.2,0,2s0,1.4,0,2c0.1,1.3,0.4,2.5,0.8,3.7s1.3,2.2,2.3,3C359.3,76.1,360.6,76.5,362.3,76.5z\"\n      />\n      <path\n        d=\"M388,80.6c-1.7,0-3.2-0.3-4.6-1s-2.5-1.6-3.4-2.8s-1.3-2.5-1.3-3.9c0-2.3,1-4.2,2.9-5.6c1.9-1.4,4.4-2.3,7.5-2.8l7.6-1.1\n\t\tv-1.5c0-1.6-0.5-2.9-1.4-3.8c-0.9-0.9-2.5-1.4-4.6-1.4c-1.5,0-2.8,0.3-3.7,0.9c-0.9,0.6-1.6,1.4-2,2.3c-0.2,0.5-0.6,0.8-1.1,0.8\n\t\th-2.3c-0.4,0-0.7-0.1-0.8-0.3c-0.2-0.2-0.3-0.5-0.3-0.8c0-0.5,0.2-1.1,0.6-1.9c0.4-0.8,1-1.5,1.8-2.2s1.9-1.3,3.1-1.8\n\t\tc1.3-0.5,2.8-0.7,4.7-0.7c2,0,3.8,0.3,5.2,0.8s2.5,1.2,3.3,2.1s1.4,1.9,1.7,3s0.5,2.3,0.5,3.4v16.5c0,0.3-0.1,0.6-0.3,0.8\n\t\tc-0.2,0.2-0.5,0.3-0.8,0.3h-2.4c-0.4,0-0.7-0.1-0.9-0.3c-0.2-0.2-0.3-0.5-0.3-0.8v-2.2c-0.4,0.6-1,1.2-1.8,1.8s-1.7,1.1-2.8,1.5\n\t\tC391,80.4,389.7,80.6,388,80.6z M389.1,76.8c1.4,0,2.7-0.3,3.8-0.9c1.2-0.6,2.1-1.5,2.7-2.8s1-2.8,1-4.8v-1.4l-5.9,0.9\n\t\tc-2.4,0.3-4.2,0.9-5.5,1.7c-1.2,0.8-1.8,1.8-1.8,3c0,0.9,0.3,1.7,0.8,2.4s1.3,1.1,2.1,1.4C387.3,76.6,388.2,76.8,389.1,76.8z\"\n      />\n      <path\n        d=\"M418,80.6c-1.9,0-3.5-0.2-4.9-0.7c-1.4-0.5-2.5-1.1-3.3-1.7c-0.9-0.7-1.5-1.4-1.9-2c-0.4-0.7-0.7-1.2-0.7-1.6\n\t\tc0-0.4,0.1-0.7,0.4-0.9c0.3-0.2,0.5-0.3,0.8-0.3h2.2c0.2,0,0.4,0,0.5,0.1s0.3,0.2,0.5,0.4c0.4,0.5,0.9,0.9,1.5,1.4s1.2,0.9,2,1.2\n\t\tc0.8,0.3,1.8,0.5,3,0.5c1.7,0,3.2-0.3,4.3-1c1.1-0.7,1.7-1.6,1.7-2.9c0-0.8-0.2-1.5-0.7-2s-1.3-1-2.5-1.4c-1.2-0.4-2.8-0.8-4.8-1.3\n\t\tc-2-0.5-3.7-1.1-4.9-1.8c-1.2-0.7-2-1.5-2.5-2.5c-0.5-1-0.8-2.1-0.8-3.3c0-1.3,0.4-2.5,1.1-3.7s1.9-2.1,3.3-2.9\n\t\tc1.5-0.8,3.3-1.1,5.4-1.1c1.8,0,3.3,0.2,4.5,0.7s2.3,1,3.1,1.7c0.8,0.7,1.4,1.3,1.8,2c0.4,0.7,0.6,1.2,0.7,1.6\n\t\tc0,0.3-0.1,0.6-0.3,0.8c-0.2,0.2-0.5,0.3-0.8,0.3h-2.1c-0.2,0-0.4-0.1-0.6-0.2c-0.1-0.1-0.3-0.2-0.4-0.4c-0.3-0.4-0.7-0.9-1.2-1.3\n\t\tc-0.5-0.4-1-0.8-1.8-1.1c-0.7-0.3-1.7-0.4-2.9-0.4c-1.7,0-3,0.4-3.8,1.1s-1.3,1.6-1.3,2.7c0,0.7,0.2,1.2,0.6,1.7\n\t\tc0.4,0.5,1.1,1,2.1,1.4c1,0.4,2.6,0.8,4.7,1.3c2.2,0.4,4,1,5.3,1.8s2.2,1.6,2.8,2.6s0.8,2.1,0.8,3.4c0,1.4-0.4,2.7-1.2,3.9\n\t\ts-2.1,2.1-3.7,2.8C422.4,80.2,420.4,80.6,418,80.6z\"\n      />\n      <path\n        d=\"M445.4,80.6c-3.5,0-6.3-1.1-8.4-3.2c-2.1-2.2-3.2-5.1-3.4-8.9c0-0.4,0-1,0-1.7s0-1.3,0-1.7c0.1-2.4,0.7-4.5,1.7-6.4\n\t\tc1-1.8,2.3-3.2,4.1-4.2s3.7-1.5,6-1.5c2.6,0,4.8,0.5,6.5,1.6s3.1,2.6,4,4.7s1.4,4.4,1.4,7v0.9c0,0.4-0.1,0.7-0.3,0.9\n\t\tc-0.2,0.2-0.5,0.3-0.8,0.3h-17.7c0,0,0,0.1,0,0.2c0,0.1,0,0.2,0,0.2c0.1,1.4,0.4,2.7,0.9,3.9c0.5,1.2,1.3,2.2,2.4,2.9\n\t\tc1,0.8,2.2,1.1,3.7,1.1c1.2,0,2.2-0.2,3.1-0.6c0.8-0.4,1.5-0.8,2-1.2c0.5-0.5,0.9-0.8,1-1.1c0.3-0.4,0.5-0.7,0.7-0.8\n\t\tc0.2-0.1,0.4-0.1,0.8-0.1h2.5c0.3,0,0.6,0.1,0.8,0.3s0.3,0.4,0.3,0.8c0,0.5-0.3,1.1-0.8,1.9s-1.2,1.5-2.2,2.2\n\t\tc-1,0.7-2.1,1.3-3.5,1.8C448.7,80.4,447.2,80.6,445.4,80.6z M438.5,64.9h14v-0.2c0-1.5-0.3-2.9-0.8-4.1s-1.4-2.1-2.4-2.8\n\t\tc-1-0.7-2.3-1.1-3.8-1.1c-1.5,0-2.7,0.3-3.8,1.1c-1,0.7-1.8,1.6-2.4,2.8s-0.8,2.6-0.8,4.1V64.9z\"\n      />\n      <path\n        d=\"M483.3,80.6c-1.7,0-3.2-0.3-4.6-1s-2.5-1.6-3.4-2.8s-1.3-2.5-1.3-3.9c0-2.3,1-4.2,2.9-5.6c1.9-1.4,4.4-2.3,7.5-2.8l7.6-1.1\n\t\tv-1.5c0-1.6-0.5-2.9-1.4-3.8c-0.9-0.9-2.5-1.4-4.6-1.4c-1.5,0-2.8,0.3-3.7,0.9c-0.9,0.6-1.6,1.4-2,2.3c-0.2,0.5-0.6,0.8-1.1,0.8\n\t\th-2.3c-0.4,0-0.7-0.1-0.8-0.3c-0.2-0.2-0.3-0.5-0.3-0.8c0-0.5,0.2-1.1,0.6-1.9c0.4-0.8,1-1.5,1.8-2.2s1.9-1.3,3.1-1.8\n\t\tc1.3-0.5,2.8-0.7,4.7-0.7c2,0,3.8,0.3,5.2,0.8c1.4,0.5,2.5,1.2,3.3,2.1s1.4,1.9,1.7,3s0.5,2.3,0.5,3.4v16.5c0,0.3-0.1,0.6-0.3,0.8\n\t\tc-0.2,0.2-0.5,0.3-0.8,0.3h-2.3c-0.4,0-0.7-0.1-0.9-0.3c-0.2-0.2-0.3-0.5-0.3-0.8v-2.2c-0.4,0.6-1,1.2-1.8,1.8s-1.7,1.1-2.8,1.5\n\t\tC486.4,80.4,485,80.6,483.3,80.6z M484.4,76.8c1.4,0,2.7-0.3,3.8-0.9s2.1-1.5,2.7-2.8s1-2.8,1-4.8v-1.4l-5.9,0.9\n\t\tc-2.4,0.3-4.2,0.9-5.5,1.7c-1.2,0.8-1.8,1.8-1.8,3c0,0.9,0.3,1.7,0.8,2.4s1.3,1.1,2.1,1.4C482.6,76.6,483.5,76.8,484.4,76.8z\"\n      />\n      <path\n        d=\"M514,80.6c-2.2,0-4-0.5-5.5-1.5s-2.7-2.3-3.5-4.1c-0.8-1.7-1.2-3.7-1.2-6V54.7c0-0.3,0.1-0.6,0.3-0.8\n\t\tc0.2-0.2,0.5-0.3,0.8-0.3h2.5c0.3,0,0.6,0.1,0.8,0.3c0.2,0.2,0.3,0.5,0.3,0.8v14.1c0,5.1,2.2,7.6,6.6,7.6c2.1,0,3.8-0.7,5.1-2\n\t\tc1.3-1.3,1.9-3.2,1.9-5.6V54.7c0-0.3,0.1-0.6,0.3-0.8c0.2-0.2,0.5-0.3,0.8-0.3h2.5c0.4,0,0.7,0.1,0.9,0.3c0.2,0.2,0.3,0.5,0.3,0.8\n\t\tv24.2c0,0.3-0.1,0.6-0.3,0.8c-0.2,0.2-0.5,0.3-0.9,0.3h-2.3c-0.3,0-0.6-0.1-0.8-0.3c-0.2-0.2-0.3-0.5-0.3-0.8v-2.2\n\t\tc-0.9,1.2-2,2.1-3.3,2.9C517.9,80.2,516.2,80.6,514,80.6z\"\n      />\n      <path\n        d=\"M545.2,80.1c-1.9,0-3.5-0.4-4.7-1.1c-1.2-0.7-2.1-1.8-2.6-3.1c-0.5-1.3-0.8-2.9-0.8-4.8V57.6h-4c-0.3,0-0.6-0.1-0.8-0.3\n\t\tc-0.2-0.2-0.3-0.5-0.3-0.8v-1.7c0-0.3,0.1-0.6,0.3-0.8c0.2-0.2,0.5-0.3,0.8-0.3h4V45c0-0.3,0.1-0.6,0.3-0.8\n\t\tc0.2-0.2,0.5-0.3,0.8-0.3h2.4c0.3,0,0.6,0.1,0.8,0.3c0.2,0.2,0.3,0.5,0.3,0.8v8.5h6.3c0.3,0,0.6,0.1,0.8,0.3\n\t\tc0.2,0.2,0.3,0.5,0.3,0.8v1.7c0,0.3-0.1,0.6-0.3,0.8c-0.2,0.2-0.5,0.3-0.8,0.3h-6.3v13.1c0,1.6,0.3,2.9,0.8,3.8\n\t\tc0.5,0.9,1.5,1.4,2.9,1.4h3.1c0.3,0,0.6,0.1,0.8,0.3c0.2,0.2,0.3,0.5,0.3,0.8v1.8c0,0.3-0.1,0.6-0.3,0.8c-0.2,0.2-0.5,0.3-0.8,0.3\n\t\tC548.7,80.1,545.2,80.1,545.2,80.1z\"\n      />\n      <path\n        d=\"M556,80.1c-0.3,0-0.6-0.1-0.8-0.3c-0.2-0.2-0.3-0.5-0.3-0.8V45c0-0.3,0.1-0.6,0.3-0.8c0.2-0.2,0.5-0.3,0.8-0.3h2.5\n\t\tc0.4,0,0.7,0.1,0.9,0.3c0.2,0.2,0.3,0.5,0.3,0.8v11.9c0.9-1.2,2-2.1,3.4-2.8s3-1.1,5-1.1c2.2,0,4.1,0.5,5.6,1.5\n\t\tc1.5,1,2.7,2.3,3.5,4s1.2,3.7,1.2,6v14.4c0,0.3-0.1,0.6-0.3,0.8c-0.2,0.2-0.5,0.3-0.9,0.3h-2.5c-0.3,0-0.6-0.1-0.8-0.3\n\t\tc-0.2-0.2-0.3-0.5-0.3-0.8V64.8c0-2.4-0.6-4.2-1.7-5.6s-2.9-2-5.1-2c-2.1,0-3.8,0.7-5.1,2c-1.3,1.3-1.9,3.2-1.9,5.6v14.1\n\t\tc0,0.3-0.1,0.6-0.3,0.8c-0.2,0.2-0.5,0.3-0.9,0.3L556,80.1L556,80.1z\"\n      />\n      <path\n        d=\"M609,80.6c-3.5,0-6.3-1.1-8.4-3.2c-2.1-2.2-3.2-5.1-3.4-8.9c0-0.4,0-1,0-1.7s0-1.3,0-1.7c0.1-2.4,0.7-4.5,1.7-6.4\n\t\tc1-1.8,2.3-3.2,4.1-4.2s3.7-1.5,6-1.5c2.6,0,4.8,0.5,6.5,1.6s3.1,2.6,4,4.7s1.4,4.4,1.4,7v0.9c0,0.4-0.1,0.7-0.3,0.9\n\t\tc-0.2,0.2-0.5,0.3-0.8,0.3H602c0,0,0,0.1,0,0.2c0,0.1,0,0.2,0,0.2c0.1,1.4,0.4,2.7,0.9,3.9c0.5,1.2,1.3,2.2,2.3,2.9\n\t\tc1,0.8,2.2,1.1,3.7,1.1c1.2,0,2.2-0.2,3.1-0.6c0.8-0.4,1.5-0.8,2-1.2s0.8-0.8,1-1.1c0.3-0.4,0.5-0.7,0.7-0.8\n\t\tc0.2-0.1,0.4-0.1,0.8-0.1h2.5c0.3,0,0.6,0.1,0.8,0.3c0.2,0.2,0.3,0.4,0.3,0.8c0,0.5-0.3,1.1-0.8,1.9s-1.2,1.5-2.2,2.2\n\t\tc-1,0.7-2.1,1.3-3.5,1.8C612.3,80.4,610.7,80.6,609,80.6z M602,64.9h14v-0.2c0-1.5-0.3-2.9-0.8-4.1s-1.4-2.1-2.4-2.8\n\t\tc-1-0.7-2.3-1.1-3.8-1.1c-1.5,0-2.7,0.3-3.8,1.1c-1,0.7-1.8,1.6-2.4,2.8s-0.8,2.6-0.8,4.1L602,64.9L602,64.9z\"\n      />\n      <path\n        d=\"M636.7,80.6c-1.9,0-3.5-0.3-4.8-1c-1.4-0.7-2.5-1.6-3.3-2.7c-0.9-1.1-1.5-2.4-1.9-3.9c-0.4-1.5-0.7-3-0.7-4.7\n\t\tc0-0.5,0-1.1,0-1.5s0-1,0-1.5c0.1-1.6,0.3-3.1,0.7-4.6s1.1-2.8,1.9-3.9s2-2.1,3.3-2.7c1.4-0.7,3-1,4.8-1c2,0,3.7,0.4,5,1.1\n\t\tc1.3,0.7,2.4,1.6,3.3,2.7V45c0-0.3,0.1-0.6,0.3-0.8c0.2-0.2,0.5-0.3,0.8-0.3h2.5c0.3,0,0.6,0.1,0.8,0.3c0.2,0.2,0.3,0.5,0.3,0.8\n\t\tv33.9c0,0.3-0.1,0.6-0.3,0.8c-0.2,0.2-0.5,0.3-0.8,0.3h-2.3c-0.4,0-0.7-0.1-0.9-0.3c-0.2-0.2-0.3-0.5-0.3-0.8v-2.1\n\t\tc-0.8,1.1-2,2-3.3,2.7S638.7,80.6,636.7,80.6z M637.8,76.5c1.7,0,3.1-0.4,4.1-1.2s1.8-1.8,2.3-3s0.8-2.4,0.8-3.7c0-0.5,0-1.2,0-2\n\t\ts0-1.4,0-2c0-1.2-0.3-2.4-0.8-3.5c-0.5-1.2-1.3-2.1-2.3-2.9s-2.4-1.1-4-1.1c-1.7,0-3.1,0.4-4.1,1.2c-1,0.8-1.8,1.8-2.2,3\n\t\tc-0.4,1.2-0.7,2.6-0.8,4c0,1,0,2,0,3c0.1,1.4,0.3,2.8,0.8,4c0.4,1.2,1.2,2.2,2.2,3C634.7,76.1,636,76.5,637.8,76.5z\"\n      />\n      <path\n        d=\"M668.2,91.3c-2.3,0-4.2-0.3-5.7-0.9s-2.6-1.3-3.5-2.2c-0.8-0.9-1.4-1.7-1.8-2.6s-0.5-1.6-0.6-2.1c0-0.3,0.1-0.6,0.3-0.9\n\t\tc0.2-0.2,0.5-0.4,0.8-0.4h2.5c0.3,0,0.6,0.1,0.8,0.2c0.2,0.1,0.4,0.4,0.5,0.9c0.2,0.5,0.5,1.1,0.9,1.7s1,1.2,1.9,1.6\n\t\tc0.8,0.5,2,0.7,3.6,0.7c1.6,0,2.9-0.2,4-0.6s1.9-1.2,2.5-2.2s0.8-2.5,0.8-4.4v-3.5c-0.8,1.1-1.9,1.9-3.2,2.6c-1.3,0.7-3,1.1-5,1.1\n\t\tc-1.9,0-3.5-0.3-4.9-1c-1.4-0.7-2.5-1.6-3.3-2.7c-0.9-1.1-1.5-2.4-1.9-3.9c-0.4-1.5-0.7-3-0.7-4.6c0-0.9,0-1.9,0-2.8\n\t\tc0.1-1.6,0.3-3.1,0.7-4.6s1.1-2.8,1.9-3.9s2-2.1,3.3-2.7c1.4-0.7,3-1,4.9-1c2,0,3.7,0.4,5.1,1.2s2.4,1.7,3.3,2.8v-2.2\n\t\tc0-0.3,0.1-0.6,0.3-0.9c0.2-0.2,0.5-0.4,0.8-0.4h2.3c0.3,0,0.6,0.1,0.9,0.4s0.4,0.5,0.4,0.9v24.8c0,2.3-0.4,4.3-1.2,6.1\n\t\tc-0.8,1.8-2,3.2-3.8,4.2C673.6,90.8,671.2,91.3,668.2,91.3z M668.1,76.2c1.7,0,3.1-0.4,4.1-1.2s1.8-1.8,2.3-3\n\t\tc0.5-1.2,0.8-2.4,0.8-3.6c0-0.5,0-1.1,0-1.8c0-0.7,0-1.3,0-1.8c0-1.2-0.3-2.4-0.8-3.6s-1.3-2.2-2.3-3s-2.4-1.2-4.1-1.2\n\t\ts-3.1,0.4-4.1,1.2c-1,0.8-1.8,1.8-2.2,3c-0.4,1.2-0.7,2.6-0.8,4c0,0.9,0,1.8,0,2.7c0.1,1.4,0.3,2.8,0.8,4c0.4,1.2,1.2,2.2,2.2,3\n\t\tC665.1,75.8,666.4,76.2,668.1,76.2z\"\n      />\n      <path\n        d=\"M698.4,80.6c-3.5,0-6.3-1.1-8.4-3.2c-2.1-2.2-3.2-5.1-3.4-8.9c0-0.4,0-1,0-1.7s0-1.3,0-1.7c0.1-2.4,0.7-4.5,1.7-6.4\n\t\tc1-1.8,2.3-3.2,4.1-4.2s3.7-1.5,6-1.5c2.6,0,4.8,0.5,6.5,1.6s3.1,2.6,4,4.7s1.4,4.4,1.4,7v0.9c0,0.4-0.1,0.7-0.3,0.9\n\t\tc-0.2,0.2-0.5,0.3-0.8,0.3h-17.7c0,0,0,0.1,0,0.2c0,0.1,0,0.2,0,0.2c0.1,1.4,0.4,2.7,0.9,3.9c0.5,1.2,1.3,2.2,2.3,2.9\n\t\tc1,0.8,2.2,1.1,3.7,1.1c1.2,0,2.2-0.2,3.1-0.6c0.8-0.4,1.5-0.8,2-1.2s0.8-0.8,1-1.1c0.3-0.4,0.5-0.7,0.7-0.8\n\t\tc0.2-0.1,0.4-0.1,0.8-0.1h2.5c0.3,0,0.6,0.1,0.8,0.3c0.2,0.2,0.3,0.4,0.3,0.8c0,0.5-0.3,1.1-0.8,1.9s-1.2,1.5-2.2,2.2\n\t\tc-1,0.7-2.1,1.3-3.5,1.8C701.8,80.4,700.2,80.6,698.4,80.6z M691.5,64.9h14v-0.2c0-1.5-0.3-2.9-0.8-4.1s-1.4-2.1-2.4-2.8\n\t\tc-1-0.7-2.3-1.1-3.8-1.1c-1.5,0-2.7,0.3-3.8,1.1c-1,0.7-1.8,1.6-2.4,2.8s-0.8,2.6-0.8,4.1L691.5,64.9L691.5,64.9z\"\n      />\n    </g>\n    <path\n      d=\"M63.5,0C28.5,0,0,28.5,0,63.5c0,35,28.5,63.5,63.5,63.5c35,0,63.5-28.5,63.5-63.5S98.5,0,63.5,0z M89.8,117.2\n\tc-4.8,2.4-10,4.1-15.4,5.1L43,90.9L89.8,117.2z M63.5,41.9l19.3,33.4H44.2L63.5,41.9z M24.9,109l11.4-19.7l33.2,33.7\n\tc-2,0.2-3.9,0.3-6,0.3C48.8,123.2,35.3,117.9,24.9,109z M94.4,114.6L55.8,93l53.9,8.4C105.4,106.6,100.2,111.1,94.4,114.6z\"\n    />\n  </svg>\n);\n\nexport default {\n  project: {\n    link: config.githubUrl\n  },\n  docsRepositoryBase: config.githubUrl + '/blob/main/docs',\n  useNextSeoProps() {\n    return {\n      titleTemplate: '%s – Firebase Authentication for Next.js 16'\n    };\n  },\n  // banner: {\n  //   text: (\n  //     <>\n  //     </>\n  //   )\n  // },\n  primaryHue: { light: 210, dark: 195 },\n  footer: {\n    component: Footer\n  },\n  navigation: true,\n  darkMode: true,\n  logo,\n  sidebar: {\n    autoCollapse: true,\n    defaultMenuCollapseLevel: 1\n  },\n  themeSwitch: {\n    component(props: ComponentProps<typeof ThemeSwitch>) {\n      return (\n        <div className=\"flex items-end justify-between\">\n          <ThemeSwitch {...props} />\n        </div>\n      );\n    }\n  },\n  navbar: {\n    component: function CustomNavbar(props: ComponentProps<typeof Navbar>) {\n      const router = useRouter();\n      const isRoot = router.pathname === '/';\n      if (!isRoot) return <Navbar {...props} />;\n\n      return (\n        <div className=\"navbar-home\">\n          <Navbar {...props} />\n        </div>\n      );\n    }\n  },\n  feedback: {\n    content: 'Provide feedback on this page',\n    useLink: () => {\n      const router = useRouter();\n      const pageConfig = useConfig();\n\n      const url = new URL(config.githubUrl);\n      url.pathname += '/issues/new';\n      url.searchParams.set('title', `[Docs]: ${pageConfig.title}`);\n      url.searchParams.set('template', 'update_docs.yml');\n      url.searchParams.set('pageLink', config.baseUrl + router.pathname);\n\n      return url.href;\n    }\n  },\n  head: () => (\n    <>\n      <link\n        href=\"/favicon/apple-touch-icon.png\"\n        rel=\"apple-touch-icon\"\n        sizes=\"180x180\"\n      />\n      <link\n        href=\"/favicon/favicon-32x32.png\"\n        rel=\"icon\"\n        sizes=\"32x32\"\n        type=\"image/png\"\n      />\n      <link\n        href=\"/favicon/favicon-16x16.png\"\n        rel=\"icon\"\n        sizes=\"16x16\"\n        type=\"image/png\"\n      />\n      <link href=\"/favicon/site.webmanifest\" rel=\"manifest\" />\n      <link\n        color=\"#5bbad5\"\n        href=\"/favicon/safari-pinned-tab.svg\"\n        rel=\"mask-icon\"\n      />\n      <meta content=\"#da532c\" name=\"msapplication-TileColor\" />\n      <meta content=\"#ffffff\" name=\"theme-color\" />\n\n      <meta content=\"next-firebase-auth-edge\" name=\"og:title\" />\n      <meta content=\"next-firebase-auth-edge\" name=\"twitter:title\" />\n\n      <meta content=\"summary_large_image\" name=\"twitter:card\" />\n\n      <meta\n        content={config.baseUrl + '/twitter-image.png'}\n        name=\"twitter:image\"\n      />\n      <meta content={config.baseUrl + '/og-image.png'} name=\"og:image\" />\n    </>\n  )\n} satisfies ThemeConfig;\n"
  },
  {
    "path": "docs/tsconfig.json",
    "content": "{\n  \"extends\": \"eslint-config-molindo/tsconfig.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"incremental\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"strict\": true\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import eslint from '@eslint/js';\nimport eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';\nimport tseslint from 'typescript-eslint';\n\nexport default tseslint.config(\n  eslint.configs.recommended,\n  ...tseslint.configs.recommended,\n  eslintPluginPrettierRecommended,\n  {\n    files: [\n      \"**/*.ts\",\n      \"**/*.tsx\"\n    ],\n    ignores: [\n      \"node_modules\",\n      \"lib\"\n    ],\n    rules: {\n      'prettier/prettier': 'error',\n    },\n  }\n);\n"
  },
  {
    "path": "examples/next-typescript-minimal/.eslintrc.json",
    "content": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": "examples/next-typescript-minimal/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n.yarn/install-state.gz\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "examples/next-typescript-minimal/README.md",
    "content": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).\n\n## Getting Started\n\nFirst, run the development server:\n\n```bash\nnpm run dev\n# or\nyarn dev\n# or\npnpm dev\n# or\nbun dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n\nYou can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.\n\nThis project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.\n\n## Learn More\n\nTo learn more about Next.js, take a look at the following resources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.\n- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.\n\nYou can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!\n\n## Deploy on Vercel\n\nThe easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.\n\nCheck out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.\n"
  },
  {
    "path": "examples/next-typescript-minimal/app/HomePage.tsx",
    "content": "\"use client\";\n\nimport { useRouter } from \"next/navigation\";\nimport { getAuth, signOut } from \"firebase/auth\";\nimport { app } from \"../firebase\";\n\ninterface HomePageProps {\n  email?: string;\n}\n\nexport default function HomePage({ email }: HomePageProps) {\n  const router = useRouter();\n\n  async function handleLogout() {\n    await signOut(getAuth(app));\n\n    await fetch(\"/api/logout\");\n\n    router.push(\"/login\");\n  }\n\n  return (\n    <main className=\"flex min-h-screen flex-col items-center justify-center p-24\">\n      <h1 className=\"text-xl mb-4\">Super secure home page</h1>\n      <p className=\"mb-8\">\n        Only <strong>{email}</strong> holds the magic key to this kingdom!\n      </p>\n      <button\n        onClick={handleLogout}\n        className=\"text-white bg-gray-600 hover:bg-gray-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-primary-800\"\n      >\n        Logout\n      </button>\n    </main>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-minimal/app/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root {\n  --foreground-rgb: 0, 0, 0;\n  --background-start-rgb: 214, 219, 220;\n  --background-end-rgb: 255, 255, 255;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --foreground-rgb: 255, 255, 255;\n    --background-start-rgb: 0, 0, 0;\n    --background-end-rgb: 0, 0, 0;\n  }\n}\n\nbody {\n  color: rgb(var(--foreground-rgb));\n  background: linear-gradient(\n      to bottom,\n      transparent,\n      rgb(var(--background-end-rgb))\n    )\n    rgb(var(--background-start-rgb));\n}\n\n@layer utilities {\n  .text-balance {\n    text-wrap: balance;\n  }\n}\n"
  },
  {
    "path": "examples/next-typescript-minimal/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Inter } from \"next/font/google\";\nimport \"./globals.css\";\n\nconst inter = Inter({ subsets: [\"latin\"] });\n\nexport const metadata: Metadata = {\n  title: \"Create Next App\",\n  description: \"Generated by create next app\",\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\">\n      <body className={inter.className}>{children}</body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-minimal/app/login/page.tsx",
    "content": "\"use client\";\n\nimport { FormEvent, useState } from \"react\";\nimport Link from \"next/link\";\nimport { useRouter } from \"next/navigation\";\nimport { getAuth, signInWithEmailAndPassword } from \"firebase/auth\";\nimport { app } from \"../../firebase\";\n\nexport default function Login() {\n  const [email, setEmail] = useState(\"\");\n  const [password, setPassword] = useState(\"\");\n  const [error, setError] = useState(\"\");\n  const router = useRouter();\n\n  async function handleSubmit(event: FormEvent) {\n    event.preventDefault();\n    setError(\"\");\n\n    try {\n      const credential = await signInWithEmailAndPassword(\n        getAuth(app),\n        email,\n        password\n      );\n      const idToken = await credential.user.getIdToken();\n\n      await fetch(\"/api/login\", {\n        headers: {\n          Authorization: `Bearer ${idToken}`,\n        },\n      });\n\n      router.push(\"/\");\n    } catch (e) {\n      setError((e as Error).message);\n    }\n  }\n\n  return (\n    <main className=\"flex min-h-screen flex-col items-center justify-center p-8\">\n      <div className=\"w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700\">\n        <div className=\"p-6 space-y-4 md:space-y-6 sm:p-8\">\n          <h1 className=\"text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white\">\n            Speak thy secret word!\n          </h1>\n          <form\n            onSubmit={handleSubmit}\n            className=\"space-y-4 md:space-y-6\"\n            action=\"#\"\n          >\n            <div>\n              <label\n                htmlFor=\"email\"\n                className=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\"\n              >\n                Your email\n              </label>\n              <input\n                type=\"email\"\n                name=\"email\"\n                value={email}\n                onChange={(e) => setEmail(e.target.value)}\n                id=\"email\"\n                className=\"bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"\n                placeholder=\"name@company.com\"\n                required\n              />\n            </div>\n            <div>\n              <label\n                htmlFor=\"password\"\n                className=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\"\n              >\n                Password\n              </label>\n              <input\n                type=\"password\"\n                name=\"password\"\n                value={password}\n                onChange={(e) => setPassword(e.target.value)}\n                id=\"password\"\n                placeholder=\"••••••••\"\n                className=\"bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"\n                required\n              />\n            </div>\n            {error && (\n              <div\n                className=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative\"\n                role=\"alert\"\n              >\n                <span className=\"block sm:inline\">{error}</span>\n              </div>\n            )}\n            <button\n              type=\"submit\"\n              className=\"w-full text-white bg-gray-600 hover:bg-gray-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-primary-800\"\n            >\n              Enter\n            </button>\n            <p className=\"text-sm font-light text-gray-500 dark:text-gray-400\">\n              Don&apos;t have an account?{\" \"}\n              <Link\n                href=\"/register\"\n                className=\"font-medium text-gray-600 hover:underline dark:text-gray-500\"\n              >\n                Register here\n              </Link>\n            </p>\n          </form>\n        </div>\n      </div>\n    </main>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-minimal/app/page.tsx",
    "content": "import { getTokens } from \"next-firebase-auth-edge\";\nimport { cookies } from \"next/headers\";\nimport { notFound } from \"next/navigation\";\nimport { clientConfig, serverConfig } from \"../config\";\nimport HomePage from \"./HomePage\";\n\nexport default async function Home() {\n  const tokens = await getTokens(cookies(), {\n    apiKey: clientConfig.apiKey,\n    cookieName: serverConfig.cookieName,\n    cookieSignatureKeys: serverConfig.cookieSignatureKeys,\n    serviceAccount: serverConfig.serviceAccount,\n  });\n\n  if (!tokens) {\n    notFound();\n  }\n\n  return <HomePage email={tokens?.decodedToken.email} />;\n}\n"
  },
  {
    "path": "examples/next-typescript-minimal/app/register/page.tsx",
    "content": "\"use client\";\n\nimport { FormEvent, useState } from \"react\";\nimport Link from \"next/link\";\nimport { getAuth, createUserWithEmailAndPassword } from \"firebase/auth\";\nimport { app } from \"../../firebase\";\nimport { useRouter } from \"next/navigation\";\n\nexport default function Register() {\n  const [email, setEmail] = useState(\"\");\n  const [password, setPassword] = useState(\"\");\n  const [confirmation, setConfirmation] = useState(\"\");\n  const [error, setError] = useState(\"\");\n  const router = useRouter();\n\n  async function handleSubmit(event: FormEvent) {\n    event.preventDefault();\n\n    setError(\"\");\n\n    if (password !== confirmation) {\n      setError(\"Passwords don't match\");\n      return;\n    }\n\n    try {\n      await createUserWithEmailAndPassword(getAuth(app), email, password);\n      router.push(\"/login\");\n    } catch (e) {\n      setError((e as Error).message);\n    }\n  }\n\n  return (\n    <main className=\"flex min-h-screen flex-col items-center justify-center p-8\">\n      <div className=\"w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700\">\n        <div className=\"p-6 space-y-4 md:space-y-6 sm:p-8\">\n          <h1 className=\"text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white\">\n            Pray tell, who be this gallant soul seeking entry to mine humble\n            abode?\n          </h1>\n          <form\n            onSubmit={handleSubmit}\n            className=\"space-y-4 md:space-y-6\"\n            action=\"#\"\n          >\n            <div>\n              <label\n                htmlFor=\"email\"\n                className=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\"\n              >\n                Your email\n              </label>\n              <input\n                type=\"email\"\n                name=\"email\"\n                value={email}\n                onChange={(e) => setEmail(e.target.value)}\n                id=\"email\"\n                className=\"bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"\n                placeholder=\"name@company.com\"\n                required\n              />\n            </div>\n            <div>\n              <label\n                htmlFor=\"password\"\n                className=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\"\n              >\n                Password\n              </label>\n              <input\n                type=\"password\"\n                name=\"password\"\n                value={password}\n                onChange={(e) => setPassword(e.target.value)}\n                id=\"password\"\n                placeholder=\"••••••••\"\n                className=\"bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"\n                required\n              />\n            </div>\n            <div>\n              <label\n                htmlFor=\"confirm-password\"\n                className=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\"\n              >\n                Confirm password\n              </label>\n              <input\n                type=\"password\"\n                name=\"confirm-password\"\n                value={confirmation}\n                onChange={(e) => setConfirmation(e.target.value)}\n                id=\"confirm-password\"\n                placeholder=\"••••••••\"\n                className=\"bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\"\n                required\n              />\n            </div>\n            {error && (\n              <div\n                className=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative\"\n                role=\"alert\"\n              >\n                <span className=\"block sm:inline\">{error}</span>\n              </div>\n            )}\n            <button\n              type=\"submit\"\n              className=\"w-full text-white bg-gray-600 hover:bg-gray-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-primary-800\"\n            >\n              Create an account\n            </button>\n            <p className=\"text-sm font-light text-gray-500 dark:text-gray-400\">\n              Already have an account?{\" \"}\n              <Link\n                href=\"/login\"\n                className=\"font-medium text-gray-600 hover:underline dark:text-gray-500\"\n              >\n                Login here\n              </Link>\n            </p>\n          </form>\n        </div>\n      </div>\n    </main>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-minimal/config.ts",
    "content": "export const serverConfig = {\n  cookieName: process.env.AUTH_COOKIE_NAME!,\n  cookieSignatureKeys: [process.env.AUTH_COOKIE_SIGNATURE_KEY_CURRENT!, process.env.AUTH_COOKIE_SIGNATURE_KEY_PREVIOUS!],\n  cookieSerializeOptions: {\n    path: \"/\",\n    httpOnly: true,\n    secure: process.env.USE_SECURE_COOKIES === \"true\",\n    sameSite: \"lax\" as const,\n    maxAge: 12 * 60 * 60 * 24,\n  },\n  serviceAccount: {\n    projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,\n    clientEmail: process.env.FIREBASE_ADMIN_CLIENT_EMAIL!,\n    privateKey: process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\\\n/g, \"\\n\")!,\n  }\n};\n\nexport const clientConfig = {\n  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,\n  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,\n  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,\n  databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,\n  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID\n};\n"
  },
  {
    "path": "examples/next-typescript-minimal/firebase.ts",
    "content": "import { initializeApp } from 'firebase/app';\nimport { clientConfig } from './config';\n\nexport const app = initializeApp(clientConfig);\n"
  },
  {
    "path": "examples/next-typescript-minimal/middleware.ts",
    "content": "import { NextRequest, NextResponse } from \"next/server\";\nimport { authMiddleware, redirectToHome, redirectToLogin } from \"next-firebase-auth-edge\";\nimport { clientConfig, serverConfig } from \"./config\";\n\nconst PUBLIC_PATHS = ['/register', '/login'];\n\nexport async function middleware(request: NextRequest) {\n  return authMiddleware(request, {\n    loginPath: \"/api/login\",\n    logoutPath: \"/api/logout\",\n    apiKey: clientConfig.apiKey,\n    cookieName: serverConfig.cookieName,\n    cookieSignatureKeys: serverConfig.cookieSignatureKeys,\n    cookieSerializeOptions: serverConfig.cookieSerializeOptions,\n    serviceAccount: serverConfig.serviceAccount,\n    handleValidToken: async ({token, decodedToken, customToken}, headers) => {\n      // Authenticated user should not be able to access /login, /register and /reset-password routes\n      if (PUBLIC_PATHS.includes(request.nextUrl.pathname)) {\n        return redirectToHome(request);\n      }\n\n      return NextResponse.next({\n        request: {\n          headers\n        }\n      });\n    },\n    handleInvalidToken: async (reason) => {\n      console.info('Missing or malformed credentials', {reason});\n\n      return redirectToLogin(request, {\n        path: '/login',\n        publicPaths: PUBLIC_PATHS\n      });\n    },\n    handleError: async (error) => {\n      console.error('Unhandled authentication error', {error});\n      \n      return redirectToLogin(request, {\n        path: '/login',\n        publicPaths: PUBLIC_PATHS\n      });\n    }\n  });\n}\n\nexport const config = {\n  matcher: [\n    \"/\",\n    \"/((?!_next|api|.*\\\\.).*)\",\n    \"/api/login\",\n    \"/api/logout\",\n  ],\n};\n"
  },
  {
    "path": "examples/next-typescript-minimal/next.config.mjs",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {};\n\nexport default nextConfig;\n"
  },
  {
    "path": "examples/next-typescript-minimal/package.json",
    "content": "{\n  \"name\": \"my-app\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"firebase\": \"^10.9.0\",\n    \"next\": \"14.1.4\",\n    \"next-firebase-auth-edge\": \"^1.4.1\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^20\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"autoprefixer\": \"^10.0.1\",\n    \"eslint\": \"^8\",\n    \"eslint-config-next\": \"14.1.4\",\n    \"postcss\": \"^8\",\n    \"tailwindcss\": \"^3.3.0\",\n    \"typescript\": \"^5\"\n  }\n}\n"
  },
  {
    "path": "examples/next-typescript-minimal/postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "examples/next-typescript-minimal/tailwind.config.ts",
    "content": "import type { Config } from \"tailwindcss\";\n\nconst config: Config = {\n  content: [\n    \"./pages/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./components/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./app/**/*.{js,ts,jsx,tsx,mdx}\",\n  ],\n  theme: {\n    extend: {\n      backgroundImage: {\n        \"gradient-radial\": \"radial-gradient(var(--tw-gradient-stops))\",\n        \"gradient-conic\":\n          \"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))\",\n      },\n    },\n  },\n  plugins: [],\n};\nexport default config;\n"
  },
  {
    "path": "examples/next-typescript-minimal/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/.dockerignore",
    "content": "Dockerfile\n.dockerignore\nnode_modules\nnpm-debug.log\n"
  },
  {
    "path": "examples/next-typescript-starter/.firebaserc",
    "content": "{\n  \"projects\": {\n    \"default\": \"fake-project\"\n  }\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/.gitignore",
    "content": "node_modules\n.vscode\nyarn-error.log\n.yarn\n.next\n.env\n.env.local\n.env.temp\nlib\ncjs\ntmp\npackage-lock.json\ncoverage\nwebpack-stats.json\n.turbo\n.DS_Store\n.idea\n*.log\ndist\n.firebase\n"
  },
  {
    "path": "examples/next-typescript-starter/Dockerfile",
    "content": "FROM node:18.19.0-alpine\n\nWORKDIR /usr/app\n\nCOPY . .\n\nRUN yarn install --production\n\nRUN printenv\n\nRUN yarn build\n\nCMD [ \"yarn\", \"start\" ]"
  },
  {
    "path": "examples/next-typescript-starter/README.md",
    "content": "# next-firebase-auth-edge starter\n\nThis is a [Next.js](https://nextjs.org/) project showcasing `next-firebase-auth-edge` library features.\n\nDemo of this project can be previewed at [next-firebase-auth-edge-starter.vercel.app](https://next-firebase-auth-edge-starter.vercel.app)\n\n## Before Getting Started\n\nTo properly run this example, you will need to setup a new Firebase Project.\n\nYou will also need to:\n\n- Create a new web app in your new Firebase Project.\n- Add Firebase Auth to your project and enable Google, Email/Password and Email Link sign-in methods\n- Add `localhost` and any other authorized domains in `Authentication > Settings > Authorized domains`\n- Add a Firestore database\n- Get your private keys from \"Project settings > Service Accounts > Generate new private keys\"\n- Make a copy of `.env.dist` and rename it to `.env.local`\n- Fill in the variables inside the `.env.local`\n- Make sure to format private variable in `.env.local` as follows to avoid parsing errors `FIREBASE_ADMIN_PRIVATE_KEY=\"-----BEGIN PRIVATE KEY-----\\nYOUR KEY\\n-----END PRIVATE KEY-----\\n\"`\n\nFirst, run the development server:\n\n```bash\nnpm run dev\n# or\nyarn dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n\n## Configuring Firestore rules\n\nThe demo shows example usage of Firestore Client SDK\n\nMake sure to update Firestore Database Rules of `user-counters` collection in [Firebase Console](https://console.firebase.google.com/).\n\nThe following Firestore Database Rules validates if user has access to update specific `user-counters` database entry\n\n```\nrules_version = '2';\n\nservice cloud.firestore {\n  match /databases/{database}/documents {\n    match /user-counters/{document} {\n      allow read, write: if request.auth.uid == resource.data.id;\n    }\n  }\n}\n```\n\n\n## Emulator support\n\nLibrary provides Firebase Authentication Emulator support\n\nUse [the official guide](https://firebase.google.com/docs/emulator-suite/connect_auth) to run the emulator locally.\n\nIn order to use emulator, copy the contents of `.env.dist` to `.env.local` and uncomment following variables:\n\n```shell\nNEXT_PUBLIC_AUTH_EMULATOR_HOST=localhost:9099\nNEXT_PUBLIC_FIRESTORE_EMULATOR_HOST=localhost:8080\n\nFIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099\n```\n\n`FIREBASE_AUTH_EMULATOR_HOST` is used internally by the library. It's required.\n\n`NEXT_PUBLIC_AUTH_EMULATOR_HOST` is used only by the example app. It can be redefined.\n\n`NEXT_PUBLIC_FIRESTORE_EMULATOR_HOST` is used only by the example app. It can be redefined.\n\nAlso, don't forget to put correct Firebase Project ID in `.firebaserc` file.\n\n## App Check support\n\nLibrary provides [Firebase App Check](https://firebase.google.com/docs/app-check) support\n\nUse [the official guide](https://firebase.google.com/docs/app-check/web/recaptcha-enterprise-provider) to integrate your app with App Check.\n\nIn order to integrate the example with App Check, you need to add two env variables to your `.env.local` file (you can copy them from `.env.dist`).\n\n```shell\nNEXT_PUBLIC_FIREBASE_APP_CHECK_KEY=XXxxxxXxXXXXXXXxxxXxxxXXXxxXXXXxxxxxXX_X\nNEXT_PUBLIC_FIREBASE_APP_ID=x:xxxxxxxxxxxx:web:xxxxxxxxxxxxxxxxxxxxxx\n```\n\n`NEXT_PUBLIC_FIREBASE_APP_CHECK_KEY` should be an app check key obtained by following [Set up your Firebase project](https://firebase.google.com/docs/app-check/web/recaptcha-enterprise-provider#project-setup) step\n\n`NEXT_PUBLIC_FIREBASE_APP_ID` can be obtained by following to `Project overview` > `Project settings`, under `Your apps` section in your Firebase Console.\n\n## Learn More\n\nTo learn more about Next.js, take a look at the following resources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.\n- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.\n\nYou can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!\n\n## Deploy on Vercel\n\nThe easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.\n\nCheck out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.\n"
  },
  {
    "path": "examples/next-typescript-starter/api/index.ts",
    "content": "import {getToken} from '@firebase/app-check';\nimport {getAppCheck} from '../app-check';\nimport {UserCredential} from 'firebase/auth';\n\nexport async function login(token: string) {\n  const headers: Record<string, string> = {\n    Authorization: `Bearer ${token}`\n  };\n\n  // This is optional. Use it if your app supports App Check – https://firebase.google.com/docs/app-check\n  if (process.env.NEXT_PUBLIC_FIREBASE_APP_CHECK_KEY) {\n    const appCheckTokenResponse = await getToken(getAppCheck(), false);\n\n    headers['X-Firebase-AppCheck'] = appCheckTokenResponse.token;\n  }\n\n  await fetch('/api/login', {\n    method: 'GET',\n    headers\n  });\n}\n\nexport async function loginWithCredential(credential: UserCredential) {\n  const idToken = await credential.user.getIdToken();\n\n  await login(idToken);\n}\n\nexport async function logout() {\n  const headers: Record<string, string> = {};\n\n  // This is optional. Use it if your app supports App Check – https://firebase.google.com/docs/app-check\n  if (process.env.NEXT_PUBLIC_FIREBASE_APP_CHECK_KEY) {\n    const appCheckTokenResponse = await getToken(getAppCheck(), false);\n\n    headers['X-Firebase-AppCheck'] = appCheckTokenResponse.token;\n  }\n\n  await fetch('/api/logout', {\n    method: 'GET',\n    headers\n  });\n}\n\nexport async function checkEmailVerification() {\n  const headers: Record<string, string> = {};\n\n  // This is optional. Use it if your app supports App Check – https://firebase.google.com/docs/app-check\n  if (process.env.NEXT_PUBLIC_FIREBASE_APP_CHECK_KEY) {\n    const appCheckTokenResponse = await getToken(getAppCheck(), false);\n\n    headers['X-Firebase-AppCheck'] = appCheckTokenResponse.token;\n  }\n\n  await fetch('/api/check-email-verification', {\n    method: 'GET',\n    headers\n  });\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/actions/login.ts",
    "content": "'use server';\n\nimport {signInWithEmailAndPassword} from 'firebase/auth';\nimport {refreshCookiesWithIdToken} from 'next-firebase-auth-edge/next/cookies';\nimport {getFirebaseAuth} from '../auth/firebase';\nimport {cookies, headers} from 'next/headers';\nimport {authConfig} from '../../config/server-config';\nimport {redirect} from 'next/navigation';\n\nexport async function loginAction(username: string, password: string) {\n  const credential = await signInWithEmailAndPassword(\n    getFirebaseAuth(),\n    username,\n    password\n  );\n\n  const idToken = await credential.user.getIdToken();\n  await refreshCookiesWithIdToken(\n    idToken,\n    await headers(),\n    await cookies(),\n    authConfig\n  );\n  redirect('/');\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/actions/refresh-cookies.ts",
    "content": "'use server';\n\nimport {cookies, headers} from 'next/headers';\nimport {getTokens} from 'next-firebase-auth-edge';\nimport {refreshServerCookies} from 'next-firebase-auth-edge/next/cookies';\nimport {authConfig} from '../../config/server-config';\n\nexport async function refreshCookies() {\n  const tokens = await getTokens(await cookies(), authConfig);\n\n  if (!tokens) {\n    throw new Error('Unauthenticated');\n  }\n\n  await refreshServerCookies(\n    await cookies(),\n    new Headers(await headers()),\n    authConfig\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/actions/user-counters.ts",
    "content": "'use server';\n\nimport {getFirestore} from 'firebase-admin/firestore';\nimport {getTokens} from 'next-firebase-auth-edge';\nimport {revalidatePath} from 'next/cache';\nimport {cookies} from 'next/headers';\nimport {getFirebaseAdminApp} from '../firebase';\nimport {authConfig} from '../../config/server-config';\n\nexport async function incrementCounter() {\n  const tokens = await getTokens(await cookies(), authConfig);\n\n  if (!tokens) {\n    throw new Error('Cannot update counter of unauthenticated user');\n  }\n\n  const db = getFirestore(getFirebaseAdminApp());\n  const snapshot = await db\n    .collection('user-counters')\n    .doc(tokens.decodedToken.uid)\n    .get();\n\n  const currentUserCounter = snapshot.data();\n\n  if (!snapshot.exists || !currentUserCounter) {\n    const userCounter = {\n      id: tokens.decodedToken.uid,\n      count: 1\n    };\n\n    await snapshot.ref.create(userCounter);\n  }\n\n  const newUserCounter = {\n    ...currentUserCounter,\n    count: currentUserCounter?.count + 1\n  };\n  await snapshot.ref.update(newUserCounter);\n\n  revalidatePath('/');\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/api/check-email-verification/route.ts",
    "content": "import {refreshNextResponseCookies} from 'next-firebase-auth-edge/next/cookies';\nimport {getTokens} from 'next-firebase-auth-edge/next/tokens';\nimport {NextResponse} from 'next/server';\nimport type { NextRequest } from 'next/server';\nimport {authConfig} from '../../../config/server-config';\n\nexport async function GET(request: NextRequest) {\n  const tokens = await getTokens(request.cookies, authConfig);\n\n  if (!tokens) {\n    throw new Error('Cannot refresh tokens of unauthenticated user');\n  }\n\n  const headers: Record<string, string> = {\n    'Content-Type': 'application/json'\n  };\n\n  const response = new NextResponse(\n    JSON.stringify({\n      success: true\n    }),\n    {\n      status: 200,\n      headers\n    }\n  );\n\n  return refreshNextResponseCookies(request, response, authConfig);\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/api/custom-claims/route.ts",
    "content": "import {getFirebaseAuth} from 'next-firebase-auth-edge/auth';\nimport {refreshNextResponseCookies} from 'next-firebase-auth-edge/next/cookies';\nimport {getTokens} from 'next-firebase-auth-edge/next/tokens';\nimport {NextResponse} from 'next/server';\nimport type {NextRequest} from 'next/server';\nimport {authConfig} from '../../../config/server-config';\n\nconst {setCustomUserClaims, getUser} = getFirebaseAuth({\n  serviceAccount: authConfig.serviceAccount,\n  apiKey: authConfig.apiKey,\n  tenantId: authConfig.tenantId,\n  enableCustomToken: authConfig.enableCustomToken\n});\n\nexport async function POST(request: NextRequest) {\n  const tokens = await getTokens(request.cookies, authConfig);\n\n  if (!tokens) {\n    throw new Error('Cannot update custom claims of unauthenticated user');\n  }\n\n  await setCustomUserClaims(tokens.decodedToken.uid, {\n    someCustomClaim: {\n      updatedAt: Date.now()\n    }\n  });\n\n  const user = await getUser(tokens.decodedToken.uid);\n  const headers: Record<string, string> = {\n    'Content-Type': 'application/json'\n  };\n\n  const response = new NextResponse(\n    JSON.stringify({\n      customClaims: user?.customClaims\n    }),\n    {\n      status: 200,\n      headers\n    }\n  );\n\n  return refreshNextResponseCookies(request, response, authConfig);\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/api/test-app-check/route.ts",
    "content": "import {NextResponse} from 'next/server';\nimport type { NextRequest } from 'next/server';\nimport {serverConfig} from '../../../config/server-config';\nimport {getAppCheck} from 'next-firebase-auth-edge/app-check';\nimport {getReferer} from 'next-firebase-auth-edge/next/utils';\n\nexport async function POST(request: NextRequest) {\n  const appCheckToken = request.headers.get('X-Firebase-AppCheck');\n  const {verifyToken} = getAppCheck({\n    serviceAccount: serverConfig.serviceAccount\n  });\n\n  if (!appCheckToken) {\n    return new NextResponse(\n      JSON.stringify({\n        message: 'X-Firebase-AppCheck header is missing'\n      }),\n      {\n        status: 400,\n        headers: {'content-type': 'application/json'}\n      }\n    );\n  }\n\n  try {\n    const response = await verifyToken(appCheckToken, {\n      referer: getReferer(request.headers) ?? ''\n    });\n\n    return new NextResponse(JSON.stringify(response.token), {\n      status: 200,\n      headers: {'content-type': 'application/json'}\n    });\n  } catch (e) {\n    return new NextResponse(\n      JSON.stringify({\n        message: (e as Error)?.message\n      }),\n      {\n        status: 500,\n        headers: {'content-type': 'application/json'}\n      }\n    );\n  }\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/api/token-test/route.ts",
    "content": "import {NextResponse} from 'next/server';\nimport type { NextRequest } from 'next/server';\nimport {getTokens} from 'next-firebase-auth-edge';\nimport {cookies} from 'next/headers';\nimport {authConfig} from '../../../config/server-config';\n\nexport async function GET(_request: NextRequest) {\n  const tokens = await getTokens(await cookies(), authConfig);\n\n  if (!tokens) {\n    throw new Error('Unauthenticated');\n  }\n\n  const headers: Record<string, string> = {\n    'Content-Type': 'application/json'\n  };\n\n  const response = new NextResponse(\n    JSON.stringify({\n      tokens\n    }),\n    {\n      status: 200,\n      headers\n    }\n  );\n\n  return response;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/api/user-counters/route.ts",
    "content": "import {NextResponse} from 'next/server';\nimport type { NextRequest } from 'next/server';\nimport {authConfig} from '../../../config/server-config';\nimport {getFirestore} from 'firebase-admin/firestore';\nimport {getTokens} from 'next-firebase-auth-edge/next/tokens';\nimport {getFirebaseAdminApp} from '../../firebase';\n\nexport async function POST(request: NextRequest) {\n  const tokens = await getTokens(request.cookies, authConfig);\n\n  if (!tokens) {\n    throw new Error('Cannot update counter of unauthenticated user');\n  }\n\n  const db = getFirestore(getFirebaseAdminApp());\n  const snapshot = await db\n    .collection('user-counters')\n    .doc(tokens.decodedToken.uid)\n    .get();\n\n  const currentUserCounter = await snapshot.data();\n\n  if (!snapshot.exists || !currentUserCounter) {\n    const userCounter = {\n      id: tokens.decodedToken.uid,\n      count: 1\n    };\n\n    await snapshot.ref.create(userCounter);\n    return NextResponse.json(userCounter);\n  }\n\n  const newUserCounter = {\n    ...currentUserCounter,\n    count: currentUserCounter.count + 1\n  };\n  await snapshot.ref.update(newUserCounter);\n\n  return NextResponse.json(newUserCounter);\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/auth/AuthContext.ts",
    "content": "import {createContext, useContext} from 'react';\nimport {UserInfo} from 'firebase/auth';\nimport {Claims} from 'next-firebase-auth-edge/auth/claims';\n\nexport interface Metadata {\n  uid: string;\n  timestamp: number;\n}\n\nexport interface User extends UserInfo {\n  idToken: string;\n  customToken?: string;\n  emailVerified: boolean;\n  customClaims: Claims;\n  metadata: Metadata;\n}\n\nexport interface AuthContextValue {\n  user: User | null;\n}\n\nexport const AuthContext = createContext<AuthContextValue>({\n  user: null\n});\n\nexport const useAuth = () => useContext(AuthContext);\n"
  },
  {
    "path": "examples/next-typescript-starter/app/auth/AuthProvider.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport {AuthContext, User} from './AuthContext';\n\nexport interface AuthProviderProps {\n  user: User | null;\n  children: React.ReactNode;\n}\n\nexport const AuthProvider: React.FunctionComponent<AuthProviderProps> = ({\n  user,\n  children\n}) => {\n  return (\n    <AuthContext.Provider\n      value={{\n        user\n      }}\n    >\n      {children}\n    </AuthContext.Provider>\n  );\n};\n"
  },
  {
    "path": "examples/next-typescript-starter/app/auth/firebase.ts",
    "content": "import { getApp, getApps, initializeApp } from 'firebase/app';\nimport {\n  connectAuthEmulator,\n  getAuth,\n  inMemoryPersistence,\n  setPersistence\n} from 'firebase/auth';\nimport { clientConfig } from '../../config/client-config';\n\nexport const getFirebaseApp = () => {\n  if (getApps().length) {\n    return getApp();\n  }\n\n  return initializeApp(clientConfig);\n};\n\nexport function getFirebaseAuth() {\n  const auth = getAuth(getFirebaseApp());\n\n  // App relies only on server token. We make sure Firebase does not store credentials in the browser.\n  // See: https://github.com/awinogrodzki/next-firebase-auth-edge/issues/143\n  setPersistence(auth, inMemoryPersistence);\n\n  if (process.env.NEXT_PUBLIC_AUTH_EMULATOR_HOST) {\n    // https://stackoverflow.com/questions/73605307/firebase-auth-emulator-fails-intermittently-with-auth-emulator-config-failed\n    (auth as unknown as any)._canInitEmulator = true;\n    connectAuthEmulator(auth, `http://${process.env.NEXT_PUBLIC_AUTH_EMULATOR_HOST}`, {\n      disableWarnings: true\n    });\n  }\n\n  if (clientConfig.tenantId) {\n    auth.tenantId = clientConfig.tenantId;\n  }\n\n  return auth;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/firebase.ts",
    "content": "import admin from 'firebase-admin';\nimport {authConfig} from '../config/server-config';\n\nconst initializeApp = () => {\n  if (!authConfig.serviceAccount) {\n    return admin.initializeApp();\n  }\n\n  // Don't use real credentials with Firebase Emulator https://firebase.google.com/docs/emulator-suite/connect_auth#admin_sdks\n  if (process.env.FIREBASE_AUTH_EMULATOR_HOST) {\n    return admin.initializeApp({\n      projectId: authConfig.serviceAccount.projectId\n    });\n  }\n\n  return admin.initializeApp({\n    credential: admin.credential.cert(authConfig.serviceAccount)\n  });\n};\n\nexport const getFirebaseAdminApp = () => {\n  if (admin.apps.length > 0) {\n    return admin.apps[0] as admin.app.App;\n  }\n\n  // admin.firestore.setLogFunction(console.log);\n\n  return initializeApp();\n};\n"
  },
  {
    "path": "examples/next-typescript-starter/app/globals.css",
    "content": "html,\nbody {\n  height: 100%;\n  padding: 0;\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,\n    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;\n}\n\na {\n  color: inherit;\n  text-decoration: none;\n}\n\n* {\n  box-sizing: border-box;\n}\n\n@media (prefers-color-scheme: dark) {\n  html {\n    color-scheme: dark;\n  }\n  body {\n    color: white;\n    background: black;\n  }\n}\n\n:root {\n  --toggle-color: rgb(204, 204, 204);\n  --background-color: #0070f3;\n  --slider-color: rgb(255, 255, 255);\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --toggle-color: rgb(255, 255, 255);\n    --background-color: #0070f3;\n    --slider-color: rgb(82, 79, 79);\n  }\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/layout.module.css",
    "content": ".container {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n}\n\n.main {\n  padding: 1.5rem 0;\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n}\n\n.footer {\n  display: flex;\n  flex: 0;\n  padding: 2rem 0;\n  border-top: 1px solid #eaeaea;\n  justify-content: center;\n  align-items: center;\n}\n\n.footer a {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  flex-grow: 1;\n}\n\n@media (prefers-color-scheme: dark) {\n  .footer {\n    border-color: #222;\n  }\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/layout.tsx",
    "content": "import { getTokens } from 'next-firebase-auth-edge';\nimport { cookies } from 'next/headers';\nimport { authConfig } from '../config/server-config';\nimport { AuthProvider } from './auth/AuthProvider';\nimport './globals.css';\nimport styles from './layout.module.css';\nimport { toUser } from './shared/user';\nimport { Metadata } from './auth/AuthContext';\n\nexport default async function RootLayout({\n  children\n}: {\n  children: React.ReactNode;\n}) {\n  const tokens = await getTokens<Metadata>(await cookies(), authConfig);\n  const user = tokens ? toUser(tokens) : null;\n\n  return (\n    <html lang=\"en\">\n      <head />\n      <body>\n        <div className={styles.container}>\n          <main className={styles.main}>\n            <AuthProvider user={user}>{children}</AuthProvider>\n          </main>\n          <footer className={styles.footer}>\n            <a\n              href=\"https://github.com/awinogrodzki/next-firebase-auth-edge\"\n              target=\"_blank\"\n            >\n              github.com/awinogrodzki/next-firebase-auth-edge\n            </a>\n          </footer>\n        </div>\n      </body>\n    </html>\n  );\n}\n\nexport const metadata = {\n  title: 'next-firebase-auth-edge example page',\n  description: 'Next.js page showcasing next-firebase-auth-edge features',\n  icons: '/favicon.ico'\n};\n"
  },
  {
    "path": "examples/next-typescript-starter/app/login/LoginPage.tsx",
    "content": "'use client';\n\nimport {\n  UserCredential,\n  getRedirectResult,\n  isSignInWithEmailLink,\n  sendSignInLinkToEmail,\n  signInWithEmailAndPassword,\n  signInWithEmailLink\n} from 'firebase/auth';\nimport Link from 'next/link';\nimport * as React from 'react';\nimport {useLoadingCallback} from 'react-loading-hook';\nimport {loginWithCredential} from '../../api';\nimport {Button} from '../../ui/Button';\nimport {ButtonGroup} from '../../ui/ButtonGroup';\nimport {MainTitle} from '../../ui/MainTitle';\nimport {PasswordForm} from '../../ui/PasswordForm';\nimport {PasswordFormValue} from '../../ui/PasswordForm/PasswordForm';\nimport {Switch} from '../../ui/Switch/Switch';\nimport {LoadingIcon} from '../../ui/icons';\nimport {getFirebaseAuth} from '../auth/firebase';\nimport {appendRedirectParam} from '../shared/redirect';\nimport {useRedirectAfterLogin} from '../shared/useRedirectAfterLogin';\nimport {useRedirectParam} from '../shared/useRedirectParam';\nimport {\n  getGoogleProvider,\n  loginWithProvider,\n  loginWithProviderUsingRedirect\n} from './firebase';\nimport styles from './login.module.css';\n\nconst auth = getFirebaseAuth();\n\nexport function LoginPage({\n  loginAction\n}: {\n  loginAction: (email: string, password: string) => void;\n}) {\n  const [hasLogged, setHasLogged] = React.useState(false);\n  const [shouldLoginWithAction, setShouldLoginWithAction] =\n    React.useState(false);\n  let [isLoginActionPending, startTransition] = React.useTransition();\n  const redirect = useRedirectParam();\n  const redirectAfterLogin = useRedirectAfterLogin();\n\n  async function handleLogin(credential: UserCredential) {\n    await loginWithCredential(credential);\n    redirectAfterLogin();\n  }\n\n  const [handleLoginWithEmailAndPassword, isEmailLoading, emailPasswordError] =\n    useLoadingCallback(async ({email, password}: PasswordFormValue) => {\n      setHasLogged(false);\n\n      const auth = getFirebaseAuth();\n\n      if (shouldLoginWithAction) {\n        startTransition(() => loginAction(email, password));\n      } else {\n        await handleLogin(\n          await signInWithEmailAndPassword(auth, email, password)\n        );\n\n        setHasLogged(true);\n      }\n    });\n\n  const [handleLoginWithGoogle, isGoogleLoading, googleError] =\n    useLoadingCallback(async () => {\n      setHasLogged(false);\n\n      const auth = getFirebaseAuth();\n      await handleLogin(await loginWithProvider(auth, getGoogleProvider(auth)));\n\n      setHasLogged(true);\n    });\n\n  const [\n    handleLoginWithGoogleUsingRedirect,\n    isGoogleUsingRedirectLoading,\n    googleUsingRedirectError\n  ] = useLoadingCallback(async () => {\n    setHasLogged(false);\n\n    const auth = getFirebaseAuth();\n    await loginWithProviderUsingRedirect(auth, getGoogleProvider(auth));\n\n    setHasLogged(true);\n  });\n\n  async function handleLoginWithRedirect() {\n    const credential = await getRedirectResult(auth);\n\n    if (credential?.user) {\n      await handleLogin(credential);\n\n      setHasLogged(true);\n    }\n  }\n\n  React.useEffect(() => {\n    handleLoginWithRedirect();\n  }, []);\n\n  const [handleLoginWithEmailLink, isEmailLinkLoading, emailLinkError] =\n    useLoadingCallback(async () => {\n      const auth = getFirebaseAuth();\n      const email = window.prompt('Please provide your email');\n\n      if (!email) {\n        return;\n      }\n\n      window.localStorage.setItem('emailForSignIn', email);\n\n      await sendSignInLinkToEmail(auth, email, {\n        url: process.env.NEXT_PUBLIC_ORIGIN + '/login',\n        handleCodeInApp: true\n      });\n    });\n\n  async function handleLoginWithEmailLinkCallback() {\n    const auth = getFirebaseAuth();\n    if (!isSignInWithEmailLink(auth, window.location.href)) {\n      return;\n    }\n\n    let email = window.localStorage.getItem('emailForSignIn');\n    if (!email) {\n      email = window.prompt('Please provide your email for confirmation');\n    }\n\n    if (!email) {\n      return;\n    }\n\n    setHasLogged(false);\n\n    await handleLogin(\n      await signInWithEmailLink(auth, email, window.location.href)\n    );\n    window.localStorage.removeItem('emailForSignIn');\n\n    setHasLogged(true);\n  }\n\n  React.useEffect(() => {\n    handleLoginWithEmailLinkCallback();\n  }, []);\n\n  return (\n    <div className={styles.page}>\n      <MainTitle>Login</MainTitle>\n      {hasLogged && (\n        <div className={styles.info}>\n          <span>\n            Redirecting to <strong>{redirect || '/'}</strong>\n          </span>\n          <LoadingIcon />\n        </div>\n      )}\n      {!hasLogged && (\n        <PasswordForm\n          loading={isEmailLoading || isLoginActionPending}\n          onSubmit={handleLoginWithEmailAndPassword}\n          actions={\n            // `firebase/auth` library is not yet compatible with Vercel's Edge environment\n            !process.env.VERCEL ? (\n              <div className={styles.loginWithAction}>\n                <Switch\n                  value={shouldLoginWithAction}\n                  onChange={setShouldLoginWithAction}\n                />\n                Login with Server Action\n              </div>\n            ) : undefined\n          }\n          error={\n            emailPasswordError ||\n            googleError ||\n            emailLinkError ||\n            googleUsingRedirectError\n          }\n        >\n          <ButtonGroup>\n            <Link\n              className={styles.link}\n              href={appendRedirectParam('/reset-password', redirect)}\n            >\n              Reset password\n            </Link>\n            <Link href={appendRedirectParam('/register', redirect)}>\n              <Button>Register</Button>\n            </Link>\n            <Button\n              loading={isGoogleLoading}\n              disabled={isGoogleLoading}\n              onClick={handleLoginWithGoogle}\n            >\n              Log in with Google (Popup)\n            </Button>\n            <Button\n              loading={isGoogleUsingRedirectLoading}\n              disabled={isGoogleUsingRedirectLoading}\n              onClick={handleLoginWithGoogleUsingRedirect}\n            >\n              Log in with Google (Redirect)\n            </Button>\n            <Button\n              loading={isEmailLinkLoading}\n              disabled={isEmailLinkLoading}\n              onClick={handleLoginWithEmailLink}\n            >\n              Log in with Email Link\n            </Button>\n          </ButtonGroup>\n        </PasswordForm>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/login/firebase.ts",
    "content": "import type {\n  Auth,\n  AuthError,\n  AuthProvider,\n  User,\n  UserCredential\n} from 'firebase/auth';\nimport {\n  browserPopupRedirectResolver,\n  GoogleAuthProvider,\n  signInWithPopup,\n  signInWithRedirect,\n  signOut,\n  useDeviceLanguage as setDeviceLanguage\n} from 'firebase/auth';\n\nconst CREDENTIAL_ALREADY_IN_USE_ERROR = 'auth/credential-already-in-use';\nexport const isCredentialAlreadyInUseError = (e: AuthError) =>\n  e?.code === CREDENTIAL_ALREADY_IN_USE_ERROR;\n\nexport const logout = async (auth: Auth): Promise<void> => {\n  return signOut(auth);\n};\n\nexport const getGoogleProvider = (auth: Auth) => {\n  const provider = new GoogleAuthProvider();\n  provider.addScope('profile');\n  provider.addScope('email');\n  setDeviceLanguage(auth);\n  provider.setCustomParameters({\n    display: 'popup'\n  });\n\n  return provider;\n};\n\nexport const loginWithProvider = async (\n  auth: Auth,\n  provider: AuthProvider\n): Promise<UserCredential> => {\n  const result = await signInWithPopup(\n    auth,\n    provider,\n    browserPopupRedirectResolver\n  );\n\n  return result;\n};\n\nexport const loginWithProviderUsingRedirect = async (\n  auth: Auth,\n  provider: AuthProvider\n): Promise<void> => {\n  await signInWithRedirect(auth, provider);\n};\n"
  },
  {
    "path": "examples/next-typescript-starter/app/login/login.module.css",
    "content": ".page {\n  width: 640px;\n  max-width: 100%;\n  height: 100%;\n  flex-direction: column;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 1.5rem;\n  gap: 1rem;\n}\n\n.info {\n  display: flex;\n  align-items: center;\n  margin-bottom: 1.5rem;\n  gap: 1rem;\n}\n\n.link {\n  display: flex;\n  justify-content: flex-end;\n  font-size: 0.675rem;\n  text-decoration: underline;\n}\n\n.titleIcon {\n  position: absolute;\n  top: 50%;\n  right: -32px;\n  transform: translateY(-50%);\n}\n\n.loginWithAction {\n  display: flex;\n  gap: 0.675rem;\n  align-items: center;\n}"
  },
  {
    "path": "examples/next-typescript-starter/app/login/page.tsx",
    "content": "import {loginAction} from '../actions/login';\nimport {LoginPage as ClientLoginPage} from './LoginPage';\n\nexport default function Login() {\n  return <ClientLoginPage loginAction={loginAction} />;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/page.module.css",
    "content": ".container {\n  padding: 0 2rem;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/page.tsx",
    "content": "import styles from './page.module.css';\nimport Link from 'next/link';\nimport {Button} from '../ui/Button';\nimport {MainTitle} from '../ui/MainTitle';\nimport {Badge} from '../ui/Badge';\nimport {Card} from '../ui/Card';\n\nexport async function generateStaticParams() {\n  return [{}];\n}\n\nexport default function Home() {\n  return (\n    <div className={styles.container}>\n      <MainTitle>\n        <span>Home</span>\n        <Badge>Static</Badge>\n      </MainTitle>\n      <Card>\n        <Link href=\"/profile\">\n          <h2>You are logged in</h2>\n          <Button style={{marginBottom: 0}}>Go to profile page</Button>\n        </Link>\n      </Card>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/profile/UserProfile/UserProfile.module.css",
    "content": ".container {\n  display: flex;\n  gap: 1rem;\n  width: 100%;\n}\n\n.title {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 1rem;\n}\n\n.claims {\n  text-align: left;\n}\n\n.claims h5 {\n  margin: 0 0 0.5rem;\n}\n\n.claims pre {\n  margin: 0;\n  padding: 0.5rem;\n  font-size: 0.675rem;\n}\n\n.content {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 1rem;\n  margin-bottom: 1.5rem;\n}\n\n.contentButton {\n  max-width: 160px;\n}\n\n.avatar {\n  width: 32px;\n  height: 32px;\n  border-radius: 16px;\n  background: #7f7f7f;\n  overflow: hidden;\n}\n\n.avatar img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  object-position: 50% 50%;\n}\n\n.section {\n  flex: 1;\n  text-align: center;\n}\n\n@media only screen and (max-width: 640px) {\n  .container {\n    flex-direction: column;\n  }\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/profile/UserProfile/UserProfile.tsx",
    "content": "'use client';\n\nimport {getToken} from '@firebase/app-check';\nimport * as React from 'react';\nimport {useLoadingCallback} from 'react-loading-hook';\n\nimport {signOut} from 'firebase/auth';\nimport {useRouter} from 'next/navigation';\nimport {checkEmailVerification, logout} from '../../../api';\nimport {getAppCheck} from '../../../app-check';\nimport {Badge} from '../../../ui/Badge';\nimport {Button} from '../../../ui/Button';\nimport {ButtonGroup} from '../../../ui/ButtonGroup';\nimport {Card} from '../../../ui/Card';\nimport {useAuth} from '../../auth/AuthContext';\nimport {getFirebaseAuth} from '../../auth/firebase';\nimport styles from './UserProfile.module.css';\nimport {incrementCounterUsingClientFirestore} from './user-counters';\nimport {refreshCookies} from '../../actions/refresh-cookies';\n\ninterface UserProfileProps {\n  count: number;\n  incrementCounter: () => void;\n}\n\nexport function UserProfile({count, incrementCounter}: UserProfileProps) {\n  const router = useRouter();\n  const {user} = useAuth();\n  const [hasLoggedOut, setHasLoggedOut] = React.useState(false);\n  const [handleLogout, isLogoutLoading] = useLoadingCallback(async () => {\n    const auth = getFirebaseAuth();\n    await signOut(auth);\n    await logout();\n\n    router.refresh();\n\n    setHasLoggedOut(true);\n  });\n\n  const [handleClaims, isClaimsLoading] = useLoadingCallback(async () => {\n    const headers: Record<string, string> = {};\n\n    // This is optional. Use it if your app supports App Check – https://firebase.google.com/docs/app-check\n    if (process.env.NEXT_PUBLIC_FIREBASE_APP_CHECK_KEY) {\n      const appCheckTokenResponse = await getToken(getAppCheck(), false);\n\n      headers['X-Firebase-AppCheck'] = appCheckTokenResponse.token;\n    }\n\n    await fetch('/api/custom-claims', {\n      method: 'POST',\n      headers\n    });\n\n    router.refresh();\n  });\n\n  const [handleAppCheck, isAppCheckLoading] = useLoadingCallback(async () => {\n    const appCheckTokenResponse = await getToken(getAppCheck(), false);\n\n    const response = await fetch('/api/test-app-check', {\n      method: 'POST',\n      headers: {\n        'X-Firebase-AppCheck': appCheckTokenResponse.token\n      }\n    });\n\n    if (response.ok) {\n      console.info(\n        'Successfully verified App Check token',\n        await response.json()\n      );\n    } else {\n      console.error('Could not verify App Check token', await response.json());\n    }\n  });\n\n  const [handleIncrementCounterApi, isIncrementCounterApiLoading] =\n    useLoadingCallback(async () => {\n      const response = await fetch('/api/user-counters', {\n        method: 'POST'\n      });\n\n      await response.json();\n      router.refresh();\n    });\n\n  const [handleIncrementCounterClient, isIncrementCounterClientLoading] =\n    useLoadingCallback(async () => {\n      if (!user) {\n        return;\n      }\n\n      if (user.customToken) {\n        await incrementCounterUsingClientFirestore(user.customToken);\n      } else {\n        console.warn(\n          'Custom token is not present. Have you set `enableCustomToken` option to `true` in `authMiddleware`?'\n        );\n      }\n\n      router.refresh();\n    });\n\n  const [handleReCheck, isReCheckLoading] = useLoadingCallback(async () => {\n    await checkEmailVerification();\n    router.refresh();\n  });\n\n  let [isIncrementCounterActionPending, startTransition] =\n    React.useTransition();\n\n  let [isRefreshCookiesActionPending, startRefreshCookiesTransition] =\n    React.useTransition();\n\n  if (!user) {\n    return null;\n  }\n\n  const isIncrementLoading =\n    isIncrementCounterApiLoading ||\n    isIncrementCounterActionPending ||\n    isIncrementCounterClientLoading;\n\n  return (\n    <div className={styles.container}>\n      <Card className={styles.section}>\n        <h3 className={styles.title}>You are logged in as</h3>\n        <div className={styles.content}>\n          <div className={styles.avatar}>\n            {user.photoURL && <img src={user.photoURL} />}\n          </div>\n          <span>{user.email}</span>\n        </div>\n\n        {!user.emailVerified && (\n          <div className={styles.content}>\n            <Badge>Email not verified.</Badge>\n            <Button\n              className={styles.contentButton}\n              loading={isReCheckLoading}\n              disabled={isReCheckLoading}\n              onClick={handleReCheck}\n            >\n              Re-check\n            </Button>\n          </div>\n        )}\n\n        <ButtonGroup>\n          <div className={styles.claims}>\n            <h5>Custom claims</h5>\n            <pre>{JSON.stringify(user.customClaims, undefined, 2)}</pre>\n          </div>\n          <div className={styles.claims}>\n            <h5>Metadata</h5>\n            <pre>{JSON.stringify(user.metadata, undefined, 2)}</pre>\n          </div>\n          <Button\n            loading={isClaimsLoading}\n            disabled={isClaimsLoading}\n            onClick={handleClaims}\n          >\n            Refresh custom user claims\n          </Button>\n          <Button\n            loading={isRefreshCookiesActionPending}\n            disabled={isRefreshCookiesActionPending}\n            onClick={() =>\n              startRefreshCookiesTransition(() => refreshCookies())\n            }\n          >\n            Refresh cookies w/ server action\n          </Button>\n          {process.env.NEXT_PUBLIC_FIREBASE_APP_CHECK_KEY && (\n            <Button\n              onClick={handleAppCheck}\n              loading={isAppCheckLoading}\n              disabled={isAppCheckLoading}\n            >\n              Test AppCheck integration\n            </Button>\n          )}\n          <Button\n            loading={isLogoutLoading || hasLoggedOut}\n            disabled={isLogoutLoading || hasLoggedOut}\n            onClick={handleLogout}\n          >\n            Log out\n          </Button>\n        </ButtonGroup>\n      </Card>\n      <Card className={styles.section}>\n        <h3 className={styles.title}>\n          {/* defaultCount is updated by server */}\n          Counter: {count}\n        </h3>\n        <ButtonGroup>\n          <Button\n            loading={isIncrementCounterApiLoading}\n            disabled={isIncrementLoading}\n            onClick={handleIncrementCounterApi}\n          >\n            Update counter w/ api endpoint\n          </Button>\n          <Button\n            loading={isIncrementCounterActionPending}\n            disabled={isIncrementLoading}\n            onClick={() => startTransition(() => incrementCounter())}\n          >\n            Update counter w/ server action\n          </Button>\n          <Button\n            loading={isIncrementCounterClientLoading}\n            disabled={isIncrementLoading}\n            onClick={handleIncrementCounterClient}\n          >\n            Update counter w/ client firestore sdk\n          </Button>\n        </ButtonGroup>\n      </Card>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/profile/UserProfile/index.ts",
    "content": "export {UserProfile} from './UserProfile';\n"
  },
  {
    "path": "examples/next-typescript-starter/app/profile/UserProfile/user-counters-server.ts",
    "content": "import {initializeServerApp} from 'firebase/app';\nimport {doc, getDoc, getFirestore} from 'firebase/firestore';\nimport {clientConfig} from '../../../config/client-config';\nimport {getAuth} from 'firebase/auth';\n\nexport async function getServerFirebase(authIdToken?: string) {\n  const app = initializeServerApp(clientConfig, {authIdToken});\n  const auth = getAuth(app);\n\n  await auth.authStateReady();\n\n  const db = getFirestore(app);\n\n  return {app, auth, db};\n}\n\nexport interface UserCounter {\n  id: string;\n  count: number;\n}\n\nexport async function getUserCounter(\n  userId: string,\n  authToken?: string\n): Promise<UserCounter | null> {\n  try {\n    const {db} = await getServerFirebase(authToken);\n\n    const counterRef = doc(db, 'user-counters', userId);\n    const counterSnap = await getDoc(counterRef);\n\n    if (counterSnap.exists()) {\n      return {\n        id: counterSnap.id,\n        ...counterSnap.data()\n      } as UserCounter;\n    }\n\n    return null;\n  } catch (error) {\n    console.error('Error fetching user counter:', error);\n    throw error;\n  }\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/profile/UserProfile/user-counters.ts",
    "content": "import {signInWithCustomToken} from 'firebase/auth';\nimport {getValidCustomToken} from 'next-firebase-auth-edge/next/client';\n\nimport {getFirebaseApp, getFirebaseAuth} from '../../auth/firebase';\nimport {\n  doc,\n  getDoc,\n  getFirestore,\n  updateDoc,\n  setDoc,\n  connectFirestoreEmulator\n} from 'firebase/firestore';\n\nconst db = getFirestore(getFirebaseApp());\n\n// Use together with Firestore Emulator https://cloud.google.com/firestore/docs/emulator#android_apple_platforms_and_web_sdks\nif (process.env.NEXT_PUBLIC_FIRESTORE_EMULATOR_HOST) {\n  const [host, port] =\n    process.env.NEXT_PUBLIC_FIRESTORE_EMULATOR_HOST.split(':');\n  connectFirestoreEmulator(db, host, Number(port));\n}\n\nconst auth = getFirebaseAuth();\n\nexport async function incrementCounterUsingClientFirestore(\n  serverCustomToken: string\n) {\n  // We use `getValidCustomToken` to fetch fresh `customToken` using /api/refresh-token endpoint if original custom token has expired.\n  // This ensures custom token is valid, even in long-running client sessions\n  const customToken = await getValidCustomToken({\n    serverCustomToken,\n    refreshTokenUrl: '/api/refresh-token'\n  });\n\n  if (!customToken) {\n    throw new Error('Invalid custom token');\n  }\n\n  const {user: firebaseUser} = await signInWithCustomToken(auth, customToken);\n\n  const docRef = doc(db, 'user-counters', firebaseUser.uid);\n  const docSnap = await getDoc(docRef);\n\n  if (docSnap.exists()) {\n    const data = docSnap.data();\n\n    await updateDoc(docRef, {count: data.count + 1});\n  } else {\n    await setDoc(docRef, {count: 1});\n  }\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/profile/page.module.css",
    "content": ".container {\n  padding: 0 1.5rem;\n  width: 800px;\n  max-width: 100%;\n  height: 100%;\n  flex-direction: column;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/profile/page.tsx",
    "content": "import styles from './page.module.css';\nimport {UserProfile} from './UserProfile';\nimport {Metadata} from 'next';\nimport {getTokens} from 'next-firebase-auth-edge/next/tokens';\nimport {cookies} from 'next/headers';\nimport {authConfig} from '../../config/server-config';\nimport {Badge} from '../../ui/Badge';\nimport {HomeLink} from '../../ui/HomeLink';\nimport {MainTitle} from '../../ui/MainTitle';\nimport {incrementCounter} from '../actions/user-counters';\nimport {getUserCounter} from './UserProfile/user-counters-server';\n\nexport default async function Profile() {\n  const tokens = await getTokens(await cookies(), authConfig);\n\n  if (!tokens) {\n    throw new Error('Cannot get counter of unauthenticated user');\n  }\n\n  const counter = await getUserCounter(tokens.decodedToken.uid, tokens.token);\n\n  return (\n    <div className={styles.container}>\n      <MainTitle>\n        <HomeLink />\n        <span>Profile</span>\n        <Badge>Rendered on server</Badge>\n      </MainTitle>\n      <UserProfile\n        count={counter?.count ?? 0}\n        incrementCounter={incrementCounter}\n      />\n    </div>\n  );\n}\n\n// Generate customized metadata based on user cookies\n// https://nextjs.org/docs/app/building-your-application/optimizing/metadata\nexport async function generateMetadata(): Promise<Metadata> {\n  const tokens = await getTokens(await cookies(), authConfig);\n\n  if (!tokens) {\n    return {};\n  }\n\n  return {\n    title: `${tokens.decodedToken.email} profile page | next-firebase-auth-edge example`\n  };\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/register/RegisterPage.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport {useLoadingCallback} from 'react-loading-hook';\nimport {\n  createUserWithEmailAndPassword,\n  sendEmailVerification\n} from 'firebase/auth';\nimport Link from 'next/link';\nimport {getFirebaseAuth} from '../auth/firebase';\nimport {Button} from '../../ui/Button';\nimport {MainTitle} from '../../ui/MainTitle';\nimport {PasswordForm} from '../../ui/PasswordForm';\nimport {PasswordFormValue} from '../../ui/PasswordForm/PasswordForm';\nimport {LoadingIcon} from '../../ui/icons';\nimport {appendRedirectParam} from '../shared/redirect';\nimport {useRedirectParam} from '../shared/useRedirectParam';\nimport styles from './register.module.css';\nimport {useRedirectAfterLogin} from '../shared/useRedirectAfterLogin';\nimport {loginWithCredential} from '../../api';\n\nexport function RegisterPage() {\n  const [hasLogged, setHasLogged] = React.useState(false);\n  const redirect = useRedirectParam();\n  const redirectAfterLogin = useRedirectAfterLogin();\n\n  const [registerWithEmailAndPassword, isRegisterLoading, error] =\n    useLoadingCallback(async ({email, password}: PasswordFormValue) => {\n      setHasLogged(false);\n      const auth = getFirebaseAuth();\n      const credential = await createUserWithEmailAndPassword(\n        auth,\n        email,\n        password\n      );\n\n      await loginWithCredential(credential);\n      await sendEmailVerification(credential.user);\n      redirectAfterLogin();\n\n      setHasLogged(true);\n    });\n\n  return (\n    <div className={styles.page}>\n      <MainTitle>Register</MainTitle>\n      {hasLogged && (\n        <div className={styles.info}>\n          <span>\n            Redirecting to <strong>{redirect || '/'}</strong>\n          </span>\n          <LoadingIcon />\n        </div>\n      )}\n      {!hasLogged && (\n        <PasswordForm\n          onSubmit={registerWithEmailAndPassword}\n          loading={isRegisterLoading}\n          error={error}\n        >\n          <Link href={appendRedirectParam('/login', redirect)}>\n            <Button disabled={isRegisterLoading}>Back to login</Button>\n          </Link>\n        </PasswordForm>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/register/firebase.ts",
    "content": "import type {Auth, AuthError, AuthProvider, User} from 'firebase/auth';\nimport {\n  browserPopupRedirectResolver,\n  GoogleAuthProvider,\n  signInWithPopup,\n  signOut,\n  useDeviceLanguage as setDeviceLanguage\n} from 'firebase/auth';\n\nconst CREDENTIAL_ALREADY_IN_USE_ERROR = 'auth/credential-already-in-use';\nexport const isCredentialAlreadyInUseError = (e: AuthError) =>\n  e?.code === CREDENTIAL_ALREADY_IN_USE_ERROR;\n\nexport const logout = async (auth: Auth): Promise<void> => {\n  return signOut(auth);\n};\n\nexport const getGoogleProvider = (auth: Auth) => {\n  const provider = new GoogleAuthProvider();\n  provider.addScope('profile');\n  provider.addScope('email');\n  setDeviceLanguage(auth);\n  provider.setCustomParameters({\n    display: 'popup'\n  });\n\n  return provider;\n};\n\nexport const loginWithProvider = async (\n  auth: Auth,\n  provider: AuthProvider\n): Promise<User> => {\n  const result = await signInWithPopup(\n    auth,\n    provider,\n    browserPopupRedirectResolver\n  );\n\n  return result.user;\n};\n"
  },
  {
    "path": "examples/next-typescript-starter/app/register/page.tsx",
    "content": "import {RegisterPage} from './RegisterPage';\n\nexport default function Register() {\n  return <RegisterPage />;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/register/register.module.css",
    "content": ".page {\n  width: 640px;\n  max-width: 100%;\n  height: 100%;\n  flex-direction: column;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 1.5rem;\n}\n\n.info {\n  display: flex;\n  align-items: center;\n  margin-bottom: 1.5rem;\n  gap: 1rem;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/reset-password/ResetPasswordPage.module.css",
    "content": ".page {\n  width: 640px;\n  max-width: 100%;\n  height: 100%;\n  flex-direction: column;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 1.5rem;\n}\n\n.info {\n  display: flex;\n  align-items: center;\n  margin: 0;\n}\n\n.form {\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/reset-password/ResetPasswordPage.tsx",
    "content": "'use client';\n\nimport * as React from 'react';\nimport {sendPasswordResetEmail} from 'firebase/auth';\nimport Link from 'next/link';\nimport {useLoadingCallback} from 'react-loading-hook';\nimport {getFirebaseAuth} from '../auth/firebase';\nimport {Button} from '../../ui/Button';\nimport {FormError} from '../../ui/FormError';\nimport {Input} from '../../ui/Input';\nimport {MainTitle} from '../../ui/MainTitle';\nimport {appendRedirectParam} from '../shared/redirect';\nimport {useRedirectParam} from '../shared/useRedirectParam';\nimport styles from './ResetPasswordPage.module.css';\nimport {ChangeEvent} from 'react';\n\nexport function ResetPasswordPage() {\n  const [email, setEmail] = React.useState('');\n  const [isSent, setIsSent] = React.useState(false);\n  const redirect = useRedirectParam();\n  const [sendResetInstructions, loading, error] = useLoadingCallback(\n    async (event: React.FormEvent) => {\n      event.preventDefault();\n      event.stopPropagation();\n\n      const auth = getFirebaseAuth();\n      setIsSent(false);\n      await sendPasswordResetEmail(auth, email);\n      setEmail('');\n      setIsSent(true);\n    }\n  );\n\n  return (\n    <div className={styles.page}>\n      <MainTitle>Reset password</MainTitle>\n      <form onSubmit={sendResetInstructions} className={styles.form}>\n        <Input\n          required\n          value={email}\n          onChange={(e: ChangeEvent<HTMLInputElement>) =>\n            setEmail(e.target.value)\n          }\n          name=\"email\"\n          type=\"email\"\n          placeholder=\"Email address\"\n        />\n        {isSent && (\n          <p className={styles.info}>Instructions sent. Check your email.</p>\n        )}\n        {error && <FormError>{error?.message}</FormError>}\n        <Button\n          loading={loading}\n          disabled={loading}\n          variant=\"contained\"\n          type=\"submit\"\n        >\n          Send reset instructions\n        </Button>\n        <Link href={appendRedirectParam('/login', redirect)}>\n          <Button disabled={loading}>Back to login</Button>\n        </Link>\n      </form>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/reset-password/firebase.ts",
    "content": "import type {Auth, AuthError, AuthProvider, User} from 'firebase/auth';\nimport {\n  browserPopupRedirectResolver,\n  GoogleAuthProvider,\n  signInWithPopup,\n  signOut,\n  useDeviceLanguage as setDefaultLanguage\n} from 'firebase/auth';\n\nconst CREDENTIAL_ALREADY_IN_USE_ERROR = 'auth/credential-already-in-use';\nexport const isCredentialAlreadyInUseError = (e: AuthError) =>\n  e?.code === CREDENTIAL_ALREADY_IN_USE_ERROR;\n\nexport const logout = async (auth: Auth): Promise<void> => {\n  return signOut(auth);\n};\n\nexport const getGoogleProvider = (auth: Auth) => {\n  const provider = new GoogleAuthProvider();\n  provider.addScope('profile');\n  provider.addScope('email');\n  setDefaultLanguage(auth);\n  provider.setCustomParameters({\n    display: 'popup'\n  });\n\n  return provider;\n};\n\nexport const loginWithProvider = async (\n  auth: Auth,\n  provider: AuthProvider\n): Promise<User> => {\n  const result = await signInWithPopup(\n    auth,\n    provider,\n    browserPopupRedirectResolver\n  );\n\n  return result.user;\n};\n"
  },
  {
    "path": "examples/next-typescript-starter/app/reset-password/page.tsx",
    "content": "import {ResetPasswordPage} from './ResetPasswordPage';\n\nexport default function ResetPassword() {\n  return <ResetPasswordPage />;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/shared/redirect.ts",
    "content": "export function appendRedirectParam(url: string, redirectUrl: string | null) {\n  if (redirectUrl) {\n    return `${url}?redirect=${redirectUrl}`;\n  }\n\n  return url;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/shared/useRedirectAfterLogin.ts",
    "content": "import {useRouter} from 'next/navigation';\nimport {useRedirectParam} from './useRedirectParam';\n\nexport function useRedirectAfterLogin() {\n  const router = useRouter();\n  const redirect = useRedirectParam();\n\n  return function () {\n    router.push(redirect ?? '/');\n    router.refresh();\n  };\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/shared/useRedirectParam.ts",
    "content": "import {useSearchParams} from 'next/navigation';\n\nexport function useRedirectParam(): string | null {\n  const params = useSearchParams();\n\n  return params?.get('redirect') ?? null;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/app/shared/user.ts",
    "content": "import {Tokens} from 'next-firebase-auth-edge';\nimport {Metadata, User} from '../auth/AuthContext';\nimport {filterStandardClaims} from 'next-firebase-auth-edge/auth/claims';\n\nexport const toUser = ({token, customToken, decodedToken, metadata}: Tokens<Metadata>): User => {\n  const {\n    uid,\n    email,\n    picture: photoURL,\n    email_verified: emailVerified,\n    phone_number: phoneNumber,\n    name: displayName,\n    source_sign_in_provider: signInProvider\n  } = decodedToken;\n\n  const customClaims = filterStandardClaims(decodedToken);\n\n  return {\n    uid,\n    email: email ?? null,\n    displayName: displayName ?? null,\n    photoURL: photoURL ?? null,\n    phoneNumber: phoneNumber ?? null,\n    emailVerified: emailVerified ?? false,\n    providerId: signInProvider,\n    customClaims,\n    idToken: token,\n    customToken,\n    metadata\n  };\n};\n"
  },
  {
    "path": "examples/next-typescript-starter/app-check/index.ts",
    "content": "import {\n  AppCheck,\n  initializeAppCheck,\n  ReCaptchaEnterpriseProvider,\n} from \"@firebase/app-check\";\nimport { getFirebaseApp } from \"../app/auth/firebase\";\nimport { FirebaseApp } from \"@firebase/app\";\n\nlet appCheck: AppCheck | null = null;\n\nexport function getOrInitializeAppCheck(app: FirebaseApp): AppCheck {\n  if (appCheck) {\n    return appCheck;\n  }\n\n  // Firebase uses a global variable to check if app check is enabled in a dev environment\n  if (process.env.NODE_ENV !== \"production\") {\n    Object.assign(window, {\n      FIREBASE_APPCHECK_DEBUG_TOKEN:\n        process.env.NEXT_PUBLIC_APP_CHECK_DEBUG_TOKEN,\n    });\n  }\n\n  return (appCheck = initializeAppCheck(app, {\n    provider: new ReCaptchaEnterpriseProvider(\n      process.env.NEXT_PUBLIC_FIREBASE_APP_CHECK_KEY!\n    ),\n    isTokenAutoRefreshEnabled: true, // Set to true to allow auto-refresh.\n  }));\n}\n\nexport function getAppCheck() {\n  const app = getFirebaseApp();\n\n  return getOrInitializeAppCheck(app);\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/config/client-config.ts",
    "content": "export const clientConfig = {\n  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,\n  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,\n  databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL!,\n  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,\n  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID!,\n  // Optional – required if your app uses AppCheck – https://firebase.google.com/docs/app-check\n  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID!,\n  // Optional – required if your app uses Multi-Tenancy – https://cloud.google.com/identity-platform/docs/multi-tenancy-authentication\n  tenantId: process.env.NEXT_PUBLIC_FIREBASE_AUTH_TENANT_ID\n};\n"
  },
  {
    "path": "examples/next-typescript-starter/config/server-config.ts",
    "content": "import {TokenSet} from 'next-firebase-auth-edge/auth';\nimport {clientConfig} from './client-config';\n\nexport const serverConfig = {\n  useSecureCookies: process.env.USE_SECURE_COOKIES === 'true',\n  firebaseApiKey: process.env.FIREBASE_API_KEY!,\n  serviceAccount: process.env.FIREBASE_ADMIN_PRIVATE_KEY\n    ? {\n        projectId: process.env.FIREBASE_PROJECT_ID!,\n        clientEmail: process.env.FIREBASE_ADMIN_CLIENT_EMAIL!,\n        privateKey: process.env.FIREBASE_ADMIN_PRIVATE_KEY.replace(\n          /\\\\n/g,\n          '\\n'\n        )!\n      }\n    : undefined\n};\n\nexport const authConfig = {\n  apiKey: serverConfig.firebaseApiKey,\n  cookieName: 'AuthToken',\n  cookieSignatureKeys: [\n    process.env.COOKIE_SECRET_CURRENT!,\n    process.env.COOKIE_SECRET_PREVIOUS!\n  ],\n  cookieSerializeOptions: {\n    path: '/',\n    httpOnly: true,\n    secure: serverConfig.useSecureCookies, // Set this to true on HTTPS environments\n    sameSite: 'lax' as const,\n    maxAge: 12 * 60 * 60 * 24 // twelve days\n  },\n  serviceAccount: serverConfig.serviceAccount,\n  // Set to false in Firebase Hosting environment due to https://stackoverflow.com/questions/44929653/firebase-cloud-function-wont-store-cookie-named-other-than-session\n  enableMultipleCookies: true,\n  // Set to false if you're not planning to use `signInWithCustomToken` Firebase Client SDK method\n  enableCustomToken: true,\n  enableTokenRefreshOnExpiredKidHeader: true,\n  debug: false,\n  tenantId: clientConfig.tenantId,\n  getMetadata: async (tokens: TokenSet) => {\n    return {uid: tokens.decodedIdToken.uid, timestamp: new Date().getTime()};\n  },\n  dynamicCustomClaimsKeys: ['someCustomClaim']\n};\n"
  },
  {
    "path": "examples/next-typescript-starter/eslint.config.mjs",
    "content": "import { dirname } from 'path';\nimport { fileURLToPath } from 'url';\nimport { FlatCompat } from '@eslint/eslintrc';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst compat = new FlatCompat({\n  baseDirectory: __dirname,\n});\n\nconst eslintConfig = [...compat.extends('next/core-web-vitals')];\n\nexport default eslintConfig;\n"
  },
  {
    "path": "examples/next-typescript-starter/firebase.json",
    "content": "{\n  \"emulators\": {\n    \"singleProjectMode\": true,\n    \"auth\": {\n      \"port\": 9099\n    },\n    \"firestore\": {\n      \"port\": 8080\n    },\n    \"ui\": {\n      \"enabled\": true\n    }\n  }\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n/// <reference types=\"next/navigation-types/compat/navigation\" />\nimport \"./.next/types/routes.d.ts\";\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "examples/next-typescript-starter/next.config.js",
    "content": "const path = require('path');\n\n/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  turbopack: {\n    root: path.resolve(__dirname),\n  },\n  // Needed for `signInWithRedirect` and custom `authDomain` configuration. See https://firebase.google.com/docs/auth/web/redirect-best-practices#proxy-requests\n  // If you don't plan to use `signInWithRedirect` or custom `authDomain`, you can safely remove `rewrites` config.\n  async rewrites() {\n    return [\n      {\n        source: '/__/auth',\n        destination: `https://${process.env.FIREBASE_PROJECT_ID}.firebaseapp.com/__/auth`\n      },\n      {\n        source: '/__/auth/:path*',\n        destination: `https://${process.env.FIREBASE_PROJECT_ID}.firebaseapp.com/__/auth/:path*`\n      },\n      {\n        source: '/__/firebase/init.json',\n        destination: `https://${process.env.FIREBASE_PROJECT_ID}.firebaseapp.com/__/firebase/init.json`\n      }\n    ];\n  },\n  typescript: {\n    // Type conflicts are expected with `link:` dependencies due to duplicate\n    // `next` installations using `unique symbol` in type declarations.\n    // This does not affect real consumers who install the package from npm.\n    ignoreBuildErrors: true,\n  },\n  env: {\n    VERCEL: process.env.VERCEL\n  }\n};\n\nmodule.exports = nextConfig;\n"
  },
  {
    "path": "examples/next-typescript-starter/package.json",
    "content": "{\n  \"name\": \"next-typescript-starter\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start -p $PORT\",\n    \"lint\": \"eslint .\"\n  },\n  \"dependencies\": {\n    \"@types/node\": \"^22.2.0\",\n    \"firebase\": \"^12.1.0\",\n    \"firebase-admin\": \"^13.4.0\",\n    \"next\": \"^16.1.6\",\n    \"next-firebase-auth-edge\": \"1.12.0-canary.1\",\n    \"react\": \"^19.1.1\",\n    \"react-dom\": \"^19.1.1\",\n    \"react-loading-hook\": \"1.1.2\",\n    \"typescript\": \"^5.5.4\"\n  },\n  \"devDependencies\": {\n    \"@eslint/eslintrc\": \"^3.1.0\",\n    \"@types/react\": \"^19.1.9\",\n    \"eslint\": \"^9.9.0\",\n    \"eslint-config-next\": \"^16.1.6\"\n  }\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/pages/api/tokens.ts",
    "content": "import {NextApiRequest, NextApiResponse} from 'next';\nimport {getApiRequestTokens} from 'next-firebase-auth-edge/next/tokens';\nimport {authConfig} from '../../config/server-config';\n\nexport default async function handler(\n  req: NextApiRequest,\n  res: NextApiResponse\n) {\n  const tokens = await getApiRequestTokens(req, {\n    apiKey: authConfig.apiKey,\n    cookieName: authConfig.cookieName,\n    cookieSignatureKeys: authConfig.cookieSignatureKeys,\n    serviceAccount: authConfig.serviceAccount\n  });\n\n  return res.status(200).json({tokens});\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/proxy.ts",
    "content": "import {NextResponse} from 'next/server';\nimport type {NextRequest} from 'next/server';\nimport {\n  authMiddleware,\n  redirectToHome,\n  redirectToLogin\n} from 'next-firebase-auth-edge';\nimport {authConfig} from './config/server-config';\n\nconst PRIVATE_PATHS = ['/', '/profile'];\nconst PUBLIC_PATHS = ['/register', '/login', '/reset-password'];\n\nexport async function proxy(request: NextRequest) {\n  return authMiddleware(request, {\n    loginPath: '/api/login',\n    logoutPath: '/api/logout',\n    refreshTokenPath: '/api/refresh-token',\n    debug: authConfig.debug,\n    enableMultipleCookies: authConfig.enableMultipleCookies,\n    enableCustomToken: authConfig.enableCustomToken,\n    apiKey: authConfig.apiKey,\n    cookieName: authConfig.cookieName,\n    cookieSerializeOptions: authConfig.cookieSerializeOptions,\n    cookieSignatureKeys: authConfig.cookieSignatureKeys,\n    serviceAccount: authConfig.serviceAccount,\n    enableTokenRefreshOnExpiredKidHeader:\n      authConfig.enableTokenRefreshOnExpiredKidHeader,\n    tenantId: authConfig.tenantId,\n    dynamicCustomClaimsKeys: authConfig.dynamicCustomClaimsKeys,\n    handleValidToken: async ({token, decodedToken, customToken}, headers) => {      \n      // Authenticated user should not be able to access /login, /register and /reset-password routes\n      if (PUBLIC_PATHS.includes(request.nextUrl.pathname)) {\n        return redirectToHome(request);\n      }\n\n      return NextResponse.next({\n        request: {\n          headers\n        }\n      });\n    },\n    handleInvalidToken: async (_reason) => {\n      return redirectToLogin(request, {\n        path: '/login',\n        privatePaths: PRIVATE_PATHS\n      });\n    },\n    handleError: async (error) => {\n      console.error('Unhandled authentication error', {error});\n\n      return redirectToLogin(request, {\n        path: '/login',\n        privatePaths: PRIVATE_PATHS\n      });\n    },\n    getMetadata: authConfig.getMetadata\n  });\n}\n\nexport const config = {\n  matcher: [\n    '/',\n    '/((?!_next|favicon.ico|__/auth|__/firebase|api|.*\\\\.).*)',\n    // Middleware api routes\n    '/api/login',\n    '/api/logout',\n    '/api/refresh-token',\n    // App api routes\n    '/api/custom-claims',\n    '/api/user-counters'\n  ]\n};\n"
  },
  {
    "path": "examples/next-typescript-starter/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\",\n    \".next/dev/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/Badge/Badge.module.css",
    "content": ".badge {\n  font-size: 0.675rem;\n  line-height: 1rem;\n  color: #0070f3;\n  border: 1px solid #0070f3;\n  padding: 0.5rem;\n  letter-spacing: 0;\n  border-radius: 1rem;\n}\n\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/Badge/Badge.tsx",
    "content": "import styles from './Badge.module.css';\nimport {cx} from '../classNames';\nimport {JSX} from 'react';\nexport function Badge(props: JSX.IntrinsicElements['span']) {\n  return <span {...props} className={cx(styles.badge, props.className)} />;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/Badge/index.ts",
    "content": "export { Badge } from \"./Badge\";\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/Button/Button.module.css",
    "content": ".button {\n  font-size: 1rem;\n  padding: 0.75rem 1rem;\n  color: inherit;\n  text-decoration: none;\n  background: transparent;\n  border: 1px solid #0070f3;\n  border-radius: 10px;\n  transition: color 0.15s ease, border-color 0.15s ease;\n  cursor: pointer;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  white-space: nowrap;\n  max-width: 100%;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  width: 280px;\n  min-width: 0;\n}\n\n.button:last-of-type {\n  margin-bottom: 0;\n}\n\n.button[disabled] {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.button:not([disabled]):hover,\n.button:not([disabled]):focus,\n.button:not([disabled]):active {\n  color: #fff;\n  background-color: #0070f3;\n}\n\n.icon {\n  margin-right: 0.5rem;\n}\n\n.outlined {\n}\n.contained {\n  color: #fff;\n  border-color: #0070f3;\n  background-color: #0070f3;\n}\n.contained:not([disabled]):hover,\n.contained:not([disabled]):focus,\n.contained:not([disabled]):active {\n  color: #fff;\n  border-color: #015ac0;\n  background-color: #015ac0;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/Button/Button.tsx",
    "content": "import * as React from 'react';\nimport styles from './Button.module.css';\nimport {LoadingIcon} from '../icons';\nimport {cx} from '../classNames';\nimport {JSX} from 'react';\n\nconst variantClassNames = {\n  contained: styles.contained,\n  outlined: styles.outlined\n};\n\nexport function Button({\n  loading,\n  children,\n  variant = 'outlined',\n  ...props\n}: JSX.IntrinsicElements['button'] & {\n  loading?: boolean;\n  variant?: 'contained' | 'outlined';\n}) {\n  return (\n    <button\n      {...props}\n      className={cx(styles.button, variantClassNames[variant], props.className)}\n    >\n      {loading && <LoadingIcon className={styles.icon} />}\n      <span>{children}</span>\n    </button>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/Button/index.ts",
    "content": "export { Button } from \"./Button\";\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/ButtonGroup/ButtonGroup.module.css",
    "content": ".group {\n  display: flex;\n  flex-direction: column;\n  padding: 0;\n  gap: 1rem;\n  max-width: 100%;\n}\n\n.group a {\n  width: 100%;\n  display: flex;\n}\n\n.group button {\n  width: 100%;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/ButtonGroup/ButtonGroup.tsx",
    "content": "import styles from './ButtonGroup.module.css';\nimport {cx} from '../classNames';\nimport {JSX} from 'react';\nexport function ButtonGroup(props: JSX.IntrinsicElements['div']) {\n  return <div {...props} className={cx(styles.group, props.className)} />;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/ButtonGroup/index.ts",
    "content": "export { ButtonGroup } from \"./ButtonGroup\";\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/Card/Card.module.css",
    "content": ".card {\n  display: block;\n  margin: 1rem auto;\n  padding: 1.5rem;\n  color: inherit;\n  text-decoration: none;\n  border: 1px solid #eaeaea;\n  border-radius: 10px;\n  transition: color 0.15s ease, border-color 0.15s ease;\n  max-width: 100%;\n  text-align: center;\n  min-width: 0px;\n}\n\n.card h2 {\n  margin: 0 0 1.5rem 0;\n  font-size: 1.5rem;\n}\n\n.card p {\n  margin: 0;\n  font-size: 1.25rem;\n  line-height: 1.5;\n}\n\n@media (prefers-color-scheme: dark) {\n  .card {\n    border-color: #222;\n  }\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/Card/Card.tsx",
    "content": "import styles from './Card.module.css';\nimport {cx} from '../classNames';\nimport {JSX} from 'react';\nexport function Card(props: JSX.IntrinsicElements['div']) {\n  return <div {...props} className={cx(styles.card, props.className)} />;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/Card/index.ts",
    "content": "export { Card } from \"./Card\";\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/FormError/FormError.module.css",
    "content": ".error {\n  font-size: 0.675rem;\n  line-height: 1rem;\n  color: #da4e42;\n  border: 1px solid #da4e42;\n  padding: 0.5rem;\n  letter-spacing: 0;\n  border-radius: 0.5rem;\n}\n\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/FormError/FormError.tsx",
    "content": "import styles from './FormError.module.css';\nimport {cx} from '../classNames';\nimport {JSX} from 'react';\nexport function FormError(props: JSX.IntrinsicElements['span']) {\n  return <span {...props} className={cx(styles.error, props.className)} />;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/FormError/index.ts",
    "content": "export { FormError } from \"./FormError\";\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/HomeLink/HomeLink.module.css",
    "content": ".home {\n  display: inline-flex;\n  width: 3rem;\n  height: 3rem;\n  align-items: center;\n  justify-content: center;\n  border-radius: 1.5rem;\n  transition: all 0.175s ease-in-out;\n  padding: 0.5rem;\n  cursor: pointer;\n}\n\n.home:hover {\n  background-color: rgba(0, 0, 0, 0.1);\n}\n\n@media (prefers-color-scheme: dark) {\n  .home:hover {\n    background: rgba(255, 255, 255, 0.1);\n  }\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/HomeLink/HomeLink.tsx",
    "content": "import styles from \"./HomeLink.module.css\";\nimport { HomeIcon } from \"../icons/HomeIcon\";\nimport Link from \"next/link\";\n\nexport function HomeLink() {\n  return (\n    <Link className={styles.home} href=\"/\">\n      <HomeIcon />\n    </Link>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/HomeLink/index.ts",
    "content": "export { HomeLink } from \"./HomeLink\";\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/IconButton/IconButton.module.css",
    "content": ".button {\n  display: inline-flex;\n  width: 2rem;\n  height: 2rem;\n  align-items: center;\n  justify-content: center;\n  border-radius: 1rem;\n  transition: all 0.175s ease-in-out;\n  background-color: transparent;\n  border: none;\n  padding: 0.5rem;\n  cursor: pointer;\n}\n\n.button:hover {\n  background-color: rgba(0, 0, 0, 0.1);\n}\n\n@media (prefers-color-scheme: dark) {\n  .button:hover {\n    background: rgba(255, 255, 255, 0.1);\n  }\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/IconButton/IconButton.tsx",
    "content": "import styles from './IconButton.module.css';\nimport {cx} from '../classNames';\nimport {JSX} from 'react';\n\nexport function IconButton(props: JSX.IntrinsicElements['button']) {\n  return (\n    <button\n      {...props}\n      className={cx(styles.button, props.className)}\n      type={props.type ?? 'button'}\n    />\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/IconButton/index.ts",
    "content": "export { IconButton } from \"./IconButton\";\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/Input/Input.module.css",
    "content": ".input {\n  display: flex;\n  font-size: 1rem;\n  padding: 0.75rem 1rem;\n  color: inherit;\n  text-decoration: none;\n  background: transparent;\n  border: 1px solid #0070f3;\n  border-radius: 10px;\n  transition: color 0.15s ease, border-color 0.15s ease;\n  max-width: 100%;\n  width: 280px;\n}\n\n.input:disabled {\n  opacity: 0.5;\n}"
  },
  {
    "path": "examples/next-typescript-starter/ui/Input/Input.tsx",
    "content": "import * as React from 'react';\nimport styles from './Input.module.css';\nimport {cx} from '../classNames';\nimport {JSX} from 'react';\n\nexport function Input({children, ...props}: JSX.IntrinsicElements['input']) {\n  return <input {...props} className={cx(styles.input, props.className)} />;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/Input/index.ts",
    "content": "export { Input } from \"./Input\";\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/MainTitle/MainTitle.module.css",
    "content": ".title {\n  position: relative;\n  margin: 0 0 0.63em;\n  line-height: 1.15;\n  font-size: 4rem;\n  font-style: normal;\n  font-weight: 800;\n  letter-spacing: -0.025em;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 1.5rem;\n}\n\n.title a {\n  text-decoration: none;\n  color: #0070f3;\n}\n\n@media (prefers-color-scheme: dark) {\n  .title {\n    background: linear-gradient(180deg, #ffffff 0%, #aaaaaa 100%);\n    -webkit-background-clip: text;\n    -webkit-text-fill-color: transparent;\n    background-clip: text;\n    text-fill-color: transparent;\n  }\n  .title a {\n    background: linear-gradient(180deg, #0070f3 0%, #0153af 100%);\n    -webkit-background-clip: text;\n    -webkit-text-fill-color: transparent;\n    background-clip: text;\n    text-fill-color: transparent;\n  }\n}\n\n@media only screen and (max-width: 960px) {\n  .title {\n    font-size: 2rem;\n    flex-direction: column;\n  }\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/MainTitle/MainTitle.tsx",
    "content": "import styles from './MainTitle.module.css';\nimport {cx} from '../classNames';\nimport {JSX} from 'react';\nexport function MainTitle(props: JSX.IntrinsicElements['h1']) {\n  return <h1 {...props} className={cx(styles.title, props.className)} />;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/MainTitle/index.ts",
    "content": "export { MainTitle } from \"./MainTitle\";\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/PasswordForm/PasswordForm.module.css",
    "content": ".form {\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n}\n\n.container {\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n}\n\n.input {\n  display: flex;\n  align-items: center;\n  gap: 0.5rem;\n  position: relative;\n}\n\n.adornment {\n  position: absolute;\n  top: 50%;\n  right: 0.5rem;\n  margin-top: -1rem;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/PasswordForm/PasswordForm.tsx",
    "content": "import * as React from 'react';\nimport styles from './PasswordForm.module.css';\nimport {cx} from '../classNames';\nimport {Input} from '../Input';\nimport {IconButton} from '../IconButton';\nimport {VisibleIcon} from '../icons/VisibleIcon';\nimport {HiddenIcon} from '../icons/HiddenIcon';\nimport {Button} from '../Button';\nimport {FirebaseError} from '@firebase/util';\nimport {FormError} from '../FormError';\nimport {JSX} from 'react';\n\nexport interface PasswordFormValue {\n  email: string;\n  password: string;\n}\n\ninterface PasswordFormProps\n  extends Omit<JSX.IntrinsicElements['form'], 'onSubmit'> {\n  loading: boolean;\n  onSubmit: (value: PasswordFormValue) => void;\n  actions?: JSX.Element;\n  disabled?: boolean;\n  error?: FirebaseError;\n}\nexport function PasswordForm({\n  children,\n  loading,\n  disabled,\n  error,\n  onSubmit,\n  actions,\n  ...props\n}: PasswordFormProps) {\n  const [email, setEmail] = React.useState('');\n  const [password, setPassword] = React.useState('');\n  const [isHidden, setIsHidden] = React.useState(true);\n\n  function handleSubmit(event: React.FormEvent) {\n    event.preventDefault();\n    event.stopPropagation();\n\n    onSubmit({\n      email,\n      password\n    });\n  }\n\n  return (\n    <div className={cx(styles.container, props.className)}>\n      <form onSubmit={handleSubmit} {...props} className={styles.form}>\n        <Input\n          required\n          value={email}\n          onChange={(e) => setEmail(e.target.value)}\n          name=\"email\"\n          type=\"email\"\n          placeholder=\"Email address\"\n          disabled={disabled}\n        />\n        <div className={styles.input}>\n          <Input\n            required\n            name=\"password\"\n            value={password}\n            onChange={(e) => setPassword(e.target.value)}\n            type={isHidden ? 'password' : 'text'}\n            placeholder=\"Password\"\n            minLength={8}\n            disabled={disabled}\n          />\n          {(isHidden && (\n            <IconButton\n              onClick={() => setIsHidden(false)}\n              className={styles.adornment}\n            >\n              <VisibleIcon />\n            </IconButton>\n          )) || (\n            <IconButton\n              onClick={() => setIsHidden(true)}\n              className={styles.adornment}\n            >\n              <HiddenIcon />\n            </IconButton>\n          )}\n        </div>\n        {actions}\n        {error && <FormError>{error.message}</FormError>}\n        <Button\n          loading={loading}\n          disabled={loading || disabled}\n          variant=\"contained\"\n          type=\"submit\"\n        >\n          Submit\n        </Button>\n      </form>\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/PasswordForm/index.ts",
    "content": "export { PasswordForm } from \"./PasswordForm\";\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/Switch/Switch.module.css",
    "content": ".switch {\n  position: relative;\n  display: inline-block;\n  width: 40px;\n  height: 22px;\n}\n\n.switch input {\n  opacity: 0;\n  width: 0;\n  height: 0;\n}\n\n.slider {\n  position: absolute;\n  cursor: pointer;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background-color: var(--slider-color);\n  -webkit-transition: .4s;\n  transition: .4s;\n  border-radius: 22px;\n  box-shadow: 0 0 1px var(--background-color);\n}\n\n.slider:before {\n  position: absolute;\n  content: \"\";\n  height: 14px;\n  width: 14px;\n  left: 4px;\n  bottom: 4px;\n  background-color: var(--toggle-color);\n  -webkit-transition: .4s;\n  transition: .4s;\n  border-radius: 50%;\n}\n\ninput:checked + .slider {\n  background-color: var(--background-color)\n}\n\ninput:checked + .slider:before {\n  transform: translateX(18px);\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/Switch/Switch.tsx",
    "content": "import styles from './Switch.module.css';\n\ninterface SwitchProps {\n  value: boolean;\n  onChange: (value: boolean) => void;\n}\n\nexport function Switch({value, onChange}: SwitchProps) {\n  return (\n    <label className={styles.switch}>\n      <input\n        checked={value}\n        onChange={(e) => onChange(e.target.checked)}\n        type=\"checkbox\"\n      />\n      <span className={styles.slider} />\n    </label>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/Switch/index.ts",
    "content": ""
  },
  {
    "path": "examples/next-typescript-starter/ui/Switch/vars.css",
    "content": ":root {\n  --toggle-color: 204, 204, 204;\n  --background-color: 214, 219, 220;\n  --slider-color: 255, 255, 255;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --toggle-color: 51, 51, 51;\n    --background-color: 0, 0, 0;\n    --slider-color: 0, 0, 0;\n  }\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/classNames.ts",
    "content": "export function cx(...className: (string | undefined)[]): string {\n  return className.filter(Boolean).join(\" \");\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/icons/HiddenIcon.tsx",
    "content": "import * as React from 'react';\nimport styles from './icons.module.css';\nimport {JSX} from 'react';\n\nexport function HiddenIcon(props: JSX.IntrinsicElements['span']) {\n  return (\n    <span\n      {...props}\n      className={[styles.icon, props.className].filter(Boolean).join(' ')}\n    >\n      <svg\n        focusable=\"false\"\n        aria-hidden=\"true\"\n        viewBox=\"0 0 24 24\"\n        fill=\"currentColor\"\n      >\n        <path d=\"M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78 3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z\" />\n      </svg>\n    </span>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/icons/HomeIcon.tsx",
    "content": "import * as React from 'react';\nimport styles from './icons.module.css';\nimport {JSX} from 'react';\n\nexport function HomeIcon(props: JSX.IntrinsicElements['span']) {\n  return (\n    <span\n      {...props}\n      className={[styles.icon, props.className].filter(Boolean).join(' ')}\n    >\n      <svg\n        focusable=\"false\"\n        aria-hidden=\"true\"\n        viewBox=\"0 0 24 24\"\n        fill=\"currentColor\"\n      >\n        <path d=\"M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z\"></path>\n      </svg>\n    </span>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/icons/LoadingIcon.tsx",
    "content": "import * as React from 'react';\nimport styles from './icons.module.css';\nimport {JSX} from 'react';\n\nexport function LoadingIcon(props: JSX.IntrinsicElements['span']) {\n  return (\n    <span\n      {...props}\n      className={[styles.icon, props.className].filter(Boolean).join(' ')}\n    >\n      <svg\n        version=\"1.1\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        xmlnsXlink=\"http://www.w3.org/1999/xlink\"\n        x=\"0px\"\n        y=\"0px\"\n        width=\"24px\"\n        height=\"24px\"\n        viewBox=\"0 0 40 40\"\n        enableBackground=\"new 0 0 40 40\"\n        xmlSpace=\"preserve\"\n      >\n        <path\n          opacity=\"0.2\"\n          fill=\"currentColor\"\n          d=\"M20.201,5.169c-8.254,0-14.946,6.692-14.946,14.946c0,8.255,6.692,14.946,14.946,14.946\n    s14.946-6.691,14.946-14.946C35.146,11.861,28.455,5.169,20.201,5.169z M20.201,31.749c-6.425,0-11.634-5.208-11.634-11.634\n    c0-6.425,5.209-11.634,11.634-11.634c6.425,0,11.633,5.209,11.633,11.634C31.834,26.541,26.626,31.749,20.201,31.749z\"\n        />\n        <path\n          fill=\"currentColor\"\n          d=\"M26.013,10.047l1.654-2.866c-2.198-1.272-4.743-2.012-7.466-2.012h0v3.312h0\n    C22.32,8.481,24.301,9.057,26.013,10.047z\"\n        >\n          <animateTransform\n            attributeType=\"xml\"\n            attributeName=\"transform\"\n            type=\"rotate\"\n            from=\"0 20 20\"\n            to=\"360 20 20\"\n            dur=\"0.5s\"\n            repeatCount=\"indefinite\"\n          />\n        </path>\n      </svg>\n    </span>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/icons/VisibleIcon.tsx",
    "content": "import * as React from 'react';\nimport styles from './icons.module.css';\nimport {JSX} from 'react';\n\nexport function VisibleIcon(props: JSX.IntrinsicElements['span']) {\n  return (\n    <span\n      {...props}\n      className={[styles.icon, props.className].filter(Boolean).join(' ')}\n    >\n      <svg\n        focusable=\"false\"\n        aria-hidden=\"true\"\n        viewBox=\"0 0 24 24\"\n        fill=\"currentColor\"\n      >\n        <path d=\"M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z\" />{' '}\n      </svg>\n    </span>\n  );\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/icons/icons.module.css",
    "content": ".icon {\n  width: 24px;\n  height: 24px;\n  display: inline-flex;\n  color: inherit;\n}\n"
  },
  {
    "path": "examples/next-typescript-starter/ui/icons/index.ts",
    "content": "export { LoadingIcon } from './LoadingIcon';\n"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n  setupFiles: ['dotenv/config'],\n  testEnvironment: 'node',\n  setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],\n  testRegex: '(/__tests__/.*|(\\\\.|/)(test|spec))\\\\.tsx?$',\n  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],\n  extensionsToTreatAsEsm: ['.ts', '.tsx'],\n  moduleNameMapper: {\n    '^(\\\\.{1,2}/.*)\\\\.js$': '$1',\n  },\n  transform: {\n    '^.+\\\\.(t|j)sx?$': '@swc/jest',\n  },\n};\n"
  },
  {
    "path": "jest.setup.ts",
    "content": "import \"isomorphic-fetch\";\nimport \"dotenv/config\";\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"next-firebase-auth-edge\",\n  \"version\": \"1.12.0\",\n  \"description\": \"Next.js Firebase Authentication for Edge and server runtimes. Compatible with latest Next.js features.\",\n  \"files\": [\n    \"lib/**/*.js\",\n    \"lib/**/*.d.ts\",\n    \"browser/**/*.js\",\n    \"browser/**/*.d.ts\",\n    \"esm/**/*.js\",\n    \"esm/**/*.d.ts\"\n  ],\n  \"sideEffects\": false,\n  \"main\": \"./lib/index.js\",\n  \"browser\": \"./browser/index.js\",\n  \"types\": \"./lib/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./lib/index.d.ts\",\n      \"bun\": \"./browser/index.js\",\n      \"deno\": \"./browser/index.js\",\n      \"browser\": \"./browser/index.js\",\n      \"worker\": \"./browser/index.js\",\n      \"workerd\": \"./browser/index.js\",\n      \"import\": \"./esm/index.js\",\n      \"require\": \"./lib/index.js\"\n    },\n    \"./app-check\": {\n      \"types\": \"./lib/app-check/index.d.ts\",\n      \"bun\": \"./browser/app-check/index.js\",\n      \"deno\": \"./browser/app-check/index.js\",\n      \"browser\": \"./browser/app-check/index.js\",\n      \"worker\": \"./browser/app-check/index.js\",\n      \"workerd\": \"./browser/app-check/index.js\",\n      \"import\": \"./esm/app-check/index.js\",\n      \"require\": \"./lib/app-check/index.js\"\n    },\n    \"./auth\": {\n      \"types\": \"./lib/auth/index.d.ts\",\n      \"bun\": \"./browser/auth/index.js\",\n      \"deno\": \"./browser/auth/index.js\",\n      \"browser\": \"./browser/auth/index.js\",\n      \"worker\": \"./browser/auth/index.js\",\n      \"workerd\": \"./browser/auth/index.js\",\n      \"import\": \"./esm/auth/index.js\",\n      \"require\": \"./lib/auth/index.js\"\n    },\n    \"./auth/error\": {\n      \"types\": \"./lib/auth/error.d.ts\",\n      \"bun\": \"./browser/auth/error.js\",\n      \"deno\": \"./browser/auth/error.js\",\n      \"browser\": \"./browser/auth/error.js\",\n      \"worker\": \"./browser/auth/error.js\",\n      \"workerd\": \"./browser/auth/error.js\",\n      \"import\": \"./esm/auth/error.js\",\n      \"require\": \"./lib/auth/error.js\"\n    },\n    \"./auth/claims\": {\n      \"types\": \"./lib/auth/claims.d.ts\",\n      \"bun\": \"./browser/auth/claims.js\",\n      \"deno\": \"./browser/auth/claims.js\",\n      \"browser\": \"./browser/auth/claims.js\",\n      \"worker\": \"./browser/auth/claims.js\",\n      \"workerd\": \"./browser/auth/claims.js\",\n      \"import\": \"./esm/auth/claims.js\",\n      \"require\": \"./lib/auth/claims.js\"\n    },\n    \"./next/utils\": {\n      \"types\": \"./lib/next/utils.d.ts\",\n      \"bun\": \"./browser/next/utils.js\",\n      \"deno\": \"./browser/next/utils.js\",\n      \"browser\": \"./browser/next/utils.js\",\n      \"worker\": \"./browser/next/utils.js\",\n      \"workerd\": \"./browser/next/utils.js\",\n      \"import\": \"./esm/next/utils.js\",\n      \"require\": \"./lib/next/utils.js\"\n    },\n    \"./next/cookies\": {\n      \"types\": \"./lib/next/cookies/index.d.ts\",\n      \"bun\": \"./browser/next/cookies/index.js\",\n      \"deno\": \"./browser/next/cookies/index.js\",\n      \"browser\": \"./browser/next/cookies/index.js\",\n      \"worker\": \"./browser/next/cookies/index.js\",\n      \"workerd\": \"./browser/next/cookies/index.js\",\n      \"import\": \"./esm/next/cookies/index.js\",\n      \"require\": \"./lib/next/cookies/index.js\"\n    },\n    \"./next/tokens\": {\n      \"types\": \"./lib/next/tokens.d.ts\",\n      \"bun\": \"./browser/next/tokens.js\",\n      \"deno\": \"./browser/next/tokens.js\",\n      \"browser\": \"./browser/next/tokens.js\",\n      \"worker\": \"./browser/next/tokens.js\",\n      \"workerd\": \"./browser/next/tokens.js\",\n      \"import\": \"./esm/next/tokens.js\",\n      \"require\": \"./lib/next/tokens.js\"\n    },\n    \"./next/client\": {\n      \"types\": \"./lib/next/client.d.ts\",\n      \"bun\": \"./browser/next/client.js\",\n      \"deno\": \"./browser/next/client.js\",\n      \"browser\": \"./browser/next/client.js\",\n      \"worker\": \"./browser/next/client.js\",\n      \"workerd\": \"./browser/next/client.js\",\n      \"import\": \"./esm/next/client.js\",\n      \"require\": \"./lib/next/client.js\"\n    },\n    \"./next/api\": {\n      \"types\": \"./lib/next/api.d.ts\",\n      \"bun\": \"./browser/next/api.js\",\n      \"deno\": \"./browser/next/api.js\",\n      \"browser\": \"./browser/next/api.js\",\n      \"worker\": \"./browser/next/api.js\",\n      \"workerd\": \"./browser/next/api.js\",\n      \"import\": \"./esm/next/api.js\",\n      \"require\": \"./lib/next/api.js\"\n    },\n    \"./next/middleware\": {\n      \"types\": \"./lib/next/middleware.d.ts\",\n      \"bun\": \"./browser/next/middleware.js\",\n      \"deno\": \"./browser/next/middleware.js\",\n      \"browser\": \"./browser/next/middleware.js\",\n      \"worker\": \"./browser/next/middleware.js\",\n      \"workerd\": \"./browser/next/middleware.js\",\n      \"import\": \"./esm/next/middleware.js\",\n      \"require\": \"./lib/next/middleware.js\"\n    },\n    \"./next/refresh-token\": {\n      \"types\": \"./lib/next/refresh-token.d.ts\",\n      \"bun\": \"./browser/next/refresh-token.js\",\n      \"deno\": \"./browser/next/refresh-token.js\",\n      \"browser\": \"./browser/next/refresh-token.js\",\n      \"worker\": \"./browser/next/refresh-token.js\",\n      \"workerd\": \"./browser/next/refresh-token.js\",\n      \"import\": \"./esm/next/refresh-token.js\",\n      \"require\": \"./lib/next/refresh-token.js\"\n    },\n    \"./lib\": {\n      \"types\": \"./lib/index.d.ts\",\n      \"bun\": \"./browser/index.js\",\n      \"deno\": \"./browser/index.js\",\n      \"browser\": \"./browser/index.js\",\n      \"worker\": \"./browser/index.js\",\n      \"workerd\": \"./browser/index.js\",\n      \"import\": \"./esm/index.js\",\n      \"require\": \"./lib/index.js\"\n    },\n    \"./lib/app-check\": {\n      \"types\": \"./lib/app-check/index.d.ts\",\n      \"bun\": \"./browser/app-check/index.js\",\n      \"deno\": \"./browser/app-check/index.js\",\n      \"browser\": \"./browser/app-check/index.js\",\n      \"worker\": \"./browser/app-check/index.js\",\n      \"workerd\": \"./browser/app-check/index.js\",\n      \"import\": \"./esm/app-check/index.js\",\n      \"require\": \"./lib/app-check/index.js\"\n    },\n    \"./lib/auth\": {\n      \"types\": \"./lib/auth/index.d.ts\",\n      \"bun\": \"./browser/auth/index.js\",\n      \"deno\": \"./browser/auth/index.js\",\n      \"browser\": \"./browser/auth/index.js\",\n      \"worker\": \"./browser/auth/index.js\",\n      \"workerd\": \"./browser/auth/index.js\",\n      \"import\": \"./esm/auth/index.js\",\n      \"require\": \"./lib/auth/index.js\"\n    },\n    \"./lib/auth/error\": {\n      \"types\": \"./lib/auth/error.d.ts\",\n      \"bun\": \"./browser/auth/error.js\",\n      \"deno\": \"./browser/auth/error.js\",\n      \"browser\": \"./browser/auth/error.js\",\n      \"worker\": \"./browser/auth/error.js\",\n      \"workerd\": \"./browser/auth/error.js\",\n      \"import\": \"./esm/auth/error.js\",\n      \"require\": \"./lib/auth/error.js\"\n    },\n    \"./lib/auth/claims\": {\n      \"types\": \"./lib/auth/claims.d.ts\",\n      \"bun\": \"./browser/auth/claims.js\",\n      \"deno\": \"./browser/auth/claims.js\",\n      \"browser\": \"./browser/auth/claims.js\",\n      \"worker\": \"./browser/auth/claims.js\",\n      \"workerd\": \"./browser/auth/claims.js\",\n      \"import\": \"./esm/auth/claims.js\",\n      \"require\": \"./lib/auth/claims.js\"\n    },\n    \"./lib/auth/token-verifier\": {\n      \"types\": \"./lib/auth/token-verifier.d.ts\",\n      \"bun\": \"./browser/auth/token-verifier.js\",\n      \"deno\": \"./browser/auth/token-verifier.js\",\n      \"browser\": \"./browser/auth/token-verifier.js\",\n      \"worker\": \"./browser/auth/token-verifier.js\",\n      \"workerd\": \"./browser/auth/token-verifier.js\",\n      \"import\": \"./esm/auth/token-verifier.js\",\n      \"require\": \"./lib/auth/token-verifier.js\"\n    },\n    \"./lib/next/utils\": {\n      \"types\": \"./lib/next/utils.d.ts\",\n      \"bun\": \"./browser/next/utils.js\",\n      \"deno\": \"./browser/next/utils.js\",\n      \"browser\": \"./browser/next/utils.js\",\n      \"worker\": \"./browser/next/utils.js\",\n      \"workerd\": \"./browser/next/utils.js\",\n      \"import\": \"./esm/next/utils.js\",\n      \"require\": \"./lib/next/utils.js\"\n    },\n    \"./lib/next/cookies\": {\n      \"types\": \"./lib/next/cookies/index.d.ts\",\n      \"bun\": \"./browser/next/cookies/index.js\",\n      \"deno\": \"./browser/next/cookies/index.js\",\n      \"browser\": \"./browser/next/cookies/index.js\",\n      \"worker\": \"./browser/next/cookies/index.js\",\n      \"workerd\": \"./browser/next/cookies/index.js\",\n      \"import\": \"./esm/next/cookies/index.js\",\n      \"require\": \"./lib/next/cookies/index.js\"\n    },\n    \"./lib/next/tokens\": {\n      \"types\": \"./lib/next/tokens.d.ts\",\n      \"bun\": \"./browser/next/tokens.js\",\n      \"deno\": \"./browser/next/tokens.js\",\n      \"browser\": \"./browser/next/tokens.js\",\n      \"worker\": \"./browser/next/tokens.js\",\n      \"workerd\": \"./browser/next/tokens.js\",\n      \"import\": \"./esm/next/tokens.js\",\n      \"require\": \"./lib/next/tokens.js\"\n    },\n    \"./lib/next/client\": {\n      \"types\": \"./lib/next/client.d.ts\",\n      \"bun\": \"./browser/next/client.js\",\n      \"deno\": \"./browser/next/client.js\",\n      \"browser\": \"./browser/next/client.js\",\n      \"worker\": \"./browser/next/client.js\",\n      \"workerd\": \"./browser/next/client.js\",\n      \"import\": \"./esm/next/client.js\",\n      \"require\": \"./lib/next/client.js\"\n    },\n    \"./lib/next/api\": {\n      \"types\": \"./lib/next/api.d.ts\",\n      \"bun\": \"./browser/next/api.js\",\n      \"deno\": \"./browser/next/api.js\",\n      \"browser\": \"./browser/next/api.js\",\n      \"worker\": \"./browser/next/api.js\",\n      \"workerd\": \"./browser/next/api.js\",\n      \"import\": \"./esm/next/api.js\",\n      \"require\": \"./lib/next/api.js\"\n    },\n    \"./lib/next/middleware\": {\n      \"types\": \"./lib/next/middleware.d.ts\",\n      \"bun\": \"./browser/next/middleware.js\",\n      \"deno\": \"./browser/next/middleware.js\",\n      \"browser\": \"./browser/next/middleware.js\",\n      \"worker\": \"./browser/next/middleware.js\",\n      \"workerd\": \"./browser/next/middleware.js\",\n      \"import\": \"./esm/next/middleware.js\",\n      \"require\": \"./lib/next/middleware.js\"\n    },\n    \"./lib/next/refresh-token\": {\n      \"types\": \"./lib/next/refresh-token.d.ts\",\n      \"bun\": \"./browser/next/refresh-token.js\",\n      \"deno\": \"./browser/next/refresh-token.js\",\n      \"browser\": \"./browser/next/refresh-token.js\",\n      \"worker\": \"./browser/next/refresh-token.js\",\n      \"workerd\": \"./browser/next/refresh-token.js\",\n      \"import\": \"./esm/next/refresh-token.js\",\n      \"require\": \"./lib/next/refresh-token.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"run-s clear build:*\",\n    \"build:cjs\": \"tsc\",\n    \"build:esm\": \"tsc -p tsconfig.esm.json\",\n    \"build:browser\": \"tsc -p tsconfig.browser.json\",\n    \"build:browser-bundle\": \"esbuild --bundle browser/index.js --format=esm --target=es2020 --outfile=browser/index.bundle.js\",\n    \"build:browser-bundle-min\": \"esbuild --minify --bundle browser/index.js --format=esm --target=es2020 --outfile=browser/index.bundle.min.js\",\n    \"build:browser-umd\": \"rollup browser/index.bundle.js --format umd --name next-firebase-auth-edge -o browser/index.umd.js && rollup browser/index.bundle.min.js --compact --format umd --name next-firebase-auth-edge -o browser/index.umd.min.js\",\n    \"clear\": \"rm -Rf lib esm browser\",\n    \"test\": \"jest src --coverage\",\n    \"lint\": \"eslint src/\",\n    \"check-circular-imports\": \"madge --extensions js,jsx,ts,tsx --ts-config tsconfig.json --circular src\"\n  },\n  \"peerDependencies\": {\n    \"next\": \"^14.0.0 || 15.0.0-rc.0 || ^15.0.0 || ^16.0.0\"\n  },\n  \"dependencies\": {\n    \"cookie\": \"^0.7.0\",\n    \"encoding\": \"^0.1.13\",\n    \"jose\": \"^5.6.3\"\n  },\n  \"devDependencies\": {\n    \"@commitlint/cli\": \"19.4.0\",\n    \"@commitlint/config-conventional\": \"19.2.2\",\n    \"@semantic-release/changelog\": \"^6.0.3\",\n    \"@semantic-release/git\": \"^10.0.1\",\n    \"@semantic-release/npm\": \"^13.1.4\",\n    \"semantic-release\": \"^25.0.3\",\n    \"@eslint/js\": \"^9.9.0\",\n    \"@swc/core\": \"^1.7.26\",\n    \"@swc/jest\": \"^0.2.36\",\n    \"@types/cookie\": \"^0.6.0\",\n    \"@types/eslint__js\": \"^8.42.3\",\n    \"@types/jest\": \"^29.5.13\",\n    \"@types/node\": \"^22.2.0\",\n    \"@types/uuid\": \"^10.0.0\",\n    \"dotenv\": \"^16.4.5\",\n    \"esbuild\": \"^0.24.0\",\n    \"eslint\": \"^9.9.0\",\n    \"eslint-config-prettier\": \"^9.1.0\",\n    \"eslint-plugin-prettier\": \"^5.2.1\",\n    \"husky\": \"9.1.4\",\n    \"isomorphic-fetch\": \"^3.0.0\",\n    \"jest\": \"^29.2.0\",\n    \"jest-fetch-mock\": \"^3.0.3\",\n    \"madge\": \"^8.0.0\",\n    \"next\": \"^16.1.6\",\n    \"npm-run-all2\": \"^6.2.3\",\n    \"prettier\": \"^3.3.3\",\n    \"react\": \"^19.1.1\",\n    \"react-dom\": \"^19.1.1\",\n    \"rollup\": \"^4.22.4\",\n    \"typescript\": \"^5.5.4\",\n    \"typescript-eslint\": \"^8.0.1\",\n    \"uuid\": \"^10.0.0\"\n  },\n  \"keywords\": [\n    \"firebase\",\n    \"authentication\",\n    \"firebase auth\",\n    \"next\",\n    \"next.js\",\n    \"edge runtime\",\n    \"edge\",\n    \"middleware\"\n  ],\n  \"author\": \"Amadeusz Winogrodzki\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/awinogrodzki/next-firebase-auth-edge.git\"\n  },\n  \"engines\": {\n    \"node\": \">=16.0.0 <26.0.0\",\n    \"npm\": \">=8.0.0 <12.0.0\",\n    \"yarn\": \">=1.22.0 <2.0.0\"\n  },\n  \"packageManager\": \"yarn@1.22.0\"\n}\n"
  },
  {
    "path": "prettier.config.js",
    "content": "module.exports = {\n  bracketSpacing: false,\n  singleQuote: true,\n  trailingComma: 'none',\n};\n"
  },
  {
    "path": "src/app-check/api-client.ts",
    "content": "import {getSdkVersion} from '../auth/auth-request-handler.js';\nimport {Credential} from '../auth/credential.js';\nimport {formatString} from '../auth/utils.js';\nimport {AppCheckToken} from './types.js';\n\nconst FIREBASE_APP_CHECK_V1_API_URL_FORMAT =\n  'https://firebaseappcheck.googleapis.com/v1/projects/{projectId}/apps/{appId}:exchangeCustomToken';\n\nconst FIREBASE_APP_CHECK_CONFIG_HEADERS = {\n  'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`\n};\n\nexport class AppCheckApiClient {\n  constructor(private credential: Credential) {}\n\n  public async exchangeToken(\n    customToken: string,\n    appId: string\n  ): Promise<AppCheckToken> {\n    const url = await this.getUrl(appId);\n    const token = await this.credential.getAccessToken(false);\n\n    const response = await fetch(url, {\n      method: 'POST',\n      headers: {\n        ...FIREBASE_APP_CHECK_CONFIG_HEADERS,\n        Authorization: `Bearer ${token.accessToken}`\n      },\n      body: JSON.stringify({customToken})\n    });\n\n    if (response.ok) {\n      return this.toAppCheckToken(response);\n    }\n\n    throw await this.toFirebaseError(response);\n  }\n\n  private async getUrl(appId: string): Promise<string> {\n    const projectId = await this.credential.getProjectId();\n    const urlParams = {\n      projectId,\n      appId\n    };\n    const baseUrl = formatString(\n      FIREBASE_APP_CHECK_V1_API_URL_FORMAT,\n      urlParams\n    );\n\n    return formatString(baseUrl);\n  }\n\n  private async toFirebaseError(\n    response: Response\n  ): Promise<FirebaseAppCheckError> {\n    const data = (await response.json()) as ErrorResponse;\n    const error: Error = data.error || {};\n    let code: AppCheckErrorCode = 'unknown-error';\n    if (error.status && error.status in APP_CHECK_ERROR_CODE_MAPPING) {\n      code = APP_CHECK_ERROR_CODE_MAPPING[error.status];\n    }\n    const message = error.message || `Unknown server error: ${response.text}`;\n    return new FirebaseAppCheckError(code, message);\n  }\n\n  private async toAppCheckToken(response: Response): Promise<AppCheckToken> {\n    const data = await response.json();\n    const token = data.token;\n    const ttlMillis = this.stringToMilliseconds(data.ttl);\n\n    return {\n      token,\n      ttlMillis\n    };\n  }\n\n  private stringToMilliseconds(duration: string): number {\n    if (!duration.endsWith('s')) {\n      throw new FirebaseAppCheckError(\n        'invalid-argument',\n        '`ttl` must be a valid duration string with the suffix `s`.'\n      );\n    }\n    const seconds = duration.slice(0, -1);\n    return Math.floor(Number(seconds) * 1000);\n  }\n}\n\nexport interface ErrorResponse {\n  error?: Error;\n}\n\nexport interface Error {\n  code?: number;\n  message?: string;\n  status?: string;\n}\n\nexport const APP_CHECK_ERROR_CODE_MAPPING: {\n  [key: string]: AppCheckErrorCode;\n} = {\n  ABORTED: 'aborted',\n  INVALID_ARGUMENT: 'invalid-argument',\n  INVALID_CREDENTIAL: 'invalid-credential',\n  INTERNAL: 'internal-error',\n  PERMISSION_DENIED: 'permission-denied',\n  UNAUTHENTICATED: 'unauthenticated',\n  NOT_FOUND: 'not-found',\n  UNKNOWN: 'unknown-error'\n};\n\nexport type AppCheckErrorCode =\n  | 'aborted'\n  | 'invalid-argument'\n  | 'invalid-credential'\n  | 'internal-error'\n  | 'permission-denied'\n  | 'unauthenticated'\n  | 'not-found'\n  | 'app-check-token-expired'\n  | 'unknown-error';\n\nexport class FirebaseAppCheckError extends Error {\n  constructor(\n    public readonly code: AppCheckErrorCode,\n    message: string\n  ) {\n    super(`(${code}): ${message}`);\n    Object.setPrototypeOf(this, FirebaseAppCheckError.prototype);\n  }\n}\n"
  },
  {
    "path": "src/app-check/index.ts",
    "content": "import {\n  Credential,\n  ServiceAccount,\n  ServiceAccountCredential\n} from '../auth/credential';\nimport {getApplicationDefault} from '../auth/default-credential.js';\nimport {cryptoSignerFromCredential} from '../auth/token-generator.js';\nimport {VerifyOptions} from '../auth/types.js';\nimport {AppCheckApiClient} from './api-client.js';\nimport {AppCheckTokenGenerator} from './token-generator.js';\nimport {AppCheckTokenVerifier} from './token-verifier.js';\nimport {\n  AppCheckToken,\n  AppCheckTokenOptions,\n  VerifyAppCheckTokenResponse\n} from './types';\n\nclass AppCheck {\n  private readonly client: AppCheckApiClient;\n  private readonly tokenGenerator: AppCheckTokenGenerator;\n  private readonly appCheckTokenVerifier: AppCheckTokenVerifier;\n\n  constructor(credential: Credential, tenantId?: string) {\n    this.client = new AppCheckApiClient(credential);\n    this.tokenGenerator = new AppCheckTokenGenerator(\n      cryptoSignerFromCredential(credential, tenantId)\n    );\n    this.appCheckTokenVerifier = new AppCheckTokenVerifier(credential);\n  }\n\n  public createToken = (\n    appId: string,\n    options?: AppCheckTokenOptions\n  ): Promise<AppCheckToken> => {\n    return this.tokenGenerator\n      .createCustomToken(appId, options)\n      .then((customToken) => {\n        return this.client.exchangeToken(customToken, appId);\n      });\n  };\n\n  public verifyToken = (\n    appCheckToken: string,\n    options: VerifyOptions\n  ): Promise<VerifyAppCheckTokenResponse> => {\n    return this.appCheckTokenVerifier\n      .verifyToken(appCheckToken, options)\n      .then((decodedToken) => {\n        return {\n          appId: decodedToken.app_id,\n          token: decodedToken\n        };\n      });\n  };\n}\n\nexport interface AppCheckOptions {\n  serviceAccount?: ServiceAccount;\n  tenantId?: string;\n}\n\nfunction isAppCheckOptions(\n  options: ServiceAccount | AppCheckOptions\n): options is AppCheckOptions {\n  const serviceAccount = options as ServiceAccount;\n\n  return (\n    !serviceAccount.privateKey ||\n    !serviceAccount.projectId ||\n    !serviceAccount.clientEmail\n  );\n}\n\nexport function getAppCheck(options: AppCheckOptions): AppCheck;\n/** @deprecated Use `AppCheckOptions` configuration object instead */\nexport function getAppCheck(\n  serviceAccount: ServiceAccount,\n  tenantId?: string\n): AppCheck;\nexport function getAppCheck(\n  serviceAccount: ServiceAccount | AppCheckOptions,\n  tenantId?: string\n) {\n  if (!isAppCheckOptions(serviceAccount)) {\n    return new AppCheck(new ServiceAccountCredential(serviceAccount), tenantId);\n  }\n\n  const options = serviceAccount;\n  const credential = options.serviceAccount\n    ? new ServiceAccountCredential(options.serviceAccount)\n    : getApplicationDefault();\n\n  return new AppCheck(credential, tenantId);\n}\n"
  },
  {
    "path": "src/app-check/test/app-check.integration.test.ts",
    "content": "import {getAppCheck} from '../index.js';\nimport {FirebaseAppCheckError} from '../api-client.js';\n\nconst {\n  FIREBASE_PROJECT_ID,\n  FIREBASE_ADMIN_CLIENT_EMAIL,\n  FIREBASE_ADMIN_PRIVATE_KEY,\n  FIREBASE_AUTH_TENANT_ID,\n  FIREBASE_APP_ID\n} = process.env;\n\nconst TEST_SERVICE_ACCOUNT = {\n  clientEmail: FIREBASE_ADMIN_CLIENT_EMAIL!,\n  privateKey: FIREBASE_ADMIN_PRIVATE_KEY!.replace(/\\\\n/g, '\\n'),\n  projectId: FIREBASE_PROJECT_ID!\n};\n\ndescribe('app check integration test', () => {\n  const scenarios = [\n    {\n      desc: 'single-tenant',\n      tenantID: undefined\n    },\n    {\n      desc: 'multi-tenant',\n      tenantId: FIREBASE_AUTH_TENANT_ID\n    }\n  ];\n  for (const {desc, tenantId} of scenarios) {\n    describe(desc, () => {\n      const {createToken, verifyToken} = getAppCheck(\n        TEST_SERVICE_ACCOUNT,\n        tenantId\n      );\n\n      it('should create and verify app check token', async () => {\n        const {token} = await createToken(FIREBASE_APP_ID!);\n\n        await verifyToken(token, {referer: 'http://localhost:3000'});\n      });\n\n      it('should throw app check expired error if token is expired', async () => {\n        const {token} = await createToken(FIREBASE_APP_ID!);\n\n        return expect(() =>\n          verifyToken(token, {\n            currentDate: new Date(Date.now() + 7200 * 1000),\n            referer: 'http://localhost:3000'\n          })\n        ).rejects.toEqual(\n          new FirebaseAppCheckError(\n            'app-check-token-expired',\n            'The provided App Check token has expired. Get a fresh App Check token from your client app and try again.'\n          )\n        );\n      });\n    });\n  }\n});\n"
  },
  {
    "path": "src/app-check/token-generator.ts",
    "content": "import {CryptoSigner} from '../auth/jwt/crypto-signer.js';\nimport {AppCheckTokenOptions} from './types.js';\nimport {FirebaseAppCheckError} from './api-client.js';\n\nconst ONE_MINUTE_IN_SECONDS = 60;\nconst ONE_MINUTE_IN_MILLIS = ONE_MINUTE_IN_SECONDS * 1000;\nconst ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1000;\n\nconst FIREBASE_APP_CHECK_AUDIENCE =\n  'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.TokenExchangeService';\n\nfunction transformMillisecondsToSecondsString(milliseconds: number): string {\n  let duration: string;\n  const seconds = Math.floor(milliseconds / 1000);\n  const nanos = Math.floor((milliseconds - seconds * 1000) * 1000000);\n  if (nanos > 0) {\n    let nanoString = nanos.toString();\n    while (nanoString.length < 9) {\n      nanoString = '0' + nanoString;\n    }\n    duration = `${seconds}.${nanoString}s`;\n  } else {\n    duration = `${seconds}s`;\n  }\n  return duration;\n}\n\nexport class AppCheckTokenGenerator {\n  private readonly signer: CryptoSigner;\n\n  constructor(signer: CryptoSigner) {\n    this.signer = signer;\n  }\n\n  public async createCustomToken(\n    appId: string,\n    options?: AppCheckTokenOptions\n  ): Promise<string> {\n    if (!appId) {\n      throw new FirebaseAppCheckError(\n        'invalid-argument',\n        '`appId` must be a non-empty string.'\n      );\n    }\n    let customOptions = {};\n    if (typeof options !== 'undefined') {\n      customOptions = this.validateTokenOptions(options);\n    }\n\n    const account = await this.signer.getAccountId();\n\n    const iat = Math.floor(Date.now() / 1000);\n    const body = {\n      iss: account,\n      sub: account,\n      app_id: appId,\n      aud: FIREBASE_APP_CHECK_AUDIENCE,\n      exp: iat + ONE_MINUTE_IN_SECONDS * 5,\n      iat,\n      ...customOptions\n    };\n\n    return this.signer.sign(body);\n  }\n\n  private validateTokenOptions(options: AppCheckTokenOptions): {\n    [key: string]: unknown;\n  } {\n    if (typeof options.ttlMillis !== 'undefined') {\n      if (\n        options.ttlMillis < ONE_MINUTE_IN_MILLIS * 30 ||\n        options.ttlMillis > ONE_DAY_IN_MILLIS * 7\n      ) {\n        throw new FirebaseAppCheckError(\n          'invalid-argument',\n          'ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).'\n        );\n      }\n\n      return {ttl: transformMillisecondsToSecondsString(options.ttlMillis)};\n    }\n    return {};\n  }\n}\n"
  },
  {
    "path": "src/app-check/token-verifier.ts",
    "content": "import {decodeJwt, decodeProtectedHeader, errors} from 'jose';\nimport {JOSEError} from 'jose/dist/types/util/errors';\nimport {Credential} from '../auth/credential.js';\nimport {ALGORITHM_RS256} from '../auth/jwt/verify.js';\nimport {\n  DecodedToken,\n  JWKSSignatureVerifier,\n  SignatureVerifier\n} from '../auth/signature-verifier';\nimport {VerifyOptions} from '../auth/types.js';\nimport {FirebaseAppCheckError} from './api-client.js';\nimport {DecodedAppCheckToken} from './types.js';\n\nconst APP_CHECK_ISSUER = 'https://firebaseappcheck.googleapis.com/';\nconst JWKS_URL = 'https://firebaseappcheck.googleapis.com/v1/jwks';\n\nexport class AppCheckTokenVerifier {\n  private readonly signatureVerifier: SignatureVerifier;\n\n  constructor(private readonly credential: Credential) {\n    this.signatureVerifier = new JWKSSignatureVerifier(JWKS_URL);\n  }\n\n  public async verifyToken(\n    token: string,\n    options: VerifyOptions\n  ): Promise<DecodedAppCheckToken> {\n    const projectId = await this.credential.getProjectId();\n    const decoded = await this.decodeAndVerify(token, projectId, options);\n\n    const decodedAppCheckToken = decoded.payload as DecodedAppCheckToken;\n    decodedAppCheckToken.app_id = decodedAppCheckToken.sub;\n    return decodedAppCheckToken;\n  }\n\n  private async decodeAndVerify(\n    token: string,\n    projectId: string,\n    options: VerifyOptions\n  ): Promise<DecodedToken> {\n    const header = decodeProtectedHeader(token);\n    const payload = decodeJwt(token);\n\n    this.verifyContent({header, payload}, projectId);\n    await this.verifySignature(token, options);\n\n    return {header, payload};\n  }\n\n  private verifyContent(\n    fullDecodedToken: DecodedToken,\n    projectId: string | null\n  ): void {\n    const header = fullDecodedToken.header;\n    const payload = fullDecodedToken.payload;\n\n    const projectIdMatchMessage =\n      ' Make sure the App Check token comes from the same ' +\n      'Firebase project as the service account used to authenticate this SDK.';\n    const scopedProjectId = `projects/${projectId}`;\n\n    let errorMessage: string | undefined;\n    if (header.alg !== ALGORITHM_RS256) {\n      errorMessage =\n        'The provided App Check token has incorrect algorithm. Expected \"' +\n        ALGORITHM_RS256 +\n        '\" but got ' +\n        '\"' +\n        header.alg +\n        '\".';\n    } else if (!payload.aud?.includes(scopedProjectId)) {\n      errorMessage =\n        'The provided App Check token has incorrect \"aud\" (audience) claim. Expected \"' +\n        scopedProjectId +\n        '\" but got \"' +\n        payload.aud +\n        '\".' +\n        projectIdMatchMessage;\n    } else if (\n      typeof payload.iss !== 'string' ||\n      !payload.iss.startsWith(APP_CHECK_ISSUER)\n    ) {\n      errorMessage =\n        'The provided App Check token has incorrect \"iss\" (issuer) claim.';\n    } else if (typeof payload.sub !== 'string') {\n      errorMessage =\n        'The provided App Check token has no \"sub\" (subject) claim.';\n    } else if (payload.sub === '') {\n      errorMessage =\n        'The provided App Check token has an empty string \"sub\" (subject) claim.';\n    }\n    if (errorMessage) {\n      throw new FirebaseAppCheckError('invalid-argument', errorMessage);\n    }\n  }\n\n  private verifySignature(\n    jwtToken: string,\n    options: VerifyOptions\n  ): Promise<void> {\n    return this.signatureVerifier\n      .verify(jwtToken, options)\n      .catch((error: JOSEError) => {\n        throw this.mapJwtErrorToAppCheckError(error);\n      });\n  }\n\n  private mapJwtErrorToAppCheckError(error: JOSEError): FirebaseAppCheckError {\n    if (error instanceof errors.JWTExpired) {\n      const errorMessage =\n        'The provided App Check token has expired. Get a fresh App Check token' +\n        ' from your client app and try again.';\n      return new FirebaseAppCheckError('app-check-token-expired', errorMessage);\n    } else if (error instanceof errors.JWSSignatureVerificationFailed) {\n      const errorMessage =\n        'The provided App Check token has invalid signature.';\n      return new FirebaseAppCheckError('invalid-argument', errorMessage);\n    } else if (error instanceof errors.JWKSNoMatchingKey) {\n      const errorMessage =\n        'The provided App Check token has \"kid\" claim which does not ' +\n        'correspond to a known public key. Most likely the provided App Check token ' +\n        'is expired, so get a fresh token from your client app and try again.';\n      return new FirebaseAppCheckError('invalid-argument', errorMessage);\n    }\n    return new FirebaseAppCheckError('invalid-argument', error.message);\n  }\n}\n"
  },
  {
    "path": "src/app-check/types.ts",
    "content": "export interface AppCheckToken {\n  token: string;\n  ttlMillis: number;\n}\n\nexport interface AppCheckTokenOptions {\n  ttlMillis?: number;\n}\n\nexport interface DecodedAppCheckToken {\n  iss: string;\n  sub: string;\n  aud: string[];\n  exp: number;\n  iat: number;\n  app_id: string;\n  [key: string]: unknown;\n}\n\nexport interface VerifyAppCheckTokenResponse {\n  appId: string;\n  token: DecodedAppCheckToken;\n}\n"
  },
  {
    "path": "src/auth/auth-request-handler.ts",
    "content": "import {\n  Credential,\n  FirebaseAccessToken,\n  getFirebaseAdminTokenProvider\n} from './credential';\nimport {AuthError, AuthErrorCode} from './error.js';\nimport {emulatorHost, useEmulator} from './firebase.js';\nimport {GetAccountInfoUserResponse} from './user-record.js';\nimport {formatString} from './utils.js';\nimport {isEmail, isNonNullObject} from './validator.js';\n\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD';\n\nexport class ApiSettings {\n  constructor(\n    private endpoint: string,\n    private httpMethod: HttpMethod = 'POST'\n  ) {}\n\n  public getEndpoint(): string {\n    return this.endpoint;\n  }\n\n  public getHttpMethod(): HttpMethod {\n    return this.httpMethod;\n  }\n}\n\nexport function getSdkVersion(): string {\n  return '11.2.0';\n}\n\nconst FIREBASE_AUTH_HEADER = {\n  'X-Client-Version': `Node/Admin/${getSdkVersion()}`,\n  Accept: 'application/json',\n  'Content-Type': 'application/json'\n};\n\nconst FIREBASE_AUTH_BASE_URL_FORMAT =\n  'https://identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}';\n\nconst FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT =\n  'http://{host}/identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}';\n\nclass AuthResourceUrlBuilder {\n  protected urlFormat: string;\n\n  constructor(\n    protected version: string = 'v1',\n    private credential: Credential\n  ) {\n    if (useEmulator()) {\n      this.urlFormat = formatString(FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT, {\n        host: emulatorHost()\n      });\n    } else {\n      this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT;\n    }\n  }\n\n  public async getUrl(api?: string, params?: object): Promise<string> {\n    const baseParams = {\n      version: this.version,\n      projectId: await this.credential.getProjectId(),\n      api: api || ''\n    };\n    const baseUrl = formatString(this.urlFormat, baseParams);\n    return formatString(baseUrl, params || {});\n  }\n}\n\nexport const FIREBASE_AUTH_CREATE_SESSION_COOKIE = new ApiSettings(\n  ':createSessionCookie',\n  'POST'\n);\n\nexport const FIREBASE_AUTH_GET_ACCOUNT_INFO = new ApiSettings(\n  '/accounts:lookup',\n  'POST'\n);\n\nexport const FIREBASE_AUTH_DELETE_ACCOUNT = new ApiSettings(\n  '/accounts:delete',\n  'POST'\n);\n\nexport const FIREBASE_AUTH_SET_ACCOUNT_INFO = new ApiSettings(\n  '/accounts:update',\n  'POST'\n);\n\nexport const FIREBASE_AUTH_SIGN_UP_NEW_USER = new ApiSettings(\n  '/accounts',\n  'POST'\n);\n\nexport const FIREBASE_AUTH_LIST_USERS_INFO = new ApiSettings(\n  '/accounts:batchGet',\n  'GET'\n);\n\nexport type ListUsersResponse = {\n  kind: string;\n  users: GetAccountInfoUserResponse[];\n  nextPageToken: string;\n};\n\nexport type GetAccountInfoByEmailResponse = {\n  users: GetAccountInfoUserResponse[];\n};\n\ntype ResponseObject = {\n  localId: string;\n};\n\nexport interface AuthRequestHandlerOptions {\n  tenantId?: string;\n}\n\nexport interface ErrorResponse {\n  error: Error;\n}\n\nexport abstract class AbstractAuthRequestHandler {\n  private authUrlBuilder: AuthResourceUrlBuilder | undefined;\n  private getToken: (forceRefresh?: boolean) => Promise<FirebaseAccessToken>;\n\n  private static getErrorCode(response: unknown): string | null {\n    return (\n      (isNonNullObject(response) &&\n        (response as ErrorResponse).error &&\n        (response as ErrorResponse).error.message) ||\n      null\n    );\n  }\n\n  constructor(\n    credential: Credential,\n    protected options: AuthRequestHandlerOptions = {}\n  ) {\n    this.getToken = useEmulator()\n      ? () =>\n          Promise.resolve({\n            accessToken: 'owner',\n            expirationTime: Infinity\n          })\n      : getFirebaseAdminTokenProvider(credential).getToken;\n  }\n\n  private prepareRequest(request: object) {\n    if (!this.options.tenantId) {\n      return request;\n    }\n\n    return {\n      ...request,\n      tenantId: this.options.tenantId\n    };\n  }\n\n  public getAccountInfoByUid(\n    uid: string\n  ): Promise<{users?: GetAccountInfoUserResponse[]}> {\n    const request = {\n      localId: [uid]\n    };\n\n    return this.invokeRequestHandler(\n      this.getAuthUrlBuilder(),\n      FIREBASE_AUTH_GET_ACCOUNT_INFO,\n      request\n    );\n  }\n\n  public deleteAccount(uid: string): Promise<ResponseObject> {\n    return this.invokeRequestHandler(\n      this.getAuthUrlBuilder(),\n      FIREBASE_AUTH_DELETE_ACCOUNT,\n      {\n        localId: uid\n      }\n    );\n  }\n\n  public getAccountInfoByEmail(\n    email: string\n  ): Promise<GetAccountInfoByEmailResponse> {\n    if (!isEmail(email)) {\n      return Promise.reject(\n        new AuthError(AuthErrorCode.INVALID_ARGUMENT, 'Invalid e-mail address')\n      );\n    }\n\n    const request = {\n      email: [email]\n    };\n\n    return this.invokeRequestHandler(\n      this.getAuthUrlBuilder(),\n      FIREBASE_AUTH_GET_ACCOUNT_INFO,\n      request\n    );\n  }\n\n  public createNewAccount(properties: CreateRequest): Promise<string> {\n    type SignUpNewUserRequest = CreateRequest & {\n      photoUrl?: string | null;\n      localId?: string;\n      mfaInfo?: AuthFactorInfo[];\n    };\n\n    const request: SignUpNewUserRequest = {\n      ...properties\n    };\n\n    if (typeof request.photoURL !== 'undefined') {\n      request.photoUrl = request.photoURL;\n      delete request.photoURL;\n    }\n\n    if (typeof request.uid !== 'undefined') {\n      request.localId = request.uid;\n      delete request.uid;\n    }\n    if (request.multiFactor) {\n      if (\n        Array.isArray(request.multiFactor.enrolledFactors) &&\n        request.multiFactor.enrolledFactors.length > 0\n      ) {\n        const mfaInfo: AuthFactorInfo[] = [];\n        try {\n          request.multiFactor.enrolledFactors.forEach((multiFactorInfo) => {\n            if ('enrollmentTime' in multiFactorInfo) {\n              throw new AuthError(\n                AuthErrorCode.INVALID_ARGUMENT,\n                '\"enrollmentTime\" is not supported when adding second factors via \"createUser()\"'\n              );\n            } else if ('uid' in multiFactorInfo) {\n              throw new AuthError(\n                AuthErrorCode.INVALID_ARGUMENT,\n                '\"uid\" is not supported when adding second factors via \"createUser()\"'\n              );\n            }\n            mfaInfo.push(convertMultiFactorInfoToServerFormat(multiFactorInfo));\n          });\n        } catch (e) {\n          return Promise.reject(e);\n        }\n        request.mfaInfo = mfaInfo;\n      }\n      delete request.multiFactor;\n    }\n\n    return this.invokeRequestHandler(\n      this.getAuthUrlBuilder(),\n      FIREBASE_AUTH_SIGN_UP_NEW_USER,\n      request\n    ).then((response) => {\n      return response.localId;\n    });\n  }\n\n  public createSessionCookie(\n    idToken: string,\n    expiresInMs: number\n  ): Promise<string> {\n    const request = {\n      idToken,\n      // To seconds\n      validDuration: expiresInMs / 1000\n    };\n\n    return this.invokeRequestHandler<{sessionCookie: string}>(\n      this.getAuthUrlBuilder(),\n      FIREBASE_AUTH_CREATE_SESSION_COOKIE,\n      request\n    ).then((response) => response.sessionCookie);\n  }\n\n  public updateExistingAccount(\n    uid: string,\n    properties: UpdateRequest\n  ): Promise<string> {\n    const request: UpdateRequest & {\n      deleteAttribute?: string[];\n      deleteProvider?: string[];\n      linkProviderUserInfo?: UserProvider & {rawId?: string};\n      photoUrl?: string | null;\n      disableUser?: boolean;\n      mfa?: {\n        enrollments?: AuthFactorInfo[];\n      };\n      localId: string;\n    } = {\n      ...properties,\n      deleteAttribute: [],\n      localId: uid\n    };\n\n    const deletableParams: {[key: string]: string} = {\n      displayName: 'DISPLAY_NAME',\n      photoURL: 'PHOTO_URL'\n    };\n\n    request.deleteAttribute = [];\n    for (const key in deletableParams) {\n      if (request[key as keyof UpdateRequest] === null) {\n        request.deleteAttribute.push(deletableParams[key]);\n        delete request[key as keyof UpdateRequest];\n      }\n    }\n    if (request.deleteAttribute.length === 0) {\n      delete request.deleteAttribute;\n    }\n\n    if (request.phoneNumber === null) {\n      if (request.deleteProvider) {\n        request.deleteProvider.push('phone');\n      } else {\n        request.deleteProvider = ['phone'];\n      }\n\n      delete request.phoneNumber;\n    }\n\n    if (typeof request.providerToLink !== 'undefined') {\n      request.linkProviderUserInfo = {...request.providerToLink};\n      delete request.providerToLink;\n\n      request.linkProviderUserInfo.rawId = request.linkProviderUserInfo.uid;\n      delete request.linkProviderUserInfo.uid;\n    }\n\n    if (typeof request.providersToUnlink !== 'undefined') {\n      if (!Array.isArray(request.deleteProvider)) {\n        request.deleteProvider = [];\n      }\n      request.deleteProvider = request.deleteProvider.concat(\n        request.providersToUnlink\n      );\n      delete request.providersToUnlink;\n    }\n\n    if (typeof request.photoURL !== 'undefined') {\n      request.photoUrl = request.photoURL;\n      delete request.photoURL;\n    }\n\n    if (typeof request.disabled !== 'undefined') {\n      request.disableUser = request.disabled;\n      delete request.disabled;\n    }\n\n    if (request.multiFactor) {\n      if (request.multiFactor.enrolledFactors === null) {\n        request.mfa = {};\n      } else if (Array.isArray(request.multiFactor.enrolledFactors)) {\n        request.mfa = {\n          enrollments: []\n        };\n        try {\n          request.multiFactor.enrolledFactors.forEach(\n            (multiFactorInfo: UpdateMultiFactorInfoRequest) => {\n              request.mfa!.enrollments!.push(\n                convertMultiFactorInfoToServerFormat(multiFactorInfo)\n              );\n            }\n          );\n        } catch (e) {\n          return Promise.reject(e);\n        }\n        if (request.mfa!.enrollments!.length === 0) {\n          delete request.mfa.enrollments;\n        }\n      }\n      delete request.multiFactor;\n    }\n\n    return this.invokeRequestHandler(\n      this.getAuthUrlBuilder(),\n      FIREBASE_AUTH_SET_ACCOUNT_INFO,\n      request\n    ).then((response) => {\n      return response.localId;\n    });\n  }\n\n  public setCustomUserClaims(\n    uid: string,\n    customUserClaims: object | null\n  ): Promise<string> {\n    if (customUserClaims === null) {\n      customUserClaims = {};\n    }\n\n    const request = {\n      localId: uid,\n      customAttributes: JSON.stringify(customUserClaims)\n    };\n\n    return this.invokeRequestHandler(\n      this.getAuthUrlBuilder(),\n      FIREBASE_AUTH_SET_ACCOUNT_INFO,\n      request\n    ).then((response) => {\n      return response.localId;\n    });\n  }\n\n  public listUsers(nextPageToken?: string, maxResults?: number) {\n    const request = {\n      nextPageToken,\n      maxResults\n    };\n\n    return this.invokeRequestHandler<ListUsersResponse>(\n      this.getAuthUrlBuilder(),\n      FIREBASE_AUTH_LIST_USERS_INFO,\n      request\n    );\n  }\n\n  private getSearchParams(requestData: object) {\n    const searchParams = new URLSearchParams();\n\n    for (const key in requestData) {\n      if (!requestData[key as keyof object]) {\n        continue;\n      }\n\n      searchParams.append(key, requestData[key as keyof object]);\n    }\n\n    return searchParams;\n  }\n\n  private decorateUrlWithParams(url: string, requestData: object) {\n    const params = this.getSearchParams(requestData);\n    const paramsString = params.toString();\n\n    if (!paramsString) {\n      return url;\n    }\n\n    return `${url}?${paramsString}`;\n  }\n\n  protected async invokeRequestHandler<T = ResponseObject>(\n    urlBuilder: AuthResourceUrlBuilder,\n    apiSettings: ApiSettings,\n    requestData: object | undefined,\n    additionalResourceParams?: object\n  ): Promise<T> {\n    let url = await urlBuilder.getUrl(\n      apiSettings.getEndpoint(),\n      additionalResourceParams\n    );\n    const token = await this.getToken();\n    const init: RequestInit = {\n      method: apiSettings.getHttpMethod(),\n      headers: {\n        ...FIREBASE_AUTH_HEADER,\n        Authorization: `Bearer ${token.accessToken}`\n      }\n    };\n\n    if (requestData && !['GET', 'HEAD'].includes(apiSettings.getHttpMethod())) {\n      init.body = JSON.stringify(this.prepareRequest(requestData));\n    }\n\n    if (requestData && ['GET', 'HEAD'].includes(apiSettings.getHttpMethod())) {\n      url = this.decorateUrlWithParams(url, this.prepareRequest(requestData));\n    }\n\n    const res = await fetch(url, init);\n\n    if (!res.ok) {\n      const error = await res.json();\n      const errorCode = AbstractAuthRequestHandler.getErrorCode(error);\n      if (!errorCode) {\n        throw new AuthError(\n          AuthErrorCode.INTERNAL_ERROR,\n          `Error returned from server: ${JSON.stringify(error)}.`\n        );\n      }\n      throw new AuthError(\n        AuthErrorCode.INTERNAL_ERROR,\n        `Error returned from server: ${JSON.stringify(\n          error\n        )}. Code: ${errorCode}`\n      );\n    }\n\n    return await res.json();\n  }\n\n  protected abstract newAuthUrlBuilder(): AuthResourceUrlBuilder;\n\n  private getAuthUrlBuilder(): AuthResourceUrlBuilder {\n    if (!this.authUrlBuilder) {\n      this.authUrlBuilder = this.newAuthUrlBuilder();\n    }\n    return this.authUrlBuilder;\n  }\n}\n\nexport class AuthRequestHandler extends AbstractAuthRequestHandler {\n  protected readonly authResourceUrlBuilder: AuthResourceUrlBuilder;\n\n  constructor(\n    private credential: Credential,\n    options?: AuthRequestHandlerOptions\n  ) {\n    super(credential, options);\n    this.authResourceUrlBuilder = new AuthResourceUrlBuilder('v2', credential);\n  }\n\n  protected newAuthUrlBuilder(): AuthResourceUrlBuilder {\n    return new AuthResourceUrlBuilder('v1', this.credential);\n  }\n}\n\nfunction isPhoneFactor(\n  multiFactorInfo: UpdateMultiFactorInfoRequest\n): multiFactorInfo is UpdatePhoneMultiFactorInfoRequest {\n  return multiFactorInfo.factorId === 'phone';\n}\n\nfunction isUTCDateString(dateString: string): boolean {\n  try {\n    return (\n      Boolean(dateString) && new Date(dateString).toUTCString() === dateString\n    );\n  } catch {\n    return false;\n  }\n}\n\nexport function convertMultiFactorInfoToServerFormat(\n  multiFactorInfo: UpdateMultiFactorInfoRequest\n): AuthFactorInfo {\n  let enrolledAt;\n  if (typeof multiFactorInfo.enrollmentTime !== 'undefined') {\n    if (isUTCDateString(multiFactorInfo.enrollmentTime)) {\n      enrolledAt = new Date(multiFactorInfo.enrollmentTime).toISOString();\n    } else {\n      throw new AuthError(\n        AuthErrorCode.INVALID_ARGUMENT,\n        `The second factor \"enrollmentTime\" for \"${multiFactorInfo.uid}\" must be a valid ` +\n          'UTC date string.'\n      );\n    }\n  }\n  if (isPhoneFactor(multiFactorInfo)) {\n    const authFactorInfo: AuthFactorInfo = {\n      mfaEnrollmentId: multiFactorInfo.uid,\n      displayName: multiFactorInfo.displayName,\n      phoneInfo: multiFactorInfo.phoneNumber,\n      enrolledAt\n    };\n    for (const objKey in authFactorInfo) {\n      if (typeof authFactorInfo[objKey] === 'undefined') {\n        delete authFactorInfo[objKey];\n      }\n    }\n    return authFactorInfo;\n  } else {\n    throw new AuthError(\n      AuthErrorCode.INVALID_ARGUMENT,\n      `Unsupported second factor \"${JSON.stringify(multiFactorInfo)}\" provided.`\n    );\n  }\n}\n\nexport interface AuthFactorInfo {\n  mfaEnrollmentId?: string;\n  displayName?: string;\n  phoneInfo?: string;\n  enrolledAt?: string;\n  [key: string]: unknown;\n}\n\nexport interface BaseUpdateMultiFactorInfoRequest {\n  uid?: string;\n  displayName?: string;\n  enrollmentTime?: string;\n  factorId: string;\n}\n\nexport interface UpdatePhoneMultiFactorInfoRequest\n  extends BaseUpdateMultiFactorInfoRequest {\n  phoneNumber: string;\n}\n\nexport type UpdateMultiFactorInfoRequest = UpdatePhoneMultiFactorInfoRequest;\n\nexport interface MultiFactorUpdateSettings {\n  enrolledFactors: UpdateMultiFactorInfoRequest[] | null;\n}\n\nexport interface UpdateRequest {\n  disabled?: boolean;\n  displayName?: string | null;\n  email?: string;\n  emailVerified?: boolean;\n  password?: string;\n  phoneNumber?: string | null;\n  photoURL?: string | null;\n  multiFactor?: MultiFactorUpdateSettings;\n  providerToLink?: UserProvider;\n  providersToUnlink?: string[];\n  tenantId?: string;\n}\n\nexport interface UserProvider {\n  uid?: string;\n  displayName?: string;\n  email?: string;\n  phoneNumber?: string;\n  photoURL?: string;\n  providerId?: string;\n}\n\nexport interface BaseCreateMultiFactorInfoRequest {\n  displayName?: string;\n  factorId: string;\n}\n\nexport interface CreatePhoneMultiFactorInfoRequest\n  extends BaseCreateMultiFactorInfoRequest {\n  phoneNumber: string;\n}\n\nexport type CreateMultiFactorInfoRequest = CreatePhoneMultiFactorInfoRequest;\n\nexport interface MultiFactorCreateSettings {\n  enrolledFactors: CreateMultiFactorInfoRequest[];\n}\n\nexport interface CreateRequest extends UpdateRequest {\n  uid?: string;\n  multiFactor?: MultiFactorCreateSettings;\n}\n"
  },
  {
    "path": "src/auth/claims.ts",
    "content": "export type Claims = {[key: string]: unknown};\n\nexport const STANDARD_CLAIMS = [\n  'aud',\n  'auth_time',\n  'email',\n  'email_verified',\n  'exp',\n  'firebase',\n  'source_sign_in_provider',\n  'iat',\n  'iss',\n  'name',\n  'phone_number',\n  'picture',\n  'sub',\n  'uid',\n  'user_id'\n];\n\nexport const filterStandardClaims = (obj: Claims = {}) => {\n  const claims: Claims = {};\n  Object.keys(obj).forEach((key) => {\n    if (!STANDARD_CLAIMS.includes(key)) {\n      claims[key] = obj[key];\n    }\n  });\n  return claims;\n};\n"
  },
  {
    "path": "src/auth/credential.ts",
    "content": "import {JWTPayload} from 'jose';\nimport {sign} from './jwt/sign.js';\nimport {fetchJson, fetchText} from './utils.js';\n\nexport interface GoogleOAuthAccessToken {\n  access_token: string;\n  expires_in: number;\n}\n\nexport interface Credential {\n  getProjectId(): Promise<string>;\n  getServiceAccountEmail(): Promise<string | null>;\n  getAccessToken(forceRefresh?: boolean): Promise<FirebaseAccessToken>;\n}\n\nconst TOKEN_EXPIRY_THRESHOLD_MILLIS = 5 * 60 * 1000;\nconst GOOGLE_TOKEN_AUDIENCE = 'https://accounts.google.com/o/oauth2/token';\nconst GOOGLE_AUTH_TOKEN_HOST = 'accounts.google.com';\nconst GOOGLE_AUTH_TOKEN_PATH = '/o/oauth2/token';\nconst ONE_HOUR_IN_SECONDS = 60 * 60;\n\nexport interface ServiceAccount {\n  projectId: string;\n  privateKey: string;\n  clientEmail: string;\n}\n\nconst accessTokenCache: Map<string, FirebaseAccessToken> = new Map();\n\nexport class ServiceAccountCredential implements Credential {\n  public readonly projectId: string;\n  public readonly privateKey: string;\n  public readonly clientEmail: string;\n\n  constructor(serviceAccount: ServiceAccount) {\n    this.projectId = serviceAccount.projectId;\n    this.privateKey = serviceAccount.privateKey;\n    this.clientEmail = serviceAccount.clientEmail;\n  }\n\n  private async fetchAccessToken(url: string): Promise<FirebaseAccessToken> {\n    const token = await this.createJwt();\n    const postData =\n      'grant_type=urn%3Aietf%3Aparams%3Aoauth%3A' +\n      'grant-type%3Ajwt-bearer&assertion=' +\n      token;\n\n    return requestAccessToken(url, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/x-www-form-urlencoded',\n        Authorization: `Bearer ${token}`,\n        Accept: 'application/json'\n      },\n      body: postData\n    });\n  }\n\n  public getProjectId(): Promise<string> {\n    return Promise.resolve(this.projectId);\n  }\n\n  public getServiceAccountEmail(): Promise<string> {\n    return Promise.resolve(this.clientEmail);\n  }\n\n  private async fetchAndCacheAccessToken(url: string) {\n    const response = await this.fetchAccessToken(url);\n    accessTokenCache.set(url.toString(), response);\n    return response;\n  }\n  public async getAccessToken(\n    forceRefresh: boolean\n  ): Promise<FirebaseAccessToken> {\n    const url = `https://${GOOGLE_AUTH_TOKEN_HOST}${GOOGLE_AUTH_TOKEN_PATH}`;\n\n    if (forceRefresh) {\n      return this.fetchAndCacheAccessToken(url);\n    }\n\n    const cachedResponse = accessTokenCache.get(url);\n\n    if (\n      !cachedResponse ||\n      cachedResponse.expirationTime - Date.now() <=\n        TOKEN_EXPIRY_THRESHOLD_MILLIS\n    ) {\n      return this.fetchAndCacheAccessToken(url);\n    }\n\n    return cachedResponse;\n  }\n\n  private async createJwt(): Promise<string> {\n    const iat = Math.floor(Date.now() / 1000);\n\n    const payload = {\n      aud: GOOGLE_TOKEN_AUDIENCE,\n      iat,\n      exp: iat + ONE_HOUR_IN_SECONDS,\n      iss: this.clientEmail,\n      sub: this.clientEmail,\n      scope: [\n        'https://www.googleapis.com/auth/cloud-platform',\n        'https://www.googleapis.com/auth/firebase.database',\n        'https://www.googleapis.com/auth/firebase.messaging',\n        'https://www.googleapis.com/auth/identitytoolkit',\n        'https://www.googleapis.com/auth/userinfo.email'\n      ].join(' ')\n    } as JWTPayload;\n\n    return sign({\n      payload,\n      privateKey: this.privateKey\n    });\n  }\n}\n\nasync function requestAccessToken(\n  urlString: string,\n  init: RequestInit\n): Promise<FirebaseAccessToken> {\n  const json = await fetchJson(urlString, init);\n\n  if (!json.access_token || !json.expires_in) {\n    throw new Error(\n      `Unexpected response while fetching access token: ${JSON.stringify(json)}`\n    );\n  }\n\n  return {\n    accessToken: json.access_token,\n    expirationTime: Date.now() + json.expires_in * 1000\n  };\n}\n\nexport function getExplicitProjectId(): string | null {\n  const projectId =\n    process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;\n  if (projectId) {\n    return projectId;\n  }\n  return null;\n}\n\nconst GOOGLE_METADATA_SERVICE_HOST = 'metadata.google.internal';\nconst GOOGLE_METADATA_SERVICE_TOKEN_PATH =\n  '/computeMetadata/v1/instance/service-accounts/default/token';\nconst GOOGLE_METADATA_SERVICE_IDENTITY_PATH =\n  '/computeMetadata/v1/instance/service-accounts/default/identity';\nconst GOOGLE_METADATA_SERVICE_PROJECT_ID_PATH =\n  '/computeMetadata/v1/project/project-id';\nconst GOOGLE_METADATA_SERVICE_ACCOUNT_ID_PATH =\n  '/computeMetadata/v1/instance/service-accounts/default/email';\n\nasync function requestIDToken(\n  url: string,\n  request: RequestInit\n): Promise<string> {\n  const text = await fetchText(url, request);\n\n  if (!text) {\n    throw new Error(\n      'Unexpected response while fetching id token: response.text is undefined'\n    );\n  }\n\n  return text;\n}\n\nexport class ComputeEngineCredential implements Credential {\n  private projectId?: string;\n  private accountId?: string;\n  private cachedToken?: FirebaseAccessToken;\n\n  constructor() {}\n\n  public async getAccessToken(\n    forceRefresh: boolean = false\n  ): Promise<FirebaseAccessToken> {\n    const url = `http://${GOOGLE_METADATA_SERVICE_HOST}${GOOGLE_METADATA_SERVICE_TOKEN_PATH}`;\n    const request = this.buildRequest();\n\n    if (\n      this.cachedToken &&\n      !forceRefresh &&\n      this.cachedToken.expirationTime - Date.now() <=\n        TOKEN_EXPIRY_THRESHOLD_MILLIS\n    ) {\n      return this.cachedToken;\n    }\n\n    return (this.cachedToken = await requestAccessToken(url, request));\n  }\n\n  public getIDToken(audience: string): Promise<string> {\n    const url = `http://${GOOGLE_METADATA_SERVICE_HOST}${GOOGLE_METADATA_SERVICE_IDENTITY_PATH}?audience=${audience}`;\n\n    const request = this.buildRequest();\n    return requestIDToken(url, request);\n  }\n\n  public async getProjectId(): Promise<string> {\n    if (this.projectId) {\n      return Promise.resolve(this.projectId);\n    }\n\n    const url = `http://${GOOGLE_METADATA_SERVICE_HOST}${GOOGLE_METADATA_SERVICE_PROJECT_ID_PATH}`;\n    const request = this.buildRequest();\n\n    try {\n      const text = await fetchText(url, request);\n      this.projectId = text;\n      return this.projectId;\n    } catch (err) {\n      throw new Error(`Failed to determine project ID: ${err}`);\n    }\n  }\n\n  public async getServiceAccountEmail(): Promise<string> {\n    if (this.accountId) {\n      return Promise.resolve(this.accountId);\n    }\n\n    const url = `http://${GOOGLE_METADATA_SERVICE_HOST}${GOOGLE_METADATA_SERVICE_ACCOUNT_ID_PATH}`;\n    const request = this.buildRequest();\n\n    try {\n      const text = await fetchText(url, request);\n      this.accountId = text;\n      return this.accountId;\n    } catch (err) {\n      throw new Error(`Failed to determine service account email: ${err}`);\n    }\n  }\n\n  private buildRequest(): RequestInit {\n    return {\n      method: 'GET',\n      headers: {\n        'Metadata-Flavor': 'Google'\n      }\n    };\n  }\n}\n\nexport interface FirebaseAccessToken {\n  accessToken: string;\n  expirationTime: number;\n}\n\nexport const getFirebaseAdminTokenProvider = (credential: Credential) => {\n  async function getToken(forceRefresh = false): Promise<FirebaseAccessToken> {\n    return credential.getAccessToken(forceRefresh);\n  }\n\n  return {\n    getToken\n  };\n};\n"
  },
  {
    "path": "src/auth/custom-token/index.test.ts",
    "content": "import {errors} from 'jose';\nimport {CustomJWTPayload, createCustomJWT, verifyCustomJWT} from '.';\n\ndescribe('custom jwt', () => {\n  const secret = 'very-secure-secret';\n  const jwt =\n    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6Ik1PQ0tfSURfVE9LRU4iLCJyZWZyZXNoX3Rva2VuIjoiTU9DS19SRUZSRVNIX1RPS0VOIiwiY3VzdG9tX3Rva2VuIjoiTU9DS19DVVNUT01fVE9LRU4ifQ.-QNV35-rSl-jCgXHiR3gMW5G-TAkKcT5AimTc7BTPFA';\n  const payload: CustomJWTPayload = {\n    id_token: 'MOCK_ID_TOKEN',\n    refresh_token: 'MOCK_REFRESH_TOKEN',\n    custom_token: 'MOCK_CUSTOM_TOKEN'\n  };\n\n  it('generates custom jwt with id, refresh and custom tokens as a payload', async () => {\n    const token = await createCustomJWT(payload, secret);\n\n    expect(token).toEqual(jwt);\n  });\n\n  it('verifies custom jwt with provided secret', async () => {\n    const result = await verifyCustomJWT(jwt, secret);\n\n    expect(result.payload).toEqual(payload);\n  });\n\n  it('throws error when secret is invalid', async () => {\n    return expect(() =>\n      verifyCustomJWT(jwt, 'invalid-secret')\n    ).rejects.toBeInstanceOf(errors.JWSSignatureVerificationFailed);\n  });\n});\n"
  },
  {
    "path": "src/auth/custom-token/index.ts",
    "content": "import {\n  FlattenedSign,\n  JWTPayload,\n  JWTVerifyResult,\n  SignJWT,\n  errors,\n  jwtVerify\n} from 'jose';\nimport {DecodedIdToken} from '../types.js';\nimport {toUint8Array} from '../utils.js';\n\nexport interface CustomTokens {\n  idToken: string;\n  refreshToken: string;\n  customToken: string;\n}\n\nexport interface ParsedCookies<Metadata extends object> {\n  idToken: string;\n  refreshToken: string;\n  customToken?: string;\n  metadata: Metadata;\n}\n\nexport interface VerifiedCookies<Metadata extends object> {\n  idToken: string;\n  refreshToken: string;\n  customToken?: string;\n  decodedIdToken: DecodedIdToken;\n  metadata: Metadata;\n}\n\nexport interface CustomJWTHeader {\n  alg: 'HS256';\n  typ: 'JWT';\n}\n\nexport interface CustomJWTPayload<Metadata extends object | undefined>\n  extends JWTPayload {\n  id_token: string;\n  refresh_token: string;\n  custom_token?: string;\n  metadata?: Metadata;\n}\n\nexport async function createCustomSignature<Metadata extends object>(\n  value: ParsedCookies<Metadata>,\n  key: string\n) {\n  let data = `${value.idToken}.${value.refreshToken}`;\n\n  if (value.customToken) {\n    data += `.${value.customToken}`;\n  }\n\n  if (value.metadata && Object.keys(value.metadata).length > 0) {\n    data += `.${JSON.stringify(value.metadata)}`;\n  }\n\n  const jws = await new FlattenedSign(toUint8Array(data))\n    .setProtectedHeader({alg: 'HS256'})\n    .sign(toUint8Array(key));\n\n  return jws.signature;\n}\n\nexport async function verifyCustomSignature<Metadata extends object>(\n  value: ParsedCookies<Metadata>,\n  signature: string,\n  key: string\n): Promise<void> {\n  if ((await createCustomSignature(value, key)) !== signature) {\n    throw new errors.JWSSignatureVerificationFailed('');\n  }\n}\n\nexport async function createCustomJWT<Metadata extends object>(\n  payload: CustomJWTPayload<Metadata>,\n  secret: string\n): Promise<string> {\n  const jwt = new SignJWT(payload);\n\n  jwt.setProtectedHeader({alg: 'HS256', typ: 'JWT'});\n\n  return jwt.sign(toUint8Array(secret));\n}\n\nexport async function verifyCustomJWT<Metadata extends object>(\n  customJWT: string,\n  secret: string\n): Promise<JWTVerifyResult<CustomJWTPayload<Metadata>>> {\n  return jwtVerify(customJWT, toUint8Array(secret));\n}\n"
  },
  {
    "path": "src/auth/default-credential.ts",
    "content": "import {ComputeEngineCredential, Credential} from './credential.js';\n\nexport const getApplicationDefault = (): Credential => {\n  return new ComputeEngineCredential();\n};\n"
  },
  {
    "path": "src/auth/error.ts",
    "content": "export enum AuthErrorCode {\n  USER_NOT_FOUND = 'USER_NOT_FOUND',\n  USER_DISABLED = 'USER_DISABLED',\n  INVALID_CREDENTIAL = 'INVALID_CREDENTIAL',\n  TOKEN_EXPIRED = 'TOKEN_EXPIRED',\n  TOKEN_REVOKED = 'TOKEN_REVOKED',\n  INVALID_ARGUMENT = 'INVALID_ARGUMENT',\n  INTERNAL_ERROR = 'INTERNAL_ERROR',\n  NO_KID_IN_HEADER = 'NO_KID_IN_HEADER',\n  INVALID_SIGNATURE = 'INVALID_SIGNATURE',\n  NO_MATCHING_KID = 'NO_MATCHING_KID',\n  MISMATCHING_TENANT_ID = 'MISMATCHING_TENANT_ID'\n}\n\nexport interface HttpError {\n  reason: string;\n  message: string;\n}\n\nconst AuthErrorMessages: Record<AuthErrorCode, string> = {\n  [AuthErrorCode.USER_NOT_FOUND]: 'User not found',\n  [AuthErrorCode.INVALID_CREDENTIAL]: 'Invalid credentials',\n  [AuthErrorCode.TOKEN_EXPIRED]: 'Token expired',\n  [AuthErrorCode.USER_DISABLED]: 'User disabled',\n  [AuthErrorCode.TOKEN_REVOKED]: 'Token revoked',\n  [AuthErrorCode.INVALID_ARGUMENT]: 'Invalid argument',\n  [AuthErrorCode.INTERNAL_ERROR]: 'Internal error',\n  [AuthErrorCode.NO_KID_IN_HEADER]: 'No kid in jwt header',\n  [AuthErrorCode.INVALID_SIGNATURE]: 'Invalid token signature.',\n  [AuthErrorCode.NO_MATCHING_KID]: 'Kid is not matching any certificate',\n  [AuthErrorCode.MISMATCHING_TENANT_ID]:\n    'Provided tenant ID does not match firebase tenant ID from the token'\n};\n\nfunction getErrorMessage(code: AuthErrorCode, customMessage?: string) {\n  if (!customMessage) {\n    return AuthErrorMessages[code];\n  }\n\n  return `${AuthErrorMessages[code]}: ${customMessage}`;\n}\n\nfunction mergeStackTraceAndCause(target: Error, original: unknown) {\n  const originalError = original as Error | undefined;\n  const originalErrorStack =\n    typeof originalError?.stack === 'string' ? originalError.stack : '';\n  const originalCause =\n    typeof originalError?.cause === 'string' ? originalError.cause : '';\n\n  target.stack = (target?.stack ?? '') + originalErrorStack;\n  target.cause = (target?.cause ?? '') + originalCause;\n}\n\nexport class AuthError extends Error {\n  public static fromError(\n    error: unknown,\n    code: AuthErrorCode,\n    customMessage?: string\n  ) {\n    const authError = new AuthError(code, customMessage);\n\n    mergeStackTraceAndCause(authError, error);\n\n    return authError;\n  }\n  constructor(\n    readonly code: AuthErrorCode,\n    customMessage?: string\n  ) {\n    super(getErrorMessage(code, customMessage));\n    Object.setPrototypeOf(this, AuthError.prototype);\n  }\n\n  public toJSON(): object {\n    return {\n      code: this.code,\n      message: this.message\n    };\n  }\n}\n\nexport enum InvalidTokenReason {\n  MISSING_CREDENTIALS = 'MISSING_CREDENTIALS',\n  MISSING_REFRESH_TOKEN = 'MISSING_REFRESH_TOKEN',\n  MALFORMED_CREDENTIALS = 'MALFORMED_CREDENTIALS',\n  INVALID_SIGNATURE = 'INVALID_SIGNATURE',\n  INVALID_CREDENTIALS = 'INVALID_CREDENTIALS',\n  INVALID_KID = 'INVALID_KID'\n}\n\nconst InvalidTokenMessages: Record<InvalidTokenReason, string> = {\n  [InvalidTokenReason.MISSING_CREDENTIALS]: 'Missing credentials',\n  [InvalidTokenReason.MALFORMED_CREDENTIALS]:\n    'Credentials are incorrectly formatted',\n  [InvalidTokenReason.INVALID_SIGNATURE]: 'Credentials have invalid signature',\n  [InvalidTokenReason.MISSING_REFRESH_TOKEN]: 'Refresh token is missing',\n  [InvalidTokenReason.INVALID_CREDENTIALS]: 'Invalid credentials',\n  [InvalidTokenReason.INVALID_KID]:\n    'Token has kid claim that cannot be matched with any known Google certificate. This usually indicates that Google certificates have expired and user has to reauthenticate.'\n};\n\nexport class InvalidTokenError extends Error {\n  public static fromError(error: unknown, reason: InvalidTokenReason) {\n    const invalidTokenError = new InvalidTokenError(reason);\n\n    mergeStackTraceAndCause(invalidTokenError, error);\n\n    return invalidTokenError;\n  }\n\n  public isInvalidTokenError = true;\n\n  constructor(public readonly reason: InvalidTokenReason) {\n    super(`${reason}: ${InvalidTokenMessages[reason]}`);\n    Object.setPrototypeOf(this, InvalidTokenError.prototype);\n  }\n}\n\nexport function isInvalidTokenError(\n  error: unknown\n): error is InvalidTokenError {\n  return (error as InvalidTokenError | undefined)?.isInvalidTokenError ?? false;\n}\n"
  },
  {
    "path": "src/auth/firebase.ts",
    "content": "export const FIREBASE_AUDIENCE =\n  'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit';\nexport const CLIENT_CERT_URL =\n  'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com';\n\nexport function emulatorHost(): string | undefined {\n  if (typeof process === 'undefined') return undefined;\n  return process.env.FIREBASE_AUTH_EMULATOR_HOST;\n}\n\nexport function useEmulator(): boolean {\n  return !!emulatorHost();\n}\n"
  },
  {
    "path": "src/auth/index.ts",
    "content": "import {debug} from '../debug/index.js';\nimport {\n  AuthRequestHandler,\n  CreateRequest,\n  UpdateRequest\n} from './auth-request-handler';\nimport {filterStandardClaims} from './claims';\nimport {\n  Credential,\n  ServiceAccount,\n  ServiceAccountCredential\n} from './credential';\nimport {CustomTokens, ParsedCookies} from './custom-token/index.js';\nimport {getApplicationDefault} from './default-credential.js';\nimport {\n  AuthError,\n  AuthErrorCode,\n  InvalidTokenError,\n  InvalidTokenReason\n} from './error';\nimport {useEmulator} from './firebase.js';\nimport {createFirebaseTokenGenerator} from './token-generator.js';\nimport {createIdTokenVerifier} from './token-verifier.js';\nimport {DecodedIdToken, TokenSet, VerifyOptions} from './types.js';\nimport {UserRecord} from './user-record.js';\n\nexport * from './error.js';\nexport * from './types.js';\n\nconst getCustomTokenEndpoint = (apiKey: string) => {\n  if (useEmulator() && process.env.FIREBASE_AUTH_EMULATOR_HOST) {\n    let protocol = 'http://';\n    if (\n      (process.env.FIREBASE_AUTH_EMULATOR_HOST as string).startsWith('http://')\n    ) {\n      protocol = '';\n    }\n    return `${protocol}${process.env\n      .FIREBASE_AUTH_EMULATOR_HOST!}/identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`;\n  }\n\n  return `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${apiKey}`;\n};\n\nconst getSignUpEndpoint = (apiKey: string) => {\n  return `https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=${apiKey}`;\n};\n\nconst getRefreshTokenEndpoint = (apiKey: string) => {\n  if (useEmulator() && process.env.FIREBASE_AUTH_EMULATOR_HOST) {\n    let protocol = 'http://';\n    if (\n      (process.env.FIREBASE_AUTH_EMULATOR_HOST as string).startsWith('http://')\n    ) {\n      protocol = '';\n    }\n\n    return `${protocol}${process.env\n      .FIREBASE_AUTH_EMULATOR_HOST!}/securetoken.googleapis.com/v1/token?key=${apiKey}`;\n  }\n\n  return `https://securetoken.googleapis.com/v1/token?key=${apiKey}`;\n};\n\ninterface CustomTokenToIdAndRefreshTokensOptions {\n  tenantId?: string;\n  appCheckToken?: string;\n  referer?: string;\n}\n\nexport async function customTokenToIdAndRefreshTokens(\n  customToken: string,\n  firebaseApiKey: string,\n  options: CustomTokenToIdAndRefreshTokensOptions\n): Promise<IdAndRefreshTokens> {\n  const headers: Record<string, string> = {\n    'Content-Type': 'application/json',\n    ...(options.referer ? {Referer: options.referer} : {})\n  };\n\n  const body: Record<string, string | boolean> = {\n    token: customToken,\n    returnSecureToken: true\n  };\n\n  if (options.appCheckToken) {\n    headers['X-Firebase-AppCheck'] = options.appCheckToken;\n  }\n\n  if (options.tenantId) {\n    body['tenantId'] = options.tenantId;\n  }\n\n  const refreshTokenResponse = await fetch(\n    getCustomTokenEndpoint(firebaseApiKey),\n    {\n      method: 'POST',\n      headers,\n      body: JSON.stringify(body)\n    }\n  );\n\n  const refreshTokenJSON =\n    (await refreshTokenResponse.json()) as DecodedIdToken;\n\n  if (!refreshTokenResponse.ok) {\n    throw new Error(\n      `Problem getting a refresh token: ${JSON.stringify(refreshTokenJSON)}`\n    );\n  }\n\n  return {\n    idToken: refreshTokenJSON.idToken as string,\n    refreshToken: refreshTokenJSON.refreshToken as string\n  };\n}\n\nexport async function createAnonymousAccount(\n  firebaseApiKey: string,\n  options: CreateAnonymousRequest = {}\n): Promise<AnonymousTokens> {\n  const headers: Record<string, string> = {\n    'Content-Type': 'application/json',\n    ...(options.referer ? {Referer: options.referer} : {})\n  };\n\n  const body: Record<string, string | boolean> = {\n    returnSecureToken: true\n  };\n\n  if (options.appCheckToken) {\n    headers['X-Firebase-AppCheck'] = options.appCheckToken;\n  }\n\n  if (options.tenantId) {\n    body['tenantId'] = options.tenantId;\n  }\n\n  const createResponse = await fetch(getSignUpEndpoint(firebaseApiKey), {\n    method: 'POST',\n    headers,\n    body: JSON.stringify(body)\n  });\n\n  return (await createResponse.json()) as AnonymousTokens;\n}\n\ninterface ErrorResponse {\n  error: {\n    code: number;\n    message: 'USER_NOT_FOUND' | 'TOKEN_EXPIRED';\n    status: 'INVALID_ARGUMENT';\n  };\n  error_description?: string;\n}\n\ninterface UserNotFoundResponse extends ErrorResponse {\n  error: {\n    code: 400;\n    message: 'USER_NOT_FOUND';\n    status: 'INVALID_ARGUMENT';\n  };\n}\n\nconst isUserNotFoundResponse = (\n  data: unknown\n): data is UserNotFoundResponse => {\n  return (\n    (data as UserNotFoundResponse)?.error?.code === 400 &&\n    (data as UserNotFoundResponse)?.error?.message === 'USER_NOT_FOUND'\n  );\n};\n\nexport interface TokenRefreshOptions {\n  apiKey: string;\n  referer?: string;\n}\n\nconst refreshExpiredIdToken = async (\n  refreshToken: string,\n  options: TokenRefreshOptions\n): Promise<IdAndRefreshTokens> => {\n  // https://firebase.google.com/docs/reference/rest/auth/#section-refresh-token\n  const response = await fetch(getRefreshTokenEndpoint(options.apiKey), {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/x-www-form-urlencoded',\n      ...(options.referer ? {Referer: options.referer} : {})\n    },\n    body: `grant_type=refresh_token&refresh_token=${refreshToken}`\n  });\n\n  if (!response.ok) {\n    const data = await response.json();\n    const errorMessage = `Error fetching access token: ${JSON.stringify(\n      data.error\n    )} ${data.error_description ? `(${data.error_description})` : ''}`;\n\n    if (isUserNotFoundResponse(data)) {\n      throw new AuthError(AuthErrorCode.USER_NOT_FOUND);\n    }\n\n    throw new AuthError(AuthErrorCode.INVALID_CREDENTIAL, errorMessage);\n  }\n\n  const data = await response.json();\n\n  return {\n    idToken: data.id_token,\n    refreshToken: data.refresh_token\n  };\n};\n\nexport function isUserNotFoundError(error: unknown): error is AuthError {\n  return (error as AuthError)?.code === AuthErrorCode.USER_NOT_FOUND;\n}\n\nexport function isInvalidCredentialError(error: unknown): error is AuthError {\n  return (error as AuthError)?.code === AuthErrorCode.INVALID_CREDENTIAL;\n}\n\nasync function handleVerifyTokenError<T>(\n  e: unknown,\n  onExpired: (e: AuthError) => Promise<T>,\n  onError: (e: unknown) => Promise<T>\n) {\n  try {\n    return await onExpired(e as AuthError);\n  } catch (e) {\n    return onError(e);\n  }\n}\n\nexport async function handleExpiredToken<T>(\n  verifyIdToken: () => Promise<T>,\n  onExpired: (e: AuthError) => Promise<T>,\n  onError: (e: unknown) => Promise<T>,\n  shouldExpireOnNoMatchingKidError: boolean\n): Promise<T> {\n  try {\n    return await verifyIdToken();\n  } catch (e: unknown) {\n    switch ((e as AuthError)?.code) {\n      case AuthErrorCode.NO_MATCHING_KID:\n        if (shouldExpireOnNoMatchingKidError) {\n          return handleVerifyTokenError(\n            e,\n            async (e) => {\n              const result = await onExpired(e);\n\n              debug(\n                'experimental_refresh_on_expired_kid: Successfully refreshed token after kid has expired'\n              );\n\n              return result;\n            },\n            (e) => {\n              debug(\n                'experimental_refresh_on_expired_kid: Error when trying to refresh token after kid has expired',\n                {message: (e as Error)?.message, stack: (e as Error)?.stack}\n              );\n\n              return onError(e);\n            }\n          );\n        }\n\n        return onError(e);\n      case AuthErrorCode.TOKEN_EXPIRED:\n        return handleVerifyTokenError(e, onExpired, onError);\n      default:\n        return onError(e);\n    }\n  }\n}\n\nexport interface IdAndRefreshTokens {\n  idToken: string;\n  refreshToken: string;\n}\n\nexport interface CreateAnonymousRequest {\n  tenantId?: string;\n  appCheckToken?: string;\n  referer?: string;\n}\n\nexport interface AnonymousTokens {\n  idToken: string;\n  refreshToken: string;\n  localId: string;\n}\n\nexport interface UsersList {\n  users: UserRecord[];\n  nextPageToken?: string;\n}\n\nexport interface GetCustomIdAndRefreshTokensOptions {\n  appCheckToken?: string;\n  referer?: string;\n  dynamicCustomClaimsKeys?: string[];\n}\n\ninterface AuthOptions {\n  credential: Credential;\n  apiKey: string;\n  tenantId?: string;\n  serviceAccountId?: string;\n  enableCustomToken?: boolean;\n}\n\nexport type Auth = ReturnType<typeof getAuth>;\n\nconst DEFAULT_VERIFY_OPTIONS = {referer: ''};\n\nfunction getAuth(options: AuthOptions) {\n  const credential = options.credential ?? getApplicationDefault();\n  const tenantId = options.tenantId;\n  const authRequestHandler = new AuthRequestHandler(credential, {\n    tenantId\n  });\n  const tokenGenerator = createFirebaseTokenGenerator(credential, tenantId);\n\n  const handleTokenRefresh = async (\n    refreshToken: string,\n    tokenRefreshOptions: {referer?: string; enableCustomToken?: boolean} = {}\n  ): Promise<TokenSet> => {\n    const {idToken, refreshToken: newRefreshToken} =\n      await refreshExpiredIdToken(refreshToken, {\n        apiKey: options.apiKey,\n        referer: tokenRefreshOptions.referer\n      });\n\n    const decodedIdToken = await verifyIdToken(idToken, {\n      referer: tokenRefreshOptions.referer\n    });\n\n    if (!tokenRefreshOptions.enableCustomToken) {\n      return {\n        decodedIdToken,\n        idToken,\n        refreshToken: newRefreshToken\n      };\n    }\n\n    const customToken = await createCustomToken(decodedIdToken.uid, {\n      email_verified: decodedIdToken.email_verified,\n      source_sign_in_provider: decodedIdToken.firebase.sign_in_provider\n    });\n\n    return {\n      decodedIdToken,\n      idToken,\n      refreshToken: newRefreshToken,\n      customToken\n    };\n  };\n\n  async function createSessionCookie(\n    idToken: string,\n    expiresInMs: number\n  ): Promise<string> {\n    // Verify tenant ID before creating session cookie\n    if (tenantId) {\n      await verifyIdToken(idToken);\n    }\n\n    return authRequestHandler.createSessionCookie(idToken, expiresInMs);\n  }\n\n  async function getUser(uid: string): Promise<UserRecord | null> {\n    return authRequestHandler.getAccountInfoByUid(uid).then((response) => {\n      return response.users?.length ? new UserRecord(response.users[0]) : null;\n    });\n  }\n\n  async function listUsers(\n    nextPageToken?: string,\n    maxResults?: number\n  ): Promise<UsersList> {\n    return authRequestHandler\n      .listUsers(nextPageToken, maxResults)\n      .then((response) => {\n        const result: UsersList = {\n          users: response.users.map((user) => new UserRecord(user))\n        };\n\n        if (response.nextPageToken) {\n          result.nextPageToken = response.nextPageToken;\n        }\n\n        return result;\n      });\n  }\n\n  async function getUserByEmail(email: string): Promise<UserRecord> {\n    return authRequestHandler.getAccountInfoByEmail(email).then((response) => {\n      if (!response.users || !response.users.length) {\n        throw new AuthError(AuthErrorCode.USER_NOT_FOUND);\n      }\n\n      return new UserRecord(response.users[0]);\n    });\n  }\n\n  async function verifyDecodedJWTNotRevokedOrDisabled(\n    decodedIdToken: DecodedIdToken\n  ): Promise<DecodedIdToken> {\n    return getUser(decodedIdToken.sub).then((user: UserRecord | null) => {\n      if (!user) {\n        throw new AuthError(AuthErrorCode.USER_NOT_FOUND);\n      }\n\n      if (user.disabled) {\n        throw new AuthError(AuthErrorCode.USER_DISABLED);\n      }\n\n      if (user.tokensValidAfterTime) {\n        const authTimeUtc = decodedIdToken.auth_time * 1000;\n        const validSinceUtc = new Date(user.tokensValidAfterTime).getTime();\n        if (authTimeUtc < validSinceUtc) {\n          throw new AuthError(AuthErrorCode.TOKEN_REVOKED);\n        }\n      }\n\n      return decodedIdToken;\n    });\n  }\n\n  async function verifyIdToken(\n    idToken: string,\n    options: VerifyOptions = DEFAULT_VERIFY_OPTIONS\n  ): Promise<DecodedIdToken> {\n    const projectId = await credential.getProjectId();\n    const idTokenVerifier = createIdTokenVerifier(projectId, tenantId);\n    const decodedIdToken = await idTokenVerifier.verifyJWT(idToken, options);\n    const checkRevoked = options.checkRevoked ?? false;\n\n    if (checkRevoked) {\n      return verifyDecodedJWTNotRevokedOrDisabled(decodedIdToken);\n    }\n\n    return decodedIdToken;\n  }\n\n  async function verifyAndRefreshExpiredIdToken<Metadata extends object>(\n    parsedCookies: ParsedCookies<Metadata>,\n    verifyOptions: VerifyOptions & {\n      onTokenRefresh?: (tokens: TokenSet) => Promise<void>;\n    } = DEFAULT_VERIFY_OPTIONS\n  ): Promise<TokenSet> {\n    return await handleExpiredToken(\n      async () => {\n        const decodedIdToken = await verifyIdToken(\n          parsedCookies.idToken,\n          verifyOptions\n        );\n        return {\n          idToken: parsedCookies.idToken,\n          decodedIdToken,\n          refreshToken: parsedCookies.refreshToken,\n          customToken: parsedCookies.customToken\n        };\n      },\n      async () => {\n        if (parsedCookies.refreshToken) {\n          const result = await handleTokenRefresh(parsedCookies.refreshToken, {\n            referer: verifyOptions.referer,\n            enableCustomToken: options.enableCustomToken\n          });\n\n          await verifyOptions.onTokenRefresh?.(result);\n\n          return result;\n        }\n\n        throw new InvalidTokenError(InvalidTokenReason.MISSING_REFRESH_TOKEN);\n      },\n      async (e) => {\n        if (\n          e instanceof AuthError &&\n          e.code === AuthErrorCode.NO_MATCHING_KID\n        ) {\n          throw InvalidTokenError.fromError(e, InvalidTokenReason.INVALID_KID);\n        }\n\n        throw InvalidTokenError.fromError(\n          e,\n          InvalidTokenReason.INVALID_CREDENTIALS\n        );\n      },\n      verifyOptions.enableTokenRefreshOnExpiredKidHeader ?? false\n    );\n  }\n\n  function createCustomToken(\n    uid: string,\n    developerClaims?: {[key: string]: unknown}\n  ): Promise<string> {\n    return tokenGenerator.createCustomToken(uid, developerClaims);\n  }\n\n  async function getCustomIdAndRefreshTokens(\n    idToken: string,\n    customTokensOptions: GetCustomIdAndRefreshTokensOptions = DEFAULT_VERIFY_OPTIONS\n  ): Promise<CustomTokens> {\n    const decodedToken = await verifyIdToken(idToken, {\n      referer: customTokensOptions.referer\n    });\n\n    const customClaims = filterStandardClaims(decodedToken);\n\n    if (customTokensOptions.dynamicCustomClaimsKeys?.length) {\n      customTokensOptions.dynamicCustomClaimsKeys.forEach((key) => {\n        delete customClaims[key];\n      });\n    }\n\n    const customToken = await createCustomToken(decodedToken.uid, {\n      ...customClaims,\n      email_verified: decodedToken.email_verified,\n      source_sign_in_provider: decodedToken.firebase.sign_in_provider\n    });\n\n    debug('Generated custom token based on provided idToken', {customToken});\n\n    const idAndRefreshTokens = await customTokenToIdAndRefreshTokens(\n      customToken,\n      options.apiKey,\n      {\n        tenantId: options.tenantId,\n        appCheckToken: customTokensOptions.appCheckToken,\n        referer: customTokensOptions.referer\n      }\n    );\n\n    return {\n      ...idAndRefreshTokens,\n      customToken\n    };\n  }\n\n  async function deleteUser(uid: string): Promise<void> {\n    await authRequestHandler.deleteAccount(uid);\n  }\n\n  async function setCustomUserClaims(\n    uid: string,\n    customUserClaims: object | null\n  ) {\n    await authRequestHandler.setCustomUserClaims(uid, customUserClaims);\n  }\n\n  async function createUser(properties: CreateRequest): Promise<UserRecord> {\n    return authRequestHandler\n      .createNewAccount(properties)\n      .then((uid) => getUser(uid))\n      .then((user) => {\n        if (!user) {\n          throw new AuthError(\n            AuthErrorCode.INTERNAL_ERROR,\n            'Could not get recently created user from database. Most likely it was deleted.'\n          );\n        }\n        return user;\n      });\n  }\n\n  async function createAnonymousUser(\n    firebaseApiKey: string\n  ): Promise<AnonymousTokens> {\n    return createAnonymousAccount(firebaseApiKey);\n  }\n\n  async function updateUser(\n    uid: string,\n    properties: UpdateRequest\n  ): Promise<UserRecord> {\n    return authRequestHandler\n      .updateExistingAccount(uid, properties)\n      .then((existingUid) => getUser(existingUid))\n      .then((user) => {\n        if (!user) {\n          throw new AuthError(\n            AuthErrorCode.INTERNAL_ERROR,\n            'Could not get recently updated user from database. Most likely it was deleted.'\n          );\n        }\n\n        return user;\n      });\n  }\n\n  return {\n    verifyAndRefreshExpiredIdToken,\n    verifyIdToken,\n    createCustomToken,\n    getCustomIdAndRefreshTokens,\n    handleTokenRefresh,\n    deleteUser,\n    setCustomUserClaims,\n    getUser,\n    getUserByEmail,\n    updateUser,\n    createUser,\n    createAnonymousUser,\n    listUsers,\n    createSessionCookie\n  };\n}\n\nfunction isFirebaseAuthOptions(\n  options: FirebaseAuthOptions | ServiceAccount\n): options is FirebaseAuthOptions {\n  const serviceAccount = options as ServiceAccount;\n\n  return (\n    !serviceAccount.privateKey ||\n    !serviceAccount.projectId ||\n    !serviceAccount.clientEmail\n  );\n}\n\nexport interface FirebaseAuthOptions {\n  serviceAccount?: ServiceAccount;\n  apiKey: string;\n  tenantId?: string;\n  serviceAccountId?: string;\n  enableCustomToken?: boolean;\n}\nexport function getFirebaseAuth(options: FirebaseAuthOptions): Auth;\n/** @deprecated Use `FirebaseAuthOptions` configuration object instead */\nexport function getFirebaseAuth(\n  serviceAccount: ServiceAccount,\n  apiKey: string,\n  tenantId?: string\n): Auth;\nexport function getFirebaseAuth(\n  serviceAccount: ServiceAccount | FirebaseAuthOptions,\n  apiKey?: string,\n  tenantId?: string\n): Auth {\n  if (!isFirebaseAuthOptions(serviceAccount)) {\n    const credential = new ServiceAccountCredential(serviceAccount);\n\n    return getAuth({credential, apiKey: apiKey!, tenantId});\n  }\n\n  const options = serviceAccount;\n\n  return getAuth({\n    credential: options.serviceAccount\n      ? new ServiceAccountCredential(options.serviceAccount)\n      : getApplicationDefault(),\n    apiKey: options.apiKey,\n    tenantId: options.tenantId,\n    serviceAccountId: options.serviceAccountId,\n    enableCustomToken: options.enableCustomToken\n  });\n}\n"
  },
  {
    "path": "src/auth/jwt/consts.ts",
    "content": "export const ALGORITHMS = {\n  RS256: {\n    name: 'RSASSA-PKCS1-v1_5' as const,\n    hash: 'SHA-256' as const\n  }\n};\n"
  },
  {
    "path": "src/auth/jwt/crypto-signer.ts",
    "content": "import {base64url, JWTPayload} from 'jose';\nimport {Credential, ServiceAccountCredential} from '../credential.js';\nimport {fetchText} from '../utils.js';\nimport {sign, signBlob} from './sign.js';\nimport {ALGORITHM_RS256} from './verify.js';\n\nexport interface CryptoSigner {\n  sign(payload: JWTPayload): Promise<string>;\n  getAccountId(): Promise<string>;\n}\n\nexport function createEmulatorToken(payload: JWTPayload) {\n  const header = {\n    alg: 'none',\n    typ: 'JWT'\n  };\n\n  return `${base64url.encode(JSON.stringify(header))}.${base64url.encode(JSON.stringify(payload))}.${base64url.encode('')}`;\n}\n\nexport class EmulatorSigner implements CryptoSigner {\n  constructor(private readonly tenantId?: string) {}\n\n  public async sign(payload: JWTPayload): Promise<string> {\n    if (this.tenantId) {\n      payload.tenant_id = this.tenantId;\n    }\n\n    return createEmulatorToken(payload);\n  }\n\n  public getAccountId(): Promise<string> {\n    return Promise.resolve('firebase-auth-emulator@example.com');\n  }\n}\n\nexport class ServiceAccountSigner implements CryptoSigner {\n  constructor(\n    private readonly credential: ServiceAccountCredential,\n    private readonly tenantId?: string\n  ) {}\n\n  public async sign(payload: JWTPayload): Promise<string> {\n    if (this.tenantId) {\n      payload.tenant_id = this.tenantId;\n    }\n\n    return sign({payload, privateKey: this.credential.privateKey});\n  }\n\n  public getAccountId(): Promise<string> {\n    return Promise.resolve(this.credential.clientEmail);\n  }\n}\n\nexport class IAMSigner implements CryptoSigner {\n  algorithm = ALGORITHM_RS256;\n\n  private credential: Credential;\n  private tenantId?: string;\n  private serviceAccountId?: string;\n\n  constructor(\n    credential: Credential,\n    tenantId?: string,\n    serviceAccountId?: string\n  ) {\n    this.credential = credential;\n    this.tenantId = tenantId;\n    this.serviceAccountId = serviceAccountId;\n  }\n\n  public async sign(payload: JWTPayload): Promise<string> {\n    if (this.tenantId) {\n      payload.tenant_id = this.tenantId;\n    }\n\n    const serviceAccount = await this.getAccountId();\n    const accessToken = await this.credential.getAccessToken();\n\n    return signBlob({\n      accessToken: accessToken.accessToken,\n      serviceAccountId: serviceAccount,\n      payload\n    });\n  }\n\n  public async getAccountId(): Promise<string> {\n    if (this.serviceAccountId) {\n      return this.serviceAccountId;\n    }\n\n    const token = await this.credential.getAccessToken();\n    const url =\n      'http://metadata/computeMetadata/v1/instance/service-accounts/default/email';\n    const request: RequestInit = {\n      method: 'GET',\n      headers: {\n        'Metadata-Flavor': 'Google',\n        Authorization: `Bearer ${token.accessToken}`\n      }\n    };\n\n    return (this.serviceAccountId = await fetchText(url, request));\n  }\n}\n"
  },
  {
    "path": "src/auth/jwt/sign.test.ts",
    "content": "import {sign} from './sign.js';\n\ntype GlobalAny = {\n  [key: string]: unknown;\n  crypto: {\n    subtle: {\n      importKey: jest.Mock;\n      sign: jest.Mock;\n    };\n  };\n};\n\nconst PRIVATE_KEY =\n  '-----BEGIN PRIVATE KEY-----\\n' +\n  'MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDFL3Pc33GkWha1\\n' +\n  'WSMRw5e3spUCQAwpGnsRBMcG6JH+eHeHOJYI0odFPqCujVQTyTOEZwKZUtzRvMWP\\n' +\n  'h+kgIdDw+1+hzsJJTDijHHNi/G/g2kPHpXXVaEdOKnwvOddhC3L79W2vxhcx2e64\\n' +\n  'LwhbdP880GKHTiCzx8CjkpRExyzN935wHQ90IGaN/mGOQcBE/3j8u6oDqRbxt+IG\\n' +\n  'xypQTRZR7TFw/Z9OYNt0pr0BI0jNMMDkmAxkUNH6Qw4eurQ7XXATS3cWnR4qiAIp\\n' +\n  'S2HIxGiCr7PpVJJSWtTVqMgDF6y2xXtiw9H8Gdo36rTytUpyFH0gQeL923+sy9ru\\n' +\n  'y4aWqL19AgMBAAECggEAXBVN6S6btmGvyx6GRwxtNIb8GSHpy+Qm5oqxmyNO0mRV\\n' +\n  'hVtCjXorW4Xkqb8sLVU/bqxgRVOx9WxPYjjZAH1qQq9ROJICnxIuPNXTeL1kTb//\\n' +\n  '+SLmxTM+YV1rwu4jC5m6J7m0cGp0eH5Kgc7M+1DGxRKXgJJWqT42Uuznurq8zK3e\\n' +\n  'iEmbqLQrDQuhjAcVsy/1DuQWkUy/TbIQ/9YxgsJfAy9T4QMh8KNngpud/gA8PWEM\\n' +\n  'BkWJlk2e5hdHB9RJdZeBmzOLaKmVQ6oIwi+V7p9udH4lWAiGqBy9ziexTVFDk8Xi\\n' +\n  'dY/zHKEFyQc+7rNhVDsKJvJaZNAZk44hLORcmfcbDQKBgQDknvpqIi3jgcyOy9G4\\n' +\n  'lO2dRIIQY75fZKH1A3etU5k3irL2JlBYIqs5+YKUpAf3gueKF/jY5p/7duxYFvWn\\n' +\n  'fj8UyHWVAweShcrp5hD51DXcWssy8i9UgzqjI/mg0sdVksUbwLllSPJ3sHv1QxY3\\n' +\n  'ylq8ng0o01BdjzKngUiE6RJq0wKBgQDczLnam9EjlqhC/8ud5/tfPyD4go3n/LEo\\n' +\n  '1E7VN8IM2rshWZ/YRAw4aZ/mM9mx42pI1LBmaG0VJmGEZMYIzM114KuNiqnQSkOe\\n' +\n  'rj4YCahkwkyHIxPjLDzX/hMIEFS/23nihOuC+FQOluHBLwmUZOOUk94s5O/oQp9x\\n' +\n  'c5JVtwZkbwKBgEuoMMaeuQDpG4DGAolK/7djzIcP+xgmfVJP63L4j2PKCp9a3ovM\\n' +\n  'LU3qPERkZB6Mu4L/m+Jrr9XP7TbZokHjjYybKg4+Cmt6y0PMVyHWEFzzzvr1GqSl\\n' +\n  'KOqEJUALgNvYzlH43WGfWl4xkVQA94FO/egdhc1U4OuVT/YO2qjhWK7xAoGBANyF\\n' +\n  'I8HwCUqP93Ei5IvK20XfWOCaE3x05cMvd6R/0bDg7DB8wKZQIBxfcbGKa4u847Pl\\n' +\n  'qGA/P2L2OELwGtFDKpjmULBGox9CbJKY169ORf6MB76YDA7BaesW+I7/MIWFgA/6\\n' +\n  'TPU7a0g+7S3x+pFYyerkW+teozTHBVNb5/TvnNTFAoGBAJSkdCSHsKUWDeTDDs8W\\n' +\n  'xJc+0rgIjrbsYfu/jFoe4gWomr4b76PVPm7A5PvDkwt91amTPO3QpFj8kTMz4mKP\\n' +\n  'RXOPfGOI0RVxREB6uKYC1H2eMOFformJXm27wxxMhdhAgbK3dye7Bn7Owrj5Ek67\\n' +\n  '4SD8Q27MvEowXdJvH32RE8ap\\n' +\n  '-----END PRIVATE KEY-----';\n\ndescribe('sign', () => {\n  const globalOriginal = {...global} as unknown as GlobalAny;\n  let globalAny = global as unknown as GlobalAny;\n\n  beforeAll(() => {\n    globalAny.crypto = {\n      subtle: {\n        importKey: jest.fn(),\n        sign: jest.fn()\n      }\n    };\n  });\n\n  afterAll(() => {\n    globalAny = globalOriginal;\n  });\n\n  beforeEach(() => {\n    const cryptoKey = {\n      algorithm: {\n        name: 'RSASSA-PKCS1-v1_5',\n        hash: 'SHA-256'\n      },\n      extractable: false,\n      type: 'JWT',\n      usages: ['sign']\n    };\n    const signature = new Uint8Array(\n      'secret'.split('').map((character) => character.charCodeAt(0))\n    ).buffer;\n\n    globalAny.crypto.subtle.importKey.mockReturnValue(cryptoKey);\n    globalAny.crypto.subtle.sign.mockReturnValue(signature);\n  });\n\n  it('provides expected JWT with privateKey', async () => {\n    expect.assertions(1);\n\n    const payload = {exp: 946688400};\n\n    expect(await sign({payload, privateKey: PRIVATE_KEY})).toBe(\n      'eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjk0NjY4ODQwMH0.qZUNyJG5IBztEQuTxAIHkLcGxI8on81rsw0MQt6gC3hHebWZx-icF05M-PwbHjJkGIGxvlzwOHdpzV1xiJN32BQtZDPa3SMx7DeMYrNS3h1gV_hAz0ylnrja-zBIGWb_Q1MZU_jMmrvYCk8wd2qU4SqbnC3LNVPxoxVsIMUMdTtA2fZ5Wk99LkYnPn-UMuR0vSMoJ2foCe2Imhwmjrfa47xxUIK0126GdX3qmY2Ico9KgfOQJz1ksJOrd1yCSVEK9QLRLGC4PAruyW_EWln1s6Bzger1v4MPjFmZULMoLGzW3R31rjdF51FFCswHf2miTP2VkJW3i_ng5XdQS-LUKA'\n    );\n  });\n\n  it('adds keyId to ecnoded JWT header', async () => {\n    expect.assertions(1);\n\n    const payload = {exp: 946688400};\n    const keyId = 'key123';\n\n    expect(await sign({payload, privateKey: PRIVATE_KEY, keyId})).toBe(\n      'eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleTEyMyJ9.eyJleHAiOjk0NjY4ODQwMH0.t60FJH4NlQysOhzsvHYwRI5eOQqcwgyUqdkOtwV9US4cl04J3ynb9Rfme1bnq29HVqzpHpTHg8qxOFcCPbBr-kwbpg9dRVXHteTtE-LH9L-9TP9eQ8KeNwx6toNyESdGPsNPduMKSSgXMoITpfN1QGpgiREXCPo7atRmL7Jmzt1-9vocwlLCqx5gx_X9x8uPwzHZPu66Q28rzj_Kib9bBjbRFObA0OYMi2raI5DrbvEj2eZgYP4QhxOmfKkYGskW5Ne0GOy4YIPNMsFnw_rH3UhM_fqb7vwIe0f3zB9vK2WknxkNkNvwCrza_R-o98PG-qmvVWsTgnrmCoFfL40rLA'\n    );\n  });\n});\n"
  },
  {
    "path": "src/auth/jwt/sign.ts",
    "content": "import {JWTPayload, KeyLike, SignJWT, base64url, importPKCS8} from 'jose';\nimport {AuthError, AuthErrorCode} from '../error.js';\nimport {fetchAny} from '../utils.js';\nimport {ALGORITHM_RS256} from './verify.js';\n\nexport type SignOptions = {\n  readonly payload: JWTPayload;\n  readonly privateKey: string;\n  readonly keyId?: string;\n};\n\nexport async function sign({\n  payload,\n  privateKey,\n  keyId\n}: SignOptions): Promise<string> {\n  let key: KeyLike;\n\n  try {\n    key = await importPKCS8(privateKey, ALGORITHM_RS256);\n  } catch (e) {\n    throw AuthError.fromError(\n      e,\n      AuthErrorCode.INVALID_ARGUMENT,\n      'It looks like the value provided for `serviceAccount.privateKey` is incorrectly formatted. Please double-check if private key has correct format. See https://github.com/awinogrodzki/next-firebase-auth-edge/issues/246#issuecomment-2321559620 for details'\n    );\n  }\n\n  return new SignJWT(payload)\n    .setProtectedHeader({alg: ALGORITHM_RS256, kid: keyId})\n    .sign(key);\n}\n\nexport type SignBlobOptions = {\n  readonly serviceAccountId: string;\n  readonly accessToken: string;\n  readonly payload: JWTPayload;\n};\n\nfunction formatBase64(value: string) {\n  return value.replace(/\\//g, '_').replace(/\\+/g, '-').replace(/=+$/, '');\n}\n\nfunction encodeSegment(segment: Record<string, string> | JWTPayload): string {\n  const value = JSON.stringify(segment);\n\n  return formatBase64(base64url.encode(value));\n}\n\nexport async function signBlob({\n  payload,\n  serviceAccountId,\n  accessToken\n}: SignBlobOptions): Promise<string> {\n  const url = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccountId}:signBlob`;\n  const header = {\n    alg: ALGORITHM_RS256,\n    typ: 'JWT'\n  };\n  const token = `${encodeSegment(header)}.${encodeSegment(payload)}`;\n  const request: RequestInit = {\n    method: 'POST',\n    headers: {\n      Authorization: `Bearer ${accessToken}`\n    },\n    body: JSON.stringify({payload: base64url.encode(token)})\n  };\n  const response = await fetchAny(url, request);\n  const blob = await response.blob();\n  const key = await blob.text();\n  const {signedBlob} = JSON.parse(key);\n\n  return `${token}.${formatBase64(signedBlob)}`;\n}\n"
  },
  {
    "path": "src/auth/jwt/verify.test.ts",
    "content": "import {sign} from './sign.js';\nimport {getPublicCryptoKey, verify} from './verify.js';\n\ndescribe('verify', () => {\n  it('verifies jwt', async () => {\n    const payload = {exp: 9123812123123};\n    const privateKey = `-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDHDTErwJZxwJQH\nq+Z6t6qwxuyciqfJauCDD6IUf619noIRQZ4GZCUqFkxX8mOPYnEhLApLQdbIlgWq\nEEFJLYBP/UH5ojaOMsmO28K+GY/M7UHvZnxweFv48xjED4R2gRcGAcS+LiuUBRke\nNAKcXs41HET50wwClHkVX8OQeOK5R7q6hwwA3+8D4o/XpPYp13rROHrWDHhYNEIP\n4s7n8klmP/wSLhYmzFoGBSzOxC4N0mH0p47vTTWHoZjyvbTDzjn4n5p6DMV7ggps\no5+1uUCAu8c5uVUiaj5MnBDtXG+CvRgbkT7RCVMuHdHZx1DYzIOtPvp0JMww201Q\nJnhZs15pAgMBAAECggEABLRmHh+eLrAbj5bbirj+mtEI1KZeUt9o0RA0h4GBC0AM\n2PWRE5uYWUdPpKCBA+mSvPL6h07WEcWh+qQJtv4RU1KsFYdk/LVsmCjPkIiwImrV\nLSBh/pKJsfek9TVcryRb8/NkwA39T7FTJ6iZCzMecpjpdHItjX4O4pdx2t9QlIp3\nvV6Ob7u+NxgnLCOVP0HvxghTwaX5rWaHbt1TcUmK8069TPPj/AItN6a1S30CCgxU\nyVDdfMOThruJUTcUB0mOTux7ZV+JVEA9oUsrChN1LN30uco2d9n77p2U+R/7/CjV\nyYOQWT6Nn4m8EmKxmPbn7NfpiHE4gOAmp7r8ZvsndQKBgQD8kV+l63q8jBGQ1maV\nX+ZeUcdROq9TV6lRbtRvSgsyXEUMCp3MJMut89XD1T3BkxVEIyYgyAUZ30QnQF/q\nHLnIfBmrnQoUp2b1ZOi/G3emOjNMycYYaeR0Y0MHiiOGxVY20KZe/jsD/knqcYB9\njqv2dPTVL/UHVtB/QFXcPf7wQwKBgQDJwaWyDxXghaNWjHCOQ4x1QHVUQAPYlhN6\nkBLI9NYHMq/4glN7G+xOjs87qKi2WE1B57WFrsp02e9eEigKDhxHGTtYKbS7xXfR\nLpnvGxVE8ZBQK7Rkiiw/0E6xWSXimeYlKPrDQ4jrL0XWh7dgJ4DBfGgpgTYO4DbX\n/1jvFAKx4wKBgQCYzNaCCfnCUjdaWevMGS3FCFK+uPNTR6ifJJ8PCUvG1v3K8C1R\nUT2MawV7qennz7VA+MbbdEdpxKJ14MNmXqSjPzlEkwiDQFfQxJDu9Y4omfNpVHUt\nVfsp0te9mvwtT/v9w7OzqrlHjDNpy+tBiuxMeauZwp7KJuKS6fhH+5XeAwKBgBVY\nrcVXH0NwIEYJ+eazcur89O0DEOUbi9gN4k7syLBeRowOjfKak7gEGB0BzUfts87j\nSytnwPf4DwFu/lmCAK/tFYBQeVTcob66JYNM5EU1IcW5ug5hKClgStMs0XtWOSl5\nWn7KaHQpvkPifB5qT48pMIQjraqJQoQ7+hbhkR9tAoGBAKrNCHGq3edhgnVjHJbj\nj3rJk3aIe7UffHoNDkq/xE32W1P4ra3t81ItdLRXEJU/XU6ZmbvEpjJUMfXiIZHt\nwmK2NLjp4+wipPmSidEwyabBAk4Epb0qIG+MM2RvMUOC8kkV2p5lNFkZYR346wtI\nAIqW5jTJYZYfCnGuCyV0F0C0\n-----END PRIVATE KEY-----`;\n\n    const publicKey = `-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxw0xK8CWccCUB6vmereq\nsMbsnIqnyWrggw+iFH+tfZ6CEUGeBmQlKhZMV/Jjj2JxISwKS0HWyJYFqhBBSS2A\nT/1B+aI2jjLJjtvCvhmPzO1B72Z8cHhb+PMYxA+EdoEXBgHEvi4rlAUZHjQCnF7O\nNRxE+dMMApR5FV/DkHjiuUe6uocMAN/vA+KP16T2Kdd60Th61gx4WDRCD+LO5/JJ\nZj/8Ei4WJsxaBgUszsQuDdJh9KeO7001h6GY8r20w845+J+aegzFe4IKbKOftblA\ngLvHOblVImo+TJwQ7Vxvgr0YG5E+0QlTLh3R2cdQ2MyDrT76dCTMMNtNUCZ4WbNe\naQIDAQAB\n-----END PUBLIC KEY-----\n`;\n\n    const jwt = await sign({\n      payload,\n      privateKey\n    });\n\n    await verify(jwt, async () => getPublicCryptoKey(publicKey), {\n      referer: 'http://localhost:3000'\n    });\n  });\n});\n"
  },
  {
    "path": "src/auth/jwt/verify.ts",
    "content": "import {\n  decodeJwt,\n  errors,\n  importSPKI,\n  importX509,\n  jwtVerify,\n  KeyLike\n} from 'jose';\nimport {useEmulator} from '../firebase.js';\nimport {DecodedIdToken, VerifyOptions} from '../types.js';\n\nexport const ALGORITHM_RS256 = 'RS256' as const;\n\nconst keyMap: Map<string, KeyLike> = new Map();\n\nasync function importPublicCryptoKey(publicKey: string) {\n  if (publicKey.startsWith('-----BEGIN CERTIFICATE-----')) {\n    return importX509(publicKey, ALGORITHM_RS256);\n  }\n\n  return importSPKI(publicKey, ALGORITHM_RS256);\n}\n\nexport async function getPublicCryptoKey(publicKey: string): Promise<KeyLike> {\n  const cachedKey = keyMap.get(publicKey);\n\n  if (cachedKey) {\n    return cachedKey;\n  }\n\n  const key = await importPublicCryptoKey(publicKey);\n  keyMap.set(publicKey, key);\n  return key;\n}\n\nexport async function verify(\n  jwtString: string,\n  getPublicKey: () => Promise<KeyLike>,\n  options: VerifyOptions\n) {\n  const currentDate = options.currentDate ?? new Date();\n  const currentTimestamp = currentDate.getTime() / 1000;\n  const payload = decodeJwt(jwtString);\n\n  if (!useEmulator()) {\n    const {payload} = await jwtVerify(jwtString, await getPublicKey(), {\n      currentDate\n    });\n\n    return payload as DecodedIdToken;\n  }\n\n  if (typeof payload.nbf !== 'undefined') {\n    if (typeof payload.nbf !== 'number') {\n      throw new errors.JWTInvalid('invalid nbf value');\n    }\n    if (payload.nbf > currentTimestamp) {\n      throw new errors.JWTExpired(\n        'jwt not active: ' + new Date(payload.nbf * 1000).toISOString(),\n        payload\n      );\n    }\n  }\n\n  if (typeof payload.exp !== 'undefined') {\n    if (typeof payload.exp !== 'number') {\n      throw new errors.JWTInvalid('invalid exp value');\n    }\n\n    if (currentTimestamp >= payload.exp) {\n      throw new errors.JWTExpired(\n        'token expired: ' + new Date(payload.exp * 1000).toISOString(),\n        payload\n      );\n    }\n  }\n\n  return payload as DecodedIdToken;\n}\n"
  },
  {
    "path": "src/auth/rotating-credential.test.ts",
    "content": "import {errors} from 'jose';\nimport {CustomJWTPayload, ParsedCookies} from './custom-token/index.js';\nimport {RotatingCredential} from './rotating-credential.js';\n\ntype MockMetadata = {foo: 'bar'};\n\ndescribe('rotating-credential', () => {\n  const jwt =\n    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6Ik1PQ0tfSURfVE9LRU4iLCJyZWZyZXNoX3Rva2VuIjoiTU9DS19SRUZSRVNIX1RPS0VOIiwiY3VzdG9tX3Rva2VuIjoiTU9DS19DVVNUT01fVE9LRU4iLCJtZXRhZGF0YSI6eyJmb28iOiJiYXIifX0.F54FoDyst6RipYvP9pma6ID7rRAcho_4Pl3Sp6Fr2I4';\n  const signature = 'OjicIJZDY8ZipDpsHnwET1M3F1n7oRwd87SMgH_77Kk';\n\n  const payload: CustomJWTPayload<MockMetadata> = {\n    id_token: 'MOCK_ID_TOKEN',\n    refresh_token: 'MOCK_REFRESH_TOKEN',\n    custom_token: 'MOCK_CUSTOM_TOKEN',\n    metadata: {foo: 'bar'}\n  };\n\n  const customTokens: ParsedCookies<MockMetadata> = {\n    idToken: 'MOCK_ID_TOKEN',\n    refreshToken: 'MOCK_REFRESH_TOKEN',\n    customToken: 'MOCK_CUSTOM_TOKEN',\n    metadata: {foo: 'bar'}\n  };\n\n  it('should sign custom jwt payload', async () => {\n    const credential = new RotatingCredential(['key1', 'key2']);\n    const customJWT = await credential.sign(payload);\n\n    expect(customJWT).toEqual(jwt);\n  });\n\n  it('should create signature', async () => {\n    const credential = new RotatingCredential(['key1', 'key2']);\n    const customSignature = await credential.createSignature(customTokens);\n\n    expect(customSignature).toEqual(signature);\n  });\n\n  it('should verify signature', async () => {\n    const credential = new RotatingCredential(['key1', 'key2']);\n\n    return expect(() => credential.verifySignature(customTokens, signature))\n      .resolves;\n  });\n\n  it('should verify signature with rotated keys', async () => {\n    const credential = new RotatingCredential(['key3', 'key1']);\n\n    await credential.verifySignature(customTokens, signature);\n  });\n\n  it('should throw invalid signature error if no keys match signature', async () => {\n    const credential = new RotatingCredential(['key3']);\n\n    return expect(() =>\n      credential.verifySignature(customTokens, signature)\n    ).rejects.toBeInstanceOf(errors.JWSSignatureVerificationFailed);\n  });\n\n  it('should verify custom jwt payload', async () => {\n    const credential = new RotatingCredential(['key1', 'key2']);\n\n    const result = await credential.verify(jwt);\n\n    expect(result).toEqual(payload);\n  });\n\n  it('should verify custom jwt payload with rotated key', async () => {\n    const credential = new RotatingCredential(['key0', 'key1']);\n\n    const result = await credential.verify(jwt);\n\n    expect(result).toEqual(payload);\n  });\n\n  it('should throw invalid signature error if no key matches signature', async () => {\n    const credential = new RotatingCredential(['key3', 'key4']);\n\n    return expect(() => credential.verify(jwt)).rejects.toBeInstanceOf(\n      errors.JWSSignatureVerificationFailed\n    );\n  });\n});\n"
  },
  {
    "path": "src/auth/rotating-credential.ts",
    "content": "import {errors} from 'jose';\nimport {\n  CustomJWTPayload,\n  ParsedCookies,\n  createCustomJWT,\n  createCustomSignature,\n  verifyCustomJWT,\n  verifyCustomSignature\n} from './custom-token/index.js';\n\nexport class RotatingCredential<Metadata extends object> {\n  constructor(private keys: string[]) {}\n\n  public async sign(payload: CustomJWTPayload<Metadata>) {\n    return createCustomJWT(payload, this.keys[0]);\n  }\n\n  public async createSignature(\n    value: ParsedCookies<Metadata>\n  ): Promise<string> {\n    return createCustomSignature(value, this.keys[0]);\n  }\n\n  public async verify(customJWT: string): Promise<CustomJWTPayload<Metadata>> {\n    for (const key of this.keys) {\n      try {\n        const result = await verifyCustomJWT<Metadata>(customJWT, key);\n        return result.payload;\n      } catch (e) {\n        if (\n          e instanceof errors.JWSSignatureVerificationFailed ||\n          e instanceof errors.JWSInvalid\n        ) {\n          continue;\n        }\n\n        throw e;\n      }\n    }\n\n    throw new errors.JWSSignatureVerificationFailed(\n      'Custom JWT could not be verified against any of the provided keys'\n    );\n  }\n\n  public async verifySignature(\n    value: ParsedCookies<Metadata>,\n    signature: string\n  ): Promise<void> {\n    for (const key of this.keys) {\n      try {\n        return await verifyCustomSignature(value, signature, key);\n      } catch (e) {\n        if (\n          e instanceof errors.JWSSignatureVerificationFailed ||\n          e instanceof errors.JWSInvalid\n        ) {\n          continue;\n        }\n\n        throw e;\n      }\n    }\n\n    throw new errors.JWSSignatureVerificationFailed(\n      'Custom tokens signature could not be verified against any of the provided keys'\n    );\n  }\n}\n"
  },
  {
    "path": "src/auth/signature-verifier.test.ts",
    "content": "import {errors} from 'jose';\nimport {AuthError, AuthErrorCode} from './error.js';\nimport {sign} from './jwt/sign.js';\nimport {KeyFetcher, PublicKeySignatureVerifier} from './signature-verifier.js';\n\nconst privateKey = `-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDHDTErwJZxwJQH\nq+Z6t6qwxuyciqfJauCDD6IUf619noIRQZ4GZCUqFkxX8mOPYnEhLApLQdbIlgWq\nEEFJLYBP/UH5ojaOMsmO28K+GY/M7UHvZnxweFv48xjED4R2gRcGAcS+LiuUBRke\nNAKcXs41HET50wwClHkVX8OQeOK5R7q6hwwA3+8D4o/XpPYp13rROHrWDHhYNEIP\n4s7n8klmP/wSLhYmzFoGBSzOxC4N0mH0p47vTTWHoZjyvbTDzjn4n5p6DMV7ggps\no5+1uUCAu8c5uVUiaj5MnBDtXG+CvRgbkT7RCVMuHdHZx1DYzIOtPvp0JMww201Q\nJnhZs15pAgMBAAECggEABLRmHh+eLrAbj5bbirj+mtEI1KZeUt9o0RA0h4GBC0AM\n2PWRE5uYWUdPpKCBA+mSvPL6h07WEcWh+qQJtv4RU1KsFYdk/LVsmCjPkIiwImrV\nLSBh/pKJsfek9TVcryRb8/NkwA39T7FTJ6iZCzMecpjpdHItjX4O4pdx2t9QlIp3\nvV6Ob7u+NxgnLCOVP0HvxghTwaX5rWaHbt1TcUmK8069TPPj/AItN6a1S30CCgxU\nyVDdfMOThruJUTcUB0mOTux7ZV+JVEA9oUsrChN1LN30uco2d9n77p2U+R/7/CjV\nyYOQWT6Nn4m8EmKxmPbn7NfpiHE4gOAmp7r8ZvsndQKBgQD8kV+l63q8jBGQ1maV\nX+ZeUcdROq9TV6lRbtRvSgsyXEUMCp3MJMut89XD1T3BkxVEIyYgyAUZ30QnQF/q\nHLnIfBmrnQoUp2b1ZOi/G3emOjNMycYYaeR0Y0MHiiOGxVY20KZe/jsD/knqcYB9\njqv2dPTVL/UHVtB/QFXcPf7wQwKBgQDJwaWyDxXghaNWjHCOQ4x1QHVUQAPYlhN6\nkBLI9NYHMq/4glN7G+xOjs87qKi2WE1B57WFrsp02e9eEigKDhxHGTtYKbS7xXfR\nLpnvGxVE8ZBQK7Rkiiw/0E6xWSXimeYlKPrDQ4jrL0XWh7dgJ4DBfGgpgTYO4DbX\n/1jvFAKx4wKBgQCYzNaCCfnCUjdaWevMGS3FCFK+uPNTR6ifJJ8PCUvG1v3K8C1R\nUT2MawV7qennz7VA+MbbdEdpxKJ14MNmXqSjPzlEkwiDQFfQxJDu9Y4omfNpVHUt\nVfsp0te9mvwtT/v9w7OzqrlHjDNpy+tBiuxMeauZwp7KJuKS6fhH+5XeAwKBgBVY\nrcVXH0NwIEYJ+eazcur89O0DEOUbi9gN4k7syLBeRowOjfKak7gEGB0BzUfts87j\nSytnwPf4DwFu/lmCAK/tFYBQeVTcob66JYNM5EU1IcW5ug5hKClgStMs0XtWOSl5\nWn7KaHQpvkPifB5qT48pMIQjraqJQoQ7+hbhkR9tAoGBAKrNCHGq3edhgnVjHJbj\nj3rJk3aIe7UffHoNDkq/xE32W1P4ra3t81ItdLRXEJU/XU6ZmbvEpjJUMfXiIZHt\nwmK2NLjp4+wipPmSidEwyabBAk4Epb0qIG+MM2RvMUOC8kkV2p5lNFkZYR346wtI\nAIqW5jTJYZYfCnGuCyV0F0C0\n-----END PRIVATE KEY-----`;\n\nconst publicKey = `-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxw0xK8CWccCUB6vmereq\nsMbsnIqnyWrggw+iFH+tfZ6CEUGeBmQlKhZMV/Jjj2JxISwKS0HWyJYFqhBBSS2A\nT/1B+aI2jjLJjtvCvhmPzO1B72Z8cHhb+PMYxA+EdoEXBgHEvi4rlAUZHjQCnF7O\nNRxE+dMMApR5FV/DkHjiuUe6uocMAN/vA+KP16T2Kdd60Th61gx4WDRCD+LO5/JJ\nZj/8Ei4WJsxaBgUszsQuDdJh9KeO7001h6GY8r20w845+J+aegzFe4IKbKOftblA\ngLvHOblVImo+TJwQ7Vxvgr0YG5E+0QlTLh3R2cdQ2MyDrT76dCTMMNtNUCZ4WbNe\naQIDAQAB\n-----END PUBLIC KEY-----\n`;\n\nconst options = {\n  referer: 'http://localhost:3000'\n};\ndescribe('signature verifier', () => {\n  it('verifies jwt with public key', async () => {\n    const mockKeyId = 'some-key-id';\n    const mockFetcher = {\n      fetchPublicKeys: jest.fn(() =>\n        Promise.resolve({\n          [mockKeyId]: publicKey\n        })\n      )\n    } as KeyFetcher;\n    const payload = {exp: Date.now() / 1000 + 1};\n    const token = await sign({payload, privateKey, keyId: mockKeyId});\n    const signatureVerifier = new PublicKeySignatureVerifier(mockFetcher);\n    await signatureVerifier.verify(token, options);\n  });\n\n  it('throws token expired error if token is expired', async () => {\n    const mockKeyId = 'some-key-id';\n    const mockFetcher = {\n      fetchPublicKeys: jest.fn(() =>\n        Promise.resolve({\n          [mockKeyId]: publicKey\n        })\n      )\n    } as KeyFetcher;\n    const payload = {exp: Date.now() / 1000 - 1};\n    const token = await sign({payload, privateKey, keyId: mockKeyId});\n    const signatureVerifier = new PublicKeySignatureVerifier(mockFetcher);\n\n    return expect(() =>\n      signatureVerifier.verify(token, options)\n    ).rejects.toBeInstanceOf(errors.JWTExpired);\n  });\n\n  it('throws no matching kid error when non of the public keys corresponds to kid', async () => {\n    const mockKeyId = 'some-key-id';\n    const mockFetcher = {\n      fetchPublicKeys: jest.fn(() =>\n        Promise.resolve({\n          'some-test-key': '',\n          'any-public-key': publicKey\n        })\n      )\n    } as KeyFetcher;\n    const payload = {exp: Date.now() / 1000 + 1};\n    const token = await sign({payload, privateKey, keyId: mockKeyId});\n    const signatureVerifier = new PublicKeySignatureVerifier(mockFetcher);\n\n    return expect(() =>\n      signatureVerifier.verify(token, options)\n    ).rejects.toEqual(new AuthError(AuthErrorCode.NO_MATCHING_KID));\n  });\n\n  it('throws expired error if one of certificates matches, but token is expired', async () => {\n    const mockFetcher = {\n      fetchPublicKeys: jest.fn(() =>\n        Promise.resolve({\n          'some-test-key': '',\n          'any-public-key': publicKey\n        })\n      )\n    } as KeyFetcher;\n    const payload = {exp: Date.now() / 1000 - 1};\n    const token = await sign({payload, privateKey, keyId: ''});\n    const signatureVerifier = new PublicKeySignatureVerifier(mockFetcher);\n\n    return expect(() =>\n      signatureVerifier.verify(token, options)\n    ).rejects.toBeInstanceOf(errors.JWTExpired);\n  });\n\n  it('validates token against all public keys if key id is missing', async () => {\n    const mockFetcher = {\n      fetchPublicKeys: jest.fn(() =>\n        Promise.resolve({\n          'any-public-key': publicKey\n        })\n      )\n    } as KeyFetcher;\n    const payload = {exp: Date.now() / 1000 + 1};\n    const token = await sign({payload, privateKey, keyId: ''});\n    const signatureVerifier = new PublicKeySignatureVerifier(mockFetcher);\n    await signatureVerifier.verify(token, options);\n  });\n\n  it('throws invalid signature error if none of existing keys is valid against token', async () => {\n    const mockFetcher = {\n      fetchPublicKeys: jest.fn(() =>\n        Promise.resolve({\n          'any-kid':\n            '-----BEGIN PUBLIC KEY-----\\n' +\n            'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn3JOtipuElI0FxM9a7Ni\\n' +\n            'IjGBPtZBa8RJofUHJNoGHRS+cN0NU+XUDvwBBozB2jDl6XRg1+fIVX3WiIokFi3O\\n' +\n            'MI0iUc6Ht++lEC2IhSpQ3F7IxeZYlpvTLA+Df5y2SCcK1haa5mxhzCYxbE3Iyu7q\\n' +\n            'Ms4wf7AgNY/zYz9wXlhI6ZomuahkLm4nu1yYnKZOxATsCWBeHx9o+skQbYOQ5fn5\\n' +\n            'e34EVa2fE592Jg4iTXobVSAF1KZIsJerP9P7tkZzrQm6qPz0qV1c7H/1kLN9k3if\\n' +\n            'EXeCUZP7tL38XtlP5iB6F49f7jmc0WgL7wOuUqyrQbkRiOxOaXP2ibAa+TPgPxP3\\n' +\n            '1wIDAQAB\\n' +\n            '-----END PUBLIC KEY-----'\n        })\n      )\n    } as KeyFetcher;\n    const payload = {exp: Date.now() / 1000 - 1};\n    const token = await sign({payload, privateKey, keyId: ''});\n    const signatureVerifier = new PublicKeySignatureVerifier(mockFetcher);\n\n    return expect(() =>\n      signatureVerifier.verify(token, options)\n    ).rejects.toEqual(new AuthError(AuthErrorCode.INVALID_SIGNATURE));\n  });\n});\n"
  },
  {
    "path": "src/auth/signature-verifier.ts",
    "content": "import {\n  JWTPayload,\n  KeyLike,\n  ProtectedHeaderParameters,\n  createRemoteJWKSet,\n  cryptoRuntime,\n  decodeProtectedHeader,\n  errors\n} from 'jose';\nimport {RemoteJWKSetOptions} from 'jose/dist/types/jwks/remote';\nimport {debug} from '../debug/index.js';\nimport {AuthError, AuthErrorCode} from './error.js';\nimport {useEmulator} from './firebase.js';\nimport {getPublicCryptoKey, verify} from './jwt/verify.js';\nimport {VerifyOptions} from './types.js';\nimport {isNonNullObject, isURL} from './validator.js';\n\nexport type PublicKeys = {[key: string]: string};\n\ninterface PublicKeysResponse {\n  keys: PublicKeys;\n  expiresAt: number;\n}\n\nexport type DecodedToken = {\n  header: ProtectedHeaderParameters;\n  payload: JWTPayload;\n};\n\nexport interface SignatureVerifier {\n  verify(token: string, options: VerifyOptions): Promise<void>;\n}\n\nexport interface KeyFetcher {\n  fetchPublicKeys(): Promise<PublicKeys>;\n}\n\nfunction getExpiresAt(res: Response) {\n  if (!res.headers.has('cache-control')) {\n    return 0;\n  }\n\n  const cacheControlHeader: string = res.headers.get('cache-control')!;\n  const parts = cacheControlHeader.split(',');\n  const maxAge = parts.reduce((acc, part) => {\n    const subParts = part.trim().split('=');\n    if (subParts[0] === 'max-age') {\n      return +subParts[1];\n    }\n\n    return acc;\n  }, 0);\n\n  return Date.now() + maxAge * 1000;\n}\n\nconst keyResponseCache: Map<string, PublicKeysResponse> = new Map();\n\nexport class UrlKeyFetcher implements KeyFetcher {\n  constructor(private clientCertUrl: string) {\n    if (!isURL(clientCertUrl)) {\n      throw new Error(\n        'The provided public client certificate URL is not a valid URL.'\n      );\n    }\n  }\n\n  private async fetchPublicKeysResponse(url: URL): Promise<PublicKeysResponse> {\n    const res = await fetch(url);\n    const headers: Record<string, string> = {};\n\n    res.headers.forEach((value, key) => {\n      headers[key] = value;\n    });\n\n    debug('Public keys fetched', {\n      responseHeaders: headers,\n      cryptoRuntime\n    });\n\n    if (!res.ok) {\n      let errorMessage = 'Error fetching public keys for Google certs: ';\n      const data = await res.json();\n      if (data.error) {\n        errorMessage += `${data.error}`;\n        if (data.error_description) {\n          errorMessage += ' (' + data.error_description + ')';\n        }\n      } else {\n        errorMessage += `${await res.text()}`;\n      }\n      throw new Error(errorMessage);\n    }\n\n    const data = await res.json();\n\n    if (data.error) {\n      throw new Error(\n        'Error fetching public keys for Google certs: ' + data.error\n      );\n    }\n\n    const expiresAt = getExpiresAt(res);\n\n    return {\n      keys: data,\n      expiresAt\n    };\n  }\n\n  private async fetchAndCachePublicKeys(url: URL): Promise<PublicKeys> {\n    debug(\n      'No public keys found in cache. Fetching public keys from Google...',\n      {\n        cryptoRuntime\n      }\n    );\n\n    const response = await this.fetchPublicKeysResponse(url);\n\n    keyResponseCache.set(url.toString(), response);\n\n    debug('Public keys cached', {\n      cacheKey: url.toString(),\n      expiresAt: response.expiresAt,\n      cryptoRuntime\n    });\n\n    return response.keys;\n  }\n\n  public async fetchPublicKeys(): Promise<PublicKeys> {\n    const url = new URL(this.clientCertUrl);\n    const cachedResponse = keyResponseCache.get(url.toString());\n\n    if (!cachedResponse) {\n      return this.fetchAndCachePublicKeys(url);\n    }\n\n    const {keys, expiresAt} = cachedResponse;\n    const now = Date.now();\n\n    debug('Get public keys from cache', {\n      expiresAt,\n      now,\n      cryptoRuntime\n    });\n\n    if (expiresAt <= now) {\n      return this.fetchAndCachePublicKeys(url);\n    }\n\n    return keys;\n  }\n}\n\nexport class JWKSSignatureVerifier implements SignatureVerifier {\n  private jwksUrl: URL;\n\n  constructor(\n    jwksUrl: string,\n    private options?: RemoteJWKSetOptions\n  ) {\n    if (!isURL(jwksUrl)) {\n      throw new Error('The provided JWKS URL is not a valid URL.');\n    }\n\n    this.jwksUrl = new URL(jwksUrl);\n  }\n\n  private async getPublicKey(\n    header: ProtectedHeaderParameters\n  ): Promise<KeyLike> {\n    const getKey = createRemoteJWKSet(this.jwksUrl, this.options);\n\n    return getKey(header);\n  }\n\n  public async verify(token: string, options: VerifyOptions): Promise<void> {\n    const header = decodeProtectedHeader(token);\n\n    try {\n      await verify(token, () => this.getPublicKey(header), options);\n    } catch (e) {\n      if (e instanceof errors.JWKSMultipleMatchingKeys) {\n        for await (const publicKey of e) {\n          try {\n            await verify(token, () => Promise.resolve(publicKey), options);\n            return;\n          } catch (innerError) {\n            if (innerError instanceof errors.JWSSignatureVerificationFailed) {\n              continue;\n            }\n            throw innerError;\n          }\n        }\n        throw new errors.JWSSignatureVerificationFailed();\n      }\n\n      throw e;\n    }\n  }\n}\n\nexport class PublicKeySignatureVerifier implements SignatureVerifier {\n  constructor(private keyFetcher: KeyFetcher) {\n    if (!isNonNullObject(keyFetcher)) {\n      throw new Error('The provided key fetcher is not an object or null.');\n    }\n  }\n\n  public static withCertificateUrl(\n    clientCertUrl: string\n  ): PublicKeySignatureVerifier {\n    return new PublicKeySignatureVerifier(new UrlKeyFetcher(clientCertUrl));\n  }\n\n  private async getPublicKey(\n    header: ProtectedHeaderParameters\n  ): Promise<KeyLike> {\n    if (useEmulator()) {\n      return {type: 'none'};\n    }\n\n    return fetchPublicKey(this.keyFetcher, header).then(getPublicCryptoKey);\n  }\n\n  public async verify(token: string, options: VerifyOptions): Promise<void> {\n    const header = decodeProtectedHeader(token);\n\n    try {\n      await verify(token, () => this.getPublicKey(header), options);\n    } catch (e) {\n      if (e instanceof AuthError && e.code === AuthErrorCode.NO_KID_IN_HEADER) {\n        await this.verifyWithoutKid(token, options);\n        return;\n      }\n\n      throw e;\n    }\n  }\n\n  private async verifyWithoutKid(\n    token: string,\n    options: VerifyOptions\n  ): Promise<void> {\n    const publicKeys = await this.keyFetcher.fetchPublicKeys();\n\n    return this.verifyWithAllKeys(token, publicKeys, options);\n  }\n\n  private async verifyWithAllKeys(\n    token: string,\n    keys: {[key: string]: string},\n    options: VerifyOptions\n  ): Promise<void> {\n    const promises: Promise<boolean>[] = [];\n\n    Object.values(keys).forEach((key) => {\n      const promise = verify(\n        token,\n        async () => getPublicCryptoKey(key),\n        options\n      )\n        .then(() => true)\n        .catch((error) => {\n          if (error instanceof errors.JWTExpired) {\n            throw error;\n          }\n          return false;\n        });\n\n      promises.push(promise);\n    });\n\n    return Promise.all(promises).then((result) => {\n      if (result.every((r) => r === false)) {\n        throw new AuthError(AuthErrorCode.INVALID_SIGNATURE);\n      }\n    });\n  }\n}\n\nexport async function fetchPublicKey(\n  fetcher: KeyFetcher,\n  header: ProtectedHeaderParameters\n): Promise<string> {\n  if (!header.kid) {\n    throw new AuthError(AuthErrorCode.NO_KID_IN_HEADER);\n  }\n\n  const kid = header.kid;\n  const publicKeys = await fetcher.fetchPublicKeys();\n\n  if (!Object.prototype.hasOwnProperty.call(publicKeys, kid)) {\n    throw new AuthError(AuthErrorCode.NO_MATCHING_KID);\n  }\n\n  return publicKeys[kid];\n}\n"
  },
  {
    "path": "src/auth/test/create-custom-token.integration.test.ts",
    "content": "import {customTokenToIdAndRefreshTokens, getFirebaseAuth} from '../index.js';\nimport {v4} from 'uuid';\nimport {AppCheckToken} from '../../app-check/types.js';\nimport {getAppCheck} from '../../app-check/index.js';\n\nconst {\n  FIREBASE_API_KEY,\n  FIREBASE_PROJECT_ID,\n  FIREBASE_ADMIN_CLIENT_EMAIL,\n  FIREBASE_ADMIN_PRIVATE_KEY,\n  FIREBASE_APP_ID\n} = process.env;\n\nconst TEST_SERVICE_ACCOUNT = {\n  clientEmail: FIREBASE_ADMIN_CLIENT_EMAIL!,\n  privateKey: FIREBASE_ADMIN_PRIVATE_KEY!.replace(/\\\\n/g, '\\n'),\n  projectId: FIREBASE_PROJECT_ID!\n};\n\nconst REFERER = 'http://localhost:3000';\n\ndescribe('create custom token integration test', () => {\n  let appCheckToken: AppCheckToken = {token: '', ttlMillis: 0};\n\n  beforeAll(async () => {\n    const {createToken} = getAppCheck(TEST_SERVICE_ACCOUNT);\n\n    appCheckToken = await createToken(FIREBASE_APP_ID!);\n  });\n\n  const {createCustomToken, getCustomIdAndRefreshTokens, verifyIdToken} =\n    getFirebaseAuth(TEST_SERVICE_ACCOUNT, FIREBASE_API_KEY!);\n\n  it('should propagate custom claims when exchanging id tokens', async () => {\n    const userId = v4();\n\n    const customToken = await createCustomToken(userId, {\n      paswordless_sign_in: true\n    });\n\n    const {idToken} = await customTokenToIdAndRefreshTokens(\n      customToken,\n      FIREBASE_API_KEY!,\n      {appCheckToken: appCheckToken.token, referer: REFERER}\n    );\n\n    const customIdAndRefreshTokens = await getCustomIdAndRefreshTokens(\n      idToken,\n      {\n        appCheckToken: appCheckToken.token\n      }\n    );\n\n    const decodedCustomIdToken = await verifyIdToken(\n      customIdAndRefreshTokens.idToken,\n      {referer: REFERER}\n    );\n\n    expect(decodedCustomIdToken.paswordless_sign_in).toBe(true);\n  });\n});\n"
  },
  {
    "path": "src/auth/test/no-matching-kid.integration.test.ts",
    "content": "import {v4} from 'uuid';\nimport {CLIENT_CERT_URL} from '../firebase.js';\nimport {customTokenToIdAndRefreshTokens, getFirebaseAuth} from '../index.js';\nimport {InvalidTokenError, InvalidTokenReason} from '../error.js';\n\nconst {\n  FIREBASE_API_KEY,\n  FIREBASE_PROJECT_ID,\n  FIREBASE_ADMIN_CLIENT_EMAIL,\n  FIREBASE_ADMIN_PRIVATE_KEY\n} = process.env;\n\nconst TEST_SERVICE_ACCOUNT = {\n  clientEmail: FIREBASE_ADMIN_CLIENT_EMAIL!,\n  privateKey: FIREBASE_ADMIN_PRIVATE_KEY!.replace(/\\\\n/g, '\\n'),\n  projectId: FIREBASE_PROJECT_ID!\n};\n\nconst REFERER = 'http://localhost:3000';\n\ndescribe('no matching kid integration test', () => {\n  const {createCustomToken, verifyAndRefreshExpiredIdToken} = getFirebaseAuth(\n    TEST_SERVICE_ACCOUNT,\n    FIREBASE_API_KEY!\n  );\n\n  beforeEach(() => {\n    let numberOfCalls = 0;\n\n    const actualFetch = global.fetch;\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    global.fetch = jest.fn((url: URL, ...args: any[]) => {\n      if (url?.href === CLIENT_CERT_URL && !numberOfCalls) {\n        numberOfCalls++;\n        return {\n          ok: true,\n          headers: {\n            forEach: () => {},\n            has: () => false\n          },\n          json: () => Promise.resolve({})\n        };\n      }\n\n      return actualFetch(url, ...args);\n    }) as jest.Mock;\n  });\n\n  it('should throw invalid token error if kid header does not match public keys', async () => {\n    const userId = v4();\n    const customToken = await createCustomToken(userId, {\n      customClaim: 'customClaimValue'\n    });\n\n    const {idToken, refreshToken} = await customTokenToIdAndRefreshTokens(\n      customToken,\n      FIREBASE_API_KEY!,\n      {referer: REFERER}\n    );\n\n    return expect(() =>\n      verifyAndRefreshExpiredIdToken(\n        {idToken, refreshToken, customToken},\n        {\n          referer: REFERER\n        }\n      )\n    ).rejects.toEqual(new InvalidTokenError(InvalidTokenReason.INVALID_KID));\n  });\n\n  it('should refresh the token if kid header does not match public keys and experimental flag is provided', async () => {\n    const userId = v4();\n    const customToken = await createCustomToken(userId, {\n      customClaim: 'customClaimValue'\n    });\n\n    const {idToken, refreshToken} = await customTokenToIdAndRefreshTokens(\n      customToken,\n      FIREBASE_API_KEY!,\n      {referer: REFERER}\n    );\n\n    const onTokenRefresh = jest.fn();\n\n    const result = await verifyAndRefreshExpiredIdToken(\n      {idToken, refreshToken, customToken},\n      {\n        referer: REFERER,\n        enableTokenRefreshOnExpiredKidHeader: true,\n        onTokenRefresh\n      }\n    );\n\n    expect(onTokenRefresh).toHaveBeenCalledWith(result);\n  });\n});\n"
  },
  {
    "path": "src/auth/test/session-cookie.test.ts",
    "content": "import {v4} from 'uuid';\nimport {customTokenToIdAndRefreshTokens, getFirebaseAuth} from '../index.js';\n\nconst {\n  FIREBASE_API_KEY,\n  FIREBASE_PROJECT_ID,\n  FIREBASE_ADMIN_CLIENT_EMAIL,\n  FIREBASE_ADMIN_PRIVATE_KEY,\n  FIREBASE_AUTH_TENANT_ID\n} = process.env;\n\nconst TEST_SERVICE_ACCOUNT = {\n  clientEmail: FIREBASE_ADMIN_CLIENT_EMAIL!,\n  privateKey: FIREBASE_ADMIN_PRIVATE_KEY!.replace(/\\\\n/g, '\\n'),\n  projectId: FIREBASE_PROJECT_ID!\n};\n\nconst REFERER = 'http://localhost:3000';\n\ndescribe('session cookie integration test', () => {\n  const scenarios = [\n    {\n      desc: 'single-tenant',\n      tenantID: undefined\n    },\n    {\n      desc: 'multi-tenant',\n      tenantId: FIREBASE_AUTH_TENANT_ID\n    }\n  ];\n  for (const {desc, tenantId} of scenarios) {\n    describe(desc, () => {\n      const {createCustomToken, createSessionCookie} = getFirebaseAuth(\n        TEST_SERVICE_ACCOUNT,\n        FIREBASE_API_KEY!,\n        tenantId\n      );\n\n      it('should create session cookie', async () => {\n        const userId = v4();\n        const customToken = await createCustomToken(userId, {\n          customClaim: 'customClaimValue'\n        });\n\n        const {idToken} = await customTokenToIdAndRefreshTokens(\n          customToken,\n          FIREBASE_API_KEY!,\n          {tenantId, referer: REFERER}\n        );\n        const cookie = await createSessionCookie(idToken, 60 * 60 * 1000);\n\n        console.log({cookie});\n      });\n    });\n  }\n});\n"
  },
  {
    "path": "src/auth/test/set-custom-user-claims.integration.test.ts",
    "content": "import {customTokenToIdAndRefreshTokens, getFirebaseAuth} from '../index.js';\nimport {v4} from 'uuid';\nimport {AppCheckToken} from '../../app-check/types.js';\nimport {getAppCheck} from '../../app-check.js';\n\nconst {\n  FIREBASE_API_KEY,\n  FIREBASE_PROJECT_ID,\n  FIREBASE_ADMIN_CLIENT_EMAIL,\n  FIREBASE_ADMIN_PRIVATE_KEY,\n  FIREBASE_APP_ID\n} = process.env;\n\nconst TEST_SERVICE_ACCOUNT = {\n  clientEmail: FIREBASE_ADMIN_CLIENT_EMAIL!,\n  privateKey: FIREBASE_ADMIN_PRIVATE_KEY!.replace(/\\\\n/g, '\\n'),\n  projectId: FIREBASE_PROJECT_ID!\n};\n\nconst REFERER = 'http://localhost:3000';\n\ndescribe('set custom user claims integration test', () => {\n  let appCheckToken: AppCheckToken = {token: '', ttlMillis: 0};\n\n  beforeAll(async () => {\n    const {createToken} = getAppCheck(TEST_SERVICE_ACCOUNT);\n\n    appCheckToken = await createToken(FIREBASE_APP_ID!);\n  });\n\n  const {createCustomToken, getUser, setCustomUserClaims, verifyIdToken} =\n    getFirebaseAuth(TEST_SERVICE_ACCOUNT, FIREBASE_API_KEY!);\n\n  it('should create custom user claims', async () => {\n    const userId = v4();\n\n    const customToken = await createCustomToken(userId, {\n      customClaim: 'customClaimValue'\n    });\n\n    const {idToken} = await customTokenToIdAndRefreshTokens(\n      customToken,\n      FIREBASE_API_KEY!,\n      {appCheckToken: appCheckToken.token, referer: REFERER}\n    );\n\n    await verifyIdToken(idToken, {referer: REFERER});\n\n    await setCustomUserClaims(userId, {\n      newCustomClaim: 'newCustomClaimValue'\n    });\n\n    const user = await getUser(userId);\n    expect(user?.uid).toEqual(userId);\n    expect(user?.customClaims).toEqual({\n      newCustomClaim: 'newCustomClaimValue'\n    });\n  });\n});\n"
  },
  {
    "path": "src/auth/test/user.integration.test.ts",
    "content": "import {getFirebaseAuth} from '../index.js';\n\nconst {\n  FIREBASE_API_KEY,\n  FIREBASE_PROJECT_ID,\n  FIREBASE_ADMIN_CLIENT_EMAIL,\n  FIREBASE_ADMIN_PRIVATE_KEY,\n  FIREBASE_AUTH_TENANT_ID\n} = process.env;\n\nconst TEST_USER_ID = '39d14e52-6e22-4afd-a844-c8aa2e685224';\n\njest.setTimeout(30000);\n\ndescribe('user integration test', () => {\n  const scenarios = [\n    {\n      desc: 'single-tenant'\n    },\n    {\n      desc: 'multi-tenant',\n      tenantId: FIREBASE_AUTH_TENANT_ID\n    }\n  ];\n  for (const {desc, tenantId} of scenarios) {\n    describe(desc, () => {\n      const {createUser, getUser, deleteUser, updateUser, listUsers} =\n        getFirebaseAuth(\n          {\n            clientEmail: FIREBASE_ADMIN_CLIENT_EMAIL!,\n            privateKey: FIREBASE_ADMIN_PRIVATE_KEY!.replace(/\\\\n/g, '\\n'),\n            projectId: FIREBASE_PROJECT_ID!\n          },\n          FIREBASE_API_KEY!,\n          tenantId\n        );\n\n      beforeEach(async () => {\n        try {\n          await deleteUser(TEST_USER_ID);\n          // eslint-disable-next-line no-empty\n        } catch {}\n      });\n\n      it('should create user', async () => {\n        await createUser({\n          uid: TEST_USER_ID,\n          displayName: 'John Doe',\n          email: 'user-integration-test@next-firebase-auth-edge.github'\n        });\n\n        expect(await getUser(TEST_USER_ID)).toEqual(\n          expect.objectContaining({\n            displayName: 'John Doe',\n            email: 'user-integration-test@next-firebase-auth-edge.github',\n            uid: TEST_USER_ID,\n            tenantId\n          })\n        );\n\n        /**\n         * Firebase returns list of all users in not defined order\n         *\n         * @TODO:\n         * This test needs to be improved. Currently, Github Actions is using next-firebase-auth-edge-starter Firebase credentials for running tests.\n         *\n         * 1. Create separate Firebase credentials for local and test environment\n         * 2. Cleanup users from Firebase after each integration test\n         * 3. Add new test that covers listing of users with and without tenantId\n         */\n        const listUserResponse = await listUsers();\n\n        expect(listUserResponse.users.length).toBeGreaterThan(0);\n        expect(listUserResponse.users[0]).toEqual(\n          expect.objectContaining({\n            uid: expect.any(String)\n          })\n        );\n      });\n\n      it('should update user', async () => {\n        await createUser({\n          uid: TEST_USER_ID,\n          displayName: 'John Doe',\n          email: 'john-doe@next-firebase-auth-edge.github'\n        });\n\n        await updateUser(TEST_USER_ID, {\n          displayName: 'John Smith',\n          email: 'john-smith@next-firebase-auth-edge.github'\n        });\n\n        expect(await getUser(TEST_USER_ID)).toEqual(\n          expect.objectContaining({\n            displayName: 'John Smith',\n            email: 'john-smith@next-firebase-auth-edge.github',\n            uid: TEST_USER_ID,\n            tenantId\n          })\n        );\n      });\n    });\n  }\n\n  it('should get user by email', async () => {\n    const {createUser, getUser, getUserByEmail, deleteUser} = getFirebaseAuth(\n      {\n        clientEmail: FIREBASE_ADMIN_CLIENT_EMAIL!,\n        privateKey: FIREBASE_ADMIN_PRIVATE_KEY!.replace(/\\\\n/g, '\\n'),\n        projectId: FIREBASE_PROJECT_ID!\n      },\n      FIREBASE_API_KEY!\n    );\n\n    try {\n      await deleteUser(TEST_USER_ID);\n      // eslint-disable-next-line no-empty\n    } catch {}\n\n    await createUser({\n      uid: TEST_USER_ID,\n      displayName: 'John Doe',\n      email: 'john-doe@ensite.in',\n      emailVerified: true\n    });\n\n    const user = await getUserByEmail('john-doe@ensite.in');\n\n    expect(await getUser(TEST_USER_ID)).toEqual(user);\n  });\n});\n"
  },
  {
    "path": "src/auth/test/verify-token.integration.test.ts",
    "content": "import {\n  customTokenToIdAndRefreshTokens,\n  getFirebaseAuth,\n  isUserNotFoundError\n} from '../index';\nimport {v4} from 'uuid';\nimport {AuthError, AuthErrorCode} from '../error.js';\nimport {getAppCheck} from '../../app-check.js';\nimport {AppCheckToken} from '../../app-check/types.js';\n\nconst {\n  FIREBASE_API_KEY,\n  FIREBASE_PROJECT_ID,\n  FIREBASE_ADMIN_CLIENT_EMAIL,\n  FIREBASE_ADMIN_PRIVATE_KEY,\n  FIREBASE_AUTH_TENANT_ID,\n  FIREBASE_APP_ID\n} = process.env;\n\nconst TEST_SERVICE_ACCOUNT = {\n  clientEmail: FIREBASE_ADMIN_CLIENT_EMAIL!,\n  privateKey: FIREBASE_ADMIN_PRIVATE_KEY!.replace(/\\\\n/g, '\\n'),\n  projectId: FIREBASE_PROJECT_ID!\n};\n\nconst REFERER = 'http://localhost:3000';\n\ndescribe('verify token integration test', () => {\n  const scenarios = [\n    {\n      desc: 'single-tenant',\n      tenantID: undefined\n    },\n    {\n      desc: 'multi-tenant',\n      tenantId: FIREBASE_AUTH_TENANT_ID\n    }\n  ];\n  for (const {desc, tenantId} of scenarios) {\n    let appCheckToken: AppCheckToken = {token: '', ttlMillis: 0};\n\n    beforeAll(async () => {\n      const {createToken} = getAppCheck(TEST_SERVICE_ACCOUNT, tenantId);\n\n      appCheckToken = await createToken(FIREBASE_APP_ID!);\n    });\n\n    describe(desc, () => {\n      const {\n        handleTokenRefresh,\n        createCustomToken,\n        verifyAndRefreshExpiredIdToken,\n        verifyIdToken,\n        deleteUser\n      } = getFirebaseAuth(TEST_SERVICE_ACCOUNT, FIREBASE_API_KEY!, tenantId);\n\n      it('should create and verify custom token', async () => {\n        const userId = v4();\n        const customToken = await createCustomToken(userId, {\n          customClaim: 'customClaimValue'\n        });\n\n        const {idToken} = await customTokenToIdAndRefreshTokens(\n          customToken,\n          FIREBASE_API_KEY!,\n          {tenantId, appCheckToken: appCheckToken.token, referer: REFERER}\n        );\n        const tenant = await verifyIdToken(idToken, {referer: REFERER});\n\n        expect(tenant.uid).toEqual(userId);\n        expect(tenant.customClaim).toEqual('customClaimValue');\n        expect(tenant.firebase.tenant).toEqual(tenantId);\n      });\n\n      it('should throw AuthError if token is expired', async () => {\n        const userId = v4();\n        const customToken = await createCustomToken(userId, {\n          customClaim: 'customClaimValue'\n        });\n\n        const {idToken} = await customTokenToIdAndRefreshTokens(\n          customToken,\n          FIREBASE_API_KEY!,\n          {tenantId, appCheckToken: appCheckToken.token, referer: REFERER}\n        );\n\n        return expect(() =>\n          verifyIdToken(idToken, {\n            currentDate: new Date(Date.now() + 7200 * 1000),\n            referer: REFERER\n          })\n        ).rejects.toHaveProperty('code', AuthErrorCode.TOKEN_EXPIRED);\n      });\n\n      it('should refresh token if expired', async () => {\n        const userId = v4();\n        const customToken = await createCustomToken(userId, {\n          customClaim: 'customClaimValue'\n        });\n\n        const {idToken, refreshToken} = await customTokenToIdAndRefreshTokens(\n          customToken,\n          FIREBASE_API_KEY!,\n          {tenantId, appCheckToken: appCheckToken.token, referer: REFERER}\n        );\n\n        const onTokenRefresh = jest.fn();\n\n        const result = await verifyAndRefreshExpiredIdToken(\n          {idToken, refreshToken, customToken},\n          {\n            currentDate: new Date(Date.now() + 7200 * 1000),\n            referer: REFERER,\n            onTokenRefresh\n          }\n        );\n\n        expect(result?.decodedIdToken?.customClaim).toEqual('customClaimValue');\n        expect(onTokenRefresh).toHaveBeenCalledWith(result);\n      });\n\n      it('should verify token', async () => {\n        const userId = v4();\n        const customToken = await createCustomToken(userId, {\n          customClaim: 'customClaimValue'\n        });\n\n        const {idToken, refreshToken} = await customTokenToIdAndRefreshTokens(\n          customToken,\n          FIREBASE_API_KEY!,\n          {tenantId, appCheckToken: appCheckToken.token, referer: REFERER}\n        );\n        const tokens = await verifyAndRefreshExpiredIdToken(\n          {idToken, refreshToken, customToken},\n          {referer: REFERER}\n        );\n\n        expect(tokens?.decodedIdToken.uid).toEqual(userId);\n        expect(tokens?.decodedIdToken.customClaim).toEqual('customClaimValue');\n        expect(tokens?.decodedIdToken.firebase.tenant).toEqual(tenantId);\n      });\n\n      it('should checked revoked token', async () => {\n        const userId = v4();\n        const customToken = await createCustomToken(userId, {\n          customClaim: 'customClaimValue'\n        });\n\n        const {idToken} = await customTokenToIdAndRefreshTokens(\n          customToken,\n          FIREBASE_API_KEY!,\n          {tenantId, appCheckToken: appCheckToken.token, referer: REFERER}\n        );\n        const tenant = await verifyIdToken(idToken, {\n          checkRevoked: true,\n          referer: REFERER\n        });\n\n        expect(tenant.uid).toEqual(userId);\n        expect(tenant.customClaim).toEqual('customClaimValue');\n        expect(tenant.firebase.tenant).toEqual(tenantId);\n      });\n\n      it('should refresh token', async () => {\n        const userId = v4();\n        const customToken = await createCustomToken(userId, {\n          customClaim: 'customClaimValue'\n        });\n\n        const {idToken, refreshToken} = await customTokenToIdAndRefreshTokens(\n          customToken,\n          FIREBASE_API_KEY!,\n          {tenantId, appCheckToken: appCheckToken.token, referer: REFERER}\n        );\n        const {decodedIdToken} = await handleTokenRefresh(refreshToken, {\n          referer: REFERER\n        });\n\n        expect(decodedIdToken.uid).toEqual(userId);\n        expect(decodedIdToken.customClaim).toEqual('customClaimValue');\n        expect(decodedIdToken.token).not.toEqual(idToken);\n      });\n\n      it('should throw firebase auth error when user is not found during token refresh', async () => {\n        const userId = v4();\n        const customToken = await createCustomToken(userId, {\n          customClaim: 'customClaimValue'\n        });\n\n        const {refreshToken} = await customTokenToIdAndRefreshTokens(\n          customToken,\n          FIREBASE_API_KEY!,\n          {tenantId, appCheckToken: appCheckToken.token, referer: REFERER}\n        );\n\n        await deleteUser(userId);\n\n        return expect(() =>\n          handleTokenRefresh(refreshToken, {referer: REFERER})\n        ).rejects.toEqual(new AuthError(AuthErrorCode.USER_NOT_FOUND));\n      });\n\n      it('should be able to catch \"user not found\" error and return null', async () => {\n        const userId = v4();\n        const customToken = await createCustomToken(userId, {\n          customClaim: 'customClaimValue'\n        });\n\n        async function customGetToken() {\n          try {\n            return await handleTokenRefresh(refreshToken, {referer: REFERER});\n          } catch (e: unknown) {\n            if (isUserNotFoundError(e)) {\n              return null;\n            }\n\n            throw e;\n          }\n        }\n\n        const {refreshToken} = await customTokenToIdAndRefreshTokens(\n          customToken,\n          FIREBASE_API_KEY!,\n          {tenantId, appCheckToken: appCheckToken.token, referer: REFERER}\n        );\n\n        await deleteUser(userId);\n\n        expect(await customGetToken()).toEqual(null);\n      });\n    });\n  }\n});\n"
  },
  {
    "path": "src/auth/token-generator.ts",
    "content": "import {JWTPayload} from 'jose';\nimport {Credential, ServiceAccountCredential} from './credential.js';\nimport {AuthError, AuthErrorCode} from './error.js';\nimport {\n  CryptoSigner,\n  EmulatorSigner,\n  IAMSigner,\n  ServiceAccountSigner\n} from './jwt/crypto-signer';\nimport {isNonNullObject} from './validator.js';\nimport {useEmulator} from './firebase.js';\n\nconst ONE_HOUR_IN_SECONDS = 60 * 60;\n\nexport const BLACKLISTED_CLAIMS = [\n  'acr',\n  'amr',\n  'at_hash',\n  'aud',\n  'auth_time',\n  'azp',\n  'cnf',\n  'c_hash',\n  'exp',\n  'iat',\n  'iss',\n  'jti',\n  'nbf',\n  'nonce'\n];\n\nconst FIREBASE_AUDIENCE =\n  'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit';\n\nexport class FirebaseTokenGenerator {\n  private readonly signer: CryptoSigner;\n\n  constructor(signer: CryptoSigner) {\n    this.signer = signer;\n  }\n\n  public createCustomToken(\n    uid: string,\n    developerClaims?: {[key: string]: unknown}\n  ): Promise<string> {\n    let errorMessage: string | undefined;\n    if (uid.length > 128) {\n      errorMessage =\n        '`uid` argument must a uid with less than or equal to 128 characters.';\n    } else if (\n      !FirebaseTokenGenerator.isDeveloperClaimsValid_(developerClaims)\n    ) {\n      errorMessage =\n        '`developerClaims` argument must be a valid, non-null object containing the developer claims.';\n    }\n\n    if (errorMessage) {\n      throw new AuthError(AuthErrorCode.INVALID_ARGUMENT, errorMessage);\n    }\n\n    const claims: {[key: string]: unknown} = {};\n    if (typeof developerClaims !== 'undefined') {\n      for (const key in developerClaims) {\n        if (Object.prototype.hasOwnProperty.call(developerClaims, key)) {\n          if (BLACKLISTED_CLAIMS.indexOf(key) !== -1) {\n            throw new AuthError(\n              AuthErrorCode.INVALID_ARGUMENT,\n              `Developer claim \"${key}\" is reserved and cannot be specified.`\n            );\n          }\n          claims[key] = developerClaims[key];\n        }\n      }\n    }\n    return this.signer.getAccountId().then(async (account) => {\n      const iat = Math.floor(Date.now() / 1000);\n      const body: JWTPayload = {\n        aud: FIREBASE_AUDIENCE,\n        iat,\n        exp: iat + ONE_HOUR_IN_SECONDS,\n        iss: account,\n        sub: account,\n        uid\n      };\n\n      if (Object.keys(claims).length > 0) {\n        body.claims = claims;\n      }\n\n      return this.signer.sign(body);\n    });\n  }\n\n  private static isDeveloperClaimsValid_(developerClaims?: object): boolean {\n    if (typeof developerClaims === 'undefined') {\n      return true;\n    }\n    return isNonNullObject(developerClaims);\n  }\n}\n\nexport function cryptoSignerFromCredential(\n  credential: Credential,\n  tenantId?: string,\n  serviceAccountId?: string\n): CryptoSigner {\n  if (credential instanceof ServiceAccountCredential) {\n    return new ServiceAccountSigner(credential, tenantId);\n  }\n\n  return new IAMSigner(credential, tenantId, serviceAccountId);\n}\n\nexport function createFirebaseTokenGenerator(\n  credential: Credential,\n  tenantId?: string,\n  serviceAccountId?: string\n): FirebaseTokenGenerator {\n  if (useEmulator()) {\n    return new FirebaseTokenGenerator(new EmulatorSigner(tenantId));\n  }\n\n  const signer = cryptoSignerFromCredential(\n    credential,\n    tenantId,\n    serviceAccountId\n  );\n\n  return new FirebaseTokenGenerator(signer);\n}\n"
  },
  {
    "path": "src/auth/token-verifier.ts",
    "content": "import {decodeJwt, decodeProtectedHeader, errors} from 'jose';\nimport {JOSEError} from 'jose/dist/types/util/errors';\nimport {AuthError, AuthErrorCode} from './error.js';\nimport {CLIENT_CERT_URL, FIREBASE_AUDIENCE, useEmulator} from './firebase.js';\nimport {ALGORITHM_RS256} from './jwt/verify.js';\nimport {\n  DecodedToken,\n  PublicKeySignatureVerifier,\n  SignatureVerifier\n} from './signature-verifier';\nimport {DecodedIdToken, VerifyOptions} from './types.js';\nimport {mapJwtPayloadToDecodedIdToken} from './utils.js';\nimport {isURL} from './validator.js';\n\nexport class FirebaseTokenVerifier {\n  private readonly signatureVerifier: SignatureVerifier;\n\n  constructor(\n    clientCertUrl: string,\n    private issuer: string,\n    private projectId: string,\n    private tenantId?: string\n  ) {\n    if (!isURL(clientCertUrl)) {\n      throw new AuthError(\n        AuthErrorCode.INVALID_ARGUMENT,\n        'The provided public client certificate URL is an invalid URL.'\n      );\n    }\n\n    this.signatureVerifier =\n      PublicKeySignatureVerifier.withCertificateUrl(clientCertUrl);\n  }\n\n  public async verifyJWT(\n    jwtToken: string,\n    options: VerifyOptions\n  ): Promise<DecodedIdToken> {\n    const decoded = await this.decodeAndVerify(\n      jwtToken,\n      this.projectId,\n      options\n    );\n\n    const decodedIdToken = mapJwtPayloadToDecodedIdToken(decoded.payload);\n\n    if (this.tenantId && decodedIdToken.firebase.tenant !== this.tenantId) {\n      throw new AuthError(AuthErrorCode.MISMATCHING_TENANT_ID);\n    }\n\n    return decodedIdToken;\n  }\n\n  private async decodeAndVerify(\n    token: string,\n    projectId: string,\n    options: VerifyOptions\n  ): Promise<DecodedToken> {\n    const header = decodeProtectedHeader(token);\n    const payload = decodeJwt(token);\n\n    this.verifyContent({header, payload}, projectId);\n    await this.verifySignature(token, options);\n\n    return {header, payload};\n  }\n\n  private verifyContent(\n    fullDecodedToken: DecodedToken,\n    projectId: string | null\n  ): void {\n    const header = fullDecodedToken && fullDecodedToken.header;\n    const payload = fullDecodedToken && fullDecodedToken.payload;\n\n    let errorMessage: string | undefined;\n    if (!useEmulator() && typeof header.kid === 'undefined') {\n      const isCustomToken = payload.aud === FIREBASE_AUDIENCE;\n      if (isCustomToken) {\n        errorMessage = `idToken was expected, but custom token was provided`;\n      } else {\n        errorMessage = `idToken has no \"kid\" claim.`;\n      }\n    } else if (!useEmulator() && header.alg !== ALGORITHM_RS256) {\n      errorMessage = `Incorrect algorithm. ${ALGORITHM_RS256} expected, ${header.alg} provided`;\n    } else if (payload.iss !== this.issuer + projectId) {\n      errorMessage = `idToken has incorrect \"iss\" (issuer) claim. Expected ${this.issuer}${projectId}, but got ${payload.iss}`;\n    } else if (payload.sub === '') {\n      errorMessage = `idToken has an empty string \"sub\" (subject) claim.`;\n    } else if (typeof payload.sub === 'string' && payload.sub.length > 128) {\n      errorMessage = `idToken has \"sub\" (subject) claim longer than 128 characters.`;\n    }\n\n    if (errorMessage) {\n      throw new AuthError(AuthErrorCode.INVALID_ARGUMENT, errorMessage);\n    }\n  }\n\n  private verifySignature(\n    jwtToken: string,\n    options: VerifyOptions\n  ): Promise<void> {\n    return this.signatureVerifier.verify(jwtToken, options).catch((error) => {\n      throw this.mapJoseErrorToAuthError(error);\n    });\n  }\n\n  private mapJoseErrorToAuthError(error: JOSEError): Error {\n    if (error instanceof errors.JWTExpired) {\n      return AuthError.fromError(\n        error,\n        AuthErrorCode.TOKEN_EXPIRED,\n        error.message\n      );\n    }\n\n    if (error instanceof errors.JWSSignatureVerificationFailed) {\n      return AuthError.fromError(error, AuthErrorCode.INVALID_SIGNATURE);\n    }\n\n    if (error instanceof AuthError) {\n      return error;\n    }\n\n    return AuthError.fromError(\n      error,\n      AuthErrorCode.INTERNAL_ERROR,\n      error.message\n    );\n  }\n}\n\nexport function createIdTokenVerifier(\n  projectId: string,\n  tenantId?: string\n): FirebaseTokenVerifier {\n  return new FirebaseTokenVerifier(\n    CLIENT_CERT_URL,\n    'https://securetoken.google.com/',\n    projectId,\n    tenantId\n  );\n}\n"
  },
  {
    "path": "src/auth/types.ts",
    "content": "export interface FirebaseClaims {\n  identities: {\n    [key: string]: unknown;\n  };\n  sign_in_provider: string;\n  sign_in_second_factor?: string;\n  second_factor_identifier?: string;\n  tenant?: string;\n  [key: string]: unknown;\n}\n\nexport interface DecodedIdToken {\n  aud: string;\n  auth_time: number;\n  email?: string;\n  email_verified?: boolean;\n  name?: string;\n  exp: number;\n  firebase: FirebaseClaims;\n  source_sign_in_provider: string;\n  iat: number;\n  iss: string;\n  phone_number?: string;\n  picture?: string;\n  sub: string;\n  uid: string;\n  [key: string]: unknown;\n}\n\nexport interface VerifyOptions {\n  currentDate?: Date;\n  checkRevoked?: boolean;\n  referer?: string;\n  enableTokenRefreshOnExpiredKidHeader?: boolean;\n}\n\nexport interface Tokens<Metadata extends object = object> {\n  decodedToken: DecodedIdToken;\n  token: string;\n  // Set `enableCustomToken` to true in `authMiddleware` to enable custom token\n  customToken?: string;\n  // Pass `getMetadata` to `authMiddleware` to save token metadata\n  metadata: Metadata;\n}\n\nexport interface TokenSet {\n  idToken: string;\n  refreshToken: string;\n  decodedIdToken: DecodedIdToken;\n  customToken?: string;\n}\n"
  },
  {
    "path": "src/auth/user-record.ts",
    "content": "import {addReadonlyGetter, deepCopy} from './utils.js';\nimport {isNonNullObject} from './validator.js';\nimport {base64url} from 'jose';\nimport {AuthError, AuthErrorCode} from './error.js';\n\nconst B64_REDACTED = base64url.encode('REDACTED');\n\nfunction parseDate(time: unknown): string | null {\n  try {\n    const date = new Date(parseInt(time as string, 10));\n    if (!isNaN(date.getTime())) {\n      return date.toUTCString();\n    }\n  } catch {\n    return null;\n  }\n\n  return null;\n}\n\nexport interface MultiFactorInfoResponse {\n  mfaEnrollmentId: string;\n  displayName?: string;\n  phoneInfo?: string;\n  enrolledAt?: string;\n  [key: string]: unknown;\n}\n\nexport interface ProviderUserInfoResponse {\n  rawId: string;\n  displayName?: string;\n  email?: string;\n  photoUrl?: string;\n  phoneNumber?: string;\n  providerId: string;\n  federatedId?: string;\n}\n\nexport interface GetAccountInfoUserResponse {\n  localId: string;\n  email?: string;\n  emailVerified?: boolean;\n  phoneNumber?: string;\n  displayName?: string;\n  photoUrl?: string;\n  disabled?: boolean;\n  passwordHash?: string;\n  salt?: string;\n  customAttributes?: string;\n  validSince?: string;\n  tenantId?: string;\n  providerUserInfo?: ProviderUserInfoResponse[];\n  mfaInfo?: MultiFactorInfoResponse[];\n  createdAt?: string;\n  lastLoginAt?: string;\n  lastRefreshAt?: string;\n  [key: string]: unknown;\n}\n\nenum MultiFactorId {\n  Phone = 'phone'\n}\n\nexport interface MultiFactorInfoType {\n  uid?: string;\n  displayName?: string;\n  factorId?: string;\n  enrollmentTime?: string;\n}\n\nexport abstract class MultiFactorInfo {\n  public readonly uid!: string;\n  public readonly displayName?: string;\n  public readonly factorId!: string;\n  public readonly enrollmentTime?: string;\n  public static initMultiFactorInfo(\n    response: MultiFactorInfoResponse\n  ): MultiFactorInfo | null {\n    let multiFactorInfo: MultiFactorInfo | null = null;\n    try {\n      multiFactorInfo = new PhoneMultiFactorInfo(response);\n    } catch {\n      return null;\n    }\n\n    return multiFactorInfo;\n  }\n\n  constructor(response: MultiFactorInfoResponse) {\n    this.initFromServerResponse(response);\n  }\n\n  public toJSON(): MultiFactorInfoType {\n    return {\n      uid: this.uid,\n      displayName: this.displayName,\n      factorId: this.factorId,\n      enrollmentTime: this.enrollmentTime\n    };\n  }\n\n  protected abstract getFactorId(\n    response: MultiFactorInfoResponse\n  ): string | null;\n\n  private initFromServerResponse(response: MultiFactorInfoResponse): void {\n    const factorId = response && this.getFactorId(response);\n    if (!factorId || !response || !response.mfaEnrollmentId) {\n      throw new AuthError(\n        AuthErrorCode.INTERNAL_ERROR,\n        'INTERNAL ASSERT FAILED: Invalid multi-factor info response'\n      );\n    }\n    addReadonlyGetter(this, 'uid', response.mfaEnrollmentId);\n    addReadonlyGetter(this, 'factorId', factorId);\n    addReadonlyGetter(this, 'displayName', response.displayName);\n    if (response.enrolledAt) {\n      addReadonlyGetter(\n        this,\n        'enrollmentTime',\n        new Date(response.enrolledAt).toUTCString()\n      );\n    } else {\n      addReadonlyGetter(this, 'enrollmentTime', null);\n    }\n  }\n}\n\nexport class PhoneMultiFactorInfo extends MultiFactorInfo {\n  public readonly phoneNumber!: string;\n\n  constructor(response: MultiFactorInfoResponse) {\n    super(response);\n    addReadonlyGetter(this, 'phoneNumber', response.phoneInfo);\n  }\n\n  public toJSON(): object {\n    return Object.assign(super.toJSON(), {\n      phoneNumber: this.phoneNumber\n    });\n  }\n\n  protected getFactorId(response: MultiFactorInfoResponse): string | null {\n    return response && response.phoneInfo ? MultiFactorId.Phone : null;\n  }\n}\n\nexport class MultiFactorSettings {\n  public enrolledFactors!: MultiFactorInfo[];\n\n  constructor(response: GetAccountInfoUserResponse) {\n    const parsedEnrolledFactors: MultiFactorInfo[] = [];\n    if (!isNonNullObject(response)) {\n      throw new AuthError(\n        AuthErrorCode.INTERNAL_ERROR,\n        'INTERNAL ASSERT FAILED: Invalid multi-factor response'\n      );\n    } else if (response.mfaInfo) {\n      response.mfaInfo.forEach((factorResponse) => {\n        const multiFactorInfo =\n          MultiFactorInfo.initMultiFactorInfo(factorResponse);\n        if (multiFactorInfo) {\n          parsedEnrolledFactors.push(multiFactorInfo);\n        }\n      });\n    }\n\n    addReadonlyGetter(\n      this,\n      'enrolledFactors',\n      Object.freeze(parsedEnrolledFactors)\n    );\n  }\n\n  public toJSON(): object {\n    return {\n      enrolledFactors: this.enrolledFactors.map((info) => info.toJSON())\n    };\n  }\n}\n\nexport interface UserMetadataType {\n  lastSignInTime?: string;\n  creationTime?: string;\n  lastRefreshTime?: string | null;\n}\n\nexport class UserMetadata {\n  public readonly creationTime!: string;\n  public readonly lastSignInTime!: string;\n  public readonly lastRefreshTime?: string | null;\n\n  constructor(response: GetAccountInfoUserResponse) {\n    addReadonlyGetter(this, 'creationTime', parseDate(response.createdAt));\n    addReadonlyGetter(this, 'lastSignInTime', parseDate(response.lastLoginAt));\n    const lastRefreshAt = response.lastRefreshAt\n      ? new Date(response.lastRefreshAt).toUTCString()\n      : null;\n    addReadonlyGetter(this, 'lastRefreshTime', lastRefreshAt);\n  }\n\n  public toJSON(): UserMetadataType {\n    return {\n      lastSignInTime: this.lastSignInTime,\n      creationTime: this.creationTime,\n      lastRefreshTime: this.lastRefreshTime\n    };\n  }\n}\n\nexport type UserInfoType = {\n  uid?: string;\n  displayName?: string;\n  email?: string;\n  photoURL?: string;\n  providerId?: string;\n  phoneNumber?: string;\n};\n\nexport class UserInfo {\n  public readonly uid!: string;\n  public readonly displayName!: string;\n  public readonly email!: string;\n  public readonly photoURL!: string;\n  public readonly providerId!: string;\n  public readonly phoneNumber!: string;\n\n  constructor(response: ProviderUserInfoResponse) {\n    if (!response.rawId || !response.providerId) {\n      throw new AuthError(\n        AuthErrorCode.INTERNAL_ERROR,\n        'INTERNAL ASSERT FAILED: Invalid user info response'\n      );\n    }\n\n    addReadonlyGetter(this, 'uid', response.rawId);\n    addReadonlyGetter(this, 'displayName', response.displayName);\n    addReadonlyGetter(this, 'email', response.email);\n    addReadonlyGetter(this, 'photoURL', response.photoUrl);\n    addReadonlyGetter(this, 'providerId', response.providerId);\n    addReadonlyGetter(this, 'phoneNumber', response.phoneNumber);\n  }\n\n  public toJSON(): UserInfoType {\n    return {\n      uid: this.uid,\n      displayName: this.displayName,\n      email: this.email,\n      photoURL: this.photoURL,\n      providerId: this.providerId,\n      phoneNumber: this.phoneNumber\n    };\n  }\n}\n\nexport class UserRecord {\n  public readonly uid!: string;\n  public readonly email?: string;\n  public readonly emailVerified!: boolean;\n  public readonly displayName?: string;\n  public readonly photoURL?: string;\n  public readonly phoneNumber?: string;\n  public readonly disabled!: boolean;\n  public readonly metadata!: UserMetadata;\n  public readonly providerData!: UserInfo[];\n  public readonly passwordHash?: string;\n  public readonly passwordSalt?: string;\n  public readonly customClaims?: {[key: string]: unknown};\n  public readonly tenantId?: string | null;\n  public readonly tokensValidAfterTime?: string;\n  public readonly multiFactor?: MultiFactorSettings;\n\n  constructor(response: GetAccountInfoUserResponse) {\n    if (!response.localId) {\n      throw new AuthError(\n        AuthErrorCode.INTERNAL_ERROR,\n        'INTERNAL ASSERT FAILED: Invalid user response'\n      );\n    }\n\n    addReadonlyGetter(this, 'uid', response.localId);\n    addReadonlyGetter(this, 'email', response.email);\n    addReadonlyGetter(this, 'emailVerified', !!response.emailVerified);\n    addReadonlyGetter(this, 'displayName', response.displayName);\n    addReadonlyGetter(this, 'photoURL', response.photoUrl);\n    addReadonlyGetter(this, 'phoneNumber', response.phoneNumber);\n    addReadonlyGetter(this, 'disabled', response.disabled || false);\n    addReadonlyGetter(this, 'metadata', new UserMetadata(response));\n    const providerData: UserInfo[] = [];\n    for (const entry of response.providerUserInfo || []) {\n      providerData.push(new UserInfo(entry));\n    }\n    addReadonlyGetter(this, 'providerData', providerData);\n\n    if (response.passwordHash === B64_REDACTED) {\n      addReadonlyGetter(this, 'passwordHash', undefined);\n    } else {\n      addReadonlyGetter(this, 'passwordHash', response.passwordHash);\n    }\n\n    addReadonlyGetter(this, 'passwordSalt', response.salt);\n    if (response.customAttributes) {\n      addReadonlyGetter(\n        this,\n        'customClaims',\n        JSON.parse(response.customAttributes)\n      );\n    }\n\n    let validAfterTime: string | null = null;\n    if (typeof response.validSince !== 'undefined') {\n      validAfterTime = parseDate(parseInt(response.validSince, 10) * 1000);\n    }\n    addReadonlyGetter(\n      this,\n      'tokensValidAfterTime',\n      validAfterTime || undefined\n    );\n    addReadonlyGetter(this, 'tenantId', response.tenantId);\n    const multiFactor = new MultiFactorSettings(response);\n    if (multiFactor.enrolledFactors.length > 0) {\n      addReadonlyGetter(this, 'multiFactor', multiFactor);\n    }\n  }\n\n  public toJSON(): UserRecordType {\n    const json: UserRecordType = {\n      uid: this.uid,\n      email: this.email,\n      emailVerified: this.emailVerified,\n      displayName: this.displayName,\n      photoURL: this.photoURL,\n      phoneNumber: this.phoneNumber,\n      disabled: this.disabled,\n      metadata: this.metadata.toJSON(),\n      passwordHash: this.passwordHash,\n      passwordSalt: this.passwordSalt,\n      customClaims: deepCopy(this.customClaims),\n      tokensValidAfterTime: this.tokensValidAfterTime,\n      tenantId: this.tenantId\n    };\n    if (this.multiFactor) {\n      json.multiFactor = this.multiFactor.toJSON();\n    }\n    json.providerData = [];\n    for (const entry of this.providerData) {\n      json.providerData.push(entry.toJSON());\n    }\n    return json;\n  }\n}\n\nexport interface UserRecordType {\n  uid?: string;\n  email?: string;\n  emailVerified?: boolean;\n  displayName?: string;\n  photoURL?: string;\n  phoneNumber?: string;\n  multiFactor?: MultiFactorInfoType;\n  disabled?: boolean;\n  metadata?: UserMetadataType;\n  passwordHash?: string;\n  passwordSalt?: string;\n  customClaims?: {[key: string]: unknown};\n  providerData?: UserInfoType[];\n  tokensValidAfterTime?: string;\n  tenantId?: string | null;\n}\n"
  },
  {
    "path": "src/auth/utils.ts",
    "content": "import {JWTPayload} from 'jose';\nimport {DecodedIdToken} from './types.js';\n\nexport function formatString(str: string, params?: object): string {\n  let formatted = str;\n  Object.keys(params || {}).forEach((key) => {\n    formatted = formatted.replace(\n      new RegExp('{' + key + '}', 'g'),\n      (params as {[key: string]: string})[key]\n    );\n  });\n  return formatted;\n}\n\nasync function getDetailFromResponse(response: Response): Promise<string> {\n  const json = await response.json();\n\n  if (!json) {\n    return 'Missing error payload';\n  }\n\n  let detail =\n    typeof json.error === 'string'\n      ? json.error\n      : (json.error?.message ?? 'Missing error payload');\n\n  if (json.error_description) {\n    detail += ' (' + json.error_description + ')';\n  }\n\n  return detail;\n}\n\nexport async function fetchJson(url: string, init: RequestInit) {\n  return (await fetchAny(url, init)).json();\n}\n\nexport async function fetchText(url: string, init: RequestInit) {\n  return (await fetchAny(url, init)).text();\n}\n\nexport async function fetchAny(url: string, init: RequestInit) {\n  const response = await fetch(url, init);\n\n  if (!response.ok) {\n    throw new Error(await getDetailFromResponse(response));\n  }\n\n  return response;\n}\n\nexport function mapJwtPayloadToDecodedIdToken(payload: JWTPayload) {\n  const decodedIdToken = payload as DecodedIdToken;\n  decodedIdToken.uid = decodedIdToken.sub;\n  return decodedIdToken;\n}\n\nexport function addReadonlyGetter(\n  obj: object,\n  prop: string,\n  value: unknown\n): void {\n  Object.defineProperty(obj, prop, {\n    value,\n    writable: false,\n    enumerable: true\n  });\n}\n\nexport function deepCopy<T>(value: T): T {\n  return deepExtend(undefined, value) as T;\n}\n\nexport function deepExtend<T>(target: T, source: T): T {\n  if (!(source instanceof Object)) {\n    return source;\n  }\n\n  switch (source.constructor) {\n    case Date: {\n      const dateValue = source as unknown as Date;\n      return new Date(dateValue.getTime()) as T;\n    }\n\n    case Object:\n      if (target === undefined) {\n        target = {} as T;\n      }\n      break;\n\n    case Array:\n      target = [] as T;\n      break;\n\n    default:\n      return source;\n  }\n\n  for (const prop in source) {\n    if (!Object.prototype.hasOwnProperty.call(source, prop)) {\n      continue;\n    }\n\n    const objectTarget = target as {[key: string]: unknown};\n    objectTarget[prop] = deepExtend(objectTarget[prop], objectTarget[prop]);\n  }\n\n  return target;\n}\n\nconst encoder = new TextEncoder();\n\nexport function toUint8Array(key: string) {\n  return encoder.encode(key);\n}\n"
  },
  {
    "path": "src/auth/validator.ts",
    "content": "export function isObject(value: unknown): boolean {\n  return typeof value === 'object' && !isArray(value);\n}\n\nexport function isArray<T>(value: unknown): value is T[] {\n  return Array.isArray(value);\n}\n\nexport function isNonNullObject<T>(value: T | null | undefined): value is T {\n  return isObject(value) && value !== null;\n}\n\nexport function isEmail(email: unknown): boolean {\n  if (typeof email !== 'string') {\n    return false;\n  }\n  // There must at least one character before the @ symbol and another after.\n  const re = /^[^@]+@[^@]+$/;\n  return re.test(email);\n}\n\nexport function isURL(urlStr: unknown): boolean {\n  if (typeof urlStr !== 'string') {\n    return false;\n  }\n\n  const re = /[^a-z0-9:/?#[\\]@!$&'()*+,;=.\\-_~%]/i;\n  if (re.test(urlStr)) {\n    return false;\n  }\n  try {\n    const uri = new URL(urlStr);\n    const scheme = uri.protocol;\n    const hostname = uri.hostname;\n    const pathname = uri.pathname;\n    if (scheme !== 'http:' && scheme !== 'https:') {\n      return false;\n    }\n\n    if (\n      !hostname ||\n      !/^[a-zA-Z0-9]+[\\w-]*([.]?[a-zA-Z0-9]+[\\w-]*)*$/.test(hostname)\n    ) {\n      return false;\n    }\n\n    const pathnameRe = /^(\\/[\\w\\-.~!$'()*+,;=:@%]+)*\\/?$/;\n    if (pathname && pathname !== '/' && !pathnameRe.test(pathname)) {\n      return false;\n    }\n  } catch {\n    return false;\n  }\n  return true;\n}\n"
  },
  {
    "path": "src/debug/index.ts",
    "content": "let debugEnabled = false;\n\nexport function enableDebugMode() {\n  debugEnabled = true;\n}\n\nexport function debug(\n  message: string,\n  metadata?: Record<string, unknown | undefined>\n) {\n  if (!debugEnabled) {\n    return;\n  }\n\n  console.log('ⓘ next-firebase-auth-edge:', message);\n\n  if (!metadata) {\n    return;\n  }\n\n  for (const key in metadata) {\n    if (metadata[key]) {\n      console.log('\\t', `${key}:`, metadata[key]);\n    }\n  }\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "export {\n  authMiddleware,\n  redirectToHome,\n  redirectToLogin,\n  redirectToPath\n} from './next/middleware.js';\n\nexport {\n  getTokens,\n  getTokensFromObject,\n  getApiRequestTokens\n} from './next/tokens.js';\n\nexport {getFirebaseAuth} from './auth/index.js';\nexport type {Tokens} from './auth/index.js';\n"
  },
  {
    "path": "src/next/api.ts",
    "content": "import type {IncomingHttpHeaders} from 'http';\nimport {NextApiRequest, NextApiResponse} from 'next';\nimport {ParsedCookies, VerifiedCookies} from '../auth/custom-token/index.js';\nimport {getFirebaseAuth} from '../auth/index.js';\nimport {AuthCookies} from './cookies/AuthCookies.js';\nimport {CookiesObject, SetAuthCookiesOptions} from './cookies/index.js';\nimport {ObjectCookiesProvider} from './cookies/parser/ObjectCookiesProvider.js';\nimport {getCookiesTokens} from './tokens.js';\nimport {getMetadataInternal} from './metadata.js';\n\nexport async function refreshApiResponseCookies<Metadata extends object>(\n  request: NextApiRequest,\n  response: NextApiResponse,\n  options: SetAuthCookiesOptions<Metadata>\n): Promise<NextApiResponse> {\n  const value = await refreshApiCookies(\n    request.cookies,\n    request.headers,\n    options\n  );\n  await appendAuthCookiesApi(request.cookies, response, value, options);\n\n  return response;\n}\n\nexport async function appendAuthCookiesApi<Metadata extends object>(\n  cookies: CookiesObject,\n  response: NextApiResponse,\n  value: ParsedCookies<Metadata>,\n  options: SetAuthCookiesOptions<Metadata>\n) {\n  const authCookies = new AuthCookies(\n    new ObjectCookiesProvider(cookies),\n    options\n  );\n\n  await authCookies.setAuthNextApiResponseHeaders(value, response);\n}\n\nexport async function refreshApiCookies<Metadata extends object>(\n  cookies: CookiesObject,\n  headers: IncomingHttpHeaders,\n  options: SetAuthCookiesOptions<Metadata>\n): Promise<VerifiedCookies<Metadata>> {\n  const referer = headers['referer'] ?? '';\n  const tokens = await getCookiesTokens(cookies, options);\n  const {handleTokenRefresh} = getFirebaseAuth({\n    serviceAccount: options.serviceAccount,\n    apiKey: options.apiKey,\n    tenantId: options.tenantId\n  });\n\n  const tokenRefreshResult = await handleTokenRefresh(tokens.refreshToken, {\n    referer,\n    enableCustomToken: options.enableCustomToken\n  });\n\n  const metadata = await getMetadataInternal<Metadata>(\n    tokenRefreshResult,\n    options\n  );\n\n  return {\n    customToken: tokenRefreshResult.customToken,\n    idToken: tokenRefreshResult.idToken,\n    refreshToken: tokenRefreshResult.refreshToken,\n    decodedIdToken: tokenRefreshResult.decodedIdToken,\n    metadata\n  };\n}\n"
  },
  {
    "path": "src/next/client.ts",
    "content": "import {decodeJwt} from 'jose';\nimport {AuthError, AuthErrorCode, HttpError} from '../auth/error.js';\n\nclass ClientTokenCache {\n  private cacheMap: Record<string, string> = {};\n\n  constructor() {}\n\n  public get(value: string) {\n    if (!this.cacheMap[value]) {\n      return value;\n    }\n\n    return this.cacheMap[value];\n  }\n\n  public set(originalValue: string, value: string) {\n    this.cacheMap = {[originalValue]: value};\n  }\n}\n\nconst idTokenCache = new ClientTokenCache();\nconst customTokenCache = new ClientTokenCache();\n\nexport interface GetValidIdTokenOptions {\n  serverIdToken: string;\n  refreshTokenUrl: string;\n  checkRevoked?: boolean;\n}\n\nexport async function getValidIdToken({\n  serverIdToken,\n  refreshTokenUrl,\n  checkRevoked\n}: GetValidIdTokenOptions): Promise<string | null> {\n  // If serverIdToken is empty, we assume user is unauthenticated and token refresh will yield null\n  if (!serverIdToken) {\n    return null;\n  }\n\n  const token = idTokenCache.get(serverIdToken);\n  const payload = decodeJwt(token);\n  const exp = payload?.exp ?? 0;\n\n  if (!checkRevoked && exp > Date.now() / 1000) {\n    return token || serverIdToken;\n  }\n\n  const response = await fetchApi<{idToken: string}>(refreshTokenUrl);\n\n  if (!response?.idToken) {\n    throw new AuthError(\n      AuthErrorCode.INTERNAL_ERROR,\n      'Refresh token endpoint returned invalid response. This URL should point to endpoint exposed by the middleware and configured using refreshTokenPath option'\n    );\n  }\n\n  idTokenCache.set(serverIdToken, response.idToken);\n\n  return response.idToken;\n}\n\nexport interface GetValidCustomTokenOptions {\n  serverCustomToken: string;\n  refreshTokenUrl: string;\n  checkRevoked?: boolean;\n}\n\nexport async function getValidCustomToken({\n  serverCustomToken,\n  refreshTokenUrl,\n  checkRevoked\n}: GetValidCustomTokenOptions): Promise<string | null> {\n  // If serverCustomToken is empty, we assume user is unauthenticated and token refresh will yield null\n  if (!serverCustomToken) {\n    return null;\n  }\n\n  const token = customTokenCache.get(serverCustomToken);\n  const payload = decodeJwt(token);\n  const exp = payload?.exp ?? 0;\n\n  if (!checkRevoked && exp > Date.now() / 1000) {\n    return token || serverCustomToken;\n  }\n\n  const response = await fetchApi<{customToken: string}>(refreshTokenUrl);\n\n  if (!response) {\n    throw new AuthError(\n      AuthErrorCode.INTERNAL_ERROR,\n      'Refresh token endpoint returned invalid response. This URL should point to endpoint exposed by the middleware and configured using refreshTokenPath option.'\n    );\n  }\n\n  if (!response.customToken) {\n    throw new AuthError(\n      AuthErrorCode.INTERNAL_ERROR,\n      'Refresh token endpoint returned empty custom token. Make sure you have set `enableCustomToken` option to `true` in `authMiddleware`'\n    );\n  }\n\n  customTokenCache.set(serverCustomToken, response.customToken);\n\n  return response.customToken;\n}\n\nasync function mapResponseToAuthError(\n  response: Response,\n  input: RequestInfo | URL,\n  init?: RequestInit\n) {\n  if (response.status === 401) {\n    const data = await safeResponse<HttpError>(response);\n\n    return new AuthError(AuthErrorCode.INVALID_CREDENTIAL, data?.message);\n  }\n\n  const text = await safeResponse(response);\n\n  return new AuthError(\n    AuthErrorCode.INTERNAL_ERROR,\n    `next-firebase-auth-edge: Internal request to ${\n      init?.method ?? 'GET'\n    } ${input.toString()} has failed: ${text}`\n  );\n}\n\nfunction safeResponse<T>(response: Response): Promise<T> {\n  const contentType = response.headers.get('content-type');\n  if (contentType && contentType.indexOf('application/json') !== -1) {\n    return response.json();\n  } else {\n    return response.text() as Promise<T>;\n  }\n}\n\nasync function fetchApi<T>(\n  input: RequestInfo | URL,\n  init?: RequestInit\n): Promise<T> {\n  const response = await fetch(input, {\n    ...init,\n    headers: {\n      ...init?.headers,\n      Accept: 'application/json',\n      'Content-Type': 'application/json'\n    }\n  });\n\n  if (!response.ok) {\n    throw await mapResponseToAuthError(response, input, init);\n  }\n\n  return safeResponse(response);\n}\n"
  },
  {
    "path": "src/next/cookies/AuthCookies.test.ts",
    "content": "import type {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\nimport {ParsedCookies} from '../../auth/custom-token/index.ts';\nimport {AuthCookies} from './AuthCookies.ts';\nimport {SetAuthCookiesOptions} from './index.ts';\nimport {ObjectCookiesProvider} from './parser/ObjectCookiesProvider.ts';\n\nconst cookieName = 'TestCookie';\nconst cookieSerializeOptions = {\n  path: '/',\n  httpOnly: true,\n  secure: true,\n  sameSite: 'lax' as const,\n  maxAge: 12 * 60 * 60 * 24,\n  expires: new Date(1727373870 * 1000)\n};\nconst setAuthCookiesOptions: SetAuthCookiesOptions<never> = {\n  cookieName,\n  cookieSerializeOptions,\n  cookieSignatureKeys: ['secret'],\n  apiKey: 'test-api-key',\n  enableCustomToken: true\n};\n\nconst mockTokens: ParsedCookies<never> = {\n  idToken: 'id-token',\n  refreshToken: 'refresh-token',\n  customToken: 'custom-token',\n  metadata: {} as never\n};\n\ndescribe('AuthCookies', () => {\n  describe('headers', () => {\n    it('should set single cookie', async () => {\n      const provider = new ObjectCookiesProvider({});\n      const cookies = new AuthCookies(provider, setAuthCookiesOptions);\n      const headers = {append: jest.fn()} as unknown as Headers;\n\n      await cookies.setAuthHeaders(mockTokens, headers);\n\n      expect(headers.append).toHaveBeenCalledTimes(1);\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6ImlkLXRva2VuIiwicmVmcmVzaF90b2tlbiI6InJlZnJlc2gtdG9rZW4iLCJjdXN0b21fdG9rZW4iOiJjdXN0b20tdG9rZW4ifQ.ExxN2rNayg2XCR6WNeZmY8tAyc_qyiZ2YdzITRbQocs; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n    });\n\n    it('should set multiple cookies', async () => {\n      const provider = new ObjectCookiesProvider({});\n      const cookies = new AuthCookies(provider, {\n        ...setAuthCookiesOptions,\n        enableMultipleCookies: true\n      });\n      const headers = {append: jest.fn()} as unknown as Headers;\n\n      await cookies.setAuthHeaders(mockTokens, headers);\n\n      expect(headers.append).toHaveBeenCalledTimes(4);\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.id=id-token; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.refresh=refresh-token; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.custom=custom-token; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.sig=QupyAMaPmI6d90CqB0lvec5Q517onmUvXEk6bONTQM0; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n    });\n\n    it('should set multiple cookies and remove single cookie if exists', async () => {\n      const provider = new ObjectCookiesProvider({\n        TestCookie: 'legacy-token'\n      });\n      const cookies = new AuthCookies(provider, {\n        ...setAuthCookiesOptions,\n        enableMultipleCookies: true\n      });\n      const headers = {append: jest.fn()} as unknown as Headers;\n\n      await cookies.setAuthHeaders(mockTokens, headers);\n\n      expect(headers.append).toHaveBeenCalledTimes(5);\n      expect(headers.append).toHaveBeenNthCalledWith(\n        1,\n        'Set-Cookie',\n        'TestCookie=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.id=id-token; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.refresh=refresh-token; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.custom=custom-token; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.sig=QupyAMaPmI6d90CqB0lvec5Q517onmUvXEk6bONTQM0; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n    });\n\n    it('should set multiple cookies and remove custom cookie if not enabled', async () => {\n      const provider = new ObjectCookiesProvider({\n        TestCookie: 'legacy-token',\n        'TestCookie.custom': 'custom-token'\n      });\n      const cookies = new AuthCookies(provider, {\n        ...setAuthCookiesOptions,\n        enableMultipleCookies: true,\n        enableCustomToken: false\n      });\n      const headers = {append: jest.fn()} as unknown as Headers;\n\n      await cookies.setAuthHeaders(mockTokens, headers);\n\n      expect(headers.append).toHaveBeenCalledTimes(5);\n      expect(headers.append).toHaveBeenNthCalledWith(\n        1,\n        'Set-Cookie',\n        'TestCookie.custom=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenNthCalledWith(\n        2,\n        'Set-Cookie',\n        'TestCookie=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.id=id-token; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.refresh=refresh-token; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenNthCalledWith(\n        5,\n        'Set-Cookie',\n        'TestCookie.sig=g-7yXxxJfMmzsR7BqkJjguoUWsOqCTGz2AndxjJBrkw; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n    });\n\n    it('should set single cookie and remove multiple cookie if exists', async () => {\n      const provider = new ObjectCookiesProvider({\n        'TestCookie.id': 'legacy-id-token',\n        'TestCookie.refresh': 'legacy-refresh-token',\n        'TestCookie.custom': 'legacy-custom-token',\n        'TestCookie.sig': 'legacy-signature'\n      });\n      const cookies = new AuthCookies(provider, setAuthCookiesOptions);\n      const headers = {append: jest.fn()} as unknown as Headers;\n\n      await cookies.setAuthHeaders(mockTokens, headers);\n\n      expect(headers.append).toHaveBeenCalledTimes(6);\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.refresh=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.custom=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.metadata=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.sig=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenNthCalledWith(\n        6,\n        'Set-Cookie',\n        'TestCookie=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6ImlkLXRva2VuIiwicmVmcmVzaF90b2tlbiI6InJlZnJlc2gtdG9rZW4iLCJjdXN0b21fdG9rZW4iOiJjdXN0b20tdG9rZW4ifQ.ExxN2rNayg2XCR6WNeZmY8tAyc_qyiZ2YdzITRbQocs; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n    });\n\n    it('should set single cookie and remove custom token if exists', async () => {\n      const provider = new ObjectCookiesProvider({\n        'TestCookie.custom': 'legacy-custom-token'\n      });\n      const cookies = new AuthCookies(provider, {\n        ...setAuthCookiesOptions,\n        enableCustomToken: false\n      });\n      const headers = {append: jest.fn()} as unknown as Headers;\n\n      await cookies.setAuthHeaders(mockTokens, headers);\n\n      expect(headers.append).toHaveBeenCalledTimes(2);\n\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.custom=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n\n      expect(headers.append).toHaveBeenNthCalledWith(\n        2,\n        'Set-Cookie',\n        'TestCookie=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6ImlkLXRva2VuIiwicmVmcmVzaF90b2tlbiI6InJlZnJlc2gtdG9rZW4ifQ.Zf81UFf9nyW96_0M0eymGmfPABKYben_nGMc1_9l86k; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n    });\n\n    it('should set single cookie and remove legacy multiple cookie if exists', async () => {\n      const provider = new ObjectCookiesProvider({\n        TestCookie: 'legacy-id-token:legacy-refresh-token',\n        'TestCookie.custom': 'legacy-custom-token',\n        'TestCookie.sig': 'legacy-signature'\n      });\n      const cookies = new AuthCookies(provider, setAuthCookiesOptions);\n      const headers = {append: jest.fn()} as unknown as Headers;\n\n      await cookies.setAuthHeaders(mockTokens, headers);\n\n      expect(headers.append).toHaveBeenCalledTimes(6);\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.refresh=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.custom=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.metadata=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.sig=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenNthCalledWith(\n        6,\n        'Set-Cookie',\n        'TestCookie=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6ImlkLXRva2VuIiwicmVmcmVzaF90b2tlbiI6InJlZnJlc2gtdG9rZW4iLCJjdXN0b21fdG9rZW4iOiJjdXN0b20tdG9rZW4ifQ.ExxN2rNayg2XCR6WNeZmY8tAyc_qyiZ2YdzITRbQocs; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n    });\n\n    it('should set single cookie without custom token', async () => {\n      const provider = new ObjectCookiesProvider({});\n      const cookies = new AuthCookies(provider, {\n        ...setAuthCookiesOptions,\n        enableCustomToken: false\n      });\n      const headers = {append: jest.fn()} as unknown as Headers;\n\n      await cookies.setAuthHeaders(mockTokens, headers);\n\n      expect(headers.append).toHaveBeenCalledTimes(1);\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6ImlkLXRva2VuIiwicmVmcmVzaF90b2tlbiI6InJlZnJlc2gtdG9rZW4ifQ.Zf81UFf9nyW96_0M0eymGmfPABKYben_nGMc1_9l86k; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n    });\n\n    it('should set multiple cookies without custom token', async () => {\n      const provider = new ObjectCookiesProvider({});\n      const cookies = new AuthCookies(provider, {\n        ...setAuthCookiesOptions,\n        enableMultipleCookies: true,\n        enableCustomToken: false\n      });\n      const headers = {append: jest.fn()} as unknown as Headers;\n\n      await cookies.setAuthHeaders(mockTokens, headers);\n\n      expect(headers.append).toHaveBeenCalledTimes(3);\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.id=id-token; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.refresh=refresh-token; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n      expect(headers.append).toHaveBeenCalledWith(\n        'Set-Cookie',\n        'TestCookie.sig=g-7yXxxJfMmzsR7BqkJjguoUWsOqCTGz2AndxjJBrkw; Max-Age=1036800; Path=/; Expires=Thu, 26 Sep 2024 18:04:30 GMT; HttpOnly; Secure; SameSite=Lax'\n      );\n    });\n  });\n\n  describe('cookies', () => {\n    it('should set single cookie', async () => {\n      const provider = new ObjectCookiesProvider({});\n      const cookies = new AuthCookies(provider, setAuthCookiesOptions);\n      const requestCookies = {\n        set: jest.fn(),\n        delete: jest.fn()\n      } as unknown as RequestCookies;\n\n      await cookies.setAuthCookies(mockTokens, requestCookies);\n\n      expect(requestCookies.set).toHaveBeenCalledTimes(1);\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie',\n        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6ImlkLXRva2VuIiwicmVmcmVzaF90b2tlbiI6InJlZnJlc2gtdG9rZW4iLCJjdXN0b21fdG9rZW4iOiJjdXN0b20tdG9rZW4ifQ.ExxN2rNayg2XCR6WNeZmY8tAyc_qyiZ2YdzITRbQocs',\n        cookieSerializeOptions\n      );\n    });\n\n    it('should set multiple cookies', async () => {\n      const provider = new ObjectCookiesProvider({});\n      const cookies = new AuthCookies(provider, {\n        ...setAuthCookiesOptions,\n        enableMultipleCookies: true\n      });\n      const requestCookies = {\n        set: jest.fn(),\n        delete: jest.fn()\n      } as unknown as RequestCookies;\n\n      await cookies.setAuthCookies(mockTokens, requestCookies);\n\n      expect(requestCookies.set).toHaveBeenCalledTimes(4);\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie.id',\n        'id-token',\n        cookieSerializeOptions\n      );\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie.refresh',\n        'refresh-token',\n        cookieSerializeOptions\n      );\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie.custom',\n        'custom-token',\n        cookieSerializeOptions\n      );\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie.sig',\n        'QupyAMaPmI6d90CqB0lvec5Q517onmUvXEk6bONTQM0',\n        cookieSerializeOptions\n      );\n    });\n\n    it('should set multiple cookies and remove single cookie if exists', async () => {\n      const provider = new ObjectCookiesProvider({\n        TestCookie: 'legacy-token'\n      });\n      const cookies = new AuthCookies(provider, {\n        ...setAuthCookiesOptions,\n        enableMultipleCookies: true\n      });\n      const requestCookies = {\n        set: jest.fn(),\n        delete: jest.fn()\n      } as unknown as RequestCookies;\n\n      await cookies.setAuthCookies(mockTokens, requestCookies);\n\n      expect(requestCookies.delete).toHaveBeenCalledTimes(1);\n      expect(requestCookies.delete).toHaveBeenCalledWith('TestCookie');\n\n      expect(requestCookies.set).toHaveBeenCalledTimes(4);\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie.id',\n        'id-token',\n        cookieSerializeOptions\n      );\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie.refresh',\n        'refresh-token',\n        cookieSerializeOptions\n      );\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie.custom',\n        'custom-token',\n        cookieSerializeOptions\n      );\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie.sig',\n        'QupyAMaPmI6d90CqB0lvec5Q517onmUvXEk6bONTQM0',\n        cookieSerializeOptions\n      );\n    });\n\n    it('should set multiple cookies and remove custom cookie if not enabled', async () => {\n      const provider = new ObjectCookiesProvider({\n        TestCookie: 'legacy-token',\n        'TestCookie.custom': 'custom-token'\n      });\n      const cookies = new AuthCookies(provider, {\n        ...setAuthCookiesOptions,\n        enableMultipleCookies: true,\n        enableCustomToken: false\n      });\n      const requestCookies = {\n        set: jest.fn(),\n        delete: jest.fn()\n      } as unknown as RequestCookies;\n\n      await cookies.setAuthCookies(mockTokens, requestCookies);\n\n      expect(requestCookies.delete).toHaveBeenCalledTimes(2);\n      expect(requestCookies.delete).toHaveBeenCalledWith('TestCookie');\n      expect(requestCookies.delete).toHaveBeenCalledWith('TestCookie.custom');\n\n      expect(requestCookies.set).toHaveBeenCalledTimes(3);\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie.id',\n        'id-token',\n        cookieSerializeOptions\n      );\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie.refresh',\n        'refresh-token',\n        cookieSerializeOptions\n      );\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie.sig',\n        'g-7yXxxJfMmzsR7BqkJjguoUWsOqCTGz2AndxjJBrkw',\n        cookieSerializeOptions\n      );\n    });\n\n    it('should set single cookie and remove multiple cookie if exists', async () => {\n      const provider = new ObjectCookiesProvider({\n        'TestCookie.id': 'legacy-id-token',\n        'TestCookie.refresh': 'legacy-refresh-token',\n        'TestCookie.custom': 'legacy-custom-token',\n        'TestCookie.sig': 'legacy-signature'\n      });\n      const cookies = new AuthCookies(provider, setAuthCookiesOptions);\n      const requestCookies = {\n        set: jest.fn(),\n        delete: jest.fn()\n      } as unknown as RequestCookies;\n\n      await cookies.setAuthCookies(mockTokens, requestCookies);\n\n      expect(requestCookies.delete).toHaveBeenCalledTimes(5);\n      expect(requestCookies.delete).toHaveBeenCalledWith('TestCookie.id');\n      expect(requestCookies.delete).toHaveBeenCalledWith('TestCookie.refresh');\n      expect(requestCookies.delete).toHaveBeenCalledWith('TestCookie.custom');\n      expect(requestCookies.delete).toHaveBeenCalledWith('TestCookie.metadata');\n      expect(requestCookies.delete).toHaveBeenCalledWith('TestCookie.sig');\n\n      expect(requestCookies.set).toHaveBeenCalledTimes(1);\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie',\n        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6ImlkLXRva2VuIiwicmVmcmVzaF90b2tlbiI6InJlZnJlc2gtdG9rZW4iLCJjdXN0b21fdG9rZW4iOiJjdXN0b20tdG9rZW4ifQ.ExxN2rNayg2XCR6WNeZmY8tAyc_qyiZ2YdzITRbQocs',\n        cookieSerializeOptions\n      );\n    });\n\n    it('should set single cookie and remove legacy multiple cookie if exists', async () => {\n      const provider = new ObjectCookiesProvider({\n        TestCookie: 'legacy-id-token:legacy-refresh-token',\n        'TestCookie.custom': 'legacy-custom-token',\n        'TestCookie.sig': 'legacy-signature'\n      });\n      const cookies = new AuthCookies(provider, setAuthCookiesOptions);\n      const requestCookies = {\n        set: jest.fn(),\n        delete: jest.fn()\n      } as unknown as RequestCookies;\n\n      await cookies.setAuthCookies(mockTokens, requestCookies);\n\n      expect(requestCookies.delete).toHaveBeenCalledTimes(5);\n      expect(requestCookies.delete).toHaveBeenCalledWith('TestCookie.id');\n      expect(requestCookies.delete).toHaveBeenCalledWith('TestCookie.refresh');\n      expect(requestCookies.delete).toHaveBeenCalledWith('TestCookie.custom');\n      expect(requestCookies.delete).toHaveBeenCalledWith('TestCookie.metadata');\n      expect(requestCookies.delete).toHaveBeenCalledWith('TestCookie.sig');\n\n      expect(requestCookies.set).toHaveBeenCalledTimes(1);\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie',\n        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6ImlkLXRva2VuIiwicmVmcmVzaF90b2tlbiI6InJlZnJlc2gtdG9rZW4iLCJjdXN0b21fdG9rZW4iOiJjdXN0b20tdG9rZW4ifQ.ExxN2rNayg2XCR6WNeZmY8tAyc_qyiZ2YdzITRbQocs',\n        cookieSerializeOptions\n      );\n    });\n\n    it('should set single cookie and remove custom cookie if exists', async () => {\n      const provider = new ObjectCookiesProvider({\n        'TestCookie.id': 'legacy-id-token',\n        'TestCookie.custom': 'legacy-custom-token'\n      });\n      const cookies = new AuthCookies(provider, {\n        ...setAuthCookiesOptions,\n        enableCustomToken: false\n      });\n      const requestCookies = {\n        set: jest.fn(),\n        delete: jest.fn()\n      } as unknown as RequestCookies;\n\n      await cookies.setAuthCookies(mockTokens, requestCookies);\n\n      expect(requestCookies.delete).toHaveBeenCalledTimes(1);\n      expect(requestCookies.delete).toHaveBeenCalledWith('TestCookie.custom');\n\n      expect(requestCookies.set).toHaveBeenCalledTimes(1);\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie',\n        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6ImlkLXRva2VuIiwicmVmcmVzaF90b2tlbiI6InJlZnJlc2gtdG9rZW4ifQ.Zf81UFf9nyW96_0M0eymGmfPABKYben_nGMc1_9l86k',\n        cookieSerializeOptions\n      );\n    });\n\n    it('should set single cookie without custom token', async () => {\n      const provider = new ObjectCookiesProvider({});\n      const cookies = new AuthCookies(provider, {\n        ...setAuthCookiesOptions,\n        enableCustomToken: false\n      });\n      const requestCookies = {\n        set: jest.fn(),\n        delete: jest.fn()\n      } as unknown as RequestCookies;\n\n      await cookies.setAuthCookies(mockTokens, requestCookies);\n\n      expect(requestCookies.set).toHaveBeenCalledTimes(1);\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie',\n        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6ImlkLXRva2VuIiwicmVmcmVzaF90b2tlbiI6InJlZnJlc2gtdG9rZW4ifQ.Zf81UFf9nyW96_0M0eymGmfPABKYben_nGMc1_9l86k',\n        cookieSerializeOptions\n      );\n    });\n\n    it('should set multiple cookies without custom token', async () => {\n      const provider = new ObjectCookiesProvider({});\n      const cookies = new AuthCookies(provider, {\n        ...setAuthCookiesOptions,\n        enableMultipleCookies: true,\n        enableCustomToken: false\n      });\n      const requestCookies = {\n        set: jest.fn(),\n        delete: jest.fn()\n      } as unknown as RequestCookies;\n\n      await cookies.setAuthCookies(mockTokens, requestCookies);\n\n      expect(requestCookies.set).toHaveBeenCalledTimes(3);\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie.id',\n        'id-token',\n        cookieSerializeOptions\n      );\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie.refresh',\n        'refresh-token',\n        cookieSerializeOptions\n      );\n      expect(requestCookies.set).toHaveBeenCalledWith(\n        'TestCookie.sig',\n        'g-7yXxxJfMmzsR7BqkJjguoUWsOqCTGz2AndxjJBrkw',\n        cookieSerializeOptions\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "src/next/cookies/AuthCookies.ts",
    "content": "import type {NextApiResponse} from 'next';\nimport type {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies';\nimport type {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\nimport {ParsedCookies} from '../../auth/custom-token/index.js';\nimport {Cookie, CookieBuilder} from './builder/CookieBuilder.js';\nimport {CookieBuilderFactory} from './builder/CookieBuilderFactory.js';\nimport {MultipleCookieExpiration} from './expiration/MultipleCookieExpiration.js';\nimport {SingleCookieExpiration} from './expiration/SingleCookieExpiration.js';\nimport {CookieParserFactory} from './parser/CookieParserFactory.js';\nimport {CookiesProvider} from './parser/CookiesProvider.js';\nimport {CookieSetter} from './setter/CookieSetter.js';\nimport {CookieSetterFactory} from './setter/CookieSetterFactory.js';\nimport {NextApiResponseCookieSetter} from './setter/NextApiResponseHeadersCookieSetter.js';\nimport {SetAuthCookiesOptions} from './types.js';\n\nexport class AuthCookies<Metadata extends object> {\n  private builder: CookieBuilder<Metadata>;\n  private targetCookies: Cookie[] | null = null;\n\n  constructor(\n    private provider: CookiesProvider,\n    private options: SetAuthCookiesOptions<Metadata>\n  ) {\n    this.builder = CookieBuilderFactory.fromOptions(options);\n  }\n\n  private shouldClearMultipleCookies() {\n    return (\n      !this.options.enableMultipleCookies &&\n      (CookieParserFactory.hasMultipleCookies(\n        this.provider,\n        this.options.cookieName\n      ) ||\n        CookieParserFactory.hasLegacyMultipleCookies(\n          this.provider,\n          this.options.cookieName\n        ))\n    );\n  }\n\n  private shouldClearCustomTokenCookie() {\n    return (\n      !this.options.enableCustomToken &&\n      CookieParserFactory.hasCustomTokenCookie(\n        this.provider,\n        this.options.cookieName\n      )\n    );\n  }\n\n  private shouldClearSingleCookie() {\n    const hasSingleCookie = Boolean(this.provider.get(this.options.cookieName));\n\n    return this.options.enableMultipleCookies && hasSingleCookie;\n  }\n\n  private clearUnusedCookies(setter: CookieSetter) {\n    if (this.shouldClearMultipleCookies()) {\n      const expiration = new MultipleCookieExpiration(\n        this.options.cookieName,\n        setter\n      );\n\n      expiration.expireCookies(this.options.cookieSerializeOptions);\n    } else if (this.shouldClearCustomTokenCookie()) {\n      const expiration = new MultipleCookieExpiration(\n        this.options.cookieName,\n        setter\n      );\n\n      expiration.expireCustomCookie(this.options.cookieSerializeOptions);\n    }\n\n    if (this.shouldClearSingleCookie()) {\n      const expiration = new SingleCookieExpiration(\n        this.options.cookieName,\n        setter\n      );\n\n      expiration.expireCookies(this.options.cookieSerializeOptions);\n    }\n  }\n\n  private async getCookies(value: ParsedCookies<Metadata>): Promise<Cookie[]> {\n    const targetValue = this.options.enableCustomToken\n      ? value\n      : {\n          idToken: value.idToken,\n          refreshToken: value.refreshToken,\n          metadata: value.metadata\n        };\n\n    if (this.targetCookies) {\n      return this.targetCookies;\n    }\n\n    return (this.targetCookies = await this.builder.buildCookies(targetValue));\n  }\n\n  public async setAuthCookies(\n    value: ParsedCookies<Metadata>,\n    requestCookies: RequestCookies | ReadonlyRequestCookies\n  ) {\n    const cookies = await this.getCookies(value);\n    const setter = CookieSetterFactory.fromRequestCookies(requestCookies);\n\n    this.clearUnusedCookies(setter);\n    setter.setCookies(cookies, this.options.cookieSerializeOptions);\n  }\n\n  public async setAuthHeaders(\n    value: ParsedCookies<Metadata>,\n    headers: Headers\n  ) {\n    const cookies = await this.getCookies(value);\n    const setter = CookieSetterFactory.fromHeaders(headers);\n\n    this.clearUnusedCookies(setter);\n    setter.setCookies(cookies, this.options.cookieSerializeOptions);\n  }\n\n  public async setAuthNextApiResponseHeaders(\n    value: ParsedCookies<Metadata>,\n    response: NextApiResponse\n  ) {\n    const cookies = await this.getCookies(value);\n    const setter = new NextApiResponseCookieSetter(response);\n\n    this.clearUnusedCookies(setter);\n    setter.setCookies(cookies, this.options.cookieSerializeOptions);\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/builder/CookieBuilder.ts",
    "content": "import {ParsedCookies} from '../../../auth/custom-token/index.js';\n\nexport interface Cookie {\n  name: string;\n  value: string;\n}\n\nexport interface CookieBuilder<Metadata extends object> {\n  buildCookies(tokens: ParsedCookies<Metadata>): Promise<Cookie[]>;\n}\n"
  },
  {
    "path": "src/next/cookies/builder/CookieBuilderFactory.ts",
    "content": "import {SetAuthCookiesOptions} from '../types.js';\nimport {MultipleCookieBuilder} from './MultipleCookieBuilder.js';\nimport {SingleCookieBuilder} from './SingleCookieBuilder.js';\n\nexport class CookieBuilderFactory {\n  static fromOptions<Metadata extends object>(\n    options: SetAuthCookiesOptions<Metadata>\n  ) {\n    if (options.enableMultipleCookies) {\n      return new MultipleCookieBuilder(\n        options.cookieName,\n        options.cookieSignatureKeys\n      );\n    }\n\n    return new SingleCookieBuilder(\n      options.cookieName,\n      options.cookieSignatureKeys\n    );\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/builder/MultipleCookieBuilder.test.ts",
    "content": "import {MultipleCookieBuilder} from './MultipleCookieBuilder.js';\n\ndescribe('MultipleCookieBuilder', () => {\n  it('should create four cookies, representing all tokens and signature', async () => {\n    const builder = new MultipleCookieBuilder('TestCookie', ['secret']);\n\n    expect(\n      await builder.buildCookies({\n        idToken: 'id-token',\n        refreshToken: 'refresh-token',\n        customToken: 'custom-token',\n        metadata: {}\n      })\n    ).toEqual([\n      {\n        name: 'TestCookie.id',\n        value: 'id-token'\n      },\n      {\n        name: 'TestCookie.refresh',\n        value: 'refresh-token'\n      },\n      {\n        name: 'TestCookie.custom',\n        value: 'custom-token'\n      },\n      {\n        name: 'TestCookie.sig',\n        value: 'QupyAMaPmI6d90CqB0lvec5Q517onmUvXEk6bONTQM0'\n      }\n    ]);\n  });\n\n  it('should create cookies with metadata and signature', async () => {\n    const builder = new MultipleCookieBuilder('TestCookie', ['secret']);\n\n    expect(\n      await builder.buildCookies({\n        idToken: 'id-token',\n        refreshToken: 'refresh-token',\n        customToken: 'custom-token',\n        metadata: {foo: 'bar'}\n      })\n    ).toEqual([\n      {\n        name: 'TestCookie.id',\n        value: 'id-token'\n      },\n      {\n        name: 'TestCookie.refresh',\n        value: 'refresh-token'\n      },\n      {\n        name: 'TestCookie.custom',\n        value: 'custom-token'\n      },\n      {\n        name: 'TestCookie.metadata',\n        value: 'eyJmb28iOiJiYXIifQ'\n      },\n      {\n        name: 'TestCookie.sig',\n        value: '4LS2ty2sdecHjVR9dSSMO8jY0gvITmMgJH1stLFKVlA'\n      }\n    ]);\n  });\n\n  it('should skip custom token if not provided', async () => {\n    const builder = new MultipleCookieBuilder('TestCookie', ['secret']);\n\n    expect(\n      await builder.buildCookies({\n        idToken: 'id-token',\n        refreshToken: 'refresh-token',\n        metadata: {}\n      })\n    ).toEqual([\n      {\n        name: 'TestCookie.id',\n        value: 'id-token'\n      },\n      {\n        name: 'TestCookie.refresh',\n        value: 'refresh-token'\n      },\n      {\n        name: 'TestCookie.sig',\n        value: 'g-7yXxxJfMmzsR7BqkJjguoUWsOqCTGz2AndxjJBrkw'\n      }\n    ]);\n  });\n});\n"
  },
  {
    "path": "src/next/cookies/builder/MultipleCookieBuilder.ts",
    "content": "import {base64url} from 'jose';\nimport {ParsedCookies} from '../../../auth/custom-token/index.js';\nimport {RotatingCredential} from '../../../auth/rotating-credential.js';\nimport {Cookie, CookieBuilder} from './CookieBuilder.js';\n\nexport class MultipleCookieBuilder<Metadata extends object>\n  implements CookieBuilder<Metadata>\n{\n  private credential: RotatingCredential<Metadata>;\n\n  constructor(\n    private cookieName: string,\n    signatureKeys: string[]\n  ) {\n    this.credential = new RotatingCredential(signatureKeys);\n  }\n\n  public async buildCookies(value: ParsedCookies<Metadata>): Promise<Cookie[]> {\n    const signature = await this.credential.createSignature(value);\n\n    const result: Cookie[] = [\n      {\n        name: `${this.cookieName}.id`,\n        value: value.idToken\n      },\n      {\n        name: `${this.cookieName}.refresh`,\n        value: value.refreshToken\n      }\n    ];\n\n    if (value.customToken) {\n      result.push({\n        name: `${this.cookieName}.custom`,\n        value: value.customToken\n      });\n    }\n\n    if (value.metadata && Object.keys(value.metadata).length > 0) {\n      result.push({\n        name: `${this.cookieName}.metadata`,\n        value: base64url.encode(JSON.stringify(value.metadata))\n      });\n    }\n\n    result.push({\n      name: `${this.cookieName}.sig`,\n      value: signature\n    });\n\n    return result;\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/builder/SingleCookieBuilder.test.ts",
    "content": "import {SingleCookieBuilder} from './SingleCookieBuilder.js';\n\ndescribe('SingleCookieBuilder', () => {\n  it('should create a signed jwt token cookie based on all tokens', async () => {\n    const builder = new SingleCookieBuilder('TestCookie', ['secret']);\n\n    expect(\n      await builder.buildCookies({\n        idToken: 'id-token',\n        refreshToken: 'refresh-token',\n        customToken: 'custom-token',\n        metadata: {}\n      })\n    ).toEqual([\n      {\n        name: 'TestCookie',\n        value:\n          'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6ImlkLXRva2VuIiwicmVmcmVzaF90b2tlbiI6InJlZnJlc2gtdG9rZW4iLCJjdXN0b21fdG9rZW4iOiJjdXN0b20tdG9rZW4ifQ.ExxN2rNayg2XCR6WNeZmY8tAyc_qyiZ2YdzITRbQocs'\n      }\n    ]);\n  });\n\n  it('should create a signed jwt token cookie based on all tokens and metadata', async () => {\n    const builder = new SingleCookieBuilder('TestCookie', ['secret']);\n\n    expect(\n      await builder.buildCookies({\n        idToken: 'id-token',\n        refreshToken: 'refresh-token',\n        customToken: 'custom-token',\n        metadata: {foo: 'bar'}\n      })\n    ).toEqual([\n      {\n        name: 'TestCookie',\n        value:\n          'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6ImlkLXRva2VuIiwicmVmcmVzaF90b2tlbiI6InJlZnJlc2gtdG9rZW4iLCJjdXN0b21fdG9rZW4iOiJjdXN0b20tdG9rZW4iLCJtZXRhZGF0YSI6eyJmb28iOiJiYXIifX0.tm6HY-N7NZMq9ipez53--tXfixsHhcF59hj9s13iEII'\n      }\n    ]);\n  });\n});\n"
  },
  {
    "path": "src/next/cookies/builder/SingleCookieBuilder.ts",
    "content": "import {\n  CustomJWTPayload,\n  ParsedCookies\n} from '../../../auth/custom-token/index.js';\nimport {RotatingCredential} from '../../../auth/rotating-credential.js';\nimport {Cookie, CookieBuilder} from './CookieBuilder.js';\n\nexport class SingleCookieBuilder<Metadata extends object>\n  implements CookieBuilder<Metadata>\n{\n  private credential: RotatingCredential<Metadata>;\n\n  constructor(\n    private cookieName: string,\n    signatureKeys: string[]\n  ) {\n    this.credential = new RotatingCredential(signatureKeys);\n  }\n\n  public async buildCookies(value: ParsedCookies<Metadata>): Promise<Cookie[]> {\n    const payload: CustomJWTPayload<Metadata> = {\n      id_token: value.idToken,\n      refresh_token: value.refreshToken,\n      custom_token: value.customToken\n    };\n\n    if (value.metadata && Object.keys(value.metadata).length > 0) {\n      payload['metadata'] = value.metadata;\n    }\n\n    const jwtToken = await this.credential.sign(payload);\n\n    return [\n      {\n        name: this.cookieName,\n        value: jwtToken\n      }\n    ];\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/expiration/CombinedCookieExpiration.ts",
    "content": "import type {CookieSerializeOptions} from 'cookie';\nimport {CookieExpiration} from './CookieExpiration.js';\nimport {MultipleCookieExpiration} from './MultipleCookieExpiration.js';\nimport {SingleCookieExpiration} from './SingleCookieExpiration.js';\n\nexport class CombinedCookieExpiration implements CookieExpiration {\n  constructor(\n    private multi: MultipleCookieExpiration,\n    private single: SingleCookieExpiration\n  ) {}\n\n  expireCookies(options: CookieSerializeOptions): void {\n    this.multi.expireCookies(options);\n    this.single.expireCookies(options);\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/expiration/CookieExpiration.ts",
    "content": "import type {CookieSerializeOptions} from 'cookie';\n\nexport interface CookieExpiration {\n  expireCookies(options: CookieSerializeOptions): void;\n}\n\nexport function getExpiredSerializeOptions(options: CookieSerializeOptions) {\n  const cookieOptions = {\n    ...options,\n    expires: new Date(0)\n  };\n  delete cookieOptions['maxAge'];\n\n  return cookieOptions;\n}\n"
  },
  {
    "path": "src/next/cookies/expiration/CookieExpirationFactory.test.ts",
    "content": "import {Cookie} from '../builder/CookieBuilder.js';\nimport {RequestCookiesProvider} from '../parser/RequestCookiesProvider.js';\nimport {CookieExpirationFactory} from './CookieExpirationFactory.js';\nimport type {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\n\nconst cookieName = 'TestCookie';\nconst cookieSerializeOptions = {\n  path: '/',\n  httpOnly: true,\n  secure: true,\n  sameSite: 'lax' as const,\n  maxAge: 12 * 60 * 60 * 24,\n  expires: new Date(1727373870 * 1000)\n};\n\nconst testCookies: Cookie[] = [\n  {\n    name: 'TestCookie.id',\n    value: 'id-token'\n  },\n  {\n    name: 'TestCookie.refresh',\n    value: 'refresh-token'\n  },\n  {\n    name: 'TestCookie.custom',\n    value: 'custom-token'\n  },\n  {\n    name: 'TestCookie.sig',\n    value: 'QupyAMaPmI6d90CqB0lvec5Q517onmUvXEk6bONTQM0'\n  }\n];\n\nconst legacyTestCookies: Cookie[] = [\n  {\n    name: 'TestCookie',\n    value: 'id-token:refresh-token'\n  },\n  {\n    name: 'TestCookie.custom',\n    value: 'custom-token'\n  },\n  {\n    name: 'TestCookie.sig',\n    value: 'QupyAMaPmI6d90CqB0lvec5Q517onmUvXEk6bONTQM0'\n  }\n];\n\nfunction getTestCookie(name: string) {\n  return testCookies.find((it) => it.name === name);\n}\n\nfunction getLegacyTestCookie(name: string) {\n  return legacyTestCookies.find((it) => it.name === name);\n}\n\nfunction getSingleCookie(name: string) {\n  if (name === cookieName) {\n    return {\n      name: cookieName,\n      value: 'single-cookie'\n    };\n  }\n\n  return undefined;\n}\n\ndescribe('CookieExpirationFactory', () => {\n  it('should remove a single cookie', () => {\n    const headers = {append: jest.fn()} as unknown as Headers;\n    const cookies = {get: jest.fn()} as unknown as RequestCookies;\n\n    const expiration = CookieExpirationFactory.fromHeaders(\n      headers,\n      new RequestCookiesProvider(cookies),\n      cookieName\n    );\n\n    expiration.expireCookies(cookieSerializeOptions);\n\n    expect(headers.append).toHaveBeenCalledTimes(1);\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n  });\n\n  it('should remove multiple cookies', () => {\n    const headers = {append: jest.fn()} as unknown as Headers;\n    const cookies = {get: jest.fn(getTestCookie)} as unknown as RequestCookies;\n\n    const expiration = CookieExpirationFactory.fromHeaders(\n      headers,\n      new RequestCookiesProvider(cookies),\n      cookieName\n    );\n\n    expiration.expireCookies(cookieSerializeOptions);\n\n    expect(headers.append).toHaveBeenCalledTimes(5);\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie.id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie.refresh=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie.custom=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie.metadata=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie.sig=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n  });\n\n  it('should remove multiple and single cookies when there are both', () => {\n    const headers = {append: jest.fn()} as unknown as Headers;\n    const cookies = {\n      get: jest.fn((name) => {\n        return getSingleCookie(name) ?? getTestCookie(name);\n      })\n    } as unknown as RequestCookies;\n\n    const expiration = CookieExpirationFactory.fromHeaders(\n      headers,\n      new RequestCookiesProvider(cookies),\n      cookieName\n    );\n\n    expiration.expireCookies(cookieSerializeOptions);\n\n    expect(headers.append).toHaveBeenCalledTimes(6);\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie.id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie.refresh=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie.custom=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie.metadata=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie.sig=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n  });\n\n  it('should remove multiple and single cookies when there are legacy cookies', () => {\n    const headers = {append: jest.fn()} as unknown as Headers;\n    const cookies = {\n      get: jest.fn((name) => {\n        return getLegacyTestCookie(name);\n      })\n    } as unknown as RequestCookies;\n\n    const expiration = CookieExpirationFactory.fromHeaders(\n      headers,\n      new RequestCookiesProvider(cookies),\n      cookieName\n    );\n\n    expiration.expireCookies(cookieSerializeOptions);\n\n    expect(headers.append).toHaveBeenCalledTimes(6);\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie.id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie.refresh=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie.custom=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie.metadata=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n    expect(headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      'TestCookie.sig=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax'\n    );\n  });\n});\n"
  },
  {
    "path": "src/next/cookies/expiration/CookieExpirationFactory.ts",
    "content": "import {CookieParserFactory} from '../parser/CookieParserFactory.js';\nimport {CookiesProvider} from '../parser/CookiesProvider.js';\nimport {CookieSetter} from '../setter/CookieSetter.js';\nimport {HeadersCookieSetter} from '../setter/HeadersCookieSetter.js';\nimport {CombinedCookieExpiration} from './CombinedCookieExpiration.js';\nimport {MultipleCookieExpiration} from './MultipleCookieExpiration.js';\nimport {SingleCookieExpiration} from './SingleCookieExpiration.js';\n\nexport class CookieExpirationFactory {\n  private static fromSetter(\n    setter: CookieSetter,\n    provider: CookiesProvider,\n    cookieName: string\n  ) {\n    const singleCookie = provider.get(cookieName);\n    const hasEnabledMultipleCookies = CookieParserFactory.hasMultipleCookies(\n      provider,\n      cookieName\n    );\n    const hasEnabledLegacyMultipleCookies =\n      CookieParserFactory.hasLegacyMultipleCookies(provider, cookieName);\n\n    if (\n      singleCookie &&\n      (hasEnabledMultipleCookies || hasEnabledLegacyMultipleCookies)\n    ) {\n      return new CombinedCookieExpiration(\n        new MultipleCookieExpiration(cookieName, setter),\n        new SingleCookieExpiration(cookieName, setter)\n      );\n    }\n\n    if (hasEnabledMultipleCookies) {\n      return new MultipleCookieExpiration(cookieName, setter);\n    }\n\n    return new SingleCookieExpiration(cookieName, setter);\n  }\n\n  static fromHeaders(\n    headers: Headers,\n    provider: CookiesProvider,\n    cookieName: string\n  ) {\n    const setter = new HeadersCookieSetter(headers);\n\n    return CookieExpirationFactory.fromSetter(setter, provider, cookieName);\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/expiration/MultipleCookieExpiration.test.ts",
    "content": "import {CookieSetter} from '../setter/CookieSetter.js';\nimport {MultipleCookieExpiration} from './MultipleCookieExpiration.js';\n\nconst mockSetter: CookieSetter = {\n  setCookies: jest.fn()\n};\n\nconst cookieSerializeOptions = {\n  path: '/',\n  httpOnly: true,\n  secure: true,\n  sameSite: 'lax' as const,\n  maxAge: 12 * 60 * 60 * 24,\n  expires: new Date(1727373870 * 1000)\n};\n\ndescribe('MultipleCookieExpiration', () => {\n  beforeEach(() => {\n    jest.resetAllMocks();\n  });\n\n  it('should remove multiple cookies', () => {\n    const expiration = new MultipleCookieExpiration('TestCookie', mockSetter);\n\n    expiration.expireCookies(cookieSerializeOptions);\n\n    expect(mockSetter.setCookies).toHaveBeenCalledWith(\n      [\n        {name: 'TestCookie.id', value: ''},\n        {name: 'TestCookie.refresh', value: ''},\n        {name: 'TestCookie.custom', value: ''},\n        {name: 'TestCookie.metadata', value: ''},\n        {name: 'TestCookie.sig', value: ''}\n      ],\n      {\n        path: '/',\n        httpOnly: true,\n        secure: true,\n        sameSite: 'lax' as const,\n        expires: new Date(0)\n      }\n    );\n  });\n\n  it('should remove custom cookie', () => {\n    const expiration = new MultipleCookieExpiration('TestCookie', mockSetter);\n\n    expiration.expireCustomCookie(cookieSerializeOptions);\n\n    expect(mockSetter.setCookies).toHaveBeenCalledWith(\n      [{name: 'TestCookie.custom', value: ''}],\n      {\n        path: '/',\n        httpOnly: true,\n        secure: true,\n        sameSite: 'lax' as const,\n        expires: new Date(0)\n      }\n    );\n  });\n});\n"
  },
  {
    "path": "src/next/cookies/expiration/MultipleCookieExpiration.ts",
    "content": "import type {CookieSerializeOptions} from 'cookie';\nimport {Cookie} from '../builder/CookieBuilder.js';\nimport {CookieSetter} from '../setter/CookieSetter.js';\nimport {\n  CookieExpiration,\n  getExpiredSerializeOptions\n} from './CookieExpiration.js';\n\nexport class MultipleCookieExpiration implements CookieExpiration {\n  public constructor(\n    private cookieName: string,\n    private setter: CookieSetter\n  ) {}\n\n  expireCustomCookie(options: CookieSerializeOptions) {\n    const cookies: Cookie[] = [\n      {\n        name: `${this.cookieName}.custom`,\n        value: ''\n      }\n    ];\n\n    this.setter.setCookies(cookies, getExpiredSerializeOptions(options));\n  }\n\n  expireCookies(options: CookieSerializeOptions): void {\n    const cookies: Cookie[] = [\n      {\n        name: `${this.cookieName}.id`,\n        value: ''\n      },\n      {\n        name: `${this.cookieName}.refresh`,\n        value: ''\n      },\n      {\n        name: `${this.cookieName}.custom`,\n        value: ''\n      },\n      {\n        name: `${this.cookieName}.metadata`,\n        value: ''\n      },\n      {\n        name: `${this.cookieName}.sig`,\n        value: ''\n      }\n    ];\n\n    this.setter.setCookies(cookies, getExpiredSerializeOptions(options));\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/expiration/SingleCookieExpiration.test.ts",
    "content": "import {CookieSetter} from '../setter/CookieSetter.js';\nimport {SingleCookieExpiration} from './SingleCookieExpiration.js';\n\nconst mockSetter: CookieSetter = {\n  setCookies: jest.fn()\n};\n\nconst cookieSerializeOptions = {\n  path: '/',\n  httpOnly: true,\n  secure: true,\n  sameSite: 'lax' as const,\n  maxAge: 12 * 60 * 60 * 24,\n  expires: new Date(1727373870 * 1000)\n};\n\ndescribe('SingleCookieExpiration', () => {\n  beforeEach(() => {\n    jest.resetAllMocks();\n  });\n\n  it('should remove single cookie', () => {\n    const expiration = new SingleCookieExpiration('TestCookie', mockSetter);\n\n    expiration.expireCookies(cookieSerializeOptions);\n\n    expect(mockSetter.setCookies).toHaveBeenCalledWith(\n      [{name: 'TestCookie', value: ''}],\n      {\n        path: '/',\n        httpOnly: true,\n        secure: true,\n        sameSite: 'lax' as const,\n        expires: new Date(0)\n      }\n    );\n  });\n});\n"
  },
  {
    "path": "src/next/cookies/expiration/SingleCookieExpiration.ts",
    "content": "import type {CookieSerializeOptions} from 'cookie';\nimport {Cookie} from '../builder/CookieBuilder.js';\nimport {CookieSetter} from '../setter/CookieSetter.js';\nimport {\n  CookieExpiration,\n  getExpiredSerializeOptions\n} from './CookieExpiration.js';\n\nexport class SingleCookieExpiration implements CookieExpiration {\n  public constructor(\n    private cookieName: string,\n    private setter: CookieSetter\n  ) {}\n\n  expireCookies(options: CookieSerializeOptions): void {\n    const cookies: Cookie[] = [\n      {\n        name: this.cookieName,\n        value: ''\n      }\n    ];\n\n    this.setter.setCookies(cookies, getExpiredSerializeOptions(options));\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/index.test.ts",
    "content": "import type {NextRequest} from 'next/server';\nimport {NextResponse} from 'next/server';\nimport {\n  SetAuthCookiesOptions,\n  appendAuthCookies,\n  refreshCredentials,\n  setAuthCookies\n} from '../cookies/index.js';\n\n// Suppress \"Property 'headers' does not exist on type NextRequest/NextResponse\" error\ndeclare module 'next/server' {\n  export interface NextRequest {\n    headers: Headers;\n  }\n\n  export interface NextResponse {\n    headers: Headers;\n  }\n}\n\njest.mock('../../auth/index.js', () => ({\n  getFirebaseAuth: () => ({\n    handleTokenRefresh: () => ({\n      idToken: 'TEST_ID_TOKEN',\n      refreshToken: 'TEST_REFRESH_TOKEN',\n      customToken: 'TEST_CUSTOM_TOKEN'\n    })\n  })\n}));\n\nconst secret = 'very-secure-secret';\nconst jwt =\n  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6Ik1PQ0tfSURfVE9LRU4iLCJjdXN0b21fdG9rZW4iOiJNT0NLX0NVU1RPTV9UT0tFTiIsInJlZnJlc2hfdG9rZW4iOiJNT0NLX1JFRlJFU0hfVE9LRU4ifQ.K5jwTcAlfffzuM2_WaKJ93QwgqeCpWjg7TMx1lulSO4';\nconst refreshedJwt =\n  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6IlRFU1RfSURfVE9LRU4iLCJyZWZyZXNoX3Rva2VuIjoiVEVTVF9SRUZSRVNIX1RPS0VOIiwiY3VzdG9tX3Rva2VuIjoiVEVTVF9DVVNUT01fVE9LRU4ifQ.2tjn-__AKP3J7w9vIDuFDFkYmPzpuGpWvHvBFksMh5E';\n\nconst MOCK_OPTIONS: SetAuthCookiesOptions<never> = {\n  cookieName: 'TestCookie',\n  cookieSignatureKeys: [secret],\n  cookieSerializeOptions: {maxAge: 123, path: '/test-path', sameSite: 'lax'},\n  apiKey: 'API_KEY',\n  authorizationHeaderName: 'Next-Authorization',\n  enableCustomToken: true\n};\n\ndescribe('cookies', () => {\n  let MOCK_REQUEST: jest.Mocked<NextRequest>;\n\n  beforeEach(() => {\n    const mockHeaders = new Headers();\n    mockHeaders.set('Cookie', `TestCookie=${jwt}`);\n    MOCK_REQUEST = {\n      cookies: {\n        has: (key: string) => {\n          if (key === 'TestCookie') {\n            return true;\n          }\n\n          return false;\n        },\n        get: jest.fn((key: string) => {\n          if (key === 'TestCookie') {\n            return {value: jwt};\n          }\n\n          return undefined;\n        }),\n        set: jest.fn(),\n        delete: jest.fn()\n      },\n      headers: mockHeaders\n    } as unknown as jest.Mocked<NextRequest>;\n  });\n\n  const customValue = {\n    idToken: 'MOCK_ID_TOKEN',\n    refreshToken: 'MOCK_REFRESH_TOKEN',\n    customToken: 'MOCK_CUSTOM_TOKEN',\n    metadata: {}\n  };\n\n  it('appends fresh cookie headers to the response', async () => {\n    const MOCK_RESPONSE = {\n      headers: {\n        append: jest.fn()\n      }\n    } as unknown as jest.Mocked<NextResponse>;\n    const result = await refreshCredentials(\n      MOCK_REQUEST,\n      MOCK_OPTIONS,\n      () => MOCK_RESPONSE\n    );\n\n    expect(MOCK_REQUEST.cookies.set).toHaveBeenCalledWith(\n      'TestCookie',\n      refreshedJwt,\n      {maxAge: 123, path: '/test-path', sameSite: 'lax'}\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      `TestCookie=${refreshedJwt}; Max-Age=123; Path=/test-path; SameSite=Lax`\n    );\n\n    expect(result).toBe(MOCK_RESPONSE);\n  });\n\n  it('appends fresh cookie headers without custom token to the response', async () => {\n    const MOCK_RESPONSE = {\n      headers: {\n        append: jest.fn()\n      }\n    } as unknown as jest.Mocked<NextResponse>;\n    const result = await refreshCredentials(\n      MOCK_REQUEST,\n      {...MOCK_OPTIONS, enableCustomToken: false},\n      () => MOCK_RESPONSE\n    );\n\n    expect(MOCK_REQUEST.cookies.set).toHaveBeenCalledWith(\n      'TestCookie',\n      'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6IlRFU1RfSURfVE9LRU4iLCJyZWZyZXNoX3Rva2VuIjoiVEVTVF9SRUZSRVNIX1RPS0VOIn0.Na_3Et62K3bs5WcTnvh6sEW_pnoiFw022gKXDCkuW-s',\n      {maxAge: 123, path: '/test-path', sameSite: 'lax'}\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      `TestCookie=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6IlRFU1RfSURfVE9LRU4iLCJyZWZyZXNoX3Rva2VuIjoiVEVTVF9SRUZSRVNIX1RPS0VOIn0.Na_3Et62K3bs5WcTnvh6sEW_pnoiFw022gKXDCkuW-s; Max-Age=123; Path=/test-path; SameSite=Lax`\n    );\n\n    expect(result).toBe(MOCK_RESPONSE);\n  });\n\n  it('accepts async response factory function', async () => {\n    const MOCK_RESPONSE = {\n      headers: {\n        append: jest.fn()\n      }\n    } as unknown as jest.Mocked<NextResponse>;\n    const result = await refreshCredentials(MOCK_REQUEST, MOCK_OPTIONS, () =>\n      Promise.resolve(MOCK_RESPONSE)\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenCalledWith(\n      'Set-Cookie',\n      `TestCookie=${refreshedJwt}; Max-Age=123; Path=/test-path; SameSite=Lax`\n    );\n\n    expect(result).toBe(MOCK_RESPONSE);\n  });\n\n  it('generates multiple cookies', async () => {\n    const MOCK_RESPONSE = {\n      headers: {\n        append: jest.fn()\n      }\n    } as unknown as jest.Mocked<NextResponse>;\n    const result = await refreshCredentials(\n      MOCK_REQUEST,\n      {...MOCK_OPTIONS, enableMultipleCookies: true},\n      () => Promise.resolve(MOCK_RESPONSE)\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      1,\n      'Set-Cookie',\n      'TestCookie=; Path=/test-path; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      2,\n      'Set-Cookie',\n      'TestCookie.id=TEST_ID_TOKEN; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      3,\n      'Set-Cookie',\n      'TestCookie.refresh=TEST_REFRESH_TOKEN; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      4,\n      'Set-Cookie',\n      'TestCookie.custom=TEST_CUSTOM_TOKEN; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      5,\n      'Set-Cookie',\n      'TestCookie.sig=MqBNRBcWwj7xL948-Yy89kj5dwPEf7fTACNx93rOFX4; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(result).toBe(MOCK_RESPONSE);\n  });\n\n  it('generates multiple cookies with metadata', async () => {\n    const MOCK_RESPONSE = {\n      headers: {\n        append: jest.fn()\n      }\n    } as unknown as jest.Mocked<NextResponse>;\n    const result = await refreshCredentials(\n      MOCK_REQUEST,\n      {\n        ...MOCK_OPTIONS,\n        enableMultipleCookies: true,\n        getMetadata: () => Promise.resolve({foo: 'bar'})\n      },\n      () => Promise.resolve(MOCK_RESPONSE)\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      1,\n      'Set-Cookie',\n      'TestCookie=; Path=/test-path; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      2,\n      'Set-Cookie',\n      'TestCookie.id=TEST_ID_TOKEN; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      3,\n      'Set-Cookie',\n      'TestCookie.refresh=TEST_REFRESH_TOKEN; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      4,\n      'Set-Cookie',\n      'TestCookie.custom=TEST_CUSTOM_TOKEN; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      5,\n      'Set-Cookie',\n      'TestCookie.metadata=eyJmb28iOiJiYXIifQ; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      6,\n      'Set-Cookie',\n      'TestCookie.sig=YICw1pt9h0SbHDrQOZYCoyapBd5Y5haBs7nnXDl4kGE; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(result).toBe(MOCK_RESPONSE);\n  });\n\n  it('appends multiple cookie headers', async () => {\n    const MOCK_RESPONSE = {\n      headers: {\n        append: jest.fn()\n      }\n    } as unknown as jest.Mocked<NextResponse>;\n    const mockHeaders = new Headers();\n    await appendAuthCookies(mockHeaders, MOCK_RESPONSE, customValue, {\n      ...MOCK_OPTIONS,\n      enableMultipleCookies: true\n    });\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      1,\n      'Set-Cookie',\n      'TestCookie.id=MOCK_ID_TOKEN; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      2,\n      'Set-Cookie',\n      'TestCookie.refresh=MOCK_REFRESH_TOKEN; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      3,\n      'Set-Cookie',\n      'TestCookie.custom=MOCK_CUSTOM_TOKEN; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      4,\n      'Set-Cookie',\n      'TestCookie.sig=AuSOlUSJENTLtShQpjf7SMRiPY4aILyFNmjr7Tc3Fig; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n  });\n\n  it('appends multiple cookie headers with metadata', async () => {\n    const MOCK_RESPONSE = {\n      headers: {\n        append: jest.fn()\n      }\n    } as unknown as jest.Mocked<NextResponse>;\n    const mockHeaders = new Headers();\n    await appendAuthCookies(\n      mockHeaders,\n      MOCK_RESPONSE,\n      {...customValue, metadata: {foo: 'bar'}},\n      {\n        ...MOCK_OPTIONS,\n        enableMultipleCookies: true\n      }\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      1,\n      'Set-Cookie',\n      'TestCookie.id=MOCK_ID_TOKEN; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      2,\n      'Set-Cookie',\n      'TestCookie.refresh=MOCK_REFRESH_TOKEN; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      3,\n      'Set-Cookie',\n      'TestCookie.custom=MOCK_CUSTOM_TOKEN; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      4,\n      'Set-Cookie',\n      'TestCookie.metadata=eyJmb28iOiJiYXIifQ; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      5,\n      'Set-Cookie',\n      'TestCookie.sig=1jhc7bzIu7TiqZh5X55nJ-_ZfSWhTb7Ui8W7SdGqttA; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n  });\n\n  it('skips custom token in multiple cookie headers', async () => {\n    const MOCK_RESPONSE = {\n      headers: {\n        append: jest.fn()\n      }\n    } as unknown as jest.Mocked<NextResponse>;\n    const mockHeaders = new Headers();\n    await appendAuthCookies(mockHeaders, MOCK_RESPONSE, customValue, {\n      ...MOCK_OPTIONS,\n      enableMultipleCookies: true,\n      enableCustomToken: false\n    });\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      1,\n      'Set-Cookie',\n      'TestCookie.id=MOCK_ID_TOKEN; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      2,\n      'Set-Cookie',\n      'TestCookie.refresh=MOCK_REFRESH_TOKEN; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n\n    expect(MOCK_RESPONSE.headers.append).toHaveBeenNthCalledWith(\n      3,\n      'Set-Cookie',\n      'TestCookie.sig=kD-gd5CZhwndsyIvECTkfFsumUjj5UE1UpuxlxX5HWk; Max-Age=123; Path=/test-path; SameSite=Lax'\n    );\n  });\n\n  it('appends custom auth headers', async () => {\n    const MOCK_RESPONSE = {\n      headers: {\n        append: jest.fn(),\n        get: jest.fn()\n      }\n    } as unknown as jest.Mocked<NextResponse>;\n\n    await setAuthCookies(MOCK_RESPONSE.headers, MOCK_OPTIONS);\n\n    expect(MOCK_RESPONSE.headers.get).toHaveBeenCalledWith(\n      'Next-Authorization'\n    );\n  });\n});\n"
  },
  {
    "path": "src/next/cookies/index.ts",
    "content": "import {type CookieSerializeOptions} from 'cookie';\nimport type {IncomingHttpHeaders} from 'http';\nimport type {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies';\nimport type {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\nimport type {NextRequest} from 'next/server';\nimport {NextResponse} from 'next/server';\nimport {ParsedCookies, VerifiedCookies} from '../../auth/custom-token/index.js';\nimport {getFirebaseAuth, TokenSet} from '../../auth/index.js';\nimport {debug} from '../../debug/index.js';\nimport {getCookiesTokens, getRequestCookiesTokens} from '../tokens.js';\nimport {getReferer} from '../utils.js';\nimport {AuthCookies} from './AuthCookies.js';\nimport {RequestCookiesProvider} from './parser/RequestCookiesProvider.js';\nimport {CookieExpirationFactory} from './expiration/CookieExpirationFactory.js';\nimport {CookiesObject, SetAuthCookiesOptions} from './types.js';\nimport {CookieRemoverFactory} from './remover/CookieRemoverFactory.js';\nimport {getMetadataInternal} from '../metadata.js';\nimport {mapJwtPayloadToDecodedIdToken} from '../../auth/utils.js';\nimport {decodeJwt} from 'jose';\n\nexport async function appendAuthCookies<Metadata extends object>(\n  headers: Headers,\n  response: NextResponse,\n  value: ParsedCookies<Metadata>,\n  options: SetAuthCookiesOptions<Metadata>\n) {\n  debug('Updating response headers with authenticated cookies');\n\n  const authCookies = new AuthCookies(\n    RequestCookiesProvider.fromHeaders(headers),\n    options\n  );\n\n  await authCookies.setAuthHeaders(value, response.headers);\n}\n\nexport async function setAuthCookies<Metadata extends object>(\n  headers: Headers,\n  options: SetAuthCookiesOptions<Metadata>\n): Promise<NextResponse> {\n  const {getCustomIdAndRefreshTokens} = getFirebaseAuth({\n    serviceAccount: options.serviceAccount,\n    apiKey: options.apiKey,\n    tenantId: options.tenantId\n  });\n  const authHeader = options.authorizationHeaderName ?? 'Authorization';\n  const token = headers.get(authHeader)?.split(' ')[1] ?? '';\n\n  if (!token) {\n    const response = new NextResponse(\n      JSON.stringify({success: false, message: 'Missing token'}),\n      {\n        status: 400,\n        headers: {'content-type': 'application/json'}\n      }\n    );\n\n    return response;\n  }\n\n  const appCheckToken = headers.get('X-Firebase-AppCheck') ?? undefined;\n  const referer = getReferer(headers) ?? '';\n\n  const customTokens = await getCustomIdAndRefreshTokens(token, {\n    appCheckToken,\n    referer,\n    dynamicCustomClaimsKeys: options.dynamicCustomClaimsKeys\n  });\n\n  debug('Successfully generated custom tokens');\n\n  const decodedIdToken = mapJwtPayloadToDecodedIdToken(\n    decodeJwt(customTokens.idToken)\n  );\n  const metadata = await getMetadataInternal(\n    {...customTokens, decodedIdToken},\n    options\n  );\n\n  const response = new NextResponse(JSON.stringify({success: true}), {\n    status: 200,\n    headers: {'content-type': 'application/json'}\n  });\n\n  await appendAuthCookies(\n    headers,\n    response,\n    {...customTokens, metadata},\n    options\n  );\n\n  return response;\n}\n\nexport interface RemoveServerCookiesOptions {\n  cookieName: string;\n}\nexport function removeServerCookies(\n  cookies: RequestCookies | ReadonlyRequestCookies,\n  options: RemoveServerCookiesOptions\n) {\n  const remover = CookieRemoverFactory.fromRequestCookies(\n    cookies,\n    RequestCookiesProvider.fromRequestCookies(cookies),\n    options.cookieName\n  );\n\n  return remover.removeCookies();\n}\n\nexport interface RemoveAuthCookiesOptions {\n  cookieName: string;\n  cookieSerializeOptions: CookieSerializeOptions;\n}\n\nexport function removeCookies(\n  headers: Headers,\n  response: NextResponse,\n  options: RemoveAuthCookiesOptions\n) {\n  const expiration = CookieExpirationFactory.fromHeaders(\n    response.headers,\n    RequestCookiesProvider.fromHeaders(headers),\n    options.cookieName\n  );\n\n  return expiration.expireCookies(options.cookieSerializeOptions);\n}\n\nexport function removeAuthCookies(\n  headers: Headers,\n  options: RemoveAuthCookiesOptions\n): NextResponse {\n  const response = new NextResponse(JSON.stringify({success: true}), {\n    status: 200,\n    headers: {'content-type': 'application/json'}\n  });\n\n  removeCookies(headers, response, options);\n\n  debug('Updating response with empty authentication cookie headers', {\n    cookieName: options.cookieName\n  });\n\n  return response;\n}\n\nexport async function verifyApiCookies<Metadata extends object>(\n  cookies: CookiesObject,\n  headers: IncomingHttpHeaders,\n  options: SetAuthCookiesOptions<Metadata>\n): Promise<VerifiedCookies<Metadata>> {\n  const tokens = await getCookiesTokens(cookies, options);\n  const {verifyAndRefreshExpiredIdToken} = getFirebaseAuth({\n    serviceAccount: options.serviceAccount,\n    apiKey: options.apiKey,\n    tenantId: options.tenantId\n  });\n\n  const verifyTokenResult = await verifyAndRefreshExpiredIdToken(tokens, {\n    referer: headers.referer ?? ''\n  });\n\n  const metadata = await getMetadataInternal<Metadata>(\n    verifyTokenResult,\n    options\n  );\n\n  return {...verifyTokenResult, metadata};\n}\n\nexport async function verifyNextCookies<Metadata extends object>(\n  cookies: RequestCookies | ReadonlyRequestCookies,\n  headers: Headers,\n  options: SetAuthCookiesOptions<Metadata>\n): Promise<VerifiedCookies<Metadata>> {\n  const {verifyAndRefreshExpiredIdToken} = getFirebaseAuth({\n    serviceAccount: options.serviceAccount,\n    apiKey: options.apiKey,\n    tenantId: options.tenantId,\n    enableCustomToken: options.enableCustomToken\n  });\n  const referer = getReferer(headers) ?? '';\n  const tokens = await getRequestCookiesTokens(cookies, options);\n  const verifyTokenResult = await verifyAndRefreshExpiredIdToken(tokens, {\n    referer,\n    enableTokenRefreshOnExpiredKidHeader:\n      options.enableTokenRefreshOnExpiredKidHeader\n  });\n\n  const metadata = await getMetadataInternal<Metadata>(\n    verifyTokenResult,\n    options\n  );\n\n  return {...verifyTokenResult, metadata};\n}\n\nexport async function refreshNextCookies<Metadata extends object>(\n  cookies: RequestCookies | ReadonlyRequestCookies,\n  headers: Headers,\n  options: SetAuthCookiesOptions<Metadata>\n): Promise<VerifiedCookies<Metadata>> {\n  const {handleTokenRefresh} = getFirebaseAuth({\n    serviceAccount: options.serviceAccount,\n    apiKey: options.apiKey,\n    tenantId: options.tenantId\n  });\n  const referer = getReferer(headers) ?? '';\n  const tokens = await getRequestCookiesTokens(cookies, options);\n  const tokenRefreshResult = await handleTokenRefresh(tokens.refreshToken, {\n    referer,\n    enableCustomToken: options.enableCustomToken\n  });\n\n  const metadata = await getMetadataInternal<Metadata>(\n    tokenRefreshResult,\n    options\n  );\n\n  return {\n    idToken: tokenRefreshResult.idToken,\n    refreshToken: tokenRefreshResult.refreshToken,\n    customToken: tokenRefreshResult.customToken,\n    decodedIdToken: tokenRefreshResult.decodedIdToken,\n    metadata\n  };\n}\n\nexport async function refreshCredentials<Metadata extends object>(\n  request: NextRequest,\n  options: SetAuthCookiesOptions<Metadata>,\n  responseFactory: (options: {\n    headers: Headers;\n    tokens: TokenSet;\n    metadata: Metadata;\n  }) => NextResponse | Promise<NextResponse>\n): Promise<NextResponse> {\n  const value = await refreshNextCookies(\n    request.cookies,\n    request.headers,\n    options\n  );\n\n  const cookies = new AuthCookies(\n    RequestCookiesProvider.fromHeaders(request.headers),\n    options\n  );\n\n  await cookies.setAuthCookies(value, request.cookies);\n\n  const responseOrPromise = responseFactory({\n    headers: request.headers,\n    tokens: {\n      idToken: value.idToken,\n      decodedIdToken: value.decodedIdToken,\n      refreshToken: value.refreshToken,\n      customToken: value.customToken\n    },\n    metadata: value.metadata\n  });\n\n  const response =\n    responseOrPromise instanceof Promise\n      ? await responseOrPromise\n      : responseOrPromise;\n\n  await cookies.setAuthHeaders(value, response.headers);\n\n  return response;\n}\n\nexport async function refreshNextResponseCookiesWithToken<\n  Metadata extends object\n>(\n  idToken: string,\n  request: NextRequest,\n  response: NextResponse,\n  options: SetAuthCookiesOptions<Metadata>\n): Promise<NextResponse> {\n  const appCheckToken = request.headers.get('X-Firebase-AppCheck') ?? undefined;\n  const referer = getReferer(request.headers) ?? '';\n\n  const {getCustomIdAndRefreshTokens} = getFirebaseAuth({\n    serviceAccount: options.serviceAccount,\n    apiKey: options.apiKey,\n    tenantId: options.tenantId\n  });\n\n  const customTokens = await getCustomIdAndRefreshTokens(idToken, {\n    appCheckToken,\n    referer,\n    dynamicCustomClaimsKeys: options.dynamicCustomClaimsKeys\n  });\n\n  const decodedIdToken = mapJwtPayloadToDecodedIdToken(\n    decodeJwt(customTokens.idToken)\n  );\n  const metadata = await getMetadataInternal<Metadata>(\n    {...customTokens, decodedIdToken},\n    options\n  );\n\n  await appendAuthCookies(\n    request.headers,\n    response,\n    {...customTokens, metadata},\n    options\n  );\n\n  return response;\n}\n\nexport async function refreshCookiesWithIdToken<Metadata extends object>(\n  idToken: string,\n  headers: Headers,\n  cookies: RequestCookies | ReadonlyRequestCookies,\n  options: SetAuthCookiesOptions<Metadata>\n): Promise<void> {\n  const appCheckToken = headers.get('X-Firebase-AppCheck') ?? undefined;\n  const referer = getReferer(headers) ?? '';\n\n  const {getCustomIdAndRefreshTokens} = getFirebaseAuth({\n    serviceAccount: options.serviceAccount,\n    apiKey: options.apiKey,\n    tenantId: options.tenantId\n  });\n\n  const customTokens = await getCustomIdAndRefreshTokens(idToken, {\n    appCheckToken,\n    referer,\n    dynamicCustomClaimsKeys: options.dynamicCustomClaimsKeys\n  });\n\n  const decodedIdToken = mapJwtPayloadToDecodedIdToken(\n    decodeJwt(customTokens.idToken)\n  );\n\n  const metadata = await getMetadataInternal<Metadata>(\n    {...customTokens, decodedIdToken},\n    options\n  );\n\n  const authCookies = new AuthCookies(\n    RequestCookiesProvider.fromHeaders(headers),\n    options\n  );\n\n  await authCookies.setAuthCookies({...customTokens, metadata}, cookies);\n}\n\nexport async function refreshNextResponseCookies<Metadata extends object>(\n  request: NextRequest,\n  response: NextResponse,\n  options: SetAuthCookiesOptions<Metadata>\n): Promise<NextResponse> {\n  const customTokens = await refreshNextCookies(\n    request.cookies,\n    request.headers,\n    options\n  );\n\n  await appendAuthCookies(request.headers, response, customTokens, options);\n\n  return response;\n}\n\nexport async function refreshServerCookies<Metadata extends object>(\n  cookies: RequestCookies | ReadonlyRequestCookies,\n  headers: Headers,\n  options: SetAuthCookiesOptions<Metadata>\n): Promise<void> {\n  const customTokens = await refreshNextCookies(cookies, headers, options);\n  const authCookies = new AuthCookies(\n    RequestCookiesProvider.fromHeaders(headers),\n    options\n  );\n\n  await authCookies.setAuthCookies(customTokens, cookies);\n  await authCookies.setAuthHeaders(customTokens, headers);\n}\n\nexport * from './types.js';\n"
  },
  {
    "path": "src/next/cookies/parser/CookieParser.ts",
    "content": "import {ParsedCookies} from '../../../auth/custom-token/index.js';\n\nexport interface CookieParser<Metadata extends object> {\n  parseCookies(): Promise<ParsedCookies<Metadata>>;\n}\n"
  },
  {
    "path": "src/next/cookies/parser/CookieParserFactory.test.ts",
    "content": "import {InvalidTokenError, InvalidTokenReason} from '../../../auth/error.ts';\nimport {Cookie} from '../builder/CookieBuilder.js';\nimport {GetCookiesTokensOptions} from '../types.ts';\nimport {CookieParserFactory} from './CookieParserFactory.js';\nimport {MultipleCookiesParser} from './MultipleCookiesParser.ts';\nimport {SingleCookieParser} from './SingleCookieParser.ts';\n\nconst testCookie = {\n  name: 'TestCookie',\n  value:\n    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6ImlkLXRva2VuIiwicmVmcmVzaF90b2tlbiI6InJlZnJlc2gtdG9rZW4iLCJjdXN0b21fdG9rZW4iOiJjdXN0b20tdG9rZW4ifQ.ExxN2rNayg2XCR6WNeZmY8tAyc_qyiZ2YdzITRbQocs'\n};\n\nconst testCookieHeader = toHeader(testCookie);\n\nfunction toHeader(cookie: Cookie): string {\n  return `${cookie.name}=${cookie.value}`;\n}\n\nconst testCookies: Cookie[] = [\n  {\n    name: 'TestCookie.id',\n    value: 'id-token'\n  },\n  {\n    name: 'TestCookie.refresh',\n    value: 'refresh-token'\n  },\n  {\n    name: 'TestCookie.custom',\n    value: 'custom-token'\n  },\n  {\n    name: 'TestCookie.sig',\n    value: 'QupyAMaPmI6d90CqB0lvec5Q517onmUvXEk6bONTQM0'\n  }\n];\n\nconst testCookiesHeader = testCookies.map(toHeader).join(';');\n\nconst legacyTestCookies: Cookie[] = [\n  {\n    name: 'TestCookie',\n    value: 'id-token:refresh-token'\n  },\n  {\n    name: 'TestCookie.custom',\n    value: 'custom-token'\n  },\n  {\n    name: 'TestCookie.sig',\n    value: 'QupyAMaPmI6d90CqB0lvec5Q517onmUvXEk6bONTQM0'\n  }\n];\n\nconst legacyCookiesHeader = legacyTestCookies.map(toHeader).join(';');\n\nconst testCookiesObj = testCookies.reduce(\n  (acc, cookie) => ({\n    ...acc,\n    [cookie.name]: cookie.value\n  }),\n  {\n    [testCookie.name]: testCookie.value\n  }\n);\n\nconst legacyTestCookiesObj = legacyTestCookies.reduce(\n  (acc, cookie) => ({\n    ...acc,\n    [cookie.name]: cookie.value\n  }),\n  {\n    [testCookie.name]: testCookie.value\n  }\n);\n\nconst mockOptions = {\n  cookieName: 'TestCookie',\n  cookieSignatureKeys: ['secret']\n} as unknown as GetCookiesTokensOptions;\n\ndescribe('CookieParserFactory', () => {\n  describe('fromHeaders', () => {\n    it('should create single cookie parser if request does not have multiple cookies', () => {\n      const mockHeaders = new Headers();\n      mockHeaders.set('Cookie', testCookieHeader);\n\n      const result = CookieParserFactory.fromHeaders(mockHeaders, mockOptions);\n      expect(result).toBeInstanceOf(SingleCookieParser);\n    });\n\n    it('should create single cookie parser if request does not have any cookies', () => {\n      const result = CookieParserFactory.fromHeaders(\n        new Headers(),\n        mockOptions\n      );\n\n      expect(result).toBeInstanceOf(SingleCookieParser);\n\n      return expect(() => result.parseCookies()).rejects.toEqual(\n        new InvalidTokenError(InvalidTokenReason.MISSING_CREDENTIALS)\n      );\n    });\n\n    it('should create multiple cookie parser if request does have all required cookies', () => {\n      const mockHeaders = new Headers();\n      mockHeaders.set('Cookie', testCookiesHeader);\n\n      const result = CookieParserFactory.fromHeaders(mockHeaders, mockOptions);\n\n      expect(result).toBeInstanceOf(MultipleCookiesParser);\n    });\n\n    it('should throw invalid credentials error if deprecated notation is used', () => {\n      const mockHeaders = new Headers();\n      mockHeaders.set(\n        'Cookie',\n        `TestCookie=${testCookies[0].value}:${testCookies[1].value}`\n      );\n\n      return expect(() =>\n        CookieParserFactory.fromHeaders(mockHeaders, mockOptions)\n      ).toThrow(new InvalidTokenError(InvalidTokenReason.INVALID_CREDENTIALS));\n    });\n\n    it('should create multiple cookie parser if request has legacy cookies', async () => {\n      const mockHeaders = new Headers();\n      mockHeaders.set('Cookie', legacyCookiesHeader);\n\n      const parser = CookieParserFactory.fromHeaders(mockHeaders, mockOptions);\n\n      expect(parser).toBeInstanceOf(MultipleCookiesParser);\n\n      const result = await parser.parseCookies();\n\n      expect(result).toEqual({\n        customToken: 'custom-token',\n        idToken: 'id-token',\n        refreshToken: 'refresh-token',\n        metadata: {}\n      });\n    });\n  });\n\n  describe('fromObject', () => {\n    it('should create single cookie parser if request does not have multiple cookies', async () => {\n      const parser = CookieParserFactory.fromObject(\n        {TestCookie: testCookiesObj['TestCookie']},\n        mockOptions\n      );\n\n      expect(parser).toBeInstanceOf(SingleCookieParser);\n\n      const result = await parser.parseCookies();\n\n      expect(result).toEqual({\n        customToken: 'custom-token',\n        idToken: 'id-token',\n        refreshToken: 'refresh-token',\n        metadata: {}\n      });\n    });\n\n    it('should create single cookie parser if request does not have any cookies', () => {\n      const result = CookieParserFactory.fromObject({}, mockOptions);\n\n      expect(result).toBeInstanceOf(SingleCookieParser);\n\n      return expect(() => result.parseCookies()).rejects.toEqual(\n        new InvalidTokenError(InvalidTokenReason.MISSING_CREDENTIALS)\n      );\n    });\n\n    it('should create multiple cookie parser if request does have all required cookies', async () => {\n      const parser = CookieParserFactory.fromObject(\n        testCookiesObj,\n        mockOptions\n      );\n\n      expect(parser).toBeInstanceOf(MultipleCookiesParser);\n\n      const result = await parser.parseCookies();\n\n      expect(result).toEqual({\n        customToken: 'custom-token',\n        idToken: 'id-token',\n        refreshToken: 'refresh-token',\n        metadata: {}\n      });\n    });\n\n    it('should create multiple cookie parser if request has legacy cookies', async () => {\n      const parser = CookieParserFactory.fromObject(\n        legacyTestCookiesObj,\n        mockOptions\n      );\n\n      expect(parser).toBeInstanceOf(MultipleCookiesParser);\n\n      const result = await parser.parseCookies();\n\n      expect(result).toEqual({\n        customToken: 'custom-token',\n        idToken: 'id-token',\n        refreshToken: 'refresh-token',\n        metadata: {}\n      });\n    });\n\n    it('should throw invalid credentials error if deprecated notation is used', () => {\n      return expect(() =>\n        CookieParserFactory.fromObject(\n          {\n            TestCookie: `${testCookies[0].value}:${testCookies[1].value}`\n          },\n          mockOptions\n        )\n      ).toThrow(new InvalidTokenError(InvalidTokenReason.INVALID_CREDENTIALS));\n    });\n  });\n});\n"
  },
  {
    "path": "src/next/cookies/parser/CookieParserFactory.ts",
    "content": "import type {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies';\nimport type {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\nimport {InvalidTokenError, InvalidTokenReason} from '../../../auth/error.js';\nimport {debug} from '../../../debug/index.js';\nimport {CookiesObject, GetCookiesTokensOptions} from '../types.js';\nimport {CookiesProvider} from './CookiesProvider.js';\nimport {MultipleCookiesParser} from './MultipleCookiesParser.js';\nimport {ObjectCookiesProvider} from './ObjectCookiesProvider.js';\nimport {RequestCookiesProvider} from './RequestCookiesProvider.js';\nimport {SingleCookieParser} from './SingleCookieParser.js';\n\nexport class CookieParserFactory {\n  public static hasMultipleCookies(\n    provider: CookiesProvider,\n    cookieName: string\n  ) {\n    return ['id', 'refresh', 'sig']\n      .map((it) => `${cookieName}.${it}`)\n      .every((it) => Boolean(provider.get(it)));\n  }\n\n  public static hasCustomTokenCookie(\n    provider: CookiesProvider,\n    cookieName: string\n  ) {\n    return Boolean(provider.get(`${cookieName}.custom`));\n  }\n\n  public static hasLegacyMultipleCookies(\n    provider: CookiesProvider,\n    cookieName: string\n  ) {\n    return (\n      ['custom', 'sig']\n        .map((it) => `${cookieName}.${it}`)\n        .every((it) => Boolean(provider.get(it))) &&\n      provider.get(cookieName)?.includes(':')\n    );\n  }\n\n  private static getCompatibleProvider(\n    legacyProvider: CookiesProvider,\n    options: GetCookiesTokensOptions\n  ) {\n    const legacyToken = legacyProvider.get(options.cookieName);\n    const [idToken, refreshToken] = legacyToken?.split(':') ?? [];\n\n    const adaptedCookies = {\n      [`${options.cookieName}.id`]: idToken,\n      [`${options.cookieName}.refresh`]: refreshToken,\n      [`${options.cookieName}.custom`]: legacyProvider.get(\n        `${options.cookieName}.custom`\n      ),\n      [`${options.cookieName}.sig`]: legacyProvider.get(\n        `${options.cookieName}.sig`\n      )\n    };\n\n    return new ObjectCookiesProvider(adaptedCookies);\n  }\n\n  private static fromProvider<Metadata extends object>(\n    provider: CookiesProvider,\n    options: GetCookiesTokensOptions\n  ) {\n    const singleCookie = provider.get(options.cookieName);\n    const hasLegacyCookie = singleCookie?.includes(':');\n    const enableMultipleCookies = CookieParserFactory.hasMultipleCookies(\n      provider,\n      options.cookieName\n    );\n\n    if (enableMultipleCookies) {\n      return new MultipleCookiesParser<Metadata>(\n        provider,\n        options.cookieName,\n        options.cookieSignatureKeys\n      );\n    }\n\n    if (\n      CookieParserFactory.hasLegacyMultipleCookies(provider, options.cookieName)\n    ) {\n      return new MultipleCookiesParser<Metadata>(\n        CookieParserFactory.getCompatibleProvider(provider, options),\n        options.cookieName,\n        options.cookieSignatureKeys\n      );\n    }\n\n    if (hasLegacyCookie) {\n      debug(\n        \"Authentication cookie is in multiple cookie format, but lacks signature and custom cookies. Clear your browser cookies and try again. If the issue keeps happening and you're using `enableMultipleCookies` option, make sure that server returns all required cookies: https://next-firebase-auth-edge-docs.vercel.app/docs/usage/middleware#multiple-cookies\"\n      );\n\n      throw new InvalidTokenError(InvalidTokenReason.INVALID_CREDENTIALS);\n    }\n\n    return new SingleCookieParser<Metadata>(\n      provider,\n      options.cookieName,\n      options.cookieSignatureKeys\n    );\n  }\n\n  static fromRequestCookies<Metadata extends object>(\n    cookies: RequestCookies | ReadonlyRequestCookies,\n    options: GetCookiesTokensOptions\n  ) {\n    const provider = RequestCookiesProvider.fromRequestCookies(cookies);\n\n    return CookieParserFactory.fromProvider<Metadata>(provider, options);\n  }\n\n  static fromHeaders<Metadata extends object>(\n    headers: Headers,\n    options: GetCookiesTokensOptions\n  ) {\n    const provider = RequestCookiesProvider.fromHeaders(headers);\n\n    return CookieParserFactory.fromProvider<Metadata>(provider, options);\n  }\n\n  static fromObject<Metadata extends object>(\n    cookies: CookiesObject,\n    options: GetCookiesTokensOptions\n  ) {\n    const provider = new ObjectCookiesProvider(cookies);\n\n    return CookieParserFactory.fromProvider<Metadata>(provider, options);\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/parser/CookiesProvider.ts",
    "content": "export interface CookiesProvider {\n  get(key: string): string | undefined;\n}\n"
  },
  {
    "path": "src/next/cookies/parser/MultipleCookiesParser.test.ts",
    "content": "import {InvalidTokenError, InvalidTokenReason} from '../../../auth/error.ts';\nimport {CookiesProvider} from './CookiesProvider.ts';\nimport {MultipleCookiesParser} from './MultipleCookiesParser.js';\nimport type {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\nimport {RequestCookiesProvider} from './RequestCookiesProvider.ts';\n\nconst testCookies = [\n  {\n    name: 'TestCookie.id',\n    value: 'id-token'\n  },\n  {\n    name: 'TestCookie.refresh',\n    value: 'refresh-token'\n  },\n  {\n    name: 'TestCookie.custom',\n    value: 'custom-token'\n  },\n  {\n    name: 'TestCookie.sig',\n    value: 'QupyAMaPmI6d90CqB0lvec5Q517onmUvXEk6bONTQM0'\n  }\n];\n\nconst testCookiesNoCustom = [\n  {\n    name: 'TestCookie.id',\n    value: 'id-token'\n  },\n  {\n    name: 'TestCookie.refresh',\n    value: 'refresh-token'\n  },\n  {\n    name: 'TestCookie.sig',\n    value: 'g-7yXxxJfMmzsR7BqkJjguoUWsOqCTGz2AndxjJBrkw'\n  }\n];\n\nconst testCookiesWithMetadata = [\n  {\n    name: 'TestCookie.id',\n    value: 'id-token'\n  },\n  {\n    name: 'TestCookie.refresh',\n    value: 'refresh-token'\n  },\n  {\n    name: 'TestCookie.custom',\n    value: 'custom-token'\n  },\n  {\n    name: 'TestCookie.metadata',\n    value: 'eyJmb28iOiJiYXIifQ'\n  },\n  {\n    name: 'TestCookie.sig',\n    value: '4LS2ty2sdecHjVR9dSSMO8jY0gvITmMgJH1stLFKVlA'\n  }\n];\n\ndescribe('MultipleCookiesParser', () => {\n  let mockCookies: RequestCookies;\n  let mockCookiesProvider: CookiesProvider;\n\n  beforeEach(() => {\n    mockCookies = {\n      get: jest.fn((name: string) =>\n        testCookies.find((cookie) => name === cookie.name)\n      )\n    } as unknown as RequestCookies;\n    mockCookiesProvider =\n      RequestCookiesProvider.fromRequestCookies(mockCookies);\n  });\n\n  it('should parse multiple cookies', async () => {\n    const parser = new MultipleCookiesParser(\n      mockCookiesProvider,\n      'TestCookie',\n      ['secret']\n    );\n\n    const result = await parser.parseCookies();\n\n    expect(result).toEqual({\n      customToken: 'custom-token',\n      idToken: 'id-token',\n      refreshToken: 'refresh-token',\n      metadata: {}\n    });\n\n    expect(mockCookies.get).toHaveBeenNthCalledWith(1, 'TestCookie.id');\n    expect(mockCookies.get).toHaveBeenNthCalledWith(2, 'TestCookie.refresh');\n    expect(mockCookies.get).toHaveBeenNthCalledWith(3, 'TestCookie.custom');\n    expect(mockCookies.get).toHaveBeenNthCalledWith(4, 'TestCookie.metadata');\n    expect(mockCookies.get).toHaveBeenNthCalledWith(5, 'TestCookie.sig');\n  });\n\n  it('should parse multiple cookies with metadata', async () => {\n    (mockCookies.get as jest.Mock).mockImplementation((name: string) =>\n      testCookiesWithMetadata.find((cookie) => name === cookie.name)\n    );\n    const parser = new MultipleCookiesParser(\n      mockCookiesProvider,\n      'TestCookie',\n      ['secret']\n    );\n\n    const result = await parser.parseCookies();\n\n    expect(result).toEqual({\n      customToken: 'custom-token',\n      idToken: 'id-token',\n      refreshToken: 'refresh-token',\n      metadata: {foo: 'bar'}\n    });\n\n    expect(mockCookies.get).toHaveBeenNthCalledWith(1, 'TestCookie.id');\n    expect(mockCookies.get).toHaveBeenNthCalledWith(2, 'TestCookie.refresh');\n    expect(mockCookies.get).toHaveBeenNthCalledWith(3, 'TestCookie.custom');\n    expect(mockCookies.get).toHaveBeenNthCalledWith(4, 'TestCookie.metadata');\n    expect(mockCookies.get).toHaveBeenNthCalledWith(5, 'TestCookie.sig');\n  });\n\n  it('should throw missing credentials error if id token is empty', () => {\n    (mockCookies.get as jest.Mock).mockImplementation((name: string) =>\n      testCookies\n        .filter((it) => !it.name.endsWith('.id'))\n        .find((it) => it.name === name)\n    );\n\n    const parser = new MultipleCookiesParser(\n      mockCookiesProvider,\n      'TestCookie',\n      ['secret']\n    );\n\n    return expect(() => parser.parseCookies()).rejects.toEqual(\n      new InvalidTokenError(InvalidTokenReason.MISSING_CREDENTIALS)\n    );\n  });\n\n  it('should throw missing credentials error if refresh token is empty', () => {\n    (mockCookies.get as jest.Mock).mockImplementation((name: string) =>\n      testCookies\n        .filter((it) => !it.name.endsWith('.refresh'))\n        .find((it) => it.name === name)\n    );\n\n    const parser = new MultipleCookiesParser(\n      mockCookiesProvider,\n      'TestCookie',\n      ['secret']\n    );\n\n    return expect(() => parser.parseCookies()).rejects.toEqual(\n      new InvalidTokenError(InvalidTokenReason.MISSING_CREDENTIALS)\n    );\n  });\n\n  it('should throw invalid signature error if custom token is empty and multiple cookies signed with custom token are provided', () => {\n    (mockCookies.get as jest.Mock).mockImplementation((name: string) =>\n      testCookies\n        .filter((it) => !it.name.endsWith('.custom'))\n        .find((it) => it.name === name)\n    );\n\n    const parser = new MultipleCookiesParser(\n      mockCookiesProvider,\n      'TestCookie',\n      ['secret']\n    );\n\n    return expect(() => parser.parseCookies()).rejects.toEqual(\n      new InvalidTokenError(InvalidTokenReason.INVALID_SIGNATURE)\n    );\n  });\n\n  it('should parse multiple cookies without custom token', async () => {\n    (mockCookies.get as jest.Mock).mockImplementation((name: string) =>\n      testCookiesNoCustom.find((cookie) => name === cookie.name)\n    );\n    const parser = new MultipleCookiesParser(\n      RequestCookiesProvider.fromRequestCookies(mockCookies),\n      'TestCookie',\n      ['secret']\n    );\n\n    const result = await parser.parseCookies();\n\n    expect(result).toEqual({\n      idToken: 'id-token',\n      refreshToken: 'refresh-token',\n      metadata: {}\n    });\n\n    expect(mockCookies.get).toHaveBeenNthCalledWith(1, 'TestCookie.id');\n    expect(mockCookies.get).toHaveBeenNthCalledWith(2, 'TestCookie.refresh');\n    expect(mockCookies.get).toHaveBeenNthCalledWith(3, 'TestCookie.custom');\n    expect(mockCookies.get).toHaveBeenNthCalledWith(4, 'TestCookie.metadata');\n    expect(mockCookies.get).toHaveBeenNthCalledWith(5, 'TestCookie.sig');\n  });\n  it('should throw missing credentials error if signature is empty', () => {\n    (mockCookies.get as jest.Mock).mockImplementation((name: string) =>\n      testCookies\n        .filter((it) => !it.name.endsWith('.sig'))\n        .find((it) => it.name === name)\n    );\n\n    const parser = new MultipleCookiesParser(\n      mockCookiesProvider,\n      'TestCookie',\n      ['secret']\n    );\n\n    return expect(() => parser.parseCookies()).rejects.toEqual(\n      new InvalidTokenError(InvalidTokenReason.MISSING_CREDENTIALS)\n    );\n  });\n\n  it('should throw invalid signature error if signature is incorrect', () => {\n    const parser = new MultipleCookiesParser(\n      mockCookiesProvider,\n      'TestCookie',\n      ['incorrect-secret']\n    );\n\n    return expect(() => parser.parseCookies()).rejects.toEqual(\n      new InvalidTokenError(InvalidTokenReason.INVALID_SIGNATURE)\n    );\n  });\n});\n"
  },
  {
    "path": "src/next/cookies/parser/MultipleCookiesParser.ts",
    "content": "import {base64url, errors} from 'jose';\nimport {ParsedCookies} from '../../../auth/custom-token/index.js';\nimport {InvalidTokenError, InvalidTokenReason} from '../../../auth/error.js';\nimport {RotatingCredential} from '../../../auth/rotating-credential.js';\nimport {CookieParser} from './CookieParser.js';\nimport {CookiesProvider} from './CookiesProvider.js';\n\nconst textDecoder = new TextDecoder();\n\nexport class MultipleCookiesParser<Metadata extends object>\n  implements CookieParser<Metadata>\n{\n  constructor(\n    private cookies: CookiesProvider,\n    private cookieName: string,\n    private signatureKeys: string[]\n  ) {}\n\n  async parseCookies(): Promise<ParsedCookies<Metadata>> {\n    const idTokenCookie = this.cookies.get(`${this.cookieName}.id`);\n    const refreshTokenCookie = this.cookies.get(`${this.cookieName}.refresh`);\n    const customTokenCookie = this.cookies.get(`${this.cookieName}.custom`);\n    const metadataCookie = this.cookies.get(`${this.cookieName}.metadata`);\n    const signatureCookie = this.cookies.get(`${this.cookieName}.sig`);\n\n    if (![idTokenCookie, refreshTokenCookie, signatureCookie].every(Boolean)) {\n      throw new InvalidTokenError(InvalidTokenReason.MISSING_CREDENTIALS);\n    }\n\n    const signature = signatureCookie!;\n    const customTokens: ParsedCookies<Metadata> = {\n      idToken: idTokenCookie!,\n      refreshToken: refreshTokenCookie!,\n      customToken: customTokenCookie,\n      metadata: metadataCookie\n        ? JSON.parse(textDecoder.decode(base64url.decode(metadataCookie)))\n        : {}\n    };\n\n    const credential = new RotatingCredential(this.signatureKeys);\n\n    try {\n      await credential.verifySignature(customTokens, signature);\n\n      return customTokens;\n    } catch (e) {\n      if (e instanceof errors.JWSSignatureVerificationFailed) {\n        throw new InvalidTokenError(InvalidTokenReason.INVALID_SIGNATURE);\n      }\n\n      throw e;\n    }\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/parser/ObjectCookiesProvider.ts",
    "content": "import {CookiesObject} from '../types.js';\n\nexport class ObjectCookiesProvider {\n  constructor(private cookies: CookiesObject) {}\n\n  get(key: string) {\n    return this.cookies[key];\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/parser/RequestCookiesProvider.test.ts",
    "content": "import {RequestCookiesProvider} from './RequestCookiesProvider.js';\ndescribe('RequestCookiesProvider', () => {\n  it('should copy initial headers', () => {\n    const headers = new Headers();\n    headers.set('Cookie', 'TestCookie=TestToken');\n\n    const provider = RequestCookiesProvider.fromHeaders(headers);\n\n    expect(provider.get('TestCookie')).toEqual('TestToken');\n\n    headers.set('Cookie', 'NewCookie=SomeNewCookie');\n\n    expect(provider.get('TestCookie')).toEqual('TestToken');\n    expect(provider.get('NewCookie')).toEqual(undefined);\n  });\n});\n"
  },
  {
    "path": "src/next/cookies/parser/RequestCookiesProvider.ts",
    "content": "import type {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies';\nimport {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\n\nexport class RequestCookiesProvider {\n  static fromHeaders(headers: Headers) {\n    const cookies = new RequestCookies(new Headers(headers));\n\n    return new RequestCookiesProvider(cookies);\n  }\n\n  static fromRequestCookies(cookies: RequestCookies | ReadonlyRequestCookies) {\n    return new RequestCookiesProvider(cookies);\n  }\n\n  constructor(private cookies: RequestCookies | ReadonlyRequestCookies) {}\n\n  get(key: string) {\n    return this.cookies.get(key)?.value;\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/parser/SingleCookieParser.test.ts",
    "content": "import {InvalidTokenError, InvalidTokenReason} from '../../../auth/error.ts';\nimport {RequestCookiesProvider} from './RequestCookiesProvider.ts';\nimport {SingleCookieParser} from './SingleCookieParser.js';\nimport type {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\n\nconst mockCookie = {\n  name: 'TestCookie',\n  value:\n    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6ImlkLXRva2VuIiwicmVmcmVzaF90b2tlbiI6InJlZnJlc2gtdG9rZW4iLCJjdXN0b21fdG9rZW4iOiJjdXN0b20tdG9rZW4ifQ.ExxN2rNayg2XCR6WNeZmY8tAyc_qyiZ2YdzITRbQocs'\n};\n\nconst mockCookieWithMetadata = {\n  name: 'TestCookie',\n  value:\n    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZF90b2tlbiI6ImlkLXRva2VuIiwicmVmcmVzaF90b2tlbiI6InJlZnJlc2gtdG9rZW4iLCJjdXN0b21fdG9rZW4iOiJjdXN0b20tdG9rZW4iLCJtZXRhZGF0YSI6eyJmb28iOiJiYXIifX0.tm6HY-N7NZMq9ipez53--tXfixsHhcF59hj9s13iEII'\n};\n\ndescribe('SingleCookieParser', () => {\n  let mockCookies: RequestCookies;\n  let mockCookiesProvider: RequestCookiesProvider;\n\n  beforeEach(() => {\n    mockCookies = {\n      get: jest.fn(() => mockCookie)\n    } as unknown as RequestCookies;\n    mockCookiesProvider = new RequestCookiesProvider(mockCookies);\n  });\n\n  it('should parse a jwt cookie', async () => {\n    const parser = new SingleCookieParser(mockCookiesProvider, 'TestCookie', [\n      'secret'\n    ]);\n\n    const result = await parser.parseCookies();\n\n    expect(result).toEqual({\n      customToken: 'custom-token',\n      idToken: 'id-token',\n      refreshToken: 'refresh-token',\n      metadata: {}\n    });\n\n    expect(mockCookies.get).toHaveBeenCalledWith('TestCookie');\n  });\n\n  it('should parse a jwt cookie with metadata', async () => {\n    (mockCookies.get as jest.Mock).mockImplementationOnce(\n      () => mockCookieWithMetadata\n    );\n\n    const parser = new SingleCookieParser(mockCookiesProvider, 'TestCookie', [\n      'secret'\n    ]);\n\n    const result = await parser.parseCookies();\n\n    expect(result).toEqual({\n      customToken: 'custom-token',\n      idToken: 'id-token',\n      refreshToken: 'refresh-token',\n      metadata: {foo: 'bar'}\n    });\n\n    expect(mockCookies.get).toHaveBeenCalledWith('TestCookie');\n  });\n\n  it('should throw missing credentials error if cookie is empty', () => {\n    (mockCookies.get as jest.Mock).mockImplementationOnce(() => ({\n      name: 'TestCookie',\n      value: undefined\n    }));\n\n    const parser = new SingleCookieParser(mockCookiesProvider, 'TestCookie', [\n      'secret'\n    ]);\n\n    return expect(() => parser.parseCookies()).rejects.toEqual(\n      new InvalidTokenError(InvalidTokenReason.MISSING_CREDENTIALS)\n    );\n  });\n\n  it('should throw invalid signature error if signature is incorrect', () => {\n    const parser = new SingleCookieParser(mockCookiesProvider, 'TestCookie', [\n      'incorrect-secret'\n    ]);\n\n    return expect(() => parser.parseCookies()).rejects.toEqual(\n      new InvalidTokenError(InvalidTokenReason.INVALID_SIGNATURE)\n    );\n  });\n});\n"
  },
  {
    "path": "src/next/cookies/parser/SingleCookieParser.ts",
    "content": "import {errors} from 'jose';\nimport {ParsedCookies} from '../../../auth/custom-token/index.js';\nimport {InvalidTokenError, InvalidTokenReason} from '../../../auth/error.js';\nimport {RotatingCredential} from '../../../auth/rotating-credential.js';\nimport {CookieParser} from './CookieParser.js';\nimport {CookiesProvider} from './CookiesProvider.js';\n\nexport class SingleCookieParser<Metadata extends object>\n  implements CookieParser<Metadata>\n{\n  constructor(\n    private cookies: CookiesProvider,\n    private cookieName: string,\n    private signatureKeys: string[]\n  ) {}\n\n  async parseCookies(): Promise<ParsedCookies<Metadata>> {\n    const jwtCookie = this.cookies.get(this.cookieName);\n\n    if (!jwtCookie) {\n      throw new InvalidTokenError(InvalidTokenReason.MISSING_CREDENTIALS);\n    }\n\n    const credential = new RotatingCredential(this.signatureKeys);\n\n    try {\n      const result = await credential.verify(jwtCookie);\n\n      return {\n        idToken: result.id_token,\n        refreshToken: result.refresh_token,\n        customToken: result.custom_token,\n        metadata: (result.metadata ?? {}) as Metadata\n      };\n    } catch (e) {\n      if (e instanceof errors.JWSSignatureVerificationFailed) {\n        throw new InvalidTokenError(InvalidTokenReason.INVALID_SIGNATURE);\n      }\n\n      throw e;\n    }\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/remover/CombinedCookieRemover.ts",
    "content": "import {SingleCookieRemover} from './SingleCookieRemover.js';\nimport {CookieRemover} from './CookieRemover.js';\nimport {MultipleCookieRemover} from './MultipleCookieRemover.js';\n\nexport class CombinedCookieRemover implements CookieRemover {\n  constructor(\n    private multi: MultipleCookieRemover,\n    private single: SingleCookieRemover\n  ) {}\n\n  removeCookies(): void {\n    this.multi.removeCookies();\n    this.single.removeCookies();\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/remover/CookieRemover.ts",
    "content": "export interface CookieRemover {\n  removeCookies(): void;\n}\n"
  },
  {
    "path": "src/next/cookies/remover/CookieRemoverFactory.test.ts",
    "content": "import {Cookie} from '../builder/CookieBuilder.js';\nimport {RequestCookiesProvider} from '../parser/RequestCookiesProvider.js';\nimport {CookieRemoverFactory} from './CookieRemoverFactory';\nimport type {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\n\nconst cookieName = 'TestCookie';\n\nconst testCookies: Cookie[] = [\n  {\n    name: 'TestCookie.id',\n    value: 'id-token'\n  },\n  {\n    name: 'TestCookie.refresh',\n    value: 'refresh-token'\n  },\n  {\n    name: 'TestCookie.custom',\n    value: 'custom-token'\n  },\n  {\n    name: 'TestCookie.sig',\n    value: 'QupyAMaPmI6d90CqB0lvec5Q517onmUvXEk6bONTQM0'\n  }\n];\n\nconst legacyTestCookies: Cookie[] = [\n  {\n    name: 'TestCookie',\n    value: 'id-token:refresh-token'\n  },\n  {\n    name: 'TestCookie.custom',\n    value: 'custom-token'\n  },\n  {\n    name: 'TestCookie.sig',\n    value: 'QupyAMaPmI6d90CqB0lvec5Q517onmUvXEk6bONTQM0'\n  }\n];\n\nfunction getTestCookie(name: string) {\n  return testCookies.find((it) => it.name === name);\n}\n\nfunction getLegacyTestCookie(name: string) {\n  return legacyTestCookies.find((it) => it.name === name);\n}\n\nfunction getSingleCookie(name: string) {\n  if (name === cookieName) {\n    return {\n      name: cookieName,\n      value: 'single-cookie'\n    };\n  }\n\n  return undefined;\n}\n\ndescribe('CookieRemoverFactory', () => {\n  it('should remove a single cookie', () => {\n    const cookies = {\n      get: jest.fn(),\n      delete: jest.fn()\n    } as unknown as RequestCookies;\n\n    const remover = CookieRemoverFactory.fromRequestCookies(\n      cookies,\n      new RequestCookiesProvider(cookies),\n      cookieName\n    );\n\n    remover.removeCookies();\n\n    expect(cookies.delete).toHaveBeenCalledTimes(1);\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie');\n  });\n\n  it('should remove multiple cookies', () => {\n    const cookies = {\n      get: jest.fn(getTestCookie),\n      delete: jest.fn()\n    } as unknown as RequestCookies;\n\n    const remover = CookieRemoverFactory.fromRequestCookies(\n      cookies,\n      new RequestCookiesProvider(cookies),\n      cookieName\n    );\n\n    remover.removeCookies();\n\n    expect(cookies.delete).toHaveBeenCalledTimes(5);\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie.id');\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie.refresh');\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie.custom');\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie.metadata');\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie.sig');\n  });\n\n  it('should remove multiple and single cookies when there are both', () => {\n    const cookies = {\n      get: jest.fn((name) => {\n        return getSingleCookie(name) ?? getTestCookie(name);\n      }),\n      delete: jest.fn()\n    } as unknown as RequestCookies;\n\n    const remover = CookieRemoverFactory.fromRequestCookies(\n      cookies,\n      new RequestCookiesProvider(cookies),\n      cookieName\n    );\n\n    remover.removeCookies();\n\n    expect(cookies.delete).toHaveBeenCalledTimes(6);\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie');\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie.id');\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie.refresh');\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie.custom');\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie.metadata');\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie.sig');\n  });\n\n  it('should remove multiple and single cookies when there are legacy cookies', () => {\n    const cookies = {\n      get: jest.fn((name) => {\n        return getLegacyTestCookie(name);\n      }),\n      delete: jest.fn()\n    } as unknown as RequestCookies;\n\n    const remover = CookieRemoverFactory.fromRequestCookies(\n      cookies,\n      new RequestCookiesProvider(cookies),\n      cookieName\n    );\n\n    remover.removeCookies();\n\n    expect(cookies.delete).toHaveBeenCalledTimes(6);\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie');\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie.id');\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie.refresh');\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie.custom');\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie.metadata');\n    expect(cookies.delete).toHaveBeenCalledWith('TestCookie.sig');\n  });\n});\n"
  },
  {
    "path": "src/next/cookies/remover/CookieRemoverFactory.ts",
    "content": "import {CookieParserFactory} from '../parser/CookieParserFactory.js';\nimport {CookiesProvider} from '../parser/CookiesProvider.js';\nimport {CombinedCookieRemover} from './CombinedCookieRemover';\nimport {MultipleCookieRemover} from './MultipleCookieRemover';\nimport {SingleCookieRemover} from './SingleCookieRemover';\nimport type {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\nimport type {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies';\n\nexport class CookieRemoverFactory {\n  static fromRequestCookies(\n    cookies: RequestCookies | ReadonlyRequestCookies,\n    provider: CookiesProvider,\n    cookieName: string\n  ) {\n    const singleCookie = provider.get(cookieName);\n    const hasEnabledMultipleCookies = CookieParserFactory.hasMultipleCookies(\n      provider,\n      cookieName\n    );\n    const hasEnabledLegacyMultipleCookies =\n      CookieParserFactory.hasLegacyMultipleCookies(provider, cookieName);\n\n    if (\n      singleCookie &&\n      (hasEnabledMultipleCookies || hasEnabledLegacyMultipleCookies)\n    ) {\n      return new CombinedCookieRemover(\n        new MultipleCookieRemover(cookieName, cookies),\n        new SingleCookieRemover(cookieName, cookies)\n      );\n    }\n\n    if (hasEnabledMultipleCookies) {\n      return new MultipleCookieRemover(cookieName, cookies);\n    }\n\n    return new SingleCookieRemover(cookieName, cookies);\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/remover/MultipleCookieRemover.test.ts",
    "content": "import {MultipleCookieRemover} from './MultipleCookieRemover.js';\nimport {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\n\nconst mockCookies: RequestCookies = {\n  delete: jest.fn()\n} as unknown as RequestCookies;\n\ndescribe('MultipleCookieRemover', () => {\n  beforeEach(() => {\n    jest.resetAllMocks();\n  });\n\n  it('should remove multiple cookies', () => {\n    const remover = new MultipleCookieRemover('TestCookie', mockCookies);\n\n    remover.removeCookies();\n\n    expect(mockCookies.delete).toHaveBeenNthCalledWith(1, 'TestCookie.id');\n    expect(mockCookies.delete).toHaveBeenNthCalledWith(2, 'TestCookie.refresh');\n    expect(mockCookies.delete).toHaveBeenNthCalledWith(3, 'TestCookie.custom');\n    expect(mockCookies.delete).toHaveBeenNthCalledWith(\n      4,\n      'TestCookie.metadata'\n    );\n    expect(mockCookies.delete).toHaveBeenNthCalledWith(5, 'TestCookie.sig');\n  });\n});\n"
  },
  {
    "path": "src/next/cookies/remover/MultipleCookieRemover.ts",
    "content": "import type {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\nimport type {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies';\nimport {CookieRemover} from './CookieRemover.js';\n\nexport class MultipleCookieRemover implements CookieRemover {\n  public constructor(\n    private cookieName: string,\n    private cookies: RequestCookies | ReadonlyRequestCookies\n  ) {}\n\n  removeCookies(): void {\n    [\n      `${this.cookieName}.id`,\n      `${this.cookieName}.refresh`,\n      `${this.cookieName}.custom`,\n      `${this.cookieName}.metadata`,\n      `${this.cookieName}.sig`\n    ].forEach((name) => this.cookies.delete(name));\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/remover/SingleCookieRemover.ts",
    "content": "import type {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\nimport type {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies';\nimport {CookieRemover} from './CookieRemover.js';\n\nexport class SingleCookieRemover implements CookieRemover {\n  public constructor(\n    private cookieName: string,\n    private cookies: RequestCookies | ReadonlyRequestCookies\n  ) {}\n\n  removeCookies(): void {\n    this.cookies.delete(this.cookieName);\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/setter/CookieSetter.ts",
    "content": "import {CookieSerializeOptions} from 'cookie';\nimport type {Cookie} from '../builder/CookieBuilder.js';\n\nexport interface CookieSetter {\n  setCookies(cookies: Cookie[], options: CookieSerializeOptions): void;\n}\n"
  },
  {
    "path": "src/next/cookies/setter/CookieSetterFactory.ts",
    "content": "import type {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies';\nimport type {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\nimport {HeadersCookieSetter} from './HeadersCookieSetter.js';\nimport {RequestCookieSetter} from './RequestCookieSetter.js';\n\nexport class CookieSetterFactory {\n  static fromRequestCookies(cookies: RequestCookies | ReadonlyRequestCookies) {\n    return new RequestCookieSetter(cookies);\n  }\n\n  static fromHeaders(headers: Headers) {\n    return new HeadersCookieSetter(headers);\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/setter/HeadersCookieSetter.test.ts",
    "content": "import {HeadersCookieSetter} from './HeadersCookieSetter.ts';\n\ndescribe('HeadersCookieSetter', () => {\n  it('should append cookie with options on provided headers', () => {\n    const mockHeaders = {append: jest.fn()} as unknown as Headers;\n    const serializeOptions = {\n      path: '/',\n      httpOnly: true,\n      secure: true,\n      sameSite: 'lax' as const,\n      maxAge: 12 * 60 * 60 * 24\n    };\n    const setter = new HeadersCookieSetter(mockHeaders);\n\n    setter.setCookies(\n      [\n        {\n          name: 'FirstCookie',\n          value: 'first'\n        },\n        {\n          name: 'SecondCookie',\n          value: 'second'\n        }\n      ],\n      serializeOptions\n    );\n\n    expect(mockHeaders.append).toHaveBeenNthCalledWith(\n      1,\n      'Set-Cookie',\n      'FirstCookie=first; Max-Age=1036800; Path=/; HttpOnly; Secure; SameSite=Lax'\n    );\n\n    expect(mockHeaders.append).toHaveBeenNthCalledWith(\n      2,\n      'Set-Cookie',\n      'SecondCookie=second; Max-Age=1036800; Path=/; HttpOnly; Secure; SameSite=Lax'\n    );\n  });\n});\n"
  },
  {
    "path": "src/next/cookies/setter/HeadersCookieSetter.ts",
    "content": "import {CookieSerializeOptions, serialize} from 'cookie';\nimport type {Cookie} from '../builder/CookieBuilder.js';\nimport type {CookieSetter} from './CookieSetter.js';\n\nexport class HeadersCookieSetter implements CookieSetter {\n  constructor(private headers: Headers) {}\n\n  setCookies(cookies: Cookie[], options: CookieSerializeOptions): void {\n    for (const cookie of cookies) {\n      this.headers.append(\n        'Set-Cookie',\n        serialize(cookie.name, cookie.value, options)\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/setter/NextApiResponseHeadersCookieSetter.ts",
    "content": "import {CookieSerializeOptions, serialize} from 'cookie';\nimport type {NextApiResponse} from 'next';\nimport type {Cookie} from '../builder/CookieBuilder.js';\nimport type {CookieSetter} from './CookieSetter.js';\n\nexport class NextApiResponseCookieSetter implements CookieSetter {\n  constructor(private response: NextApiResponse) {}\n\n  setCookies(cookies: Cookie[], options: CookieSerializeOptions): void {\n    for (const cookie of cookies) {\n      this.response.setHeader('Set-Cookie', [\n        serialize(cookie.name, cookie.value, options)\n      ]);\n    }\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/setter/RequestCookieSetter.test.ts",
    "content": "import type {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\nimport {RequestCookieSetter} from './RequestCookieSetter.js';\n\ndescribe('RequestCookieSetter', () => {\n  it('should set cookie with options on provided request', () => {\n    const mockCookies = {set: jest.fn()} as unknown as RequestCookies;\n    const serializeOptions = {\n      path: '/',\n      httpOnly: true,\n      secure: true,\n      sameSite: 'lax' as const,\n      maxAge: 12 * 60 * 60 * 24\n    };\n    const setter = new RequestCookieSetter(mockCookies);\n\n    setter.setCookies(\n      [\n        {\n          name: 'FirstCookie',\n          value: 'first'\n        },\n        {\n          name: 'SecondCookie',\n          value: 'second'\n        }\n      ],\n      serializeOptions\n    );\n\n    expect(mockCookies.set).toHaveBeenNthCalledWith(\n      1,\n      'FirstCookie',\n      'first',\n      serializeOptions\n    );\n\n    expect(mockCookies.set).toHaveBeenNthCalledWith(\n      2,\n      'SecondCookie',\n      'second',\n      serializeOptions\n    );\n  });\n\n  it('should delete empty cookies', () => {\n    const mockCookies = {\n      set: jest.fn(),\n      delete: jest.fn()\n    } as unknown as RequestCookies;\n    const serializeOptions = {\n      path: '/',\n      httpOnly: true,\n      secure: true,\n      sameSite: 'lax' as const,\n      maxAge: 12 * 60 * 60 * 24\n    };\n    const setter = new RequestCookieSetter(mockCookies);\n\n    setter.setCookies(\n      [\n        {\n          name: 'FirstCookie',\n          value: ''\n        },\n        {\n          name: 'SecondCookie',\n          value: ''\n        }\n      ],\n      serializeOptions\n    );\n\n    expect(mockCookies.set).not.toHaveBeenCalled();\n\n    expect(mockCookies.delete).toHaveBeenNthCalledWith(1, 'FirstCookie');\n\n    expect(mockCookies.delete).toHaveBeenNthCalledWith(2, 'SecondCookie');\n  });\n});\n"
  },
  {
    "path": "src/next/cookies/setter/RequestCookieSetter.ts",
    "content": "import type {CookieSerializeOptions} from 'cookie';\nimport type {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies';\nimport type {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\nimport type {Cookie} from '../builder/CookieBuilder.js';\nimport type {CookieSetter} from './CookieSetter.js';\n\nexport class RequestCookieSetter implements CookieSetter {\n  constructor(private cookies: RequestCookies | ReadonlyRequestCookies) {}\n\n  setCookies(cookies: Cookie[], options: CookieSerializeOptions): void {\n    for (const cookie of cookies) {\n      if (cookie.value) {\n        this.cookies.set(cookie.name, cookie.value, options);\n      } else {\n        this.cookies.delete(cookie.name);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/next/cookies/types.ts",
    "content": "import type {CookieSerializeOptions} from 'cookie';\nimport {ServiceAccount} from '../../auth/credential.js';\nimport {TokenSet} from '../../auth/types.js';\n\nexport interface SetAuthCookiesOptions<Metadata extends object> {\n  cookieName: string;\n  cookieSignatureKeys: string[];\n  cookieSerializeOptions: CookieSerializeOptions;\n  enableMultipleCookies?: boolean;\n  enableCustomToken?: boolean;\n  serviceAccount?: ServiceAccount;\n  apiKey: string;\n  tenantId?: string;\n  authorizationHeaderName?: string;\n  dynamicCustomClaimsKeys?: string[];\n  getMetadata?: (tokens: TokenSet) => Promise<Metadata>;\n  enableTokenRefreshOnExpiredKidHeader?: boolean;\n}\n\nexport type CookiesObject = Partial<{[K in string]: string}>;\n\nexport interface GetCookiesTokensOptions {\n  cookieName: string;\n  cookieSignatureKeys: string[];\n}\n"
  },
  {
    "path": "src/next/metadata.ts",
    "content": "import {TokenSet} from '../auth/types.js';\nimport {SetAuthCookiesOptions} from './cookies/types.js';\n\nexport async function getMetadataInternal<Metadata extends object>(\n  tokens: TokenSet,\n  options: SetAuthCookiesOptions<Metadata>\n): Promise<Metadata> {\n  if (!options.getMetadata) {\n    return {} as Metadata;\n  }\n\n  return await options.getMetadata(tokens);\n}\n"
  },
  {
    "path": "src/next/middleware.ts",
    "content": "import type {NextRequest} from 'next/server';\nimport {NextResponse} from 'next/server';\nimport {ServiceAccount} from '../auth/credential.js';\nimport {\n  AuthError,\n  AuthErrorCode,\n  InvalidTokenError,\n  InvalidTokenReason,\n  isInvalidTokenError\n} from '../auth/error.js';\nimport {getFirebaseAuth, handleExpiredToken, Tokens} from '../auth/index.js';\nimport {debug, enableDebugMode} from '../debug/index.js';\nimport {AuthCookies} from './cookies/AuthCookies.js';\nimport {\n  removeAuthCookies,\n  setAuthCookies,\n  SetAuthCookiesOptions\n} from './cookies/index.js';\nimport {RequestCookiesProvider} from './cookies/parser/RequestCookiesProvider.js';\nimport {refreshToken} from './refresh-token.js';\nimport {getRequestCookiesTokens, validateOptions} from './tokens.js';\nimport {getReferer} from './utils.js';\nimport {getMetadataInternal} from './metadata.js';\nimport {mapJwtPayloadToDecodedIdToken} from '../auth/utils.js';\nimport {decodeJwt} from 'jose';\n\nexport interface CreateAuthMiddlewareOptions<Metadata extends object>\n  extends SetAuthCookiesOptions<Metadata> {\n  loginPath: string;\n  logoutPath: string;\n  refreshTokenPath?: string;\n  experimental_createAnonymousUserIfUserNotFound?: boolean;\n}\n\ninterface RedirectToPathOptions {\n  shouldClearSearchParams: boolean;\n}\n\nexport function redirectToPath(\n  request: NextRequest,\n  path: string,\n  options: RedirectToPathOptions = {shouldClearSearchParams: false}\n) {\n  const url = request.nextUrl.clone();\n  url.pathname = path;\n\n  if (options.shouldClearSearchParams) {\n    url.search = '';\n  }\n\n  return NextResponse.redirect(url);\n}\n\ninterface RedirectToHomeOptions {\n  path: string;\n}\n\nexport function redirectToHome(\n  request: NextRequest,\n  options: RedirectToHomeOptions = {\n    path: '/'\n  }\n) {\n  return redirectToPath(request, options.path, {shouldClearSearchParams: true});\n}\n\nexport type Path = string | RegExp;\n\n// @deprecated - Use `Path` instead\nexport type PublicPath = Path;\n\nexport interface RedirectToLoginOptions {\n  path: string;\n  redirectParamKeyName?: string;\n  publicPaths?: Path[];\n  privatePaths?: Path[];\n}\n\nfunction doesRequestPathnameMatchPath(request: NextRequest, path: Path) {\n  if (typeof path === 'string') {\n    return path === getUrlWithoutTrailingSlash(request.nextUrl.pathname);\n  }\n\n  return path.test(getUrlWithoutTrailingSlash(request.nextUrl.pathname));\n}\n\nfunction doesRequestPathnameMatchOneOfPaths(\n  request: NextRequest,\n  paths: Path[]\n) {\n  return paths.some((path) => doesRequestPathnameMatchPath(request, path));\n}\n\nfunction getUrlWithoutTrailingSlash(url: string) {\n  if (url === '/') {\n    return '/';\n  }\n\n  return url.endsWith('/') ? url.slice(0, -1) : url;\n}\n\nfunction createLoginRedirectResponse(\n  request: NextRequest,\n  options: RedirectToLoginOptions\n) {\n  const redirectKey = options.redirectParamKeyName || 'redirect';\n  const url = request.nextUrl.clone();\n  url.pathname = options.path;\n  const encodedRedirect = encodeURIComponent(\n    `${request.nextUrl.pathname}${url.search}`\n  );\n  url.search = `${redirectKey}=${encodedRedirect}`;\n\n  return NextResponse.redirect(url);\n}\n\nexport function redirectToLogin(\n  request: NextRequest,\n  options: RedirectToLoginOptions = {\n    path: '/login',\n    publicPaths: ['/login']\n  }\n) {\n  if (\n    options.publicPaths &&\n    doesRequestPathnameMatchOneOfPaths(request, options.publicPaths)\n  ) {\n    return NextResponse.next();\n  }\n\n  if (\n    options.privatePaths &&\n    !doesRequestPathnameMatchOneOfPaths(request, options.privatePaths)\n  ) {\n    return NextResponse.next();\n  }\n\n  return createLoginRedirectResponse(request, options);\n}\n\nexport async function createAuthMiddlewareResponse<Metadata extends object>(\n  request: NextRequest,\n  options: CreateAuthMiddlewareOptions<Metadata>\n): Promise<NextResponse> {\n  const url = getUrlWithoutTrailingSlash(request.nextUrl.pathname);\n  if (url === getUrlWithoutTrailingSlash(options.loginPath)) {\n    return setAuthCookies(request.headers, options);\n  }\n\n  if (url === getUrlWithoutTrailingSlash(options.logoutPath)) {\n    return removeAuthCookies(request.headers, {\n      cookieName: options.cookieName,\n      cookieSerializeOptions: options.cookieSerializeOptions\n    });\n  }\n\n  if (\n    options.refreshTokenPath &&\n    url === getUrlWithoutTrailingSlash(options.refreshTokenPath)\n  ) {\n    return refreshToken(request, options);\n  }\n\n  return NextResponse.next();\n}\n\nexport type HandleInvalidToken = (\n  reason: InvalidTokenReason\n) => Promise<NextResponse>;\nexport type HandleValidToken<Metadata extends object> = (\n  tokens: Tokens<Metadata>,\n  headers: Headers\n) => Promise<NextResponse>;\nexport type HandleError = (e: unknown) => Promise<NextResponse>;\n\nexport interface AuthMiddlewareOptions<Metadata extends object>\n  extends CreateAuthMiddlewareOptions<Metadata> {\n  serviceAccount?: ServiceAccount;\n  apiKey: string;\n  debug?: boolean;\n  headers?: Headers;\n  checkRevoked?: boolean;\n  handleInvalidToken?: HandleInvalidToken;\n  handleValidToken?: HandleValidToken<Metadata>;\n  handleError?: HandleError;\n  enableTokenRefreshOnExpiredKidHeader?: boolean;\n}\n\nconst defaultInvalidTokenHandler = async () => NextResponse.next();\n\nconst defaultValidTokenHandler = async <Metadata extends object>(\n  _tokens: Tokens<Metadata>,\n  headers: Headers\n) =>\n  NextResponse.next({\n    request: {\n      headers\n    }\n  });\n\nexport async function authMiddleware<Metadata extends object>(\n  request: NextRequest,\n  middlewareOptions: AuthMiddlewareOptions<Metadata>\n): Promise<NextResponse> {\n  const options: AuthMiddlewareOptions<Metadata> = {\n    enableTokenRefreshOnExpiredKidHeader: true,\n    ...middlewareOptions\n  };\n\n  if (options.debug) {\n    enableDebugMode();\n  }\n\n  validateOptions(options);\n\n  const referer = getReferer(request.headers) ?? '';\n  const handleValidToken = options.handleValidToken ?? defaultValidTokenHandler;\n  const handleError = options.handleError ?? defaultInvalidTokenHandler;\n  const handleInvalidToken =\n    options.handleInvalidToken ?? defaultInvalidTokenHandler;\n\n  debug('Handle request', {\n    path: getUrlWithoutTrailingSlash(request.nextUrl.pathname)\n  });\n\n  const authMiddlewareResponseRoutes = [\n    options.loginPath,\n    options.logoutPath,\n    options.refreshTokenPath\n  ]\n    .filter(Boolean)\n    .map((url) => getUrlWithoutTrailingSlash(url as string));\n\n  if (\n    authMiddlewareResponseRoutes.includes(\n      getUrlWithoutTrailingSlash(request.nextUrl.pathname)\n    )\n  ) {\n    debug('Handle authentication API route');\n    return createAuthMiddlewareResponse(request, options);\n  }\n\n  const {verifyIdToken, handleTokenRefresh, createAnonymousUser} =\n    getFirebaseAuth({\n      serviceAccount: options.serviceAccount,\n      apiKey: options.apiKey,\n      tenantId: options.tenantId\n    });\n\n  try {\n    debug('Attempt to fetch request cookies tokens');\n\n    const tokens = await getRequestCookiesTokens<Metadata>(\n      request.cookies,\n      options\n    );\n\n    return await handleExpiredToken(\n      async () => {\n        debug('Verifying user credentials...');\n\n        const decodedToken = await verifyIdToken(tokens.idToken, {\n          checkRevoked: options.checkRevoked,\n          referer\n        });\n\n        debug('Credentials verified successfully');\n\n        const response = await handleValidToken(\n          {\n            token: tokens.idToken,\n            decodedToken,\n            customToken: tokens.customToken,\n            metadata: tokens.metadata\n          },\n          request.headers\n        );\n\n        debug('Successfully handled authenticated response');\n\n        return response;\n      },\n      async () => {\n        debug('Token has expired. Refreshing token...');\n\n        const {idToken, decodedIdToken, refreshToken, customToken} =\n          await handleTokenRefresh(tokens.refreshToken, {\n            referer,\n            enableCustomToken: options.enableCustomToken\n          });\n\n        debug(\n          'Token refreshed successfully. Updating response cookie headers...'\n        );\n\n        const metadata = await getMetadataInternal<Metadata>(\n          {\n            idToken,\n            decodedIdToken,\n            refreshToken,\n            customToken\n          },\n          options\n        );\n\n        const valueToSign = {\n          idToken,\n          refreshToken,\n          customToken,\n          metadata\n        };\n\n        const cookies = new AuthCookies(\n          RequestCookiesProvider.fromHeaders(request.headers),\n          options\n        );\n\n        await cookies.setAuthCookies(valueToSign, request.cookies);\n        const response = await handleValidToken(\n          {token: idToken, decodedToken: decodedIdToken, customToken, metadata},\n          request.headers\n        );\n\n        debug('Successfully handled authenticated response');\n\n        await cookies.setAuthHeaders(valueToSign, response.headers);\n\n        return response;\n      },\n      async (e) => {\n        if (\n          e instanceof AuthError &&\n          e.code === AuthErrorCode.NO_MATCHING_KID\n        ) {\n          throw InvalidTokenError.fromError(e, InvalidTokenReason.INVALID_KID);\n        }\n\n        debug('Authentication failed with error', {error: e});\n\n        return handleError(e);\n      },\n      options.enableTokenRefreshOnExpiredKidHeader ?? false\n    );\n  } catch (error: unknown) {\n    if (isInvalidTokenError(error)) {\n      debug(\n        `Token is missing or has incorrect formatting. This is expected and usually means that user has not yet logged in`,\n        {\n          reason: error.reason\n        }\n      );\n      if (options.experimental_createAnonymousUserIfUserNotFound) {\n        const {idToken, refreshToken} = await createAnonymousUser(\n          options.apiKey\n        );\n\n        const decodedIdToken = mapJwtPayloadToDecodedIdToken(\n          decodeJwt(idToken)\n        );\n\n        const metadata = await getMetadataInternal<Metadata>(\n          {\n            idToken,\n            decodedIdToken,\n            refreshToken\n          },\n          options\n        );\n\n        const valueToSign = {\n          idToken,\n          refreshToken,\n          metadata\n        };\n\n        const cookies = new AuthCookies(\n          RequestCookiesProvider.fromHeaders(request.headers),\n          options\n        );\n\n        await cookies.setAuthCookies(valueToSign, request.cookies);\n\n        const decodedToken = await verifyIdToken(idToken, {\n          checkRevoked: options.checkRevoked,\n          referer\n        });\n\n        const response = await handleValidToken(\n          {token: idToken, decodedToken, metadata},\n          request.headers\n        );\n        await cookies.setAuthHeaders(valueToSign, response.headers);\n\n        return response;\n      }\n      return handleInvalidToken(error.reason);\n    }\n\n    throw error;\n  }\n}\n"
  },
  {
    "path": "src/next/refresh-token.ts",
    "content": "import {NextResponse} from 'next/server';\nimport type {NextRequest} from 'next/server';\nimport {\n  SetAuthCookiesOptions,\n  appendAuthCookies,\n  verifyNextCookies\n} from './cookies/index.js';\nimport {HttpError, isInvalidTokenError} from '../auth/index.js';\n\nexport async function refreshToken<Metadata extends object>(\n  request: NextRequest,\n  options: SetAuthCookiesOptions<Metadata>\n) {\n  try {\n    const result = await verifyNextCookies(\n      request.cookies,\n      request.headers,\n      options\n    );\n\n    const headers: Record<string, string> = {\n      'Content-Type': 'application/json'\n    };\n\n    if (!result) {\n      return new NextResponse(JSON.stringify({idToken: null}), {\n        status: 200,\n        headers\n      });\n    }\n\n    const response = new NextResponse(\n      JSON.stringify({\n        idToken: result.idToken,\n        customToken: result.customToken\n      }),\n      {\n        status: 200,\n        headers\n      }\n    );\n\n    await appendAuthCookies(request.headers, response, result, options);\n\n    return response;\n  } catch (error: unknown) {\n    if (isInvalidTokenError(error)) {\n      return new NextResponse(\n        JSON.stringify({\n          reason: error.reason,\n          message: error.message\n        } as HttpError),\n        {\n          status: 401\n        }\n      );\n    }\n\n    throw error;\n  }\n}\n"
  },
  {
    "path": "src/next/tokens.ts",
    "content": "import type {CookieSerializeOptions} from 'cookie';\nimport {decodeJwt} from 'jose';\nimport {NextApiRequest} from 'next';\nimport type {ReadonlyRequestCookies} from 'next/dist/server/web/spec-extension/adapters/request-cookies';\nimport type {RequestCookies} from 'next/dist/server/web/spec-extension/cookies';\nimport {ServiceAccount} from '../auth/credential.js';\nimport {ParsedCookies} from '../auth/custom-token/index.js';\nimport {isInvalidTokenError} from '../auth/error.js';\nimport {Tokens} from '../auth/index.js';\nimport {mapJwtPayloadToDecodedIdToken} from '../auth/utils.js';\nimport {debug, enableDebugMode} from '../debug/index.js';\nimport {CookieParserFactory} from './cookies/parser/CookieParserFactory.js';\nimport {CookiesObject, GetCookiesTokensOptions} from './cookies/types.js';\n\nexport interface GetTokensOptions extends GetCookiesTokensOptions {\n  cookieSerializeOptions?: CookieSerializeOptions;\n  serviceAccount?: ServiceAccount;\n  apiKey: string;\n  debug?: boolean;\n  enableTokenRefreshOnExpiredKidHeader?: boolean;\n  tenantId?: string;\n}\n\nexport function validateOptions(options: GetTokensOptions) {\n  if (!options.cookieSignatureKeys.length || !options.cookieSignatureKeys[0]) {\n    throw new Error(\n      `Expected cookieSignatureKeys to contain at least one signature key. Received: ${JSON.stringify(\n        options.cookieSignatureKeys\n      )}`\n    );\n  }\n}\n\nexport function getRequestCookiesTokens<Metadata extends object>(\n  cookies: RequestCookies | ReadonlyRequestCookies,\n  options: GetCookiesTokensOptions\n): Promise<ParsedCookies<Metadata>> {\n  const parser = CookieParserFactory.fromRequestCookies<Metadata>(\n    cookies,\n    options\n  );\n\n  return parser.parseCookies();\n}\n\nexport async function getTokens<Metadata extends object>(\n  cookies: RequestCookies | ReadonlyRequestCookies,\n  options: GetTokensOptions\n): Promise<Tokens<Metadata> | null> {\n  const now = Date.now();\n\n  if (options.debug) {\n    enableDebugMode();\n  }\n\n  validateOptions(options);\n\n  try {\n    const tokens = await getRequestCookiesTokens<Metadata>(cookies, options);\n    debug('getTokens: Tokens successfully extracted from cookies');\n    const payload = decodeJwt(tokens.idToken);\n\n    return {\n      token: tokens.idToken,\n      decodedToken: mapJwtPayloadToDecodedIdToken(payload),\n      customToken: tokens.customToken,\n      metadata: tokens.metadata\n    };\n  } catch (error: unknown) {\n    if (isInvalidTokenError(error)) {\n      debug(\n        `Token is missing or has incorrect formatting. This is expected and usually means that user has not yet logged in`,\n        {\n          reason: error.reason\n        }\n      );\n      return null;\n    }\n\n    throw error;\n  } finally {\n    debug(`getTokens: took ${(Date.now() - now) / 1000}ms`);\n  }\n}\n\nexport function getCookiesTokens<Metadata extends object>(\n  cookies: CookiesObject,\n  options: GetCookiesTokensOptions\n): Promise<ParsedCookies<Metadata>> {\n  const parser = CookieParserFactory.fromObject<Metadata>(cookies, options);\n\n  return parser.parseCookies();\n}\n\nexport async function getApiRequestTokens<Metadata extends object>(\n  request: NextApiRequest,\n  options: GetTokensOptions\n): Promise<Tokens<Metadata> | null> {\n  try {\n    const tokens = await getCookiesTokens<Metadata>(request.cookies, options);\n    const payload = decodeJwt(tokens.idToken);\n\n    return {\n      token: tokens.idToken,\n      decodedToken: mapJwtPayloadToDecodedIdToken(payload),\n      customToken: tokens.customToken,\n      metadata: tokens.metadata\n    };\n  } catch (error: unknown) {\n    if (isInvalidTokenError(error)) {\n      return null;\n    }\n\n    throw error;\n  }\n}\n\n/**\n * @deprecated\n * Use `getApiRequestTokens` instead\n */\nexport async function getTokensFromObject<Metadata extends object>(\n  cookies: CookiesObject,\n  options: GetTokensOptions\n): Promise<Tokens<Metadata> | null> {\n  try {\n    const tokens = await getCookiesTokens<Metadata>(cookies, options);\n    const payload = decodeJwt(tokens.idToken);\n\n    return {\n      token: tokens.idToken,\n      decodedToken: mapJwtPayloadToDecodedIdToken(payload),\n      customToken: tokens.customToken,\n      metadata: tokens.metadata\n    };\n  } catch (error: unknown) {\n    if (isInvalidTokenError(error)) {\n      return null;\n    }\n\n    throw error;\n  }\n}\n"
  },
  {
    "path": "src/next/utils.ts",
    "content": "export function getReferer(headers: Headers) {\n  const host =\n    headers.get('X-Forwarded-Host') ?? headers.get('Host') ?? undefined;\n  const protocol = headers.get('X-Forwarded-Proto');\n  const fallback = protocol && host ? `${protocol}://${host}/` : undefined;\n\n  return fallback ?? host;\n}\n"
  },
  {
    "path": "tsconfig.base.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [\"ES2020\", \"DOM\", \"esnext\", \"DOM.iterable\"],\n    \"types\": [],\n    \"strict\": true,\n    \"noUnusedParameters\": true,\n    \"removeComments\": true,\n    \"verbatimModuleSyntax\": false,\n    \"sourceMap\": false,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noImplicitReturns\": true,\n    \"noImplicitThis\": true,\n    \"noImplicitAny\": true,\n    \"strictNullChecks\": true,\n    \"noUnusedLocals\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"./src/**/*.ts\", \"./src/**/*.tsx\",  \"./src/*.tsx\"],\n  \"exclude\": [\"node_modules\", \"**/*.spec.ts\", \"**/*.test.ts\"]\n}\n"
  },
  {
    "path": "tsconfig.browser.json",
    "content": "{\n    \"extends\": \"./tsconfig.base.json\",\n    \"compilerOptions\": {\n      \"target\": \"ES2020\",\n      \"module\": \"ES2020\",\n      \"outDir\": \"browser\",\n      \"paths\": {\n        \"next\": [\"./node_modules/next/index.d.ts\"],\n        \"next/server\": [\"./node_modules/next/server.d.ts\"],\n        \"next/dist/server/web/spec-extension/cookies\": [\"./node_modules/next/dist/server/web/spec-extension/cookies.d.ts\"],\n        \"jose\": [\"./node_modules/jose/dist/types/index.d.ts\"],\n        \"jose/dist/types/util/errors\": [\"./node_modules/jose/dist/types/util/errors.d.ts\"],\n        \"jose/dist/types/jwks/remote\": [\"./node_modules/jose/dist/types/jwks/remote.d.ts\"],\n        \"next/dist/server/web/spec-extension/adapters/request-cookies\": [\"./node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts\"]\n      }\n    }\n  }"
  },
  {
    "path": "tsconfig.esm.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": false,\n    \"module\": \"ES2022\",\n    \"outDir\": \"esm\",\n    \"paths\": {\n      \"next\": [\"./node_modules/next/index.d.ts\"],\n      \"next/server\": [\"./node_modules/next/server.d.ts\"],\n      \"next/dist/server/web/spec-extension/cookies\": [\"./node_modules/next/dist/server/web/spec-extension/cookies.d.ts\"],\n      \"jose\": [\"./node_modules/jose/dist/types/index.d.ts\"],\n      \"jose/dist/types/util/errors\": [\"./node_modules/jose/dist/types/util/errors.d.ts\"],\n      \"jose/dist/types/jwks/remote\": [\"./node_modules/jose/dist/types/jwks/remote.d.ts\"],\n      \"next/dist/server/web/spec-extension/adapters/request-cookies\": [\"./node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts\"]\n    }\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n    \"extends\": \"./tsconfig.base.json\",\n    \"compilerOptions\": {\n      \"types\": [\"node\"],\n      \"target\": \"ES2022\",\n      \"module\": \"CommonJS\",\n      \"outDir\": \"lib\",\n      \"declaration\": true\n    }\n  }"
  },
  {
    "path": "tsconfig.test.json",
    "content": "{\n  \"extends\": \"./tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"types\": [\"node\", \"jest\"],\n    \"target\": \"ES2022\",\n    \"module\": \"CommonJS\",\n    \"outDir\": \"./lib\",\n    \"rootDir\": \"./src\"\n  },\n  \"include\": [\"./src/**/*.ts\", \"./src/**/*.tsx\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  }
]