aboutsummaryrefslogtreecommitdiff
path: root/castle-search/src/search.ts
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-11-06 03:33:30 +0000
committerIgor Soarez <igor@soarez.org>2026-11-06 03:33:30 +0000
commit561f0ee076c684a79fd5671af1bad0894f33e4de (patch)
tree7849677412a8b5f80835fa812f2e255f222a2c82 /castle-search/src/search.ts
parent147b3b1507ebcdf5912fbd4c474c9dfb35265148 (diff)
castle-search: resolve Google links lazily, in Chrome, not eagerly over plain HTTPHEADmaster
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.ts47
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);