Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 51 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,74 @@ on:
pull_request:

jobs:
build-and-lint:
name: typecheck · build · web-ext lint
checks:
name: lint · typecheck · test · build · web-ext · size
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: npm

- name: Install dependencies
run: npm ci

# Security lint: blocks HTML-sink XSS patterns (innerHTML, insertAdjacentHTML,
# eval, …). Fast, so it runs first.
- name: Lint (security)
run: npm run lint

- name: Typecheck
run: npm run typecheck

- name: Test
run: npm test

- name: Build (Chrome + Firefox)
run: npm run build:all

# Lints the built Firefox extension. Fails the job on errors
# (e.g. invalid manifest, wrong icon size); benign warnings (such as
# Lints the built Firefox extension. Fails on errors (e.g. invalid
# manifest, wrong icon size); benign warnings (such as
# data_collection_permissions min-version notes) do not fail the build.
- name: Lint Firefox build (web-ext)
run: npx --yes web-ext lint --source-dir=dist-firefox

# content.js is injected on every page — guard against size regressions.
- name: Bundle-size budget
run: npm run size

# BUILD.md promises AMO reviewers a byte-for-byte reproducible package.
# Verify run-to-run determinism: two clean builds must be identical.
- name: Reproducible-build check
run: |
checksum() { (cd "$1" && find . -type f -exec sha256sum {} \; | sort); }
npm run clean && npm run build:all
{ checksum dist; checksum dist-firefox; } > /tmp/build-a.sha
npm run clean && npm run build:all
{ checksum dist; checksum dist-firefox; } > /tmp/build-b.sha
if diff -u /tmp/build-a.sha /tmp/build-b.sha; then
echo "Build output is reproducible run-to-run."
else
echo "::error::Build output is not deterministic — see the diff above."
exit 1
fi

permissions-guard:
name: manifest permissions guard
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-node@v4
with:
node-version: 24

# Non-failing: annotates the PR when permissions / host_permissions change
# (they trigger store re-review + a user re-consent prompt).
- name: Diff manifest permissions vs base
run: node scripts/check-permissions.mjs "${{ github.event.pull_request.base.sha }}"
43 changes: 43 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Security-focused ESLint config. Deliberately narrow: this enforces the
// extension's XSS-safe DOM convention, not a general style overhaul (so it
// doesn't churn on `any`/formatting). The extension injects a content script
// on <all_urls> and has a prior XSS fix in its history — treating HTML sinks
// as hard errors keeps that class of bug out.
import tsparser from '@typescript-eslint/parser';
import nounsanitized from 'eslint-plugin-no-unsanitized';

export default [
{ ignores: ['dist/', 'dist-firefox/', 'node_modules/'] },
{
files: ['src/**/*.ts'],
languageOptions: {
parser: tsparser,
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
},
plugins: { 'no-unsanitized': nounsanitized },
rules: {
// Untrusted data into HTML sinks (innerHTML/outerHTML assignment,
// insertAdjacentHTML, document.write, Range.createContextualFragment…).
'no-unsanitized/property': 'error',
'no-unsanitized/method': 'error',
// The repo convention is stricter than no-unsanitized: never touch these
// sinks at all, regardless of the value — build DOM via textContent /
// createElement. Encoded as a hard syntactic ban.
'no-restricted-syntax': [
'error',
{
selector: 'MemberExpression[property.name=/^(innerHTML|outerHTML)$/]',
message:
'Do not use innerHTML/outerHTML — build DOM via textContent/createElement (XSS-safe convention).',
},
{
selector: "CallExpression[callee.property.name='insertAdjacentHTML']",
message:
'Do not use insertAdjacentHTML — build DOM nodes instead (XSS-safe convention).',
},
],
'no-eval': 'error',
'no-implied-eval': 'error',
},
},
];
Loading
Loading