← Commit history
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.1.42\ No newline at end of file+0.1.43\ No newline at end of file
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" }