@parall/daemon 1.45.0 → 1.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/bundle/manifest.json +15 -15
  2. package/bundle/parall-browser-pod.js +29726 -375
  3. package/bundle/parall-channel-exec.js +2 -0
  4. package/bundle/parall-claude-agent.js +26091 -273
  5. package/bundle/parall-codex-agent.js +26650 -331
  6. package/bundle/parall-daemon.js +31384 -1998
  7. package/bundle/parall-openclaw-agent.js +1 -0
  8. package/dist/browser-pod.d.ts +23 -1
  9. package/dist/browser-pod.d.ts.map +1 -1
  10. package/dist/browser-pod.js +104 -34
  11. package/dist/browser-profile-reconcile.d.ts +21 -0
  12. package/dist/browser-profile-reconcile.d.ts.map +1 -0
  13. package/dist/browser-profile-reconcile.js +188 -0
  14. package/dist/clip-runtime/browser-cdp.d.ts +40 -0
  15. package/dist/clip-runtime/browser-cdp.d.ts.map +1 -0
  16. package/dist/clip-runtime/browser-cdp.js +218 -0
  17. package/dist/clip-runtime/browser-profile-manager.d.ts +97 -24
  18. package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
  19. package/dist/clip-runtime/browser-profile-manager.js +316 -182
  20. package/dist/clip-runtime/browser-profile-pool.d.ts +3 -0
  21. package/dist/clip-runtime/browser-profile-pool.d.ts.map +1 -1
  22. package/dist/clip-runtime/browser-profile-pool.js +18 -1
  23. package/dist/clip-runtime/browser-proxy-reconcile.d.ts +77 -0
  24. package/dist/clip-runtime/browser-proxy-reconcile.d.ts.map +1 -0
  25. package/dist/clip-runtime/browser-proxy-reconcile.js +139 -0
  26. package/dist/clip-runtime/browser-proxy-state.d.ts +55 -0
  27. package/dist/clip-runtime/browser-proxy-state.d.ts.map +1 -0
  28. package/dist/clip-runtime/browser-proxy-state.js +149 -0
  29. package/dist/clip-runtime/browser-quiescence.d.ts +71 -0
  30. package/dist/clip-runtime/browser-quiescence.d.ts.map +1 -0
  31. package/dist/clip-runtime/browser-quiescence.js +136 -0
  32. package/dist/clip-runtime/browser-readiness.d.ts +64 -0
  33. package/dist/clip-runtime/browser-readiness.d.ts.map +1 -0
  34. package/dist/clip-runtime/browser-readiness.js +161 -0
  35. package/dist/clip-runtime/browser-state-store.d.ts +13 -2
  36. package/dist/clip-runtime/browser-state-store.d.ts.map +1 -1
  37. package/dist/clip-runtime/browser-state-store.js +15 -6
  38. package/dist/clip-runtime/browser-target-registry.d.ts +143 -0
  39. package/dist/clip-runtime/browser-target-registry.d.ts.map +1 -0
  40. package/dist/clip-runtime/browser-target-registry.js +297 -0
  41. package/dist/clip-runtime/browser-viewer-streamer.d.ts +13 -14
  42. package/dist/clip-runtime/browser-viewer-streamer.d.ts.map +1 -1
  43. package/dist/clip-runtime/browser-viewer-streamer.js +11 -63
  44. package/dist/daemon-main.d.ts.map +1 -1
  45. package/dist/daemon-main.js +3 -1
  46. package/dist/supervisor.d.ts +6 -0
  47. package/dist/supervisor.d.ts.map +1 -1
  48. package/dist/supervisor.js +48 -187
  49. package/package.json +8 -6
@@ -29,6 +29,9 @@ export interface BrowserProfilePoolOptions {
29
29
  /** Per-profile outbound proxy resolver (#1642), threaded to each profile's
30
30
  * manager so its isolated Chromium binds its own egress proxy at account_create. */
31
31
  resolveProxy?: BrowserProfileManagerOptions['resolveProxy'];
32
+ /** Proxy readiness probe endpoint, threaded to each manager (see
33
+ * browser-readiness.ts). */
34
+ proxyProbeUrl?: string;
32
35
  /**
33
36
  * Per-profile runtime factory. Defaults to a real {@link BrowserProfileManager};
34
37
  * overridable in tests to avoid spawning a real bb-browser / Chromium.
@@ -1 +1 @@
1
- {"version":3,"file":"browser-profile-pool.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-profile-pool.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"browser-profile-pool.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-profile-pool.ts"],"names":[],"mappings":"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"}
@@ -151,7 +151,23 @@ export class BrowserProfilePool {
151
151
  if (generation !== undefined)
152
152
  this.generations.set(profileId, generation);
153
153
  this.stopped.delete(profileId); // explicit open revives a stopped profile
154
- await this.manager(profileId).openProfile(profileId, startUrl);
154
+ try {
155
+ await this.manager(profileId).openProfile(profileId, startUrl);
156
+ }
157
+ catch (err) {
158
+ // Failed open: tear the half-started runtime down (no zombie Chromium
159
+ // burning resources behind an `error` row) and re-fence so late hub
160
+ // invokes are rejected until an explicit reopen. The manager already
161
+ // reported `error` with the structured cause; keep the rejection.
162
+ // stopped.add only — no fence() bump, so an open QUEUED behind this one
163
+ // (captured seq unchanged) still revives normally.
164
+ this.stopped.add(profileId);
165
+ const manager = this.managers.get(profileId);
166
+ if (manager) {
167
+ await this.dispose(profileId, manager).catch(() => { });
168
+ }
169
+ throw err;
170
+ }
155
171
  }));
156
172
  }
157
173
  /** True if a newer stop was observed after this revive captured `sinceSeq`. */
@@ -416,6 +432,7 @@ export class BrowserProfilePool {
416
432
  // manager, so each isolated Chromium binds its own egress proxy at
417
433
  // account_create (the resolver is keyed by profileId === account).
418
434
  resolveProxy: this.opts.resolveProxy,
435
+ proxyProbeUrl: this.opts.proxyProbeUrl,
419
436
  });
420
437
  this.managers.set(profileId, manager);
421
438
  return manager;
@@ -0,0 +1,77 @@
1
+ import type { BrowserProxyConfig } from './browser-profile-manager.js';
2
+ import type { BrowserTargetRegistry } from './browser-target-registry.js';
3
+ /**
4
+ * Cookie-preserving proxy reconciliation (desired-state enforcement).
5
+ *
6
+ * bb-browser binds an account's proxy at account_create and REPLAYS the proxy
7
+ * stored in its own account JSON on every later context activation — there is
8
+ * no update API, and `account_delete` (the only rebind path) destroys the
9
+ * cookie jar. So a proxy edit in Parall would either never apply or cost the
10
+ * user their logins. This module closes that gap without touching bb-browser:
11
+ *
12
+ * 1. capture the FULL cookie jar (HttpOnly included) straight from Chromium
13
+ * via CDP `Storage.getCookies({browserContextId})`;
14
+ * 2. persist it as a backup (crash safety — a fresh account JSON starts empty,
15
+ * so a crash mid-reconcile must not lose the jar);
16
+ * 3. `account_delete` → `account_create` with the DESIRED proxy fields;
17
+ * 4. restore the jar into the recreated context via `Storage.setCookies`;
18
+ * 5. DELETE the backup, THEN record the applied fingerprint in the sidecar —
19
+ * in that order (fix #8). Writing the fingerprint LAST guarantees a backup
20
+ * is never left on disk alongside an up-to-date fingerprint, so a later
21
+ * proxy change can never resurrect a stale jar over the current login, and
22
+ * old cookies never linger. bb-browser's own autosave then persists the
23
+ * restored cookies into its new account JSON — Parall never writes that file.
24
+ *
25
+ * Scope note: account BrowserContexts are CDP-created (incognito-style), so
26
+ * their localStorage/IndexedDB were never durable across a Chromium restart in
27
+ * bb-browser's design (only cookies persist, via its account JSON). Every
28
+ * reconcile runs at open-after-stop — i.e. after a Chromium restart — so
29
+ * reconciliation never loses state that would otherwise have survived.
30
+ */
31
+ export interface ProxyReconcileHost {
32
+ log: {
33
+ info(msg: string): void;
34
+ warn(msg: string): void;
35
+ error(msg: string): void;
36
+ };
37
+ homeDir: string;
38
+ targets: BrowserTargetRegistry;
39
+ sendCommand(request: Record<string, unknown> & {
40
+ method: string;
41
+ account?: string;
42
+ }): Promise<Record<string, unknown>>;
43
+ cdpEndpoint(): Promise<{
44
+ host: string;
45
+ port: number;
46
+ }>;
47
+ }
48
+ /** Render a proxy config into bb-browser `account_create` fields (camelCase, the
49
+ * bb-browser-pro 0.15 contract). Empty when there is no proxy → direct egress. */
50
+ export declare function proxyCreateFields(proxy: BrowserProxyConfig | null): Record<string, string>;
51
+ /**
52
+ * Whether the account must be rebuilt to honor `desired`. Unknown recorded
53
+ * state (pre-upgrade account, wiped sidecar) reconciles unless BOTH the desired
54
+ * config and the account's visible proxy are direct — the account JSON may
55
+ * carry a stale proxy (or a stale password) that Parall's config no longer
56
+ * matches, and one context rebuild with cookies preserved is invisible.
57
+ */
58
+ export declare function proxyReconcileNeeded(recordedFingerprint: string | undefined, desired: BrowserProxyConfig | null, accountShowsProxy: boolean): boolean;
59
+ /**
60
+ * Rebuild the account's BrowserContext with the desired proxy, preserving its
61
+ * cookie jar. The account MUST exist; callers hold the per-profile op mutex.
62
+ */
63
+ export declare function reconcileAccountProxy(host: ProxyReconcileHost, account: string, desired: BrowserProxyConfig | null): Promise<void>;
64
+ /**
65
+ * Recover a reconcile that crashed after account_delete: the account no longer
66
+ * exists, but a cookie backup does. Called right after a fresh account_create —
67
+ * runs the same settle tail (restore → clear → fingerprint). Without a backup,
68
+ * just records the applied fingerprint for the fresh account.
69
+ */
70
+ export declare function settlePendingCookieBackup(host: ProxyReconcileHost, account: string, applied: string): Promise<void>;
71
+ /**
72
+ * A log/error-safe proxy label: scheme + host:port ONLY. The username is auth
73
+ * material (customer / region / session selectors) and `server` may itself embed
74
+ * `user:pass@` userinfo, so neither is ever emitted — only the endpoint.
75
+ */
76
+ export declare function redactedProxyLabel(proxy: BrowserProxyConfig): string;
77
+ //# sourceMappingURL=browser-proxy-reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-proxy-reconcile.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-proxy-reconcile.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAQvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAE1E;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACpF,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,qBAAqB,CAAC;IAC/B,WAAW,CACT,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GACtE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpC,WAAW,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxD;AAED;mFACmF;AACnF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAM1F;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,mBAAmB,EAAE,MAAM,GAAG,SAAS,EACvC,OAAO,EAAE,kBAAkB,GAAG,IAAI,EAClC,iBAAiB,EAAE,OAAO,GACzB,OAAO,CAKT;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,kBAAkB,GAAG,IAAI,GACjC,OAAO,CAAC,IAAI,CAAC,CAyBf;AAqBD;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAUf;AAiDD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAapE"}
@@ -0,0 +1,139 @@
1
+ import { BrowserCdpClient, getContextCookies, setContextCookies, targetBrowserContextId, } from './browser-cdp.js';
2
+ import { clearCookieBackup, proxyFingerprint, readCookieBackup, writeAppliedProxyFingerprint, writeCookieBackup, } from './browser-proxy-state.js';
3
+ /** Render a proxy config into bb-browser `account_create` fields (camelCase, the
4
+ * bb-browser-pro 0.15 contract). Empty when there is no proxy → direct egress. */
5
+ export function proxyCreateFields(proxy) {
6
+ if (!proxy?.server)
7
+ return {};
8
+ const fields = { proxyServer: proxy.server };
9
+ if (proxy.username)
10
+ fields.proxyUsername = proxy.username;
11
+ if (proxy.password)
12
+ fields.proxyPassword = proxy.password;
13
+ return fields;
14
+ }
15
+ /**
16
+ * Whether the account must be rebuilt to honor `desired`. Unknown recorded
17
+ * state (pre-upgrade account, wiped sidecar) reconciles unless BOTH the desired
18
+ * config and the account's visible proxy are direct — the account JSON may
19
+ * carry a stale proxy (or a stale password) that Parall's config no longer
20
+ * matches, and one context rebuild with cookies preserved is invisible.
21
+ */
22
+ export function proxyReconcileNeeded(recordedFingerprint, desired, accountShowsProxy) {
23
+ const want = proxyFingerprint(desired);
24
+ if (recordedFingerprint !== undefined)
25
+ return recordedFingerprint !== want;
26
+ if (desired === null && !accountShowsProxy)
27
+ return false;
28
+ return true;
29
+ }
30
+ /**
31
+ * Rebuild the account's BrowserContext with the desired proxy, preserving its
32
+ * cookie jar. The account MUST exist; callers hold the per-profile op mutex.
33
+ */
34
+ export async function reconcileAccountProxy(host, account, desired) {
35
+ const want = proxyFingerprint(desired);
36
+ host.log.info(`[bb-browser] reconciling proxy for ${account} → ${desired ? redactedProxyLabel(desired) : 'direct egress'} (${want.slice(0, 19)}…)`);
37
+ // A backup from a crashed prior attempt is the authoritative pre-reconcile jar
38
+ // — the live context may already be a half-rebuilt empty one, so never
39
+ // overwrite the backup with a fresh capture in that case. (Invariant: a backup
40
+ // present ⟹ the fingerprint is NOT yet up to date, so this jar is genuinely
41
+ // pre-reconcile, never a stale leftover — see browser-proxy-state.ts.)
42
+ let jar = readCookieBackup(host.homeDir, account);
43
+ if (!jar) {
44
+ jar = await captureAccountCookies(host, account);
45
+ writeCookieBackup(host.homeDir, account, jar);
46
+ }
47
+ else {
48
+ host.log.warn(`[bb-browser] resuming interrupted proxy reconcile for ${account} from cookie backup (${jar.length} cookies)`);
49
+ }
50
+ await host.sendCommand({ method: 'account_delete', account });
51
+ await host.sendCommand({ method: 'account_create', account, ...proxyCreateFields(desired) });
52
+ await settleProxyRebuild(host, account, jar, want);
53
+ host.log.info(`[bb-browser] proxy reconciled for ${account}; ${jar.length} cookies preserved`);
54
+ }
55
+ /**
56
+ * The ONE settle tail of every proxy rebuild — restore the jar into the fresh
57
+ * context, clear the backup, write the applied fingerprint, in exactly that
58
+ * order. The ordering is load-bearing: the fingerprint is written LAST, so a
59
+ * crash at any point leaves it stale → the next open reconciles again — and it
60
+ * can never read a leftover backup as if it were pre-reconcile (invariant: a
61
+ * backup never coexists with an up-to-date fingerprint).
62
+ */
63
+ async function settleProxyRebuild(host, account, jar, want) {
64
+ await restoreAccountCookies(host, account, jar);
65
+ clearCookieBackup(host.homeDir, account);
66
+ writeAppliedProxyFingerprint(host.homeDir, account, want);
67
+ }
68
+ /**
69
+ * Recover a reconcile that crashed after account_delete: the account no longer
70
+ * exists, but a cookie backup does. Called right after a fresh account_create —
71
+ * runs the same settle tail (restore → clear → fingerprint). Without a backup,
72
+ * just records the applied fingerprint for the fresh account.
73
+ */
74
+ export async function settlePendingCookieBackup(host, account, applied) {
75
+ const jar = readCookieBackup(host.homeDir, account);
76
+ if (!jar) {
77
+ writeAppliedProxyFingerprint(host.homeDir, account, applied);
78
+ return;
79
+ }
80
+ host.log.warn(`[bb-browser] restoring ${jar.length} cookies for ${account} from an interrupted proxy reconcile`);
81
+ await settleProxyRebuild(host, account, jar, applied);
82
+ }
83
+ /** Capture the full jar of the account's BrowserContext via CDP. */
84
+ async function captureAccountCookies(host, account) {
85
+ // Ensure the context is live (a stopped account's cookies exist only in
86
+ // bb-browser's JSON until activation re-injects them) — an owned tab forces
87
+ // activation, and its targetId maps to the context id.
88
+ const targetId = await host.targets.ensureOwnedTab(account);
89
+ const { host: cdpHost, port } = await host.cdpEndpoint();
90
+ const cdp = await BrowserCdpClient.connect(cdpHost, port);
91
+ try {
92
+ const contextId = await targetBrowserContextId(cdp, targetId);
93
+ if (!contextId) {
94
+ throw new Error(`owned tab ${targetId} for ${account} reports no browserContextId — refusing to capture the default context's cookies`);
95
+ }
96
+ return await getContextCookies(cdp, contextId);
97
+ }
98
+ finally {
99
+ cdp.close();
100
+ }
101
+ }
102
+ /** Restore a jar into the account's (fresh) BrowserContext via CDP. */
103
+ async function restoreAccountCookies(host, account, cookies) {
104
+ const targetId = await host.targets.ensureOwnedTab(account);
105
+ if (cookies.length === 0)
106
+ return;
107
+ const { host: cdpHost, port } = await host.cdpEndpoint();
108
+ const cdp = await BrowserCdpClient.connect(cdpHost, port);
109
+ try {
110
+ const contextId = await targetBrowserContextId(cdp, targetId);
111
+ if (!contextId) {
112
+ throw new Error(`owned tab ${targetId} for ${account} reports no browserContextId — refusing to restore cookies into the default context`);
113
+ }
114
+ await setContextCookies(cdp, contextId, cookies);
115
+ }
116
+ finally {
117
+ cdp.close();
118
+ }
119
+ }
120
+ /**
121
+ * A log/error-safe proxy label: scheme + host:port ONLY. The username is auth
122
+ * material (customer / region / session selectors) and `server` may itself embed
123
+ * `user:pass@` userinfo, so neither is ever emitted — only the endpoint.
124
+ */
125
+ export function redactedProxyLabel(proxy) {
126
+ const raw = proxy.server;
127
+ try {
128
+ const u = new URL(raw.includes('://') ? raw : `http://${raw}`);
129
+ // u.host is hostname[:port] with NO userinfo; keep the real scheme when present.
130
+ return raw.includes('://') ? `${u.protocol}//${u.host}` : u.host;
131
+ }
132
+ catch {
133
+ // Unparseable — return a FIXED safe placeholder. Do not attempt to salvage a
134
+ // "host" from attacker-controlled malformed text: a hand-rolled strip can still
135
+ // leak a credential or tokenized fragment that URL() rejected but our regex
136
+ // didn't anticipate. If URL() couldn't parse it, emit nothing derived from it.
137
+ return '<proxy>';
138
+ }
139
+ }
@@ -0,0 +1,55 @@
1
+ import type { BrowserProxyConfig } from './browser-profile-manager.js';
2
+ /**
3
+ * Proxy desired-state plumbing for browser profiles.
4
+ *
5
+ * Parall's stored profile config is the ONLY source of truth for a profile's
6
+ * proxy. bb-browser persists whatever proxy an account was created with inside
7
+ * its own account JSON and silently replays it on every context activation —
8
+ * that file must never win over Parall's config. So the daemon records the
9
+ * fingerprint of the proxy it ACTUALLY applied in a Parall-owned sidecar (next
10
+ * to, never inside, bb-browser's files) and reconciles on open/ensure whenever
11
+ * the desired config differs (see browser-proxy-reconcile.ts).
12
+ *
13
+ * Validation policy lives in ONE place — the Go API
14
+ * (server/pkg/model/browser_proxy.go validates at create, update, AND open,
15
+ * which re-checks stored legacy rows before any runtime is dispatched). The
16
+ * daemon does NOT re-implement that policy; normalizeProxyConfig below keeps
17
+ * only the minimal fail-closed guards against a config that slipped past the
18
+ * canonical validator (corrupt row, stale env), in exactly the shapes where
19
+ * the runtime would otherwise LEAK silently instead of failing visibly.
20
+ */
21
+ /** Structured codes for the runtime guards (subset of the Go codes). */
22
+ export declare class BrowserProxyConfigError extends Error {
23
+ readonly code: 'PROXY_SERVER_REQUIRED' | 'PROXY_AUTH_INCOMPLETE' | 'PROXY_SOCKS_AUTH_UNSUPPORTED' | 'PROXY_CREDENTIALS_REQUIRED';
24
+ constructor(code: 'PROXY_SERVER_REQUIRED' | 'PROXY_AUTH_INCOMPLETE' | 'PROXY_SOCKS_AUTH_UNSUPPORTED' | 'PROXY_CREDENTIALS_REQUIRED', message: string);
25
+ }
26
+ /**
27
+ * Minimal fail-closed sanity check on the canonical (Go-validated) proxy DTO.
28
+ * Exactly three guards — each one a shape where bb-browser/Chromium would
29
+ * SILENTLY drop the proxy and egress from the real IP rather than error:
30
+ *
31
+ * 1. credentials without a server (corrupt row — nothing to proxy through);
32
+ * 2. SOCKS5 (Chromium has no SOCKS auth; without auth, bb-browser drops the
33
+ * proxy entirely) — structurally unsupported, matching the API contract;
34
+ * 3. missing/incomplete credentials (bb-browser applies a proxy only when
35
+ * server+username+password are ALL present).
36
+ *
37
+ * Everything else — scheme allowlist, host:port authority shape, length caps —
38
+ * is the Go validator's job and is NOT re-checked here. Returns the config
39
+ * (trimmed server), or null for "no proxy" (direct egress).
40
+ */
41
+ export declare function normalizeProxyConfig(proxy: BrowserProxyConfig | null): BrowserProxyConfig | null;
42
+ /**
43
+ * Stable fingerprint of the COMPLETE normalized proxy config (scheme, host,
44
+ * port, username, password). Only ever logged/persisted as a hash — the
45
+ * password itself never appears in logs or the sidecar.
46
+ */
47
+ export declare function proxyFingerprint(proxy: BrowserProxyConfig | null): string;
48
+ export declare function appliedProxyFingerprint(homeDir: string, account: string): string | undefined;
49
+ export declare function writeAppliedProxyFingerprint(homeDir: string, account: string, fingerprint: string): void;
50
+ /** The pre-reconcile jar for an interrupted reconcile, or null when none. */
51
+ export declare function readCookieBackup(homeDir: string, account: string): Array<Record<string, unknown>> | null;
52
+ export declare function writeCookieBackup(homeDir: string, account: string, cookies: Array<Record<string, unknown>>): void;
53
+ /** Remove the account's cookie backup entirely (post-restore cleanup). */
54
+ export declare function clearCookieBackup(homeDir: string, account: string): void;
55
+ //# sourceMappingURL=browser-proxy-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-proxy-state.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-proxy-state.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAEvE;;;;;;;;;;;;;;;;;;GAkBG;AAEH,wEAAwE;AACxE,qBAAa,uBAAwB,SAAQ,KAAK;IAE9C,QAAQ,CAAC,IAAI,EACT,uBAAuB,GACvB,uBAAuB,GACvB,8BAA8B,GAC9B,4BAA4B;gBAJvB,IAAI,EACT,uBAAuB,GACvB,uBAAuB,GACvB,8BAA8B,GAC9B,4BAA4B,EAChC,OAAO,EAAE,MAAM;CAKlB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,GAAG,kBAAkB,GAAG,IAAI,CAmChG;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,GAAG,MAAM,CAQzE;AAmCD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAG5F;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GAClB,IAAI,CAIN;AAuBD,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,CAKvC;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACtC,IAAI,CAIN;AAED,0EAA0E;AAC1E,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAIxE"}
@@ -0,0 +1,149 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
3
+ import * as path from 'node:path';
4
+ /**
5
+ * Proxy desired-state plumbing for browser profiles.
6
+ *
7
+ * Parall's stored profile config is the ONLY source of truth for a profile's
8
+ * proxy. bb-browser persists whatever proxy an account was created with inside
9
+ * its own account JSON and silently replays it on every context activation —
10
+ * that file must never win over Parall's config. So the daemon records the
11
+ * fingerprint of the proxy it ACTUALLY applied in a Parall-owned sidecar (next
12
+ * to, never inside, bb-browser's files) and reconciles on open/ensure whenever
13
+ * the desired config differs (see browser-proxy-reconcile.ts).
14
+ *
15
+ * Validation policy lives in ONE place — the Go API
16
+ * (server/pkg/model/browser_proxy.go validates at create, update, AND open,
17
+ * which re-checks stored legacy rows before any runtime is dispatched). The
18
+ * daemon does NOT re-implement that policy; normalizeProxyConfig below keeps
19
+ * only the minimal fail-closed guards against a config that slipped past the
20
+ * canonical validator (corrupt row, stale env), in exactly the shapes where
21
+ * the runtime would otherwise LEAK silently instead of failing visibly.
22
+ */
23
+ /** Structured codes for the runtime guards (subset of the Go codes). */
24
+ export class BrowserProxyConfigError extends Error {
25
+ code;
26
+ constructor(code, message) {
27
+ super(`${code}: ${message}`);
28
+ this.code = code;
29
+ this.name = 'BrowserProxyConfigError';
30
+ }
31
+ }
32
+ /**
33
+ * Minimal fail-closed sanity check on the canonical (Go-validated) proxy DTO.
34
+ * Exactly three guards — each one a shape where bb-browser/Chromium would
35
+ * SILENTLY drop the proxy and egress from the real IP rather than error:
36
+ *
37
+ * 1. credentials without a server (corrupt row — nothing to proxy through);
38
+ * 2. SOCKS5 (Chromium has no SOCKS auth; without auth, bb-browser drops the
39
+ * proxy entirely) — structurally unsupported, matching the API contract;
40
+ * 3. missing/incomplete credentials (bb-browser applies a proxy only when
41
+ * server+username+password are ALL present).
42
+ *
43
+ * Everything else — scheme allowlist, host:port authority shape, length caps —
44
+ * is the Go validator's job and is NOT re-checked here. Returns the config
45
+ * (trimmed server), or null for "no proxy" (direct egress).
46
+ */
47
+ export function normalizeProxyConfig(proxy) {
48
+ if (!proxy || !proxy.server?.trim()) {
49
+ if (proxy && (proxy.username || proxy.password)) {
50
+ throw new BrowserProxyConfigError('PROXY_SERVER_REQUIRED', 'proxy credentials are set but the proxy server is empty');
51
+ }
52
+ return null;
53
+ }
54
+ const server = proxy.server.trim();
55
+ const username = proxy.username ?? '';
56
+ const password = proxy.password ?? '';
57
+ if (/^socks5h?:\/\//i.test(server)) {
58
+ throw new BrowserProxyConfigError('PROXY_SOCKS_AUTH_UNSUPPORTED', 'SOCKS5 proxies are not supported by the browser runtime (authenticated HTTP/HTTPS only)');
59
+ }
60
+ if ((username === '') !== (password === '')) {
61
+ throw new BrowserProxyConfigError('PROXY_AUTH_INCOMPLETE', 'proxy_username and proxy_password must be provided together');
62
+ }
63
+ if (username === '') {
64
+ // bb-browser applies a proxy only when server+username+password are ALL
65
+ // present; a credential-less config is silently dropped and the profile
66
+ // egresses from the host's real IP. Fail closed instead of leaking.
67
+ throw new BrowserProxyConfigError('PROXY_CREDENTIALS_REQUIRED', 'the browser runtime cannot apply a credential-less proxy and would silently egress from the real IP');
68
+ }
69
+ return { server, username, password };
70
+ }
71
+ /**
72
+ * Stable fingerprint of the COMPLETE normalized proxy config (scheme, host,
73
+ * port, username, password). Only ever logged/persisted as a hash — the
74
+ * password itself never appears in logs or the sidecar.
75
+ */
76
+ export function proxyFingerprint(proxy) {
77
+ if (!proxy)
78
+ return 'direct';
79
+ const canonical = JSON.stringify({
80
+ server: proxy.server,
81
+ username: proxy.username ?? '',
82
+ password: proxy.password ?? '',
83
+ });
84
+ return `sha256:${createHash('sha256').update(canonical).digest('hex')}`;
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // Applied-proxy sidecar — `<homeDir>/.parall-proxy-state.json`
88
+ //
89
+ // Records, per account, the fingerprint of the proxy configuration the daemon
90
+ // last APPLIED at account_create. Parall-owned (bb-browser never reads it);
91
+ // lives in the profile home so a BYOC reset's home wipe clears it and the
92
+ // hosted pod's S3 snapshot carries it alongside the state it describes.
93
+ // Missing/corrupt reads as "unknown" → reconcile decides conservatively.
94
+ // ---------------------------------------------------------------------------
95
+ const PROXY_STATE_FILE = '.parall-proxy-state.json';
96
+ const COOKIE_BACKUP_FILE = '.parall-proxy-cookie-backup.json';
97
+ // Parse a sidecar as a PLAIN OBJECT. `JSON.parse` of `null`, an array, or a
98
+ // primitive does NOT throw, so a corrupt sidecar would otherwise slip past a bare
99
+ // try/catch and crash the next property access/assignment (`state.applied = …`),
100
+ // wedging proxy recovery. Coerce any non-object shape to {} so a corrupt file
101
+ // reads as empty and the reconcile rebuilds it.
102
+ function readSidecarObject(homeDir, file) {
103
+ try {
104
+ const parsed = JSON.parse(readFileSync(path.join(homeDir, file), 'utf8'));
105
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
106
+ ? parsed
107
+ : {};
108
+ }
109
+ catch {
110
+ return {};
111
+ }
112
+ }
113
+ export function appliedProxyFingerprint(homeDir, account) {
114
+ const value = readSidecarObject(homeDir, PROXY_STATE_FILE).applied?.[account];
115
+ return typeof value === 'string' && value ? value : undefined;
116
+ }
117
+ export function writeAppliedProxyFingerprint(homeDir, account, fingerprint) {
118
+ const state = readSidecarObject(homeDir, PROXY_STATE_FILE);
119
+ state.applied = { ...state.applied, [account]: fingerprint };
120
+ atomicWriteJson(path.join(homeDir, PROXY_STATE_FILE), state);
121
+ }
122
+ /** The pre-reconcile jar for an interrupted reconcile, or null when none. */
123
+ export function readCookieBackup(homeDir, account) {
124
+ const cookies = readSidecarObject(homeDir, COOKIE_BACKUP_FILE).backups?.[account];
125
+ return Array.isArray(cookies) ? cookies : null;
126
+ }
127
+ export function writeCookieBackup(homeDir, account, cookies) {
128
+ updateCookieBackupFile(homeDir, (file) => {
129
+ file.backups = { ...file.backups, [account]: cookies };
130
+ });
131
+ }
132
+ /** Remove the account's cookie backup entirely (post-restore cleanup). */
133
+ export function clearCookieBackup(homeDir, account) {
134
+ updateCookieBackupFile(homeDir, (file) => {
135
+ if (file.backups)
136
+ delete file.backups[account];
137
+ });
138
+ }
139
+ function updateCookieBackupFile(homeDir, mutate) {
140
+ const file = readSidecarObject(homeDir, COOKIE_BACKUP_FILE);
141
+ mutate(file);
142
+ atomicWriteJson(path.join(homeDir, COOKIE_BACKUP_FILE), file);
143
+ }
144
+ function atomicWriteJson(filePath, value) {
145
+ mkdirSync(path.dirname(filePath), { recursive: true });
146
+ const tmp = `${filePath}.tmp-${process.pid}`;
147
+ writeFileSync(tmp, JSON.stringify(value));
148
+ renameSync(tmp, filePath);
149
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Browser quiescence — the Chrome-exit barrier before a hosted pod's FINAL
3
+ * checkpoint.
4
+ *
5
+ * Why it exists: bb-browser's SIGTERM handler sends Chrome SIGTERM and
6
+ * `process.exit(0)`s WITHOUT awaiting it, so "the bb-browser child exited"
7
+ * does not mean the user-data-dir is quiet — an orphaned Chromium may still be
8
+ * flushing cookies/IndexedDB, and a snapshot taken then is torn (SQLite
9
+ * mid-write). The final checkpoint must run only after every browser process
10
+ * is provably gone.
11
+ *
12
+ * Ownership model: the daemon spawns bb-browser with `detached: true`, making
13
+ * it the LEADER of a fresh process group; bb-browser spawns Chrome with
14
+ * `detached: false` (verified against its bundled source), so Chrome and every
15
+ * renderer inherit that group. The group id (= the bb-browser child's pid) is
16
+ * therefore a complete, daemon-owned handle on the whole browser tree:
17
+ *
18
+ * - liveness = `kill(-pgid, 0)` (ESRCH ⇒ no member left);
19
+ * - shutdown = one signal to `-pgid` reaches every member;
20
+ * - safety = POSIX reserves a pid while it names a live process group, so
21
+ * the pgid cannot be recycled out from under us while any member lives.
22
+ * EPERM on the probe means "members exist but none are ours" — our whole
23
+ * tree (same uid, always signalable) must already be dead and the id was
24
+ * recycled — so it reads as exited, and signalling stops immediately.
25
+ *
26
+ * No pid scanning, no /proc string matching, no directory-mtime polling: a
27
+ * profile's group contains exactly the processes its manager spawned, so
28
+ * quiescing profile A can never signal profile B.
29
+ *
30
+ * One CDP refusal probe remains as a CONFIRMATION (the DevTools listener lives
31
+ * inside the Chrome process, so a refused connect independently corroborates
32
+ * process exit) — it is not a second ownership authority.
33
+ *
34
+ * Everything is bounded by ONE absolute deadline. If exit cannot be confirmed
35
+ * by then, the answer is `quiesced: false` and the caller must skip the
36
+ * checkpoint — never guess safety.
37
+ */
38
+ export interface QuiescenceResult {
39
+ quiesced: boolean;
40
+ /** True when exit needed SIGTERM/SIGKILL escalation to the group. */
41
+ escalated: boolean;
42
+ reason?: string;
43
+ }
44
+ export interface QuiescenceLogger {
45
+ info(msg: string): void;
46
+ warn(msg: string): void;
47
+ }
48
+ export interface QuiescenceOptions {
49
+ /**
50
+ * Process-group id of the browser tree (the detached bb-browser child's pid).
51
+ * null → no browser was ever spawned → trivially quiesced.
52
+ */
53
+ pgid: number | null;
54
+ /** Last known CDP endpoint for the confirmation probe; null → skip it. */
55
+ cdpEndpoint?: {
56
+ host: string;
57
+ port: number;
58
+ } | null;
59
+ /** How long to wait for self-exit (bb-browser's own shutdown already ran). */
60
+ exitDeadlineMs?: number;
61
+ /** Grace after each escalation signal. */
62
+ escalateGraceMs?: number;
63
+ /** Absolute ceiling for the whole barrier. */
64
+ totalDeadlineMs?: number;
65
+ pollMs?: number;
66
+ /** Signal seam (default process.kill) — lets tests keep a group "unkillable". */
67
+ kill?: (pid: number, signal: NodeJS.Signals | 0) => void;
68
+ log?: QuiescenceLogger;
69
+ }
70
+ export declare function awaitBrowserQuiescence(opts: QuiescenceOptions): Promise<QuiescenceResult>;
71
+ //# sourceMappingURL=browser-quiescence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-quiescence.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-quiescence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,OAAO,CAAC;IAClB,qEAAqE;IACrE,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,0EAA0E;IAC1E,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACpD,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0CAA0C;IAC1C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8CAA8C;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,CAAC,KAAK,IAAI,CAAC;IACzD,GAAG,CAAC,EAAE,gBAAgB,CAAC;CACxB;AA2DD,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAqD/F"}