app
Adom Desktop - Puppeteer Bridge
Public Made by Adomby adom
The Adom Desktop Puppeteer (pup) bridge — drive a real headful browser on the user's desktop from the cloud: open/screenshot/eval/record. Cold-start drives the browser ALREADY on the machine (installed Chrome, else Microsoft Edge — every Windows PC ships with Edge) with a fresh isolated profile, so the common case needs NO download; Chrome for Testing is fetched only as a last resort. Two artifacts: the Releases zip is the bridge runtime (also bundled in Adom Desktop); the pkg installs the container-side Claude skills.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
// credential_vault.js — HTTP Basic Auth credential vault for pup sessions.
//
// Why this exists
// ───────────────
// When a pup tab navigates to a host that issues an HTTP Basic Auth challenge
// (nxp.componentsearchengine.com, mouser.componentsearchengine.com, several
// vendor design portals), Chrome shows a NATIVE auth dialog. That dialog
// lives outside the page's DOM and CANNOT be driven via browser_eval / DOM
// manipulation. Without this vault, the user has to manually type the same
// email + password every single time. Users have called this "heinous."
//
// Puppeteer exposes `page.authenticate({username, password})`, which
// intercepts basic-auth challenges BEFORE the dialog shows and supplies
// stored creds programmatically. This module owns the storage + lookup
// half of that flow.
//
// Storage
// ───────
// Two parts:
// 1. The PASSWORD lives in the OS keychain via `keytar` (Windows DPAPI /
// macOS Keychain / Linux libsecret). Service name `adom-desktop-pup`.
// Account name is the host pattern. The OS encrypts at rest using the
// logged-in-user's session key, so even disk theft can't extract creds.
// 2. The INDEX (host pattern + username + timestamps, NEVER passwords)
// lives at `plugins/puppeteer/credentials.index.json` so we can list
// entries fast without poking keytar 50 times.
//
// Host matching
// ─────────────
// Patterns support a simple glob:
// "*.componentsearchengine.com" → matches any subdomain (suffix match)
// "componentsearchengine.com" → exact match only
// "secure.example.com" → exact match only
// Best-match wins (longest non-glob suffix beats shorter glob).
// For a navigation to https://nxp.componentsearchengine.com/preview.php,
// host = "nxp.componentsearchengine.com" — would match "*.componentsearchengine.com".
//
// What this module does NOT do
// ────────────────────────────
// - Ship its own crypto. Never. OS keychain only.
// - Echo passwords back via list/get APIs. Index returns username + host only.
// - Persist anything to the wiki / cloud. All local, all per-machine.
const fs = require('fs');
const path = require('path');
const KEYTAR_SERVICE = 'adom-desktop-pup';
const INDEX_PATH = path.join(__dirname, 'credentials.index.json');
let keytar = null;
let keytarLoadError = null;
try {
// keytar is a native module; load lazily so the bridge starts even if it's
// missing. The first credential_set call will surface a clear error.
keytar = require('keytar');
} catch (e) {
keytarLoadError = e.message;
console.log(`[credential_vault] keytar load failed (will be required for credential_set): ${e.message}`);
}
// ── Index helpers ──────────────────────────────────────────────────────
function readIndex() {
try {
const raw = fs.readFileSync(INDEX_PATH, 'utf8');
const parsed = JSON.parse(raw);
if (Array.isArray(parsed.entries)) return parsed.entries;
} catch {}
return [];
}
function writeIndex(entries) {
const data = { version: 1, entries };
fs.writeFileSync(INDEX_PATH, JSON.stringify(data, null, 2));
}
// ── Glob matching ──────────────────────────────────────────────────────
function hostMatchesPattern(host, pattern) {
if (!host || !pattern) return false;
const h = host.toLowerCase();
const p = pattern.toLowerCase();
if (p === h) return true;
if (p.startsWith('*.')) {
const suffix = p.slice(1); // ".componentsearchengine.com"
if (h === suffix.slice(1)) return true; // bare apex is a valid match
if (h.endsWith(suffix)) return true;
}
return false;
}
// "Best match" = longest non-glob suffix wins. Exact match beats glob match
// of the same length. So "nxp.componentsearchengine.com" entry would win
// over "*.componentsearchengine.com" entry for that exact host.
function scorePattern(pattern, host) {
if (!hostMatchesPattern(host, pattern)) return -1;
const exact = !pattern.startsWith('*.');
return (exact ? 1000 : 0) + pattern.length;
}
// ── Public API ─────────────────────────────────────────────────────────
async function setCredential({ host, username, password }) {
if (!host || !username || !password) {
throw new Error('host, username, and password are all required');
}
if (!keytar) {
throw new Error(
`keytar is not installed in plugins/puppeteer/. Install: cd plugins/puppeteer && npm install keytar. Original load error: ${keytarLoadError}`
);
}
await keytar.setPassword(KEYTAR_SERVICE, host, password);
const entries = readIndex();
const idx = entries.findIndex(e => e.host === host);
const now = new Date().toISOString();
if (idx >= 0) {
entries[idx] = { ...entries[idx], username, updatedAt: now };
} else {
entries.push({ host, username, addedAt: now, updatedAt: now });
}
writeIndex(entries);
return { ok: true, host, username };
}
async function getCredentialForUrl(url) {
if (!keytar) return null;
let host;
try {
host = new URL(url).hostname;
} catch {
return null;
}
const entries = readIndex();
let best = null;
let bestScore = -1;
for (const e of entries) {
const s = scorePattern(e.host, host);
if (s > bestScore) {
bestScore = s;
best = e;
}
}
if (!best) return null;
const password = await keytar.getPassword(KEYTAR_SERVICE, best.host);
if (!password) return null;
return { host: best.host, username: best.username, password };
}
function listCredentials() {
// Index already excludes passwords by construction
return readIndex().map(e => ({
host: e.host,
username: e.username,
addedAt: e.addedAt,
updatedAt: e.updatedAt,
}));
}
async function deleteCredential(host) {
if (!host) throw new Error('host required');
let removed = false;
if (keytar) {
try {
removed = await keytar.deletePassword(KEYTAR_SERVICE, host);
} catch (e) {
console.log(`[credential_vault] keytar deletePassword error: ${e.message}`);
}
}
const entries = readIndex();
const before = entries.length;
const next = entries.filter(e => e.host !== host);
if (next.length !== before) {
writeIndex(next);
return { ok: true, host, removedFromKeychain: removed, removedFromIndex: true };
}
return { ok: true, host, removedFromKeychain: removed, removedFromIndex: false };
}
// Apply stored creds to a page given the target URL it's about to load.
// Idempotent: if no match, no-op. Returns the matched host pattern (or null).
async function applyCredentialsToPage(page, url) {
const creds = await getCredentialForUrl(url);
if (!creds) return null;
try {
await page.authenticate({ username: creds.username, password: creds.password });
return creds.host;
} catch (e) {
console.log(`[credential_vault] page.authenticate failed for ${creds.host}: ${e.message}`);
return null;
}
}
module.exports = {
setCredential,
getCredentialForUrl,
listCredentials,
deleteCredential,
applyCredentialsToPage,
// Exposed for tests + diagnostics
hostMatchesPattern,
scorePattern,
};