← Commit history

New pup_download verb: download a file THROUGH THE BROWSER exactly as the user clicking it would (real session/cookies/anti-bot cookies), with Chrome's own download manager saving to Downloads and completion confirmed via CDP Browser.downloadProgress so the response carries the real saved path + byte count. Pass selector to click the actual download button, or url to force-download via an in-page a[download]. This is the browser-native path that gets past the anti-bot walls a server-side fetch/curl never can. No bridge-side fetch added; pup stays about the browser.

John Lauer ·07c0c239b4 ·2mo ago ·parent e93aa85
4 files changed +80−6
adom-bridge-puppeteer-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "2.0.141",-  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.141/adom-bridge-puppeteer-v2.0.141.zip",-  "sha256": "83dbac9ebf9d93e14060f3fba60b3d0d8cf25640ab7163eeef2041578d08eddd",-  "size": 892474,+  "version": "2.0.142",+  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.142/adom-bridge-puppeteer-v2.0.142.zip",+  "sha256": "b735968c96b93d3e54f43f3a1cd66e672bb5d998825b9ea516c3bdd49f5adf59",+  "size": 894635,   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-2.0.141+2.0.142
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "2.0.141",+  "version": "2.0.142",   "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. pup_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. pup_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, pup_alert_window posts a native toast whose click focuses the window, pup_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-bridge",   "author": "Adom Inc.",
src/server.js+74
@@ -11948,6 +11948,7 @@ const server = http.createServer(async (req, res) => {             V('pup_set_viewport', 'Set the page viewport (CDP device-metrics) deterministically, independent of the OS window size — fixes a too-small innerWidth so fullPage/element shots render at a known width. width+height required.', { sessionId: 'required str', width: 'required int px', height: 'required int px', deviceScaleFactor: 'optional number (default 1)' }, 15, { sessionId: 'work', width: 1280, height: 900 }),             V('pup_evaluate', 'Evaluate JS in the page; returns the result.', { sessionId: 'required str', expression: 'required str' }, 30, { sessionId: 'work', expression: 'document.title' }),             V('pup_fetch_url', 'Fetch a URL\'s content via the browser (cookies/session intact).', { sessionId: 'required str', url: 'required str' }, 45, { sessionId: 'work', url: 'https://api.example.com' }),+            V('pup_download', 'Download a file THROUGH THE BROWSER, exactly as the user clicking it would — real session, real cookies, real anti-bot cookies — and Chrome saves it to the Downloads folder. This is the right verb for vendor files (datasheets, KiCad/Altium libraries, STEP 3D models) that a server-side fetch/curl can never get past the site\'s bot wall. Pass selector to click the actual download button (authentic), or url to force-download via an in-page a[download]. Confirms completion via Chrome\'s download events and returns the ACTUAL saved path + byte count + file magic. For a gated file, log in on the window first (pup_login).', { sessionId: 'optional str', selector: 'optional str — the real download control to click', url: 'optional str — download this URL via an in-page a[download] click', saveAs: 'optional str — filename override', saveDir: 'optional str — default the user\'s Downloads folder' }, 100, { sessionId: 'work', selector: 'button.download' }),             V('pup_input_dispatch', 'Low-level input (mouse/keyboard) at coords — beats some overlays.', { sessionId: 'required str', type: 'required str', x: 'optional int', y: 'optional int' }, 20, { sessionId: 'work', type: 'mousePressed', x: 100, y: 200 }),             V('pup_record_start', 'Start recording the page (video).', { sessionId: 'required str' }, 20, { sessionId: 'work' }),             V('pup_record_stop', 'Stop recording; returns the saved file.', { sessionId: 'required str' }, 30, { sessionId: 'work' }),@@ -15440,6 +15441,79 @@ const server = http.createServer(async (req, res) => {           }           return;         }+        // v2.0.142 (John: "you need to download via the browser just like a user does — pup is about+        // the browser"). A BROWSER-NATIVE download: pup drives Chrome to fetch the file exactly as a+        // user clicking the button would — real session, real cookies, real anti-bot cookies, real+        // Content-Disposition — and Chrome's own download manager writes it to the Downloads folder.+        // NO bridge-side fetch/curl (that trips every vendor's anti-bot wall; if a caller wants curl+        // it does that outside pup). Completion is confirmed via CDP Browser.downloadProgress, so the+        // response carries the ACTUAL saved path + byte count, not a guess.+        //   selector : click THIS real download control (the authentic user action). OR+        //   url      : synthesize an <a href=url download> in the page and real-click it (forces the+        //              download path even for a URL Chrome would otherwise render inline, e.g. a PDF).+        case 'pup_download': {+          if (!args.selector && !args.url) { sendJSON(res, { success: false, error: 'pup_download needs {selector} (a real download button to click) OR {url} (downloaded via an in-page <a download> click). Optional saveAs.' }); return; }+          let session, resolvedId;+          try { ({ session, resolvedId } = await recoverOrRelaunchSession(args)); }+          catch (e) { sendSessionResolveError(res, e); return; }+          let tab, tabId;+          try { ({ tab, tabId } = resolveTabOrError(session, resolvedId, args)); }+          catch (e) { sendTabResolveError(res, e); return; }+          const dir = args.saveDir || (session._downloadPath) || path.join(os.homedir(), 'Downloads');+          let cdp = null;+          try {+            cdp = await tab.page.target().createCDPSession();+            // allow downloads to `dir` AND emit Browser.download* events so we can await completion.+            try { fs.mkdirSync(dir, { recursive: true }); } catch (e) {}+            await cdp.send('Browser.setDownloadBehavior', { behavior: 'allow', downloadPath: dir, eventsEnabled: true });+            let willBegin = null;+            const done = new Promise((resolve, reject) => {+              const to = setTimeout(() => reject(new Error('download did not complete within 90s')), 90000);+              cdp.on('Browser.downloadWillBegin', (e) => { willBegin = e; });+              cdp.on('Browser.downloadProgress', (e) => {+                if (e.state === 'completed') { clearTimeout(to); resolve(e); }+                else if (e.state === 'canceled') { clearTimeout(to); reject(new Error('download was canceled by the page/Chrome')); }+              });+            });+            // Trigger the download the way a user does.+            if (args.selector) {+              await withTimeout(tab.page.click(args.selector, {}), 12000, 'download.click').catch(async () => {+                // fall back to a scroll-into-view + coordinate click if the plain click missed+                const box = await tab.page.$eval(args.selector, el => { el.scrollIntoView({ block: 'center' }); const r = el.getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; }).catch(() => null);+                if (box) await tab.page.mouse.click(box.x, box.y);+              });+            } else {+              await tab.page.evaluate((u, nm) => {+                const a = document.createElement('a'); a.href = u; if (nm) a.download = nm; else a.setAttribute('download', '');+                a.style.display = 'none'; document.body.appendChild(a); a.click(); setTimeout(() => a.remove(), 1000);+              }, args.url, args.saveAs || '');+            }+            const ev = await done;+            // Resolve the real file: allow-mode keeps the suggested name; the guid file is renamed on complete.+            let fname = (willBegin && willBegin.suggestedFilename) || (args.saveAs || '') || ('download-' + (ev.guid || 'file'));+            let abs = path.join(dir, fname);+            // Verify on disk; if the suggested name isn't there yet, find the newest non-.crdownload file.+            let sizeOnDisk = null;+            try { sizeOnDisk = fs.statSync(abs).size; } catch (e) {+              try {+                const recent = fs.readdirSync(dir).filter(f => !f.endsWith('.crdownload'))+                  .map(f => ({ f, m: fs.statSync(path.join(dir, f)).mtimeMs }))+                  .sort((a, b) => b.m - a.m)[0];+                if (recent) { abs = path.join(dir, recent.f); fname = recent.f; sizeOnDisk = fs.statSync(abs).size; }+              } catch (e2) {}+            }+            const magic = (() => { try { return fs.readFileSync(abs).slice(0, 5).toString('latin1').replace(/[^\x20-\x7e]/g, '.'); } catch (e) { return null; } })();+            sendJSON(res, { success: true, output: JSON.stringify({+              ok: true, savedTo: abs, filename: fname, bytes: (ev.totalReceivedBytes || ev.receivedBytes || sizeOnDisk), sizeOnDisk, magic,+              triggeredBy: args.selector ? ('click ' + args.selector) : ('a[download] ' + args.url),+              sessionId: resolvedId, tabId,+              _hint: `Chrome downloaded this to ${abs} (${sizeOnDisk != null ? sizeOnDisk + ' bytes' : 'size unread'}${magic ? ', starts "' + magic + '"' : ''}) — the real browser download, session cookies and all. To bring it into the container, pull it with Bridge file verbs (desktop_pull_file).`,+            }) });+          } catch (e) {+            sendJSON(res, { success: false, errorCode: 'download_failed', error: `pup_download failed: ${e.message}`, _hint: args.selector ? `The click may not have started a download. Verify the selector is the real download control (pup_screenshot to see it), and that this window is logged in if the file is gated.` : 'The URL did not download — for a gated file, sign in on this window first (pup_login).' });+          } finally { try { if (cdp) await cdp.detach(); } catch (e) {} }+          return;+        }         case 'pup_errors': {           let session, resolvedId;           // v1.6.1+: auto-recover (reconnect or relaunch) before surfacing