Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/proxy/captcha-happy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,25 @@ describe("alias lifecycle (install/remove descriptor contract)", () => {
expect(Object.getOwnPropertyDescriptor(g, "setTimeout")?.get).toBeUndefined();
});
});

describe("browser globals the guest SDK dereferences bare", () => {
test("requestAnimationFrame/cancelAnimationFrame resolve during a solve", () => {
// Bun has no native rAF. While these sat in HOST_CRITICAL_GLOBALS the
// alias pass skipped them, so the FeiLin bundle's 9 bare
// `requestAnimationFrame` references (only one `typeof`-guarded) were
// unresolvable — the same silent fingerprint degradation `print` caused,
// visible only as a lower solve rate.
const g: Record<string, unknown> = {};
const w: Record<string, unknown> = {
requestAnimationFrame: (cb: () => void) => 1,
cancelAnimationFrame: () => {},
};
installGlobalWindowAlias(g, w, 15);
try {
expect(typeof g.requestAnimationFrame).toBe("function");
expect(typeof g.cancelAnimationFrame).toBe("function");
} finally {
removeGlobalWindowAlias(g, w);
}
});
});
15 changes: 13 additions & 2 deletions src/proxy/captcha-happy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,13 @@ function installNativeToString(w) {
try {
const v = desc.get.call(obj);
if (typeof v === "function") mask(v);
} catch (_) {}
// Probing a getter can hand back an already-rejected promise (WHATWG
// stream `closed`/`ready` reject when the receiver is the prototype
// rather than an instance). Nobody awaits these, so without a sink
// each probe surfaced as an unhandledRejection on every solve —
// noise that buried real diagnostics.
else if (v && typeof v.then === "function") v.catch(() => {});
} catch {}
}
if (depth < 3) {
try {
Expand Down Expand Up @@ -1774,7 +1780,12 @@ async function createDom(region, prefix) {
const HOST_CRITICAL_GLOBALS = new Set([
"process", "Bun", "console", "performance", "crypto", "fetch",
"queueMicrotask", "structuredClone", "TextEncoder", "TextDecoder",
"requestAnimationFrame", "cancelAnimationFrame",
// NOTE: requestAnimationFrame/cancelAnimationFrame were removed from this
// list (2026-09-06). Bun has no native rAF, so skipping the alias left a
// bare `requestAnimationFrame` in the FeiLin bundle unresolvable (9 call
// sites, only one `typeof`-guarded) — the same silent fingerprint
// degradation that `print` caused. Aliasing the window's implementation
// shadows nothing on the host.
// NOTE: `print` was removed from this list (2026-08-29). The polyfill
// defines a harmless no-op on the window, but the alias pass skipped it,
// so under Bun (guest scripts run in the HOST realm) the Aliyun pe risk
Expand Down