@apifuse/provider-sdk 2.2.0-beta.24 → 2.2.0-beta.26

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.
Files changed (66) hide show
  1. package/AUTHORING.md +7 -6
  2. package/CHANGELOG.md +9 -1
  3. package/README.md +3 -3
  4. package/bin/apifuse-check.ts +62 -3
  5. package/bin/apifuse-pack-check.ts +8 -2
  6. package/bin/apifuse-pack-smoke.ts +43 -2
  7. package/bin/apifuse-pack-types.ts +58 -0
  8. package/bin/apifuse-submit-check.ts +15 -2
  9. package/dist/auth.js +29 -0
  10. package/dist/cli/templates/provider/README.md.tpl +4 -4
  11. package/dist/contract-serialization.d.ts +20 -1
  12. package/dist/contract-serialization.js +583 -8
  13. package/dist/contract.d.ts +2 -0
  14. package/dist/contract.js +9 -5
  15. package/dist/declaration-validation.d.ts +23 -0
  16. package/dist/declaration-validation.js +159 -0
  17. package/dist/define.d.ts +1 -1
  18. package/dist/define.js +13 -2
  19. package/dist/index.d.ts +4 -3
  20. package/dist/index.js +3 -3
  21. package/dist/lint.js +85 -3
  22. package/dist/provider.d.ts +1 -1
  23. package/dist/provider.js +1 -1
  24. package/dist/runtime/cache.d.ts +1 -0
  25. package/dist/runtime/cache.js +169 -15
  26. package/dist/runtime/resolver-vendors/bindings.d.ts +42 -2
  27. package/dist/runtime/resolver-vendors/bindings.js +31 -6
  28. package/dist/runtime/resolver-vendors/browser.d.ts +3 -7
  29. package/dist/runtime/resolver-vendors/browser.js +7 -22
  30. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  31. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  32. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +23 -0
  33. package/dist/runtime/resolver-vendors/twocaptcha.js +264 -0
  34. package/dist/runtime/resolver-vendors/types.d.ts +44 -3
  35. package/dist/runtime/resolver-vendors/types.js +10 -0
  36. package/dist/runtime/resolver.d.ts +17 -2
  37. package/dist/runtime/resolver.js +237 -15
  38. package/dist/runtime/stealth.d.ts +26 -4
  39. package/dist/runtime/stealth.js +224 -114
  40. package/dist/schema.d.ts +63 -0
  41. package/dist/schema.js +808 -8
  42. package/dist/server/serve.js +8 -0
  43. package/dist/stealth/profiles.js +16 -7
  44. package/dist/types.d.ts +37 -4
  45. package/package.json +2 -2
  46. package/src/auth.ts +40 -0
  47. package/src/cli/templates/provider/README.md.tpl +4 -4
  48. package/src/contract-serialization.ts +857 -8
  49. package/src/contract.ts +16 -5
  50. package/src/declaration-validation.ts +202 -0
  51. package/src/define.ts +23 -2
  52. package/src/index.ts +13 -0
  53. package/src/lint.ts +98 -3
  54. package/src/provider.ts +10 -0
  55. package/src/runtime/cache.ts +189 -14
  56. package/src/runtime/resolver-vendors/bindings.ts +40 -15
  57. package/src/runtime/resolver-vendors/browser.ts +9 -31
  58. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  59. package/src/runtime/resolver-vendors/twocaptcha.ts +366 -0
  60. package/src/runtime/resolver-vendors/types.ts +54 -0
  61. package/src/runtime/resolver.ts +304 -24
  62. package/src/runtime/stealth.ts +317 -136
  63. package/src/schema.ts +1060 -9
  64. package/src/server/serve.ts +8 -0
  65. package/src/stealth/profiles.ts +17 -7
  66. package/src/types.ts +39 -6
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 optional 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.
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 `impit` behind that interface, so do
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/Firefox-style profiles; use `browser.engine:
740
- "playwright-stealth"` for Safari-specific or real browser Providers
741
- (`nodriver` is Python-runtime only); install local browser assets with
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 impit streams them. It applies to every redirect hop, uses a
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.26
4
+
5
+ - Release candidate for main commit 924fe13d1101e7840d799a562a7a70174355185d.
6
+
7
+ ## 2.2.0-beta.25
8
+
9
+ - Release candidate for main commit e6df658b95c0728b671fd221919ec2f85f82b0b7.
10
+
3
11
  ## 2.2.0-beta.24
4
12
 
5
13
  - Release candidate for main commit 0f21a2959dcbb9b96fe426259259d0870cfc537f.
@@ -201,7 +209,7 @@
201
209
 
202
210
  ## 2.1.0-beta.3
203
211
 
204
- - Replace the legacy TypeScript request transport with `ctx.stealth`, backed by `impit` browser-grade TLS/HTTP2 impersonation without Python runtime dependencies.
212
+ - Replace the legacy TypeScript request transport with `ctx.stealth`, backed by browser-grade TLS/HTTP2 impersonation without Python runtime dependencies.
205
213
  - Add the public `apifuse submit-check` / `apifuse bounty-check` CLI for score-based pre-submission provider quality checks.
206
214
  - Ship `SUBMISSION.md` in the npm package so bounty contributors can follow the checklist without access to the private monorepo.
207
215
  - 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 impit-backed browser stealth transport and
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/Firefox-style profiles are
149
- supported; use `ctx.browser` when Safari-specific behavior is required.
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
@@ -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
- const providerModule = existsSync(indexPath)
123
- ? await import(pathToFileURL(indexPath).href)
124
- : undefined;
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("impit")) {
159
+ if (!content.includes("wreq-js")) {
160
160
  throw new Error(
161
- `${label} must include impit stealth runtime guidance for TLS/browser bounties.`,
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("impit")) {
231
- throw new Error("Generated README is missing impit stealth runtime guidance.");
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",
@@ -1691,12 +1691,25 @@ function isScannableProviderSourceFile(relativePath: string): boolean {
1691
1691
  );
1692
1692
  }
1693
1693
 
1694
+ const EXCLUDED_SOURCE_DIRECTORY_SEGMENTS = new Set([
1695
+ ".git",
1696
+ ".worktree",
1697
+ "node_modules",
1698
+ "dist",
1699
+ "build",
1700
+ "coverage",
1701
+ ]);
1702
+
1694
1703
  // `.agents`/`.apifuse` stay IN scope on purpose: managed content there is
1695
1704
  // markdown/JSON (never scannable), while a planted `.ts`/`.sh` under those
1696
1705
  // directories must not become a scan-exempt hiding place for secrets, raw
1697
- // fetch, or vendor imports.
1706
+ // fetch, or vendor imports. Nested dependency/build output directories under
1707
+ // them use the same segment exclusions; `.worktree` is excluded because task
1708
+ // worktrees are duplicate repository checkouts, not provider source.
1698
1709
  function shouldScanSourceDirectory(relativePath: string): boolean {
1699
- return ![".git", "node_modules", "dist", "build", "coverage"].includes(relativePath);
1710
+ return relativePath
1711
+ .split("/")
1712
+ .every((segment) => !EXCLUDED_SOURCE_DIRECTORY_SEGMENTS.has(segment));
1700
1713
  }
1701
1714
 
1702
1715
  function isExcludedTestSource(relativePath: string): boolean {
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 `impit` internally.
118
- `ctx.stealth` supports Chrome/Firefox-style profiles. For TypeScript browser
119
- Providers or Safari-specific behavior use `browser.engine: "playwright-stealth"`
120
- (`nodriver` is Python-runtime only), then install local Chromium with
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
@@ -1,4 +1,23 @@
1
1
  import { type JsonValue } from "./contract-json.js";
2
2
  import type { SchemaLike } from "./types.js";
3
- export declare function describeSchema(schema: SchemaLike): JsonValue;
3
+ interface DescribeSchemaOptions {
4
+ readonly eventName?: string;
5
+ readonly operationId?: string;
6
+ readonly outputTextTrust?: boolean;
7
+ }
8
+ export declare class OutputTextTrustProjectionError extends Error {
9
+ readonly schemaPath: string;
10
+ readonly operationId?: string | undefined;
11
+ readonly eventName?: string | undefined;
12
+ readonly classification: "untrusted";
13
+ readonly code = "output_text_trust_projection_failed";
14
+ constructor(schemaPath: string, cause: unknown, operationId?: string | undefined, eventName?: string | undefined);
15
+ }
16
+ export declare class OutputTextTrustProjectionMarkerError extends TypeError {
17
+ readonly schemaPath: string;
18
+ readonly code = "invalid_output_text_trust_projection_marker";
19
+ constructor(schemaPath: string);
20
+ }
21
+ export declare function describeSchema(schema: SchemaLike, options?: DescribeSchemaOptions): JsonValue;
4
22
  export declare function serializeSmsMatcher(value: Record<string, unknown>): Record<string, unknown>;
23
+ export {};