@apifuse/provider-sdk 2.2.0-beta.25 → 2.2.0-beta.27
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 +7 -6
- package/CHANGELOG.md +9 -1
- package/README.md +3 -3
- package/bin/apifuse-check.ts +62 -3
- package/bin/apifuse-pack-check.ts +8 -2
- package/bin/apifuse-pack-smoke.ts +43 -2
- package/bin/apifuse-pack-types.ts +58 -0
- package/dist/auth.js +29 -0
- package/dist/cli/templates/provider/README.md.tpl +4 -4
- package/dist/contract-serialization.js +4 -8
- package/dist/declaration-validation.d.ts +23 -0
- package/dist/declaration-validation.js +159 -0
- package/dist/define.d.ts +1 -1
- package/dist/define.js +13 -2
- package/dist/index.d.ts +1 -0
- package/dist/lint.js +85 -3
- package/dist/runtime/resolver-vendors/bindings.d.ts +42 -2
- package/dist/runtime/resolver-vendors/bindings.js +31 -6
- package/dist/runtime/resolver-vendors/browser.d.ts +3 -7
- package/dist/runtime/resolver-vendors/browser.js +7 -22
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +23 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +264 -0
- package/dist/runtime/resolver-vendors/types.d.ts +44 -3
- package/dist/runtime/resolver-vendors/types.js +10 -0
- package/dist/runtime/resolver.d.ts +17 -2
- package/dist/runtime/resolver.js +237 -15
- package/dist/runtime/stealth.d.ts +26 -4
- package/dist/runtime/stealth.js +224 -114
- package/dist/server/serve.js +8 -0
- package/dist/stealth/profiles.js +16 -7
- package/dist/types.d.ts +34 -1
- package/package.json +2 -2
- package/src/auth.ts +40 -0
- package/src/cli/templates/provider/README.md.tpl +4 -4
- package/src/contract-serialization.ts +5 -7
- package/src/declaration-validation.ts +202 -0
- package/src/define.ts +23 -2
- package/src/index.ts +1 -0
- package/src/lint.ts +98 -3
- package/src/runtime/resolver-vendors/bindings.ts +40 -15
- package/src/runtime/resolver-vendors/browser.ts +9 -31
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +366 -0
- package/src/runtime/resolver-vendors/types.ts +54 -0
- package/src/runtime/resolver.ts +304 -24
- package/src/runtime/stealth.ts +317 -136
- package/src/server/serve.ts +8 -0
- package/src/stealth/profiles.ts +17 -7
- package/src/types.ts +36 -3
package/AUTHORING.md
CHANGED
|
@@ -365,7 +365,7 @@ export default defineProvider({
|
|
|
365
365
|
```
|
|
366
366
|
<!-- @magic-end:sample -->
|
|
367
367
|
|
|
368
|
-
The journey runner supplies `ctx.gateway`, `ctx.sms.waitForOtp()`, `ctx.journal.sideEffect()`, `ctx.state`, and `ctx.event.operation()` to the
|
|
368
|
+
The journey runner supplies `ctx.gateway`, `ctx.sms.waitForOtp()`, `ctx.journal.sideEffect()`, `ctx.state`, and `ctx.event.operation()` to the required journey `run` function. Provider authors should keep `run` small: call the covered operations in step order, stop at the declared safe boundary, and let the generated health metadata carry schedule, timeout, required secret, and SMS matcher information to the health monitor.
|
|
369
369
|
|
|
370
370
|
For authenticated journeys, open a fresh connection inside `run` with `ctx.gateway.connect({ input: { ... } })`, execute covered operations with the returned `connectionId`, and disconnect in a `finally` block. Do not require or store long-lived `HEALTH_MONITOR_*_CONNECTION_ID` secrets; those stale connection IDs can hide broken login ceremonies.
|
|
371
371
|
|
|
@@ -734,11 +734,12 @@ const credentialsAuth = defineCredentialsAuth({
|
|
|
734
734
|
request's `context`.
|
|
735
735
|
- Stealth/browser providers may require local runtime setup outside Provider code:
|
|
736
736
|
keep access-sensitive operations on `ctx.stealth.fetch()` with an SDK stealth
|
|
737
|
-
`profile`; the TypeScript runtime uses `
|
|
737
|
+
`profile`; the TypeScript runtime uses `wreq-js` behind that interface, so do
|
|
738
738
|
not add per-operation JA3, HTTP/2 SETTINGS, or pseudo-header tuning. `ctx.stealth`
|
|
739
|
-
supports Chrome
|
|
740
|
-
|
|
741
|
-
(`nodriver` is Python-runtime only);
|
|
739
|
+
supports Chrome, Firefox, and Safari profiles; use `ctx.browser` when a
|
|
740
|
+
Provider needs real browser execution. TypeScript browser Providers use
|
|
741
|
+
`browser.engine: "playwright-stealth"` (`nodriver` is Python-runtime only);
|
|
742
|
+
install local browser assets with
|
|
742
743
|
`bunx playwright install chromium`, or set
|
|
743
744
|
`APIFUSE__CDP_POOL__URL` for remote browser debugging.
|
|
744
745
|
|
|
@@ -786,7 +787,7 @@ its own diagnostics.
|
|
|
786
787
|
|
|
787
788
|
Set `maxBodyBytes` on `ctx.stealth.fetch()` or `session.redirects.run()` when an
|
|
788
789
|
upstream response has a known safe maximum. The limit is opt-in and counts
|
|
789
|
-
decoded bytes as
|
|
790
|
+
decoded bytes as `wreq-js` streams them. It applies to every redirect hop, uses a
|
|
790
791
|
parseable `Content-Length` for an early rejection, and still enforces the limit
|
|
791
792
|
incrementally when the header is absent or inaccurate. Exceeding the limit
|
|
792
793
|
aborts the response and throws a non-retryable `TransportError` with code
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0-beta.27
|
|
4
|
+
|
|
5
|
+
- Release candidate for main commit 579d9e7fd22d8414b151be2b71a43a0990456911.
|
|
6
|
+
|
|
7
|
+
## 2.2.0-beta.26
|
|
8
|
+
|
|
9
|
+
- Release candidate for main commit 924fe13d1101e7840d799a562a7a70174355185d.
|
|
10
|
+
|
|
3
11
|
## 2.2.0-beta.25
|
|
4
12
|
|
|
5
13
|
- Release candidate for main commit e6df658b95c0728b671fd221919ec2f85f82b0b7.
|
|
@@ -205,7 +213,7 @@
|
|
|
205
213
|
|
|
206
214
|
## 2.1.0-beta.3
|
|
207
215
|
|
|
208
|
-
- Replace the legacy TypeScript request transport with `ctx.stealth`, backed by
|
|
216
|
+
- Replace the legacy TypeScript request transport with `ctx.stealth`, backed by browser-grade TLS/HTTP2 impersonation without Python runtime dependencies.
|
|
209
217
|
- Add the public `apifuse submit-check` / `apifuse bounty-check` CLI for score-based pre-submission provider quality checks.
|
|
210
218
|
- Ship `SUBMISSION.md` in the npm package so bounty contributors can follow the checklist without access to the private monorepo.
|
|
211
219
|
- Include submit-check in generated provider validation scripts and packed-artifact smoke coverage.
|
package/README.md
CHANGED
|
@@ -137,7 +137,7 @@ the bad request path; provider/runtime failures include `code`, `message`, and
|
|
|
137
137
|
`context` object.
|
|
138
138
|
- **Stealth-sensitive providers**: use `ctx.http` for normal JSON/REST calls and
|
|
139
139
|
`ctx.stealth.fetch()` when you need browser-like session or cookie control.
|
|
140
|
-
`ctx.stealth.fetch()` uses the
|
|
140
|
+
`ctx.stealth.fetch()` uses the `wreq-js`-backed browser stealth transport and
|
|
141
141
|
accepts request controls for `params`, `sensitiveParams`, `proxy`, `timeout`, `profile`,
|
|
142
142
|
`maxBodyBytes`, `redirect`, `throwOnHttpError`, and
|
|
143
143
|
`stealth.insecureSkipVerify`. For login
|
|
@@ -145,8 +145,8 @@ the bad request path; provider/runtime failures include `code`, `message`, and
|
|
|
145
145
|
a session with `ctx.stealth.createSession()` and use `session.redirects.run()`;
|
|
146
146
|
inspect accumulated cookies through `session.cookies`. Select an SDK stealth
|
|
147
147
|
`profile` such as `chrome-146`; do not tune JA3, HTTP/2 SETTINGS, or
|
|
148
|
-
pseudo-header order in provider code. Chrome
|
|
149
|
-
supported; use `ctx.browser` when
|
|
148
|
+
pseudo-header order in provider code. Chrome, Firefox, and Safari profiles
|
|
149
|
+
are supported; use `ctx.browser` when the provider needs browser execution.
|
|
150
150
|
- **Query-parameter credentials**: when an upstream requires a credential in
|
|
151
151
|
its URL query, pass it through `sensitiveParams`, not `params` and never a
|
|
152
152
|
hand-built URL. It is sent as a normal query parameter while the SDK redacts
|
package/bin/apifuse-check.ts
CHANGED
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
PROMPT_ASSET_SYNC_REMEDIATION,
|
|
12
12
|
verifyPromptAssets,
|
|
13
13
|
} from "../src/cli/prompt-assets.js";
|
|
14
|
+
import {
|
|
15
|
+
DECLARATION_INVALID_CODE,
|
|
16
|
+
validateFailClosedDeclaration,
|
|
17
|
+
} from "../src/declaration-validation.js";
|
|
18
|
+
import { isProviderError } from "../src/errors.js";
|
|
14
19
|
import type { ProviderDefinition } from "../src/index.js";
|
|
15
20
|
import { lintProvider, type ProviderLintMode } from "../src/lint.js";
|
|
16
21
|
import { safeParseSchemaSync } from "../src/schema.js";
|
|
@@ -119,14 +124,25 @@ export async function runChecks(
|
|
|
119
124
|
const dockerfilePath = resolve(providerRoot, "Dockerfile");
|
|
120
125
|
const packageJsonPath = resolve(providerRoot, "package.json");
|
|
121
126
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
let providerModule: Record<string, unknown> | undefined;
|
|
128
|
+
let providerImportError: unknown;
|
|
129
|
+
if (existsSync(indexPath)) {
|
|
130
|
+
try {
|
|
131
|
+
providerModule = (await import(pathToFileURL(indexPath).href)) as Record<string, unknown>;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (isProviderError(error) && error.code === DECLARATION_INVALID_CODE) {
|
|
134
|
+
providerImportError = error;
|
|
135
|
+
} else {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
125
140
|
const provider = assertProviderDefinition(providerModule?.default);
|
|
126
141
|
const providerSourceFiles = collectProviderSourceFiles(providerRoot);
|
|
127
142
|
|
|
128
143
|
return [
|
|
129
144
|
checkIndex(indexPath, provider),
|
|
145
|
+
checkDeclaration(provider, providerImportError),
|
|
130
146
|
checkOperations(provider),
|
|
131
147
|
checkFixtures(provider),
|
|
132
148
|
checkSchemas(provider),
|
|
@@ -138,6 +154,49 @@ export async function runChecks(
|
|
|
138
154
|
];
|
|
139
155
|
}
|
|
140
156
|
|
|
157
|
+
const DECLARATION_CHECK_MESSAGE = "Provider declaration passes fail-closed validation";
|
|
158
|
+
|
|
159
|
+
function checkDeclaration(
|
|
160
|
+
provider: ProviderDefinition | undefined,
|
|
161
|
+
importError: unknown,
|
|
162
|
+
): CheckResult {
|
|
163
|
+
if (importError !== undefined) {
|
|
164
|
+
return {
|
|
165
|
+
message: DECLARATION_CHECK_MESSAGE,
|
|
166
|
+
passed: false,
|
|
167
|
+
details: formatDeclarationError(importError),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (!provider) return { message: DECLARATION_CHECK_MESSAGE, passed: false };
|
|
171
|
+
try {
|
|
172
|
+
validateFailClosedDeclaration(provider);
|
|
173
|
+
return { message: DECLARATION_CHECK_MESSAGE, passed: true };
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return {
|
|
176
|
+
message: DECLARATION_CHECK_MESSAGE,
|
|
177
|
+
passed: false,
|
|
178
|
+
details: formatDeclarationError(error),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function formatDeclarationError(error: unknown): string[] {
|
|
184
|
+
if (isProviderError(error) && error.code === DECLARATION_INVALID_CODE) {
|
|
185
|
+
const details = error.details;
|
|
186
|
+
if (isRecord(details) && Array.isArray(details.violations)) {
|
|
187
|
+
return details.violations.map((violation) => {
|
|
188
|
+
if (!isRecord(violation)) return String(violation);
|
|
189
|
+
const ruleId = typeof violation.ruleId === "string" ? violation.ruleId : "unknown-rule";
|
|
190
|
+
const path = typeof violation.path === "string" ? violation.path : "unknown-path";
|
|
191
|
+
const message = typeof violation.message === "string" ? `: ${violation.message}` : "";
|
|
192
|
+
const fix = typeof violation.fix === "string" ? ` Fix: ${violation.fix}` : "";
|
|
193
|
+
return `${path} [${ruleId}]${message}${fix}`;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return [error instanceof Error ? error.message : String(error)];
|
|
198
|
+
}
|
|
199
|
+
|
|
141
200
|
export const PROMPT_ASSETS_CHECK_MESSAGE =
|
|
142
201
|
"Agent prompt assets match the installed SDK version";
|
|
143
202
|
|
|
@@ -156,9 +156,9 @@ function assertPublicSmokeDocs(label: string, content: string): void {
|
|
|
156
156
|
);
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
if (!content.includes("
|
|
159
|
+
if (!content.includes("wreq-js")) {
|
|
160
160
|
throw new Error(
|
|
161
|
-
`${label} must include
|
|
161
|
+
`${label} must include wreq-js stealth runtime guidance for TLS/browser bounties.`,
|
|
162
162
|
);
|
|
163
163
|
}
|
|
164
164
|
|
|
@@ -168,6 +168,12 @@ function assertPublicSmokeDocs(label: string, content: string): void {
|
|
|
168
168
|
);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
if (!content.includes("Chrome, Firefox, and Safari")) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`${label} must document the browser families supported by the TypeScript stealth runtime.`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
171
177
|
if (
|
|
172
178
|
!content.includes('browser.engine: "playwright-stealth"') ||
|
|
173
179
|
!content.includes("nodriver")
|
|
@@ -73,6 +73,7 @@ try {
|
|
|
73
73
|
],
|
|
74
74
|
consumerDir,
|
|
75
75
|
);
|
|
76
|
+
smokePackedStealthNative(consumerDir);
|
|
76
77
|
|
|
77
78
|
const cliBin = join(consumerDir, "node_modules", ".bin", "apifuse");
|
|
78
79
|
if (!existsSync(cliBin)) {
|
|
@@ -216,6 +217,43 @@ function run(command: string, args: string[], cwd: string): void {
|
|
|
216
217
|
}
|
|
217
218
|
}
|
|
218
219
|
|
|
220
|
+
function smokePackedStealthNative(consumerDir: string): void {
|
|
221
|
+
run(
|
|
222
|
+
"bun",
|
|
223
|
+
[
|
|
224
|
+
"--eval",
|
|
225
|
+
[
|
|
226
|
+
'import { createServer } from "node:http";',
|
|
227
|
+
'import { createStealthClient } from "@apifuse/provider-sdk";',
|
|
228
|
+
"const server = createServer((_request, response) => {",
|
|
229
|
+
' response.setHeader("set-cookie", "pack_native_cookie=landed; Path=/");',
|
|
230
|
+
' response.end("packed native stealth ok");',
|
|
231
|
+
"});",
|
|
232
|
+
"await new Promise((resolve, reject) => {",
|
|
233
|
+
' server.once("error", reject);',
|
|
234
|
+
' server.listen(0, "127.0.0.1", resolve);',
|
|
235
|
+
"});",
|
|
236
|
+
"const address = server.address();",
|
|
237
|
+
'if (!address || typeof address === "string") throw new Error("Local server has no TCP address");',
|
|
238
|
+
'const baseUrl = "http://127.0.0.1:" + address.port;',
|
|
239
|
+
'const session = createStealthClient(baseUrl).createSession({ profile: "safari-17" });',
|
|
240
|
+
"try {",
|
|
241
|
+
' const response = await session.fetch("/native");',
|
|
242
|
+
' if (response.body !== "packed native stealth ok") throw new Error("Unexpected stealth body: " + response.body);',
|
|
243
|
+
' if (session.cookies.get("pack_native_cookie", baseUrl + "/native") !== "landed") {',
|
|
244
|
+
' throw new Error("Packed stealth Set-Cookie did not land in the SDK jar");',
|
|
245
|
+
" }",
|
|
246
|
+
' console.log("packed native Safari stealth request OK");',
|
|
247
|
+
"} finally {",
|
|
248
|
+
" session.close();",
|
|
249
|
+
" await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));",
|
|
250
|
+
"}",
|
|
251
|
+
].join("\n"),
|
|
252
|
+
],
|
|
253
|
+
consumerDir,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
219
257
|
function assertGeneratedReadme(providerDir: string): void {
|
|
220
258
|
const readme = readFileSync(join(providerDir, "README.md"), "utf8");
|
|
221
259
|
if (!readme.includes('"requestId":"req_local_ping"')) {
|
|
@@ -227,8 +265,11 @@ function assertGeneratedReadme(providerDir: string): void {
|
|
|
227
265
|
if (!readme.includes("bunx playwright install chromium")) {
|
|
228
266
|
throw new Error("Generated README is missing browser runtime troubleshooting guidance.");
|
|
229
267
|
}
|
|
230
|
-
if (!readme.includes("
|
|
231
|
-
throw new Error("Generated README is missing
|
|
268
|
+
if (!readme.includes("wreq-js")) {
|
|
269
|
+
throw new Error("Generated README is missing wreq-js stealth runtime guidance.");
|
|
270
|
+
}
|
|
271
|
+
if (!readme.includes("Chrome, Firefox, and Safari")) {
|
|
272
|
+
throw new Error("Generated README is missing supported stealth browser families.");
|
|
232
273
|
}
|
|
233
274
|
if (!readme.includes("bun run submit-check")) {
|
|
234
275
|
throw new Error("Generated README must document the submit-check pre-submission workflow.");
|
|
@@ -143,6 +143,51 @@ const NEGATIVE_CONTROLS = [
|
|
|
143
143
|
"",
|
|
144
144
|
].join("\n"),
|
|
145
145
|
},
|
|
146
|
+
{
|
|
147
|
+
filename: "negative-control-aws-waf-site-key-type.ts",
|
|
148
|
+
expectedCode: "TS2322",
|
|
149
|
+
description: "aws_waf siteKey must be a string",
|
|
150
|
+
source: [
|
|
151
|
+
'import type { ProviderChallenge } from "@apifuse/provider-sdk";',
|
|
152
|
+
"",
|
|
153
|
+
"export const mustNotCompile: ProviderChallenge = {",
|
|
154
|
+
'\tkind: "aws_waf",',
|
|
155
|
+
'\tpageUrl: "https://example.com",',
|
|
156
|
+
"\tsiteKey: 123,",
|
|
157
|
+
"};",
|
|
158
|
+
"",
|
|
159
|
+
].join("\n"),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
filename: "negative-control-aws-waf-context-type.ts",
|
|
163
|
+
expectedCode: "TS2322",
|
|
164
|
+
description: "aws_waf context must be a string",
|
|
165
|
+
source: [
|
|
166
|
+
'import type { ProviderChallenge } from "@apifuse/provider-sdk";',
|
|
167
|
+
"",
|
|
168
|
+
"export const mustNotCompile: ProviderChallenge = {",
|
|
169
|
+
'\tkind: "aws_waf",',
|
|
170
|
+
'\tpageUrl: "https://example.com",',
|
|
171
|
+
"\tcontext: 123,",
|
|
172
|
+
"};",
|
|
173
|
+
"",
|
|
174
|
+
].join("\n"),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
filename: "negative-control-aws-waf-unknown-field.ts",
|
|
178
|
+
expectedCode: "TS2353",
|
|
179
|
+
description: "aws_waf rejects fields it does not declare",
|
|
180
|
+
source: [
|
|
181
|
+
'import type { ProviderChallenge } from "@apifuse/provider-sdk";',
|
|
182
|
+
"",
|
|
183
|
+
"export const mustNotCompile: ProviderChallenge = {",
|
|
184
|
+
'\tkind: "aws_waf",',
|
|
185
|
+
'\tpageUrl: "https://example.com",',
|
|
186
|
+
"\tunknownField: 1,",
|
|
187
|
+
"};",
|
|
188
|
+
"",
|
|
189
|
+
].join("\n"),
|
|
190
|
+
},
|
|
146
191
|
{
|
|
147
192
|
filename: "negative-control-recaptcha-v3-action.ts",
|
|
148
193
|
expectedCode: "TS2322",
|
|
@@ -184,6 +229,19 @@ const NEGATIVE_CONTROLS = [
|
|
|
184
229
|
"",
|
|
185
230
|
].join("\n"),
|
|
186
231
|
},
|
|
232
|
+
{
|
|
233
|
+
filename: "negative-control-resolver-runtime-adapter-factories.ts",
|
|
234
|
+
expectedCode: "TS2353",
|
|
235
|
+
description: "ResolverRuntimeOptions does not accept caller-supplied adapter factories",
|
|
236
|
+
source: [
|
|
237
|
+
'import type { ResolverRuntimeOptions } from "@apifuse/provider-sdk";',
|
|
238
|
+
"",
|
|
239
|
+
"export const mustNotCompile: ResolverRuntimeOptions = {",
|
|
240
|
+
"\tadapterFactories: {},",
|
|
241
|
+
"};",
|
|
242
|
+
"",
|
|
243
|
+
].join("\n"),
|
|
244
|
+
},
|
|
187
245
|
{
|
|
188
246
|
filename: "negative-control-resolver-runtime-allowed-hosts.ts",
|
|
189
247
|
expectedCode: "TS2322",
|
package/dist/auth.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AuthError, ProviderError } from "./errors.js";
|
|
2
|
+
import { declarationInvalidError, DECLARATION_RULE_IDS, } from "./declaration-validation.js";
|
|
2
3
|
const CREDENTIALS_AUTH_CHALLENGE_CONTEXT_KEY = "__credentialsAuthChallenge";
|
|
3
4
|
const DEFAULT_COMPLETE_TURN_ID = "auth.complete";
|
|
4
5
|
const DEFAULT_ABORT_TURN_ID = "auth.abort";
|
|
@@ -379,6 +380,7 @@ export function defineCredentialsAuth(options) {
|
|
|
379
380
|
const retryTurnId = options.retryTurnId ?? "credentials.retry";
|
|
380
381
|
const completeTurnId = options.completeTurnId ?? "credentials.complete";
|
|
381
382
|
const challenges = (options.challenges ?? {});
|
|
383
|
+
validateCredentialsAuthChallenges(challenges);
|
|
382
384
|
return {
|
|
383
385
|
auth: {
|
|
384
386
|
mode: "credentials",
|
|
@@ -425,3 +427,30 @@ export function defineCredentialsAuth(options) {
|
|
|
425
427
|
},
|
|
426
428
|
};
|
|
427
429
|
}
|
|
430
|
+
function validateCredentialsAuthChallenges(challenges) {
|
|
431
|
+
const violations = [];
|
|
432
|
+
for (const [challengeId, challenge] of Object.entries(challenges)) {
|
|
433
|
+
const fieldCount = challenge.fields && typeof challenge.fields === "object"
|
|
434
|
+
? Object.keys(challenge.fields).length
|
|
435
|
+
: 0;
|
|
436
|
+
const fieldsDeclared = challenge.fields !== undefined && challenge.fields !== null;
|
|
437
|
+
const hasFields = fieldCount > 0;
|
|
438
|
+
const hasVerify = typeof challenge.verify === "function";
|
|
439
|
+
const hasPoll = typeof challenge.poll === "function";
|
|
440
|
+
const isInteractive = hasFields && hasVerify && !hasPoll;
|
|
441
|
+
const isPolling = !fieldsDeclared && !hasVerify && hasPoll;
|
|
442
|
+
const isHybrid = hasFields && hasVerify && hasPoll;
|
|
443
|
+
const emptyFieldsDeclared = fieldsDeclared && !hasFields;
|
|
444
|
+
if (!emptyFieldsDeclared && (isInteractive || isPolling || isHybrid))
|
|
445
|
+
continue;
|
|
446
|
+
const path = `challenges.${challengeId}`;
|
|
447
|
+
violations.push({
|
|
448
|
+
ruleId: DECLARATION_RULE_IDS.challengeShape,
|
|
449
|
+
path,
|
|
450
|
+
message: "challenge must be interactive, polling, or an explicit hybrid.",
|
|
451
|
+
fix: `Give ${path} non-empty fields plus verify, poll alone, or all three for a hybrid.`,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (violations.length > 0)
|
|
455
|
+
throw declarationInvalidError(violations);
|
|
456
|
+
}
|
|
@@ -114,10 +114,10 @@ Structured errors return an `error` object with `code`, `message`,
|
|
|
114
114
|
- Auth flow: call `/auth/start`, then `/auth/continue` with the same `flowId`;
|
|
115
115
|
carry returned `contextPatch` values into the next request's `context`.
|
|
116
116
|
- Stealth/browser runtime: keep access-sensitive operations on `ctx.stealth.fetch()` with an
|
|
117
|
-
SDK stealth `profile`; the TypeScript stealth runtime uses `
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
(`nodriver` is Python-runtime only)
|
|
117
|
+
SDK stealth `profile`; the TypeScript stealth runtime uses `wreq-js` internally
|
|
118
|
+
and supports Chrome, Firefox, and Safari profiles. Use `ctx.browser` only when
|
|
119
|
+
the provider needs browser execution; TypeScript browser Providers use
|
|
120
|
+
`browser.engine: "playwright-stealth"` (`nodriver` is Python-runtime only). Install local Chromium with
|
|
121
121
|
`bunx playwright install chromium` or set `APIFUSE__CDP_POOL__URL`.
|
|
122
122
|
|
|
123
123
|
## Next steps
|
|
@@ -54,15 +54,11 @@ function isZodSchema(schema) {
|
|
|
54
54
|
return schema instanceof z.ZodType;
|
|
55
55
|
}
|
|
56
56
|
function zodJsonSchema(schema) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
if (error instanceof Error)
|
|
63
|
-
return undefined;
|
|
64
|
-
throw error;
|
|
57
|
+
const jsonSchema = toJsonValue(z.toJSONSchema(schema));
|
|
58
|
+
if (jsonSchema === undefined) {
|
|
59
|
+
throw new TypeError("z.toJSONSchema() returned a non-JSON value");
|
|
65
60
|
}
|
|
61
|
+
return jsonSchema;
|
|
66
62
|
}
|
|
67
63
|
function getSchemaTypeName(schema) {
|
|
68
64
|
if (!isRecord(schema))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ProviderError } from "./errors.js";
|
|
2
|
+
import type { ProviderDefinition } from "./types.js";
|
|
3
|
+
export declare const DECLARATION_INVALID_CODE = "DECLARATION_INVALID";
|
|
4
|
+
export declare const DECLARATION_RULE_IDS: {
|
|
5
|
+
readonly challengeShape: "credentials-challenge-shape";
|
|
6
|
+
readonly journeyExecutable: "health-journey-executable";
|
|
7
|
+
readonly schemaSerializable: "operation-schema-serializable";
|
|
8
|
+
readonly proxyExplicitPolicy: "proxy-explicit-policy";
|
|
9
|
+
readonly proxyVendorExclusive: "proxy-vendor-fields-exclusive";
|
|
10
|
+
readonly proxyNoMixedVendors: "proxy-no-mixed-vendors";
|
|
11
|
+
readonly proxySmartproxyGeo: "proxy-smartproxy-country-only";
|
|
12
|
+
readonly operationUpstreamProxy: "operation-upstream-proxy-unsupported";
|
|
13
|
+
};
|
|
14
|
+
export type DeclarationRuleId = (typeof DECLARATION_RULE_IDS)[keyof typeof DECLARATION_RULE_IDS];
|
|
15
|
+
export type DeclarationViolation = {
|
|
16
|
+
ruleId: DeclarationRuleId;
|
|
17
|
+
path: string;
|
|
18
|
+
message: string;
|
|
19
|
+
fix: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function declarationInvalidError(violations: readonly DeclarationViolation[]): ProviderError;
|
|
22
|
+
/** Enforces declaration rules whose runtime behavior would otherwise fail open. */
|
|
23
|
+
export declare function validateFailClosedDeclaration(provider: ProviderDefinition): void;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describeSchema } from "./contract-serialization.js";
|
|
2
|
+
import { ProviderError } from "./errors.js";
|
|
3
|
+
export const DECLARATION_INVALID_CODE = "DECLARATION_INVALID";
|
|
4
|
+
export const DECLARATION_RULE_IDS = {
|
|
5
|
+
challengeShape: "credentials-challenge-shape",
|
|
6
|
+
journeyExecutable: "health-journey-executable",
|
|
7
|
+
schemaSerializable: "operation-schema-serializable",
|
|
8
|
+
proxyExplicitPolicy: "proxy-explicit-policy",
|
|
9
|
+
proxyVendorExclusive: "proxy-vendor-fields-exclusive",
|
|
10
|
+
proxyNoMixedVendors: "proxy-no-mixed-vendors",
|
|
11
|
+
proxySmartproxyGeo: "proxy-smartproxy-country-only",
|
|
12
|
+
operationUpstreamProxy: "operation-upstream-proxy-unsupported",
|
|
13
|
+
};
|
|
14
|
+
export function declarationInvalidError(violations) {
|
|
15
|
+
const summary = violations
|
|
16
|
+
.map((violation) => `${violation.path} [${violation.ruleId}]: ${violation.message}`)
|
|
17
|
+
.join("\n");
|
|
18
|
+
return new ProviderError(`Provider declaration is invalid (${violations.length} violation${violations.length === 1 ? "" : "s"}).${summary ? `\n${summary}` : ""}`, {
|
|
19
|
+
code: DECLARATION_INVALID_CODE,
|
|
20
|
+
details: { violations: [...violations] },
|
|
21
|
+
fix: "Apply every violation's fix hint, then validate the declaration again.",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Enforces declaration rules whose runtime behavior would otherwise fail open. */
|
|
25
|
+
export function validateFailClosedDeclaration(provider) {
|
|
26
|
+
const violations = [];
|
|
27
|
+
validateHealthDeclaration(provider, violations);
|
|
28
|
+
validateSchemaDeclaration(provider, violations);
|
|
29
|
+
validateProxyDeclaration(provider, violations);
|
|
30
|
+
validateOperationDeclaration(provider, violations);
|
|
31
|
+
if (violations.length > 0)
|
|
32
|
+
throw declarationInvalidError(violations);
|
|
33
|
+
}
|
|
34
|
+
function validateHealthDeclaration(provider, violations) {
|
|
35
|
+
for (const [index, journey] of (provider.healthJourneys ?? []).entries()) {
|
|
36
|
+
if (!journey || typeof journey !== "object")
|
|
37
|
+
continue;
|
|
38
|
+
if (typeof journey.run !== "function") {
|
|
39
|
+
const journeyPath = healthJourneyPath(journey, index);
|
|
40
|
+
violations.push({
|
|
41
|
+
ruleId: DECLARATION_RULE_IDS.journeyExecutable,
|
|
42
|
+
path: `${journeyPath}.run`,
|
|
43
|
+
message: "coversOperations cannot provide health coverage without executable run logic.",
|
|
44
|
+
fix: `Add an async run(ctx) implementation to ${journeyPath}.`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// NOTE: healthCheck.cases[].enabled is intentionally NOT validated here.
|
|
49
|
+
// self-test.ts reports a gated case as status "skipped" with skipReason
|
|
50
|
+
// "disabled", so the skip is visible in results rather than silent — it is a
|
|
51
|
+
// supported conditional-execution feature, not a class-1 silent no-op.
|
|
52
|
+
}
|
|
53
|
+
function healthJourneyPath(journey, index) {
|
|
54
|
+
return typeof journey.id === "string" && journey.id.length > 0
|
|
55
|
+
? `healthJourneys.${journey.id}`
|
|
56
|
+
: `healthJourneys[${index}]`;
|
|
57
|
+
}
|
|
58
|
+
function validateSchemaDeclaration(provider, violations) {
|
|
59
|
+
for (const [operationId, operation] of Object.entries(provider.operations ?? {})) {
|
|
60
|
+
const schemaEntries = [
|
|
61
|
+
[`operations.${operationId}.input`, operation.input],
|
|
62
|
+
[`operations.${operationId}.output`, operation.output],
|
|
63
|
+
];
|
|
64
|
+
// SSE event schemas reach contract extraction the same way input/output do,
|
|
65
|
+
// so a transform-bearing event schema would abort extraction at runtime while
|
|
66
|
+
// passing declaration checks. Validate them under the same rule.
|
|
67
|
+
const transport = operation.transport;
|
|
68
|
+
if (transport?.kind === "sse") {
|
|
69
|
+
for (const [eventName, eventSchema] of Object.entries(transport.events ?? {})) {
|
|
70
|
+
schemaEntries.push([
|
|
71
|
+
`operations.${operationId}.transport.events.${eventName}`,
|
|
72
|
+
eventSchema,
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const [path, schema] of schemaEntries) {
|
|
77
|
+
try {
|
|
78
|
+
describeSchema(schema);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
82
|
+
violations.push({
|
|
83
|
+
ruleId: DECLARATION_RULE_IDS.schemaSerializable,
|
|
84
|
+
path,
|
|
85
|
+
message: `schema conversion to JSON Schema failed: ${reason}`,
|
|
86
|
+
fix: `Replace unsupported constructs in ${path} so z.toJSONSchema() succeeds.`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const MANAGED_PROXY_VENDORS = new Set(["smartproxy", "nodemaven"]);
|
|
93
|
+
const STATIC_PROXY_VENDORS = new Set(["custom", "decodo"]);
|
|
94
|
+
function validateProxyDeclaration(provider, violations) {
|
|
95
|
+
if (provider.proxy === true) {
|
|
96
|
+
violations.push({
|
|
97
|
+
ruleId: DECLARATION_RULE_IDS.proxyExplicitPolicy,
|
|
98
|
+
path: "proxy",
|
|
99
|
+
message: "proxy: true does not require resolvable proxy egress.",
|
|
100
|
+
fix: 'Replace proxy: true with an explicit policy such as proxy: { mode: "required", providers: ["smartproxy"] }.',
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!provider.proxy || typeof provider.proxy !== "object")
|
|
105
|
+
return;
|
|
106
|
+
const policy = provider.proxy;
|
|
107
|
+
const hasProvider = policy.provider !== undefined;
|
|
108
|
+
const hasProviders = policy.providers !== undefined;
|
|
109
|
+
if (hasProvider && hasProviders) {
|
|
110
|
+
violations.push({
|
|
111
|
+
ruleId: DECLARATION_RULE_IDS.proxyVendorExclusive,
|
|
112
|
+
path: "proxy",
|
|
113
|
+
message: "provider and providers are ambiguous when declared together.",
|
|
114
|
+
fix: "Keep either proxy.provider or proxy.providers, and remove the other field.",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const vendors = declaredProxyVendors(policy);
|
|
118
|
+
if (vendors.some((vendor) => MANAGED_PROXY_VENDORS.has(vendor)) &&
|
|
119
|
+
vendors.some((vendor) => STATIC_PROXY_VENDORS.has(vendor))) {
|
|
120
|
+
violations.push({
|
|
121
|
+
ruleId: DECLARATION_RULE_IDS.proxyNoMixedVendors,
|
|
122
|
+
path: hasProviders ? "proxy.providers" : "proxy.provider",
|
|
123
|
+
message: "managed and deprecated static proxy vendors cannot share a chain.",
|
|
124
|
+
fix: "Use only smartproxy/nodemaven vendors, or only deprecated static markers, in one policy.",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (vendors.includes("smartproxy")) {
|
|
128
|
+
for (const field of ["subdivision", "city"]) {
|
|
129
|
+
if (policy.geo?.[field] === undefined)
|
|
130
|
+
continue;
|
|
131
|
+
const path = `proxy.geo.${field}`;
|
|
132
|
+
violations.push({
|
|
133
|
+
ruleId: DECLARATION_RULE_IDS.proxySmartproxyGeo,
|
|
134
|
+
path,
|
|
135
|
+
message: `smartproxy cannot honor ${field}-level geo targeting.`,
|
|
136
|
+
fix: `Remove ${path} or use a vendor chain that can honor it.`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function declaredProxyVendors(policy) {
|
|
142
|
+
const vendors = [...(policy.providers ?? [])];
|
|
143
|
+
if (policy.provider !== undefined)
|
|
144
|
+
vendors.push(policy.provider);
|
|
145
|
+
return vendors;
|
|
146
|
+
}
|
|
147
|
+
function validateOperationDeclaration(provider, violations) {
|
|
148
|
+
for (const [operationId, operation] of Object.entries(provider.operations ?? {})) {
|
|
149
|
+
if (!operation.upstream?.proxy)
|
|
150
|
+
continue;
|
|
151
|
+
const path = `operations.${operationId}.upstream.proxy`;
|
|
152
|
+
violations.push({
|
|
153
|
+
ruleId: DECLARATION_RULE_IDS.operationUpstreamProxy,
|
|
154
|
+
path,
|
|
155
|
+
message: "operation-level proxy policy is not wired into operation execution.",
|
|
156
|
+
fix: `Remove ${path} and declare the effective policy at provider.proxy.`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
package/dist/define.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ interface ProviderImplementationProfile {
|
|
|
9
9
|
visibility: "internal" | "operator";
|
|
10
10
|
}
|
|
11
11
|
export declare const VALID_PROVIDER_RESOLVER_VENDORS: readonly ["browser", "capsolver", "capmonster", "2captcha", "custom"];
|
|
12
|
-
export declare const VALID_PROVIDER_CHALLENGE_KINDS: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf"];
|
|
12
|
+
export declare const VALID_PROVIDER_CHALLENGE_KINDS: readonly ["turnstile", "recaptcha_v2", "recaptcha_v3", "hcaptcha", "cloudflare_interstitial", "aws_waf", "akamai_sec_cpt", "akamai_sensor"];
|
|
13
13
|
type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
|
|
14
14
|
type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<OperationDefinition<TInput, TOutput>, "handler"> & {
|
|
15
15
|
handler(ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0], input: InferSchemaOutput<TInput>): OperationHandlerResult<InferSchemaOutput<TOutput>> | Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
|
package/dist/define.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ms from "ms";
|
|
2
|
+
import { validateFailClosedDeclaration } from "./declaration-validation.js";
|
|
2
3
|
import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
|
|
3
4
|
import { ProviderError, ValidationError } from "./errors.js";
|
|
4
5
|
import { NativeEgressPolicyValidationError, validateNativeProviderConfig, } from "./native-egress-policy.js";
|
|
@@ -66,6 +67,8 @@ export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray()([
|
|
|
66
67
|
"hcaptcha",
|
|
67
68
|
"cloudflare_interstitial",
|
|
68
69
|
"aws_waf",
|
|
70
|
+
"akamai_sec_cpt",
|
|
71
|
+
"akamai_sensor",
|
|
69
72
|
]);
|
|
70
73
|
const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
71
74
|
const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
@@ -423,9 +426,15 @@ function validateProviderResolver(config) {
|
|
|
423
426
|
fix: `Set resolver for provider "${config.id}" to { vendors: ["2captcha"], kinds: ["turnstile"] }.`,
|
|
424
427
|
});
|
|
425
428
|
}
|
|
426
|
-
rejectUnknownFields(resolver, new Set(["vendors", "kinds"]), "resolver", config.id);
|
|
429
|
+
rejectUnknownFields(resolver, new Set(["vendors", "kinds", "clientProfile"]), "resolver", config.id);
|
|
427
430
|
validateResolverLiteralArray(resolver.vendors, "resolver.vendors", VALID_PROVIDER_RESOLVER_VENDORS, config.id);
|
|
428
431
|
validateResolverLiteralArray(resolver.kinds, "resolver.kinds", VALID_PROVIDER_CHALLENGE_KINDS, config.id);
|
|
432
|
+
if (resolver.clientProfile !== undefined &&
|
|
433
|
+
(typeof resolver.clientProfile !== "string" || !resolver.clientProfile.trim())) {
|
|
434
|
+
throw new ValidationError(`Provider "${config.id}" has invalid resolver.clientProfile: must be a non-empty string.`, {
|
|
435
|
+
fix: `Set resolver.clientProfile for provider "${config.id}" to a transport-owned profile name.`,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
429
438
|
}
|
|
430
439
|
function validateResolverLiteralArray(value, field, validValues, providerId) {
|
|
431
440
|
if (!Array.isArray(value)) {
|
|
@@ -1616,7 +1625,7 @@ export function defineProvider(config) {
|
|
|
1616
1625
|
});
|
|
1617
1626
|
if (config.browser && config.runtime !== "browser")
|
|
1618
1627
|
throw new ProviderError(`Provider "${config.id}" cannot define browser config unless runtime is "browser"`, { fix: 'Set runtime: "browser" or remove the browser config' });
|
|
1619
|
-
|
|
1628
|
+
const provider = {
|
|
1620
1629
|
id: config.id,
|
|
1621
1630
|
version: config.version,
|
|
1622
1631
|
runtime: config.runtime,
|
|
@@ -1645,4 +1654,6 @@ export function defineProvider(config) {
|
|
|
1645
1654
|
healthProbe: config.healthProbe ?? config.healthMonitor,
|
|
1646
1655
|
healthJourneys: config.healthJourneys,
|
|
1647
1656
|
};
|
|
1657
|
+
validateFailClosedDeclaration(provider);
|
|
1658
|
+
return provider;
|
|
1648
1659
|
}
|