← Commit history
0.3.1: fix composite handlers referencing an out-of-scope args alias (30s hang instead of an error) + guarded() so a throw never hangs a caller again
5 files changed
+31−21
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.3.0+0.3.1
bridge/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Native Browser",- "version": "0.3.0",+ "version": "0.3.1", "expectedExtensionVersion": "0.2.0", "description": "Drives the user's REAL signed-in Chrome/Edge, in two layers. Layer 1 (nb) needs NOTHING installed and works on any machine: nbrowser_open_os_window (open a URL in the user's real profile, background+flashing, the OAuth/consent path), nbrowser_os_windows, nbrowser_profiles, nbrowser_wake_profile, nbrowser_force_close. Layer 2 (nbe, the Adom Native Browser Extension) unlocks the rest: CDP driving (navigate/click/read/screenshot) inside their logged-in profile. The bridge binds an ephemeral loopback port and the native-messaging host pumps stdio <-> bridge.", "homepage": "https://wiki.adom.inc/adom/nb-bridge",
bridge/package.json+1−1@@ -1,6 +1,6 @@ { "name": "adom-native-browser-bridge",- "version": "0.3.0",+ "version": "0.3.1", "private": true, "description": "Native-browser bridge for Bridge (ab): layer 1 (nb) plus layer 2 via nbe, the Adom Native Browser Extension (Chrome + Edge).", "main": "server.js",
bridge/server.js+23−13@@ -1350,6 +1350,13 @@ function decorateHint(verb, r) { // Unwrap a dispatch() result into its parsed payload - so composite verbs (state/demo) can reuse the // real verb paths instead of duplicating their logic (and inheriting their guards for free).+// Wrap an async composite so a throw becomes a fast, readable failure instead of a 30s hang+// (a rejected IIFE never calls resolve - the caller just waits out the timeout with no clue).+function guarded(resolve, verb, fn) {+ Promise.resolve().then(fn).catch((e) => {+ try { resolve({ success: false, errorCode: 'internal_error', error: `${verb} threw: ${(e && e.message) || e}`, _hint: 'This is a bridge bug, not your call. Report it on adom/nb-bridge with the verb and args; a throw inside a composite used to hang the caller for the full timeout instead of failing fast.' }); } catch {}+ });+} async function callSelf(verb, args) { try { const r = await dispatch(verb, args || {}); if (r && typeof r.output === 'string') { try { return JSON.parse(r.output); } catch { return { ok: r.success !== false, raw: r.output }; } } return r || {}; } catch (e) { return { ok: false, error: String((e && e.message) || e) }; }@@ -1841,8 +1848,9 @@ function dispatch(verb, args) { // exact hwnd + tabId + live url/title per session, and optionally the page bitmap inline (base64), // so a WEB page or an AI can render it without a second round-trip or any window hunting. if (verb === 'nbrowser_state') {- (async () => {- const want = a && a.sessionId;+ const sa = args || {};+ guarded(resolve, 'nbrowser_state', async () => {+ const want = sa.sessionId; const wins = await callSelf('nbrowser_list_windows', want ? { sessionId: want } : {}); const rows = (wins.windows || wins.sessions || []).filter((w) => !want || w.sessionId === want); const sessions = rows.map((w) => {@@ -1853,7 +1861,7 @@ function dispatch(verb, args) { purpose: sessionPurpose.get(sid) || null, osOpened: osOpened.has(sid) }; }); let shot = null;- if (a && a.screenshot && sessions.length) {+ if (sa.screenshot && sessions.length) { const s = await callSelf('nbrowser_screenshot', { sessionId: want || sessions[0].sessionId }); if (s && s.base64) shot = { sessionId: want || sessions[0].sessionId, base64: s.base64, width: s.width, height: s.height }; }@@ -1861,22 +1869,23 @@ function dispatch(verb, args) { _hint: sessions.length ? `The EXACT windows this bridge is driving - hwnd is authoritative, so never match a browser window by TITLE (a title is the active tab and changes while loading; several windows can share a substring - that is what broke the web-control demo, issue #22). Pass screenshot:true to get the page bitmap inline (base64) in the same call. Act on a window with its hwnd (ab desktop_screenshot_window {hwnd} for the frame + native popups) or its sessionId (nbrowser_* for the page).` : 'This bridge is driving NO windows right now. Open one with nbrowser_open_window {sessionId, thread, purpose, profile, url} - it returns the hwnd + tabId of exactly that window.' }) });- })();+ }); return; } // nbrowser_demo - one call that showcases layer 2: open a representative page in the user's REAL // signed-in browser and hand back the exact window + an inline screenshot. Built for the ab // web-control demo (issue #22) so a page can show "drive my real browser" with zero title-guessing. if (verb === 'nbrowser_demo') {- (async () => {+ const da = args || {};+ guarded(resolve, 'nbrowser_demo', async () => { const steps = [];- const url = (a && a.url) || 'https://wiki.adom.inc/adom/nb-bridge';- const sid = (a && a.sessionId) || 'nbdemo';- let profile = a && a.profile;+ const url = da.url || 'https://wiki.adom.inc/adom/nb-bridge';+ const sid = da.sessionId || 'nbdemo';+ let profile = da.profile; if (!profile) { const ps = await callSelf('nbrowser_profiles'); const live = (ps.profiles || []).find((p) => p.live && p.email) || (ps.profiles || []).find((p) => p.email); profile = live && live.profile; steps.push({ step: 'resolve profile', ok: !!profile, profile }); } if (!profile) return resolve({ success: false, error: 'no drivable profile found', _hint: 'Run nbrowser_readiness: if state is needs-extension, offer the nbe install (installing-the-extension).', steps }); await callSelf('nbrowser_close_window', { sessionId: sid }); // idempotent re-run- const open = await callSelf('nbrowser_open_window', { sessionId: sid, thread: (a && a.thread) || 'nbrowser_demo', purpose: 'nb demo: show the AI driving the real signed-in browser', profile, url });+ const open = await callSelf('nbrowser_open_window', { sessionId: sid, thread: da.thread || 'nbrowser_demo', purpose: 'nb demo: show the AI driving the real signed-in browser', profile, url }); steps.push({ step: 'open in real browser (background)', ok: open.ok === true, hwnd: open.hwnd, tabId: open.tabId }); if (open.ok !== true) return resolve({ success: false, error: open.error || 'open failed', steps }); await new Promise((r) => setTimeout(r, 1200));@@ -1886,11 +1895,12 @@ function dispatch(verb, args) { base64: shot.base64 || null, width: shot.width, height: shot.height, steps, _hint: `Opened ${url} in the user's REAL signed-in browser (profile ${profile}) in the BACKGROUND, badged as AI-driven, and captured that EXACT window - no title matching anywhere. Render base64 inline. Clean up with nbrowser_close_window {sessionId:"${sid}"} when the demo is done.`, _next: 'nbrowser_close_window' }) });- })();+ }); return; } if (verb === 'nbrowser_uninstall') {- (async () => {+ const ua = args || {};+ guarded(resolve, 'nbrowser_uninstall', async () => { const removed = [], failed = []; const HOST = 'inc.adom.native_browser'; // ab's registry verbs take {hive, key} - NOT a single joined path (verified against `commands`,@@ -1907,13 +1917,13 @@ function dispatch(verb, args) { } const stillInstalled = knownProfiles().filter((kp) => (adomExtStateForProfile(kp.browser, kp.profileDir) || {}).installed).map((kp) => kp.key); resolve({ success: true, output: JSON.stringify({- ok: true, cascade: !!(a && a.cascade), removed, failed,+ ok: true, cascade: !!ua.cascade, removed, failed, extensionStillInstalled: stillInstalled, _hint: `Cleaned nb's OWN artifacts: the HKCU native-messaging host registration (Chrome + Edge), ~/.adom/bridges/${BRIDGE}, the activity trail, and the icon cache. ab removes the bridge cache dir next.` + (stillInstalled.length ? ` NOT REMOVED (deliberately): the nbe extension is still loaded in ${stillInstalled.join(', ')} - it lives in the USER'S browser profile and is their choice. It will simply stop working (its native host is gone). To remove it properly walk them through the uninstalling-the-extension skill; never rip an extension out from under a user.` : ` No profile currently has the nbe extension installed, so nothing is left in the user's browser.`) }) });- })();+ }); return; } if (verb === 'nbrowser_os_windows') {
native-browser-bridge-manifest.json+5−5@@ -1,8 +1,8 @@ { "manifest_version": 1,- "version": "0.3.0",- "url": "https://wiki.adom.inc/download/adom/nb-bridge/0.3.0/native-browser-bridge-v0.3.0.zip",- "sha256": "94b763d5357a0221a7d9ff8605a5fb0a922dfe6c0772b1aca514973e2fe5bf58",- "size": 105779,- "released_at": "2026-08-18T14:52:21Z"+ "version": "0.3.1",+ "url": "https://wiki.adom.inc/download/adom/nb-bridge/0.3.1/native-browser-bridge-v0.3.1.zip",+ "sha256": "de4a1252d3658cf71460a8bb6333c2a6c18613d588e7f7ee677dc60a4487e444",+ "size": 106040,+ "released_at": "2026-08-19T08:16:46Z" }