Skip to content

feat: add Edge/Chromium browser extension support#138

Open
phelix001 wants to merge 3 commits intolinux-credentials:mainfrom
phelix001:feat/edge-chromium-webext
Open

feat: add Edge/Chromium browser extension support#138
phelix001 wants to merge 3 commits intolinux-credentials:mainfrom
phelix001:feat/edge-chromium-webext

Conversation

@phelix001
Copy link

@phelix001 phelix001 commented Feb 16, 2026

Summary

Adds Edge/Chromium (Chrome 111+, Edge 111+) support to the browser extension, unified with the existing Firefox extension into a single codebase.

  • Unified webext/add-on/ with shared JS and browser-specific manifests
  • Both browsers use the same MAIN + ISOLATED world content script architecture

Architecture

Both Firefox and Chromium now share the same content script architecture:

Script World Role
content-main.js MAIN Overrides navigator.credentials.create/get, serializes ArrayBuffers via native Uint8Array.toBase64()/fromBase64()
content-bridge.js ISOLATED Bridges window.postMessage to runtime.connect() for native messaging
background.js Background Forwards messages between content scripts and native messaging host

Browser differences handled via:

  • manifest.firefox.json — background scripts, gecko.strict_min_version: 140.0
  • manifest.chromium.json — service worker, Chrome 111+
  • globalThis.browser || globalThis.chrome for API detection

The Python native messaging host (credential_manager_shim.py) is reused unchanged.

Commits

  1. feat: add Edge/Chromium web extension port — initial port with separate add-on-edge/ directory
  2. refactor: merge Firefox and Chromium add-ons into unified folder — addresses review feedback, eliminates code duplication, removes cloneInto()/exportFunction() in favor of shared MAIN+ISOLATED architecture
  3. docs: update READMEs for unified browser extension — updates README for Chromium support, fixes stale add-on-edge/ references in webext/README

Test plan

  • Load extension in Edge via edge://extensions → "Load unpacked" (copy manifest.chromium.json to manifest.json first)
  • Configure native messaging manifest with extension ID
  • Start credentialsd + credentialsd-ui D-Bus services
  • Test credential registration on https://webauthn.io
  • Test credential assertion on https://webauthn.io
  • Test on Chrome (chrome://extensions) with equivalent setup
  • Verify Firefox extension still works unchanged (copy manifest.firefox.json to manifest.json)

Port the Firefox web extension to Edge/Chromium (MV3, Chrome 111+).

Key architectural differences from Firefox version:
- Two content scripts: MAIN world (overrides navigator.credentials)
  and ISOLATED world (bridges to background via chrome.runtime)
- window.postMessage bridge between MAIN and ISOLATED worlds
  (Firefox uses exportFunction/cloneInto which don't exist in Chromium)
- Base64url encoding via btoa/atob helpers instead of
  Uint8Array.toBase64/fromBase64 (not available in Chromium)
- Service worker background script instead of persistent background page
- chrome.* namespace instead of browser.*

New files:
- webext/add-on-edge/ - Complete Edge/Chromium extension
- webext/app/credential_manager_shim_edge.json.in - Native messaging
  manifest template for Chromium-based browsers

Updated README with Edge/Chromium setup instructions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Member

@iinuwa iinuwa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello! Thanks for looking into this!

I haven't gotten the chance to test this out yet, but I did an initial look through, and there's quite a bit of duplicated code. We hope not to have to keep this around long term, but I still think it'd be helpful not to duplicate the code.

I think this means that I'd like to see if we can keep all the JavaScript files in the one add-on folder, with different manifests and "utils" files that contain the differences between Firefox and Chromium, and a check at runtime to import the correct one. If that means creating the extra "bridge" port in Firefox and/or a shim of cloneInto() for Chromium even if it's technically unnecessary, then that's fine with me.

Then we'd use Meson to bundle the add-ons for each browser platform.

I can help with the Meson parts; would you be willing to look into merging these two folders together?

Comment on lines +9 to +27
function arrayBufferToBase64url(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

function base64urlToBytes(str) {
if (!str) return null;
const padded = str.replace(/-/g, '+').replace(/_/g, '/');
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is built into Chromium as Uint8Array.from/toBase64; can we use those?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — switched to native Uint8Array.toBase64() / fromBase64() with {alphabet: "base64url", omitPadding: true} throughout. The manual btoa/atob helpers are removed entirely.

Comment on lines +10 to +29
// Base64url helpers (Chromium doesn't have Uint8Array.toBase64/fromBase64)
function arrayBufferToBase64url(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

function base64urlToArrayBuffer(str) {
if (!str) return null;
const padded = str.replace(/-/g, '+').replace(/_/g, '/');
const binary = atob(padded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here; the comment is out of date: Chrome has had these for about 6 months.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — removed the outdated comment and the manual helpers. Using native Uint8Array.toBase64() / fromBase64() here as well.

Address PR review feedback to eliminate code duplication between
webext/add-on/ (Firefox) and webext/add-on-edge/ (Chromium).

Key changes:
- Unified architecture: both browsers now use MAIN + ISOLATED world
  content scripts with window.postMessage bridge, eliminating the
  need for Firefox-specific cloneInto()/exportFunction() APIs
- Use native Uint8Array.toBase64()/fromBase64() for base64url
  encoding/decoding (supported in both Firefox 140+ and Chrome 111+)
- Simplified background.js: ArrayBuffer serialization now happens in
  content-main.js, so background just forwards messages
- Browser-specific manifests: manifest.firefox.json (background
  scripts) and manifest.chromium.json (service worker)
- Browser API detection via globalThis.browser || globalThis.chrome
  in content-bridge.js and background.js

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phelix001
Copy link
Author

Thanks for the review! I've pushed a commit that addresses all the feedback:

Merged into a single add-on/ folder — deleted add-on-edge/ entirely. Both browsers now share the same JS files:

  • content-main.js (MAIN world) — overrides navigator.credentials, handles all ArrayBuffer serialization
  • content-bridge.js (ISOLATED world) — relays messages between MAIN world and background via window.postMessageruntime.connect
  • background.js — simplified to just forward messages (no more serializeRequest needed since content-main.js handles serialization)

Browser-specific manifests: manifest.firefox.json (background scripts) and manifest.chromium.json (service worker)

Native Uint8Array.toBase64()/fromBase64() used everywhere — all manual btoa/atob helpers removed.

Browser API detection via globalThis.browser || globalThis.chrome in background.js and content-bridge.js.

Firefox now uses the same MAIN + ISOLATED world architecture as Chromium (with the bridge port as you suggested), which eliminates the need for cloneInto()/exportFunction() entirely.

I left a TODO in meson.build for the Chromium build target — happy to take your help on that part.

@phelix001 phelix001 changed the title feat: add Edge/Chromium web extension port feat: add Edge/Chromium browser extension support Feb 24, 2026
@phelix001
Copy link
Author

phelix001 commented Feb 24, 2026

Pushed two new commits on top of the refactor:

docs: update READMEs for unified browser extension (58d581e)

  • Updated main README to document Edge/Chromium support alongside Firefox
  • Fixed webext/README.md — references to the deleted add-on-edge/ directory are gone, and both Firefox and Chromium dev workflows now include the manifest.json copy step (since browsers won't read manifest.firefox.json or manifest.chromium.json directly)
  • Added webext/add-on/manifest.json to .gitignore (generated file from the copy step)

ci: pin Rust toolchain, update actions, add audit and JS checks (612f201)

  • Added rust-toolchain.toml pinning to Rust 1.85 (MSRV for edition 2024 used by credentialsd-common and credentialsd-ui)
  • Updated actions/checkout from v2 to v4
  • Removed manual rustup component add rustfmt clippy — now declared in rust-toolchain.toml
  • Added cargo audit step for dependency vulnerability scanning
  • Added node --check step to validate web extension JS syntax

Happy to split the CI/toolchain changes into a separate PR if you'd prefer to keep this one focused on the web extension work.

- Update README to document Edge/Chromium support alongside Firefox
- Fix webext/README references to deleted add-on-edge/ directory
- Add manifest.json copy step for both Firefox and Chromium dev workflows
- Add webext/add-on/manifest.json to .gitignore (generated for local dev)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phelix001 phelix001 force-pushed the feat/edge-chromium-webext branch from 1f2f730 to 612f201 Compare February 24, 2026 22:42
@iinuwa
Copy link
Member

iinuwa commented Feb 27, 2026

Thanks for the update! I'm hoping to get to this sometime this weekend.

In the meantime can you remove the CLAUDE.md? We're not ready to declare a policy on AI usage yet.

The unrelated CI changes should also move to a separate PR to make this one easier to review.

@phelix001 phelix001 force-pushed the feat/edge-chromium-webext branch from 612f201 to 58d581e Compare February 27, 2026 21:50
@phelix001
Copy link
Author

Done! Two changes:

  1. CLAUDE.md removed — it was never in the committed files, but I've cleaned up the PR description that referenced it. It won't appear anywhere in this PR.

  2. CI changes split out — the toolchain/actions commit is now in a separate PR: ci: pin Rust toolchain, update actions, add audit and JS checks #143

This PR is now 3 commits, all focused on the web extension work.

@iinuwa ready when you are this weekend.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants