console fill: never copy Google session cookies by default (device-bound); own profile in a Chrome started without automation switches

Google binds its session cookies to the device (Device Bound Session Credentials) and signs out
sessions whose cookies look stolen, which can sign the owner out of their real Chrome. The default is
now storebound's own profile per store; storebound starts Chrome itself (--user-data-dir,
--remote-debugging-port=0, no --enable-automation, navigator.webdriver false) and attaches over CDP.
--use-chrome-session google|apple is an explicit opt-in that warns. /signin/rejected is said plainly.
This commit is contained in:
CRTkafa 2026-10-12 07:46:04 +08:00
parent e0bef82b06
commit 0035931734
22 changed files with 456 additions and 156 deletions

View file

@ -1,39 +1,92 @@
// The browser `storebound console fill` works in, picked so the owner signs in as rarely as possible (the log says which):
// The browser `storebound console fill` works in (the log says which):
// (default) storebound's own persistent profile, one per store (~/.storebound/console-profile/<store>, or --profile /
// STOREBOUND_CONSOLE_PROFILE): the owner signs in there once, by hand (storebound never types passwords), and the
// sign-in is kept between runs. Nothing of the owner's own Chrome is read or copied.
// (a) attach: Chrome already runs with remote debugging (a DevToolsActivePort file in its user data dir, or
// --cdp http://127.0.0.1:<port>): connectOverCDP and one new tab in that session; storebound closes only its tab.
// (b) copy (default on macOS when Google Chrome is installed): a temporary profile with ONLY `Local State` and the
// profile's `Cookies` (+ `Cookies-journal`), filtered down to the cookies App Store Connect and Play Console use
// (apple.com, google.com domains: appstoreconnect / idmsa / play / accounts), opened in real Google Chrome
// (Chrome 136+ refuses automation on its default user data dir, a copy is fine). Chrome decrypts the cookies with
// its own Keychain key ("Chrome Safe Storage"); macOS may ask once to allow that. Never copied: passwords
// (Login Data), history, extensions, anything else. The temporary profile is deleted when the run ends (also on
// an error, Ctrl+C or when the desktop app cancels). `--chrome-profile <dir name>` picks a profile ("Profile 1");
// default: the last used one. `--fresh-profile` skips (a) and (b).
// (c) storebound's own persistent profile (~/.storebound/console-profile/<store>): the owner signs in there once.
// Cookies are never printed, logged or sent anywhere: only file names and counts are reported.
// Keeping the sign-in (b, c): App Store Connect's and Google's sign-in cookies are partly SESSION cookies (no expiry).
// The owner's own browser, in place: no cookie leaves it.
// (b) copy, OPT-IN ONLY (`--use-chrome-session google|apple`, macOS): a temporary profile with ONLY `Local State` and
// the profile's `Cookies` (+ `Cookies-journal`), filtered down to the cookies of the ONE store the form is for
// (apple: apple.com hosts; google: play.google.com / accounts.google.com), opened in real Google Chrome. Never
// copied: passwords (Login Data), history, extensions, anything else. Deleted when the run ends.
// Why the copy is opt-in (an incident, October 2026): a copied Google session reached Play Console once, then Google
// answered /v3/signin/rejected and the account chooser. Google binds its session cookies to the device: Chrome's Device
// Bound Session Credentials keep a non-exportable key in the TPM / Secure Enclave and Google only renews the
// short-lived cookies for the browser holding it (public on Windows in Chrome 146, rolling out on macOS from Chrome
// 147), and Google signs out a session whose cookie looks stolen: "the user is logged out of their account for that
// session and any related suspicious sessions on that device" and must pass an extra verification to sign back in.
// A copy of the owner's Google cookies used by a second Chrome is exactly what that detection looks for, so it can
// sign the owner out of their REAL Chrome or flag the account. Apple publishes no device binding or cookie-theft
// sign-out for Apple ID web sessions (fastlane's spaceauth reuses them on other machines; Apple ties them to a region),
// but it does not rule one out either, so the Apple copy is opt-in as well.
// The window: storebound starts Chrome ITSELF (`--user-data-dir=<its profile> --remote-debugging-port=0`; allowed on a
// non-default user data dir) and attaches over CDP, instead of Playwright's launch: no `--enable-automation` (no
// "controlled by automated test software" bar), `--disable-blink-features=AutomationControlled` (navigator.webdriver
// is false), none of Playwright's other switches (mock keychain, disabled sync / component updates ...), the browser's
// own user agent. Google decides by itself whether it accepts a sign-in there; when it answers /signin/rejected,
// storebound says so (googleRejected).
// Keeping the sign-in: App Store Connect's and Google's sign-in cookies are partly SESSION cookies (no expiry).
// Chromium writes them to its Cookies file but deletes them at the next start unless that start restores the last
// session, so storebound launches with --restore-last-session (the tabs it brings back are closed again; storebound
// works in one new tab) and always closes the browser gracefully (window closed, Ctrl+C), never kills it, so Chromium
// flushes its cookie store. Apple keeps a long sign-in only with "Keep me signed in" / "Trust this browser" ticked
// (storebound says so when it sees the sign-in page). Copy mode: the owner's Chrome Cookies file holds the session
// cookies only while that Chrome runs, or after it quit with "On startup: Continue where you left off" on (a normal
// Chrome start without it deletes them); otherwise the owner signs in once in the window storebound opens.
// The session.restore_on_startup preference is not written: it is a tracked (MAC-protected) Chrome preference, and an
// edit from outside can be reset by Chrome; the switch does the same for this launch only.
// Sources (checked October 2026): Chrome user data dir and DevToolsActivePort
// https://chromium.googlesource.com/chromium/src/+/main/docs/user_data_dir.md ; Chrome 136 remote debugging change
// https://developer.chrome.com/blog/remote-debugging-port ; Playwright connectOverCDP / ignoreDefaultArgs
// https://playwright.dev/docs/api/class-browsertype
// works in one new tab) and always closes the browser gracefully (CDP Browser.close, window closed, Ctrl+C), never
// kills it, so Chromium flushes its cookie store. Apple keeps a long sign-in only with "Keep me signed in" / "Trust
// this browser" ticked (storebound says so when it sees the sign-in page).
// Cookies are never printed, logged or sent anywhere: only file names and counts are reported.
// Sources (checked October 2026): DBSC https://blog.google/security/protecting-cookies-with-device-bound-session-credentials/
// and https://developer.chrome.com/docs/web-platform/device-bound-session-credentials ; Google's sign-out on suspicious
// session cookies https://knowledge.workspace.google.com/admin/security/investigate-and-take-action-on-suspicious-session-cookies ;
// Apple sessions on other machines https://docs.fastlane.tools/getting-started/ios/authentication/ ; Chrome user data
// dir and DevToolsActivePort https://chromium.googlesource.com/chromium/src/+/main/docs/user_data_dir.md ; Chrome 136
// remote debugging change https://developer.chrome.com/blog/remote-debugging-port ; Playwright connectOverCDP
// https://playwright.dev/docs/api/class-browsertype#browser-type-connect-over-cdp
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { homedir, tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { chromiumFor, type LaunchDeps, launchPersistent } from '../shots/browser';
import { chromiumFor, installedBrowsers, type LaunchDeps, persistentExecutable, profileHolder } from '../shots/browser';
/* eslint-disable @typescript-eslint/no-explicit-any */
/** Hosts whose cookies the consoles need; a cookie is kept when its domain applies to one of them. */
export const CONSOLE_HOSTS = ['appstoreconnect.apple.com', 'idmsa.apple.com', 'apple.com', 'play.google.com', 'accounts.google.com'];
/** Whose sign-in an opt-in copy takes: Apple's (App Store Connect) or Google's (Play Console). */
export type SessionProvider = 'apple' | 'google';
/** Per provider, the hosts whose cookies an opt-in copy keeps; a cookie is kept when its domain applies to one of them. */
export const PROVIDER_HOSTS: Record<SessionProvider, string[]> = {
apple: ['appstoreconnect.apple.com', 'idmsa.apple.com', 'apple.com'],
google: ['play.google.com', 'accounts.google.com'],
};
/** Both consoles' hosts (an opt-in copy always takes ONE provider's: PROVIDER_HOSTS). */
export const CONSOLE_HOSTS = [...PROVIDER_HOSTS.apple, ...PROVIDER_HOSTS.google];
/** The provider whose sign-in a store's console uses. */
export const storeProvider = (store: 'apple' | 'play'): SessionProvider => (store === 'play' ? 'google' : 'apple');
/** `--use-chrome-session <google|apple[,..]>`: the providers named; anything else is an error. Absent: none. */
export function parseUseChromeSession(v: string | boolean | undefined): SessionProvider[] {
if (v === undefined || v === false) return [];
const names = typeof v === 'string' ? v.split(',').map(x => x.trim().toLowerCase()).filter(Boolean) : [];
if (!names.length || names.some(n => n !== 'google' && n !== 'apple')) throw new Error('--use-chrome-session: google | apple (copies that sign-in from your Chrome; read the warning in the console-autofill skill first)');
return [...new Set(names)] as SessionProvider[];
}
/** Said before an opt-in copy of the owner's Google cookies (DBSC / cookie-theft sign-out, see the header). */
export const GOOGLE_COPY_WARNING = 'WARNING --use-chrome-session google: Google binds its sign-in cookies to your device (Device Bound Session Credentials) and signs out sessions whose cookies look copied. Using a copy of them in a second Chrome can make Google refuse the copy (/signin/rejected) AND sign you out of your real Chrome, or ask you to verify your account again. Without this flag storebound uses its own profile: sign in there once.';
/** Said before an opt-in copy of the owner's Apple cookies. */
export const APPLE_COPY_NOTE = 'note --use-chrome-session apple: Apple publishes no device binding for its web sign-in, but it does not rule out a security check either; if App Store Connect asks you to sign in or verify again, do that, and prefer storebound\'s own profile (run without this flag).';
/** Google refused the sign-in in this window (accounts.google.com/v3/signin/rejected and older /signin/.../rejected). */
export function googleRejected(url: string): boolean {
try {
const u = new URL(url);
return u.hostname === 'accounts.google.com' && /(^|\/)signin\/(v\d+\/)?rejected(\/|$)/i.test(u.pathname);
} catch { return false; }
}
/** What storebound says when Google refused the sign-in (googleRejected). */
export const GOOGLE_REJECTED = 'Google refused sign-in in this window (/signin/rejected). storebound uses a normal Chrome window; try again, or sign in at accounts.google.com first in this window.';
/** What to do the first time in storebound's own profile (the owner signs in by hand; storebound never types passwords). */
export function firstSignInSteps(store: 'apple' | 'play'): string {
return store === 'play'
? 'first run in this profile: in the window, sign in to the Google account that owns your Play developer account yourself (email, password, 2-step verification; storebound never types them), tick "Stay signed in" if asked. If Google answers "Couldn\'t sign you in", open accounts.google.com in that window and sign in there first. The sign-in is kept for the next runs.'
: 'first run in this profile: in the window, sign in to App Store Connect yourself (Apple ID, password, two-factor code; storebound never types them) and tick "Keep me signed in" / "Trust" this browser. The sign-in is kept for the next runs.';
}
/** Files of a Chrome profile that are never copied (passwords, history, extensions, form data ...). */
export const NEVER_COPY = /(^|\/)(Login Data|History|Extensions|Web Data|Bookmarks|Sessions|Preferences|Secure Preferences|Local Storage|IndexedDB|Account Web Data)/;
@ -151,52 +204,57 @@ export interface ConsoleBrowser {
cleanup: () => void;
/** Copy mode: the temporary profile (deleted by cleanup and when the window closes). */
tempDir?: string;
/** Copy / profile: the command line storebound started Chrome with (never automation switches). */
args?: string[];
}
export interface OpenOpts {
store: 'apple' | 'play';
/** --cdp: a local DevTools endpoint to attach to. */
cdp?: string;
/** --chrome-profile. */
/** --chrome-profile (only with useChromeSession). */
chromeProfile?: string;
/** --use-chrome-session: the providers whose Chrome sign-in the owner opted in to copy. Default: none (own profile). */
useChromeSession?: SessionProvider[];
/** --fresh-profile: skip attach and copy. */
fresh?: boolean;
/** storebound's own profile dir (mode c). */
/** storebound's own profile dir (the default). */
profileDir: string;
/** Overrides for tests: Chrome's user data dir, the hosts kept, headless, the platform. */
chromeDir?: string;
hosts?: string[];
headless?: boolean;
platform?: string;
/** Tests: Playwright's launch timeout for modes (b) and (c) (default: Playwright's 30 s). */
/** Tests: how long the browser may take to start (default 30 s). */
launchTimeoutMs?: number;
/** Tests: Playwright's chromium and the installed browsers the launch resolver sees. */
launchDeps?: LaunchDeps;
say: (m: string) => void;
}
function wrapContext(mode: BrowserMode, context: any, page: any, cleanup: () => void): ConsoleBrowser {
let closed = false;
const done = new Promise<void>(r => context.on('close', async () => {
closed = true;
cleanup();
await Bun.sleep(300); // a late write of the exiting browser
cleanup();
r();
}));
return { mode, context, page, pages: () => context.pages(), closed: done, isClosed: () => closed, close: () => gracefully(() => context.close()), cleanup };
}
/** Runs a graceful close, at most `ms`; errors (already closed) are ignored. */
async function gracefully(fn: () => Promise<unknown>, ms = 10_000): Promise<void> {
await Promise.race([Promise.resolve().then(fn).catch(() => { /* already closed */ }), Bun.sleep(ms)]);
}
/**
* Launch switches of the copy and profile modes: bring back the last session's cookies (session cookies included,
* Launch switch of the copy and profile modes: bring back the last session's cookies (session cookies included,
* see the header). The restored tabs are closed again by ownTab.
*/
export const KEEP_SESSION_ARGS = ['--restore-last-session'];
/** No automation marker a page can see: navigator.webdriver stays false (also with --headless, used by tests only). */
export const NO_AUTOMATION_ARGS = ['--disable-blink-features=AutomationControlled'];
/** Switches storebound never passes to the console window (Playwright's launch adds the first). */
export const AUTOMATION_SWITCHES = ['--enable-automation', '--remote-debugging-pipe', '--use-mock-keychain', '--password-store=basic'];
/**
* The command line of the Chrome storebound starts itself for the console window: its own user data dir, a DevTools
* port the OS picks (written to <dir>/DevToolsActivePort, loopback only), session restore, no automation markers, no
* first-run dialogs. Nothing else: Chrome keeps its own user agent, keychain and updates.
*/
export function consoleChromeArgs(userDataDir: string, o: { headless?: boolean } = {}): string[] {
return [`--user-data-dir=${userDataDir}`, '--remote-debugging-port=0', '--no-first-run', '--no-default-browser-check', ...KEEP_SESSION_ARGS, ...NO_AUTOMATION_ARGS, ...(o.headless ? ['--headless=new'] : [])];
}
/** One new tab for storebound; the tabs the session restore brought back are closed (they would compete for "the active tab"). */
async function ownTab(context: any): Promise<any> {
@ -206,11 +264,72 @@ async function ownTab(context: any): Promise<any> {
return page;
}
/** Opens the browser in the first mode that works: attach, copy, then storebound's own profile. */
/** What the browser decision is, before anything starts (pure; tests). */
export function browserPlan(o: Pick<OpenOpts, 'store' | 'cdp' | 'fresh' | 'useChromeSession' | 'platform'>, chromeDirExists: boolean): { attach: boolean; copy: SessionProvider | null } {
const provider = storeProvider(o.store);
const copy = !o.fresh && (o.useChromeSession ?? []).includes(provider) && (o.platform ?? process.platform) === 'darwin' && chromeDirExists ? provider : null;
return { attach: !o.fresh, copy };
}
/**
* Starts `exe` on `userDataDir` (consoleChromeArgs) and attaches over CDP. Chrome is closed with CDP Browser.close (a
* normal shutdown: cookies flushed), SIGTERM after 10 s; the window counts as closed when Chrome exits or its last tab
* is closed (macOS keeps Chrome running without windows: then storebound closes it gracefully).
*/
async function startOwnChrome(root: string, mode: 'copy' | 'profile', exe: { label: string; path: string }, userDataDir: string, o: OpenOpts, cleanup: () => void): Promise<ConsoleBrowser> {
const holder = profileHolder(userDataDir);
if (holder) throw new Error(`the browser profile ${userDataDir} is still open in another browser (process ${holder}), probably an earlier storebound run that waits for its window to close. Close that window (or end that run) and start again`);
const portFile = join(userDataDir, 'DevToolsActivePort');
rmSync(portFile, { force: true });
const args = consoleChromeArgs(userDataDir, { headless: o.headless ?? false });
const proc = Bun.spawn([exe.path, ...args], { stdin: 'ignore', stdout: 'ignore', stderr: 'ignore' });
let exited = false;
const gone = proc.exited.then(() => { exited = true; });
const end = Date.now() + (o.launchTimeoutMs ?? 30_000);
let endpoint: string | null = null;
while (!endpoint && !exited && Date.now() < end) { endpoint = devToolsEndpoint(userDataDir); if (!endpoint) await Bun.sleep(100); }
if (!endpoint) {
if (!exited) { proc.kill('SIGTERM'); await Promise.race([gone, Bun.sleep(5000)]); }
throw new Error(`${exe.label} did not start${exited ? ` (exit code ${proc.exitCode})` : ' in time'}`);
}
let browser: any;
try {
const pw = o.launchDeps?.pw ?? await chromiumFor(root);
browser = await pw.connectOverCDP(endpoint, { timeout: Math.max(5000, end - Date.now()) });
} catch (e) {
proc.kill('SIGTERM'); await Promise.race([gone, Bun.sleep(5000)]);
throw new Error(`${exe.label}: could not attach (${(e as Error).message.split('\n')[0]})`, { cause: e });
}
const context = browser.contexts()[0] ?? await browser.newContext();
const close = async () => {
if (exited) return;
await gracefully(async () => { const s = await browser.newBrowserCDPSession(); await s.send('Browser.close'); });
await Promise.race([gone, Bun.sleep(10_000)]);
if (!exited) { proc.kill('SIGTERM'); await Promise.race([gone, Bun.sleep(5000)]); }
};
let closed = false;
const done = gone.then(async () => { closed = true; cleanup(); await Bun.sleep(300); cleanup(); });
const page = await ownTab(context);
const watch = (p: any) => p.on('close', () => { if (!exited && context.pages().length === 0) void close(); });
for (const p of context.pages()) watch(p);
context.on('page', watch);
return { mode, context, page, pages: () => context.pages(), closed: done, isClosed: () => closed, close, cleanup, args: [exe.path, ...args], ...(mode === 'copy' ? { tempDir: userDataDir } : {}) };
}
/** The executable for the console window: kind "visible" (own profile) or "chrome" (a copy: only real Chrome decrypts it). */
function executableFor(root: string, kind: 'visible' | 'chrome', o: OpenOpts): Promise<{ label: string; path: string }> {
return (async () => {
const installed = o.launchDeps?.installed ?? installedBrowsers(o.launchDeps?.pw ?? await chromiumFor(root).catch(() => null));
return persistentExecutable(kind, installed);
})();
}
/** Opens the browser: attach (a running Chrome with remote debugging), an opt-in copy, else storebound's own profile. */
export async function openConsoleBrowser(root: string, o: OpenOpts): Promise<ConsoleBrowser> {
const chromeDir = o.chromeDir ?? chromeDataDir(o.platform);
if (!o.fresh) {
// (a) attach to a running Chrome with remote debugging
const plan = browserPlan(o, existsSync(chromeDir));
if (plan.attach) {
// (a) attach to a running Chrome with remote debugging: the owner's own browser, nothing copied
if (o.cdp && !LOCAL_CDP.test(o.cdp)) throw new Error('--cdp: only a local DevTools endpoint (http://127.0.0.1:<port>)');
const endpoint = o.cdp ?? devToolsEndpoint(chromeDir);
if (endpoint) {
@ -227,35 +346,36 @@ export async function openConsoleBrowser(root: string, o: OpenOpts): Promise<Con
return { mode: 'attach', context, page, pages: () => [...own].filter(p => !p.isClosed()), closed: done, isClosed: () => closed, close: () => gracefully(() => page.close()), cleanup: () => { /* nothing copied */ } };
} catch (e) {
if (o.cdp) throw new Error(`--cdp: could not attach to ${o.cdp} (${(e as Error).message.split('\n')[0]})`);
o.say('browser: Chrome has a DevTools port file but storebound could not attach; trying your Chrome sign-in instead');
}
}
// (b) a copy of only the session files, opened in real Google Chrome
if ((o.platform ?? process.platform) === 'darwin' && existsSync(chromeDir)) {
let copy: SessionCopy | null = null;
try {
copy = await copySession(chromeDir, { ...(o.chromeProfile ? { profile: o.chromeProfile } : {}), ...(o.hosts ? { hosts: o.hosts } : {}) });
const context = await launchPersistent(root, copy.dir, {
headless: o.headless ?? false, viewport: null, args: KEEP_SESSION_ARGS, ...(o.launchTimeoutMs ? { timeout: o.launchTimeoutMs } : {}),
// Playwright's mock keychain would hide Chrome's own key, and the cookies could not be read
ignoreDefaultArgs: ['--use-mock-keychain', '--password-store=basic'],
}, { ...o.launchDeps, kind: 'chrome' }); // only real Google Chrome decrypts the owner's cookies
o.say(`browser: your Chrome sign-in (profile "${copy.profile}"). storebound copied only ${copy.files.join(', ')} into a temporary profile`
+ `${copy.filtered ? `, kept only the cookies for App Store Connect and Play Console (${copy.kept ?? 0})` : ' (the whole cookie file: it could not be filtered)'}`
+ ', and deletes it when this run ends. Passwords, history and extensions are never copied. macOS may ask once to let Google Chrome use "Chrome Safe Storage" in your Keychain: that is Chrome reading its own cookies. Opt out: --fresh-profile');
const page = await ownTab(context);
const cp = copy;
return { ...wrapContext('copy', context, page, () => cp.cleanup()), tempDir: cp.dir };
} catch (e) {
copy?.cleanup();
o.say(`browser: could not use your Chrome sign-in (${(e as Error).message.split('\n')[0]}); using storebound's own profile`);
o.say('browser: Chrome has a DevTools port file but storebound could not attach; using storebound\'s own profile');
}
}
}
// (c) storebound's persistent profile, the owner signs in there
const asked = (o.useChromeSession ?? []).filter(p => p === storeProvider(o.store));
if (asked.length && !plan.copy) o.say(`browser: --use-chrome-session ${asked[0]} needs macOS and Google Chrome's data folder (${chromeDir}); using storebound's own profile`);
if (plan.copy) {
// (b) OPT-IN: a copy of only this store's session cookies, opened in real Google Chrome
o.say(plan.copy === 'google' ? GOOGLE_COPY_WARNING : APPLE_COPY_NOTE);
let copy: SessionCopy | null = null;
try {
copy = await copySession(chromeDir, { ...(o.chromeProfile ? { profile: o.chromeProfile } : {}), hosts: o.hosts ?? PROVIDER_HOSTS[plan.copy] });
const exe = await executableFor(root, 'chrome', o);
const cp = copy;
const b = await startOwnChrome(root, 'copy', exe, cp.dir, o, () => cp.cleanup());
o.say(`browser: your Chrome sign-in for ${plan.copy === 'google' ? 'Google' : 'Apple'} (profile "${cp.profile}", opted in). storebound copied only ${cp.files.join(', ')} into a temporary profile`
+ `${cp.filtered ? `, kept only the ${plan.copy === 'google' ? 'Google' : 'Apple'} sign-in cookies (${cp.kept ?? 0})` : ' (the whole cookie file: it could not be filtered)'}`
+ ', and deletes it when this run ends. Passwords, history and extensions are never copied. macOS may ask once to let Google Chrome use "Chrome Safe Storage" in your Keychain: that is Chrome reading its own cookies.');
return b;
} catch (e) {
copy?.cleanup();
o.say(`browser: could not use your Chrome sign-in (${(e as Error).message.split('\n')[0]}); using storebound's own profile`);
}
}
// (default) storebound's own persistent profile, the owner signs in there once
const fresh = !existsSync(o.profileDir);
mkdirSync(o.profileDir, { recursive: true });
const context = await launchPersistent(root, o.profileDir, { headless: o.headless ?? false, viewport: null, args: KEEP_SESSION_ARGS, ...(o.launchTimeoutMs ? { timeout: o.launchTimeoutMs } : {}) }, { ...o.launchDeps, kind: 'visible' });
o.say(`browser: storebound's own profile (${o.profileDir}); sign in there once, it is kept for the next run`);
const page = await ownTab(context);
return wrapContext('profile', context, page, () => { /* kept on purpose */ });
const exe = await executableFor(root, 'visible', o);
const b = await startOwnChrome(root, 'profile', exe, o.profileDir, o, () => { /* kept on purpose */ });
o.say(`browser: storebound's own profile (${o.profileDir}) in ${exe.label}, started as a normal Chrome window (no automation flags); nothing is copied from your own Chrome. Sign in there once, it is kept for the next run`);
if (fresh) o.say(firstSignInSteps(o.store));
return b;
}