A chromium extension for automatically opening tabs according to some predefined standard.
  • JavaScript 77.3%
  • HTML 8.4%
  • CSS 7.8%
  • Nix 6.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-20 18:14:25 -05:00
.forgejo/workflows feat: scaffold out basic features 2026-10-16 13:14:12 -05:00
extension feat: scaffold out basic features 2026-10-16 13:14:12 -05:00
nix-modules build: restructure nix checks like tabberwockey 2026-10-17 00:23:29 -05:00
tests feat: scaffold out basic features 2026-10-16 13:14:12 -05:00
.envrc feat: scaffold out basic features 2026-10-16 13:14:12 -05:00
.gitignore feat: scaffold out basic features 2026-10-16 13:14:12 -05:00
flake.lock feat: scaffold out basic features 2026-10-16 13:14:12 -05:00
flake.nix feat: scaffold out basic features 2026-10-16 13:14:12 -05:00
LICENSE Initial commit 2026-10-16 01:06:37 -05:00
README.md feat: scaffold out basic features 2026-10-16 13:14:12 -05:00

Sourcerer

A chromium extension for automatically opening tabs according to some predefined standard.

A user provides a URL to poll and a script that runs on the page data. An informative example would be one that automatically opens up MRs for a GitLab repository every 10 minutes. It of course checks that the MR isn't already open in the browser.

How it works

A job is a URL, an interval, and a script. On every interval the service worker polls the URL, hands the result to your script inside a manifest-sandboxed page (the only place MV3 permits new Function on user code), and opens every URL the script returns as a background tab — skipping URLs already open in the browser or previously opened by that job (closing a tab does not reopen it; use Reset seen to forget them).

Scripts are the body of a function with context bound, running in strict mode. Return a URL, an array of URLs, or {url} objects. Relative URLs are resolved against the polled URL.

Fetch mode — the URL is fetched (with cookies, so logged-in sessions work) and:

  • context.body is the parsed response when it is JSON, otherwise the raw text
  • context.text is always the raw response body
  • context.status / context.url are the response status and final URL

DOM mode — the URL loads in a background tab, the page settles (give client-rendered pages time), and context.doc is the parsed page. A <base> tag is injected so a.href resolves relative links against the polled page.

The GitLab example

MR lists on GitLab's web UI are rendered client-side, so the reliable source is the JSON API (the options page ships this example under Load GitLab example):

// Poll: https://gitlab.com/api/v4/projects/gitlab-org%2Fgitlab/merge_requests
//       ?state=opened&scope=all&order_by=updated_at&per_page=5
return context.body
  .filter((mr) => mr.state === 'opened')
  .map((mr) => mr.web_url);

Authentication (PATs and friends)

Polling private APIs needs a token. Sourcerer does this without leaking it:

  • Secrets are stored under Options → Secrets: values are never displayed again, never placed in the polled URL (URLs land in run logs and the seen-set), never passed to the job script (which only ever sees {url, status, body, text}), and never synced through your browser account.
  • A job references a secret by name and a header (e.g. header PRIVATE-TOKEN, or header Authorization + scheme Bearer). The service worker attaches the header when it fetches. If the referenced secret is missing, the run is recorded as an error instead of sending an unauthenticated request.
  • Lifetime — off by default, secrets live in chrome.storage.session: in-memory only, nothing written to the profile on disk, wiped when the browser exits (so re-enter them after a restart). If you opt into persistence, the store moves to chrome.storage.local: secrets survive restarts but sit unencrypted in the browser profile on disk, readable by anything with access to that profile. Turning the toggle back off wipes them from disk.

The honest boundary: anything running as your user (or with access to your unlocked browser) can read browser memory; no extension can defend against a compromised host. Session storage keeps the token off disk and out of every other channel (logs, scripts, sync, exports).

Development

Everything runs through nix; the devshell provides all tooling (no global installs, no npm/pip):

direnv allow            # enter the devshell (or: nix develop)

nix flake check                                     # package + lint + unit + fixture e2e
nix build .#sourcerer                               # extension in result/share/sourcerer + web-store zip
playwright test -c tests/e2e/playwright.config.js   # fixture e2e (local, headed-able)
playwright test -c tests/e2e/playwright.network.config.js  # gitlab.com e2e (needs network)

To try the extension by hand: nix build .#sourcerer, then in Chromium open chrome://extensions, enable developer mode, and Load unpacked pointing at result/share/sourcerer. (The PNG icons are rendered from icons/icon.svg during the build; the repo's extension/ directory alone is not loadable.)

Layout

  • extension/ — the extension: service worker, options page, popup, sandbox
  • extension/lib/ — pure logic (validation, result normalization, dedupe), unit-tested
  • tests/unit/ — node --test unit tests, no dependencies
  • tests/e2e/ — Playwright tests: specs/ run hermetically against a local fixture server (part of nix flake check); specs-network/ hit real gitlab.com and run via the devshell/CI
  • nix-modules/ — flake-parts modules (package, checks, devshell)

CI lives in .forgejo/workflows/ci.yml: nix flake check, the network e2e suite, and every locally-built store path pushed to an Attic cache via jingus/actions.

License

MIT