// The browser `storebound console fill` works in (the log says which): // (default) storebound's own persistent profile, one per store (~/.storebound/console-profile/, 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:): connectOverCDP and one new tab in that session; storebound closes only its tab. // 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= --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 (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, installedBrowsers, type LaunchDeps, persistentExecutable, profileHolder, sandboxArgs } from '../shots/browser'; import * as rt from '../lib/rt'; /* eslint-disable @typescript-eslint/no-explicit-any */ /** 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 = { 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 `: 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)/; /** Google Chrome's user data dir on this OS. */ export function chromeDataDir(platform: string = process.platform, home: string = homedir(), env: Record = process.env): string { if (platform === 'darwin') return join(home, 'Library', 'Application Support', 'Google', 'Chrome'); if (platform === 'win32') return join(env.LOCALAPPDATA ?? join(home, 'AppData', 'Local'), 'Google', 'Chrome', 'User Data'); return join(env.XDG_CONFIG_HOME ?? join(home, '.config'), 'google-chrome'); } /** The profile dir name: --chrome-profile, else Local State's last used profile, else Default. */ export function pickProfile(localState: string | null, requested?: string): string { if (requested) { if (!/^[\w .-]+$/.test(requested) || requested.includes('..')) throw new Error('--chrome-profile: a profile folder name such as "Default" or "Profile 1"'); return requested; } try { const last = JSON.parse(localState ?? '{}')?.profile?.last_used; if (typeof last === 'string' && /^[\w .-]+$/.test(last) && !last.includes('..')) return last; } catch { /* default */ } return 'Default'; } /** * The files the copy takes, as [source relative to the user data dir, destination relative to the temp dir]: Local * State and the profile's cookie store (Cookies, or Network/Cookies on newer layouts) with its journal. The profile * always lands in `Default` of the temp dir. Nothing else, ever. */ export function sessionFiles(profile: string, exists: (rel: string) => boolean): [string, string][] { const out: [string, string][] = []; if (exists('Local State')) out.push(['Local State', 'Local State']); for (const sub of ['', 'Network/']) { const db = `${profile}/${sub}Cookies`; if (!exists(db)) continue; out.push([db, `Default/${sub}Cookies`]); if (exists(`${db}-journal`)) out.push([`${db}-journal`, `Default/${sub}Cookies-journal`]); } for (const [s] of out) if (NEVER_COPY.test(s)) throw new Error(`session copy: refusing to copy ${s}`); return out; } /** A cookie stored for `hostKey` (".google.com" or "play.google.com") is sent to one of `hosts`. */ export function cookieApplies(hostKey: string, hosts: string[] = CONSOLE_HOSTS): boolean { const d = hostKey.replace(/^\./, '').toLowerCase(); if (!d.includes('.')) return false; // never a whole top-level domain return hosts.some(h => h === d || h.endsWith(`.${d}`)); } /** Deletes every cookie of the copied DB that the consoles never get. Returns counts only (never values or names). */ export async function filterCookies(dbPath: string, hosts: string[] = CONSOLE_HOSTS): Promise<{ kept: number; removed: number }> { // bun:sqlite under Bun, node:sqlite on Node 22.5+; without SQLite (Node 20) this throws and the caller says so const db = await rt.openSqlite(dbPath); try { const keys = db.all<{ h: string }>('SELECT DISTINCT host_key AS h FROM cookies').map(r => r.h); const drop = keys.filter(k => !cookieApplies(k, hosts)); const count = Number(db.get<{ n: number }>('SELECT COUNT(*) AS n FROM cookies')!.n); db.exec('BEGIN'); try { for (const k of drop) db.run('DELETE FROM cookies WHERE host_key = ?', k); db.exec('COMMIT'); } catch (e) { db.exec('ROLLBACK'); throw e; } const kept = Number(db.get<{ n: number }>('SELECT COUNT(*) AS n FROM cookies')!.n); db.exec('VACUUM'); return { kept, removed: count - kept }; } finally { db.close(); } } export interface SessionCopy { dir: string; profile: string; files: string[]; filtered: boolean; kept?: number; cleanup: () => void } /** * Copies the session files of `src` (a Chrome user data dir) into a new temp dir and filters the cookies to `hosts`. * On any error the temp dir is removed before the error is thrown. */ export async function copySession(src: string, opts: { profile?: string; hosts?: string[]; tmpRoot?: string } = {}): Promise { const ls = existsSync(join(src, 'Local State')) ? readFileSync(join(src, 'Local State'), 'utf8') : null; const profile = pickProfile(ls, opts.profile); const files = sessionFiles(profile, rel => existsSync(join(src, rel))); if (!files.some(([s]) => s.endsWith('Cookies'))) throw new Error(`no Chrome cookie store in profile "${profile}"`); const dir = mkdtempSync(join(opts.tmpRoot ?? tmpdir(), 'storebound-chrome-')); const cleanup = () => { // Chrome may still write a file while it exits: retry, and only stop once the folder is gone for (let i = 0; i < 5 && existsSync(dir); i++) { try { rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 }); } catch { rt.sleepSync(100); } } }; try { for (const [s, d] of files) { mkdirSync(dirname(join(dir, d)), { recursive: true }); copyFileSync(join(src, s), join(dir, d)); } let filtered = false, kept: number | undefined; try { for (const [, d] of files.filter(([, d]) => d.endsWith('Cookies'))) kept = (kept ?? 0) + (await filterCookies(join(dir, d), opts.hosts)).kept; filtered = true; } catch { /* the whole cookie store stays (said in the log) */ } return { dir, profile, files: files.map(([s]) => s), filtered, ...(kept !== undefined ? { kept } : {}), cleanup }; } catch (e) { cleanup(); throw e; } } /** The DevTools endpoint of a Chrome running with remote debugging (its DevToolsActivePort file), or null. */ export function devToolsEndpoint(dataDir: string): string | null { try { const [port] = readFileSync(join(dataDir, 'DevToolsActivePort'), 'utf8').split('\n'); return port && /^\d+$/.test(port.trim()) ? `http://127.0.0.1:${port.trim()}` : null; } catch { return null; } } const LOCAL_CDP = /^(https?|wss?):\/\/(127\.0\.0\.1|localhost|\[::1\])(:\d+)?(\/|$)/; export type BrowserMode = 'attach' | 'copy' | 'profile'; export interface ConsoleBrowser { mode: BrowserMode; context: any; /** The tab storebound works in. */ page: any; /** The tabs storebound may look at (its own tab and the popups it opened; in copy / profile mode every tab). */ pages: () => any[]; /** Resolves when the owner closed the window (copy / profile) or storebound's tab (attach). */ closed: Promise; isClosed: () => boolean; /** Closes gracefully (cookies flushed): the whole browser (copy / profile) or storebound's tab (attach). Max 10 s. */ close: () => Promise; /** Removes the temporary profile (copy mode); safe to call twice. */ 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 (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 (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: 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; } /** Runs a graceful close, at most `ms`; errors (already closed) are ignored. */ async function gracefully(fn: () => Promise, ms = 10_000): Promise { await Promise.race([Promise.resolve().then(fn).catch(() => { /* already closed */ }), rt.sleep(ms)]); } /** * 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 /DevToolsActivePort, loopback only), session restore, no automation markers, no * first-run dialogs; --no-sandbox only where Chromium cannot start without it (root on Linux, a container: * sandboxArgs). Nothing else: Chrome keeps its own user agent, keychain and updates. */ export function consoleChromeArgs(userDataDir: string, o: { headless?: boolean; sandbox?: string[] } = {}): string[] { return [`--user-data-dir=${userDataDir}`, '--remote-debugging-port=0', '--no-first-run', '--no-default-browser-check', ...KEEP_SESSION_ARGS, ...NO_AUTOMATION_ARGS, ...(o.sandbox ?? sandboxArgs()), ...(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 { const restored: any[] = context.pages(); const page = await context.newPage(); for (const p of restored) await p.close().catch(() => { /* gone */ }); return page; } /** What the browser decision is, before anything starts (pure; tests). */ export function browserPlan(o: Pick, 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 { 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 = rt.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 rt.sleep(100); } if (!endpoint) { if (!exited) { proc.kill('SIGTERM'); await Promise.race([gone, rt.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, rt.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, rt.sleep(10_000)]); if (!exited) { proc.kill('SIGTERM'); await Promise.race([gone, rt.sleep(5000)]); } }; let closed = false; const done = gone.then(async () => { closed = true; cleanup(); await rt.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 { const chromeDir = o.chromeDir ?? chromeDataDir(o.platform); 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:)'); const endpoint = o.cdp ?? devToolsEndpoint(chromeDir); if (endpoint) { try { const c = o.launchDeps?.pw ?? await chromiumFor(root); const browser = await c.connectOverCDP(endpoint, { timeout: 5000 }); const context = browser.contexts()[0] ?? await browser.newContext(); const page = await context.newPage(); const own = new Set([page]); page.on('popup', (p: any) => own.add(p)); let closed = false; const done = new Promise(r => page.on('close', () => { closed = true; r(); })); o.say('browser: attached to your running Chrome (remote debugging); storebound works in one new tab and closes only that tab'); 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; using storebound\'s own profile'); } } } 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 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; }