unbrowse 2.9.0 → 2.9.1

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/dist/cli.js CHANGED
@@ -501,11 +501,75 @@ async function getCookies(tabId) {
501
501
  const raw = await kuriGet("/cookies", { tab_id: tabId });
502
502
  return raw?.result?.cookies ?? [];
503
503
  }
504
+ async function setCookieViaCDP(wsUrl, cookie) {
505
+ return new Promise((resolve) => {
506
+ const timer = setTimeout(() => {
507
+ resolve(false);
508
+ }, 3000);
509
+ try {
510
+ const ws = new (__require("ws"))(wsUrl);
511
+ ws.on("open", () => {
512
+ ws.send(JSON.stringify({
513
+ id: 1,
514
+ method: "Network.setCookie",
515
+ params: {
516
+ ...cookie,
517
+ url: `https://${cookie.domain.replace(/^\./, "")}/`
518
+ }
519
+ }));
520
+ });
521
+ ws.on("message", (data) => {
522
+ clearTimeout(timer);
523
+ try {
524
+ const msg = JSON.parse(data.toString());
525
+ if (msg.id === 1) {
526
+ ws.close();
527
+ resolve(msg.result?.success ?? false);
528
+ }
529
+ } catch {
530
+ ws.close();
531
+ resolve(false);
532
+ }
533
+ });
534
+ ws.on("error", () => {
535
+ clearTimeout(timer);
536
+ resolve(false);
537
+ });
538
+ } catch {
539
+ clearTimeout(timer);
540
+ resolve(false);
541
+ }
542
+ });
543
+ }
504
544
  async function setCookie(tabId, cookie) {
545
+ const value = cookie.value.replace(/^"|"$/g, "");
546
+ if (cookie.secure || cookie.httpOnly) {
547
+ try {
548
+ const res = await fetch("http://127.0.0.1:9222/json", { signal: AbortSignal.timeout(1000) }).catch(() => null);
549
+ if (res?.ok) {
550
+ const pages = await res.json();
551
+ const page = pages.find((p) => p.id === tabId);
552
+ if (page?.webSocketDebuggerUrl) {
553
+ const success = await setCookieViaCDP(page.webSocketDebuggerUrl, {
554
+ name: cookie.name,
555
+ value,
556
+ domain: cookie.domain,
557
+ path: cookie.path || "/",
558
+ secure: cookie.secure ?? false,
559
+ httpOnly: cookie.httpOnly ?? false,
560
+ sameSite: cookie.sameSite || "Lax",
561
+ ...cookie.expires && cookie.expires > 0 ? { expires: cookie.expires } : {}
562
+ });
563
+ if (success)
564
+ return;
565
+ }
566
+ }
567
+ } catch {}
568
+ }
505
569
  await kuriGet("/cookies", {
506
570
  tab_id: tabId,
507
571
  name: cookie.name,
508
- value: cookie.value,
572
+ value,
509
573
  domain: cookie.domain,
510
574
  ...cookie.path ? { path: cookie.path } : {}
511
575
  });
@@ -4596,6 +4660,7 @@ __export(exports_client2, {
4596
4660
  publishGraphEdges: () => publishGraphEdges,
4597
4661
  normalizeAgentEmail: () => normalizeAgentEmail2,
4598
4662
  listSkills: () => listSkills,
4663
+ isX402Error: () => isX402Error,
4599
4664
  isValidAgentEmail: () => isValidAgentEmail2,
4600
4665
  isLocalOnlyMode: () => isLocalOnlyMode,
4601
4666
  hashApiKey: () => hashApiKey,
@@ -4638,6 +4703,9 @@ function decodeBase64Json2(value) {
4638
4703
  return;
4639
4704
  }
4640
4705
  }
4706
+ function isX402Error(err) {
4707
+ return !!err && typeof err === "object" && err.x402 === true;
4708
+ }
4641
4709
  function scopedSkillKey(skillId, scopeId) {
4642
4710
  return scopeId ? `${scopeId}:${skillId}` : skillId;
4643
4711
  }
@@ -5142,10 +5210,20 @@ async function searchIntentResolve(intent, domain, domainK = 5, globalK = 10) {
5142
5210
  domain_k: domainK,
5143
5211
  global_k: globalK
5144
5212
  });
5145
- } catch {
5213
+ } catch (err) {
5214
+ if (isX402Error(err))
5215
+ throw err;
5146
5216
  const [domain_results, global_results] = await Promise.all([
5147
- domain ? searchIntentInDomain(intent, domain, domainK).catch(() => []) : Promise.resolve([]),
5148
- searchIntent(intent, globalK).catch(() => [])
5217
+ domain ? searchIntentInDomain(intent, domain, domainK).catch((fallbackErr) => {
5218
+ if (isX402Error(fallbackErr))
5219
+ throw fallbackErr;
5220
+ return [];
5221
+ }) : Promise.resolve([]),
5222
+ searchIntent(intent, globalK).catch((fallbackErr) => {
5223
+ if (isX402Error(fallbackErr))
5224
+ throw fallbackErr;
5225
+ return [];
5226
+ })
5149
5227
  ]);
5150
5228
  return { domain_results, global_results, skipped_global: false };
5151
5229
  }
@@ -15053,17 +15131,51 @@ async function resolveAndExecute(intent, params = {}, context, projection, optio
15053
15131
  const MARKETPLACE_TIMEOUT_MS = context?.url ? 5000 : 30000;
15054
15132
  if (!forceCapture) {
15055
15133
  const ts0 = Date.now();
15056
- const { domain_results: domainResults, global_results: globalResults } = await Promise.race([
15057
- searchIntentResolve(queryIntent, requestedDomain ?? undefined, MARKETPLACE_DOMAIN_SEARCH_K, MARKETPLACE_GLOBAL_SEARCH_K),
15058
- new Promise((resolve) => setTimeout(() => {
15059
- console.log(`[marketplace] timeout after ${MARKETPLACE_TIMEOUT_MS}ms falling through to browser`);
15060
- resolve({ domain_results: [], global_results: [], skipped_global: true });
15061
- }, MARKETPLACE_TIMEOUT_MS))
15062
- ]).catch(() => ({
15063
- domain_results: [],
15064
- global_results: [],
15065
- skipped_global: false
15066
- }));
15134
+ let searchResponse;
15135
+ try {
15136
+ searchResponse = await Promise.race([
15137
+ searchIntentResolve(queryIntent, requestedDomain ?? undefined, MARKETPLACE_DOMAIN_SEARCH_K, MARKETPLACE_GLOBAL_SEARCH_K),
15138
+ new Promise((resolve) => setTimeout(() => {
15139
+ console.log(`[marketplace] timeout after ${MARKETPLACE_TIMEOUT_MS}ms — falling through to browser`);
15140
+ resolve({ domain_results: [], global_results: [], skipped_global: true });
15141
+ }, MARKETPLACE_TIMEOUT_MS))
15142
+ ]);
15143
+ } catch (err) {
15144
+ if (isX402Error(err)) {
15145
+ const trace2 = {
15146
+ trace_id: nanoid7(),
15147
+ skill_id: "marketplace-search",
15148
+ endpoint_id: "search",
15149
+ started_at: new Date().toISOString(),
15150
+ completed_at: new Date().toISOString(),
15151
+ success: false,
15152
+ status_code: 402,
15153
+ error: "payment_required"
15154
+ };
15155
+ return {
15156
+ result: {
15157
+ error: "payment_required",
15158
+ payment_status: "payment_required",
15159
+ wallet_provider: "lobster.cash",
15160
+ message: "Marketplace search requires payment before shared graph results are returned.",
15161
+ next_step: "Pay the Tier 3 search fee, or re-run with force capture for free local discovery.",
15162
+ indexing_fallback_available: true,
15163
+ tier: "tier3",
15164
+ terms: err.terms
15165
+ },
15166
+ trace: trace2,
15167
+ source: "marketplace",
15168
+ skill: undefined,
15169
+ timing: finalize("marketplace", null, undefined, undefined, trace2)
15170
+ };
15171
+ }
15172
+ searchResponse = {
15173
+ domain_results: [],
15174
+ global_results: [],
15175
+ skipped_global: false
15176
+ };
15177
+ }
15178
+ const { domain_results: domainResults, global_results: globalResults } = searchResponse;
15067
15179
  timing.search_ms = Date.now() - ts0;
15068
15180
  console.log(`[marketplace] search: ${domainResults.length} domain + ${globalResults.length} global results (${timing.search_ms}ms)`);
15069
15181
  const seen = new Set;
@@ -16995,6 +17107,7 @@ async function registerRoutes(app) {
16995
17107
  if (session.domain && session.domain !== newDomain) {
16996
17108
  await authProfileSave(session.tabId, session.domain).catch(() => {});
16997
17109
  }
17110
+ let cookiesInjected = 0;
16998
17111
  if (newDomain && newDomain !== session.domain) {
16999
17112
  await authProfileLoad(session.tabId, newDomain).catch(() => {});
17000
17113
  try {
@@ -17003,6 +17116,7 @@ async function registerRoutes(app) {
17003
17116
  for (const c of browserCookies) {
17004
17117
  await setCookie(session.tabId, c).catch(() => {});
17005
17118
  }
17119
+ cookiesInjected = browserCookies.length;
17006
17120
  }
17007
17121
  } catch {}
17008
17122
  }
@@ -17015,7 +17129,7 @@ async function registerRoutes(app) {
17015
17129
  session.url = typeof finalUrl === "string" && finalUrl.startsWith("http") ? finalUrl : url;
17016
17130
  session.domain = profileName(session.url);
17017
17131
  await injectInterceptor(session.tabId);
17018
- return reply.send({ ok: true, url: session.url, tab_id: session.tabId, auth_profile: session.domain });
17132
+ return reply.send({ ok: true, url: session.url, tab_id: session.tabId, auth_profile: session.domain, ...cookiesInjected > 0 ? { cookies_injected: cookiesInjected } : {} });
17019
17133
  });
17020
17134
  app.post("/v1/browse/snap", async (req, reply) => {
17021
17135
  const { filter } = req.body ?? {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unbrowse",
3
- "version": "2.9.0",
3
+ "version": "2.9.1",
4
4
  "description": "Reverse-engineer any website into reusable API skills. Zero-dep single binary with embedded browser engine.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -845,6 +845,7 @@ export async function registerRoutes(app: FastifyInstance) {
845
845
  }
846
846
 
847
847
  // Inject cookies: try Kuri auth profile first, fall back to Chrome SQLite extraction
848
+ let cookiesInjected = 0;
848
849
  if (newDomain && newDomain !== session.domain) {
849
850
  await kuri.authProfileLoad(session.tabId, newDomain).catch(() => {});
850
851
 
@@ -855,6 +856,7 @@ export async function registerRoutes(app: FastifyInstance) {
855
856
  for (const c of browserCookies) {
856
857
  await kuri.setCookie(session.tabId, c).catch(() => {});
857
858
  }
859
+ cookiesInjected = browserCookies.length;
858
860
  }
859
861
  } catch { /* non-fatal */ }
860
862
  }
@@ -872,7 +874,7 @@ export async function registerRoutes(app: FastifyInstance) {
872
874
  // Re-inject interceptor via evaluate for current page context
873
875
  await injectInterceptor(session.tabId); // chunked injection for current page
874
876
 
875
- return reply.send({ ok: true, url: session.url, tab_id: session.tabId, auth_profile: session.domain });
877
+ return reply.send({ ok: true, url: session.url, tab_id: session.tabId, auth_profile: session.domain, ...(cookiesInjected > 0 ? { cookies_injected: cookiesInjected } : {}) });
876
878
  });
877
879
 
878
880
  // POST /v1/browse/snap — a11y snapshot
@@ -28,6 +28,10 @@ function decodeBase64Json(value: string): unknown {
28
28
  }
29
29
  }
30
30
 
31
+ export function isX402Error(err: unknown): err is Error & { x402: true; terms?: unknown; status?: number } {
32
+ return !!err && typeof err === "object" && (err as { x402?: unknown }).x402 === true;
33
+ }
34
+
31
35
  function scopedSkillKey(skillId: string, scopeId?: string): string {
32
36
  return scopeId ? `${scopeId}:${skillId}` : skillId;
33
37
  }
@@ -678,12 +682,19 @@ export async function searchIntentResolve(
678
682
  domain_k: domainK,
679
683
  global_k: globalK,
680
684
  });
681
- } catch {
685
+ } catch (err) {
686
+ if (isX402Error(err)) throw err;
682
687
  const [domain_results, global_results] = await Promise.all([
683
688
  domain
684
- ? searchIntentInDomain(intent, domain, domainK).catch(() => [] as Array<{ id: number; score: number; metadata: Record<string, unknown> }>)
689
+ ? searchIntentInDomain(intent, domain, domainK).catch((fallbackErr) => {
690
+ if (isX402Error(fallbackErr)) throw fallbackErr;
691
+ return [] as Array<{ id: number; score: number; metadata: Record<string, unknown> }>;
692
+ })
685
693
  : Promise.resolve([] as Array<{ id: number; score: number; metadata: Record<string, unknown> }>),
686
- searchIntent(intent, globalK).catch(() => [] as Array<{ id: number; score: number; metadata: Record<string, unknown> }>),
694
+ searchIntent(intent, globalK).catch((fallbackErr) => {
695
+ if (isX402Error(fallbackErr)) throw fallbackErr;
696
+ return [] as Array<{ id: number; score: number; metadata: Record<string, unknown> }>;
697
+ }),
687
698
  ]);
688
699
  return { domain_results, global_results, skipped_global: false };
689
700
  }
@@ -526,12 +526,78 @@ export async function getCookies(tabId: string): Promise<KuriCookie[]> {
526
526
  return raw?.result?.cookies ?? [];
527
527
  }
528
528
 
529
- /** Set a single cookie. */
529
+ /** Set a cookie via raw CDP WebSocket — supports all cookie attributes (secure, httpOnly, sameSite, expires). */
530
+ async function setCookieViaCDP(wsUrl: string, cookie: {
531
+ name: string; value: string; domain: string; path: string;
532
+ secure: boolean; httpOnly: boolean; sameSite: string; expires?: number;
533
+ }): Promise<boolean> {
534
+ return new Promise((resolve) => {
535
+ const timer = setTimeout(() => { resolve(false); }, 3000);
536
+ try {
537
+ const ws = new (require("ws") as typeof import("ws"))(wsUrl);
538
+ ws.on("open", () => {
539
+ ws.send(JSON.stringify({
540
+ id: 1,
541
+ method: "Network.setCookie",
542
+ params: {
543
+ ...cookie,
544
+ url: `https://${cookie.domain.replace(/^\./, "")}/`,
545
+ },
546
+ }));
547
+ });
548
+ ws.on("message", (data: Buffer) => {
549
+ clearTimeout(timer);
550
+ try {
551
+ const msg = JSON.parse(data.toString());
552
+ if (msg.id === 1) {
553
+ ws.close();
554
+ resolve(msg.result?.success ?? false);
555
+ }
556
+ } catch { ws.close(); resolve(false); }
557
+ });
558
+ ws.on("error", () => { clearTimeout(timer); resolve(false); });
559
+ } catch { clearTimeout(timer); resolve(false); }
560
+ });
561
+ }
562
+
563
+ /** Set a single cookie via raw CDP (Chrome debug port) for full attribute support.
564
+ * Falls back to Kuri's /cookies endpoint if CDP is unavailable. */
565
+ /** Set a single cookie via raw CDP (Chrome debug port) for full attribute support.
566
+ * Falls back to Kuri's /cookies endpoint if CDP is unavailable. */
530
567
  export async function setCookie(tabId: string, cookie: KuriCookie): Promise<void> {
568
+ // Strip wrapping quotes from cookie values (Chrome stores some values like JSESSIONID with literal quotes)
569
+ const value = cookie.value.replace(/^"|"$/g, "");
570
+
571
+ // Try raw CDP first — Kuri's /cookies endpoint doesn't pass secure/httpOnly/sameSite/expires
572
+ // which causes auth failures on sites like LinkedIn that require secure cookies.
573
+ if (cookie.secure || cookie.httpOnly) {
574
+ try {
575
+ const res = await fetch("http://127.0.0.1:9222/json", { signal: AbortSignal.timeout(1000) }).catch(() => null);
576
+ if (res?.ok) {
577
+ const pages = await res.json() as Array<{ id: string; webSocketDebuggerUrl?: string }>;
578
+ const page = pages.find(p => p.id === tabId);
579
+ if (page?.webSocketDebuggerUrl) {
580
+ const success = await setCookieViaCDP(page.webSocketDebuggerUrl, {
581
+ name: cookie.name,
582
+ value,
583
+ domain: cookie.domain,
584
+ path: cookie.path || "/",
585
+ secure: cookie.secure ?? false,
586
+ httpOnly: cookie.httpOnly ?? false,
587
+ sameSite: cookie.sameSite || "Lax",
588
+ ...(cookie.expires && cookie.expires > 0 ? { expires: cookie.expires } : {}),
589
+ });
590
+ if (success) return;
591
+ }
592
+ }
593
+ } catch { /* CDP unavailable, fall through to Kuri */ }
594
+ }
595
+
596
+ // Fallback: Kuri's /cookies endpoint (no secure/httpOnly support)
531
597
  await kuriGet("/cookies", {
532
598
  tab_id: tabId,
533
599
  name: cookie.name,
534
- value: cookie.value,
600
+ value,
535
601
  domain: cookie.domain,
536
602
  ...(cookie.path ? { path: cookie.path } : {}),
537
603
  });
@@ -1,4 +1,4 @@
1
- import { searchIntentResolve, recordOrchestrationPerf } from "../client/index.js";
1
+ import { isX402Error, searchIntentResolve, recordOrchestrationPerf } from "../client/index.js";
2
2
  import * as kuri from "../kuri/client.js";
3
3
  import { emitRouteTrace, recordFailure } from "../telemetry.js";
4
4
  import { publishSkill, getSkill } from "../marketplace/index.js";
@@ -2931,24 +2931,62 @@ export async function resolveAndExecute(
2931
2931
  // 1. Search marketplace — single remote call, capped by timeout when URL available
2932
2932
  const ts0 = Date.now();
2933
2933
  type SearchResult = { id: number; score: number; metadata: Record<string, unknown> };
2934
- const { domain_results: domainResults, global_results: globalResults } = await Promise.race([
2935
- searchIntentResolve(
2936
- queryIntent,
2937
- requestedDomain ?? undefined,
2938
- MARKETPLACE_DOMAIN_SEARCH_K,
2939
- MARKETPLACE_GLOBAL_SEARCH_K,
2940
- ),
2941
- new Promise<{ domain_results: SearchResult[]; global_results: SearchResult[]; skipped_global: boolean }>((resolve) =>
2942
- setTimeout(() => {
2943
- console.log(`[marketplace] timeout after ${MARKETPLACE_TIMEOUT_MS}ms — falling through to browser`);
2944
- resolve({ domain_results: [], global_results: [], skipped_global: true });
2945
- }, MARKETPLACE_TIMEOUT_MS),
2946
- ),
2947
- ]).catch(() => ({
2948
- domain_results: [] as SearchResult[],
2949
- global_results: [] as SearchResult[],
2950
- skipped_global: false,
2951
- }));
2934
+ let searchResponse: {
2935
+ domain_results: SearchResult[];
2936
+ global_results: SearchResult[];
2937
+ skipped_global: boolean;
2938
+ };
2939
+ try {
2940
+ searchResponse = await Promise.race([
2941
+ searchIntentResolve(
2942
+ queryIntent,
2943
+ requestedDomain ?? undefined,
2944
+ MARKETPLACE_DOMAIN_SEARCH_K,
2945
+ MARKETPLACE_GLOBAL_SEARCH_K,
2946
+ ),
2947
+ new Promise<{ domain_results: SearchResult[]; global_results: SearchResult[]; skipped_global: boolean }>((resolve) =>
2948
+ setTimeout(() => {
2949
+ console.log(`[marketplace] timeout after ${MARKETPLACE_TIMEOUT_MS}ms — falling through to browser`);
2950
+ resolve({ domain_results: [], global_results: [], skipped_global: true });
2951
+ }, MARKETPLACE_TIMEOUT_MS),
2952
+ ),
2953
+ ]);
2954
+ } catch (err) {
2955
+ if (isX402Error(err)) {
2956
+ const trace: ExecutionTrace = {
2957
+ trace_id: nanoid(),
2958
+ skill_id: "marketplace-search",
2959
+ endpoint_id: "search",
2960
+ started_at: new Date().toISOString(),
2961
+ completed_at: new Date().toISOString(),
2962
+ success: false,
2963
+ status_code: 402,
2964
+ error: "payment_required",
2965
+ };
2966
+ return {
2967
+ result: {
2968
+ error: "payment_required",
2969
+ payment_status: "payment_required",
2970
+ wallet_provider: "lobster.cash",
2971
+ message: "Marketplace search requires payment before shared graph results are returned.",
2972
+ next_step: "Pay the Tier 3 search fee, or re-run with force capture for free local discovery.",
2973
+ indexing_fallback_available: true,
2974
+ tier: "tier3",
2975
+ terms: err.terms,
2976
+ },
2977
+ trace,
2978
+ source: "marketplace",
2979
+ skill: undefined as any,
2980
+ timing: finalize("marketplace", null, undefined, undefined, trace),
2981
+ };
2982
+ }
2983
+ searchResponse = {
2984
+ domain_results: [] as SearchResult[],
2985
+ global_results: [] as SearchResult[],
2986
+ skipped_global: false,
2987
+ };
2988
+ }
2989
+ const { domain_results: domainResults, global_results: globalResults } = searchResponse;
2952
2990
  timing.search_ms = Date.now() - ts0;
2953
2991
  console.log(`[marketplace] search: ${domainResults.length} domain + ${globalResults.length} global results (${timing.search_ms}ms)`);
2954
2992