unbrowse 2.12.2 → 2.12.7
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/README.md +8 -44
- package/dist/cli.js +514 -20723
- package/package.json +4 -10
- package/runtime-src/api/routes.ts +15 -801
- package/runtime-src/auth/index.ts +32 -142
- package/runtime-src/capture/index.ts +101 -436
- package/runtime-src/cli.ts +371 -956
- package/runtime-src/client/index.ts +29 -622
- package/runtime-src/execution/index.ts +85 -345
- package/runtime-src/graph/index.ts +10 -128
- package/runtime-src/intent-match.ts +27 -27
- package/runtime-src/kuri/client.ts +82 -543
- package/runtime-src/orchestrator/index.ts +462 -2246
- package/runtime-src/reverse-engineer/index.ts +22 -220
- package/runtime-src/runtime/local-server.ts +16 -149
- package/runtime-src/runtime/paths.ts +5 -9
- package/runtime-src/runtime/setup.ts +1 -52
- package/runtime-src/server.ts +11 -6
- package/runtime-src/transform/schema-hints.ts +358 -0
- package/runtime-src/types/skill.ts +2 -49
- package/runtime-src/verification/index.ts +0 -15
- package/runtime-src/version.ts +13 -13
- package/vendor/kuri/darwin-arm64/kuri +0 -0
- package/vendor/kuri/darwin-x64/kuri +0 -0
- package/vendor/kuri/linux-arm64/kuri +0 -0
- package/vendor/kuri/linux-x64/kuri +0 -0
- package/bin/unbrowse-wrapper.mjs +0 -39
- package/bin/unbrowse.js +0 -38
- package/runtime-src/analytics-session.ts +0 -33
- package/runtime-src/api/browse-index.ts +0 -254
- package/runtime-src/api/browse-session.ts +0 -179
- package/runtime-src/api/browse-submit.ts +0 -455
- package/runtime-src/auth/runtime.ts +0 -116
- package/runtime-src/browser/index.ts +0 -635
- package/runtime-src/browser/types.ts +0 -41
- package/runtime-src/capture/prefetch.ts +0 -122
- package/runtime-src/capture/rsc.ts +0 -45
- package/runtime-src/cli/shortcuts.ts +0 -273
- package/runtime-src/client/graph-client.ts +0 -99
- package/runtime-src/execution/robots.ts +0 -167
- package/runtime-src/execution/search-forms.ts +0 -188
- package/runtime-src/graph/planner.ts +0 -411
- package/runtime-src/graph/session.ts +0 -294
- package/runtime-src/graph/trace-store.ts +0 -136
- package/runtime-src/indexer/index.ts +0 -480
- package/runtime-src/orchestrator/browser-agent.ts +0 -374
- package/runtime-src/orchestrator/dag-advisor.ts +0 -59
- package/runtime-src/orchestrator/dag-feedback.ts +0 -256
- package/runtime-src/orchestrator/first-pass-action.ts +0 -362
- package/runtime-src/orchestrator/passive-publish.ts +0 -152
- package/runtime-src/orchestrator/timing-economics.ts +0 -80
- package/runtime-src/payments/cascade.ts +0 -137
- package/runtime-src/payments/index.ts +0 -268
- package/runtime-src/payments/wallet.ts +0 -33
- package/runtime-src/reverse-engineer/description-prompt.ts +0 -132
- package/runtime-src/router.ts +0 -17
- package/runtime-src/runtime/browser-access.ts +0 -11
- package/runtime-src/runtime/browser-host.ts +0 -48
- package/runtime-src/runtime/lifecycle.ts +0 -17
- package/runtime-src/runtime/supervisor.ts +0 -69
- package/runtime-src/single-binary.ts +0 -141
- package/runtime-src/telemetry.ts +0 -253
- package/runtime-src/verification/matrix.ts +0 -30
- package/scripts/postinstall.mjs +0 -81
|
@@ -2,19 +2,6 @@ import * as kuri from "../kuri/client.js";
|
|
|
2
2
|
import { nanoid } from "nanoid";
|
|
3
3
|
import { getRegistrableDomain } from "../domain.js";
|
|
4
4
|
import { log } from "../logger.js";
|
|
5
|
-
import type { BrowserAccessConfig } from "../runtime/browser-access.js";
|
|
6
|
-
import { DEFAULT_BROWSER_ACCESS } from "../runtime/browser-access.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Check whether the current BrowserAccessConfig allows browser-based capture.
|
|
10
|
-
* Returns true when the default or fallback path is "unbrowse" or "direct"
|
|
11
|
-
* (i.e. not proxy-only), meaning we can launch a local browser via Kuri.
|
|
12
|
-
*/
|
|
13
|
-
export function isBrowserAccessAvailable(
|
|
14
|
-
config: BrowserAccessConfig = DEFAULT_BROWSER_ACCESS,
|
|
15
|
-
): boolean {
|
|
16
|
-
return config.default_path !== "proxy" || config.fallback_path !== "proxy";
|
|
17
|
-
}
|
|
18
5
|
|
|
19
6
|
// BUG-GC-012: Use a real Chrome UA — HeadlessChrome is actively blocked by Google and others.
|
|
20
7
|
const CHROME_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
@@ -27,34 +14,10 @@ const waitQueue: Array<() => void> = [];
|
|
|
27
14
|
// Active tab registry — tracked for graceful shutdown
|
|
28
15
|
const activeTabRegistry = new Set<string>();
|
|
29
16
|
|
|
30
|
-
// Tracks tabs where scriptInject has been registered (persistent across navigations)
|
|
31
|
-
const interceptorInjectedTabs = new Set<string>();
|
|
32
|
-
|
|
33
17
|
// Hard timeout per capture: 90s prevents stuck tabs from holding slots forever
|
|
34
18
|
const CAPTURE_TIMEOUT_MS = 90_000;
|
|
35
19
|
const CAPTURE_NAV_TIMEOUT_MS = 20_000;
|
|
36
20
|
|
|
37
|
-
/**
|
|
38
|
-
* Races `fn()` against an AbortSignal so that each CDP phase exits immediately
|
|
39
|
-
* when the overall capture timeout fires — instead of waiting for kuri's own
|
|
40
|
-
* 30s per-request timeout to expire (which caused 120s+ hangs, bug #113).
|
|
41
|
-
*/
|
|
42
|
-
export function withBrowserPhaseTimeout<T>(
|
|
43
|
-
signal: AbortSignal,
|
|
44
|
-
label: string,
|
|
45
|
-
fn: () => Promise<T>,
|
|
46
|
-
): Promise<T> {
|
|
47
|
-
if (signal.aborted) return Promise.reject(new Error(`capture phase '${label}' timed out`));
|
|
48
|
-
return new Promise<T>((resolve, reject) => {
|
|
49
|
-
const onAbort = () => reject(new Error(`capture phase '${label}' timed out`));
|
|
50
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
51
|
-
fn().then(
|
|
52
|
-
(val) => { signal.removeEventListener("abort", onAbort); resolve(val); },
|
|
53
|
-
(err) => { signal.removeEventListener("abort", onAbort); reject(err); },
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
21
|
// Client hint headers to avoid headless detection
|
|
59
22
|
const CLIENT_HINT_HEADERS: Record<string, string> = {
|
|
60
23
|
"sec-ch-ua": '"Not_A Brand";v="8", "Chromium";v="131", "Google Chrome";v="131"',
|
|
@@ -87,7 +50,7 @@ async function acquireTabSlot(): Promise<void> {
|
|
|
87
50
|
}
|
|
88
51
|
|
|
89
52
|
function releaseTabSlot(tabId?: string): void {
|
|
90
|
-
if (tabId)
|
|
53
|
+
if (tabId) activeTabRegistry.delete(tabId);
|
|
91
54
|
activeTabs--;
|
|
92
55
|
const next = waitQueue.shift();
|
|
93
56
|
if (next) next();
|
|
@@ -116,11 +79,6 @@ export interface CaptureResult {
|
|
|
116
79
|
ws_messages?: CapturedWsMessage[];
|
|
117
80
|
html?: string;
|
|
118
81
|
js_bundles?: Map<string, string>;
|
|
119
|
-
graph_session?: {
|
|
120
|
-
observed_operations: string[];
|
|
121
|
-
known_bindings: Record<string, unknown>;
|
|
122
|
-
suggested_next: string[];
|
|
123
|
-
};
|
|
124
82
|
}
|
|
125
83
|
|
|
126
84
|
export interface RawRequest {
|
|
@@ -132,29 +90,6 @@ export interface RawRequest {
|
|
|
132
90
|
response_headers: Record<string, string>;
|
|
133
91
|
response_body?: string;
|
|
134
92
|
timestamp: string;
|
|
135
|
-
/** Query hook bridge: which action step triggered this request (#114) */
|
|
136
|
-
triggered_by_step?: number;
|
|
137
|
-
triggered_by_action?: string;
|
|
138
|
-
triggered_by_ref?: string;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export interface QueryHookEvent {
|
|
142
|
-
step_index: number;
|
|
143
|
-
action_type: string;
|
|
144
|
-
selector: string;
|
|
145
|
-
requests_triggered: Array<{ url: string; method: string }>;
|
|
146
|
-
timestamp: number;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
interface ExtensionEntry {
|
|
150
|
-
url: string;
|
|
151
|
-
method: string;
|
|
152
|
-
type: string; // "xmlhttprequest", "fetch", "main_frame", etc.
|
|
153
|
-
statusCode?: number;
|
|
154
|
-
requestHeaders?: Array<{ name: string; value: string }>;
|
|
155
|
-
responseHeaders?: Array<{ name: string; value: string }>;
|
|
156
|
-
tabId?: number;
|
|
157
|
-
timestamp: number;
|
|
158
93
|
}
|
|
159
94
|
|
|
160
95
|
export type CapturedCookie = {
|
|
@@ -347,11 +282,11 @@ export function hasUsefulCapturedResponses(
|
|
|
347
282
|
* Inject a fetch/XHR interceptor into the page to capture request/response data.
|
|
348
283
|
* Returns captured entries via __unbrowse_intercepted global.
|
|
349
284
|
*/
|
|
350
|
-
|
|
285
|
+
const INTERCEPTOR_SCRIPT = `(function() {
|
|
351
286
|
if (window.__unbrowse_interceptor_installed) return;
|
|
352
287
|
window.__unbrowse_interceptor_installed = true;
|
|
353
288
|
window.__unbrowse_intercepted = [];
|
|
354
|
-
var MAX_BODY =
|
|
289
|
+
var MAX_BODY = 512 * 1024;
|
|
355
290
|
var MAX_JS_BODY = 2 * 1024 * 1024;
|
|
356
291
|
var MAX_ENTRIES = 500;
|
|
357
292
|
|
|
@@ -375,10 +310,9 @@ export const INTERCEPTOR_SCRIPT = `(function() {
|
|
|
375
310
|
if (window.__unbrowse_intercepted.length >= MAX_ENTRIES) return response;
|
|
376
311
|
var ct = response.headers.get('content-type') || '';
|
|
377
312
|
var isJs = ct.indexOf('javascript') !== -1 || /\\.js(\\?|$)/.test(url);
|
|
378
|
-
var isData = ct.indexOf('json') !== -1 || ct.indexOf('
|
|
379
|
-
ct.indexOf('text/plain') !== -1 ||
|
|
380
|
-
url.indexOf('batchexecute') !== -1 || url.indexOf('/api/') !== -1
|
|
381
|
-
url.indexOf('graphql') !== -1 || url.indexOf('voyager') !== -1;
|
|
313
|
+
var isData = ct.indexOf('application/json') !== -1 || ct.indexOf('+json') !== -1 ||
|
|
314
|
+
ct.indexOf('application/x-protobuf') !== -1 || ct.indexOf('text/plain') !== -1 ||
|
|
315
|
+
url.indexOf('batchexecute') !== -1 || url.indexOf('/api/') !== -1;
|
|
382
316
|
if (!isJs && !isData) return response;
|
|
383
317
|
if (/\\.(css|woff2?|png|jpg|svg|ico)(\\?|$)/.test(url)) return response;
|
|
384
318
|
var clone = response.clone();
|
|
@@ -425,10 +359,9 @@ export const INTERCEPTOR_SCRIPT = `(function() {
|
|
|
425
359
|
var ct = xhr.getResponseHeader('content-type') || '';
|
|
426
360
|
var url = xhr.__unbrowse_url || '';
|
|
427
361
|
var isJs = ct.indexOf('javascript') !== -1 || /\\.js(\\?|$)/.test(url);
|
|
428
|
-
var isData = ct.indexOf('json') !== -1 || ct.indexOf('
|
|
429
|
-
ct.indexOf('text/plain') !== -1 ||
|
|
430
|
-
url.indexOf('batchexecute') !== -1 || url.indexOf('/api/') !== -1
|
|
431
|
-
url.indexOf('graphql') !== -1 || url.indexOf('voyager') !== -1;
|
|
362
|
+
var isData = ct.indexOf('application/json') !== -1 || ct.indexOf('+json') !== -1 ||
|
|
363
|
+
ct.indexOf('application/x-protobuf') !== -1 || ct.indexOf('text/plain') !== -1 ||
|
|
364
|
+
url.indexOf('batchexecute') !== -1 || url.indexOf('/api/') !== -1;
|
|
432
365
|
if (!isJs && !isData) return;
|
|
433
366
|
if (/\\.(css|woff2?|png|jpg|svg|ico)(\\?|$)/.test(url)) return;
|
|
434
367
|
var respBody = xhr.responseText || '';
|
|
@@ -454,7 +387,7 @@ export const INTERCEPTOR_SCRIPT = `(function() {
|
|
|
454
387
|
/**
|
|
455
388
|
* Collect intercepted requests from the page.
|
|
456
389
|
*/
|
|
457
|
-
|
|
390
|
+
async function collectInterceptedRequests(tabId: string): Promise<Array<{
|
|
458
391
|
url: string;
|
|
459
392
|
method: string;
|
|
460
393
|
request_headers: Record<string, string>;
|
|
@@ -475,135 +408,6 @@ export async function collectInterceptedRequests(tabId: string): Promise<Array<{
|
|
|
475
408
|
return [];
|
|
476
409
|
}
|
|
477
410
|
|
|
478
|
-
/**
|
|
479
|
-
* Inject the interceptor script in chunks to work around kuri's ~1KB evaluate limit.
|
|
480
|
-
* Falls back to scriptInject for persistent injection on new navigations.
|
|
481
|
-
*/
|
|
482
|
-
export async function injectInterceptor(tabId: string): Promise<void> {
|
|
483
|
-
// Split into setup (globals + fetch) and XHR parts
|
|
484
|
-
const SETUP = `(function(){if(window.__unbrowse_interceptor_installed)return;window.__unbrowse_interceptor_installed=true;window.__unbrowse_intercepted=[];window.__UB_MAX=2*1024*1024;window.__UB_MAX_JS=2*1024*1024;window.__UB_MAX_N=500;})()`;
|
|
485
|
-
|
|
486
|
-
const FETCH_PATCH = `(function(){if(!window.__unbrowse_interceptor_installed)return;var M=window.__UB_MAX,MJ=window.__UB_MAX_JS,MN=window.__UB_MAX_N;var oF=window.fetch;window.fetch=function(){var a=arguments,u=typeof a[0]==='string'?a[0]:(a[0]&&a[0].url?a[0].url:''),o=a[1]||{},m=(o.method||'GET').toUpperCase(),rb=o.body?String(o.body).substring(0,M):void 0,rh={};if(o.headers){if(typeof o.headers.forEach==='function')o.headers.forEach(function(v,k){rh[k]=v});else Object.keys(o.headers).forEach(function(k){rh[k]=o.headers[k]})}return oF.apply(this,a).then(function(r){if(window.__unbrowse_intercepted.length>=MN)return r;var ct=r.headers.get('content-type')||'';var isJ=ct.indexOf('javascript')!==-1||/\\.js(\\?|$)/.test(u);var isD=ct.indexOf('json')!==-1||ct.indexOf('x-protobuf')!==-1||ct.indexOf('text/plain')!==-1||u.indexOf('/api/')!==-1||u.indexOf('graphql')!==-1||u.indexOf('voyager')!==-1;if(!isJ&&!isD)return r;if(/\\.(css|woff2?|png|jpg|svg|ico)(\\?|$)/.test(u))return r;var c=r.clone();c.text().then(function(b){var lim=isJ?MJ:M;if(b.length>lim)return;var rr={};r.headers.forEach(function(v,k){rr[k]=v});window.__unbrowse_intercepted.push({url:u,method:m,request_headers:rh,request_body:rb,response_status:r.status,response_headers:rr,response_body:b,content_type:ct,is_js:isJ,timestamp:new Date().toISOString()})}).catch(function(){});return r}).catch(function(e){throw e})}})()`;
|
|
487
|
-
|
|
488
|
-
const XHR_PATCH = `(function(){if(!window.__unbrowse_interceptor_installed)return;var M=window.__UB_MAX,MJ=window.__UB_MAX_JS,MN=window.__UB_MAX_N;var oO=XMLHttpRequest.prototype.open,oS=XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.open=function(m,u){this.__ub_m=m;this.__ub_u=u;this.__ub_h={};var oSH=this.setRequestHeader.bind(this);this.setRequestHeader=function(k,v){this.__ub_h[k]=v;oSH(k,v)}.bind(this);return oO.apply(this,arguments)};XMLHttpRequest.prototype.send=function(b){var x=this;x.addEventListener('load',function(){if(window.__unbrowse_intercepted.length>=MN)return;var ct=x.getResponseHeader('content-type')||'',u=x.__ub_u||'';var isJ=ct.indexOf('javascript')!==-1||/\\.js(\\?|$)/.test(u);var isD=ct.indexOf('json')!==-1||ct.indexOf('x-protobuf')!==-1||ct.indexOf('text/plain')!==-1||u.indexOf('/api/')!==-1||u.indexOf('graphql')!==-1||u.indexOf('voyager')!==-1;if(!isJ&&!isD)return;if(/\\.(css|woff2?|png|jpg|svg|ico)(\\?|$)/.test(u))return;var rb=x.responseText||'';var lim=isJ?MJ:M;if(rb.length>lim)return;window.__unbrowse_intercepted.push({url:u,method:(x.__ub_m||'GET').toUpperCase(),request_headers:x.__ub_h||{},request_body:b?String(b).substring(0,M):void 0,response_status:x.status,response_headers:{},response_body:rb,content_type:ct,is_js:isJ,timestamp:new Date().toISOString()})});return oS.apply(this,arguments)}})()`;
|
|
489
|
-
|
|
490
|
-
// Inject in 3 small chunks
|
|
491
|
-
for (const chunk of [SETUP, FETCH_PATCH, XHR_PATCH]) {
|
|
492
|
-
await kuri.evaluate(tabId, chunk).catch(() => {});
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Merge three passive capture data sources into a unified RawRequest list.
|
|
499
|
-
* Priority: JS interceptor (has bodies) > HAR entries > extension observer > responseBodies-only.
|
|
500
|
-
* Deduplicates by URL, keeps the highest-priority version.
|
|
501
|
-
*/
|
|
502
|
-
function mergePassiveCaptureData(
|
|
503
|
-
intercepted: Array<{ url: string; method: string; response_body?: string; response_status: number; request_headers: Record<string, string>; response_headers: Record<string, string>; request_body?: string; content_type?: string; is_js?: boolean; timestamp: string }>,
|
|
504
|
-
harEntries: kuri.KuriHarEntry[],
|
|
505
|
-
extensionEntries: ExtensionEntry[],
|
|
506
|
-
responseBodies: Map<string, string>,
|
|
507
|
-
): RawRequest[] {
|
|
508
|
-
const seen = new Map<string, RawRequest>();
|
|
509
|
-
|
|
510
|
-
// Priority 1: JS-intercepted entries (have response bodies)
|
|
511
|
-
for (const entry of intercepted) {
|
|
512
|
-
if (entry.is_js) continue; // skip JS bundles
|
|
513
|
-
seen.set(entry.url, {
|
|
514
|
-
url: entry.url,
|
|
515
|
-
method: entry.method,
|
|
516
|
-
request_headers: entry.request_headers,
|
|
517
|
-
request_body: entry.request_body,
|
|
518
|
-
response_status: entry.response_status,
|
|
519
|
-
response_headers: entry.response_headers,
|
|
520
|
-
response_body: entry.response_body,
|
|
521
|
-
timestamp: entry.timestamp,
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Priority 2: HAR entries (supplement with responseBodies map)
|
|
526
|
-
// Skip OPTIONS (CORS preflight) — they pollute method attribution
|
|
527
|
-
for (const entry of harEntries) {
|
|
528
|
-
const url = entry.request?.url;
|
|
529
|
-
if (!url || seen.has(url)) continue;
|
|
530
|
-
if (entry.request.method === "OPTIONS") continue;
|
|
531
|
-
const reqHeaders: Record<string, string> = {};
|
|
532
|
-
for (const h of entry.request.headers ?? []) reqHeaders[h.name] = h.value;
|
|
533
|
-
const respHeaders: Record<string, string> = {};
|
|
534
|
-
for (const h of entry.response.headers ?? []) respHeaders[h.name] = h.value;
|
|
535
|
-
seen.set(url, {
|
|
536
|
-
url,
|
|
537
|
-
method: entry.request.method,
|
|
538
|
-
request_headers: reqHeaders,
|
|
539
|
-
request_body: entry.request.postData?.text,
|
|
540
|
-
response_status: entry.response.status,
|
|
541
|
-
response_headers: respHeaders,
|
|
542
|
-
response_body: responseBodies.get(url) ?? entry.response.content?.text,
|
|
543
|
-
timestamp: entry.startedDateTime,
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// Priority 3: Extension entries (URL+headers supplement, no bodies)
|
|
548
|
-
for (const entry of extensionEntries) {
|
|
549
|
-
if (seen.has(entry.url)) continue;
|
|
550
|
-
const reqHeaders: Record<string, string> = {};
|
|
551
|
-
for (const h of entry.requestHeaders ?? []) reqHeaders[h.name] = h.value;
|
|
552
|
-
const respHeaders: Record<string, string> = {};
|
|
553
|
-
for (const h of entry.responseHeaders ?? []) respHeaders[h.name] = h.value;
|
|
554
|
-
seen.set(entry.url, {
|
|
555
|
-
url: entry.url,
|
|
556
|
-
method: entry.method,
|
|
557
|
-
request_headers: reqHeaders,
|
|
558
|
-
response_status: entry.statusCode ?? 0,
|
|
559
|
-
response_headers: respHeaders,
|
|
560
|
-
response_body: responseBodies.get(entry.url),
|
|
561
|
-
timestamp: new Date(entry.timestamp).toISOString(),
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Priority 4: responseBodies-only entries (from Performance API replay / HAR replay)
|
|
566
|
-
// These URLs have bodies but weren't in HAR, interceptor, or extension data
|
|
567
|
-
for (const [bodyUrl, body] of responseBodies) {
|
|
568
|
-
if (seen.has(bodyUrl)) continue;
|
|
569
|
-
seen.set(bodyUrl, {
|
|
570
|
-
url: bodyUrl,
|
|
571
|
-
method: "GET",
|
|
572
|
-
request_headers: {},
|
|
573
|
-
response_status: 200,
|
|
574
|
-
response_headers: {},
|
|
575
|
-
response_body: body,
|
|
576
|
-
timestamp: new Date().toISOString(),
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
return Array.from(seen.values());
|
|
581
|
-
}
|
|
582
|
-
/**
|
|
583
|
-
* Collect network requests observed by kuri's builtin extension (chrome.webRequest).
|
|
584
|
-
* Gracefully returns [] if the extension relay is not yet wired.
|
|
585
|
-
*/
|
|
586
|
-
async function collectExtensionRequests(tabId: string): Promise<ExtensionEntry[]> {
|
|
587
|
-
try {
|
|
588
|
-
// Query the builtin extension's network log via the agent bridge
|
|
589
|
-
const raw = await kuri.evaluate(tabId, `
|
|
590
|
-
(function() {
|
|
591
|
-
if (!window.__kuri || !window.__kuri._networkLog) return '[]';
|
|
592
|
-
var log = window.__kuri._networkLog;
|
|
593
|
-
window.__kuri._networkLog = []; // drain
|
|
594
|
-
return JSON.stringify(log);
|
|
595
|
-
})()
|
|
596
|
-
`);
|
|
597
|
-
if (typeof raw !== "string" || !raw.startsWith("[")) return [];
|
|
598
|
-
const entries: ExtensionEntry[] = JSON.parse(raw);
|
|
599
|
-
log("capture", `extension observer: ${entries.length} entries collected`);
|
|
600
|
-
return entries;
|
|
601
|
-
} catch {
|
|
602
|
-
log("capture", "extension observer: not available (expected if relay not wired)");
|
|
603
|
-
return [];
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
411
|
/**
|
|
608
412
|
* Poll document.readyState until "complete" or timeout.
|
|
609
413
|
* Replaces page.waitForLoadState("networkidle").
|
|
@@ -814,12 +618,6 @@ export async function captureSession(
|
|
|
814
618
|
): Promise<CaptureResult> {
|
|
815
619
|
await acquireTabSlot();
|
|
816
620
|
|
|
817
|
-
// Guard: check browser access config before launching
|
|
818
|
-
if (!isBrowserAccessAvailable()) {
|
|
819
|
-
releaseTabSlot("no-tab");
|
|
820
|
-
throw new Error("Browser access is not available (proxy-only config)");
|
|
821
|
-
}
|
|
822
|
-
|
|
823
621
|
// Ensure Kuri is running and tabs are discovered
|
|
824
622
|
await kuri.start();
|
|
825
623
|
await kuri.discoverTabs(); // Sync Chrome tabs into Kuri's registry
|
|
@@ -841,112 +639,54 @@ export async function captureSession(
|
|
|
841
639
|
let captureTimedOut = false;
|
|
842
640
|
let retryFreshTab = false;
|
|
843
641
|
let captureError: unknown;
|
|
844
|
-
const
|
|
845
|
-
const { signal } = abortController;
|
|
846
|
-
// phase() races each CDP call against the overall capture timeout (bug #113 fix)
|
|
847
|
-
const phase = <T>(label: string, fn: () => Promise<T>) =>
|
|
848
|
-
withBrowserPhaseTimeout(signal, label, fn);
|
|
849
|
-
const timeoutHandle = setTimeout(() => {
|
|
642
|
+
const timeoutHandle = setTimeout(async () => {
|
|
850
643
|
captureTimedOut = true;
|
|
851
|
-
|
|
852
|
-
resetTab(tabId).catch(() => {});
|
|
644
|
+
await resetTab(tabId);
|
|
853
645
|
}, CAPTURE_TIMEOUT_MS);
|
|
854
646
|
|
|
855
647
|
try {
|
|
856
648
|
// Set headers: client hints + auth headers
|
|
857
649
|
const allHeaders = { ...CLIENT_HINT_HEADERS, ...(authHeaders ?? {}) };
|
|
858
|
-
await
|
|
650
|
+
await kuri.setHeaders(tabId, allHeaders);
|
|
859
651
|
|
|
860
|
-
//
|
|
861
|
-
// (CDP setCookie on about:blank doesn't bind to the target domain properly)
|
|
652
|
+
// Inject cookies
|
|
862
653
|
if (cookies && cookies.length > 0) {
|
|
863
|
-
|
|
864
|
-
await phase("navigate:origin", () => kuri.navigate(tabId, origin));
|
|
865
|
-
await phase("waitForLoad:origin", () => kuri.waitForLoad(tabId, 10_000).catch(() => {}));
|
|
866
|
-
|
|
867
|
-
// Check if browser already has a valid session (not redirected to login).
|
|
868
|
-
// If so, skip vault cookie injection — browser cookies are fresher and
|
|
869
|
-
// injecting stale vault cookies (e.g. JSESSIONID) causes HTTP 400.
|
|
870
|
-
const postOriginUrl = await phase("checkOriginUrl", () => kuri.getCurrentUrl(tabId));
|
|
871
|
-
const LOGIN_PATHS_RE = /\/(login|signin|sign-in|sso|auth|uas\/login|checkpoint|oauth)/i;
|
|
872
|
-
const originHost = new URL(origin).hostname;
|
|
873
|
-
let browserAlreadyAuthed = false;
|
|
874
|
-
try {
|
|
875
|
-
const postHost = new URL(postOriginUrl).hostname;
|
|
876
|
-
browserAlreadyAuthed = postHost === originHost && !LOGIN_PATHS_RE.test(new URL(postOriginUrl).pathname);
|
|
877
|
-
} catch { /* bad URL — inject cookies as fallback */ }
|
|
878
|
-
|
|
879
|
-
if (browserAlreadyAuthed) {
|
|
880
|
-
log("capture", `browser already authenticated at ${postOriginUrl} — skipping vault cookie injection`);
|
|
881
|
-
} else {
|
|
882
|
-
await phase("injectCookies", () => injectCookies(tabId, cookies!));
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
// Inject interceptor persistently — survives navigations via Page.addScriptToEvaluateOnNewDocument
|
|
887
|
-
if (!interceptorInjectedTabs.has(tabId)) {
|
|
888
|
-
try {
|
|
889
|
-
await phase("scriptInject", () => kuri.scriptInject(tabId, INTERCEPTOR_SCRIPT));
|
|
890
|
-
interceptorInjectedTabs.add(tabId);
|
|
891
|
-
log("capture", "interceptor installed via scriptInject (persistent)");
|
|
892
|
-
} catch {
|
|
893
|
-
// Fallback for older kuri versions without scriptInject
|
|
894
|
-
log("capture", "scriptInject unavailable — falling back to evaluate injection");
|
|
895
|
-
}
|
|
654
|
+
await injectCookies(tabId, cookies);
|
|
896
655
|
}
|
|
897
656
|
|
|
898
657
|
// Start HAR recording
|
|
899
|
-
await
|
|
658
|
+
await kuri.harStart(tabId);
|
|
900
659
|
|
|
901
660
|
// Determine page domain for JS bundle filtering
|
|
902
661
|
let pageDomain: string | undefined;
|
|
903
662
|
try { pageDomain = getRegistrableDomain(new URL(url).hostname); } catch { /* bad url */ }
|
|
904
663
|
|
|
664
|
+
// Inject fetch/XHR interceptor BEFORE navigation to capture all response bodies
|
|
665
|
+
// Navigate directly to target URL — skip origin pre-navigation to save 1-2s on heavy SPAs.
|
|
666
|
+
// The interceptor is re-injected after navigation anyway (page context resets on navigate).
|
|
667
|
+
await kuri.evaluate(tabId, INTERCEPTOR_SCRIPT).catch(() => {});
|
|
668
|
+
|
|
905
669
|
// Navigate to target URL
|
|
906
|
-
await
|
|
670
|
+
await kuri.navigate(tabId, url);
|
|
907
671
|
|
|
908
|
-
//
|
|
909
|
-
|
|
910
|
-
await
|
|
911
|
-
|
|
912
|
-
}
|
|
672
|
+
// Re-inject interceptor after navigation (page context resets on navigate)
|
|
673
|
+
try {
|
|
674
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
675
|
+
await kuri.evaluate(tabId, INTERCEPTOR_SCRIPT);
|
|
676
|
+
} catch { /* page may not be ready */ }
|
|
913
677
|
|
|
914
678
|
// Build response bodies map from intercepted requests
|
|
915
679
|
const responseBodies = new Map<string, string>();
|
|
916
680
|
const jsBundleBodies = new Map<string, string>();
|
|
917
681
|
const MAX_JS_BUNDLES = 20;
|
|
918
682
|
|
|
919
|
-
// Helper: drain intercepted data into responseBodies/jsBundleBodies
|
|
920
|
-
const drainIntercepted = async () => {
|
|
921
|
-
try {
|
|
922
|
-
const entries = await collectInterceptedRequests(tabId);
|
|
923
|
-
for (const entry of entries) {
|
|
924
|
-
if (entry.is_js && jsBundleBodies.size < MAX_JS_BUNDLES && pageDomain) {
|
|
925
|
-
try {
|
|
926
|
-
const jsDomain = getRegistrableDomain(new URL(entry.url).hostname);
|
|
927
|
-
if (jsDomain === pageDomain && entry.response_body) {
|
|
928
|
-
jsBundleBodies.set(entry.url, entry.response_body);
|
|
929
|
-
}
|
|
930
|
-
} catch { /* bad url */ }
|
|
931
|
-
} else if (entry.response_body && !entry.is_js) {
|
|
932
|
-
responseBodies.set(entry.url, entry.response_body);
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
return entries;
|
|
936
|
-
} catch { return []; }
|
|
937
|
-
};
|
|
938
|
-
|
|
939
683
|
// Adaptive wait: handle Cloudflare challenges + SPA content loading + intent-aware API wait
|
|
940
|
-
await
|
|
684
|
+
await waitForContentReady(tabId, url, intent, responseBodies);
|
|
941
685
|
|
|
942
|
-
//
|
|
943
|
-
await
|
|
944
|
-
|
|
945
|
-
// Collect all intercepted requests (final sweep)
|
|
946
|
-
const intercepted = await phase("collectIntercepted", () => collectInterceptedRequests(tabId));
|
|
947
|
-
const extensionEntries = await phase("collectExtension", () => collectExtensionRequests(tabId));
|
|
686
|
+
// Collect all intercepted requests
|
|
687
|
+
const intercepted = await collectInterceptedRequests(tabId);
|
|
948
688
|
|
|
949
|
-
// Separate JS bundles from data responses
|
|
689
|
+
// Separate JS bundles from data responses
|
|
950
690
|
for (const entry of intercepted) {
|
|
951
691
|
if (entry.is_js && jsBundleBodies.size < MAX_JS_BUNDLES && pageDomain) {
|
|
952
692
|
try {
|
|
@@ -960,107 +700,97 @@ export async function captureSession(
|
|
|
960
700
|
}
|
|
961
701
|
}
|
|
962
702
|
|
|
963
|
-
//
|
|
964
|
-
//
|
|
965
|
-
// HAR captures URLs but not response bodies.
|
|
966
|
-
// Use Performance API to discover all fetch/XHR URLs, then replay-fetch via sync
|
|
967
|
-
// XHR to get response bodies. This runs in-page so cookies/CORS are preserved.
|
|
968
|
-
const REPLAY_SKIP = /\.(js|css|woff2?|png|jpe?g|gif|svg|ico|webp|avif|map|ttf)(\?|$)/i;
|
|
703
|
+
// Also collect via Performance API for requests the interceptor might have missed
|
|
704
|
+
// (requests that started before the interceptor was injected)
|
|
969
705
|
try {
|
|
970
|
-
const
|
|
706
|
+
const perfResult = await kuri.evaluate(tabId, `JSON.stringify(
|
|
971
707
|
performance.getEntriesByType('resource')
|
|
972
708
|
.filter(function(e) { return e.initiatorType === 'fetch' || e.initiatorType === 'xmlhttprequest'; })
|
|
973
|
-
.map(function(e) { return e.name; })
|
|
974
|
-
)`)
|
|
975
|
-
|
|
976
|
-
const perfUrls: string[] = JSON.parse(perfRaw);
|
|
977
|
-
log("capture", `Performance API found ${perfUrls.length} fetch/XHR URLs`);
|
|
978
|
-
let replayCount = 0;
|
|
979
|
-
for (const perfUrl of perfUrls) {
|
|
980
|
-
if (responseBodies.has(perfUrl)) continue;
|
|
981
|
-
if (REPLAY_SKIP.test(perfUrl)) continue;
|
|
982
|
-
if (replayCount >= 30) break;
|
|
983
|
-
try {
|
|
984
|
-
const body = await phase("replay-fetch", () =>
|
|
985
|
-
kuri.evaluate(tabId, `(function(){var x=new XMLHttpRequest();x.open('GET','${perfUrl.replace(/'/g, "\\'")}',false);x.send();return x.status>=200&&x.status<400?x.responseText:''})()`)
|
|
986
|
-
);
|
|
987
|
-
if (typeof body === "string" && body.length > 0 && body.length < 512 * 1024) {
|
|
988
|
-
responseBodies.set(perfUrl, body);
|
|
989
|
-
replayCount++;
|
|
990
|
-
log("capture", `replay-fetched ${perfUrl.substring(0, 80)} (${body.length}B)`);
|
|
991
|
-
}
|
|
992
|
-
} catch { /* non-fatal */ }
|
|
993
|
-
}
|
|
994
|
-
if (replayCount > 0) {
|
|
995
|
-
log("capture", `replay-fetched ${replayCount} API response bodies via Performance API`);
|
|
996
|
-
}
|
|
997
|
-
}
|
|
709
|
+
.map(function(e) { return { url: e.name, duration: e.duration }; })
|
|
710
|
+
)`);
|
|
711
|
+
// Performance API only gives us URLs, not bodies — but useful for request tracking
|
|
998
712
|
} catch { /* non-fatal */ }
|
|
999
713
|
|
|
1000
714
|
// Stop HAR recording and merge with intercepted data
|
|
1001
715
|
let harEntries: kuri.KuriHarEntry[] = [];
|
|
1002
716
|
try {
|
|
1003
|
-
const harResult = await
|
|
717
|
+
const harResult = await kuri.harStop(tabId);
|
|
1004
718
|
harEntries = harResult.entries;
|
|
1005
719
|
} catch { /* HAR may not be available */ }
|
|
1006
720
|
|
|
1007
|
-
// --- HAR-based replay ---
|
|
1008
|
-
// CDP HAR sees ALL network requests (even ones the JS interceptor missed).
|
|
1009
|
-
// For any API-like HAR entry without a body in responseBodies, replay-fetch it.
|
|
1010
|
-
const HAR_REPLAY_CT = /application\/json|text\/plain|\+json/i;
|
|
1011
|
-
let harReplayCount = 0;
|
|
1012
|
-
for (const entry of harEntries) {
|
|
1013
|
-
const harUrl = entry.request?.url;
|
|
1014
|
-
if (!harUrl || responseBodies.has(harUrl)) continue;
|
|
1015
|
-
if (REPLAY_SKIP.test(harUrl)) continue;
|
|
1016
|
-
const method = entry.request?.method?.toUpperCase();
|
|
1017
|
-
if (method === "OPTIONS" || method === "HEAD") continue;
|
|
1018
|
-
const status = entry.response?.status ?? 0;
|
|
1019
|
-
if (status < 200 || status >= 400) continue;
|
|
1020
|
-
const ct = (entry.response?.headers ?? []).find((h: { name: string }) => h.name.toLowerCase() === "content-type")?.value ?? "";
|
|
1021
|
-
if (!HAR_REPLAY_CT.test(ct) && !harUrl.includes("/api/") && !harUrl.includes("_search") && !harUrl.includes("graphql")) continue;
|
|
1022
|
-
if (harReplayCount >= 20) break;
|
|
1023
|
-
try {
|
|
1024
|
-
const postData = method !== "GET" ? entry.request?.postData?.text : undefined;
|
|
1025
|
-
const replayScript = method === "GET" || !postData
|
|
1026
|
-
? `(function(){var x=new XMLHttpRequest();x.open('GET','${harUrl.replace(/'/g, "\\'")}',false);x.send();return x.status>=200&&x.status<400?x.responseText:''})()`
|
|
1027
|
-
: `(function(){var x=new XMLHttpRequest();x.open('${method}','${harUrl.replace(/'/g, "\\'")}',false);x.setRequestHeader('Content-Type','application/json');x.send(${JSON.stringify(postData)});return x.status>=200&&x.status<400?x.responseText:''})()`;
|
|
1028
|
-
const body = await phase("har-replay", () => kuri.evaluate(tabId, replayScript));
|
|
1029
|
-
if (typeof body === "string" && body.length > 0 && body.length < 512 * 1024) {
|
|
1030
|
-
responseBodies.set(harUrl, body);
|
|
1031
|
-
harReplayCount++;
|
|
1032
|
-
log("capture", `har-replay-fetched ${harUrl.substring(0, 80)} (${body.length}B)`);
|
|
1033
|
-
}
|
|
1034
|
-
} catch { /* non-fatal */ }
|
|
1035
|
-
}
|
|
1036
|
-
if (harReplayCount > 0) {
|
|
1037
|
-
log("capture", `har-replay-fetched ${harReplayCount} API response bodies from HAR entries`);
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
721
|
const har_lineage_id = nanoid();
|
|
1041
722
|
|
|
1042
723
|
// Debug: log captured counts
|
|
1043
|
-
log("capture", `tracked ${harEntries.length} HAR, ${intercepted.length} intercepted, ${
|
|
724
|
+
log("capture", `tracked ${harEntries.length} HAR entries, ${intercepted.length} intercepted, ${responseBodies.size} response bodies`);
|
|
1044
725
|
for (const [bodyUrl] of responseBodies) {
|
|
1045
726
|
log("capture", `response body captured: ${bodyUrl.substring(0, 150)}`);
|
|
1046
727
|
}
|
|
1047
728
|
|
|
729
|
+
|
|
1048
730
|
let final_url = url;
|
|
1049
731
|
let html: string | undefined;
|
|
1050
732
|
try {
|
|
1051
|
-
const rawUrl = await
|
|
733
|
+
const rawUrl = await kuri.getCurrentUrl(tabId);
|
|
1052
734
|
final_url = typeof rawUrl === "string" ? rawUrl : String(rawUrl ?? url);
|
|
1053
735
|
// Validate it's actually a URL, fall back to original if not
|
|
1054
736
|
try { new URL(final_url); } catch { final_url = url; }
|
|
1055
|
-
html = await
|
|
737
|
+
html = await kuri.getPageHtml(tabId);
|
|
1056
738
|
} catch {}
|
|
1057
739
|
|
|
1058
|
-
//
|
|
1059
|
-
const requests: RawRequest[] =
|
|
1060
|
-
|
|
740
|
+
// Build requests from HAR entries
|
|
741
|
+
const requests: RawRequest[] = harEntries.map((entry) => {
|
|
742
|
+
const reqHeaders: Record<string, string> = {};
|
|
743
|
+
for (const h of entry.request.headers) reqHeaders[h.name] = h.value;
|
|
744
|
+
const respHeaders: Record<string, string> = {};
|
|
745
|
+
for (const h of entry.response.headers) respHeaders[h.name] = h.value;
|
|
746
|
+
return {
|
|
747
|
+
url: entry.request.url,
|
|
748
|
+
method: entry.request.method,
|
|
749
|
+
request_headers: reqHeaders,
|
|
750
|
+
request_body: entry.request.postData?.text,
|
|
751
|
+
response_status: entry.response.status,
|
|
752
|
+
response_headers: respHeaders,
|
|
753
|
+
response_body: responseBodies.get(entry.request.url) ?? entry.response.content?.text,
|
|
754
|
+
timestamp: entry.startedDateTime,
|
|
755
|
+
};
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// Synthesize RawRequests for intercepted responses not in HAR
|
|
759
|
+
const harUrls = new Set(harEntries.map((e) => e.request.url));
|
|
760
|
+
for (const entry of intercepted) {
|
|
761
|
+
if (entry.is_js) continue;
|
|
762
|
+
if (!harUrls.has(entry.url)) {
|
|
763
|
+
requests.push({
|
|
764
|
+
url: entry.url,
|
|
765
|
+
method: entry.method,
|
|
766
|
+
request_headers: entry.request_headers,
|
|
767
|
+
request_body: entry.request_body,
|
|
768
|
+
response_status: entry.response_status,
|
|
769
|
+
response_headers: entry.response_headers,
|
|
770
|
+
response_body: entry.response_body,
|
|
771
|
+
timestamp: entry.timestamp,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Synthesize RawRequests for response bodies captured during intent-aware wait
|
|
777
|
+
const allTrackedUrls = new Set([...harUrls, ...intercepted.map((e) => e.url)]);
|
|
778
|
+
for (const [bodyUrl, body] of responseBodies) {
|
|
779
|
+
if (!allTrackedUrls.has(bodyUrl)) {
|
|
780
|
+
requests.push({
|
|
781
|
+
url: bodyUrl,
|
|
782
|
+
method: "GET",
|
|
783
|
+
request_headers: {},
|
|
784
|
+
response_status: 200,
|
|
785
|
+
response_headers: {},
|
|
786
|
+
response_body: body,
|
|
787
|
+
timestamp: new Date().toISOString(),
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}
|
|
1061
791
|
|
|
1062
792
|
// Extract session cookies via document.cookie
|
|
1063
|
-
const rawCookies = await
|
|
793
|
+
const rawCookies = await extractCookiesFromPage(tabId, url);
|
|
1064
794
|
const sessionCookies = filterFirstPartySessionCookies(rawCookies, url, final_url);
|
|
1065
795
|
|
|
1066
796
|
if (captureTimedOut) throw new Error(`captureSession timed out after ${CAPTURE_TIMEOUT_MS}ms for ${url}`);
|
|
@@ -1105,7 +835,6 @@ export async function captureSession(
|
|
|
1105
835
|
}
|
|
1106
836
|
} finally {
|
|
1107
837
|
clearTimeout(timeoutHandle);
|
|
1108
|
-
abortController.abort(); // no-op if already aborted; prevents stale phase rejections
|
|
1109
838
|
await resetTab(tabId);
|
|
1110
839
|
releaseTabSlot(tabId);
|
|
1111
840
|
}
|
|
@@ -1124,9 +853,6 @@ export async function executeInBrowser(
|
|
|
1124
853
|
authHeaders?: Record<string, string>,
|
|
1125
854
|
cookies?: Array<{ name: string; value: string; domain: string; path?: string; secure?: boolean; httpOnly?: boolean; sameSite?: string; expires?: number }>
|
|
1126
855
|
): Promise<{ status: number; data: unknown; trace_id: string }> {
|
|
1127
|
-
if (!isBrowserAccessAvailable()) {
|
|
1128
|
-
throw new Error("Browser access is not available (proxy-only config)");
|
|
1129
|
-
}
|
|
1130
856
|
await kuri.start();
|
|
1131
857
|
await kuri.discoverTabs();
|
|
1132
858
|
|
|
@@ -1272,55 +998,6 @@ export interface BrowserActionStep {
|
|
|
1272
998
|
timeoutMs?: number;
|
|
1273
999
|
}
|
|
1274
1000
|
|
|
1275
|
-
/** Internal record of when each action step started, used for request provenance tagging. */
|
|
1276
|
-
interface StepTimingEntry {
|
|
1277
|
-
stepIndex: number;
|
|
1278
|
-
action: string;
|
|
1279
|
-
ref?: string;
|
|
1280
|
-
startedAt: string; // ISO timestamp
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
/**
|
|
1284
|
-
* Tag captured requests with the action step that triggered them.
|
|
1285
|
-
* Uses timestamp comparison: a request is attributed to the latest step
|
|
1286
|
-
* whose startedAt is <= the request's timestamp. Requests that arrived
|
|
1287
|
-
* before any step (e.g. during initial navigation) are left untagged.
|
|
1288
|
-
*
|
|
1289
|
-
* Exported for testability.
|
|
1290
|
-
*/
|
|
1291
|
-
export function tagRequestProvenance(
|
|
1292
|
-
requests: RawRequest[],
|
|
1293
|
-
stepTimings: StepTimingEntry[],
|
|
1294
|
-
): void {
|
|
1295
|
-
if (stepTimings.length === 0) return;
|
|
1296
|
-
|
|
1297
|
-
// Sort step timings by startedAt ascending (should already be, but be safe)
|
|
1298
|
-
const sorted = [...stepTimings].sort(
|
|
1299
|
-
(a, b) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime(),
|
|
1300
|
-
);
|
|
1301
|
-
|
|
1302
|
-
for (const req of requests) {
|
|
1303
|
-
if (!req.timestamp) continue;
|
|
1304
|
-
const reqTime = new Date(req.timestamp).getTime();
|
|
1305
|
-
|
|
1306
|
-
// Find the latest step that started at or before this request's timestamp
|
|
1307
|
-
let matchedStep: StepTimingEntry | undefined;
|
|
1308
|
-
for (const step of sorted) {
|
|
1309
|
-
if (new Date(step.startedAt).getTime() <= reqTime) {
|
|
1310
|
-
matchedStep = step;
|
|
1311
|
-
} else {
|
|
1312
|
-
break;
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
if (matchedStep) {
|
|
1317
|
-
req.triggered_by_step = matchedStep.stepIndex;
|
|
1318
|
-
req.triggered_by_action = matchedStep.action;
|
|
1319
|
-
req.triggered_by_ref = matchedStep.ref;
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
1001
|
/** Result of a first-pass browser action execution. */
|
|
1325
1002
|
export interface BrowserActionResult {
|
|
1326
1003
|
/** Whether the action sequence completed without errors. */
|
|
@@ -1370,7 +1047,7 @@ export async function executeActionSequence(
|
|
|
1370
1047
|
|
|
1371
1048
|
const traceId = nanoid();
|
|
1372
1049
|
const stepResults: BrowserActionResult["steps"] = [];
|
|
1373
|
-
|
|
1050
|
+
|
|
1374
1051
|
try {
|
|
1375
1052
|
// Setup: headers + cookies
|
|
1376
1053
|
const allHeaders = { ...CLIENT_HINT_HEADERS, ...(options?.authHeaders ?? {}) };
|
|
@@ -1395,17 +1072,10 @@ export async function executeActionSequence(
|
|
|
1395
1072
|
}
|
|
1396
1073
|
|
|
1397
1074
|
// Execute each action step
|
|
1398
|
-
for (
|
|
1399
|
-
const step = steps[stepIdx];
|
|
1400
|
-
// Record step start time for request provenance tagging (#214)
|
|
1401
|
-
stepTimings.push({
|
|
1402
|
-
stepIndex: stepIdx,
|
|
1403
|
-
action: step.action,
|
|
1404
|
-
ref: step.ref,
|
|
1405
|
-
startedAt: new Date().toISOString(),
|
|
1406
|
-
});
|
|
1075
|
+
for (const step of steps) {
|
|
1407
1076
|
try {
|
|
1408
1077
|
let result: unknown;
|
|
1078
|
+
|
|
1409
1079
|
switch (step.action) {
|
|
1410
1080
|
case "snapshot":
|
|
1411
1081
|
result = await kuri.snapshot(tabId, step.value);
|
|
@@ -1475,10 +1145,8 @@ export async function executeActionSequence(
|
|
|
1475
1145
|
}
|
|
1476
1146
|
|
|
1477
1147
|
// Collect final state
|
|
1478
|
-
const
|
|
1479
|
-
const
|
|
1480
|
-
const rawHtml = await kuri.getPageHtml(tabId).catch(() => undefined);
|
|
1481
|
-
const html = typeof rawHtml === "string" && rawHtml.trimStart().startsWith("<") ? rawHtml : undefined;
|
|
1148
|
+
const finalUrl = String(await kuri.getCurrentUrl(tabId).catch(() => url));
|
|
1149
|
+
const html = await kuri.getPageHtml(tabId).catch(() => undefined);
|
|
1482
1150
|
const lastSnapshot = await kuri.snapshot(tabId).catch(() => undefined);
|
|
1483
1151
|
|
|
1484
1152
|
// Collect passive capture in the background
|
|
@@ -1497,9 +1165,9 @@ export async function executeActionSequence(
|
|
|
1497
1165
|
|
|
1498
1166
|
const requests: RawRequest[] = harResult.entries.map((entry) => {
|
|
1499
1167
|
const reqHeaders: Record<string, string> = {};
|
|
1500
|
-
for (const h of entry.request.headers
|
|
1168
|
+
for (const h of entry.request.headers) reqHeaders[h.name] = h.value;
|
|
1501
1169
|
const respHeaders: Record<string, string> = {};
|
|
1502
|
-
for (const h of entry.response.headers
|
|
1170
|
+
for (const h of entry.response.headers) respHeaders[h.name] = h.value;
|
|
1503
1171
|
return {
|
|
1504
1172
|
url: entry.request.url,
|
|
1505
1173
|
method: entry.request.method,
|
|
@@ -1528,9 +1196,6 @@ export async function executeActionSequence(
|
|
|
1528
1196
|
});
|
|
1529
1197
|
}
|
|
1530
1198
|
|
|
1531
|
-
// Tag each request with the action step that triggered it (#214)
|
|
1532
|
-
tagRequestProvenance(requests, stepTimings);
|
|
1533
|
-
|
|
1534
1199
|
const sessionCookies = filterFirstPartySessionCookies(
|
|
1535
1200
|
await extractCookiesFromPage(tabId, url),
|
|
1536
1201
|
url,
|