Repository: cloudflare/pp-browser-extension
Branch: main
Commit: 94fc4ce9938c
Files: 47
Total size: 65.8 KB
Directory structure:
gitextract_k1shepog/
├── .eslintrc.json
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── feature_request.md
│ │ ├── new_bug.md
│ │ └── ui_ux_feedback.md
│ └── workflows/
│ ├── action.yml
│ └── semgrep.yml
├── .gitignore
├── .npmrc
├── .prettierrc.json
├── CONTRIBUTING.md
├── DEVELOPER.md
├── LICENSE
├── README.md
├── devel.md
├── devel_pat.md
├── jest.config.mjs
├── jest.setup.mjs
├── package.json
├── platform/
│ ├── chromium/
│ │ ├── index.ts
│ │ ├── manifest.json
│ │ └── tsconfig.json
│ ├── firefox/
│ │ ├── global.d.ts
│ │ ├── index.ts
│ │ ├── manifest.json
│ │ └── tsconfig.json
│ └── mv3/
│ └── chromium/
│ ├── index.ts
│ ├── manifest.json
│ └── tsconfig.json
├── src/
│ ├── background/
│ │ ├── attesters.ts
│ │ ├── const.ts
│ │ ├── index.ts
│ │ ├── pubVerifToken.ts
│ │ ├── replay.ts
│ │ ├── rules.ts
│ │ ├── tsconfig.json
│ │ ├── util.test.ts
│ │ └── util.ts
│ ├── common/
│ │ ├── const.ts
│ │ ├── index.ts
│ │ ├── logger.ts
│ │ └── settings.ts
│ └── options/
│ ├── global.d.ts
│ ├── index.css
│ ├── index.html
│ ├── index.ts
│ └── tsconfig.json
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.json
================================================
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"plugins": [
"@typescript-eslint",
"security",
"prettier"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:security/recommended-legacy",
"plugin:prettier/recommended",
"prettier"
],
"ignorePatterns": [
"**/*.d.ts",
"**/*.js",
"coverage/*",
"lib/*"
],
"rules": {
"@typescript-eslint/member-delimiter-style": 0,
"@typescript-eslint/no-namespace": [
"warn"
],
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_"
}
],
"no-case-declarations": 0,
"no-console": [
"error",
{
"allow": [
"warn",
"error"
]
}
]
}
}
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature-request
assignees: ''
---
**Is your feature request related to a problem?**
If so, make sure your problem hasn't been listed before.
**Describe the solution you'd like**
Comment about what can be improved, or what would you like to happen in response to some action.
**Additional context**
Add any other context about the feature request here.
================================================
FILE: .github/ISSUE_TEMPLATE/new_bug.md
================================================
---
name: Bug Report
about: Create a report for a bug in Privacy Pass browser extension
title: ''
labels: triage
assignees: ''
---
Before reporting a new bug, verify if your request is already being tracked by another issue: https://github.com/cloudflare/pp-browser-extension/issues.
---
If 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.
**Describe the bug**
A clear and concise description of the bug.
**How to reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A description of what is expected to happen.
**System (please complete the following information):**
- OS: [e.g. iOS/Windows]
- Attesters configuration from extension options
- Browser [e.g. Chrome, Firefox]
- Browser Version [e.g. 79, 80, ]
- Silk Version [e.g. 4.0.0, 4.0.1 ]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/ui_ux_feedback.md
================================================
---
name: UI/UX Feedback
about: We welcome feedback regarding user experience
title: ''
labels: enhancement
assignees: ''
---
**Describe** how your experience can be improved.
**Note** that this report does not consider errors or bugs in the extension.
Get some inspiration from these questions:
- *What do you expect to see when you perform some action?*
- *There exist some troubles on rendering?*
- *Would you like browsers have builtin support for Privacy Pass?*
================================================
FILE: .github/workflows/action.yml
================================================
---
name: Silk Privacy Pass Client Extension
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
testing:
name: Running on Node v${{ matrix.node-version }}
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 18]
steps:
- name: Checking out
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4
- name: Setup Node v${{ matrix.node-version }}
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: ${{ matrix.node-version }}
- name: Installing
run: npm ci
- name: Linting
run: npm run lint
- name: Building
run: npm run build
- name: Testing
run: npm test
analyze:
name: Analyze CodeQL
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: javascript-typescript
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
================================================
FILE: .github/workflows/semgrep.yml
================================================
on:
pull_request: {}
workflow_dispatch: {}
push:
branches:
- main
- master
schedule:
- cron: '0 0 * * *'
name: Semgrep config
jobs:
semgrep:
name: semgrep/ci
runs-on: ubuntu-latest
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
SEMGREP_URL: https://cloudflare.semgrep.dev
SEMGREP_APP_URL: https://cloudflare.semgrep.dev
SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@v4
- run: semgrep ci
================================================
FILE: .gitignore
================================================
*.swp
/node_modules
/dist
/lib
/coverage
================================================
FILE: .npmrc
================================================
engine-strict=true
================================================
FILE: .prettierrc.json
================================================
{
"semi": true,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "all",
"printWidth": 100
}
================================================
FILE: CONTRIBUTING.md
================================================
## Contribution Guidelines
### Code Style
Code is written in TypeScript and is automatically formatted with prettier.
```bash
npm run format
```
### Naming convention
It is recommended to follow style guide for [TypeScript](https://google.github.io/styleguide/tsguide.html).
================================================
FILE: DEVELOPER.md
================================================
## Directory Structure
```
pp-browser-extension
├──📂 platform: Contains platform specific assets, such as manifest for browsers, or brackground and service worker script used by each platform.
├──📂 public: Contains all the assets which are neither the business logic files nor the style sheets.
└──📂 src: Contains all the business logic files and the style sheets.
└──📂 background: The business logic of the extension service worker.
└──📂 common: The shared logic between the extension background script and its options context.
└──📂 options: The web app defining option page in the browser settings.
```
================================================
FILE: LICENSE
================================================
Copyright (c) 2017-2022, Privacy Pass Team, Cloudflare, Inc., and other contributors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: README.md
================================================
[](https://github.com/cloudflare/pp-browser-extension/releases/)
[](https://github.com/cloudflare/pp-browser-extension/actions)
[](https://opensource.org/licenses/BSD-3-Clause)
# Silk - Privacy Pass Client for the browser

This browser extension implements the client-side of the Privacy Pass protocol providing unlinkable cryptographic tokens.
**Specification:* Compliant with IETF [draft-ietf-privacypass-protocol v11](https://datatracker.ietf.org/doc/draft-ietf-privacypass-protocol/).
**Support:**
* ✅ Public-Verifiable tokens (Blind-RSA)
* 🚧 Private-Verifiable tokens (VOPRF)
* 🚧 Batched tokens
* 🚧 Rate limited tokens
## Installation
| **[Chrome][chrome-store]** | **[Firefox][firefox-store]** |
| -- | -- |
| [][chrome-store] | [][firefox-store] |
## How it works?
**Privacy Pass Attesters:** 🟩 [Cloudflare Research with Turnstile][cf-url]
[cf-url]: https://pp-attester-turnstile.research.cloudflare.com/
[chrome-store]: https://chrome.google.com/webstore/detail/privacy-pass/ajhmfdgkijocedmfjonnpjfojldioehi/
[firefox-store]: https://addons.mozilla.org/firefox/addon/privacy-pass/
**Get tokens**
- If a website requests a Privacy Pass token, the extension is automatically going to request you to perform the associated challenge.
- One page will open with a challenge to be solved.
- Solve successfully the challenge and the extension will get **one** token.
See [FAQs](#faqs) and [Known Issues](#known-issues) section: if something is not working as expected.
---
## Installing from Sources
We 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.
### Building
```sh
git clone https://github.com/cloudflare/pp-browser-extension
nvm use 20
npm ci
npm run build
```
Once these steps complete, the `dist` folder will contain all files required to load the extension.
### Running Tests
```sh
nvm use 20
npm ci
npm test
```
### Manually Loading Extension
#### Firefox
1. Open Firefox and navigate to [about:debugging#/runtime/this-firefox/](about:debugging#/runtime/this-firefox/)
1. Click on 'Load Temporary Add-on' button.
1. Select `manifest.json` from the `dist` folder.
1. Check extension logo appears in the top-right corner of the browser.
#### Chrome
1. Open Chrome and navigate to [chrome://extensions/](chrome://extensions/)
1. Turn on the 'Developer mode' on the top-right corner.
1. Click on 'Load unpacked' button.
1. Select the `dist` folder.
1. Check extension logo appears in the top-right corner of the browser.
1. If you cannot see the extension logo, it's likely not pinned to the toolbar.
#### Edge
- Open Edge and navigate to [edge://extensions/](edge://extensions/)
- Turn on the 'Developer mode' on the left bar.
- Click on 'Load unpacked' button in the main panel.
- Select the `dist` folder.
- The extension will appear listed in the main panel.
- To see the extension in the bar, click in the puzzle icon and enable it, so it gets pinned to the toolbar.
---
### Highlights
**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.
**2022** -- The Privacy Pass protocol can also use RSA blind signatures.
**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.
**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.
**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.
**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/).
#### Acknowledgements
The 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.
The 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.
---
## FAQs
#### As a user, how can I add new attestation methods
Depending 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.
#### My website support Privacy Pass authentication scheme, but the extension does nothing
This 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).
#### As a service operator, how to roll out out my own attestation method
Privacy 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.
If 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.
---
## Known Issues
#### Extensions that modify user-agent or headers
There 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.
Compounded to that, Cloudflare will ignore clearance cookies when the user-agent request does not match the one used when obtaining the cookie.
## Chrome support via Client replay API
### Overview
Chrome 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.
### Requirements
Your 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.
### System definition
On every request, the extension adds header `Private-Token-Client-Replay: <requestID>`. `requestID` is a UUID identifying your next replay attempt.
For 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.
Given 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>`.
**`GET /requestID/<requestID>`**
* Response URL: `data:text/plain,<status>`
* `status` is `pending`, `fulfilled`, `not-found`
Your website should do the following depending on the returned `status`:
* if `pending`, wait and query `/requestID/<requestID>` again in the future
* if `fulfilled`, replay the initial request
* if `not-found`, cancel the initial request
A sequence diagram illustrate the flow below
```mermaid
sequenceDiagram
participant E as Client
participant O as Origin
participant B as Extension
E->>O: GET example.com/img.png
O->>B: WWW-Authenticate: challenge=[x]
Note over B: Cannot block request
B->>B: Adds "Private-Token-Client-Replay: requestID"
B->>E: 401 Unauthorized + "Private-Token-Client-Replay: requestID"
par Extension fetches token
Note over B: Interact with Attester to retrieve Token [x]
and Client wait
E->>E: Check "Private-Token-Client-Replay" header
E->>B: GET /requestID/<requestID>
B->>E: "200 data:text/plain,pending"
end
B->>E: "200 data:text/plain,fulfilled"
Note over E: Extension is ready to query the Origin
Note over B: Extension is intercepting request to example.com
E->>B: GET example.com/img.png
B->>O: GET example.com/img.png + Authorization [x]
O->>E: <data>Cat Picture</data>
```
### Design considerations
The 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/).
Specifically, the following choices have been engineered as follow
* Use a dedicated replay domain to allow any website to query the extension without hardcoding its ID
* Use a dedicated replay domain that does not resolve to prevent DoS risks, and ensure there are no traffic interception with a faulty extension
* 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
* Redirect to a ["data" URL](https://www.rfc-editor.org/rfc/rfc2397#section-2) to prevent DNS resolution and other extensions interference
* Return `Private-Token-Client-Replay: <requestID>` on every request due to the inability to dynamically append headers to responses in MV3
* Use a pull based method to ease implementation on the extension and origin side
================================================
FILE: devel.md
================================================
## Setting Up Chrome Extension for Development
In a terminal:
```sh
$ git clone https://github.com/cloudflare/pp-browser-extension
$ nvm use 20 // it should work with Node v16+.
$ npm ci
$ npm run build
```
In Chrome:
1. Remove all extensions of Privacy pass. current public extension is v3.0.5.
1. We will install extension v4.0.0 which has **Manifest v3**.
1. Load the extension from the source code.
1. Upon loading, you should see Privacy Pass extension with a red logo. This means you are using the local build.
1. Open the Service worker & Devtools of a blank tab.
1. Navigate to https://demo-pat.research.cloudflare.com/login.
1. 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.
================================================
FILE: devel_pat.md
================================================
## Setting Up PAT in Local Machine.
```sh
$ git clone https://github.com/armfazh/pat-app --branch devel_branch
cd pat-app
```
```sh
$ make certs
```
Make sure to follow the instructions in README for setting up certificates in your machine using mkcert.
In three terminals:
- `make issuer`
- `make origin`
- `make attester`
Running Go client
```sh
$ ./pat-app fetch --origin origin.example:4568 --secret `cat client.secret` --attester attester.example:4569 --resource "/index.html" --token-type basic
```
When 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)
A succesfull run looks like:
```sh
$ ./pat-app fetch --origin origin.example:4568 --secret `cat client.secret` --attester attester.example:4569 --resource "/index.html" --token-type basic
body was fetched!!
<!doctype
```
---
## Setting Up Chrome Extension
In a terminal:
```sh
$ git clone https://github.com/cloudflare/pp-browser-extension
$ nvm use 18 // it should work with Node v16+.
$ npm ci
$ npm run build
```
In Chrome:
1. Remove all extensions of privacy pass. Current extension is v3.0.5.
1. We will install ext v4.1.0
which has **Manifest v3**.
1. Load the extension from the source code.
1. Open the Background Page & Devtools of a blank tab.
1. Navigate to https://origin.example:4568/?type=2
(Make sure the certificates from `mkcert` step work, otherwise cannot load https from localhost). As you can see, we must specify type=2.
1. 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.
================================================
FILE: jest.config.mjs
================================================
export default {
moduleFileExtensions: ['js'],
transform: {},
setupFiles: ['./jest.setup.mjs'],
collectCoverage: true,
verbose: true,
};
================================================
FILE: jest.setup.mjs
================================================
// Mocking crypto with Node WebCrypto API.
import { webcrypto } from 'node:crypto';
if (typeof crypto === 'undefined') {
global.crypto = webcrypto;
}
if (typeof chrome === 'undefined') {
global.chrome = {
declarativeNetRequest: {
ResourceType: {
MAIN_FRAME: 'main_frame',
SUB_FRAME: 'sub_frame',
XMLHTTPREQUEST: 'xmlhttprequest',
},
},
};
}
================================================
FILE: package.json
================================================
{
"name": "silk-privacy-pass-client",
"version": "4.0.2",
"contributors": [
"Suphanat Chunhapanya <pop@cloudflare.com>",
"Armando Faz <armfazh@cloudflare.com>",
"Thibault Meunier <thibault@cloudflare.com>",
"Cefan Rubin <cefan@cloudflare.com>"
],
"main": "index.js",
"license": "BSD-3-Clause",
"type": "module",
"engines": {
"node": ">=18"
},
"scripts": {
"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/",
"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",
"build:chromium": "export PLATFORM='chromium' && npm run build:privacypass",
"build:firefox": "export PLATFORM='firefox' && npm run build:privacypass",
"build:mv3-chromium": "export PLATFORM='mv3/chromium' && npm run build:privacypass",
"build": "npm run build:chromium && npm run build:firefox && npm run build:mv3-chromium",
"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",
"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",
"bundle:firefox": "npm run build:firefox && web-ext build -s dist/firefox --overwrite-dest -a dist/web-ext-artifacts -n firefox.zip",
"bundle": "npm run bundle:chromium && npm run bundle:firefox && npm run bundle:mv3-chromium",
"test": "tsc -b && node --experimental-vm-modules node_modules/jest/bin/jest.js --ci",
"lint": "eslint .",
"format": "eslint . --fix",
"clean": "rimraf lib coverage dist"
},
"dependencies": {
"@cloudflare/privacypass-ts": "0.4.0"
},
"devDependencies": {
"@types/chrome": "0.0.263",
"@types/jest": "29.5.12",
"@types/node": "20.11.25",
"@typescript-eslint/eslint-plugin": "7.1.1",
"@typescript-eslint/parser": "7.1.1",
"esbuild": "0.25.0",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-security": "2.1.1",
"jest": "29.7.0",
"prettier": "3.2.5",
"rimraf": "5.0.5",
"web-ext": "8.10.0"
}
}
================================================
FILE: platform/chromium/index.ts
================================================
import {
BROWSERS,
handleBeforeRequest,
handleBeforeSendHeaders,
handleHeadersReceived,
handleInstall,
handleStartup,
} from '../../src/background';
const BROWSER = BROWSERS.CHROME;
const STORAGE = chrome.storage.local;
chrome.runtime.onInstalled.addListener(handleInstall(STORAGE));
chrome.runtime.onStartup.addListener(handleStartup(STORAGE));
chrome.webRequest.onBeforeRequest.addListener(handleBeforeRequest(), { urls: ['<all_urls>'] }, [
'blocking',
]);
chrome.webRequest.onBeforeSendHeaders.addListener(
handleBeforeSendHeaders(STORAGE),
{ urls: ['<all_urls>'] },
['requestHeaders', 'blocking', 'extraHeaders'],
);
chrome.webRequest.onHeadersReceived.addListener(
handleHeadersReceived(BROWSER, STORAGE),
{ urls: ['<all_urls>'] },
['responseHeaders', 'blocking'],
);
================================================
FILE: platform/chromium/manifest.json
================================================
{
"name": "Silk - Privacy Pass Client",
"manifest_version": 2,
"description": "Client support for Privacy Pass anonymous authorization protocol.",
"version": "4.0.2",
"icons": {
"32": "icons/32/gold.png",
"48": "icons/48/gold.png",
"64": "icons/64/gold.png",
"128": "icons/128/gold.png"
},
"background": {
"scripts": [
"background.js"
]
},
"permissions": [
"<all_urls>",
"storage",
"tabs",
"webRequest",
"webRequestBlocking"
],
"options_ui": {
"page": "options/index.html"
}
}
================================================
FILE: platform/chromium/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"resolveJsonModule": true
},
"include": [
".",
"../../src/background",
"../../src/common"
]
}
================================================
FILE: platform/firefox/global.d.ts
================================================
declare const browser: typeof chrome;
================================================
FILE: platform/firefox/index.ts
================================================
import {
BROWSERS,
handleBeforeSendHeaders,
handleHeadersReceived,
handleInstall,
handleStartup,
} from '../../src/background';
const BROWSER = BROWSERS.FIREFOX;
const STORAGE = browser.storage.local;
chrome.runtime.onInstalled.addListener(handleInstall(STORAGE));
chrome.runtime.onStartup.addListener(handleStartup(STORAGE));
chrome.webRequest.onBeforeSendHeaders.addListener(
handleBeforeSendHeaders(STORAGE),
{ urls: ['<all_urls>'] },
['requestHeaders', 'blocking'],
);
chrome.webRequest.onHeadersReceived.addListener(
handleHeadersReceived(BROWSER, STORAGE),
{ urls: ['<all_urls>'] },
['responseHeaders', 'blocking'],
);
================================================
FILE: platform/firefox/manifest.json
================================================
{
"name": "Silk - Privacy Pass Client",
"manifest_version": 2,
"description": "Client support for Privacy Pass anonymous authorization protocol.",
"version": "4.0.2",
"icons": {
"32": "icons/32/gold.png",
"48": "icons/48/gold.png",
"64": "icons/64/gold.png",
"128": "icons/128/gold.png"
},
"background": {
"scripts": [
"background.js"
]
},
"permissions": [
"<all_urls>",
"storage",
"tabs",
"webRequest",
"webRequestBlocking"
],
"options_ui": {
"page": "options/index.html"
},
"browser_specific_settings": {
"gecko": {
"id": "{48748554-4c01-49e8-94af-79662bf34d50}"
}
}
}
================================================
FILE: platform/firefox/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"resolveJsonModule": true
},
"include": [
".",
"../../src/background",
"../../../src/common"
]
}
================================================
FILE: platform/mv3/chromium/index.ts
================================================
import {
BROWSERS,
handleBeforeRequest,
handleBeforeSendHeaders,
handleHeadersReceived,
handleInstall,
handleStartup,
} from '../../../src/background';
const BROWSER = BROWSERS.CHROME;
const STORAGE = chrome.storage.local;
chrome.runtime.onInstalled.addListener(handleInstall(STORAGE));
chrome.runtime.onStartup.addListener(handleStartup(STORAGE));
chrome.webRequest.onBeforeRequest.addListener(handleBeforeRequest(), { urls: ['<all_urls>'] });
chrome.webRequest.onBeforeSendHeaders.addListener(
handleBeforeSendHeaders(STORAGE),
{ urls: ['<all_urls>'] },
['requestHeaders'],
);
chrome.webRequest.onHeadersReceived.addListener(
handleHeadersReceived(BROWSER, STORAGE),
{ urls: ['<all_urls>'] },
['responseHeaders'],
);
================================================
FILE: platform/mv3/chromium/manifest.json
================================================
{
"name": "Silk - Privacy Pass Client",
"manifest_version": 3,
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+7N/GLYPSBqf+O7fGOaLQ+3hIvXrhsjo8Pq73aSVw08YZRWIIsM+xRnaqdgcu3dXgQGjuVbFVD0nQzg2ymOJk40PSZ/EDdbmWmvD6IrKV88qdmLAlMCvgYW6oRQG5EIBNrmUOqQvTHpdaHrYxY3g4OQfaCe5mvYS64VG6Tb9f1G/UqUcNQzdo1A7x6ElCMjl+isMkt1in749KxgCmhHlZshQfCf1tXwcjhh09luatwP+KCFqCw0VPNMij1eBHi+Z1r43o1RNSTVoiot4mXHMej/rZiApG3U0eVfhp6yOLLakNGnzy1hR5OO00vlMVSyg2HR4VnvPyJuZ+VRNGBo7wIDAQAB",
"description": "Client support for Privacy Pass anonymous authorization protocol.",
"version": "4.0.2",
"icons": {
"32": "icons/32/gold.png",
"48": "icons/48/gold.png",
"64": "icons/64/gold.png",
"128": "icons/128/gold.png"
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"permissions": [
"declarativeNetRequest",
"storage",
"tabs",
"webRequest"
],
"options_ui": {
"page": "options/index.html"
},
"host_permissions": [
"<all_urls>"
]
}
================================================
FILE: platform/mv3/chromium/tsconfig.json
================================================
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"resolveJsonModule": true
},
"include": [
".",
"../../../src/background",
"../../../src/common"
]
}
================================================
FILE: src/background/attesters.ts
================================================
export { refreshAttesterLookupByIssuerKey } from '../common';
import { STORAGE_ID_ATTESTER_CONFIGURATION } from './const';
// return an attester challenge URI based on the public key presented in 401 response
export const keyToAttesterURI = async (
storage: chrome.storage.StorageArea,
key: string,
): Promise<string | undefined> => {
// get attester hostname that corresponds to this key
const storageItem: Record<string, Record<string, string>> = await new Promise((resolve) =>
storage.get([STORAGE_ID_ATTESTER_CONFIGURATION], resolve),
);
const attestersByIssuerKey: Record<string, string> =
storageItem[STORAGE_ID_ATTESTER_CONFIGURATION];
return attestersByIssuerKey[key];
};
================================================
FILE: src/background/const.ts
================================================
export * from '../common';
export const BROWSERS = {
CHROME: 'Chrome',
FIREFOX: 'Firefox',
EDGE: 'Edge',
} as const;
export type BROWSERS = (typeof BROWSERS)[keyof typeof BROWSERS];
export const PRIVACY_PASS_API_REPLAY_HEADER = 'private-token-client-replay';
export const PRIVACY_PASS_API_REPLAY_URI = 'https://no-reply.private-token.research.cloudflare.com';
================================================
FILE: src/background/index.ts
================================================
import { keyToAttesterURI, refreshAttesterLookupByIssuerKey } from './attesters';
import { SERVICE_WORKER_MODE, getRawSettings, getSettings } from '../common';
import { BROWSERS, PRIVACY_PASS_API_REPLAY_HEADER, PRIVACY_PASS_API_REPLAY_URI } from './const';
import { getLogger } from '../common/logger';
import { fetchPublicVerifToken } from './pubVerifToken';
import { REPLAY_STATE, getRequestID, setReplayDomainRule } from './replay';
import { getAuthorizationRule, getIdentificationRule, removeAuthorizationRule } from './rules';
import { QueryablePromise, isManifestV3, promiseToQueryable, uint8ToB64URL } from './util';
import { PrivateToken, TOKEN_TYPES } from '@cloudflare/privacypass-ts';
export { BROWSERS, PRIVACY_PASS_API_REPLAY_URI } from './const';
interface SessionCachedData {
[key: string]: UrlOriginTabs;
}
interface UrlOriginTabs {
[key: number]: OriginAttesterTabDetails;
}
interface OriginAttesterTabDetails {
attesterTabId: number;
attestationData: string;
}
const PENDING = REPLAY_STATE.PENDING;
type PENDING = typeof PENDING;
const TOKENS: Record<string, string | PENDING> = {};
const cachedTabs: Record<number, chrome.tabs.Tab> = {};
export const headerToToken = async (
url: string,
tabId: number,
header: string,
storage: chrome.storage.StorageArea,
): Promise<string | undefined> => {
const { serviceWorkerMode: mode } = getSettings();
const logger = getLogger(mode);
const tokenDetails = PrivateToken.parse(header);
if (tokenDetails.length === 0) {
return undefined;
}
const td = tokenDetails.slice(-1)[0];
switch (td.challenge.tokenType) {
case TOKEN_TYPES.BLIND_RSA.value:
logger.debug(`type of challenge: ${td.challenge.tokenType} is supported`);
// Slow down if demo
if (mode === SERVICE_WORKER_MODE.DEMO) {
await new Promise((resolve) => setTimeout(resolve, 5 * 1000));
}
const tokenPublicKey: string = uint8ToB64URL(td.tokenKey);
let attesterURI = await keyToAttesterURI(storage, tokenPublicKey);
if (!attesterURI) {
return undefined;
}
// API expects interactive Challenge at /challenge
attesterURI = `${attesterURI}/challenge`;
// To minimize the number of tabs opened, we check if the requesting tab is the focused tab
const tabInfo: chrome.tabs.Tab | undefined = await new Promise((resolve) => {
chrome.tabs.get(tabId, resolve);
});
const tabWindow: chrome.windows.Window | undefined = await new Promise((resolve) => {
if (tabInfo) {
chrome.windows.get(tabInfo.windowId, resolve);
}
});
if (!tabWindow?.focused || !tabInfo?.active) {
logger.debug('Not opening a new tab due to requesting tab not being active');
return undefined;
}
const tab: chrome.tabs.Tab = await new Promise((resolve) =>
chrome.tabs.create({ url: attesterURI }, resolve),
);
// save this new tabId of attester tab to session storage under the originTabId
const existing: SessionCachedData = await new Promise((resolve) =>
storage.get(url, resolve),
);
if (existing[url][tabId]) {
existing[url][tabId]['attesterTabId'] = tab.id!;
storage.set({ [url]: existing[url] });
}
const token = await fetchPublicVerifToken(td, tabId, storage);
const encodedToken = uint8ToB64URL(token.serialize());
return encodedToken;
default:
logger.error(`unrecognized type of challenge: ${td.challenge.tokenType}`);
}
return undefined;
};
export const handleInstall =
(storage: chrome.storage.StorageArea) => async (_details: chrome.runtime.InstalledDetails) => {
const { serviceWorkerMode: mode } = await getRawSettings(storage);
const logger = getLogger(mode);
if (isManifestV3(chrome)) {
chrome.declarativeNetRequest
.updateSessionRules(removeAuthorizationRule())
.catch((e: unknown) => logger.debug(`failed to remove session rules:`, e));
}
// Refresh lookup of attester by issuer key lookup used for auto selection of attester
handleStartup(storage);
getRequestID();
setReplayDomainRule(REPLAY_STATE.NOT_FOUND);
};
export const handleStartup = (storage: chrome.storage.StorageArea) => async () => {
const { serviceWorkerMode: mode } = await getRawSettings(storage);
const logger = getLogger(mode);
const alarmName = 'refreshAttesterLookupByIssuerKey';
chrome.alarms.clear(alarmName);
chrome.alarms.create(alarmName, {
delayInMinutes: 0,
periodInMinutes: 24 * 60, // trigger once a day
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === alarmName) {
logger.debug('refreshing attester lookup by issuer key');
refreshAttesterLookupByIssuerKey(storage);
}
});
};
const pendingRequests: Map<string, QueryablePromise<void>> = new Map();
export const handleBeforeRequest = () => (details: chrome.webRequest.WebRequestBodyDetails) => {
const settings = getSettings();
const { serviceWorkerMode: mode } = settings;
const logger = getLogger(mode);
try {
chrome.tabs.get(details.tabId, (tab) => (cachedTabs[details.tabId] = tab));
} catch (err) {
logger.debug(err);
}
// Handle active replay without generating a network request
const url = new URL(details.url);
if (url.origin !== PRIVACY_PASS_API_REPLAY_URI) {
return;
}
const labels = url.pathname.split('/');
if (labels.length !== 2 || labels[0] !== '' || labels[1] !== 'requestID') {
return { redirectUrl: `data:text/plain,${REPLAY_STATE.NOT_FOUND}` };
}
const requestID = labels[2];
const promise = pendingRequests.get(requestID);
let state: REPLAY_STATE = REPLAY_STATE.NOT_FOUND;
if (promise?.isFullfilled) {
state = REPLAY_STATE.FULFILLED;
pendingRequests.delete(requestID);
}
if (promise?.isPending) {
state = REPLAY_STATE.PENDING;
}
return {
redirectUrl: `data:text/plain,${state}`,
};
};
export const handleBeforeSendHeaders =
(storage: chrome.storage.StorageArea) =>
(
details: chrome.webRequest.WebRequestHeadersDetails,
): void | chrome.webRequest.BlockingResponse => {
if (!details) {
return;
}
const ppToken = TOKENS[details.url];
if (ppToken === PENDING) {
return;
}
if (ppToken && !chrome.declarativeNetRequest) {
const headers = details.requestHeaders ?? [];
headers.push({ name: 'Authorization', value: `PrivateToken token=${ppToken}` });
delete TOKENS[details.url];
return { requestHeaders: headers };
}
if (!details.requestHeaders) {
return;
}
if (details.requestHeaders && details.url) {
// check for an attestation data sent from attester
const pp_hdr = details.requestHeaders.find(
(x) => x.name.toLowerCase() === 'private-token-attester-data',
)?.value;
if (pp_hdr) {
const callback = (url_tab_data: SessionCachedData) => {
// if we opened an attesterTab that matches the source of this data then store it
lookForAttesterTabId: for (const url in url_tab_data) {
for (const originTab of Object.values(url_tab_data[url])) {
const tabDetails: OriginAttesterTabDetails = originTab;
if (
tabDetails.attesterTabId &&
tabDetails.attesterTabId === details.tabId
) {
originTab.attestationData = pp_hdr;
storage.set(url_tab_data);
// break to label above
break lookForAttesterTabId;
}
}
}
};
storage.get(null, callback);
}
}
if (isManifestV3(chrome)) {
const { serviceWorkerMode: mode } = getSettings();
const logger = getLogger(mode);
chrome.declarativeNetRequest
.updateSessionRules(removeAuthorizationRule())
.catch((e: unknown) => logger.debug(`failed to remove session rules:`, e));
}
return;
};
export const handleHeadersReceived =
(browser: BROWSERS, storage: chrome.storage.StorageArea) =>
(
details: chrome.webRequest.WebResponseHeadersDetails & {
frameAncestors?: Array<{ url: string }>;
},
): void | chrome.webRequest.BlockingResponse => {
// if no header were received with request
if (!details.responseHeaders) {
return;
}
// Check if there's a valid PrivateToken header
const privateTokenChl = details.responseHeaders.find(
(x) => x.name.toLowerCase() == 'www-authenticate',
)?.value;
if (!privateTokenChl) {
return;
}
if (PrivateToken.parse(privateTokenChl).length === 0) {
return;
}
const settings = getSettings();
if (Object.keys(settings).length === 0) {
getRawSettings(storage);
return;
}
const { attesters, serviceWorkerMode: mode } = settings;
const logger = getLogger(mode);
let initiator: string | undefined = undefined;
if (details.frameId === 0) {
initiator = details.url;
} else {
initiator =
details.frameAncestors?.at(0)?.url ??
cachedTabs[details.tabId]?.url ??
details.initiator;
}
if (!initiator) {
return;
}
const initiatorURL = new URL(initiator)?.origin;
const isAttesterFrame = attesters.map((a) => new URL(a).origin).includes(initiatorURL);
if (isAttesterFrame) {
logger.info('PrivateToken support disabled on attester websites.');
return;
}
if (TOKENS[details.url] === PENDING) {
return;
}
if (isManifestV3(chrome)) {
// TODO: convert to static rule for simplicity perhaps?
chrome.declarativeNetRequest.updateSessionRules(getIdentificationRule(details.url));
}
// create a new entry storing this originTabId
storage.get(details.url, (existing: UrlOriginTabs) => {
existing[details.tabId] = { attesterTabId: -1, attestationData: '' };
storage.set({ [details.url]: existing });
});
// turn this received header into a token (tab opening and attester handling within here)
const w3HeaderValue = headerToToken(details.url, details.tabId, privateTokenChl, storage);
// Add a rule to declarativeNetRequest here if you want to block
// or modify a header from this request. The rule is registered and
// changes are observed between the onBeforeSendHeaders and
// onSendHeaders methods.
if (!chrome.declarativeNetRequest) {
TOKENS[details.url] = PENDING;
}
const redirectPromise = w3HeaderValue
.then(async (value): Promise<void | chrome.webRequest.BlockingResponse> => {
if (!value) {
delete TOKENS[details.url];
return;
}
if (isManifestV3(chrome)) {
await chrome.declarativeNetRequest.updateSessionRules(
getAuthorizationRule(details.url, `PrivateToken token=${value}`),
);
} else {
TOKENS[details.url] = value;
}
return { redirectUrl: details.url };
})
.catch((err) => {
logger.error(`failed to retrieve PrivateToken token: ${err}`);
});
switch (browser) {
case BROWSERS.FIREFOX:
// typing is incorrect, but force it for Firefox because browser is compatible
return redirectPromise as unknown as chrome.webRequest.BlockingResponse;
case BROWSERS.CHROME:
// Refresh tab in chrome.
const requestID = getRequestID();
setReplayDomainRule('pending', requestID);
pendingRequests.set(
requestID,
promiseToQueryable(
redirectPromise.then(() => {
setReplayDomainRule(REPLAY_STATE.FULFILLED, requestID);
}),
),
);
// Detect call context, and only refresh if it's a top level request
redirectPromise.then(async () => {
if (details.type === 'main_frame') {
chrome.tabs.update(details.tabId, { url: details.url });
}
});
const responseHeaders = details.responseHeaders ?? [];
responseHeaders.push({ name: PRIVACY_PASS_API_REPLAY_HEADER, value: requestID });
return {
responseHeaders,
};
}
};
================================================
FILE: src/background/pubVerifToken.ts
================================================
import { Client, PrivateToken, Token, TokenResponse } from '@cloudflare/privacypass-ts';
export async function fetchPublicVerifToken(
privateToken: PrivateToken,
originTabId: number,
storage: chrome.storage.StorageArea,
): Promise<Token> {
let attesterIssuerProxyURI: string;
const attesterToken: string = await new Promise((resolve) => {
storage.onChanged.addListener(async (changes) => {
for (const [, value] of Object.entries(changes)) {
if (!value.newValue) {
continue;
}
const newValue = value.newValue;
if (
newValue[originTabId] &&
newValue[originTabId].hasOwnProperty('attestationData') && // eslint-disable-line no-prototype-builtins
newValue[originTabId].attestationData != ''
) {
// before we close it retrieve the URL of the attester tab
const tab: chrome.tabs.Tab = await new Promise((resolve) =>
chrome.tabs.get(newValue[originTabId].attesterTabId, resolve),
);
if (!tab) {
continue;
}
attesterIssuerProxyURI = tab.url!;
// close the attester tab as we no longer need to interact with the attester front-end
chrome.tabs.remove(newValue[originTabId].attesterTabId);
resolve(newValue[originTabId].attestationData);
}
}
});
});
chrome.tabs.update(originTabId, { active: true });
// Create a TokenRequest.
const client = new Client();
const tokenRequest = await client.createTokenRequest(privateToken);
// Send TokenRequest to Issuer (via Attester) as proxy
const tokenResponse = await tokenRequest.send(
attesterIssuerProxyURI!,
TokenResponse,
new Headers({ 'private-token-attester-data': attesterToken }),
);
// Produce a token by Finalizing the TokenResponse.
const token = await client.finalize(tokenResponse);
return token;
}
================================================
FILE: src/background/replay.ts
================================================
import { PRIVACY_PASS_API_REPLAY_URI } from './const';
import { getRedirectRule, getReplayRule } from './rules';
import { isManifestV3 } from './util';
export const REPLAY_STATE = {
FULFILLED: 'fulfilled',
NOT_FOUND: 'not-found',
PENDING: 'pending',
} as const;
export type REPLAY_STATE = (typeof REPLAY_STATE)[keyof typeof REPLAY_STATE];
export const getRequestID = (() => {
let requestID = crypto.randomUUID();
return () => {
const oldRequestID = requestID;
requestID = crypto.randomUUID();
if (isManifestV3(chrome)) {
chrome.declarativeNetRequest.updateSessionRules(getReplayRule(requestID));
}
return oldRequestID;
};
})();
type UUID = ReturnType<typeof crypto.randomUUID>;
export const setReplayDomainRule = (state: REPLAY_STATE, requestID?: UUID) => {
if (!chrome.declarativeNetRequest) {
return;
}
const filterSuffix = requestID ? `/requestID/${requestID}` : '/*';
const urlFilter = `${PRIVACY_PASS_API_REPLAY_URI}${filterSuffix}`;
chrome.declarativeNetRequest.updateSessionRules(
getRedirectRule(urlFilter, `data:text/plain,${state}`),
);
};
================================================
FILE: src/background/rules.ts
================================================
import { PRIVACY_PASS_API_REPLAY_HEADER } from './const';
// First size digits of md5 of 'privacy-pass-extension-identification' as integer
export const PRIVACY_PASS_EXTENSION_RULE_OFFSET = 11943591;
export function getRuleID(x: number) {
return PRIVACY_PASS_EXTENSION_RULE_OFFSET + x;
}
const RULE_IDS = {
IDENTIFICATION: getRuleID(1),
AUTHORIZATION: getRuleID(2),
REPLAY: getRuleID(3),
REDIRECT: getRuleID(4),
};
const EXTENSION_SUPPORTED_RESOURCE_TYPES = chrome.declarativeNetRequest
? [
chrome.declarativeNetRequest.ResourceType.MAIN_FRAME,
chrome.declarativeNetRequest.ResourceType.SUB_FRAME,
chrome.declarativeNetRequest.ResourceType.XMLHTTPREQUEST,
]
: [];
export function getIdentificationRule(url: string): chrome.declarativeNetRequest.UpdateRuleOptions {
// TODO: convert to static rule for simplicity perhaps?
return {
removeRuleIds: [RULE_IDS.IDENTIFICATION],
addRules: [
{
action: {
type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS,
responseHeaders: [
// Use Server-Timimg header because Set-Cookie is unreliable in this context in Chrome
{
header: 'Server-Timing',
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
value: 'PrivacyPassExtensionV; desc=4',
},
],
},
condition: {
urlFilter: new URL(url).toString(),
resourceTypes: [chrome.declarativeNetRequest.ResourceType.MAIN_FRAME],
},
id: RULE_IDS.IDENTIFICATION,
priority: 1,
},
],
};
}
export function getAuthorizationRule(
url: string,
authorizationHeader: string,
): chrome.declarativeNetRequest.UpdateRuleOptions {
return {
removeRuleIds: [RULE_IDS.AUTHORIZATION],
addRules: [
{
id: RULE_IDS.AUTHORIZATION,
priority: 1,
action: {
type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS,
requestHeaders: [
{
header: 'Authorization',
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
value: authorizationHeader,
},
],
},
condition: {
// Note: The urlFilter must be composed of only ASCII characters.
urlFilter: new URL(url).toString(),
resourceTypes: EXTENSION_SUPPORTED_RESOURCE_TYPES,
},
},
],
};
}
export function removeAuthorizationRule(): chrome.declarativeNetRequest.UpdateRuleOptions {
return {
removeRuleIds: [RULE_IDS.AUTHORIZATION],
};
}
export function getReplayRule(
replayHeader: string,
): chrome.declarativeNetRequest.UpdateRuleOptions {
return {
removeRuleIds: [RULE_IDS.REPLAY],
addRules: [
{
id: RULE_IDS.REPLAY,
priority: 10,
action: {
type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS,
responseHeaders: [
{
header: PRIVACY_PASS_API_REPLAY_HEADER,
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
value: replayHeader,
},
],
},
condition: {
// 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.
urlFilter: '*',
resourceTypes: EXTENSION_SUPPORTED_RESOURCE_TYPES,
},
},
],
};
}
export function getRedirectRule(
urlFilter: string,
redirectURL: string,
): chrome.declarativeNetRequest.UpdateRuleOptions {
return {
removeRuleIds: [RULE_IDS.REDIRECT],
addRules: [
{
id: RULE_IDS.REDIRECT,
priority: 5,
action: {
type: chrome.declarativeNetRequest.RuleActionType.REDIRECT,
redirect: { url: redirectURL },
},
condition: {
urlFilter,
resourceTypes: EXTENSION_SUPPORTED_RESOURCE_TYPES,
},
},
],
};
}
================================================
FILE: src/background/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"resolveJsonModule": true
},
"include": [ ".", "../common" ]
}
================================================
FILE: src/background/util.test.ts
================================================
import { uint8ToB64URL } from './util.js';
describe('uint8ToB64URL', () => {
it('should convert simple arrays', () => {
const u1 = Uint8Array.from([1]);
expect(uint8ToB64URL(u1)).toBe('AQ==');
});
it('should return an empty string on an empty array', () => {
const u = new Uint8Array();
expect(uint8ToB64URL(u)).toBe('');
});
it('should return 2 an empty string on an empty array', () => {
const u = new Uint8Array(16);
expect(uint8ToB64URL(u)).toBe('AAAAAAAAAAAAAAAAAAAAAA==');
});
});
================================================
FILE: src/background/util.ts
================================================
function u8ToB64(u: Uint8Array): string {
return btoa(String.fromCharCode(...u));
}
function b64ToB64URL(s: string): string {
return s.replace(/\+/g, '-').replace(/\//g, '_');
}
export function uint8ToB64URL(u: Uint8Array): string {
return b64ToB64URL(u8ToB64(u));
}
// JavaScript Promise don't expose the current status of the promise
// This extension aims to work exactly like a native Promise (therefore extends) with the addition of getter for the current status
export interface QueryablePromise<T> extends Promise<T> {
isPending: boolean;
isFullfilled: boolean;
isRejected: boolean;
isResolved: boolean;
}
export function promiseToQueryable<T>(p: Promise<T>): QueryablePromise<T> {
let _isFullfilled = false;
let _isRejected = false;
let _isResolved = false;
const ret: QueryablePromise<T> = {
get isPending() {
return !_isFullfilled;
},
get isFullfilled() {
return _isFullfilled;
},
get isRejected() {
return _isRejected;
},
get isResolved() {
return _isResolved;
},
then: p.then,
catch: p.catch,
finally: p.finally,
[Symbol.toStringTag]: p[Symbol.toStringTag],
};
p.then((_result) => {
_isFullfilled = true;
_isResolved = true;
}).catch((_error) => {
_isFullfilled = true;
_isRejected = true;
});
return ret;
}
export function isManifestV3(browser: typeof chrome) {
return !!browser.declarativeNetRequest;
}
================================================
FILE: src/common/const.ts
================================================
export const STORAGE_ID_ATTESTER_CONFIGURATION = 'attesters_by_issuer_key';
================================================
FILE: src/common/index.ts
================================================
export * from './const';
export * from './logger';
export * from './settings';
================================================
FILE: src/common/logger.ts
================================================
/* eslint-disable no-console */
import { SERVICE_WORKER_MODE } from './settings';
interface Logger {
info: (...obj: unknown[]) => void; // Log an info level message
error: (...obj: unknown[]) => void; // Log an error level message
debug: (...obj: unknown[]) => void; // Log a debug level message
warn: (...obj: unknown[]) => void; // Log a Warning level message
log: (...obj: unknown[]) => void; // Log a message
}
class ConsoleLogger implements Logger {
public info = (...obj: unknown[]): void => {
console.info(...obj);
};
public error = (...obj: unknown[]): void => {
console.error(...obj);
};
public debug = (...obj: unknown[]): void => {
console.debug(...obj);
};
public warn = (...obj: unknown[]): void => {
console.warn(...obj);
};
public log = (...obj: unknown[]): void => {
console.log(...obj);
};
}
class ModeLogger extends ConsoleLogger {
private static ALLOWED_CALLS: Record<string, SERVICE_WORKER_MODE[]> = {
INFO: [SERVICE_WORKER_MODE.DEVELOPMENT],
ERROR: [
SERVICE_WORKER_MODE.DEMO,
SERVICE_WORKER_MODE.DEVELOPMENT,
SERVICE_WORKER_MODE.PRODUCTION,
],
DEBUG: [SERVICE_WORKER_MODE.DEVELOPMENT],
WARN: [SERVICE_WORKER_MODE.DEVELOPMENT],
LOG: [
SERVICE_WORKER_MODE.DEMO,
SERVICE_WORKER_MODE.DEVELOPMENT,
SERVICE_WORKER_MODE.PRODUCTION,
],
};
constructor(public mode: SERVICE_WORKER_MODE) {
super();
}
public info = (...obj: unknown[]): void => {
if (!ModeLogger.ALLOWED_CALLS.INFO.includes(this.mode)) {
return;
}
console.info(...obj);
};
public error = (...obj: unknown[]): void => {
if (!ModeLogger.ALLOWED_CALLS.ERROR.includes(this.mode)) {
return;
}
console.error(...obj);
};
public debug = (...obj: unknown[]): void => {
if (!ModeLogger.ALLOWED_CALLS.DEBUG.includes(this.mode)) {
return;
}
console.debug(...obj);
};
public warn = (...obj: unknown[]): void => {
if (!ModeLogger.ALLOWED_CALLS.WARN.includes(this.mode)) {
return;
}
console.warn(...obj);
};
public log = (...obj: unknown[]): void => {
if (!ModeLogger.ALLOWED_CALLS.LOG.includes(this.mode)) {
return;
}
console.log(...obj);
};
}
export function getLogger(mode: SERVICE_WORKER_MODE) {
return new ModeLogger(mode);
}
================================================
FILE: src/common/settings.ts
================================================
import { IssuerConfig } from '@cloudflare/privacypass-ts';
import { STORAGE_ID_ATTESTER_CONFIGURATION } from './const';
import { getLogger } from './logger';
export const SETTING_NAMES = {
SERVICE_WORKER_MODE: 'serviceWorkerMode',
ATTESTERS: 'attesters',
} as const;
export type SettingsKeys = (typeof SETTING_NAMES)[keyof typeof SETTING_NAMES];
export type RawSettings = {
serviceWorkerMode: SERVICE_WORKER_MODE;
attesters: string;
};
export type Settings = {
serviceWorkerMode: SERVICE_WORKER_MODE;
attesters: string[];
};
let settings: Settings = {} as unknown as Settings;
export const rawSettingToSettingAttester = (attestersRaw: string): string[] => {
return attestersRaw
.split(/[\n,]+/) // either split on new line or comma
.filter((attester) => {
try {
new URL(attester);
return true;
} catch (_) {
return false;
}
});
};
export function getRawSettings(storage: chrome.storage.StorageArea): Promise<RawSettings> {
return new Promise((resolve) =>
storage.get(Object.values(SETTING_NAMES), async (items) => {
if (Object.entries(items).length < 2) {
await resetSettings(storage);
const settings = getSettings();
resolve({
attesters: settings.attesters.join('\n'),
serviceWorkerMode: settings.serviceWorkerMode,
});
} else {
const rawSettings = items as unknown as RawSettings;
settings = {
serviceWorkerMode: rawSettings.serviceWorkerMode,
attesters: rawSettingToSettingAttester(rawSettings.attesters),
};
resolve(rawSettings);
}
}),
);
}
export function getSettings(): Settings {
return settings;
}
export const refreshAttesterLookupByIssuerKey = async (storage: chrome.storage.StorageArea) => {
// Force reset associations between public keys and issuers that trust each attester that we know about
const attestersByIssuerKey = new Map<string, string>();
const { attesters, serviceWorkerMode: mode } = getSettings();
const logger = getLogger(mode);
// Populate issuer keys for issuers that trust our ATTESTERS
for (const attester of attesters) {
const nowTimestamp = Date.now();
// Connect to each of the ATTESTERS we know about for their issuer directory
const response = await fetch(`${attester}/v1/private-token-issuer-directory`);
if (!response.ok) {
logger.log(`"${attester}" issuer keys not available, attester will not be used.`);
return;
}
const directory: Record<string, IssuerConfig> = await response.json();
// for each attester in the directory
for (const { 'token-keys': tokenKeys } of Object.values(directory)) {
for (const key of tokenKeys) {
const notBefore = key['not-before'];
if (!notBefore || notBefore > nowTimestamp) {
continue;
}
attestersByIssuerKey.set(key['token-key'], attester);
}
}
storage.set({
[STORAGE_ID_ATTESTER_CONFIGURATION]: Object.fromEntries(attestersByIssuerKey),
});
}
logger.info('Attester lookup by Issuer key populated');
};
export async function saveSettings(
storage: chrome.storage.StorageArea,
name: SettingsKeys,
value: string,
): Promise<void> {
switch (name) {
case 'attesters':
settings[name] = rawSettingToSettingAttester(value);
await refreshAttesterLookupByIssuerKey(storage);
break;
case 'serviceWorkerMode':
if (
Object.values(SERVICE_WORKER_MODE)
.map((s) => s as string)
.includes(value)
) {
settings[name] = value as SERVICE_WORKER_MODE;
}
}
return storage.set({ [name]: value });
}
export async function resetSettings(storage: chrome.storage.StorageArea): Promise<void> {
const DEFAULT = {
serviceWorkerMode: SERVICE_WORKER_MODE.PRODUCTION,
attesters: [
'https://pp-attester-turnstile.research.cloudflare.com',
'https://pp-attester-turnstile-dev.research.cloudflare.com',
],
};
await saveSettings(storage, 'serviceWorkerMode', DEFAULT.serviceWorkerMode);
await saveSettings(storage, 'attesters', DEFAULT.attesters.join('\n'));
}
export const SERVICE_WORKER_MODE = {
PRODUCTION: 'production',
DEVELOPMENT: 'development',
DEMO: 'demo',
} as const;
export type SERVICE_WORKER_MODE = (typeof SERVICE_WORKER_MODE)[keyof typeof SERVICE_WORKER_MODE];
================================================
FILE: src/options/global.d.ts
================================================
declare const browser: typeof chrome;
================================================
FILE: src/options/index.css
================================================
textarea {
width: 80%;
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */
}
.hidden {
display: none;
}
main {
display: flex;
flex-direction: column;
}
section {
margin-bottom: 1em;
}
================================================
FILE: src/options/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Privacy Pass Popup</title>
<link rel="stylesheet" href="index.css">
<script src="index.mjs" type="module"></script>
</head>
<body>
<main>
<section>
<label for="attesters">
Attesters URLs. One URL per line of the form <code>https://example.com</code>.
</label>
<textarea id="attesters" rows="5"></textarea>
</section>
<section>
<label for="serviceWorkerMode">
Development configuration.
</label>
<select id="serviceWorkerMode">
<option value="production">Production</option>
<option value="development">Development</option>
<option value="demo">Demo (Slow)</option>
</select>
</section>
</main>
</body>
</html>
================================================
FILE: src/options/index.ts
================================================
import { getRawSettings, saveSettings, SETTING_NAMES, SettingsKeys } from '../common';
const STORAGE = typeof browser !== 'undefined' ? browser.storage.local : chrome.storage.local;
// When the popup is loaded, load component that are stored in local/sync storage
const onload = async () => {
// Load current settings from sync storage
const settings = await getRawSettings(STORAGE);
// Every setting has a dedicated input which we need to set the default value, and onchange behaviour
for (const name in settings) {
const dropdown = document.getElementById(name) as HTMLInputElement;
dropdown.value = settings[name];
dropdown.addEventListener('change', (event) => {
const target = event.target as HTMLInputElement;
if (!Object.values(SETTING_NAMES).includes(target.id as SettingsKeys)) {
return;
}
saveSettings(STORAGE, target.id as SettingsKeys, target.value.trim());
});
}
// Links won't open correctly within an extension popup. The following overwrite the default behaviour to open a new tab
for (const a of [...document.getElementsByTagName('a')]) {
a.addEventListener('click', (event) => {
event.preventDefault();
chrome.tabs.create({ url: a.href });
});
}
};
document.addEventListener('DOMContentLoaded', onload);
================================================
FILE: src/options/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"resolveJsonModule": true
},
"include": [ ".", "../common" ]
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es2020",
"module": "es2020",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"composite": true,
"incremental": true,
"removeComments": true,
"isolatedModules": true,
"strict": true,
"strictNullChecks": true,
"noEmitOnError": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"allowSyntheticDefaultImports": true,
"rootDir": ".",
"outDir": "lib"
},
"files": [],
"include": [],
"references": [
{
"path": "./src/background"
}
]
}
gitextract_k1shepog/ ├── .eslintrc.json ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── feature_request.md │ │ ├── new_bug.md │ │ └── ui_ux_feedback.md │ └── workflows/ │ ├── action.yml │ └── semgrep.yml ├── .gitignore ├── .npmrc ├── .prettierrc.json ├── CONTRIBUTING.md ├── DEVELOPER.md ├── LICENSE ├── README.md ├── devel.md ├── devel_pat.md ├── jest.config.mjs ├── jest.setup.mjs ├── package.json ├── platform/ │ ├── chromium/ │ │ ├── index.ts │ │ ├── manifest.json │ │ └── tsconfig.json │ ├── firefox/ │ │ ├── global.d.ts │ │ ├── index.ts │ │ ├── manifest.json │ │ └── tsconfig.json │ └── mv3/ │ └── chromium/ │ ├── index.ts │ ├── manifest.json │ └── tsconfig.json ├── src/ │ ├── background/ │ │ ├── attesters.ts │ │ ├── const.ts │ │ ├── index.ts │ │ ├── pubVerifToken.ts │ │ ├── replay.ts │ │ ├── rules.ts │ │ ├── tsconfig.json │ │ ├── util.test.ts │ │ └── util.ts │ ├── common/ │ │ ├── const.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ └── settings.ts │ └── options/ │ ├── global.d.ts │ ├── index.css │ ├── index.html │ ├── index.ts │ └── tsconfig.json └── tsconfig.json
SYMBOL INDEX (52 symbols across 13 files)
FILE: platform/chromium/index.ts
constant BROWSER (line 10) | const BROWSER = BROWSERS.CHROME;
constant STORAGE (line 11) | const STORAGE = chrome.storage.local;
FILE: platform/firefox/index.ts
constant BROWSER (line 9) | const BROWSER = BROWSERS.FIREFOX;
constant STORAGE (line 10) | const STORAGE = browser.storage.local;
FILE: platform/mv3/chromium/index.ts
constant BROWSER (line 10) | const BROWSER = BROWSERS.CHROME;
constant STORAGE (line 11) | const STORAGE = chrome.storage.local;
FILE: src/background/const.ts
constant BROWSERS (line 3) | const BROWSERS = {
type BROWSERS (line 8) | type BROWSERS = (typeof BROWSERS)[keyof typeof BROWSERS];
constant PRIVACY_PASS_API_REPLAY_HEADER (line 10) | const PRIVACY_PASS_API_REPLAY_HEADER = 'private-token-client-replay';
constant PRIVACY_PASS_API_REPLAY_URI (line 11) | const PRIVACY_PASS_API_REPLAY_URI = 'https://no-reply.private-token.rese...
FILE: src/background/index.ts
type SessionCachedData (line 14) | interface SessionCachedData {
type UrlOriginTabs (line 18) | interface UrlOriginTabs {
type OriginAttesterTabDetails (line 22) | interface OriginAttesterTabDetails {
constant PENDING (line 27) | const PENDING = REPLAY_STATE.PENDING;
type PENDING (line 28) | type PENDING = typeof PENDING;
constant TOKENS (line 29) | const TOKENS: Record<string, string | PENDING> = {};
FILE: src/background/pubVerifToken.ts
function fetchPublicVerifToken (line 3) | async function fetchPublicVerifToken(
FILE: src/background/replay.ts
constant REPLAY_STATE (line 5) | const REPLAY_STATE = {
type REPLAY_STATE (line 10) | type REPLAY_STATE = (typeof REPLAY_STATE)[keyof typeof REPLAY_STATE];
type UUID (line 25) | type UUID = ReturnType<typeof crypto.randomUUID>;
FILE: src/background/rules.ts
constant PRIVACY_PASS_EXTENSION_RULE_OFFSET (line 4) | const PRIVACY_PASS_EXTENSION_RULE_OFFSET = 11943591;
function getRuleID (line 5) | function getRuleID(x: number) {
constant RULE_IDS (line 9) | const RULE_IDS = {
constant EXTENSION_SUPPORTED_RESOURCE_TYPES (line 16) | const EXTENSION_SUPPORTED_RESOURCE_TYPES = chrome.declarativeNetRequest
function getIdentificationRule (line 24) | function getIdentificationRule(url: string): chrome.declarativeNetReques...
function getAuthorizationRule (line 52) | function getAuthorizationRule(
function removeAuthorizationRule (line 82) | function removeAuthorizationRule(): chrome.declarativeNetRequest.UpdateR...
function getReplayRule (line 88) | function getReplayRule(
function getRedirectRule (line 117) | function getRedirectRule(
FILE: src/background/util.ts
function u8ToB64 (line 1) | function u8ToB64(u: Uint8Array): string {
function b64ToB64URL (line 5) | function b64ToB64URL(s: string): string {
function uint8ToB64URL (line 9) | function uint8ToB64URL(u: Uint8Array): string {
type QueryablePromise (line 15) | interface QueryablePromise<T> extends Promise<T> {
function promiseToQueryable (line 22) | function promiseToQueryable<T>(p: Promise<T>): QueryablePromise<T> {
function isManifestV3 (line 54) | function isManifestV3(browser: typeof chrome) {
FILE: src/common/const.ts
constant STORAGE_ID_ATTESTER_CONFIGURATION (line 1) | const STORAGE_ID_ATTESTER_CONFIGURATION = 'attesters_by_issuer_key';
FILE: src/common/logger.ts
type Logger (line 5) | interface Logger {
class ConsoleLogger (line 13) | class ConsoleLogger implements Logger {
class ModeLogger (line 35) | class ModeLogger extends ConsoleLogger {
method constructor (line 52) | constructor(public mode: SERVICE_WORKER_MODE) {
function getLogger (line 92) | function getLogger(mode: SERVICE_WORKER_MODE) {
FILE: src/common/settings.ts
constant SETTING_NAMES (line 5) | const SETTING_NAMES = {
type SettingsKeys (line 9) | type SettingsKeys = (typeof SETTING_NAMES)[keyof typeof SETTING_NAMES];
type RawSettings (line 10) | type RawSettings = {
type Settings (line 15) | type Settings = {
function getRawSettings (line 35) | function getRawSettings(storage: chrome.storage.StorageArea): Promise<Ra...
function getSettings (line 57) | function getSettings(): Settings {
function saveSettings (line 98) | async function saveSettings(
function resetSettings (line 120) | async function resetSettings(storage: chrome.storage.StorageArea): Promi...
constant SERVICE_WORKER_MODE (line 132) | const SERVICE_WORKER_MODE = {
type SERVICE_WORKER_MODE (line 137) | type SERVICE_WORKER_MODE = (typeof SERVICE_WORKER_MODE)[keyof typeof SER...
FILE: src/options/index.ts
constant STORAGE (line 3) | const STORAGE = typeof browser !== 'undefined' ? browser.storage.local :...
Condensed preview — 47 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (72K chars).
[
{
"path": ".eslintrc.json",
"chars": 1183,
"preview": "{\n \"parser\": \"@typescript-eslint/parser\",\n \"parserOptions\": {\n \"ecmaVersion\": 2020,\n \"sourceType\": \""
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 440,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: feature-request\nassignees: ''\n\n---\n\n"
},
{
"path": ".github/ISSUE_TEMPLATE/new_bug.md",
"chars": 1019,
"preview": "---\nname: Bug Report\nabout: Create a report for a bug in Privacy Pass browser extension\ntitle: ''\nlabels: triage\nassigne"
},
{
"path": ".github/ISSUE_TEMPLATE/ui_ux_feedback.md",
"chars": 474,
"preview": "---\nname: UI/UX Feedback\nabout: We welcome feedback regarding user experience\ntitle: ''\nlabels: enhancement\nassignees: '"
},
{
"path": ".github/workflows/action.yml",
"chars": 1274,
"preview": "---\nname: Silk Privacy Pass Client Extension\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njob"
},
{
"path": ".github/workflows/semgrep.yml",
"chars": 586,
"preview": "on:\n pull_request: {}\n workflow_dispatch: {}\n push: \n branches:\n - main\n - master\n schedule:\n - cron"
},
{
"path": ".gitignore",
"chars": 41,
"preview": "*.swp\n/node_modules\n/dist\n/lib\n/coverage\n"
},
{
"path": ".npmrc",
"chars": 19,
"preview": "engine-strict=true\n"
},
{
"path": ".prettierrc.json",
"chars": 116,
"preview": "{\n \"semi\": true,\n \"singleQuote\": true,\n \"tabWidth\": 4,\n \"trailingComma\": \"all\",\n \"printWidth\": 100\n}\n"
},
{
"path": "CONTRIBUTING.md",
"chars": 280,
"preview": "## Contribution Guidelines\n\n### Code Style\n\nCode is written in TypeScript and is automatically formatted with prettier.\n"
},
{
"path": "DEVELOPER.md",
"chars": 618,
"preview": "## Directory Structure\n\n```\npp-browser-extension\n├──📂 platform: Contains platform specific assets, such as manifest for "
},
{
"path": "LICENSE",
"chars": 1536,
"preview": "Copyright (c) 2017-2022, Privacy Pass Team, Cloudflare, Inc., and other contributors. All rights reserved.\n\nRedistributi"
},
{
"path": "README.md",
"chars": 11392,
"preview": "[](https://github.com/cloudf"
},
{
"path": "devel.md",
"chars": 917,
"preview": "## Setting Up Chrome Extension for Development\n\nIn a terminal:\n\n```sh\n$ git clone https://github.com/cloudflare/pp-brows"
},
{
"path": "devel_pat.md",
"chars": 1854,
"preview": "## Setting Up PAT in Local Machine.\n\n```sh\n $ git clone https://github.com/armfazh/pat-app --branch devel_branch\ncd pat-"
},
{
"path": "jest.config.mjs",
"chars": 157,
"preview": "export default {\n moduleFileExtensions: ['js'],\n transform: {},\n setupFiles: ['./jest.setup.mjs'],\n collectC"
},
{
"path": "jest.setup.mjs",
"chars": 443,
"preview": "// Mocking crypto with Node WebCrypto API.\nimport { webcrypto } from 'node:crypto';\n\nif (typeof crypto === 'undefined') "
},
{
"path": "package.json",
"chars": 3092,
"preview": "{\n \"name\": \"silk-privacy-pass-client\",\n \"version\": \"4.0.2\",\n \"contributors\": [\n \"Suphanat Chunhapanya <p"
},
{
"path": "platform/chromium/index.ts",
"chars": 831,
"preview": "import {\n BROWSERS,\n handleBeforeRequest,\n handleBeforeSendHeaders,\n handleHeadersReceived,\n handleInstal"
},
{
"path": "platform/chromium/manifest.json",
"chars": 633,
"preview": "{\n \"name\": \"Silk - Privacy Pass Client\",\n \"manifest_version\": 2,\n \"description\": \"Client support for Privacy Pa"
},
{
"path": "platform/chromium/tsconfig.json",
"chars": 202,
"preview": "{\n \"extends\": \"../../tsconfig.json\",\n \"compilerOptions\": {\n \"resolveJsonModule\": true\n },\n \"include\":"
},
{
"path": "platform/firefox/global.d.ts",
"chars": 37,
"preview": "declare const browser: typeof chrome;"
},
{
"path": "platform/firefox/index.ts",
"chars": 674,
"preview": "import {\n BROWSERS,\n handleBeforeSendHeaders,\n handleHeadersReceived,\n handleInstall,\n handleStartup,\n} f"
},
{
"path": "platform/firefox/manifest.json",
"chars": 762,
"preview": "{\n \"name\": \"Silk - Privacy Pass Client\",\n \"manifest_version\": 2,\n \"description\": \"Client support for Privacy Pa"
},
{
"path": "platform/firefox/tsconfig.json",
"chars": 206,
"preview": "{\n \"extends\": \"../../tsconfig.json\",\n \"compilerOptions\": {\n \"resolveJsonModule\": true\n },\n \"include\":"
},
{
"path": "platform/mv3/chromium/index.ts",
"chars": 773,
"preview": "import {\n BROWSERS,\n handleBeforeRequest,\n handleBeforeSendHeaders,\n handleHeadersReceived,\n handleInstal"
},
{
"path": "platform/mv3/chromium/manifest.json",
"chars": 1084,
"preview": "{\n \"name\": \"Silk - Privacy Pass Client\",\n \"manifest_version\": 3,\n \"key\": \"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBC"
},
{
"path": "platform/mv3/chromium/tsconfig.json",
"chars": 211,
"preview": "{\n \"extends\": \"../../../tsconfig.json\",\n \"compilerOptions\": {\n \"resolveJsonModule\": true\n },\n \"includ"
},
{
"path": "src/background/attesters.ts",
"chars": 727,
"preview": "export { refreshAttesterLookupByIssuerKey } from '../common';\nimport { STORAGE_ID_ATTESTER_CONFIGURATION } from './const"
},
{
"path": "src/background/const.ts",
"chars": 374,
"preview": "export * from '../common';\n\nexport const BROWSERS = {\n CHROME: 'Chrome',\n FIREFOX: 'Firefox',\n EDGE: 'Edge',\n} "
},
{
"path": "src/background/index.ts",
"chars": 13821,
"preview": "import { keyToAttesterURI, refreshAttesterLookupByIssuerKey } from './attesters';\nimport { SERVICE_WORKER_MODE, getRawSe"
},
{
"path": "src/background/pubVerifToken.ts",
"chars": 2182,
"preview": "import { Client, PrivateToken, Token, TokenResponse } from '@cloudflare/privacypass-ts';\n\nexport async function fetchPub"
},
{
"path": "src/background/replay.ts",
"chars": 1172,
"preview": "import { PRIVACY_PASS_API_REPLAY_URI } from './const';\nimport { getRedirectRule, getReplayRule } from './rules';\nimport "
},
{
"path": "src/background/rules.ts",
"chars": 4830,
"preview": "import { PRIVACY_PASS_API_REPLAY_HEADER } from './const';\n\n// First size digits of md5 of 'privacy-pass-extension-identi"
},
{
"path": "src/background/tsconfig.json",
"chars": 144,
"preview": "{\n \"extends\": \"../../tsconfig.json\",\n \"compilerOptions\": {\n \"resolveJsonModule\": true\n },\n \"include\":"
},
{
"path": "src/background/util.test.ts",
"chars": 562,
"preview": "import { uint8ToB64URL } from './util.js';\n\ndescribe('uint8ToB64URL', () => {\n it('should convert simple arrays', () "
},
{
"path": "src/background/util.ts",
"chars": 1566,
"preview": "function u8ToB64(u: Uint8Array): string {\n return btoa(String.fromCharCode(...u));\n}\n\nfunction b64ToB64URL(s: string)"
},
{
"path": "src/common/const.ts",
"chars": 76,
"preview": "export const STORAGE_ID_ATTESTER_CONFIGURATION = 'attesters_by_issuer_key';\n"
},
{
"path": "src/common/index.ts",
"chars": 79,
"preview": "export * from './const';\nexport * from './logger';\nexport * from './settings';\n"
},
{
"path": "src/common/logger.ts",
"chars": 2584,
"preview": "/* eslint-disable no-console */\n\nimport { SERVICE_WORKER_MODE } from './settings';\n\ninterface Logger {\n info: (...obj"
},
{
"path": "src/common/settings.ts",
"chars": 4860,
"preview": "import { IssuerConfig } from '@cloudflare/privacypass-ts';\nimport { STORAGE_ID_ATTESTER_CONFIGURATION } from './const';\n"
},
{
"path": "src/options/global.d.ts",
"chars": 37,
"preview": "declare const browser: typeof chrome;"
},
{
"path": "src/options/index.css",
"chars": 341,
"preview": "textarea {\n width: 80%;\n -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */\n -moz-box-sizing: bo"
},
{
"path": "src/options/index.html",
"chars": 758,
"preview": "<!DOCTYPE html>\n<html>\n\n<head>\n <title>Privacy Pass Popup</title>\n <link rel=\"stylesheet\" href=\"index.css\">\n <script "
},
{
"path": "src/options/index.ts",
"chars": 1396,
"preview": "import { getRawSettings, saveSettings, SETTING_NAMES, SettingsKeys } from '../common';\n\nconst STORAGE = typeof browser !"
},
{
"path": "src/options/tsconfig.json",
"chars": 144,
"preview": "{\n \"extends\": \"../../tsconfig.json\",\n \"compilerOptions\": {\n \"resolveJsonModule\": true\n },\n \"include\":"
},
{
"path": "tsconfig.json",
"chars": 899,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"es2020\",\n \"module\": \"es2020\",\n \"declaration\": true,\n "
}
]
About this extraction
This page contains the full source code of the cloudflare/pp-browser-extension GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 47 files (65.8 KB), approximately 16.6k tokens, and a symbol index with 52 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.