@apifuse/provider-sdk 2.2.0-beta.57 → 2.2.0-beta.59
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/AUTHORING.md +9 -0
- package/CHANGELOG.md +8 -0
- package/README.md +22 -5
- package/SUBMISSION.md +2 -1
- package/bin/apifuse-pack-types.ts +49 -0
- package/bin/apifuse-record.ts +6 -0
- package/dist/cli/templates/provider/README.md.tpl +19 -5
- package/dist/config/loader.d.ts +7 -0
- package/dist/config/loader.js +30 -6
- package/dist/declaration-validation.d.ts +2 -1
- package/dist/declaration-validation.js +39 -0
- package/dist/define.d.ts +5 -4
- package/dist/define.js +25 -1
- package/dist/engine.d.ts +16 -0
- package/dist/engine.js +58 -1
- package/dist/error-resolution.js +13 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -2
- package/dist/lint.js +3 -3
- package/dist/provider.d.ts +3 -3
- package/dist/provider.js +2 -2
- package/dist/runtime/bounded-expiring-map.d.ts +21 -0
- package/dist/runtime/bounded-expiring-map.js +64 -0
- package/dist/runtime/cache.d.ts +2 -0
- package/dist/runtime/cache.js +8 -1
- package/dist/runtime/egress-lease.d.ts +63 -0
- package/dist/runtime/egress-lease.js +233 -0
- package/dist/runtime/http.js +1 -0
- package/dist/runtime/proxy-retry-policy.d.ts +4 -1
- package/dist/runtime/proxy-retry-policy.js +11 -2
- package/dist/runtime/request-telemetry.d.ts +7 -9
- package/dist/runtime/resolver-config.d.ts +7 -0
- package/dist/runtime/resolver-config.js +8 -0
- package/dist/runtime/resolver-public.d.ts +3 -1
- package/dist/runtime/resolver-public.js +1 -1
- package/dist/runtime/resolver-shared.d.ts +9 -0
- package/dist/runtime/resolver-shared.js +57 -0
- package/dist/runtime/resolver-telemetry.d.ts +134 -0
- package/dist/runtime/resolver-telemetry.js +204 -0
- package/dist/runtime/resolver-usage.d.ts +24 -0
- package/dist/runtime/resolver-usage.js +60 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +5 -0
- package/dist/runtime/resolver-vendors/bindings.js +5 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +2 -2
- package/dist/runtime/resolver-vendors/capsolver.js +21 -9
- package/dist/runtime/resolver-vendors/hypersolutions.d.ts +27 -0
- package/dist/runtime/resolver-vendors/hypersolutions.js +369 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -2
- package/dist/runtime/resolver-vendors/twocaptcha.js +26 -10
- package/dist/runtime/resolver-vendors/types.d.ts +35 -5
- package/dist/runtime/resolver-vendors/types.js +20 -10
- package/dist/runtime/resolver.d.ts +5 -1
- package/dist/runtime/resolver.js +397 -109
- package/dist/runtime/stealth-akamai-sbsd.d.ts +54 -0
- package/dist/runtime/stealth-akamai-sbsd.js +142 -0
- package/dist/runtime/stealth.js +547 -53
- package/dist/schema.d.ts +9 -0
- package/dist/schema.js +16 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +1 -0
- package/dist/server/self-test.d.ts +4 -0
- package/dist/server/self-test.js +10 -1
- package/dist/server/serve-implementation.d.ts +7 -1
- package/dist/server/serve-implementation.js +181 -53
- package/dist/server/types.d.ts +6 -0
- package/dist/server/types.js +14 -0
- package/dist/stealth/profiles.d.ts +7 -1
- package/dist/stealth/profiles.js +9 -0
- package/dist/testing/run.js +6 -0
- package/dist/types.d.ts +115 -12
- package/package.json +1 -1
- package/src/cli/templates/provider/README.md.tpl +19 -5
- package/src/config/loader.ts +34 -6
- package/src/declaration-validation.ts +47 -1
- package/src/define.ts +46 -3
- package/src/engine.ts +76 -1
- package/src/error-resolution.ts +13 -0
- package/src/index.ts +32 -1
- package/src/lint.ts +3 -3
- package/src/provider.ts +9 -0
- package/src/runtime/bounded-expiring-map.ts +67 -0
- package/src/runtime/cache.ts +10 -1
- package/src/runtime/egress-lease.ts +336 -0
- package/src/runtime/http.ts +1 -0
- package/src/runtime/proxy-retry-policy.ts +12 -1
- package/src/runtime/request-telemetry.ts +5 -7
- package/src/runtime/resolver-config.ts +12 -0
- package/src/runtime/resolver-public.ts +21 -0
- package/src/runtime/resolver-shared.ts +75 -0
- package/src/runtime/resolver-telemetry.ts +362 -0
- package/src/runtime/resolver-usage.ts +96 -0
- package/src/runtime/resolver-vendors/bindings.ts +5 -0
- package/src/runtime/resolver-vendors/capsolver.ts +22 -3
- package/src/runtime/resolver-vendors/hypersolutions.ts +480 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +26 -12
- package/src/runtime/resolver-vendors/types.ts +60 -12
- package/src/runtime/resolver.ts +465 -124
- package/src/runtime/stealth-akamai-sbsd.ts +207 -0
- package/src/runtime/stealth.ts +727 -95
- package/src/schema.ts +17 -0
- package/src/server/index.ts +21 -0
- package/src/server/self-test.ts +13 -0
- package/src/server/serve-implementation.ts +268 -48
- package/src/server/types.ts +14 -0
- package/src/stealth/profiles.ts +10 -0
- package/src/testing/run.ts +6 -0
- package/src/types.ts +130 -22
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ChallengeSolution, ProviderChallenge, StealthResponse } from "../types.js";
|
|
2
|
+
import type { ResolverVendorTransport } from "./resolver-vendors/types.js";
|
|
3
|
+
import type { StealthCookieJar } from "./stealth-cookies.js";
|
|
4
|
+
export type AkamaiSbsdChallenge = Extract<ProviderChallenge, {
|
|
5
|
+
readonly kind: "akamai_sbsd";
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* SDK-owned response detection and solve wiring derived from the provider declaration.
|
|
9
|
+
* The server attaches it to the stealth client options it builds; it is deliberately not
|
|
10
|
+
* part of the public `StealthClientOptions` type.
|
|
11
|
+
*/
|
|
12
|
+
export type StealthChallengeRuntime = {
|
|
13
|
+
readonly akamaiSbsd?: {
|
|
14
|
+
readonly allowedHosts: readonly string[];
|
|
15
|
+
/** Resolver-declared client profile; must name the initiating session's browser family. */
|
|
16
|
+
readonly clientProfile?: string;
|
|
17
|
+
readonly solve?: (challenge: AkamaiSbsdChallenge, transport: ResolverVendorTransport, signal: AbortSignal) => Promise<ChallengeSolution>;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export type AkamaiSbsdSessionState = {
|
|
21
|
+
/**
|
|
22
|
+
* Latest v-only script for this session; Phase 2 deliberately has no wall-clock TTL.
|
|
23
|
+
* Challenge-state expiry belongs to the Phase 3 ceremony/solve lease handle
|
|
24
|
+
* (ADR-0010 v1.1), not to the stealth session.
|
|
25
|
+
*/
|
|
26
|
+
rememberedScript?: URL;
|
|
27
|
+
/** In-flight solves keyed by akamaiSbsdChallengeKey; an entry leaves only when it settles. */
|
|
28
|
+
readonly transactions: Map<string, {
|
|
29
|
+
readonly result: Promise<{
|
|
30
|
+
readonly solved: true;
|
|
31
|
+
} | {
|
|
32
|
+
readonly solved: false;
|
|
33
|
+
error: unknown;
|
|
34
|
+
}>;
|
|
35
|
+
}>;
|
|
36
|
+
/**
|
|
37
|
+
* Key of the last automatic (safe-method) solve whose refetch was not challenged again.
|
|
38
|
+
* An explicit replay for the same key reuses the solved cookies once instead of solving
|
|
39
|
+
* again; any newer solve or lease expiry clears it.
|
|
40
|
+
*/
|
|
41
|
+
completedSuccessKey?: string;
|
|
42
|
+
};
|
|
43
|
+
export declare function findAkamaiSbsdScript(body: string, page: URL): URL | undefined;
|
|
44
|
+
export declare function detectAkamaiSbsdChallenge(response: Pick<StealthResponse, "url" | "body">, pageUrl: string, jar: Pick<StealthCookieJar, "has">, allowedHosts: readonly string[], state: AkamaiSbsdSessionState): AkamaiSbsdChallenge | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* One in-flight solve per session, challenge, and bound identity: requests on one session
|
|
47
|
+
* may still pick a different wreq browser/OS or proxy, and a solve is only valid for the
|
|
48
|
+
* identity that ran it.
|
|
49
|
+
*/
|
|
50
|
+
export declare function akamaiSbsdChallengeKey(challenge: AkamaiSbsdChallenge, identity: {
|
|
51
|
+
readonly wreqBrowser: string;
|
|
52
|
+
readonly wreqOs: string;
|
|
53
|
+
readonly proxyUrl: string | undefined;
|
|
54
|
+
}): string;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { normalizedResolverHostname } from "./resolver-vendors/hosts.js";
|
|
2
|
+
/**
|
|
3
|
+
* Live SBSD `<script src>` paths are per-site obfuscated (zozo.jp captures, 2026-08-28:
|
|
4
|
+
* `/EdTyEb8Lyxqf/9iGcpl/GmKux0/DXObrkm53w/B1p4AQ/ZCF5f/VoJN08X?v=<uuid>` on the
|
|
5
|
+
* `Access Denied` shape, `/SHO9K/...?v=<uuid>&t=<token>` on the behavioral-tile hard shape).
|
|
6
|
+
* `/.well-known/sbsd` is only the POST endpoint string inside the deobfuscated
|
|
7
|
+
* script, never the page's script path. The stable shape is a same-origin script whose `v`
|
|
8
|
+
* is a UUID; cache-busted bundles (`?v=abc123`, hashes, semver) never match it.
|
|
9
|
+
*/
|
|
10
|
+
const SBSD_SCRIPT_VERSION = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/iu;
|
|
11
|
+
const SBSD_INTERSTITIAL_MAX_BYTES = 4_000;
|
|
12
|
+
const SBSD_SCRIPT_DISCOVERY_MAX_BYTES = 4 * 1_024 * 1_024;
|
|
13
|
+
const SBSD_CHALLENGE_TOKEN_MAX_BYTES = 1_024;
|
|
14
|
+
const SBSD_INTERSTITIAL_MARKER = /sec-bc-tile-container|Access Denied|Reference #\d|Pardon Our Interruption|cpr_chlge/iu;
|
|
15
|
+
function htmlAttribute(value) {
|
|
16
|
+
return value.replace(/&/giu, "&");
|
|
17
|
+
}
|
|
18
|
+
function isDeclaredHost(url, allowedHosts) {
|
|
19
|
+
const hostname = normalizedResolverHostname(url.hostname);
|
|
20
|
+
return allowedHosts.some((host) => normalizedResolverHostname(host) === hostname);
|
|
21
|
+
}
|
|
22
|
+
export function findAkamaiSbsdScript(body, page) {
|
|
23
|
+
// Only markup carries the script tag; JSON and binary bodies skip the scan entirely.
|
|
24
|
+
if (!/^\s*</u.test(body) || Buffer.byteLength(body) > SBSD_SCRIPT_DISCOVERY_MAX_BYTES) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
let passiveScript;
|
|
28
|
+
for (const match of body.matchAll(/<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*>/giu)) {
|
|
29
|
+
let script;
|
|
30
|
+
try {
|
|
31
|
+
// Relative sources resolve against the page URL, as the browser would.
|
|
32
|
+
script = new URL(htmlAttribute(match[1]), page);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const version = script.searchParams.get("v")?.trim();
|
|
38
|
+
if (script.origin !== page.origin || !version || !SBSD_SCRIPT_VERSION.test(version)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (script.searchParams.get("t")?.trim())
|
|
42
|
+
return script;
|
|
43
|
+
passiveScript ??= script;
|
|
44
|
+
}
|
|
45
|
+
return passiveScript;
|
|
46
|
+
}
|
|
47
|
+
function isAkamaiSbsdInterstitial(body) {
|
|
48
|
+
return (Buffer.byteLength(body) < SBSD_INTERSTITIAL_MAX_BYTES && SBSD_INTERSTITIAL_MARKER.test(body));
|
|
49
|
+
}
|
|
50
|
+
function parseAkamaiSbsdChallengeToken(body) {
|
|
51
|
+
if (Buffer.byteLength(body) >= SBSD_INTERSTITIAL_MAX_BYTES)
|
|
52
|
+
return undefined;
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = JSON.parse(body);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (!parsed ||
|
|
61
|
+
typeof parsed !== "object" ||
|
|
62
|
+
Array.isArray(parsed) ||
|
|
63
|
+
!("cpr_chlge" in parsed) ||
|
|
64
|
+
parsed.cpr_chlge !== "true" ||
|
|
65
|
+
!("t" in parsed) ||
|
|
66
|
+
typeof parsed.t !== "string") {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
const token = parsed.t.trim();
|
|
70
|
+
return token && Buffer.byteLength(token) <= SBSD_CHALLENGE_TOKEN_MAX_BYTES ? token : undefined;
|
|
71
|
+
}
|
|
72
|
+
export function detectAkamaiSbsdChallenge(response, pageUrl, jar, allowedHosts, state) {
|
|
73
|
+
let page;
|
|
74
|
+
try {
|
|
75
|
+
page = new URL(response.url ?? pageUrl);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (!isDeclaredHost(page, allowedHosts))
|
|
81
|
+
return undefined;
|
|
82
|
+
const stateCookieName = jar.has("sbsd_o", page.toString())
|
|
83
|
+
? "sbsd_o"
|
|
84
|
+
: jar.has("bm_so", page.toString())
|
|
85
|
+
? "bm_so"
|
|
86
|
+
: undefined;
|
|
87
|
+
const interstitial = isAkamaiSbsdInterstitial(response.body);
|
|
88
|
+
// Without the state cookie (the jar already holds the Set-Cookie of this very response;
|
|
89
|
+
// the live capture sets bm_so on the page that carries the script) or an interstitial,
|
|
90
|
+
// nothing here is Akamai's: do not scan, and do not let an unrelated UUID-versioned
|
|
91
|
+
// bundle become the remembered script.
|
|
92
|
+
if (!stateCookieName && !interstitial)
|
|
93
|
+
return undefined;
|
|
94
|
+
const currentScript = findAkamaiSbsdScript(response.body, page);
|
|
95
|
+
if (currentScript) {
|
|
96
|
+
const version = currentScript.searchParams.get("v")?.trim();
|
|
97
|
+
if (version) {
|
|
98
|
+
// Clone rather than re-parse the pathname: a `//x` path would re-parse as a host.
|
|
99
|
+
const rememberedScript = new URL(currentScript);
|
|
100
|
+
rememberedScript.search = "";
|
|
101
|
+
rememberedScript.hash = "";
|
|
102
|
+
rememberedScript.searchParams.set("v", version);
|
|
103
|
+
state.rememberedScript = rememberedScript;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!stateCookieName)
|
|
107
|
+
return undefined;
|
|
108
|
+
const laterToken = parseAkamaiSbsdChallengeToken(response.body);
|
|
109
|
+
const rememberedScript = state.rememberedScript;
|
|
110
|
+
if (laterToken && rememberedScript && rememberedScript.origin === page.origin) {
|
|
111
|
+
return {
|
|
112
|
+
kind: "akamai_sbsd",
|
|
113
|
+
pageUrl: page.toString(),
|
|
114
|
+
scriptUrl: rememberedScript.toString(),
|
|
115
|
+
stateCookieName,
|
|
116
|
+
challengeToken: laterToken,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (!currentScript || !interstitial)
|
|
120
|
+
return undefined;
|
|
121
|
+
return {
|
|
122
|
+
kind: "akamai_sbsd",
|
|
123
|
+
pageUrl: page.toString(),
|
|
124
|
+
scriptUrl: currentScript.toString(),
|
|
125
|
+
stateCookieName,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* One in-flight solve per session, challenge, and bound identity: requests on one session
|
|
130
|
+
* may still pick a different wreq browser/OS or proxy, and a solve is only valid for the
|
|
131
|
+
* identity that ran it.
|
|
132
|
+
*/
|
|
133
|
+
export function akamaiSbsdChallengeKey(challenge, identity) {
|
|
134
|
+
return JSON.stringify([
|
|
135
|
+
new URL(challenge.scriptUrl).toString(),
|
|
136
|
+
challenge.challengeToken ?? "",
|
|
137
|
+
challenge.stateCookieName,
|
|
138
|
+
identity.wreqBrowser,
|
|
139
|
+
identity.wreqOs,
|
|
140
|
+
identity.proxyUrl ?? "",
|
|
141
|
+
]);
|
|
142
|
+
}
|