diff options
| author | Igor Soarez <igor@soarez.org> | 2026-11-06 03:33:30 +0000 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-11-06 03:33:30 +0000 |
| commit | 561f0ee076c684a79fd5671af1bad0894f33e4de (patch) | |
| tree | 7849677412a8b5f80835fa812f2e255f222a2c82 /castle-search/src/search.ts | |
| parent | 147b3b1507ebcdf5912fbd4c474c9dfb35265148 (diff) | |
The Node-side resolver sent one curl-like request per result with no
cookies, JS or browser; Google answers that with its 'Verifying your
request' countdown after a handful, and the testing itself triggered it.
A click or direct navigation in Chrome redirects at once (measured).
Search now leaves Google links wrapped and prints the breadcrumb Google
shows for each. castle_cdp_resolve follows one wrapped link in a Chrome tab
when the agent picks it, accepts only google.com/goto?url=<token>, and
reports the verification page instead of waiting it out.
Diffstat (limited to 'castle-search/src/search.ts')
| -rw-r--r-- | castle-search/src/search.ts | 47 |
1 files changed, 45 insertions, 2 deletions
diff --git a/castle-search/src/search.ts b/castle-search/src/search.ts index c64735d..b5effb8 100644 --- a/castle-search/src/search.ts +++ b/castle-search/src/search.ts @@ -8,7 +8,8 @@ * first search and closed by an idempotent `session_shutdown`. */ -import { resolveGoogleLinks } from "./resolve.ts"; +import { isGoogleGoto } from "./google-links.ts"; +import { GoogleVerificationError } from "./errors.ts"; import { BrowserConnection, PageSession, raceAbort } from "./cdp.ts"; import { type CdpTarget, @@ -332,7 +333,7 @@ export const search = async ( } return { - results: await resolveGoogleLinks(probe.results.slice(0, request.numResults), signal), + results: probe.results.slice(0, request.numResults), searchUrl, finalUrl: probe.url || searchUrl, endpoint, @@ -350,3 +351,45 @@ export const search = async ( release(); } }; + +export type ResolveOutcome = { from: string; to: string; endpoint: string }; + +/** + * Follows one Google `/goto` link in Chrome and reports where it lands. + * + * Lazy by design: search leaves the links wrapped, and the caller resolves only + * the ones it means to open. One tab, one navigation, closed afterwards — the + * pattern of a person clicking a result, which Google serves directly. + * + * If Google answers with its "Verifying your request" page instead, the tab is + * still on google.com when the page loads. That is reported as such. The + * countdown is not waited out and nothing is clicked. + */ +export const resolveLink = async (url: string, signal: AbortSignal): Promise<ResolveOutcome> => { + if (!isGoogleGoto(url)) { + throw new Error( + "castle_cdp_resolve only follows Google result links of the form https://www.google.com/goto?url=… " + + "as printed by castle_cdp_search. Results from other engines already carry their real URL.", + ); + } + const release = await acquire(signal); + try { + const conn = await ensureConnection(signal); + const endpoint = describeCdpTarget(conn.target); + const page = await PageSession.open(conn, signal); + try { + await page.navigate(url, signal); + const landed = await page.evaluate<string>("location.href", signal); + if (!landed || isGoogleHost(landed)) { + throw new GoogleVerificationError(landed || url); + } + return { from: url, to: landed, endpoint }; + } finally { + await page.close(); + } + } finally { + release(); + } +}; + +const isGoogleHost = (url: string): boolean => /^https?:\/\/([a-z0-9-]+\.)*google\.(?:com?\.)?[a-z]{2,3}\//.test(url); |
