@parall/daemon 1.44.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.
- package/bundle/manifest.json +15 -15
- package/bundle/parall-browser-pod.js +29726 -375
- package/bundle/parall-channel-exec.js +2 -0
- package/bundle/parall-claude-agent.js +26446 -384
- package/bundle/parall-codex-agent.js +27490 -1549
- package/bundle/parall-daemon.js +31558 -1617
- package/bundle/parall-openclaw-agent.js +1 -0
- package/dist/browser-pod.d.ts +23 -1
- package/dist/browser-pod.d.ts.map +1 -1
- package/dist/browser-pod.js +104 -34
- package/dist/browser-profile-reconcile.d.ts +21 -0
- package/dist/browser-profile-reconcile.d.ts.map +1 -0
- package/dist/browser-profile-reconcile.js +188 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +229 -2
- package/dist/clip-runtime/browser-cdp.d.ts +40 -0
- package/dist/clip-runtime/browser-cdp.d.ts.map +1 -0
- package/dist/clip-runtime/browser-cdp.js +218 -0
- package/dist/clip-runtime/browser-profile-manager.d.ts +97 -24
- package/dist/clip-runtime/browser-profile-manager.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-manager.js +316 -182
- package/dist/clip-runtime/browser-profile-pool.d.ts +3 -0
- package/dist/clip-runtime/browser-profile-pool.d.ts.map +1 -1
- package/dist/clip-runtime/browser-profile-pool.js +18 -1
- package/dist/clip-runtime/browser-proxy-reconcile.d.ts +77 -0
- package/dist/clip-runtime/browser-proxy-reconcile.d.ts.map +1 -0
- package/dist/clip-runtime/browser-proxy-reconcile.js +139 -0
- package/dist/clip-runtime/browser-proxy-state.d.ts +55 -0
- package/dist/clip-runtime/browser-proxy-state.d.ts.map +1 -0
- package/dist/clip-runtime/browser-proxy-state.js +149 -0
- package/dist/clip-runtime/browser-quiescence.d.ts +71 -0
- package/dist/clip-runtime/browser-quiescence.d.ts.map +1 -0
- package/dist/clip-runtime/browser-quiescence.js +136 -0
- package/dist/clip-runtime/browser-readiness.d.ts +64 -0
- package/dist/clip-runtime/browser-readiness.d.ts.map +1 -0
- package/dist/clip-runtime/browser-readiness.js +161 -0
- package/dist/clip-runtime/browser-state-store.d.ts +13 -2
- package/dist/clip-runtime/browser-state-store.d.ts.map +1 -1
- package/dist/clip-runtime/browser-state-store.js +15 -6
- package/dist/clip-runtime/browser-target-registry.d.ts +143 -0
- package/dist/clip-runtime/browser-target-registry.d.ts.map +1 -0
- package/dist/clip-runtime/browser-target-registry.js +297 -0
- package/dist/clip-runtime/browser-viewer-streamer.d.ts +13 -14
- package/dist/clip-runtime/browser-viewer-streamer.d.ts.map +1 -1
- package/dist/clip-runtime/browser-viewer-streamer.js +11 -63
- package/dist/config.d.ts.map +1 -1
- package/dist/daemon-main.d.ts.map +1 -1
- package/dist/daemon-main.js +3 -1
- package/dist/runtime-bin-resolver.d.ts +7 -1
- package/dist/runtime-bin-resolver.d.ts.map +1 -1
- package/dist/runtime-bin-resolver.js +57 -22
- package/dist/runtimes.d.ts +15 -4
- package/dist/runtimes.d.ts.map +1 -1
- package/dist/runtimes.js +60 -5
- package/dist/supervisor.d.ts +6 -0
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +52 -188
- package/dist/win-lifecycle.d.ts +96 -0
- package/dist/win-lifecycle.d.ts.map +1 -0
- package/dist/win-lifecycle.js +229 -0
- package/dist/win-service.d.ts +119 -0
- package/dist/win-service.d.ts.map +1 -0
- package/dist/win-service.js +226 -0
- package/package.json +8 -6
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
const DEFAULT_EXIT_DEADLINE_MS = 8_000;
|
|
39
|
+
const DEFAULT_ESCALATE_GRACE_MS = 2_000;
|
|
40
|
+
const DEFAULT_TOTAL_DEADLINE_MS = 20_000;
|
|
41
|
+
const DEFAULT_POLL_MS = 100;
|
|
42
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, Math.max(0, ms)));
|
|
43
|
+
/** Probe the group with signal 0. ESRCH → gone; EPERM → gone (see the header:
|
|
44
|
+
* our members are always signalable, so EPERM proves the id was recycled to a
|
|
45
|
+
* foreign group AFTER our tree fully exited). Anything else → assume alive
|
|
46
|
+
* (fail closed: an unreadable probe must not green-light a checkpoint). */
|
|
47
|
+
function groupState(pgid, kill) {
|
|
48
|
+
try {
|
|
49
|
+
kill(-pgid, 0);
|
|
50
|
+
return 'alive';
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
const code = err.code;
|
|
54
|
+
return code === 'ESRCH' || code === 'EPERM' ? 'gone' : 'alive';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Signal the whole group; swallow ESRCH (already gone) and EPERM (recycled to
|
|
58
|
+
* a foreign group — never signal what we do not own). */
|
|
59
|
+
function signalGroup(pgid, signal, kill) {
|
|
60
|
+
try {
|
|
61
|
+
kill(-pgid, signal);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// ESRCH/EPERM: nothing of ours left to signal.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* cdpAccepting: does the browser's DevTools endpoint still accept connections?
|
|
69
|
+
* The listener lives inside the Chrome process, so acceptance ⇒ a browser
|
|
70
|
+
* process survives. Bare fetch with an abort budget; any failure ⇒ refused.
|
|
71
|
+
*/
|
|
72
|
+
async function cdpAccepting(endpoint, budgetMs) {
|
|
73
|
+
if (budgetMs <= 0)
|
|
74
|
+
return false;
|
|
75
|
+
try {
|
|
76
|
+
await fetch(`http://${endpoint.host}:${endpoint.port}/json/version`, {
|
|
77
|
+
signal: AbortSignal.timeout(Math.min(1_000, budgetMs)),
|
|
78
|
+
});
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function awaitBrowserQuiescence(opts) {
|
|
86
|
+
const kill = opts.kill ?? process.kill.bind(process);
|
|
87
|
+
const poll = opts.pollMs ?? DEFAULT_POLL_MS;
|
|
88
|
+
const totalDeadline = Date.now() + (opts.totalDeadlineMs ?? DEFAULT_TOTAL_DEADLINE_MS);
|
|
89
|
+
const capped = (d) => Math.min(d, totalDeadline);
|
|
90
|
+
const remaining = () => totalDeadline - Date.now();
|
|
91
|
+
const log = opts.log;
|
|
92
|
+
if (opts.pgid == null) {
|
|
93
|
+
// Nothing was ever spawned — there is no writer to wait for.
|
|
94
|
+
return { quiesced: true, escalated: false };
|
|
95
|
+
}
|
|
96
|
+
const pgid = opts.pgid;
|
|
97
|
+
// Phase 1 — self-exit: bb-browser's shutdown already SIGTERM'd Chrome; give
|
|
98
|
+
// the group its own deadline to finish flushing and exit.
|
|
99
|
+
const exitDeadline = capped(Date.now() + (opts.exitDeadlineMs ?? DEFAULT_EXIT_DEADLINE_MS));
|
|
100
|
+
let escalated = false;
|
|
101
|
+
while (groupState(pgid, kill) === 'alive') {
|
|
102
|
+
if (Date.now() >= exitDeadline)
|
|
103
|
+
break;
|
|
104
|
+
await sleep(Math.min(poll, remaining()));
|
|
105
|
+
}
|
|
106
|
+
// Phase 2 — escalation: SIGTERM the group, then SIGKILL, each with a capped
|
|
107
|
+
// grace. A group that survives SIGKILL to the deadline is unconfirmable.
|
|
108
|
+
for (const signal of ['SIGTERM', 'SIGKILL']) {
|
|
109
|
+
if (groupState(pgid, kill) === 'gone')
|
|
110
|
+
break;
|
|
111
|
+
escalated = true;
|
|
112
|
+
log?.warn(`browser-quiescence: process group ${pgid} still alive — sending ${signal}`);
|
|
113
|
+
signalGroup(pgid, signal, kill);
|
|
114
|
+
const grace = capped(Date.now() + (opts.escalateGraceMs ?? DEFAULT_ESCALATE_GRACE_MS));
|
|
115
|
+
while (groupState(pgid, kill) === 'alive' && Date.now() < grace) {
|
|
116
|
+
await sleep(Math.min(poll, remaining()));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (groupState(pgid, kill) === 'alive') {
|
|
120
|
+
return {
|
|
121
|
+
quiesced: false,
|
|
122
|
+
escalated,
|
|
123
|
+
reason: `browser process group ${pgid} survived SIGKILL to the deadline`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Phase 3 — CDP refusal confirmation (corroboration, not authority).
|
|
127
|
+
if (opts.cdpEndpoint && (await cdpAccepting(opts.cdpEndpoint, remaining()))) {
|
|
128
|
+
return {
|
|
129
|
+
quiesced: false,
|
|
130
|
+
escalated,
|
|
131
|
+
reason: 'CDP endpoint still accepting connections after the process group exited',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
log?.info(`browser-quiescence: process group ${pgid} exited${escalated ? ' (escalated)' : ''}`);
|
|
135
|
+
return { quiesced: true, escalated };
|
|
136
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { BrowserProxyConfig } from './browser-profile-manager.js';
|
|
2
|
+
import type { BrowserTargetRegistry } from './browser-target-registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* Open readiness — an `open` may only report `running` after the runtime is
|
|
5
|
+
* PROVEN usable, not merely after the processes exist:
|
|
6
|
+
*
|
|
7
|
+
* 1. bb-browser answers and the profile's OWNED page target exists (a
|
|
8
|
+
* tab-scoped command round-trips against exactly that target);
|
|
9
|
+
* 2. when a proxy is configured, a real navigation through the proxy
|
|
10
|
+
* completes without a transport error — `net::ERR_*` (proxy connect, DNS,
|
|
11
|
+
* auth, tunnel, timeout) fails the open with a structured code, while any
|
|
12
|
+
* HTTP status FROM THE TARGET (4xx/5xx included) proves the tunnel works. A
|
|
13
|
+
* 407 is the exception: it is the PROXY demanding auth, not the target, so it
|
|
14
|
+
* fails closed as an auth failure — a wrong-credential proxy never passes.
|
|
15
|
+
*
|
|
16
|
+
* The probe navigates the owned tab to a ~zero-byte Parall-owned endpoint
|
|
17
|
+
* (PRLL_BROWSER_PROXY_PROBE_URL override for tests/ops). A 204/2xx response
|
|
18
|
+
* doesn't commit a navigation, so the page the user opened stays put; the
|
|
19
|
+
* verdict is read from bb-browser's per-tab network log (`failed` +
|
|
20
|
+
* `failureReason` carry Chromium's net error). Bounded polling — observation,
|
|
21
|
+
* never a fixed sleep.
|
|
22
|
+
*/
|
|
23
|
+
export type BrowserReadinessCode = 'BROWSER_PROXY_UNREACHABLE' | 'BROWSER_PROXY_AUTH_FAILED' | 'BROWSER_OPEN_TIMEOUT' | 'BROWSER_RUNTIME_NOT_READY';
|
|
24
|
+
export declare class BrowserReadinessError extends Error {
|
|
25
|
+
readonly code: BrowserReadinessCode;
|
|
26
|
+
constructor(code: BrowserReadinessCode, message: string);
|
|
27
|
+
}
|
|
28
|
+
export interface ReadinessHost {
|
|
29
|
+
log: {
|
|
30
|
+
info(msg: string): void;
|
|
31
|
+
warn(msg: string): void;
|
|
32
|
+
};
|
|
33
|
+
targets: BrowserTargetRegistry;
|
|
34
|
+
sendCommand(request: Record<string, unknown> & {
|
|
35
|
+
method: string;
|
|
36
|
+
account?: string;
|
|
37
|
+
}): Promise<Record<string, unknown>>;
|
|
38
|
+
}
|
|
39
|
+
export interface OpenReadinessOptions {
|
|
40
|
+
proxy: BrowserProxyConfig | null;
|
|
41
|
+
/** Probe endpoint; tests point this at a local observable proxy target. */
|
|
42
|
+
probeUrl?: string;
|
|
43
|
+
deadlineMs?: number;
|
|
44
|
+
pollIntervalMs?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Last-resort proxy readiness probe target when no Parall-owned public endpoint
|
|
48
|
+
* is derivable (e.g. a hosted pod without an injected probe URL). A well-known,
|
|
49
|
+
* globally-reachable, zero-body 204 (Google's connectivity check) — the probe
|
|
50
|
+
* only needs SOME public host the proxy can reach to prove egress; the "any HTTP
|
|
51
|
+
* status = reachable" rule means it never depends on this specific host's
|
|
52
|
+
* content. Overridable via PRLL_BROWSER_PROXY_PROBE_URL; tests always override
|
|
53
|
+
* it to a local observable proxy target, never the public internet.
|
|
54
|
+
*/
|
|
55
|
+
export declare const PUBLIC_PROXY_PROBE_FALLBACK_URL = "https://www.gstatic.com/generate_204";
|
|
56
|
+
/** Throws BrowserReadinessError when the profile runtime is not actually usable. */
|
|
57
|
+
export declare function assertOpenReady(host: ReadinessHost, account: string, opts: OpenReadinessOptions): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Map Chromium's net error on the probe request to a structured readiness
|
|
60
|
+
* failure. HTTP-level failures never reach here (any status = pass), so every
|
|
61
|
+
* failureReason on a proxied probe indicates the proxy path itself broke.
|
|
62
|
+
*/
|
|
63
|
+
export declare function classifyProxyFailure(proxy: BrowserProxyConfig, failureReason: string): BrowserReadinessError;
|
|
64
|
+
//# sourceMappingURL=browser-readiness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-readiness.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-readiness.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAEvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAG1E;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,oBAAoB,GAC5B,2BAA2B,GAC3B,2BAA2B,GAC3B,sBAAsB,GACtB,2BAA2B,CAAC;AAEhC,qBAAa,qBAAsB,SAAQ,KAAK;IAE5C,QAAQ,CAAC,IAAI,EAAE,oBAAoB;gBAA1B,IAAI,EAAE,oBAAoB,EACnC,OAAO,EAAE,MAAM;CAKlB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC1D,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;CACrC;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACjC,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAMD;;;;;;;;GAQG;AACH,eAAO,MAAM,+BAA+B,yCAAyC,CAAC;AAEtF,oFAAoF;AACpF,wBAAsB,eAAe,CACnC,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,IAAI,CAAC,CA6Bf;AA+GD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,kBAAkB,EACzB,aAAa,EAAE,MAAM,GACpB,qBAAqB,CAqCvB"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { redactedProxyLabel } from './browser-proxy-reconcile.js';
|
|
2
|
+
import { sleep } from './subprocess.js';
|
|
3
|
+
export class BrowserReadinessError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(`${code}: ${message}`);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.name = 'BrowserReadinessError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const DEFAULT_PROBE_DEADLINE_MS = 20_000;
|
|
12
|
+
const DEFAULT_POLL_INTERVAL_MS = 250;
|
|
13
|
+
let probeSeq = 0;
|
|
14
|
+
/**
|
|
15
|
+
* Last-resort proxy readiness probe target when no Parall-owned public endpoint
|
|
16
|
+
* is derivable (e.g. a hosted pod without an injected probe URL). A well-known,
|
|
17
|
+
* globally-reachable, zero-body 204 (Google's connectivity check) — the probe
|
|
18
|
+
* only needs SOME public host the proxy can reach to prove egress; the "any HTTP
|
|
19
|
+
* status = reachable" rule means it never depends on this specific host's
|
|
20
|
+
* content. Overridable via PRLL_BROWSER_PROXY_PROBE_URL; tests always override
|
|
21
|
+
* it to a local observable proxy target, never the public internet.
|
|
22
|
+
*/
|
|
23
|
+
export const PUBLIC_PROXY_PROBE_FALLBACK_URL = 'https://www.gstatic.com/generate_204';
|
|
24
|
+
/** Throws BrowserReadinessError when the profile runtime is not actually usable. */
|
|
25
|
+
export async function assertOpenReady(host, account, opts) {
|
|
26
|
+
// 1. Owned target exists + a command round-trips against exactly that target.
|
|
27
|
+
const targetId = await host.targets.resolveCommandTarget(account);
|
|
28
|
+
try {
|
|
29
|
+
await host.sendCommand({
|
|
30
|
+
method: 'network',
|
|
31
|
+
action: 'requests',
|
|
32
|
+
account,
|
|
33
|
+
tabId: targetId,
|
|
34
|
+
limit: 1,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
throw new BrowserReadinessError('BROWSER_RUNTIME_NOT_READY', `profile page did not answer a command: ${err instanceof Error ? err.message : String(err)}`);
|
|
39
|
+
}
|
|
40
|
+
// 2. Proxy reachability — only when a proxy is configured.
|
|
41
|
+
if (!opts.proxy)
|
|
42
|
+
return;
|
|
43
|
+
if (!opts.probeUrl) {
|
|
44
|
+
// No probe endpoint available (misconfigured host) — surface loudly rather
|
|
45
|
+
// than silently skipping the check the caller believes happened.
|
|
46
|
+
throw new BrowserReadinessError('BROWSER_RUNTIME_NOT_READY', 'no proxy probe endpoint configured (PRLL_BROWSER_PROXY_PROBE_URL)');
|
|
47
|
+
}
|
|
48
|
+
await probeProxy(host, account, opts);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Probe the proxy on a SACRIFICIAL profile-owned tab, never the user's page
|
|
52
|
+
* (fix #5). A fresh `tab_new {account}` lands in the profile's own
|
|
53
|
+
* BrowserContext (so it egresses through the same proxy), gets navigated to the
|
|
54
|
+
* ~zero-byte probe URL, and is CLOSED in `finally` — the user's primary tab
|
|
55
|
+
* keeps whatever URL it was on. The probe URL must be a public, proxy-resolvable
|
|
56
|
+
* endpoint (see the daemon/pod probe-URL resolution); a cluster-internal host
|
|
57
|
+
* would never resolve through an external proxy.
|
|
58
|
+
*/
|
|
59
|
+
async function probeProxy(host, account, opts) {
|
|
60
|
+
const proxy = opts.proxy;
|
|
61
|
+
const tag = `prllprobe${(probeSeq++).toString(36)}${process.pid.toString(36)}`;
|
|
62
|
+
const probeUrl = withProbeTag(opts.probeUrl, tag);
|
|
63
|
+
const deadline = Date.now() + (opts.deadlineMs ?? DEFAULT_PROBE_DEADLINE_MS);
|
|
64
|
+
const interval = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
65
|
+
const created = await host.sendCommand({ method: 'tab_new', account, url: 'about:blank' });
|
|
66
|
+
const probeTabId = typeof created.tabId === 'string' ? created.tabId : undefined;
|
|
67
|
+
if (!probeTabId) {
|
|
68
|
+
throw new BrowserReadinessError('BROWSER_RUNTIME_NOT_READY', 'could not open a probe tab in the profile context');
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await host.sendCommand({ method: 'open', account, tabId: probeTabId, url: probeUrl });
|
|
72
|
+
while (Date.now() < deadline) {
|
|
73
|
+
const entry = await findProbeEntry(host, account, probeTabId, tag);
|
|
74
|
+
if (entry) {
|
|
75
|
+
// A 407 is the PROXY itself demanding auth — it proves the proxy is
|
|
76
|
+
// reachable but NOT that we authenticated, so it must fail closed as an
|
|
77
|
+
// auth failure, never count as a working tunnel (bad-credential case).
|
|
78
|
+
if (entry.status === 407) {
|
|
79
|
+
throw new BrowserReadinessError('BROWSER_PROXY_AUTH_FAILED', `proxy ${redactedProxyLabel(proxy)} returned 407 (authentication required/rejected)`);
|
|
80
|
+
}
|
|
81
|
+
// Any OTHER HTTP status — 4xx/5xx included — means the request traversed
|
|
82
|
+
// the proxy and got an answer from the TARGET: network path OK. (A 204
|
|
83
|
+
// probe also emits a benign ERR_ABORTED after the response; status wins.)
|
|
84
|
+
if (typeof entry.status === 'number') {
|
|
85
|
+
host.log.info(`[bb-browser] proxy probe ok for ${account} via ${redactedProxyLabel(proxy)} (HTTP ${entry.status})`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (entry.failed === true && typeof entry.failureReason === 'string') {
|
|
89
|
+
throw classifyProxyFailure(proxy, entry.failureReason);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
await sleep(interval);
|
|
93
|
+
}
|
|
94
|
+
throw new BrowserReadinessError('BROWSER_OPEN_TIMEOUT', `proxy probe through ${redactedProxyLabel(proxy)} did not complete within ${opts.deadlineMs ?? DEFAULT_PROBE_DEADLINE_MS}ms`);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
// Always tear down the sacrificial tab — a leaked probe tab would accumulate
|
|
98
|
+
// and could be mistaken for the profile's primary. Best-effort: a close
|
|
99
|
+
// failure must not mask the probe verdict.
|
|
100
|
+
try {
|
|
101
|
+
await host.sendCommand({ method: 'close', account, tabId: probeTabId });
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
host.log.warn(`[bb-browser] failed to close proxy probe tab for ${account}: ${String(err)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function findProbeEntry(host, account, targetId, tag) {
|
|
109
|
+
const result = await host.sendCommand({
|
|
110
|
+
method: 'network',
|
|
111
|
+
action: 'requests',
|
|
112
|
+
account,
|
|
113
|
+
tabId: targetId,
|
|
114
|
+
filter: tag,
|
|
115
|
+
});
|
|
116
|
+
const requests = Array.isArray(result.networkRequests)
|
|
117
|
+
? result.networkRequests
|
|
118
|
+
: [];
|
|
119
|
+
// Newest matching entry wins (retries reuse the tab).
|
|
120
|
+
for (let i = requests.length - 1; i >= 0; i--) {
|
|
121
|
+
const r = requests[i];
|
|
122
|
+
if (typeof r.url === 'string' && r.url.includes(tag)) {
|
|
123
|
+
return {
|
|
124
|
+
status: typeof r.status === 'number' ? r.status : undefined,
|
|
125
|
+
failed: r.failed === true,
|
|
126
|
+
failureReason: typeof r.failureReason === 'string' ? r.failureReason : undefined,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Map Chromium's net error on the probe request to a structured readiness
|
|
134
|
+
* failure. HTTP-level failures never reach here (any status = pass), so every
|
|
135
|
+
* failureReason on a proxied probe indicates the proxy path itself broke.
|
|
136
|
+
*/
|
|
137
|
+
export function classifyProxyFailure(proxy, failureReason) {
|
|
138
|
+
const reason = failureReason.replace(/^net::/, '');
|
|
139
|
+
const label = redactedProxyLabel(proxy);
|
|
140
|
+
// Chromium surfaces proxy-auth failure under several codes: ERR_PROXY_AUTH_*
|
|
141
|
+
// (challenge/requested), and ERR_{INVALID,MISSING}_AUTH_CREDENTIALS when the
|
|
142
|
+
// proxy REJECTS the supplied credentials — the wrong-password case, which does
|
|
143
|
+
// NOT contain "PROXY_AUTH". Fail closed as an auth failure (not merely
|
|
144
|
+
// "unreachable") so a bad credential never rides through as a transport blip.
|
|
145
|
+
if (/PROXY_AUTH|INVALID_AUTH_CREDENTIALS|MISSING_AUTH_CREDENTIALS|ERR_TOO_MANY_RETRIES/i.test(reason)) {
|
|
146
|
+
return new BrowserReadinessError('BROWSER_PROXY_AUTH_FAILED', `proxy ${label} rejected the credentials (${reason})`);
|
|
147
|
+
}
|
|
148
|
+
if (/TIMED_OUT/i.test(reason)) {
|
|
149
|
+
return new BrowserReadinessError('BROWSER_PROXY_UNREACHABLE', `proxy ${label} timed out (${reason})`);
|
|
150
|
+
}
|
|
151
|
+
if (/NAME_NOT_RESOLVED|NAME_RESOLUTION_FAILED/i.test(reason)) {
|
|
152
|
+
return new BrowserReadinessError('BROWSER_PROXY_UNREACHABLE', `proxy host in ${label} did not resolve (${reason})`);
|
|
153
|
+
}
|
|
154
|
+
// ERR_PROXY_CONNECTION_FAILED / ERR_TUNNEL_CONNECTION_FAILED /
|
|
155
|
+
// ERR_SOCKS_CONNECTION_FAILED / ERR_CONNECTION_REFUSED / ... — with a proxy
|
|
156
|
+
// configured, a transport error on the probe is a proxy-path failure.
|
|
157
|
+
return new BrowserReadinessError('BROWSER_PROXY_UNREACHABLE', `could not reach the network through proxy ${label} (${reason})`);
|
|
158
|
+
}
|
|
159
|
+
function withProbeTag(probeUrl, tag) {
|
|
160
|
+
return probeUrl.includes('?') ? `${probeUrl}&${tag}=1` : `${probeUrl}?${tag}=1`;
|
|
161
|
+
}
|
|
@@ -35,8 +35,16 @@ export interface BrowserStateLogger {
|
|
|
35
35
|
warn(msg: string): void;
|
|
36
36
|
error(msg: string): void;
|
|
37
37
|
}
|
|
38
|
-
/**
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* buildStateKey is the SSOT key layout, mirrored by the Go controller
|
|
40
|
+
* (state_gc.go stateObjectKeyForEpoch). The state EPOCH (the profile's
|
|
41
|
+
* reset_generation at assignment time) scopes the key: epoch 0 is the legacy
|
|
42
|
+
* un-suffixed key so pre-epoch profiles need no data migration; each Reset bumps
|
|
43
|
+
* the epoch, so a new pod hydrates a fresh (empty) key while every pre-reset pod
|
|
44
|
+
* keeps reading/writing only its own superseded object — the old state is
|
|
45
|
+
* structurally unreachable, no tombstone or write-ordering needed.
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildStateKey(orgId: string, profileId: string, epoch?: number): string;
|
|
40
48
|
/** Thrown by a StateBackend.put when a conditional write precondition fails (S3
|
|
41
49
|
* 412). The store treats it as "lost the CAS race" and aborts the checkpoint. */
|
|
42
50
|
export declare class PreconditionFailedError extends Error {
|
|
@@ -69,6 +77,9 @@ export interface BrowserStateStoreConfig {
|
|
|
69
77
|
profileId: string;
|
|
70
78
|
/** Lease activation generation — the monotonic fencing token (design §3.2). */
|
|
71
79
|
generation: number;
|
|
80
|
+
/** State epoch (reset_generation at assignment) — scopes the S3 key, see
|
|
81
|
+
* buildStateKey. Missing/0 → the legacy un-suffixed key. */
|
|
82
|
+
epoch?: number;
|
|
72
83
|
/** bb-browser user-data dir (BB_BROWSER_HOME) to snapshot/restore. */
|
|
73
84
|
homeDir: string;
|
|
74
85
|
/** Optional S3 endpoint (MinIO/dev); empty → AWS S3. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-state-store.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-state-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAaH,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAgCD
|
|
1
|
+
{"version":3,"file":"browser-state-store.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-state-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -35,8 +35,8 @@ import { createHash } from 'node:crypto';
|
|
|
35
35
|
import { mkdirSync } from 'node:fs';
|
|
36
36
|
import { gunzipSync, gzipSync } from 'node:zlib';
|
|
37
37
|
import { GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3';
|
|
38
|
-
/** S3 object user-metadata keys. Lowercase; the Go
|
|
39
|
-
*
|
|
38
|
+
/** S3 object user-metadata keys. Lowercase; the Go side writes them in lockstep
|
|
39
|
+
* (state_gc.go). SSOT. */
|
|
40
40
|
const META_GENERATION = 'generation';
|
|
41
41
|
const META_SHA256 = 'sha256';
|
|
42
42
|
const META_FINAL = 'final';
|
|
@@ -62,9 +62,18 @@ const ARCHIVE_EXCLUDES = [
|
|
|
62
62
|
'*/component_crx_cache/*',
|
|
63
63
|
'*/Crashpad/*',
|
|
64
64
|
];
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
/**
|
|
66
|
+
* buildStateKey is the SSOT key layout, mirrored by the Go controller
|
|
67
|
+
* (state_gc.go stateObjectKeyForEpoch). The state EPOCH (the profile's
|
|
68
|
+
* reset_generation at assignment time) scopes the key: epoch 0 is the legacy
|
|
69
|
+
* un-suffixed key so pre-epoch profiles need no data migration; each Reset bumps
|
|
70
|
+
* the epoch, so a new pod hydrates a fresh (empty) key while every pre-reset pod
|
|
71
|
+
* keeps reading/writing only its own superseded object — the old state is
|
|
72
|
+
* structurally unreachable, no tombstone or write-ordering needed.
|
|
73
|
+
*/
|
|
74
|
+
export function buildStateKey(orgId, profileId, epoch = 0) {
|
|
75
|
+
const base = `${STATE_KEY_PREFIX}/${orgId}/${profileId}`;
|
|
76
|
+
return epoch > 0 ? `${base}/e${epoch}/${STATE_OBJECT}` : `${base}/${STATE_OBJECT}`;
|
|
68
77
|
}
|
|
69
78
|
/** Thrown by a StateBackend.put when a conditional write precondition fails (S3
|
|
70
79
|
* 412). The store treats it as "lost the CAS race" and aborts the checkpoint. */
|
|
@@ -87,7 +96,7 @@ export class BrowserStateStore {
|
|
|
87
96
|
lastUploadedSha = null;
|
|
88
97
|
constructor(cfg, backend) {
|
|
89
98
|
this.cfg = cfg;
|
|
90
|
-
this.key = buildStateKey(cfg.orgId, cfg.profileId);
|
|
99
|
+
this.key = buildStateKey(cfg.orgId, cfg.profileId, cfg.epoch ?? 0);
|
|
91
100
|
// Injected backend wins (tests). Otherwise build a live S3 backend iff a
|
|
92
101
|
// bucket is configured; no bucket → disabled (stateless pod).
|
|
93
102
|
this.backend = backend ?? (cfg.bucket ? new S3Backend(buildS3Client(cfg), cfg.bucket) : null);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrowserTargetRegistry — binds every browser command to a page target OWNED by
|
|
3
|
+
* the profile's bb-browser account, so no command can fall through to
|
|
4
|
+
* bb-browser's account-blind global routing.
|
|
5
|
+
*
|
|
6
|
+
* WHY. bb-browser-pro 0.15.0 resolves a tab-less command via
|
|
7
|
+
* `ensurePageTarget(undefined)` → global `currentTargetId` → `targets[0]`
|
|
8
|
+
* (verified in its daemon source; `request.account` is never consulted). Chrome
|
|
9
|
+
* is launched with a literal `about:blank` positional tab in the DEFAULT
|
|
10
|
+
* browser context — no account cookies, no account proxy — and nothing ever
|
|
11
|
+
* closes it, so it IS `targets[0]` on a fresh Chromium. Worse, the daemon's own
|
|
12
|
+
* tab-less `tab_list` warm-up is dispatched through `ensurePageTarget` too and
|
|
13
|
+
* pins the global `currentTargetId` to that blank page. Any agent command sent
|
|
14
|
+
* without an explicit `tabId` (navigate/get/click/screenshot/eval/...) then
|
|
15
|
+
* executes against a cookie-less, proxy-less page — including real-IP egress
|
|
16
|
+
* for proxy-configured profiles. The registry closes this by resolving an
|
|
17
|
+
* OWNED target (per `tab_list`'s per-tab `account` attribution) and stamping
|
|
18
|
+
* its full CDP targetId on every tab-addressed command.
|
|
19
|
+
*
|
|
20
|
+
* Ownership ground truth is bb-browser's own `tab_list` attribution (targetId →
|
|
21
|
+
* account, derived from its BrowserContext bookkeeping), so a recreated tab —
|
|
22
|
+
* `tab_new {account}` routes through `createTabInContext` → `ensureContext` —
|
|
23
|
+
* always lands in the profile's BrowserContext with its cookies re-injected,
|
|
24
|
+
* never the default context.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Thrown when a command's target cannot be bound to a profile-owned page: a
|
|
28
|
+
* caller-supplied ref that is not the profile's own tab (foreign tab, numeric
|
|
29
|
+
* global index, stale id), or an owned page that cannot be (re)created. The
|
|
30
|
+
* command is NEVER retargeted at another page — fail closed.
|
|
31
|
+
*/
|
|
32
|
+
export declare class BrowserTargetError extends Error {
|
|
33
|
+
readonly code: 'BROWSER_TARGET_FOREIGN' | 'BROWSER_TARGET_UNAVAILABLE';
|
|
34
|
+
constructor(message: string, code: 'BROWSER_TARGET_FOREIGN' | 'BROWSER_TARGET_UNAVAILABLE');
|
|
35
|
+
}
|
|
36
|
+
/** The slice of BrowserProfileManager the registry drives (account-scoped /command). */
|
|
37
|
+
export interface TargetRegistryHost {
|
|
38
|
+
sendCommand(request: Record<string, unknown> & {
|
|
39
|
+
method: string;
|
|
40
|
+
account?: string;
|
|
41
|
+
}): Promise<Record<string, unknown>>;
|
|
42
|
+
}
|
|
43
|
+
/** A profile-owned page tab as reported by `tab_list` (account-attributed). */
|
|
44
|
+
export interface OwnedTab {
|
|
45
|
+
/** Full CDP targetId (`tabId` in bb-browser responses). */
|
|
46
|
+
targetId: string;
|
|
47
|
+
/** bb-browser short tab id (`tab` in responses). */
|
|
48
|
+
shortId?: string;
|
|
49
|
+
url?: string;
|
|
50
|
+
active?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/** Tunable timings for domain-tab navigation commit (injectable so tests don't
|
|
53
|
+
* wait the full production budget). */
|
|
54
|
+
export interface TargetRegistryOptions {
|
|
55
|
+
/** How long to wait for a created domain tab to commit navigation (default 10s). */
|
|
56
|
+
commitTimeoutMs?: number;
|
|
57
|
+
/** Post-commit settle before the tab is used (default 750ms). */
|
|
58
|
+
commitSettleMs?: number;
|
|
59
|
+
/** Poll interval while waiting for commit (default 300ms). */
|
|
60
|
+
commitPollMs?: number;
|
|
61
|
+
}
|
|
62
|
+
export declare class BrowserTargetRegistry {
|
|
63
|
+
private readonly host;
|
|
64
|
+
private readonly opts;
|
|
65
|
+
constructor(host: TargetRegistryHost, opts?: TargetRegistryOptions);
|
|
66
|
+
/** `tab_list` rows owned by the account (per-tab `account` attribution). */
|
|
67
|
+
ownedTabs(account: string): Promise<OwnedTab[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the full CDP targetId a command must run against.
|
|
70
|
+
*
|
|
71
|
+
* With `ref` (caller-supplied `tabId`/`tab`): it must be one of the account's
|
|
72
|
+
* OWN tabs (full targetId or short id) — anything else throws
|
|
73
|
+
* BrowserTargetError. Numeric refs are rejected outright: bb-browser resolves
|
|
74
|
+
* bare numbers as indices into the GLOBAL page-target array, which can address
|
|
75
|
+
* another context's page.
|
|
76
|
+
*
|
|
77
|
+
* Without `ref`: the owned tab Chrome marks active, else the first owned
|
|
78
|
+
* tab, else a fresh `about:blank` created INSIDE the account's
|
|
79
|
+
* BrowserContext (never the default context). Stateless — derived from the
|
|
80
|
+
* live tab_list every time.
|
|
81
|
+
*/
|
|
82
|
+
resolveCommandTarget(account: string, ref?: unknown): Promise<string>;
|
|
83
|
+
/**
|
|
84
|
+
* Validate + canonicalize a caller-supplied tab ref against owned tabs only.
|
|
85
|
+
* The ref is matched as a string against owned full/short ids and NEVER
|
|
86
|
+
* forwarded raw — so bb-browser's numeric-index fallback (which indexes into
|
|
87
|
+
* the GLOBAL page array, any context) is unreachable.
|
|
88
|
+
*/
|
|
89
|
+
private resolveExplicitRef;
|
|
90
|
+
/**
|
|
91
|
+
* Ensure the account owns at least one page tab, returning its targetId. A
|
|
92
|
+
* fresh account (or one whose pages were all closed) gets an `about:blank`
|
|
93
|
+
* tab created in ITS BrowserContext via `tab_new {account}` — bb-browser's
|
|
94
|
+
* `createTabInContext` re-activates the context (cookies re-injected) when it
|
|
95
|
+
* was disposed, so recovery never lands in the default context.
|
|
96
|
+
*/
|
|
97
|
+
ensureOwnedTab(account: string): Promise<string>;
|
|
98
|
+
private createOwnedTab;
|
|
99
|
+
/** Owned tab currently on `host` (www-tolerant), optionally only a specific ref. */
|
|
100
|
+
findOwnedTabOnHost(account: string, host: string, onlyTargetId?: string): Promise<string | undefined>;
|
|
101
|
+
/** Whether any owned tab is already on `url` (same resource, http→https tolerant). */
|
|
102
|
+
findOwnedTabMatchingUrl(account: string, url: string): Promise<string | undefined>;
|
|
103
|
+
/**
|
|
104
|
+
* Account-scoped tab-by-domain resolution for `eval {domain}` (bb-browser's
|
|
105
|
+
* own `resolveTabByDomain` is account-blind — the reason this exists). Two
|
|
106
|
+
* properties the bare `tab_new` + eval approach lacked, both caught on the
|
|
107
|
+
* staging closed-loop E2E (2026-06-04):
|
|
108
|
+
*
|
|
109
|
+
* 1. Reuse: an existing account tab already on the domain is reused instead
|
|
110
|
+
* of opening a new tab per eval (upstream reuses matching tabs too).
|
|
111
|
+
* 2. Load wait: after creating a tab, wait for the navigation to commit
|
|
112
|
+
* before eval — upstream waits (~10s poll + settle); without it the clip
|
|
113
|
+
* script races `about:blank` and relative fetches fail
|
|
114
|
+
* ("Failed to parse URL from /hot.json").
|
|
115
|
+
*/
|
|
116
|
+
resolveDomainTab(account: string, domain: string): Promise<string | undefined>;
|
|
117
|
+
/** Close a tab we created but could not commit, so it does not linger as a
|
|
118
|
+
* residual owned tab. Best-effort — a close failure (already gone / rejected)
|
|
119
|
+
* is swallowed. bb-browser's close command is `close` (→ Target.closeTarget). */
|
|
120
|
+
private closeTabBestEffort;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Commands that take no page target at all — everything else is tab-addressed
|
|
124
|
+
* and MUST carry an owned tabId. `tab_new`, `site_info`, and the `site_*` listing
|
|
125
|
+
* commands resolve/create their page inside the account's context upstream
|
|
126
|
+
* (createTabInContext / account-scoped adapter execution), so they pass `account`
|
|
127
|
+
* instead of a pinned tab. NOTE `tab_list` IS dispatched through bb-browser's
|
|
128
|
+
* `ensurePageTarget` (its global-current-tab side effect is harmless once every
|
|
129
|
+
* real command pins its own target). `site_run` is deliberately NOT tab-less:
|
|
130
|
+
* the manager resolves the adapter's domain via site_info and pins an owned tab
|
|
131
|
+
* (or fails closed for a domain-less adapter), closing the old global-current-tab
|
|
132
|
+
* fallthrough — see BrowserProfileManager.buildCommandRequest.
|
|
133
|
+
*/
|
|
134
|
+
export declare const TABLESS_COMMANDS: Set<string>;
|
|
135
|
+
/** Host comparison tolerant of a `www.` prefix on either side. */
|
|
136
|
+
export declare function hostsMatch(a: string, b: string): boolean;
|
|
137
|
+
export declare function normalizeDomainUrl(domain: string): string;
|
|
138
|
+
export declare function comparableUrl(url: string): {
|
|
139
|
+
protocol: string;
|
|
140
|
+
host: string;
|
|
141
|
+
target: string;
|
|
142
|
+
} | null;
|
|
143
|
+
//# sourceMappingURL=browser-target-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-target-registry.d.ts","sourceRoot":"","sources":["../../src/clip-runtime/browser-target-registry.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAGzC,QAAQ,CAAC,IAAI,EAAE,wBAAwB,GAAG,4BAA4B;gBADtE,OAAO,EAAE,MAAM,EACN,IAAI,EAAE,wBAAwB,GAAG,4BAA4B;CAKzE;AAED,wFAAwF;AACxF,MAAM,WAAW,kBAAkB;IACjC,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;CACrC;AAED,+EAA+E;AAC/E,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;wCACwC;AACxC,MAAM,WAAW,qBAAqB;IACpC,oFAAoF;IACpF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,qBAAqB;IAM9B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBADJ,IAAI,EAAE,kBAAkB,EACxB,IAAI,GAAE,qBAA0B;IAGnD,4EAA4E;IACtE,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAoBrD;;;;;;;;;;;;;OAaG;IACG,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAU3E;;;;;OAKG;YACW,kBAAkB;IAgBhC;;;;;;OAMG;IACG,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAOxC,cAAc;IAqB5B,oFAAoF;IAC9E,kBAAkB,CACtB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAc9B,sFAAsF;IAChF,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAiBxF;;;;;;;;;;;;OAYG;IACG,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IA4CpF;;sFAEkF;YACpE,kBAAkB;CAOjC;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,aAU3B,CAAC;AAEH,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAGxD;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,GACV;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAW3D"}
|