← Commit history
Update 5 file(s)
5 files changed
+71−19
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.1.42+0.1.43
bridge/bridge.json+2−2@@ -2,9 +2,9 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Browser Extension",- "version": "0.1.42",+ "version": "0.1.43", "expectedExtensionVersion": "0.1.3",- "description": "Drives the user's REAL signed-in Chrome/Edge via an installed extension (native-messaging transport). Mirrors the puppeteer bridge's verb surface as nbrowser_* but runs inside the actual logged-in profile, so cookies/SSO/saved-logins/captcha human-trust all work. The bridge binds an ephemeral loopback port (PORTS.md compliant) and writes a discovery file; the native-messaging host pumps stdio <-> this bridge; the extension never touches a TCP port.",+ "description": "Drives the user's REAL signed-in Chrome/Edge. MOST verbs require the Adom extension installed in that browser profile (CDP driving: navigate/click/read/screenshot). TWO verbs need NO extension at all and work on any machine: nbrowser_open_os_window (open a URL in the user's real profile, background+flashing - the OAuth/consent path) and nbrowser_os_windows (enumerate browser windows). Also extension-free: nbrowser_profiles, nbrowser_wake_profile, nbrowser_force_close. The bridge binds an ephemeral loopback port and the native-messaging host pumps stdio <-> bridge.", "homepage": "https://wiki.adom.inc/adom/adom-browser-extension", "author": "Adom Inc.", "license": "MIT",
bridge/package.json+1−1@@ -1,6 +1,6 @@ { "name": "adom-native-browser-bridge",- "version": "0.1.42",+ "version": "0.1.43", "private": true, "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)", "main": "server.js",
bridge/server.js+63−11@@ -325,7 +325,8 @@ function discoverAdBase(fileOnly) { } catch {} return 'http://127.0.0.1:47200'; }-function adRequest(base, payload) {+let adTokenRejected = false; // AD rotates the bridge token on self-update; a stale one 403s every call+function adRequest(base, payload, noToken) { return new Promise((resolve) => { let u; try { u = new URL('/command', base); } catch { u = new URL('http://127.0.0.1:47200/command'); } const data = Buffer.from(JSON.stringify(payload));@@ -333,7 +334,7 @@ function adRequest(base, payload) { // env; sending it badges our outbound calls as "bridge" in the Activity Log. It is NOT an approval // gate (we're trusted-by-install; AD binds 127.0.0.1 only). Omitting it is fine; a stale token 403s. const headers = { 'content-type': 'application/json', 'content-length': data.length };- if (process.env.ADOM_BRIDGE_TOKEN) headers['x-adom-bridge-token'] = process.env.ADOM_BRIDGE_TOKEN;+ if (process.env.ADOM_BRIDGE_TOKEN && !noToken && !adTokenRejected) headers['x-adom-bridge-token'] = process.env.ADOM_BRIDGE_TOKEN; const req = http.request({ host: u.hostname, port: u.port || 80, path: u.pathname, method: 'POST', headers, timeout: 20000 }, (res) => { let b = ''; res.on('data', (c) => (b += c)); res.on('end', () => { let j; try { j = JSON.parse(b); } catch { j = b; } resolve({ status: res.statusCode, body: j }); }); });@@ -351,16 +352,36 @@ function adCommand(app, command, args, target) { // AD via the relay - so a bridge running on a VM reaches the user on their LAPTOP. Omitted = local AD. if (target) payload.target = target; const first = discoverAdBase();- return adRequest(first, payload).then((r) => {- if (r.status !== 0) {+ const ok2xx = (r) => r && r.status >= 200 && r.status < 300;+ return adRequest(first, payload).then(async (r) => {+ // A STALE BRIDGE TOKEN 403s EVERY CALL (AD rotates it when it self-updates). The token is+ // ATTRIBUTION-ONLY, so the correct recovery is to DROP it and retry - otherwise every AD-backed+ // feature (window correlation, taskbar badge, flash) dies SILENTLY. Real incident 2026-07-21: the+ // fusion-bridge maintainer saw nbrowser_os_windows report 0 windows while AD saw 21.+ if (r.status === 401 || r.status === 403) {+ const retry = await adRequest(first, payload, true);+ if (ok2xx(retry)) {+ if (!adTokenRejected) { adTokenRejected = true; console.error('[native-browser] AD rejected our bridge token (403) - it is attribution-only, so dropping it and continuing. AD likely self-updated and rotated it.'); logActivity({ verb: '_ad_link', ok: true, errorCode: 'token_dropped', internal: true }); }+ adDownSince = 0;+ return retry;+ }+ r = retry;+ }+ if (ok2xx(r)) { if (adDownSince) { console.error(`[native-browser] AD link RECOVERED after ${Math.round((Date.now() - adDownSince) / 1000)}s`); logActivity({ verb: '_ad_link', ok: true, internal: true }); adDownSince = 0; } return r; }+ // Any other non-2xx is a REAL failure - never treat it as success (that is what silently produced+ // empty window lists). Log it so a missing badge/flash is diagnosable from nbrowser_activity.+ if (r.status !== 0) {+ if (!adDownSince) { adDownSince = Date.now(); console.error(`[native-browser] AD returned HTTP ${r.status} for ${command} - AD-backed features (badge/flash/window correlation) are degraded`); logActivity({ verb: '_ad_link', ok: false, errorCode: `http_${r.status}`, internal: true }); }+ return r;+ } // Connection-level failure -> AD may have restarted on a new port. Re-discover from the FILE and retry. const fromFile = discoverAdBase(true); const retry = fromFile && fromFile !== first ? adRequest(fromFile, payload) : Promise.resolve(r); return retry.then((r2) => {- if (r2.status !== 0) {+ if (ok2xx(r2)) { adBaseOverride = fromFile; console.error(`[native-browser] AD port rotated (${first} dead) - rediscovered ${fromFile} from file, link restored`); logActivity({ verb: '_ad_link', ok: true, internal: true });@@ -513,7 +534,19 @@ const b64 = (s) => Buffer.from(s, 'utf8').toString('base64'); // badge, painted on the real browser window by hwnd. (Replaces the old run_script FlashWindowEx hack.) function taskbar(hwnd, opts) { if (!hwnd) return Promise.resolve();- return adCommand('desktop', 'desktop_taskbar', { hwnd, ...opts }).catch(() => {});+ return adCommand('desktop', 'desktop_taskbar', { hwnd, ...opts }).then((r) => {+ // LOG every overlay/flash (John, 2026-07-21): "log when you place the adom icon and when you flash+ // the taskbar so we can debug you better." A silently-failing badge is a broken TRUST signal, so it+ // must be visible in nbrowser_activity, with the HTTP status when AD refuses.+ const ops = [];+ const o = opts && opts.overlay;+ if (o) ops.push(o.icon ? 'overlay:composite' : `overlay:${o.badge || '?'}`);+ if (opts && opts.flash) ops.push(`flash:${opts.flash}`);+ if (opts && opts.progress) ops.push(`progress:${(opts.progress.state) || '?'}`);+ const ok = !!(r && r.status >= 200 && r.status < 300);+ logActivity({ verb: '_taskbar', hwnd, ops: ops.join('+') || 'none', ok, errorCode: ok ? null : `http_${(r && r.status) || 0}`, internal: true });+ return r;+ }).catch(() => {}); } function flashHwnd(hwnd, stop) { taskbar(hwnd, { flash: stop ? 'stop' : 'until_focused' }); } @@ -531,6 +564,7 @@ function armBadgeIdle(sid) { // onto that profile's AVATAR instead of evicting it (the taskbar overlay has one slot = the avatar's slot). // AD can't derive this itself: Chrome/Edge run ONE browser process for all profiles, so hwnd→PID→cmdline // has no --profile-directory. We read the browser's Local State (profile.info_cache: dir → {user_name}).+const ADOM_EXT_ID_PREFIX = 'nncjgceobm'; // our pinned (keyed) extension id - stable across packed/unpacked const _avatarCache = new Map(); // profileKey -> {userDataDir, profileDir} | null function avatarForProfile(profileKey) { if (!profileKey) return null;@@ -1575,7 +1609,15 @@ function dispatch(verb, args) { : ['C:/Program Files/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe', local ? path.join(local, 'Google', 'Chrome', 'Application', 'chrome.exe') : '']; const exe = exes.find((e) => e && fs.existsSync(e)); if (!exe) return resolve({ success: false, error: `${kp.browser} executable not found at a standard path` });- const extSt = adomExtStateForProfile(kp.browser, kp.profileDir);+ // AUTHORITATIVE ORDER: a LIVE native-messaging connection for this profile proves the extension is+ // installed AND running - that is what nbrowser_readiness reports. The on-disk inventory is only a+ // FALLBACK for profiles that are not connected (it cannot see a running-but-unlisted extension, and+ // it under-reports load-unpacked installs). Reporting them inconsistently made the upsell hint tell+ // users WITH the extension to install it (fusion-bridge report, 2026-07-21).+ const liveConn = (() => { for (const [k, c] of conns) { if (k === kp.key && c.sock && !c.sock.destroyed) return c; } return null; })();+ const extSt = liveConn+ ? { checked: true, installed: true, enabled: true, live: true, version: (liveConn.profile && liveConn.profile.version) || null }+ : adomExtStateForProfile(kp.browser, kp.profileDir); let hwnd = null, minimized = false, flashed = false; if (a.foreground !== true) { // OPEN like Hydrogen Desktop's browser picker (control.rs /open-in-profile), which opens native@@ -1635,10 +1677,15 @@ function dispatch(verb, args) { const upsell = extSt && extSt.installed ? `NOTE: the Adom extension IS installed in this profile${extSt.enabled ? '' : ' (but DISABLED)'} - you could have FULL control instead: nbrowser_wake_profile then nbrowser_open_window gives you navigation, reading, clicking, screenshots, and ownership guards. Prefer that path.` : `UPSELL (tell the user when the moment is right): this window is OPEN but I cannot see or drive it - the Adom extension is not installed in this profile. With ABE installed (see installing-the-extension - it can be installed FOR the user in ~a minute) the assistant could fill the form, read the result, screenshot progress, and clean up after itself. Until then the USER must do the clicking.`;- resolve({ success: true, output: JSON.stringify({ ok: !!hwnd, hwnd, minimized, flashed, profile: kp.key, displayName: kp.displayName, extensionInstalled: !!(extSt && extSt.installed), url,+ // STABLE SUCCESS CONTRACT (fusion-bridge report, 2026-07-21): a fired launch IS success for a+ // consent flow - "the page is now in front of the user". hwnd resolution is a SEPARATE fact.+ // Callers must never have to regex _hint. ok/launched = did the browser launch; hwndResolved =+ // can we address the window afterwards (minimize/close/foreground).+ const hwndReason = hwnd ? null : 'grouped_into_existing_window';+ resolve({ success: true, output: JSON.stringify({ ok: true, launched: true, hwndResolved: !!hwnd, hwndReason, hwnd, minimized, flashed, profile: kp.key, displayName: kp.displayName, extensionInstalled: !!(extSt && extSt.installed), extensionLive: !!(extSt && extSt.live), url, _hint: hwnd ? `Opened ${url} in the user's REAL ${kp.browser} profile '${kp.displayName}' (${kp.profileDir}) WITHOUT the extension. ${a.foreground === true ? 'Foregrounded per your foregroundReason.' : `It opened MINIMIZED (background) with an ORANGE flashing taskbar button - the user clicks it when ready (perfect for OAuth consent: they are already signed in; they just approve). TO FOREGROUND IT (only when the user asks to see it, e.g. "bring it up" / a step that needs their eyes): desktop_bring_to_front {hwnd:${hwnd}} - that restores + raises this exact window (the flashing button already points them at it, so you rarely need to). Do NOT foreground for your own convenience - it steals their focus.`} You CANNOT read/drive this window (no extension in this profile) - poll the OUTCOME out-of-band (e.g. the OAuth CLI's token poll, or ask the user). Close it afterwards via nbrowser_force_close {hwnd:${hwnd}} (two-step attestation). ${upsell}`- : `Launch fired but the new window's hwnd was not found within 8s - it may still have opened (grouped into an existing ${kp.browser} window). Check nbrowser_os_windows. ${upsell}` }) });+ : `LAUNCH SUCCEEDED (ok:true, launched:true) but hwndResolved:false - ${kp.browser} almost certainly GROUPED the page into an existing window instead of creating a new one, so there is no new hwnd to address. The page IS in front of the user, which is success for a consent flow. Consequence: no minimize/flash/close by hwnd for this one - find it with nbrowser_os_windows if you need to address it. Do NOT treat this as a failure. ${upsell}` }) }); })(); return; }@@ -2230,8 +2277,13 @@ function adomExtStateForProfile(browser, profileDir) { const settings = (j.extensions && j.extensions.settings) || {}; for (const [id, ent] of Object.entries(settings)) { const nm = String((ent && ent.manifest && ent.manifest.name) || '');- if (!/adom/i.test(nm)) continue;- return { checked: true, installed: true, id, name: nm, version: (ent.manifest && ent.manifest.version) || null, enabled: ent.state === 1, unpackedPath: ent.path || null };+ const pth = String((ent && ent.path) || '');+ // A LOAD-UNPACKED extension often has an EMPTY manifest.name in Secure Preferences, so matching+ // on the name alone reported installed:false for a profile that HAD it (real bug, 2026-07-21).+ // Match on our pinned extension ID or the source path too.+ const hit = /adom/i.test(nm) || id.startsWith(ADOM_EXT_ID_PREFIX) || /adom-browser-extension|[\\/]adom[\\/]/i.test(pth);+ if (!hit) continue;+ return { checked: true, installed: true, id, name: nm || 'Adom (unpacked)', version: (ent.manifest && ent.manifest.version) || null, enabled: ent.state !== 0, unpackedPath: ent.path || null, unpacked: !nm }; } } return { checked: true, installed: false };
native-browser-bridge-manifest.json+4−4@@ -1,8 +1,8 @@ { "manifest_version": 1,- "version": "0.1.42",- "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.42/native-browser-bridge-v0.1.42.zip",- "sha256": "07a0f05540f622a3364705ff8efe50e24f865df37534e9d82a2d1d6483ae9d8d",- "size": 86469,+ "version": "0.1.43",+ "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.43/native-browser-bridge-v0.1.43.zip",+ "sha256": "f477d573c7d5f61bbdd259ca6b660ba55d40edc2cf7dedd012744cf23e7236cc",+ "size": 88203, "released_at": "2026-07-21T00:00:00Z" }