← Commit history

browser_import_scan: read-only discovery verb for the comprehensive credential import — enumerates every Chromium profile (Chrome/Edge/Brave) with source-account identity tag, saved-password count, and ABE status; foundation for the Settings import picker. Actual decryption is AD-core (adom/adom-desktop #51).

John Lauer ·b54adcb2b6 ·3mo ago ·parent ec3148e
4 files changed +98−8
adom-bridge-puppeteer-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "1.9.321",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.321/adom-bridge-puppeteer-v1.9.321.zip",-  "sha256": "d94a45f5e1962d106fa60c70425605172bafd931d23c302be2b6cce325febd66",-  "size": 613928,+  "version": "1.9.322",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.322/adom-bridge-puppeteer-v1.9.322.zip",+  "sha256": "490fcd9d57963f569aeb498f3d8003674207824e981cd62519417401b652296a",+  "size": 616629,   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.321+1.9.322
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "1.9.321",+  "version": "1.9.322",   "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. browser_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. browser_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, browser_alert_window posts a native toast whose click focuses the window, browser_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.",   "homepage": "https://wiki.adom.inc/adom/adom-desktop",   "author": "Adom Inc.",
src/server.js+92−2
@@ -3698,7 +3698,7 @@ function clearSessionBusy(sessionId) { // the bar for them would make every window look permanently busy (the audit loops alone poll these). const PASSIVE_VERBS = new Set([   'browser_list_windows', 'browser_list_tabs', 'browser_readiness', 'browser_status', 'browser_maximize', 'browser_hover', 'browser_deps', 'browser_dashboard',-  'browser_describe', 'browser_configure', 'browser_prewarm', 'browser_rescan', 'browser_wait',+  'browser_describe', 'browser_configure', 'browser_prewarm', 'browser_rescan', 'browser_wait', 'browser_import_scan', ]);  // v1.9.58 — THUMBNAIL TOOLTIP (AD >= 1.9.152: desktop_taskbar { thumbnailTooltip }).@@ -8261,6 +8261,7 @@ const VERB_META = {   desktop_record_stop: { hint: 'Stops the desktop recording and returns the file.', related: ['desktop_record_start', 'desktop_record_list'], pitfalls: [] },   browser_readiness: { hint: 'Cold-start readiness probe: {ready, chromeForTestingInstalled, installing, installProgressPct}. On a fresh PC, poll this until ready:true before browser_open_window — the bridge auto-installs Chrome for Testing on its own.', related: ['browser_prewarm', 'browser_open_window'], pitfalls: ['ready:false with installing:true just means Chrome for Testing is still downloading — wait + poll, do NOT treat it as an error', 'installPhase:"failed" → surface lastError to the user (usually network/proxy blocking the download, or low disk)'] },   browser_prewarm: { hint: 'Installs Chrome for Testing (the browser pup drives) WITHOUT opening a window — the warm-before-first-use path. Returns fast; poll browser_readiness for completion.', related: ['browser_readiness', 'browser_open_window'], pitfalls: ['the ~150 MB download needs network access to storage.googleapis.com', 'does not open a window — call browser_open_window once readiness reports ready:true'] },+  browser_import_scan: { hint: 'READ-ONLY discovery for the comprehensive credential import (adom-you seed): enumerates every profile of every Chromium browser (Chrome/Edge/Brave) with its source account email(s), saved-password count, and ABE status. Decrypts NOTHING — actual decryption needs the AD-core elevated SYSTEM helper (adom/adom-desktop #51).', related: ['browser_open_window'], pitfalls: ['Windows-only', 'passwordCount is null if python is not on PATH', 'ABE (v20) passwords cannot be decrypted by this user process — that is the AD SYSTEM helper’s job'] },   browser_describe: { hint: 'This list — the bridge\'s verbs + schemas + hints, for AD\'s Verbs tab. Read it once to learn the surface.', related: ['browser_status'], pitfalls: [] }, }; @@ -8539,7 +8540,7 @@ function maybeAttachBugInvite(obj, verb) {     if (!verb) return;     const PROBE_VERBS = new Set([       'browser_readiness', 'browser_status', 'browser_describe', 'browser_prewarm',-      'browser_list_windows', 'browser_list_tabs', 'browser_rescan', 'browser_configure',+      'browser_list_windows', 'browser_list_tabs', 'browser_rescan', 'browser_configure', 'browser_import_scan',     ]);     const isProbe = PROBE_VERBS.has(verb);     let mode = 'on';@@ -12765,6 +12766,95 @@ const server = http.createServer(async (req, res) => {           });           return;         }+        case 'browser_import_scan': {+          // READ-ONLY discovery pass for the comprehensive credential import (adom-you seed).+          // Enumerates EVERY profile of EVERY Chromium browser (Chrome/Edge/Brave) on this box+          // with: source account email(s) (the identity tag — Preferences.account_info[].email),+          // saved-password count (parsed from the Login Data SQLite via the desktop's python,+          // NEVER a byte-scan — see dev-skills/pup-browser-import), Login Data size, and ABE+          // status. It DECRYPTS NOTHING — decryption needs the AD-core elevated SYSTEM helper+          // (adom/adom-desktop #51, desktop_decrypt_browser_credentials). This verb is the+          // foundation the first-run prompt + Settings "Import logins" picker read.+          if (process.platform !== 'win32') {+            sendJSON(res, { success: true, output: JSON.stringify({ profiles: [] }), profiles: [], _hint: 'Credential import discovery is Windows-only (Chromium ABE / DPAPI). No profiles enumerated on this platform.' });+            return;+          }+          const scanPs = [+            "$ErrorActionPreference='SilentlyContinue'",+            "$local = $env:LOCALAPPDATA",+            "$browsers = @(",+            "  @{ id='chrome'; name='Google Chrome'; root=(Join-Path $local 'Google\\Chrome\\User Data') },",+            "  @{ id='edge';   name='Microsoft Edge'; root=(Join-Path $local 'Microsoft\\Edge\\User Data') },",+            "  @{ id='brave';  name='Brave';          root=(Join-Path $local 'BraveSoftware\\Brave-Browser\\User Data') }",+            ")",+            "$py = $null",+            "foreach($c in @('python','python3','py')){ $g = (Get-Command $c -ErrorAction SilentlyContinue); if($g){ $py=$g.Source; break } }",+            "$pyLines = @(",+            "  'import sqlite3,sys',",+            "  'c=sqlite3.connect(sys.argv[1])',",+            "  'try:',",+            "  '    n=c.execute(\"select count(*) from logins where blank_password=0\").fetchone()[0]',",+            "  'except Exception:',",+            "  '    n=c.execute(\"select count(*) from logins\").fetchone()[0]',",+            "  'print(n)'",+            ") -join [char]10",+            "$codeFile = $null",+            "if($py){ $codeFile = Join-Path $env:TEMP ('pup_ldcount_' + [guid]::NewGuid().ToString('N') + '.py'); Set-Content -LiteralPath $codeFile -Value $pyLines -Encoding ASCII }",+            "$out = @()",+            "foreach($b in $browsers){",+            "  if(-not (Test-Path $b.root)){ continue }",+            "  $abe=$false; $legacy=$false",+            "  $lsPath = Join-Path $b.root 'Local State'",+            "  if(Test-Path $lsPath){ try { $ls = Get-Content -Raw -LiteralPath $lsPath | ConvertFrom-Json; if($ls.os_crypt.app_bound_encrypted_key){ $abe=$true }; if($ls.os_crypt.encrypted_key){ $legacy=$true } } catch {} }",+            "  $profs = @()",+            "  $d = Get-Item -LiteralPath (Join-Path $b.root 'Default') -ErrorAction SilentlyContinue; if($d){ $profs += $d }",+            "  $profs += (Get-ChildItem -LiteralPath $b.root -Directory -Filter 'Profile *' -ErrorAction SilentlyContinue)",+            "  foreach($pf in $profs){",+            "    if(-not $pf){ continue }",+            "    $prefPath = Join-Path $pf.FullName 'Preferences'",+            "    if(-not (Test-Path $prefPath)){ continue }",+            "    $emails=@(); $pname=$pf.Name",+            "    try { $pref = Get-Content -Raw -LiteralPath $prefPath | ConvertFrom-Json; if($pref.profile.name){ $pname = $pref.profile.name }; if($pref.account_info){ foreach($a in $pref.account_info){ if($a.email){ $emails += $a.email } } } } catch {}",+            "    $ldPath = Join-Path $pf.FullName 'Login Data'",+            "    $pwCount=$null; $ldBytes=0",+            "    if(Test-Path $ldPath){",+            "      $ldBytes = (Get-Item -LiteralPath $ldPath).Length",+            "      if($py -and $codeFile){",+            "        $tmp = Join-Path $env:TEMP ('pup_ld_' + [guid]::NewGuid().ToString('N') + '.db')",+            "        try { Copy-Item -LiteralPath $ldPath -Destination $tmp -Force; $r = & $py $codeFile $tmp 2>$null; if($r){ $pwCount = [int]($r | Select-Object -First 1) } } catch {}",+            "        Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue",+            "      }",+            "    }",+            "    $out += [pscustomobject]@{ browser=$b.id; browserName=$b.name; profileDir=$pf.Name; profileName=$pname; accounts=@($emails); passwordCount=$pwCount; loginDataBytes=$ldBytes; abe=$abe; legacyKey=$legacy }",+            "  }",+            "}",+            "if($codeFile){ Remove-Item -LiteralPath $codeFile -Force -ErrorAction SilentlyContinue }",+            "[pscustomobject]@{ pythonAvailable=[bool]$py; profiles=@($out) } | ConvertTo-Json -Depth 6 -Compress"+          ].join('\n');+          const scanPath = path.join(require('os').tmpdir(), '_pup_import_scan.ps1');+          let scan = { pythonAvailable: false, profiles: [] };+          try {+            fs.writeFileSync(scanPath, scanPs);+            const raw = (await runPsHiddenPromise(scanPath, { timeout: 25000 }) || '').trim();+            if (raw) scan = JSON.parse(raw);+          } catch (e) { console.log('[import-scan] failed:', e && e.message); }+          const profiles = Array.isArray(scan.profiles) ? scan.profiles : (scan.profiles ? [scan.profiles] : []);+          const totalPw = profiles.reduce((s, p) => s + (p.passwordCount || 0), 0);+          const accts = [...new Set(profiles.flatMap(p => p.accounts || []))];+          const anyAbe = profiles.some(p => p.abe);+          sendJSON(res, {+            success: true,+            output: JSON.stringify({ profiles, totalPasswords: totalPw, accounts: accts }),+            profiles,+            totalPasswords: totalPw,+            accounts: accts,+            pythonAvailable: !!scan.pythonAvailable,+            _hint: profiles.length+              ? `Found ${profiles.length} browser profile(s) across ${new Set(profiles.map(p=>p.browser)).size} browser(s), ~${totalPw} saved password(s), identities: ${accts.join(', ') || 'none tagged'}.${anyAbe ? ' These are App-Bound-Encrypted (v20) — a USER process (this bridge) canNOT decrypt them.' : ''} This verb is DISCOVERY ONLY. Actual decryption needs the AD-core elevated SYSTEM helper (adom/adom-desktop #51, desktop_decrypt_browser_credentials): one UAC prompt → unwraps each browser's ABE master key → decrypts every profile's v20 passwords → returns identity-tagged rows, which pup then merges into the adom-you vault (credential_vault.js). ${scan.pythonAvailable ? '' : 'NOTE: python not found on PATH, so passwordCount is null — only file presence/size known. '}Next: the Settings "Import logins from Chrome/Edge" picker reads this list; the import button invokes the AD helper once #51 lands.`+              : `No Chromium browser profiles with a Preferences file found under %LOCALAPPDATA% (Chrome/Edge/Brave). Nothing to import.`,+          });+          return;+        }         case 'browser_prewarm': {           // Install Chrome for Testing WITHOUT opening a window — the warm-before-           // first-use path. Default non-blocking so AD/HD can fire-and-forget on