[
  {
    "path": ".eslintrc.json",
    "content": "{\n    \"parser\": \"@typescript-eslint/parser\",\n    \"parserOptions\": {\n        \"ecmaVersion\": 2020,\n        \"sourceType\": \"module\",\n        \"ecmaFeatures\": {\n            \"jsx\": true\n        }\n    },\n    \"plugins\": [\n        \"@typescript-eslint\",\n        \"security\",\n        \"prettier\"\n    ],\n    \"extends\": [\n        \"eslint:recommended\",\n        \"plugin:@typescript-eslint/eslint-recommended\",\n        \"plugin:@typescript-eslint/recommended\",\n        \"plugin:security/recommended-legacy\",\n        \"plugin:prettier/recommended\",\n        \"prettier\"\n    ],\n    \"ignorePatterns\": [\n        \"**/*.d.ts\",\n        \"**/*.js\",\n        \"coverage/*\",\n        \"lib/*\"\n    ],\n    \"rules\": {\n        \"@typescript-eslint/member-delimiter-style\": 0,\n        \"@typescript-eslint/no-namespace\": [\n            \"warn\"\n        ],\n        \"@typescript-eslint/no-unused-vars\": [\n            \"error\",\n            {\n                \"argsIgnorePattern\": \"^_\"\n            }\n        ],\n        \"no-case-declarations\": 0,\n        \"no-console\": [\n            \"error\",\n            {\n                \"allow\": [\n                    \"warn\",\n                    \"error\"\n                ]\n            }\n        ]\n    }\n}\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: feature-request\nassignees: ''\n\n---\n\n**Is your feature request related to a problem?**\nIf so, make sure your problem hasn't been listed before.\n\n**Describe the solution you'd like**\nComment about what can be improved, or what would you like to happen in response to some action.\n\n**Additional context**\nAdd any other context about the feature request here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/new_bug.md",
    "content": "---\nname: Bug Report\nabout: Create a report for a bug in Privacy Pass browser extension\ntitle: ''\nlabels: triage\nassignees: ''\n\n---\n\nBefore reporting a new bug, verify if your request is already being tracked by another issue: https://github.com/cloudflare/pp-browser-extension/issues.\n\n---\n\nIf you believe that this is a new bug, please proceed to create an issue. The issue will be investigated after you have filled in the following information.\n\n**Describe the bug**\nA clear and concise description of the bug.\n\n**How to reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA description of what is expected to happen.\n\n**System (please complete the following information):**\n - OS: [e.g. iOS/Windows]\n - Attesters configuration from extension options\n - Browser [e.g. Chrome, Firefox]\n - Browser Version [e.g. 79, 80, ]\n - Silk Version [e.g. 4.0.0, 4.0.1 ]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/ui_ux_feedback.md",
    "content": "---\nname: UI/UX Feedback\nabout: We welcome feedback regarding user experience\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Describe** how your experience can be improved.\n\n**Note** that this report does not consider errors or bugs in the extension.\n\nGet some inspiration from these questions:\n - *What do you expect to see when you perform some action?*\n - *There exist some troubles on rendering?*\n - *Would you like browsers have builtin support for Privacy Pass?*\n"
  },
  {
    "path": ".github/workflows/action.yml",
    "content": "---\nname: Silk Privacy Pass Client Extension\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  testing:\n    name: Running on Node v${{ matrix.node-version }}\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node-version: [20, 18]\n    steps:\n      - name: Checking out\n        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4\n      - name: Setup Node v${{ matrix.node-version }}\n        uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2\n        with:\n          node-version: ${{ matrix.node-version }}\n      - name: Installing\n        run: npm ci\n      - name: Linting\n        run: npm run lint\n      - name: Building\n        run: npm run build\n      - name: Testing\n        run: npm test\n  analyze:\n    name: Analyze CodeQL\n    runs-on: ubuntu-latest\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v4\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v3\n      with:\n        languages: javascript-typescript\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v3\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v3\n"
  },
  {
    "path": ".github/workflows/semgrep.yml",
    "content": "on:\n  pull_request: {}\n  workflow_dispatch: {}\n  push: \n    branches:\n      - main\n      - master\n  schedule:\n    - cron: '0 0 * * *'\nname: Semgrep config\njobs:\n  semgrep:\n    name: semgrep/ci\n    runs-on: ubuntu-latest\n    env:\n      SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}\n      SEMGREP_URL: https://cloudflare.semgrep.dev\n      SEMGREP_APP_URL: https://cloudflare.semgrep.dev\n      SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version\n    container:\n      image: semgrep/semgrep\n    steps:\n      - uses: actions/checkout@v4\n      - run: semgrep ci\n"
  },
  {
    "path": ".gitignore",
    "content": "*.swp\n/node_modules\n/dist\n/lib\n/coverage\n"
  },
  {
    "path": ".npmrc",
    "content": "engine-strict=true\n"
  },
  {
    "path": ".prettierrc.json",
    "content": "{\n    \"semi\": true,\n    \"singleQuote\": true,\n    \"tabWidth\": 4,\n    \"trailingComma\": \"all\",\n    \"printWidth\": 100\n}\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "## Contribution Guidelines\n\n### Code Style\n\nCode is written in TypeScript and is automatically formatted with prettier.\n\n```bash\nnpm run format\n```\n\n### Naming convention\n\nIt is recommended to follow style guide for [TypeScript](https://google.github.io/styleguide/tsguide.html).\n"
  },
  {
    "path": "DEVELOPER.md",
    "content": "## Directory Structure\n\n```\npp-browser-extension\n├──📂 platform: Contains platform specific assets, such as manifest for browsers, or brackground and service worker script used by each platform.\n├──📂 public: Contains all the assets which are neither the business logic files nor the style sheets.\n└──📂 src: Contains all the business logic files and the style sheets.\n    └──📂 background: The business logic of the extension service worker.\n    └──📂 common: The shared logic between the extension background script and its options context.\n    └──📂 options: The web app defining option page in the browser settings.\n```\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2017-2022, Privacy Pass Team, Cloudflare, Inc., and other contributors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\nlist of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\nthis list of conditions and the following disclaimer in the documentation\nand/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its contributors\nmay be used to endorse or promote products derived from this software without\nspecific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "[![github release](https://img.shields.io/github/release/cloudflare/pp-browser-extension.svg)](https://github.com/cloudflare/pp-browser-extension/releases/)\n[![Privacy Pass](https://github.com/cloudflare/pp-browser-extension/actions/workflows/action.yml/badge.svg)](https://github.com/cloudflare/pp-browser-extension/actions)\n[![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)\n\n# Silk - Privacy Pass Client for the browser\n\n![Privacy Pass logo](./public/icons/128/gold.png)\n\nThis browser extension implements the client-side of the Privacy Pass protocol providing unlinkable cryptographic tokens.\n\n**Specification:* Compliant with IETF [draft-ietf-privacypass-protocol v11](https://datatracker.ietf.org/doc/draft-ietf-privacypass-protocol/).\n\n**Support:**\n\n* ✅ Public-Verifiable tokens (Blind-RSA)\n* 🚧 Private-Verifiable tokens (VOPRF)\n* 🚧 Batched tokens\n* 🚧 Rate limited tokens\n\n\n## Installation\n\n| **[Chrome][chrome-store]** | **[Firefox][firefox-store]** |\n| -- | -- |\n| [![chrome logo](./public/icons/browser/chrome.png)][chrome-store] | [![firefox logo](./public/icons/browser/firefox.png)][firefox-store] |\n\n## How it works?\n\n**Privacy Pass Attesters:**  🟩 [Cloudflare Research with Turnstile][cf-url]\n\n[cf-url]: https://pp-attester-turnstile.research.cloudflare.com/\n[chrome-store]: https://chrome.google.com/webstore/detail/privacy-pass/ajhmfdgkijocedmfjonnpjfojldioehi/\n[firefox-store]: https://addons.mozilla.org/firefox/addon/privacy-pass/\n\n**Get tokens**\n - If a website requests a Privacy Pass token, the extension is automatically going to request you to perform the associated challenge.\n - One page will open with a challenge to be solved.\n - Solve successfully the challenge and the extension will get **one** token.\n\n\nSee [FAQs](#faqs) and [Known Issues](#known-issues) section: if something is not working as expected.\n\n---\n\n## Installing from Sources\n\nWe recommend to install the extension using the official browser stores listed in [Installation](#Installation) section above. If you want to compile the sources or your browser is not supported, you can install the extension as follows.\n\n### Building\n\n```sh\ngit clone https://github.com/cloudflare/pp-browser-extension\nnvm use 20\nnpm ci\nnpm run build\n```\n\nOnce these steps complete, the `dist` folder will contain all files required to load the extension.\n\n### Running Tests\n\n```sh\nnvm use 20\nnpm ci\nnpm test\n```\n\n### Manually Loading Extension\n\n#### Firefox\n\n1. Open Firefox and navigate to [about:debugging#/runtime/this-firefox/](about:debugging#/runtime/this-firefox/)\n1. Click on 'Load Temporary Add-on' button.\n1. Select `manifest.json` from the `dist` folder.\n1. Check extension logo appears in the top-right corner of the browser.\n\n#### Chrome\n\n1. Open Chrome and navigate to [chrome://extensions/](chrome://extensions/)\n1. Turn on the 'Developer mode' on the top-right corner.\n1. Click on 'Load unpacked' button.\n1. Select the `dist` folder.\n1. Check extension logo appears in the top-right corner of the browser.\n1. If you cannot see the extension logo, it's likely not pinned to the toolbar.\n\n#### Edge\n\n-   Open Edge and navigate to [edge://extensions/](edge://extensions/)\n-   Turn on the 'Developer mode' on the left bar.\n-   Click on 'Load unpacked' button in the main panel.\n-   Select the `dist` folder.\n-   The extension will appear listed in the main panel.\n-   To see the extension in the bar, click in the puzzle icon and enable it, so it gets pinned to the toolbar.\n---\n\n### Highlights\n\n**2023** -- The extension updates to Privacy Pass Protocol draft 16, with the cryptographic part in a dedicated library [cloudflare/privacypass-ts](https://github.com/cloudflare/privacypass-ts). Introducing the notion of Attesters and Issuers.\n\n**2022** -- The Privacy Pass protocol can also use RSA blind signatures.\n\n**2021** -- In this [blog post](https://blog.cloudflare.com/privacy-pass-v3), we announced the [v3](https://github.com/cloudflare/pp-browser-extension/tree/v3.0.0) version of this extension, which makes the code base more resilient, extensible, and maintainable.\n\n**2020** -- The CFRG (part of IRTF/IETF) started a [working group](https://datatracker.ietf.org/wg/privacypass/about/) seeking for the standardization of the Privacy Pass protocol.\n\n**2019** -- The CAPTCHA provider [hCaptcha](https://www.hcaptcha.com/privacy-pass) announced support for Privacy Pass, and the [v2](https://github.com/cloudflare/pp-browser-extension/tree/2.0.0) version was released.\n\n**2018** -- The Privacy Pass protocol is based on a _Verifiable, Oblivious Pseudorandom Function_ (VOPRF) first established by [Jarecki et al. 2014](https://eprint.iacr.org/2014/650.pdf). The details of the protocol were published at [PoPETS 2018](https://doi.org/10.1515/popets-2018-0026) paper authored by Alex Davidson, Ian Goldberg, Nick Sullivan, George Tankersley, and Filippo Valsorda. Its homepage is still available at [https://privacypass.github.io](https://privacypass.github.io/).\n\n#### Acknowledgements\n\nThe creation of the Privacy Pass protocol was a joint effort by the team made up of George Tankersley, Ian Goldberg, Nick Sullivan, Filippo Valsorda, and Alex Davidson.\n\nThe Privacy Pass team would like to thank Eric Tsai for creating the logo and extension design, Dan Boneh for helping us develop key parts of the protocol, as well as Peter Wu and Blake Loring for their helpful code reviews. We would also like to acknowledge Sharon Goldberg, Christopher Wood, Peter Eckersley, Brian Warner, Zaki Manian, Tony Arcieri, Prateek Mittal, Zhuotao Liu, Isis Lovecruft, Henry de Valence, Mike Perry, Trevor Perrin, Zi Lin, Justin Paine, Marek Majkowski, Eoin Brady, Aaran McGuire, Suphanat Chunhapanya, Armando Faz Hernández, Benedikt Wolters, Maxime Guerreiro, Cefan Rubin, Thibault Meunier and many others who were involved in one way or another and whose efforts are appreciated.\n\n---\n\n## FAQs\n\n#### As a user, how can I add new attestation methods\n\nDepending on your browser settings, the local storage of your browser may be cleared when it is restarted. Privacy Pass stores passes in local storage and so these will also be cleared. This behavior may also be observed if you clear out the cache of your browser.\n\n#### My website support Privacy Pass authentication scheme, but the extension does nothing\n\nThis can be an issuer issue, or a Chrome issue. In the later case, make sure you implement a [client replay on your website](#chrome-support-via-client-replay-api).\n\n#### As a service operator, how to roll out out my own attestation method\n\nPrivacy Pass does not propose a standard attester API. This extension relies on attester to implement the [cloudflare/pp-attester](https://github.com/cloudflare/pp-attester) API.\n\nIf you have such an attester, you should ask your user to update their attesters in the extension options. The attester order matters, first one has a higher priority than the second.\n\n---\n\n## Known Issues\n\n#### Extensions that modify user-agent or headers\n\nThere is a [conflict resolution](https://developer.chrome.com/docs/extensions/reference/webRequest/#conflict-resolution) happening when more than one extension tries to modify the headers of a request. According to documentation, the more recent installed extension is the one that can update headers, while others will fail.\n\nCompounded to that, Cloudflare will ignore clearance cookies when the user-agent request does not match the one used when obtaining the cookie.\n\n## Chrome support via Client replay API\n\n### Overview\n\nChrome does not allow extensions to block a request and perform another action, such as completing an attestation flow. To this extent, the extension enables websites to orchestrate a client side replay as defined below.\n\n### Requirements\n\nYour website fetches subressources with JavaScript. If your server returns a challenge on the main frame, the extension automatically refreshes the page without any action needed from your side.\n\n### System definition\n\nOn every request, the extension adds header `Private-Token-Client-Replay: <requestID>`. `requestID` is a UUID identifying your next replay attempt.\nFor resources you know need to be replayed because they contain a `WWW-Authenticate: PrivateToken ...` header, you are going to use this `requestID` to query the extension on the state of the associated token retrieval. Once the extension has retrieved the token, you can replay the request.\n\nGiven a `requestID`, you can query `https://no-reply.private-token.research.cloudflare.com/requestID/<requestID>`. The domain `no-reply.private-token.research.cloudflare.com` [does not resolve](https://dnsviz.net/d/no-reply.private-token.research.cloudflare.com/dnssec/) to an IP address, and is captured by the browser extension for replay purposes. Its reply is always going to be a redirect to a [\"data\" URL](https://www.rfc-editor.org/rfc/rfc2397#section-2) of the form `data:text/plain,<status>`.\n\n**`GET /requestID/<requestID>`**\n* Response URL: `data:text/plain,<status>`\n* `status` is `pending`, `fulfilled`, `not-found`\n\nYour website should do the following depending on the returned `status`:\n* if `pending`, wait and query `/requestID/<requestID>` again in the future\n* if `fulfilled`, replay the initial request\n* if `not-found`, cancel the initial request\n\nA sequence diagram illustrate the flow below\n\n```mermaid\nsequenceDiagram\n\tparticipant E as Client\n\tparticipant O as Origin\n\tparticipant B as Extension\n\t\n\tE->>O: GET example.com/img.png\n\tO->>B: WWW-Authenticate: challenge=[x]\n\tNote over B: Cannot block request\n\tB->>B: Adds \"Private-Token-Client-Replay: requestID\"\n\tB->>E: 401 Unauthorized + \"Private-Token-Client-Replay: requestID\"\n\tpar Extension fetches token\n        Note over B: Interact with Attester to retrieve Token [x]\n\tand Client wait\n        E->>E: Check \"Private-Token-Client-Replay\" header\n        E->>B: GET /requestID/<requestID>\n        B->>E: \"200 data:text/plain,pending\"\n\tend\n    B->>E: \"200 data:text/plain,fulfilled\"\n    Note over E: Extension is ready to query the Origin\n    Note over B: Extension is intercepting request to example.com\n    E->>B: GET example.com/img.png\n\tB->>O: GET example.com/img.png + Authorization [x]\n\tO->>E: <data>Cat Picture</data>\n```\n\n### Design considerations\n\nThe proposed replay mechanism has been contrained by [Chrome API reference](https://developer.chrome.com/docs/extensions/reference/), specifically the [declarativeNetRequest API](https://developer.chrome.com/docs/extensions/reference/declarativeNetRequest/).\n\nSpecifically, the following choices have been engineered as follow\n* Use a dedicated replay domain to allow any website to query the extension without hardcoding its ID\n* Use a dedicated replay domain that does not resolve to prevent DoS risks, and ensure there are no traffic interception with a faulty extension\n* Have every parameter, namely `requestID` be part of the URL to allow for a synchronous response from the extension, headers not being available on every event\n* Redirect to a [\"data\" URL](https://www.rfc-editor.org/rfc/rfc2397#section-2) to prevent DNS resolution and other extensions interference\n* Return `Private-Token-Client-Replay: <requestID>` on every request due to the inability to dynamically append headers to responses in MV3\n* Use a pull based method to ease implementation on the extension and origin side\n"
  },
  {
    "path": "devel.md",
    "content": "## Setting Up Chrome Extension for Development\n\nIn a terminal:\n\n```sh\n$ git clone https://github.com/cloudflare/pp-browser-extension\n$ nvm use 20 // it should work with Node v16+.\n$ npm ci\n$ npm run build\n```\n\nIn Chrome:\n\n1. Remove all extensions of Privacy pass. current public extension is v3.0.5.\n\n1. We will install extension v4.0.0 which has **Manifest v3**.\n\n1. Load the extension from the source code.\n\n1. Upon loading, you should see Privacy Pass extension with a red logo. This means you are using the local build.\n\n1. Open the Service worker & Devtools of a blank tab.\n\n1. Navigate to https://demo-pat.research.cloudflare.com/login.\n\n1. The behaviour expected is that browser first receives a 401 error, which is catched by the extension and then a reload that brings the body 'Token OK'. Chrome extension uses a replay mechanism documented in the [README](./README.md) as request blocking is not supported."
  },
  {
    "path": "devel_pat.md",
    "content": "## Setting Up PAT in Local Machine.\n\n```sh\n $ git clone https://github.com/armfazh/pat-app --branch devel_branch\ncd pat-app\n```\n\n\n```sh\n $ make certs\n ```\n Make sure to follow the instructions in README for setting up certificates in your machine using mkcert.\n\n\nIn three terminals:\n - `make issuer`\n - `make origin`\n - `make attester`\n\nRunning Go client\n\n```sh\n$ ./pat-app fetch --origin origin.example:4568 --secret `cat client.secret` --attester attester.example:4569 --resource \"/index.html\" --token-type basic\n```\n\nWhen the client is run, it fetches the privacypass webpage from origin using 'basic' (Type 2) tokens. (We will use Type 2 tokens initially, so it must be explicitly specified in the command/URL)\n\nA succesfull run looks like:\n\n```sh\n$ ./pat-app fetch --origin origin.example:4568 --secret `cat client.secret` --attester attester.example:4569 --resource \"/index.html\" --token-type basic\nbody was fetched!!\n<!doctype\n```\n\n---\n\n## Setting Up Chrome Extension\n\nIn a terminal:\n\n```sh\n$ git clone https://github.com/cloudflare/pp-browser-extension\n$ nvm use 18 // it should work with Node v16+.\n$ npm ci\n$ npm run build\n```\n\nIn Chrome:\n\n1. Remove all extensions of privacy pass. Current extension is v3.0.5.\n\n1. We will install ext v4.1.0\nwhich has **Manifest v3**.\n\n1. Load the extension from the source code.\n\n1. Open the Background Page & Devtools of a blank tab.\n\n1. Navigate to https://origin.example:4568/?type=2\n(Make sure the certificates from `mkcert` step work, otherwise cannot load https from localhost). As you can see, we must specify type=2.\n\n1. The behaviour expected is that browser first receives a 401 error, which is catched by the extension and then a reload that brings the body of the privaypass website. *There could be some errors fetching other resources (.css files), but these are not served by the pat-app demo.\n"
  },
  {
    "path": "jest.config.mjs",
    "content": "export default {\n    moduleFileExtensions: ['js'],\n    transform: {},\n    setupFiles: ['./jest.setup.mjs'],\n    collectCoverage: true,\n    verbose: true,\n};\n"
  },
  {
    "path": "jest.setup.mjs",
    "content": "// Mocking crypto with Node WebCrypto API.\nimport { webcrypto } from 'node:crypto';\n\nif (typeof crypto === 'undefined') {\n    global.crypto = webcrypto;\n}\n\nif (typeof chrome === 'undefined') {\n    global.chrome = {\n        declarativeNetRequest: {\n            ResourceType: {\n                MAIN_FRAME: 'main_frame',\n                SUB_FRAME: 'sub_frame',\n                XMLHTTPREQUEST: 'xmlhttprequest',\n            },\n        },\n    };\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n    \"name\": \"silk-privacy-pass-client\",\n    \"version\": \"4.0.2\",\n    \"contributors\": [\n        \"Suphanat Chunhapanya <pop@cloudflare.com>\",\n        \"Armando Faz <armfazh@cloudflare.com>\",\n        \"Thibault Meunier <thibault@cloudflare.com>\",\n        \"Cefan Rubin <cefan@cloudflare.com>\"\n    ],\n    \"main\": \"index.js\",\n    \"license\": \"BSD-3-Clause\",\n    \"type\": \"module\",\n    \"engines\": {\n        \"node\": \">=18\"\n    },\n    \"scripts\": {\n        \"build:options\": \"esbuild src/options/index.ts --bundle --minify --format=esm --target=chrome100 --outfile=dist/build/options/index.mjs && cp src/options/index.html src/options/index.css dist/build/options/\",\n        \"build:privacypass\": \"npm run build:options && esbuild platform/$PLATFORM/index.ts --bundle --format=esm --target=chrome100 --outfile=dist/$PLATFORM/background.js && cp -r public/icons dist/$PLATFORM/icons && cp platform/$PLATFORM/manifest.json dist/$PLATFORM && mkdir -p dist/$PLATFORM/options && cp -r dist/build/options/* dist/$PLATFORM/options\",\n        \"build:chromium\": \"export PLATFORM='chromium' && npm run build:privacypass\",\n        \"build:firefox\": \"export PLATFORM='firefox' && npm run build:privacypass\",\n        \"build:mv3-chromium\": \"export PLATFORM='mv3/chromium' && npm run build:privacypass\",\n        \"build\": \"npm run build:chromium && npm run build:firefox && npm run build:mv3-chromium\",\n        \"bundle:chromium\": \"npm run build:chromium && mkdir -p dist/build/bundle/chromium && cp -r dist/chromium/* dist/build/bundle/chromium && sed -i '/\\\"key\\\"/d' dist/build/bundle/chromium/manifest.json && web-ext build -s dist/build/bundle/chromium --overwrite-dest -a dist/web-ext-artifacts -n chromium.zip\",\n        \"bundle:mv3-chromium\": \"npm run build:mv3-chromium && mkdir -p dist/build/bundle/mv3/chromium && cp -r dist/mv3/chromium/* dist/build/bundle/mv3/chromium && sed -i '/\\\"key\\\"/d' dist/build/bundle/mv3/chromium/manifest.json && web-ext build -s dist/build/bundle/mv3/chromium --overwrite-dest -a dist/web-ext-artifacts -n mv3-chromium.zip\",\n        \"bundle:firefox\": \"npm run build:firefox && web-ext build -s dist/firefox --overwrite-dest -a dist/web-ext-artifacts -n firefox.zip\",\n        \"bundle\": \"npm run bundle:chromium && npm run bundle:firefox && npm run bundle:mv3-chromium\",\n        \"test\": \"tsc -b && node --experimental-vm-modules node_modules/jest/bin/jest.js --ci\",\n        \"lint\": \"eslint .\",\n        \"format\": \"eslint . --fix\",\n        \"clean\": \"rimraf lib coverage dist\"\n    },\n    \"dependencies\": {\n        \"@cloudflare/privacypass-ts\": \"0.4.0\"\n    },\n    \"devDependencies\": {\n        \"@types/chrome\": \"0.0.263\",\n        \"@types/jest\": \"29.5.12\",\n        \"@types/node\": \"20.11.25\",\n        \"@typescript-eslint/eslint-plugin\": \"7.1.1\",\n        \"@typescript-eslint/parser\": \"7.1.1\",\n        \"esbuild\": \"0.25.0\",\n        \"eslint\": \"8.57.0\",\n        \"eslint-config-prettier\": \"9.1.0\",\n        \"eslint-plugin-prettier\": \"5.1.3\",\n        \"eslint-plugin-security\": \"2.1.1\",\n        \"jest\": \"29.7.0\",\n        \"prettier\": \"3.2.5\",\n        \"rimraf\": \"5.0.5\",\n        \"web-ext\": \"8.10.0\"\n    }\n}\n"
  },
  {
    "path": "platform/chromium/index.ts",
    "content": "import {\n    BROWSERS,\n    handleBeforeRequest,\n    handleBeforeSendHeaders,\n    handleHeadersReceived,\n    handleInstall,\n    handleStartup,\n} from '../../src/background';\n\nconst BROWSER = BROWSERS.CHROME;\nconst STORAGE = chrome.storage.local;\n\nchrome.runtime.onInstalled.addListener(handleInstall(STORAGE));\n\nchrome.runtime.onStartup.addListener(handleStartup(STORAGE));\n\nchrome.webRequest.onBeforeRequest.addListener(handleBeforeRequest(), { urls: ['<all_urls>'] }, [\n    'blocking',\n]);\n\nchrome.webRequest.onBeforeSendHeaders.addListener(\n    handleBeforeSendHeaders(STORAGE),\n    { urls: ['<all_urls>'] },\n    ['requestHeaders', 'blocking', 'extraHeaders'],\n);\n\nchrome.webRequest.onHeadersReceived.addListener(\n    handleHeadersReceived(BROWSER, STORAGE),\n    { urls: ['<all_urls>'] },\n    ['responseHeaders', 'blocking'],\n);\n"
  },
  {
    "path": "platform/chromium/manifest.json",
    "content": "{\n    \"name\": \"Silk - Privacy Pass Client\",\n    \"manifest_version\": 2,\n    \"description\": \"Client support for Privacy Pass anonymous authorization protocol.\",\n    \"version\": \"4.0.2\",\n    \"icons\": {\n        \"32\": \"icons/32/gold.png\",\n        \"48\": \"icons/48/gold.png\",\n        \"64\": \"icons/64/gold.png\",\n        \"128\": \"icons/128/gold.png\"\n    },\n    \"background\": {\n        \"scripts\": [\n            \"background.js\"\n        ]\n    },\n    \"permissions\": [\n        \"<all_urls>\",\n        \"storage\",\n        \"tabs\",\n        \"webRequest\",\n        \"webRequestBlocking\"\n    ],\n    \"options_ui\": {\n        \"page\": \"options/index.html\"\n    }\n}\n"
  },
  {
    "path": "platform/chromium/tsconfig.json",
    "content": "{\n    \"extends\": \"../../tsconfig.json\",\n    \"compilerOptions\": {\n        \"resolveJsonModule\": true\n    },\n    \"include\": [\n        \".\",\n        \"../../src/background\",\n        \"../../src/common\"\n    ]\n}"
  },
  {
    "path": "platform/firefox/global.d.ts",
    "content": "declare const browser: typeof chrome;"
  },
  {
    "path": "platform/firefox/index.ts",
    "content": "import {\n    BROWSERS,\n    handleBeforeSendHeaders,\n    handleHeadersReceived,\n    handleInstall,\n    handleStartup,\n} from '../../src/background';\n\nconst BROWSER = BROWSERS.FIREFOX;\nconst STORAGE = browser.storage.local;\n\nchrome.runtime.onInstalled.addListener(handleInstall(STORAGE));\n\nchrome.runtime.onStartup.addListener(handleStartup(STORAGE));\n\nchrome.webRequest.onBeforeSendHeaders.addListener(\n    handleBeforeSendHeaders(STORAGE),\n    { urls: ['<all_urls>'] },\n    ['requestHeaders', 'blocking'],\n);\n\nchrome.webRequest.onHeadersReceived.addListener(\n    handleHeadersReceived(BROWSER, STORAGE),\n    { urls: ['<all_urls>'] },\n    ['responseHeaders', 'blocking'],\n);\n"
  },
  {
    "path": "platform/firefox/manifest.json",
    "content": "{\n    \"name\": \"Silk - Privacy Pass Client\",\n    \"manifest_version\": 2,\n    \"description\": \"Client support for Privacy Pass anonymous authorization protocol.\",\n    \"version\": \"4.0.2\",\n    \"icons\": {\n        \"32\": \"icons/32/gold.png\",\n        \"48\": \"icons/48/gold.png\",\n        \"64\": \"icons/64/gold.png\",\n        \"128\": \"icons/128/gold.png\"\n    },\n    \"background\": {\n        \"scripts\": [\n            \"background.js\"\n        ]\n    },\n    \"permissions\": [\n        \"<all_urls>\",\n        \"storage\",\n        \"tabs\",\n        \"webRequest\",\n        \"webRequestBlocking\"\n    ],\n    \"options_ui\": {\n        \"page\": \"options/index.html\"\n    },\n    \"browser_specific_settings\": {\n        \"gecko\": {\n            \"id\": \"{48748554-4c01-49e8-94af-79662bf34d50}\"\n        }\n    }\n}"
  },
  {
    "path": "platform/firefox/tsconfig.json",
    "content": "{\n    \"extends\": \"../../tsconfig.json\",\n    \"compilerOptions\": {\n        \"resolveJsonModule\": true\n    },\n    \"include\": [\n        \".\",\n        \"../../src/background\",\n        \"../../../src/common\"\n    ]\n}\n"
  },
  {
    "path": "platform/mv3/chromium/index.ts",
    "content": "import {\n    BROWSERS,\n    handleBeforeRequest,\n    handleBeforeSendHeaders,\n    handleHeadersReceived,\n    handleInstall,\n    handleStartup,\n} from '../../../src/background';\n\nconst BROWSER = BROWSERS.CHROME;\nconst STORAGE = chrome.storage.local;\n\nchrome.runtime.onInstalled.addListener(handleInstall(STORAGE));\n\nchrome.runtime.onStartup.addListener(handleStartup(STORAGE));\n\nchrome.webRequest.onBeforeRequest.addListener(handleBeforeRequest(), { urls: ['<all_urls>'] });\n\nchrome.webRequest.onBeforeSendHeaders.addListener(\n    handleBeforeSendHeaders(STORAGE),\n    { urls: ['<all_urls>'] },\n    ['requestHeaders'],\n);\n\nchrome.webRequest.onHeadersReceived.addListener(\n    handleHeadersReceived(BROWSER, STORAGE),\n    { urls: ['<all_urls>'] },\n    ['responseHeaders'],\n);\n"
  },
  {
    "path": "platform/mv3/chromium/manifest.json",
    "content": "{\n    \"name\": \"Silk - Privacy Pass Client\",\n    \"manifest_version\": 3,\n    \"key\": \"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+7N/GLYPSBqf+O7fGOaLQ+3hIvXrhsjo8Pq73aSVw08YZRWIIsM+xRnaqdgcu3dXgQGjuVbFVD0nQzg2ymOJk40PSZ/EDdbmWmvD6IrKV88qdmLAlMCvgYW6oRQG5EIBNrmUOqQvTHpdaHrYxY3g4OQfaCe5mvYS64VG6Tb9f1G/UqUcNQzdo1A7x6ElCMjl+isMkt1in749KxgCmhHlZshQfCf1tXwcjhh09luatwP+KCFqCw0VPNMij1eBHi+Z1r43o1RNSTVoiot4mXHMej/rZiApG3U0eVfhp6yOLLakNGnzy1hR5OO00vlMVSyg2HR4VnvPyJuZ+VRNGBo7wIDAQAB\",\n    \"description\": \"Client support for Privacy Pass anonymous authorization protocol.\",\n    \"version\": \"4.0.2\",\n    \"icons\": {\n        \"32\": \"icons/32/gold.png\",\n        \"48\": \"icons/48/gold.png\",\n        \"64\": \"icons/64/gold.png\",\n        \"128\": \"icons/128/gold.png\"\n    },\n    \"background\": {\n        \"service_worker\": \"background.js\",\n        \"type\": \"module\"\n    },\n    \"permissions\": [\n        \"declarativeNetRequest\",\n        \"storage\",\n        \"tabs\",\n        \"webRequest\"\n    ],\n    \"options_ui\": {\n        \"page\": \"options/index.html\"\n    },\n    \"host_permissions\": [\n        \"<all_urls>\"\n    ]\n}\n"
  },
  {
    "path": "platform/mv3/chromium/tsconfig.json",
    "content": "{\n    \"extends\": \"../../../tsconfig.json\",\n    \"compilerOptions\": {\n        \"resolveJsonModule\": true\n    },\n    \"include\": [\n        \".\",\n        \"../../../src/background\",\n        \"../../../src/common\"\n    ]\n}"
  },
  {
    "path": "src/background/attesters.ts",
    "content": "export { refreshAttesterLookupByIssuerKey } from '../common';\nimport { STORAGE_ID_ATTESTER_CONFIGURATION } from './const';\n\n// return an attester challenge URI based on the public key presented in 401 response\nexport const keyToAttesterURI = async (\n    storage: chrome.storage.StorageArea,\n    key: string,\n): Promise<string | undefined> => {\n    // get attester hostname that corresponds to this key\n    const storageItem: Record<string, Record<string, string>> = await new Promise((resolve) =>\n        storage.get([STORAGE_ID_ATTESTER_CONFIGURATION], resolve),\n    );\n\n    const attestersByIssuerKey: Record<string, string> =\n        storageItem[STORAGE_ID_ATTESTER_CONFIGURATION];\n\n    return attestersByIssuerKey[key];\n};\n"
  },
  {
    "path": "src/background/const.ts",
    "content": "export * from '../common';\n\nexport const BROWSERS = {\n    CHROME: 'Chrome',\n    FIREFOX: 'Firefox',\n    EDGE: 'Edge',\n} as const;\nexport type BROWSERS = (typeof BROWSERS)[keyof typeof BROWSERS];\n\nexport const PRIVACY_PASS_API_REPLAY_HEADER = 'private-token-client-replay';\nexport const PRIVACY_PASS_API_REPLAY_URI = 'https://no-reply.private-token.research.cloudflare.com';\n"
  },
  {
    "path": "src/background/index.ts",
    "content": "import { keyToAttesterURI, refreshAttesterLookupByIssuerKey } from './attesters';\nimport { SERVICE_WORKER_MODE, getRawSettings, getSettings } from '../common';\nimport { BROWSERS, PRIVACY_PASS_API_REPLAY_HEADER, PRIVACY_PASS_API_REPLAY_URI } from './const';\nimport { getLogger } from '../common/logger';\nimport { fetchPublicVerifToken } from './pubVerifToken';\nimport { REPLAY_STATE, getRequestID, setReplayDomainRule } from './replay';\nimport { getAuthorizationRule, getIdentificationRule, removeAuthorizationRule } from './rules';\nimport { QueryablePromise, isManifestV3, promiseToQueryable, uint8ToB64URL } from './util';\n\nimport { PrivateToken, TOKEN_TYPES } from '@cloudflare/privacypass-ts';\n\nexport { BROWSERS, PRIVACY_PASS_API_REPLAY_URI } from './const';\n\ninterface SessionCachedData {\n    [key: string]: UrlOriginTabs;\n}\n\ninterface UrlOriginTabs {\n    [key: number]: OriginAttesterTabDetails;\n}\n\ninterface OriginAttesterTabDetails {\n    attesterTabId: number;\n    attestationData: string;\n}\n\nconst PENDING = REPLAY_STATE.PENDING;\ntype PENDING = typeof PENDING;\nconst TOKENS: Record<string, string | PENDING> = {};\n\nconst cachedTabs: Record<number, chrome.tabs.Tab> = {};\n\nexport const headerToToken = async (\n    url: string,\n    tabId: number,\n    header: string,\n    storage: chrome.storage.StorageArea,\n): Promise<string | undefined> => {\n    const { serviceWorkerMode: mode } = getSettings();\n    const logger = getLogger(mode);\n\n    const tokenDetails = PrivateToken.parse(header);\n    if (tokenDetails.length === 0) {\n        return undefined;\n    }\n\n    const td = tokenDetails.slice(-1)[0];\n    switch (td.challenge.tokenType) {\n        case TOKEN_TYPES.BLIND_RSA.value:\n            logger.debug(`type of challenge: ${td.challenge.tokenType} is supported`);\n\n            // Slow down if demo\n            if (mode === SERVICE_WORKER_MODE.DEMO) {\n                await new Promise((resolve) => setTimeout(resolve, 5 * 1000));\n            }\n\n            const tokenPublicKey: string = uint8ToB64URL(td.tokenKey);\n\n            let attesterURI = await keyToAttesterURI(storage, tokenPublicKey);\n            if (!attesterURI) {\n                return undefined;\n            }\n\n            // API expects interactive Challenge at /challenge\n            attesterURI = `${attesterURI}/challenge`;\n\n            // To minimize the number of tabs opened, we check if the requesting tab is the focused tab\n            const tabInfo: chrome.tabs.Tab | undefined = await new Promise((resolve) => {\n                chrome.tabs.get(tabId, resolve);\n            });\n            const tabWindow: chrome.windows.Window | undefined = await new Promise((resolve) => {\n                if (tabInfo) {\n                    chrome.windows.get(tabInfo.windowId, resolve);\n                }\n            });\n            if (!tabWindow?.focused || !tabInfo?.active) {\n                logger.debug('Not opening a new tab due to requesting tab not being active');\n                return undefined;\n            }\n            const tab: chrome.tabs.Tab = await new Promise((resolve) =>\n                chrome.tabs.create({ url: attesterURI }, resolve),\n            );\n\n            // save this new tabId of attester tab to session storage under the originTabId\n\n            const existing: SessionCachedData = await new Promise((resolve) =>\n                storage.get(url, resolve),\n            );\n\n            if (existing[url][tabId]) {\n                existing[url][tabId]['attesterTabId'] = tab.id!;\n                storage.set({ [url]: existing[url] });\n            }\n\n            const token = await fetchPublicVerifToken(td, tabId, storage);\n\n            const encodedToken = uint8ToB64URL(token.serialize());\n\n            return encodedToken;\n\n        default:\n            logger.error(`unrecognized type of challenge: ${td.challenge.tokenType}`);\n    }\n    return undefined;\n};\n\nexport const handleInstall =\n    (storage: chrome.storage.StorageArea) => async (_details: chrome.runtime.InstalledDetails) => {\n        const { serviceWorkerMode: mode } = await getRawSettings(storage);\n        const logger = getLogger(mode);\n\n        if (isManifestV3(chrome)) {\n            chrome.declarativeNetRequest\n                .updateSessionRules(removeAuthorizationRule())\n                .catch((e: unknown) => logger.debug(`failed to remove session rules:`, e));\n        }\n\n        // Refresh lookup of attester by issuer key lookup used for auto selection of attester\n        handleStartup(storage);\n        getRequestID();\n        setReplayDomainRule(REPLAY_STATE.NOT_FOUND);\n    };\n\nexport const handleStartup = (storage: chrome.storage.StorageArea) => async () => {\n    const { serviceWorkerMode: mode } = await getRawSettings(storage);\n    const logger = getLogger(mode);\n\n    const alarmName = 'refreshAttesterLookupByIssuerKey';\n    chrome.alarms.clear(alarmName);\n    chrome.alarms.create(alarmName, {\n        delayInMinutes: 0,\n        periodInMinutes: 24 * 60, // trigger once a day\n    });\n    chrome.alarms.onAlarm.addListener((alarm) => {\n        if (alarm.name === alarmName) {\n            logger.debug('refreshing attester lookup by issuer key');\n            refreshAttesterLookupByIssuerKey(storage);\n        }\n    });\n};\n\nconst pendingRequests: Map<string, QueryablePromise<void>> = new Map();\n\nexport const handleBeforeRequest = () => (details: chrome.webRequest.WebRequestBodyDetails) => {\n    const settings = getSettings();\n    const { serviceWorkerMode: mode } = settings;\n    const logger = getLogger(mode);\n    try {\n        chrome.tabs.get(details.tabId, (tab) => (cachedTabs[details.tabId] = tab));\n    } catch (err) {\n        logger.debug(err);\n    }\n\n    // Handle active replay without generating a network request\n    const url = new URL(details.url);\n    if (url.origin !== PRIVACY_PASS_API_REPLAY_URI) {\n        return;\n    }\n    const labels = url.pathname.split('/');\n    if (labels.length !== 2 || labels[0] !== '' || labels[1] !== 'requestID') {\n        return { redirectUrl: `data:text/plain,${REPLAY_STATE.NOT_FOUND}` };\n    }\n\n    const requestID = labels[2];\n    const promise = pendingRequests.get(requestID);\n    let state: REPLAY_STATE = REPLAY_STATE.NOT_FOUND;\n    if (promise?.isFullfilled) {\n        state = REPLAY_STATE.FULFILLED;\n        pendingRequests.delete(requestID);\n    }\n    if (promise?.isPending) {\n        state = REPLAY_STATE.PENDING;\n    }\n    return {\n        redirectUrl: `data:text/plain,${state}`,\n    };\n};\n\nexport const handleBeforeSendHeaders =\n    (storage: chrome.storage.StorageArea) =>\n    (\n        details: chrome.webRequest.WebRequestHeadersDetails,\n    ): void | chrome.webRequest.BlockingResponse => {\n        if (!details) {\n            return;\n        }\n\n        const ppToken = TOKENS[details.url];\n        if (ppToken === PENDING) {\n            return;\n        }\n        if (ppToken && !chrome.declarativeNetRequest) {\n            const headers = details.requestHeaders ?? [];\n            headers.push({ name: 'Authorization', value: `PrivateToken token=${ppToken}` });\n            delete TOKENS[details.url];\n            return { requestHeaders: headers };\n        }\n\n        if (!details.requestHeaders) {\n            return;\n        }\n\n        if (details.requestHeaders && details.url) {\n            // check for an attestation data sent from attester\n            const pp_hdr = details.requestHeaders.find(\n                (x) => x.name.toLowerCase() === 'private-token-attester-data',\n            )?.value;\n            if (pp_hdr) {\n                const callback = (url_tab_data: SessionCachedData) => {\n                    // if we opened an attesterTab that matches the source of this data then store it\n                    lookForAttesterTabId: for (const url in url_tab_data) {\n                        for (const originTab of Object.values(url_tab_data[url])) {\n                            const tabDetails: OriginAttesterTabDetails = originTab;\n                            if (\n                                tabDetails.attesterTabId &&\n                                tabDetails.attesterTabId === details.tabId\n                            ) {\n                                originTab.attestationData = pp_hdr;\n                                storage.set(url_tab_data);\n                                // break to label above\n                                break lookForAttesterTabId;\n                            }\n                        }\n                    }\n                };\n\n                storage.get(null, callback);\n            }\n        }\n\n        if (isManifestV3(chrome)) {\n            const { serviceWorkerMode: mode } = getSettings();\n            const logger = getLogger(mode);\n            chrome.declarativeNetRequest\n                .updateSessionRules(removeAuthorizationRule())\n                .catch((e: unknown) => logger.debug(`failed to remove session rules:`, e));\n        }\n\n        return;\n    };\n\nexport const handleHeadersReceived =\n    (browser: BROWSERS, storage: chrome.storage.StorageArea) =>\n    (\n        details: chrome.webRequest.WebResponseHeadersDetails & {\n            frameAncestors?: Array<{ url: string }>;\n        },\n    ): void | chrome.webRequest.BlockingResponse => {\n        // if no header were received with request\n        if (!details.responseHeaders) {\n            return;\n        }\n\n        // Check if there's a valid PrivateToken header\n        const privateTokenChl = details.responseHeaders.find(\n            (x) => x.name.toLowerCase() == 'www-authenticate',\n        )?.value;\n        if (!privateTokenChl) {\n            return;\n        }\n        if (PrivateToken.parse(privateTokenChl).length === 0) {\n            return;\n        }\n\n        const settings = getSettings();\n        if (Object.keys(settings).length === 0) {\n            getRawSettings(storage);\n            return;\n        }\n        const { attesters, serviceWorkerMode: mode } = settings;\n        const logger = getLogger(mode);\n\n        let initiator: string | undefined = undefined;\n        if (details.frameId === 0) {\n            initiator = details.url;\n        } else {\n            initiator =\n                details.frameAncestors?.at(0)?.url ??\n                cachedTabs[details.tabId]?.url ??\n                details.initiator;\n        }\n        if (!initiator) {\n            return;\n        }\n        const initiatorURL = new URL(initiator)?.origin;\n        const isAttesterFrame = attesters.map((a) => new URL(a).origin).includes(initiatorURL);\n        if (isAttesterFrame) {\n            logger.info('PrivateToken support disabled on attester websites.');\n            return;\n        }\n\n        if (TOKENS[details.url] === PENDING) {\n            return;\n        }\n\n        if (isManifestV3(chrome)) {\n            // TODO: convert to static rule for simplicity perhaps?\n            chrome.declarativeNetRequest.updateSessionRules(getIdentificationRule(details.url));\n        }\n\n        // create a new entry storing this originTabId\n        storage.get(details.url, (existing: UrlOriginTabs) => {\n            existing[details.tabId] = { attesterTabId: -1, attestationData: '' };\n            storage.set({ [details.url]: existing });\n        });\n\n        // turn this received header into a token (tab opening and attester handling within here)\n        const w3HeaderValue = headerToToken(details.url, details.tabId, privateTokenChl, storage);\n\n        // Add a rule to declarativeNetRequest here if you want to block\n        // or modify a header from this request. The rule is registered and\n        // changes are observed between the onBeforeSendHeaders and\n        // onSendHeaders methods.\n        if (!chrome.declarativeNetRequest) {\n            TOKENS[details.url] = PENDING;\n        }\n        const redirectPromise = w3HeaderValue\n            .then(async (value): Promise<void | chrome.webRequest.BlockingResponse> => {\n                if (!value) {\n                    delete TOKENS[details.url];\n                    return;\n                }\n                if (isManifestV3(chrome)) {\n                    await chrome.declarativeNetRequest.updateSessionRules(\n                        getAuthorizationRule(details.url, `PrivateToken token=${value}`),\n                    );\n                } else {\n                    TOKENS[details.url] = value;\n                }\n\n                return { redirectUrl: details.url };\n            })\n            .catch((err) => {\n                logger.error(`failed to retrieve PrivateToken token: ${err}`);\n            });\n\n        switch (browser) {\n            case BROWSERS.FIREFOX:\n                // typing is incorrect, but force it for Firefox because browser is compatible\n                return redirectPromise as unknown as chrome.webRequest.BlockingResponse;\n            case BROWSERS.CHROME:\n                // Refresh tab in chrome.\n                const requestID = getRequestID();\n                setReplayDomainRule('pending', requestID);\n                pendingRequests.set(\n                    requestID,\n                    promiseToQueryable(\n                        redirectPromise.then(() => {\n                            setReplayDomainRule(REPLAY_STATE.FULFILLED, requestID);\n                        }),\n                    ),\n                );\n                // Detect call context, and only refresh if it's a top level request\n                redirectPromise.then(async () => {\n                    if (details.type === 'main_frame') {\n                        chrome.tabs.update(details.tabId, { url: details.url });\n                    }\n                });\n                const responseHeaders = details.responseHeaders ?? [];\n                responseHeaders.push({ name: PRIVACY_PASS_API_REPLAY_HEADER, value: requestID });\n                return {\n                    responseHeaders,\n                };\n        }\n    };\n"
  },
  {
    "path": "src/background/pubVerifToken.ts",
    "content": "import { Client, PrivateToken, Token, TokenResponse } from '@cloudflare/privacypass-ts';\n\nexport async function fetchPublicVerifToken(\n    privateToken: PrivateToken,\n    originTabId: number,\n    storage: chrome.storage.StorageArea,\n): Promise<Token> {\n    let attesterIssuerProxyURI: string;\n\n    const attesterToken: string = await new Promise((resolve) => {\n        storage.onChanged.addListener(async (changes) => {\n            for (const [, value] of Object.entries(changes)) {\n                if (!value.newValue) {\n                    continue;\n                }\n                const newValue = value.newValue;\n                if (\n                    newValue[originTabId] &&\n                    newValue[originTabId].hasOwnProperty('attestationData') && // eslint-disable-line no-prototype-builtins\n                    newValue[originTabId].attestationData != ''\n                ) {\n                    // before we close it retrieve the URL of the attester tab\n                    const tab: chrome.tabs.Tab = await new Promise((resolve) =>\n                        chrome.tabs.get(newValue[originTabId].attesterTabId, resolve),\n                    );\n                    if (!tab) {\n                        continue;\n                    }\n                    attesterIssuerProxyURI = tab.url!;\n\n                    // close the attester tab as we no longer need to interact with the attester front-end\n                    chrome.tabs.remove(newValue[originTabId].attesterTabId);\n\n                    resolve(newValue[originTabId].attestationData);\n                }\n            }\n        });\n    });\n\n    chrome.tabs.update(originTabId, { active: true });\n\n    // Create a TokenRequest.\n    const client = new Client();\n    const tokenRequest = await client.createTokenRequest(privateToken);\n\n    // Send TokenRequest to Issuer (via Attester) as proxy\n    const tokenResponse = await tokenRequest.send(\n        attesterIssuerProxyURI!,\n        TokenResponse,\n        new Headers({ 'private-token-attester-data': attesterToken }),\n    );\n\n    // Produce a token by Finalizing the TokenResponse.\n    const token = await client.finalize(tokenResponse);\n\n    return token;\n}\n"
  },
  {
    "path": "src/background/replay.ts",
    "content": "import { PRIVACY_PASS_API_REPLAY_URI } from './const';\nimport { getRedirectRule, getReplayRule } from './rules';\nimport { isManifestV3 } from './util';\n\nexport const REPLAY_STATE = {\n    FULFILLED: 'fulfilled',\n    NOT_FOUND: 'not-found',\n    PENDING: 'pending',\n} as const;\nexport type REPLAY_STATE = (typeof REPLAY_STATE)[keyof typeof REPLAY_STATE];\n\nexport const getRequestID = (() => {\n    let requestID = crypto.randomUUID();\n\n    return () => {\n        const oldRequestID = requestID;\n        requestID = crypto.randomUUID();\n        if (isManifestV3(chrome)) {\n            chrome.declarativeNetRequest.updateSessionRules(getReplayRule(requestID));\n        }\n        return oldRequestID;\n    };\n})();\n\ntype UUID = ReturnType<typeof crypto.randomUUID>;\n\nexport const setReplayDomainRule = (state: REPLAY_STATE, requestID?: UUID) => {\n    if (!chrome.declarativeNetRequest) {\n        return;\n    }\n    const filterSuffix = requestID ? `/requestID/${requestID}` : '/*';\n    const urlFilter = `${PRIVACY_PASS_API_REPLAY_URI}${filterSuffix}`;\n\n    chrome.declarativeNetRequest.updateSessionRules(\n        getRedirectRule(urlFilter, `data:text/plain,${state}`),\n    );\n};\n"
  },
  {
    "path": "src/background/rules.ts",
    "content": "import { PRIVACY_PASS_API_REPLAY_HEADER } from './const';\n\n// First size digits of md5 of 'privacy-pass-extension-identification' as integer\nexport const PRIVACY_PASS_EXTENSION_RULE_OFFSET = 11943591;\nexport function getRuleID(x: number) {\n    return PRIVACY_PASS_EXTENSION_RULE_OFFSET + x;\n}\n\nconst RULE_IDS = {\n    IDENTIFICATION: getRuleID(1),\n    AUTHORIZATION: getRuleID(2),\n    REPLAY: getRuleID(3),\n    REDIRECT: getRuleID(4),\n};\n\nconst EXTENSION_SUPPORTED_RESOURCE_TYPES = chrome.declarativeNetRequest\n    ? [\n          chrome.declarativeNetRequest.ResourceType.MAIN_FRAME,\n          chrome.declarativeNetRequest.ResourceType.SUB_FRAME,\n          chrome.declarativeNetRequest.ResourceType.XMLHTTPREQUEST,\n      ]\n    : [];\n\nexport function getIdentificationRule(url: string): chrome.declarativeNetRequest.UpdateRuleOptions {\n    // TODO: convert to static rule for simplicity perhaps?\n    return {\n        removeRuleIds: [RULE_IDS.IDENTIFICATION],\n        addRules: [\n            {\n                action: {\n                    type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS,\n                    responseHeaders: [\n                        // Use Server-Timimg header because Set-Cookie is unreliable in this context in Chrome\n                        {\n                            header: 'Server-Timing',\n                            operation: chrome.declarativeNetRequest.HeaderOperation.SET,\n                            value: 'PrivacyPassExtensionV; desc=4',\n                        },\n                    ],\n                },\n                condition: {\n                    urlFilter: new URL(url).toString(),\n                    resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],\n                },\n                id: RULE_IDS.IDENTIFICATION,\n                priority: 1,\n            },\n        ],\n    };\n}\n\nexport function getAuthorizationRule(\n    url: string,\n    authorizationHeader: string,\n): chrome.declarativeNetRequest.UpdateRuleOptions {\n    return {\n        removeRuleIds: [RULE_IDS.AUTHORIZATION],\n        addRules: [\n            {\n                id: RULE_IDS.AUTHORIZATION,\n                priority: 1,\n                action: {\n                    type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS,\n                    requestHeaders: [\n                        {\n                            header: 'Authorization',\n                            operation: chrome.declarativeNetRequest.HeaderOperation.SET,\n                            value: authorizationHeader,\n                        },\n                    ],\n                },\n                condition: {\n                    // Note: The urlFilter must be composed of only ASCII characters.\n                    urlFilter: new URL(url).toString(),\n                    resourceTypes: EXTENSION_SUPPORTED_RESOURCE_TYPES,\n                },\n            },\n        ],\n    };\n}\n\nexport function removeAuthorizationRule(): chrome.declarativeNetRequest.UpdateRuleOptions {\n    return {\n        removeRuleIds: [RULE_IDS.AUTHORIZATION],\n    };\n}\n\nexport function getReplayRule(\n    replayHeader: string,\n): chrome.declarativeNetRequest.UpdateRuleOptions {\n    return {\n        removeRuleIds: [RULE_IDS.REPLAY],\n        addRules: [\n            {\n                id: RULE_IDS.REPLAY,\n                priority: 10,\n                action: {\n                    type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS,\n                    responseHeaders: [\n                        {\n                            header: PRIVACY_PASS_API_REPLAY_HEADER,\n                            operation: chrome.declarativeNetRequest.HeaderOperation.SET,\n                            value: replayHeader,\n                        },\n                    ],\n                },\n                condition: {\n                    // Chrome declarativeNetRequest have to be defined before the request is made. Given a PP request can be made from any URL, returning a responseID for all URLs is required.\n                    urlFilter: '*',\n                    resourceTypes: EXTENSION_SUPPORTED_RESOURCE_TYPES,\n                },\n            },\n        ],\n    };\n}\n\nexport function getRedirectRule(\n    urlFilter: string,\n    redirectURL: string,\n): chrome.declarativeNetRequest.UpdateRuleOptions {\n    return {\n        removeRuleIds: [RULE_IDS.REDIRECT],\n        addRules: [\n            {\n                id: RULE_IDS.REDIRECT,\n                priority: 5,\n                action: {\n                    type: chrome.declarativeNetRequest.RuleActionType.REDIRECT,\n                    redirect: { url: redirectURL },\n                },\n                condition: {\n                    urlFilter,\n                    resourceTypes: EXTENSION_SUPPORTED_RESOURCE_TYPES,\n                },\n            },\n        ],\n    };\n}\n"
  },
  {
    "path": "src/background/tsconfig.json",
    "content": "{\n    \"extends\": \"../../tsconfig.json\",\n    \"compilerOptions\": {\n        \"resolveJsonModule\": true\n    },\n    \"include\": [ \".\", \"../common\" ]\n}\n"
  },
  {
    "path": "src/background/util.test.ts",
    "content": "import { uint8ToB64URL } from './util.js';\n\ndescribe('uint8ToB64URL', () => {\n    it('should convert simple arrays', () => {\n        const u1 = Uint8Array.from([1]);\n        expect(uint8ToB64URL(u1)).toBe('AQ==');\n    });\n\n    it('should return an empty string on an empty array', () => {\n        const u = new Uint8Array();\n        expect(uint8ToB64URL(u)).toBe('');\n    });\n\n    it('should return 2 an empty string on an empty array', () => {\n        const u = new Uint8Array(16);\n        expect(uint8ToB64URL(u)).toBe('AAAAAAAAAAAAAAAAAAAAAA==');\n    });\n});\n"
  },
  {
    "path": "src/background/util.ts",
    "content": "function u8ToB64(u: Uint8Array): string {\n    return btoa(String.fromCharCode(...u));\n}\n\nfunction b64ToB64URL(s: string): string {\n    return s.replace(/\\+/g, '-').replace(/\\//g, '_');\n}\n\nexport function uint8ToB64URL(u: Uint8Array): string {\n    return b64ToB64URL(u8ToB64(u));\n}\n\n// JavaScript Promise don't expose the current status of the promise\n// This extension aims to work exactly like a native Promise (therefore extends) with the addition of getter for the current status\nexport interface QueryablePromise<T> extends Promise<T> {\n    isPending: boolean;\n    isFullfilled: boolean;\n    isRejected: boolean;\n    isResolved: boolean;\n}\n\nexport function promiseToQueryable<T>(p: Promise<T>): QueryablePromise<T> {\n    let _isFullfilled = false;\n    let _isRejected = false;\n    let _isResolved = false;\n    const ret: QueryablePromise<T> = {\n        get isPending() {\n            return !_isFullfilled;\n        },\n        get isFullfilled() {\n            return _isFullfilled;\n        },\n        get isRejected() {\n            return _isRejected;\n        },\n        get isResolved() {\n            return _isResolved;\n        },\n        then: p.then,\n        catch: p.catch,\n        finally: p.finally,\n        [Symbol.toStringTag]: p[Symbol.toStringTag],\n    };\n    p.then((_result) => {\n        _isFullfilled = true;\n        _isResolved = true;\n    }).catch((_error) => {\n        _isFullfilled = true;\n        _isRejected = true;\n    });\n    return ret;\n}\n\nexport function isManifestV3(browser: typeof chrome) {\n    return !!browser.declarativeNetRequest;\n}\n"
  },
  {
    "path": "src/common/const.ts",
    "content": "export const STORAGE_ID_ATTESTER_CONFIGURATION = 'attesters_by_issuer_key';\n"
  },
  {
    "path": "src/common/index.ts",
    "content": "export * from './const';\nexport * from './logger';\nexport * from './settings';\n"
  },
  {
    "path": "src/common/logger.ts",
    "content": "/* eslint-disable no-console */\n\nimport { SERVICE_WORKER_MODE } from './settings';\n\ninterface Logger {\n    info: (...obj: unknown[]) => void; // Log an info level message\n    error: (...obj: unknown[]) => void; // Log an error level message\n    debug: (...obj: unknown[]) => void; // Log a debug level message\n    warn: (...obj: unknown[]) => void; // Log a Warning level message\n    log: (...obj: unknown[]) => void; // Log a message\n}\n\nclass ConsoleLogger implements Logger {\n    public info = (...obj: unknown[]): void => {\n        console.info(...obj);\n    };\n\n    public error = (...obj: unknown[]): void => {\n        console.error(...obj);\n    };\n\n    public debug = (...obj: unknown[]): void => {\n        console.debug(...obj);\n    };\n\n    public warn = (...obj: unknown[]): void => {\n        console.warn(...obj);\n    };\n\n    public log = (...obj: unknown[]): void => {\n        console.log(...obj);\n    };\n}\n\nclass ModeLogger extends ConsoleLogger {\n    private static ALLOWED_CALLS: Record<string, SERVICE_WORKER_MODE[]> = {\n        INFO: [SERVICE_WORKER_MODE.DEVELOPMENT],\n        ERROR: [\n            SERVICE_WORKER_MODE.DEMO,\n            SERVICE_WORKER_MODE.DEVELOPMENT,\n            SERVICE_WORKER_MODE.PRODUCTION,\n        ],\n        DEBUG: [SERVICE_WORKER_MODE.DEVELOPMENT],\n        WARN: [SERVICE_WORKER_MODE.DEVELOPMENT],\n        LOG: [\n            SERVICE_WORKER_MODE.DEMO,\n            SERVICE_WORKER_MODE.DEVELOPMENT,\n            SERVICE_WORKER_MODE.PRODUCTION,\n        ],\n    };\n\n    constructor(public mode: SERVICE_WORKER_MODE) {\n        super();\n    }\n\n    public info = (...obj: unknown[]): void => {\n        if (!ModeLogger.ALLOWED_CALLS.INFO.includes(this.mode)) {\n            return;\n        }\n        console.info(...obj);\n    };\n\n    public error = (...obj: unknown[]): void => {\n        if (!ModeLogger.ALLOWED_CALLS.ERROR.includes(this.mode)) {\n            return;\n        }\n        console.error(...obj);\n    };\n\n    public debug = (...obj: unknown[]): void => {\n        if (!ModeLogger.ALLOWED_CALLS.DEBUG.includes(this.mode)) {\n            return;\n        }\n        console.debug(...obj);\n    };\n\n    public warn = (...obj: unknown[]): void => {\n        if (!ModeLogger.ALLOWED_CALLS.WARN.includes(this.mode)) {\n            return;\n        }\n        console.warn(...obj);\n    };\n\n    public log = (...obj: unknown[]): void => {\n        if (!ModeLogger.ALLOWED_CALLS.LOG.includes(this.mode)) {\n            return;\n        }\n        console.log(...obj);\n    };\n}\n\nexport function getLogger(mode: SERVICE_WORKER_MODE) {\n    return new ModeLogger(mode);\n}\n"
  },
  {
    "path": "src/common/settings.ts",
    "content": "import { IssuerConfig } from '@cloudflare/privacypass-ts';\nimport { STORAGE_ID_ATTESTER_CONFIGURATION } from './const';\nimport { getLogger } from './logger';\n\nexport const SETTING_NAMES = {\n    SERVICE_WORKER_MODE: 'serviceWorkerMode',\n    ATTESTERS: 'attesters',\n} as const;\nexport type SettingsKeys = (typeof SETTING_NAMES)[keyof typeof SETTING_NAMES];\nexport type RawSettings = {\n    serviceWorkerMode: SERVICE_WORKER_MODE;\n    attesters: string;\n};\n\nexport type Settings = {\n    serviceWorkerMode: SERVICE_WORKER_MODE;\n    attesters: string[];\n};\n\nlet settings: Settings = {} as unknown as Settings;\n\nexport const rawSettingToSettingAttester = (attestersRaw: string): string[] => {\n    return attestersRaw\n        .split(/[\\n,]+/) // either split on new line or comma\n        .filter((attester) => {\n            try {\n                new URL(attester);\n                return true;\n            } catch (_) {\n                return false;\n            }\n        });\n};\n\nexport function getRawSettings(storage: chrome.storage.StorageArea): Promise<RawSettings> {\n    return new Promise((resolve) =>\n        storage.get(Object.values(SETTING_NAMES), async (items) => {\n            if (Object.entries(items).length < 2) {\n                await resetSettings(storage);\n                const settings = getSettings();\n                resolve({\n                    attesters: settings.attesters.join('\\n'),\n                    serviceWorkerMode: settings.serviceWorkerMode,\n                });\n            } else {\n                const rawSettings = items as unknown as RawSettings;\n                settings = {\n                    serviceWorkerMode: rawSettings.serviceWorkerMode,\n                    attesters: rawSettingToSettingAttester(rawSettings.attesters),\n                };\n                resolve(rawSettings);\n            }\n        }),\n    );\n}\n\nexport function getSettings(): Settings {\n    return settings;\n}\n\nexport const refreshAttesterLookupByIssuerKey = async (storage: chrome.storage.StorageArea) => {\n    // Force reset associations between public keys and issuers that trust each attester that we know about\n    const attestersByIssuerKey = new Map<string, string>();\n\n    const { attesters, serviceWorkerMode: mode } = getSettings();\n    const logger = getLogger(mode);\n\n    // Populate issuer keys for issuers that trust our ATTESTERS\n    for (const attester of attesters) {\n        const nowTimestamp = Date.now();\n\n        // Connect to each of the ATTESTERS we know about for their issuer directory\n        const response = await fetch(`${attester}/v1/private-token-issuer-directory`);\n        if (!response.ok) {\n            logger.log(`\"${attester}\" issuer keys not available, attester will not be used.`);\n            return;\n        }\n\n        const directory: Record<string, IssuerConfig> = await response.json();\n        // for each attester in the directory\n        for (const { 'token-keys': tokenKeys } of Object.values(directory)) {\n            for (const key of tokenKeys) {\n                const notBefore = key['not-before'];\n                if (!notBefore || notBefore > nowTimestamp) {\n                    continue;\n                }\n                attestersByIssuerKey.set(key['token-key'], attester);\n            }\n        }\n        storage.set({\n            [STORAGE_ID_ATTESTER_CONFIGURATION]: Object.fromEntries(attestersByIssuerKey),\n        });\n    }\n\n    logger.info('Attester lookup by Issuer key populated');\n};\n\nexport async function saveSettings(\n    storage: chrome.storage.StorageArea,\n    name: SettingsKeys,\n    value: string,\n): Promise<void> {\n    switch (name) {\n        case 'attesters':\n            settings[name] = rawSettingToSettingAttester(value);\n            await refreshAttesterLookupByIssuerKey(storage);\n            break;\n        case 'serviceWorkerMode':\n            if (\n                Object.values(SERVICE_WORKER_MODE)\n                    .map((s) => s as string)\n                    .includes(value)\n            ) {\n                settings[name] = value as SERVICE_WORKER_MODE;\n            }\n    }\n    return storage.set({ [name]: value });\n}\n\nexport async function resetSettings(storage: chrome.storage.StorageArea): Promise<void> {\n    const DEFAULT = {\n        serviceWorkerMode: SERVICE_WORKER_MODE.PRODUCTION,\n        attesters: [\n            'https://pp-attester-turnstile.research.cloudflare.com',\n            'https://pp-attester-turnstile-dev.research.cloudflare.com',\n        ],\n    };\n    await saveSettings(storage, 'serviceWorkerMode', DEFAULT.serviceWorkerMode);\n    await saveSettings(storage, 'attesters', DEFAULT.attesters.join('\\n'));\n}\n\nexport const SERVICE_WORKER_MODE = {\n    PRODUCTION: 'production',\n    DEVELOPMENT: 'development',\n    DEMO: 'demo',\n} as const;\nexport type SERVICE_WORKER_MODE = (typeof SERVICE_WORKER_MODE)[keyof typeof SERVICE_WORKER_MODE];\n"
  },
  {
    "path": "src/options/global.d.ts",
    "content": "declare const browser: typeof chrome;"
  },
  {
    "path": "src/options/index.css",
    "content": "textarea {\n    width: 80%;\n    -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */\n    -moz-box-sizing: border-box;    /* Firefox, other Gecko */\n    box-sizing: border-box;         /* Opera/IE 8+ */\n}\n\n.hidden {\n    display: none;\n}\n\nmain {\n    display: flex;\n    flex-direction: column;\n}\n\nsection {\n    margin-bottom: 1em;\n}"
  },
  {
    "path": "src/options/index.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n  <title>Privacy Pass Popup</title>\n  <link rel=\"stylesheet\" href=\"index.css\">\n  <script src=\"index.mjs\" type=\"module\"></script>\n</head>\n\n<body>\n  <main>\n    <section>\n      <label for=\"attesters\">\n        Attesters URLs. One URL per line of the form <code>https://example.com</code>.\n      </label>\n      <textarea id=\"attesters\" rows=\"5\"></textarea>\n    </section>\n    <section>\n      <label for=\"serviceWorkerMode\">\n        Development configuration.\n      </label>\n      <select id=\"serviceWorkerMode\">\n        <option value=\"production\">Production</option>\n        <option value=\"development\">Development</option>\n        <option value=\"demo\">Demo (Slow)</option>\n      </select>\n    </section>\n  </main>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/options/index.ts",
    "content": "import { getRawSettings, saveSettings, SETTING_NAMES, SettingsKeys } from '../common';\n\nconst STORAGE = typeof browser !== 'undefined' ? browser.storage.local : chrome.storage.local;\n\n// When the popup is loaded, load component that are stored in local/sync storage\nconst onload = async () => {\n    // Load current settings from sync storage\n    const settings = await getRawSettings(STORAGE);\n\n    // Every setting has a dedicated input which we need to set the default value, and onchange behaviour\n    for (const name in settings) {\n        const dropdown = document.getElementById(name) as HTMLInputElement;\n\n        dropdown.value = settings[name];\n        dropdown.addEventListener('change', (event) => {\n            const target = event.target as HTMLInputElement;\n            if (!Object.values(SETTING_NAMES).includes(target.id as SettingsKeys)) {\n                return;\n            }\n            saveSettings(STORAGE, target.id as SettingsKeys, target.value.trim());\n        });\n    }\n\n    // Links won't open correctly within an extension popup. The following overwrite the default behaviour to open a new tab\n    for (const a of [...document.getElementsByTagName('a')]) {\n        a.addEventListener('click', (event) => {\n            event.preventDefault();\n            chrome.tabs.create({ url: a.href });\n        });\n    }\n};\n\ndocument.addEventListener('DOMContentLoaded', onload);\n"
  },
  {
    "path": "src/options/tsconfig.json",
    "content": "{\n    \"extends\": \"../../tsconfig.json\",\n    \"compilerOptions\": {\n        \"resolveJsonModule\": true\n    },\n    \"include\": [ \".\", \"../common\" ]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"es2020\",\n        \"module\": \"es2020\",\n        \"declaration\": true,\n        \"declarationMap\": true,\n        \"sourceMap\": true,\n        \"composite\": true,\n        \"incremental\": true,\n        \"removeComments\": true,\n        \"isolatedModules\": true,\n        \"strict\": true,\n        \"strictNullChecks\": true,\n        \"noEmitOnError\": true,\n        \"noUnusedLocals\": true,\n        \"noUnusedParameters\": true,\n        \"noImplicitReturns\": true,\n        \"noFallthroughCasesInSwitch\": true,\n        \"moduleResolution\": \"node\",\n        \"esModuleInterop\": true,\n        \"experimentalDecorators\": true,\n        \"emitDecoratorMetadata\": true,\n        \"allowSyntheticDefaultImports\": true,\n        \"rootDir\": \".\",\n        \"outDir\": \"lib\"\n    },\n    \"files\": [],\n    \"include\": [],\n    \"references\": [\n        {\n            \"path\": \"./src/background\"\n        }\n    ]\n}"
  }
]