@apifuse/provider-sdk 2.2.0-beta.58 → 2.2.0-beta.60
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 +25 -0
- package/CHANGELOG.md +8 -0
- package/README.md +18 -2
- package/bin/apifuse-dev.ts +2 -1
- package/bin/apifuse-pack-types.ts +49 -0
- package/bin/apifuse-perf.ts +41 -4
- package/bin/apifuse-record.ts +9 -2
- package/dist/cli/templates/provider/README.md.tpl +15 -3
- package/dist/config/loader.d.ts +7 -0
- package/dist/config/loader.js +43 -18
- package/dist/declaration-validation.d.ts +2 -1
- package/dist/declaration-validation.js +39 -0
- package/dist/define.d.ts +4 -4
- package/dist/define.js +25 -1
- package/dist/engine.d.ts +16 -0
- package/dist/engine.js +59 -3
- package/dist/error-resolution.js +13 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +4 -2
- package/dist/lint.js +38 -17
- 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/browser.js +2 -1
- package/dist/runtime/cache.d.ts +2 -0
- package/dist/runtime/cache.js +11 -3
- package/dist/runtime/diagnostic-env-values.d.ts +5 -0
- package/dist/runtime/diagnostic-env-values.js +57 -0
- package/dist/runtime/diagnostic-env.d.ts +11 -0
- package/dist/runtime/diagnostic-env.js +35 -0
- package/dist/runtime/diagnostic-redactor.d.ts +49 -0
- package/dist/runtime/diagnostic-redactor.js +569 -0
- package/dist/runtime/egress-lease.d.ts +65 -0
- package/dist/runtime/egress-lease.js +236 -0
- package/dist/runtime/env.js +2 -1
- package/dist/runtime/http.js +1 -0
- package/dist/runtime/instrumentation.js +3 -0
- package/dist/runtime/ocr.js +2 -1
- package/dist/runtime/otlp.js +5 -4
- package/dist/runtime/proxy-nodemaven.js +3 -2
- package/dist/runtime/proxy-retry-policy.d.ts +4 -1
- package/dist/runtime/proxy-retry-policy.js +11 -2
- package/dist/runtime/request-options.d.ts +7 -0
- package/dist/runtime/request-options.js +62 -22
- 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/browser.js +0 -1
- 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 +400 -111
- 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/runtime/stt.js +3 -2
- package/dist/runtime/trace.d.ts +5 -0
- package/dist/runtime/trace.js +115 -20
- package/dist/schema.d.ts +20 -0
- package/dist/schema.js +72 -2
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +1 -0
- package/dist/server/self-test-redaction.js +8 -33
- package/dist/server/self-test.d.ts +4 -0
- package/dist/server/self-test.js +10 -1
- package/dist/server/sensitive-values.d.ts +26 -0
- package/dist/server/sensitive-values.js +131 -0
- package/dist/server/serve-implementation.d.ts +7 -1
- package/dist/server/serve-implementation.js +394 -125
- package/dist/server/trace-output.d.ts +2 -1
- package/dist/server/trace-output.js +46 -14
- 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/trace-sanitization.d.ts +4 -3
- package/dist/trace-sanitization.js +45 -22
- package/dist/types.d.ts +106 -10
- package/package.json +2 -1
- package/src/cli/templates/provider/README.md.tpl +15 -3
- package/src/config/loader.ts +47 -18
- package/src/declaration-validation.ts +47 -1
- package/src/define.ts +45 -3
- package/src/engine.ts +78 -3
- package/src/error-resolution.ts +13 -0
- package/src/index.ts +34 -1
- package/src/lint.ts +44 -18
- package/src/provider.ts +10 -0
- package/src/runtime/bounded-expiring-map.ts +67 -0
- package/src/runtime/browser.ts +2 -1
- package/src/runtime/cache.ts +13 -3
- package/src/runtime/diagnostic-env-values.ts +56 -0
- package/src/runtime/diagnostic-env.ts +55 -0
- package/src/runtime/diagnostic-redactor.ts +662 -0
- package/src/runtime/egress-lease.ts +341 -0
- package/src/runtime/env.ts +2 -1
- package/src/runtime/http.ts +1 -0
- package/src/runtime/instrumentation.ts +3 -0
- package/src/runtime/ocr.ts +2 -1
- package/src/runtime/otlp.ts +5 -4
- package/src/runtime/proxy-nodemaven.ts +3 -2
- package/src/runtime/proxy-retry-policy.ts +12 -1
- package/src/runtime/request-options.ts +76 -28
- 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/browser.ts +0 -1
- 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 +468 -126
- package/src/runtime/stealth-akamai-sbsd.ts +207 -0
- package/src/runtime/stealth.ts +727 -95
- package/src/runtime/stt.ts +3 -2
- package/src/runtime/trace.ts +168 -26
- package/src/schema.ts +80 -2
- package/src/server/index.ts +21 -0
- package/src/server/self-test-redaction.ts +10 -40
- package/src/server/self-test.ts +13 -0
- package/src/server/sensitive-values.ts +200 -0
- package/src/server/serve-implementation.ts +561 -136
- package/src/server/trace-output.ts +77 -17
- package/src/server/types.ts +14 -0
- package/src/stealth/profiles.ts +10 -0
- package/src/testing/run.ts +6 -0
- package/src/trace-sanitization.ts +70 -23
- package/src/types.ts +124 -19
package/AUTHORING.md
CHANGED
|
@@ -130,6 +130,12 @@ not list `APIFUSE__PROXY__SMARTPROXY_APP_KEY`,
|
|
|
130
130
|
`APIFUSE__PROXY__NODEMAVEN_PASSWORD` in provider `secrets`; `proxy` contains
|
|
131
131
|
policy intent only.
|
|
132
132
|
|
|
133
|
+
Hosted runtime settings are engine-owned as well: every `APIFUSE__CDP_POOL__*`
|
|
134
|
+
variable, the Cloudflare OCR/STT tokens and account identifier,
|
|
135
|
+
`APIFUSE__OCR__API_KEY`, `APIFUSE__CACHE__KEY_PEPPER`, and
|
|
136
|
+
`APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET`. `defineProvider`
|
|
137
|
+
rejects them in `secrets`; the engine reads them from its own environment.
|
|
138
|
+
|
|
133
139
|
### Factored operations
|
|
134
140
|
|
|
135
141
|
`defineProvider(declaration)` returns the builder that accepts `operations`, so
|
|
@@ -220,6 +226,22 @@ const utcInput = resolveHealthCheckInputDateTokens({ date: "+45d" }, new Date(),
|
|
|
220
226
|
Do not re-resolve a health assertion's dates in UTC when its case input used the
|
|
221
227
|
default KST calendar.
|
|
222
228
|
|
|
229
|
+
### Sensitive vs public fields
|
|
230
|
+
|
|
231
|
+
The platform fixture-privacy ratchet flags name-, phone-, and address-shaped keys
|
|
232
|
+
unless the schema at that path declares them one way or the other. Declare every hit:
|
|
233
|
+
|
|
234
|
+
- `sensitive(schema, kind)` / `fields.*()` — anything about a person: user, profile,
|
|
235
|
+
passenger, driver, or member names, emails, member ids, personal phone numbers.
|
|
236
|
+
Emits `x-apifuse-sensitive: true`; the value is redacted in published artifacts.
|
|
237
|
+
- `publicField(schema)` — reviewed public data: business, shop, product, artist, or
|
|
238
|
+
facility names, hospital/hotel/station phones, timezone names. Emits
|
|
239
|
+
`x-apifuse-sensitive: false`; nothing is redacted. It is a review verdict, not a mute.
|
|
240
|
+
Credential-shaped keys (token, password, apiKey, …) cannot be declared public.
|
|
241
|
+
|
|
242
|
+
Apply either helper to the leaf, before `.optional()`/`.nullable()`/`.default()`;
|
|
243
|
+
`publicField()` over a wrapper that hides a `sensitive()` leaf throws at definition time.
|
|
244
|
+
|
|
223
245
|
### Real-handler E2E in standard tests
|
|
224
246
|
|
|
225
247
|
`runStandardTests(provider)` validates declarations and fixtures but reports a
|
|
@@ -501,6 +523,9 @@ SDK observability is emitted separately in the
|
|
|
501
523
|
```
|
|
502
524
|
|
|
503
525
|
Treat this header as telemetry, not as provider-controlled public error detail.
|
|
526
|
+
It is SDK-owned even when a handler returns a raw `Response`: a caller-set
|
|
527
|
+
header is dropped, and a raw status of 400 or above gets a status-derived one
|
|
528
|
+
(retryable only for 408, 429, and 5xx) while the body is left untouched.
|
|
504
529
|
Its category, taxonomy version, retryability, and optional upstream status match
|
|
505
530
|
the structured `provider_request_failed` log event.
|
|
506
531
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0-beta.60
|
|
4
|
+
|
|
5
|
+
- Release candidate for main commit 5f417c6282b3a4d92bf31c8258ba913b5e53dc8b.
|
|
6
|
+
|
|
7
|
+
## 2.2.0-beta.59
|
|
8
|
+
|
|
9
|
+
- Release candidate for main commit 55ae556fcf33e28a228c2d54ce76e7afa41e5dc7.
|
|
10
|
+
|
|
3
11
|
## 2.2.0-beta.58
|
|
4
12
|
|
|
5
13
|
- Release candidate for main commit e137940caa8bc32ce768571a6197651e581c2270.
|
package/README.md
CHANGED
|
@@ -76,7 +76,14 @@ The operation request body is the same envelope used by the APIFuse gateway:
|
|
|
76
76
|
| `requestId` | yes | Any unique string is fine for local debugging; it is echoed in structured errors. |
|
|
77
77
|
| `input` | yes | The operation input after schema validation. |
|
|
78
78
|
| `headers` | no | Extra caller headers to expose through `ctx.request.headers`. |
|
|
79
|
-
| `
|
|
79
|
+
| `connectionId` | no | Connection identity only; it does not include credentials. The gateway sends it for `optional` connection mode, and only when the caller supplied a connection that passed authorization. Exposed as `ctx.request.connectionId`. |
|
|
80
|
+
| `connection` | no | Credential-bearing connection data. The gateway sends it for `required` connection mode; omit it for no-auth/public (`none` mode) operations. For local debugging, pass an object with `id`, `mode`, `secrets`, `metadata`, and `externalRef`. Do not pass `null`. |
|
|
81
|
+
|
|
82
|
+
The gateway sends only `connection` for `required` mode, only `connectionId`
|
|
83
|
+
for `optional` mode (and nothing when the caller passed no connection), and
|
|
84
|
+
neither field for `none` mode. If a malformed or manually constructed envelope
|
|
85
|
+
contains both, a non-empty nested `connection.id` takes precedence over the
|
|
86
|
+
top-level `connectionId`; an empty string is treated as absent.
|
|
80
87
|
|
|
81
88
|
Credential-bearing local smoke example:
|
|
82
89
|
|
|
@@ -146,7 +153,11 @@ the bad request path; provider/runtime failures include `code`, `message`, and
|
|
|
146
153
|
a session with `ctx.stealth.createSession()` and use `session.redirects.run()`;
|
|
147
154
|
inspect accumulated cookies through `session.cookies`. Select an SDK stealth
|
|
148
155
|
identity with structured `stealth: { browser, os }` options; omitted `os`
|
|
149
|
-
explicitly defaults to `macos`.
|
|
156
|
+
explicitly defaults to `macos`. The transport owns `User-Agent`,
|
|
157
|
+
`Accept-Encoding`, `sec-ch-ua*`, and every `Sec-Fetch-*` header and throws
|
|
158
|
+
`STEALTH_HEADER_OVERRIDE_UNSUPPORTED` if you set one; declare
|
|
159
|
+
`stealth.requestClass: "navigation" | "xhr" | "post"` instead. Do not pin a
|
|
160
|
+
browser version
|
|
150
161
|
or tune JA3, HTTP/2 SETTINGS, or
|
|
151
162
|
pseudo-header order in provider code. Chrome, Firefox, and Safari profiles
|
|
152
163
|
are supported; use `ctx.browser` when the provider needs browser execution.
|
|
@@ -325,3 +336,8 @@ External bounty contributors should submit standalone Provider source plus
|
|
|
325
336
|
`bun run check` / `bun run test` evidence. APIFuse maintainers own monorepo
|
|
326
337
|
import, registry generation, deployment projection checks, and release
|
|
327
338
|
publishing.
|
|
339
|
+
|
|
340
|
+
Relayed user-generated text (listing bodies, reviews, posts) is contained by the
|
|
341
|
+
platform, not by the provider. Wrap such output fields in `untrustedContent(...)`
|
|
342
|
+
so the schema carries `x-apifuse-content-trust: untrusted` and
|
|
343
|
+
`x-apifuse-content-provenance: external`; do not strip instruction-like prose.
|
package/bin/apifuse-dev.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { readDiagnosticEnv } from "../src/runtime/diagnostic-env.js";
|
|
2
3
|
|
|
3
4
|
import { existsSync } from "node:fs";
|
|
4
5
|
import { dirname, relative, resolve } from "node:path";
|
|
@@ -87,7 +88,7 @@ export function createProviderContext(provider: ProviderDefinition): {
|
|
|
87
88
|
process.env,
|
|
88
89
|
provider.secrets?.map((secret) => secret.name) ?? [],
|
|
89
90
|
);
|
|
90
|
-
const providerEnv = { get: (key: string) =>
|
|
91
|
+
const providerEnv = { get: (key: string) => readDiagnosticEnv(key, providerEnvironment) };
|
|
91
92
|
const engineEnv = createEnvContext([PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV]);
|
|
92
93
|
const engineCredentials = readEngineProxyCredentials();
|
|
93
94
|
const credential = createCredentialContext();
|
|
@@ -401,6 +401,55 @@ const NEGATIVE_CONTROLS = [
|
|
|
401
401
|
"",
|
|
402
402
|
].join("\n"),
|
|
403
403
|
},
|
|
404
|
+
{
|
|
405
|
+
filename: "negative-control-resolver-telemetry-description.ts",
|
|
406
|
+
expectedCode: "TS2353",
|
|
407
|
+
description: "resolver gateway telemetry rejects vendor error descriptions",
|
|
408
|
+
source: [
|
|
409
|
+
'import type { ResolverTelemetryHeaderPayload } from "@apifuse/provider-sdk";',
|
|
410
|
+
"",
|
|
411
|
+
"export const mustNotCompile: ResolverTelemetryHeaderPayload = {",
|
|
412
|
+
"\tattempts: 0,",
|
|
413
|
+
"\tfailovers: 0,",
|
|
414
|
+
"\tvendorChain: [],",
|
|
415
|
+
"\tpollCount: 0,",
|
|
416
|
+
'\tvendorErrorDescription: "free text must stay log-only",',
|
|
417
|
+
"};",
|
|
418
|
+
"",
|
|
419
|
+
].join("\n"),
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
filename: "negative-control-resolver-telemetry-phase.ts",
|
|
423
|
+
expectedCode: "TS2322",
|
|
424
|
+
description: "resolver gateway telemetry rejects phases outside its closed vocabulary",
|
|
425
|
+
source: [
|
|
426
|
+
'import { closedEnum, type ResolverTelemetryHeaderPayload } from "@apifuse/provider-sdk";',
|
|
427
|
+
"",
|
|
428
|
+
"export const mustNotCompile: ResolverTelemetryHeaderPayload = {",
|
|
429
|
+
"\tattempts: 1,",
|
|
430
|
+
"\tfailovers: 0,",
|
|
431
|
+
'\tvendorChain: [closedEnum("hypersolutions")],',
|
|
432
|
+
"\tpollCount: 0,",
|
|
433
|
+
'\tattemptSamples: [{ v: closedEnum("hypersolutions"), p: closedEnum("solve_exchange"), o: closedEnum("error"), ms: 1 }],',
|
|
434
|
+
"};",
|
|
435
|
+
"",
|
|
436
|
+
].join("\n"),
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
filename: "positive-control-resolver-telemetry-contributor.ts",
|
|
440
|
+
expectedCode: "",
|
|
441
|
+
description: "resolver telemetry contributor satisfies the gateway-ingestible contract",
|
|
442
|
+
source: [
|
|
443
|
+
'import { closedEnum, ResolverTelemetryCollector, type GatewayIngestible, type ResolverTelemetryHeaderPayload, type ResolverTelemetryLogPayload, type ResolverTelemetryPhase, type TelemetryContributor } from "@apifuse/provider-sdk";',
|
|
444
|
+
"",
|
|
445
|
+
"export const resolver: TelemetryContributor<ResolverTelemetryLogPayload, ResolverTelemetryHeaderPayload> = new ResolverTelemetryCollector();",
|
|
446
|
+
'const hyperPhases: ResolverTelemetryPhase[] = ["measure_ip", "fetch_script", "generate_payload", "post_payload"];',
|
|
447
|
+
'export const hyperPhaseHeader: ResolverTelemetryHeaderPayload = { attempts: 1, failovers: 0, vendorChain: [closedEnum("hypersolutions")], pollCount: 0, attemptSamples: hyperPhases.map((phase) => ({ v: closedEnum("hypersolutions"), p: closedEnum(phase), o: closedEnum("ok"), ms: 1 })) };',
|
|
448
|
+
"declare const header: ResolverTelemetryHeaderPayload;",
|
|
449
|
+
"export const ingestible: GatewayIngestible<ResolverTelemetryHeaderPayload> = header;",
|
|
450
|
+
"",
|
|
451
|
+
].join("\n"),
|
|
452
|
+
},
|
|
404
453
|
] as const;
|
|
405
454
|
|
|
406
455
|
const tempRoot = mkdtempSync(join(tmpdir(), "apifuse-provider-sdk-pack-types-"));
|
package/bin/apifuse-perf.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @ts-nocheck
|
|
3
|
+
import { readDiagnosticEnv } from "../src/runtime/diagnostic-env.js";
|
|
3
4
|
|
|
4
5
|
import { existsSync } from "node:fs";
|
|
5
6
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
@@ -25,11 +26,20 @@ import {
|
|
|
25
26
|
type StealthResponse,
|
|
26
27
|
wrapWithInstrumentation,
|
|
27
28
|
} from "../src/index.js";
|
|
29
|
+
import {
|
|
30
|
+
bindDiagnosticSensitiveRegistry,
|
|
31
|
+
type CompiledDiagnosticSensitiveValues,
|
|
32
|
+
compileProcessDiagnosticSensitiveValues,
|
|
33
|
+
createDiagnosticRedactor,
|
|
34
|
+
redactDiagnosticText,
|
|
35
|
+
} from "../src/runtime/diagnostic-redactor.js";
|
|
28
36
|
import { computeStats, groupSpansByName, type PerfStats } from "../src/runtime/perf.js";
|
|
29
37
|
import { createMemoryProviderRuntimeState } from "../src/runtime/state.js";
|
|
30
38
|
import { createStealthClient } from "../src/runtime/stealth.js";
|
|
31
39
|
import { createTraceContext, resolveTraceContextOptions } from "../src/runtime/trace.js";
|
|
32
40
|
import { renderWaterfall } from "../src/runtime/waterfall.js";
|
|
41
|
+
import { collectStaticDiagnosticSensitiveValues } from "../src/server/sensitive-values.js";
|
|
42
|
+
import { sanitizeSpanForOutput } from "../src/trace-sanitization.js";
|
|
33
43
|
import type { BrowserClient } from "../src/types.js";
|
|
34
44
|
|
|
35
45
|
type CliArgs = {
|
|
@@ -100,6 +110,9 @@ export async function main() {
|
|
|
100
110
|
const providerDirectory = resolve(process.cwd(), args.providerPath);
|
|
101
111
|
const providerEntry = resolveProviderEntry(providerDirectory);
|
|
102
112
|
const provider = await loadProvider(providerEntry);
|
|
113
|
+
const staticSensitiveValues = compileProcessDiagnosticSensitiveValues(
|
|
114
|
+
collectStaticDiagnosticSensitiveValues(provider),
|
|
115
|
+
);
|
|
103
116
|
const providerId = basename(providerDirectory);
|
|
104
117
|
const config = await loadApiFuseConfig(process.cwd());
|
|
105
118
|
const operation = getOperation(provider, args.operation);
|
|
@@ -119,6 +132,7 @@ export async function main() {
|
|
|
119
132
|
operationName: args.operation,
|
|
120
133
|
outputSchema,
|
|
121
134
|
proxyEnabled: false,
|
|
135
|
+
staticSensitiveValues,
|
|
122
136
|
});
|
|
123
137
|
|
|
124
138
|
let proxySuite: ProfileSuite | undefined;
|
|
@@ -135,6 +149,7 @@ export async function main() {
|
|
|
135
149
|
operationName: args.operation,
|
|
136
150
|
outputSchema,
|
|
137
151
|
proxyEnabled: true,
|
|
152
|
+
staticSensitiveValues,
|
|
138
153
|
});
|
|
139
154
|
}
|
|
140
155
|
|
|
@@ -170,8 +185,8 @@ export async function main() {
|
|
|
170
185
|
runs: args.runs,
|
|
171
186
|
warmup: args.warmup,
|
|
172
187
|
concurrency: args.concurrency,
|
|
173
|
-
direct: directSuite,
|
|
174
|
-
proxy: proxySuite,
|
|
188
|
+
direct: sanitizeProfileSuiteForExport(directSuite),
|
|
189
|
+
proxy: proxySuite ? sanitizeProfileSuiteForExport(proxySuite) : undefined,
|
|
175
190
|
flamePath,
|
|
176
191
|
});
|
|
177
192
|
console.log(`Exported JSON: ${resolve(process.cwd(), args.exportPath)}`);
|
|
@@ -480,6 +495,7 @@ async function runProfileSuite(options: {
|
|
|
480
495
|
operationName: string;
|
|
481
496
|
outputSchema: { parse(input: unknown): unknown };
|
|
482
497
|
proxyEnabled: boolean;
|
|
498
|
+
staticSensitiveValues: CompiledDiagnosticSensitiveValues;
|
|
483
499
|
}): Promise<ProfileSuite> {
|
|
484
500
|
for (let index = 0; index < options.args.warmup; index += 1) {
|
|
485
501
|
await profileRun({
|
|
@@ -492,6 +508,7 @@ async function runProfileSuite(options: {
|
|
|
492
508
|
operationName: options.operationName,
|
|
493
509
|
outputSchema: options.outputSchema,
|
|
494
510
|
proxyEnabled: options.proxyEnabled,
|
|
511
|
+
staticSensitiveValues: options.staticSensitiveValues,
|
|
495
512
|
});
|
|
496
513
|
}
|
|
497
514
|
|
|
@@ -509,6 +526,7 @@ async function runProfileSuite(options: {
|
|
|
509
526
|
operationName: options.operationName,
|
|
510
527
|
outputSchema: options.outputSchema,
|
|
511
528
|
proxyEnabled: options.proxyEnabled,
|
|
529
|
+
staticSensitiveValues: options.staticSensitiveValues,
|
|
512
530
|
}),
|
|
513
531
|
);
|
|
514
532
|
runs.push(...(await Promise.all(batch)));
|
|
@@ -545,6 +563,7 @@ async function profileRun(options: {
|
|
|
545
563
|
operationName: string;
|
|
546
564
|
outputSchema: { parse(input: unknown): unknown };
|
|
547
565
|
proxyEnabled: boolean;
|
|
566
|
+
staticSensitiveValues: CompiledDiagnosticSensitiveValues;
|
|
548
567
|
}): Promise<RunResult> {
|
|
549
568
|
try {
|
|
550
569
|
return await executeProfileRun({ ...options, forceFixtureReplay: false });
|
|
@@ -576,9 +595,16 @@ async function executeProfileRun(options: {
|
|
|
576
595
|
operationName: string;
|
|
577
596
|
outputSchema: { parse(input: unknown): unknown };
|
|
578
597
|
proxyEnabled: boolean;
|
|
598
|
+
staticSensitiveValues: CompiledDiagnosticSensitiveValues;
|
|
579
599
|
}): Promise<RunResult> {
|
|
580
600
|
const _operation = getOperation(options.provider, options.operationName);
|
|
581
|
-
const
|
|
601
|
+
const sensitiveRegistry = createDiagnosticRedactor([], options.staticSensitiveValues);
|
|
602
|
+
const redact = (text: string) => redactDiagnosticText(text, sensitiveRegistry.redact);
|
|
603
|
+
const traceContext = createTraceContext({
|
|
604
|
+
...resolveTraceContextOptions(options.config.trace),
|
|
605
|
+
redact,
|
|
606
|
+
});
|
|
607
|
+
bindDiagnosticSensitiveRegistry(traceContext, sensitiveRegistry);
|
|
582
608
|
const baseContext = createBaseContext({
|
|
583
609
|
config: options.config,
|
|
584
610
|
provider: options.provider,
|
|
@@ -650,7 +676,7 @@ function createBaseContext(options: {
|
|
|
650
676
|
});
|
|
651
677
|
|
|
652
678
|
const env = {
|
|
653
|
-
get: (key: string) =>
|
|
679
|
+
get: (key: string) => readDiagnosticEnv(key),
|
|
654
680
|
};
|
|
655
681
|
const credential = {
|
|
656
682
|
mode: "none" as const,
|
|
@@ -925,6 +951,17 @@ function selectRepresentativeRun(runs: RunResult[]): RunResult {
|
|
|
925
951
|
return first;
|
|
926
952
|
}
|
|
927
953
|
|
|
954
|
+
function sanitizeProfileSuiteForExport(suite: ProfileSuite): ProfileSuite {
|
|
955
|
+
// The perf CLI has no server request scope; retain the shared output policy on disk.
|
|
956
|
+
return {
|
|
957
|
+
...suite,
|
|
958
|
+
runs: suite.runs.map((run) => ({
|
|
959
|
+
...run,
|
|
960
|
+
spans: run.spans.map((span) => sanitizeSpanForOutput(span)),
|
|
961
|
+
})),
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
928
965
|
async function writeExport(filePath: string, payload: unknown): Promise<void> {
|
|
929
966
|
const absolutePath = resolve(process.cwd(), filePath);
|
|
930
967
|
await mkdir(dirname(absolutePath), { recursive: true });
|
package/bin/apifuse-record.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { readDiagnosticEnv } from "../src/runtime/diagnostic-env.js";
|
|
2
3
|
|
|
3
4
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
5
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
@@ -519,8 +520,8 @@ export function createCaptureContext(
|
|
|
519
520
|
process.env,
|
|
520
521
|
provider.secrets?.map((secret) => secret.name) ?? [],
|
|
521
522
|
);
|
|
522
|
-
const env = { get: (key: string) =>
|
|
523
|
-
const engineEnv = { get: (key: string) =>
|
|
523
|
+
const env = { get: (key: string) => readDiagnosticEnv(key, providerEnvironment) };
|
|
524
|
+
const engineEnv = { get: (key: string) => readDiagnosticEnv(key) };
|
|
524
525
|
const engineCredentials = readEngineProxyCredentials();
|
|
525
526
|
const credential = {
|
|
526
527
|
mode: "none" as const,
|
|
@@ -832,6 +833,12 @@ function proxyStealthSession(
|
|
|
832
833
|
onResponse(order, response);
|
|
833
834
|
return response;
|
|
834
835
|
},
|
|
836
|
+
replayChallenged: async (...args: Parameters<StealthSession["replayChallenged"]>) => {
|
|
837
|
+
const order = reserveOrder();
|
|
838
|
+
const response = await session.replayChallenged(...args);
|
|
839
|
+
onResponse(order, response);
|
|
840
|
+
return response;
|
|
841
|
+
},
|
|
835
842
|
cookies: session.cookies,
|
|
836
843
|
redirects: {
|
|
837
844
|
run: async (...args: Parameters<StealthSession["redirects"]["run"]>) => {
|
|
@@ -97,9 +97,21 @@ The `POST /v1/{operation}` body is a request envelope:
|
|
|
97
97
|
- `requestId` is required and can be any unique local debugging string.
|
|
98
98
|
- `input` contains the operation input shape.
|
|
99
99
|
- `headers` is optional.
|
|
100
|
-
- `
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
- `connectionId` is optional connection identity only and does not include
|
|
101
|
+
credentials. The gateway sends it for `optional` connection mode, and only
|
|
102
|
+
when the caller supplied a connection that passed authorization. It is
|
|
103
|
+
exposed as `ctx.request.connectionId`.
|
|
104
|
+
- `connection` is optional credential-bearing connection data. The gateway
|
|
105
|
+
sends it for `required` connection mode; omit it for no-auth/public (`none`
|
|
106
|
+
mode) operations. For local debugging, pass
|
|
107
|
+
`{ "id", "mode", "secrets", "metadata", "externalRef" }` with local-only
|
|
108
|
+
secret values.
|
|
109
|
+
|
|
110
|
+
The gateway sends only `connection` for `required` mode, only `connectionId`
|
|
111
|
+
for `optional` mode (and nothing when the caller passed no connection), and
|
|
112
|
+
neither field for `none` mode. If a malformed or manually constructed envelope
|
|
113
|
+
contains both, a non-empty nested `connection.id` takes precedence over the
|
|
114
|
+
top-level `connectionId`; an empty string is treated as absent.
|
|
103
115
|
|
|
104
116
|
Structured errors return an `error` object with `code`, `message`,
|
|
105
117
|
`requestId`, and optional `details`; validation failures include field paths in
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -140,6 +140,13 @@ export declare function providerStateRedisUrlFromEnv(): string | undefined;
|
|
|
140
140
|
/** @internal Test-only hook for exercising shared proxy-cache behavior. */
|
|
141
141
|
export declare function __setProxyRedisForTests(redis: ProxyRedisClient | undefined): void;
|
|
142
142
|
export declare function __setSmartproxyAllocatorDeadlineMsForTests(deadlineMs: number | undefined): void;
|
|
143
|
+
/** @internal Test-only hook for exercising the pool-cache capacity bound at a small size. */
|
|
144
|
+
export declare function __setSmartproxyPoolCacheMaxEntriesForTests(maxEntries: number | undefined): void;
|
|
145
|
+
/** @internal Test-only view of the in-memory Smartproxy cache sizes. */
|
|
146
|
+
export declare function __getSmartproxyCacheSizesForTests(): {
|
|
147
|
+
pools: number;
|
|
148
|
+
tombstones: number;
|
|
149
|
+
};
|
|
143
150
|
export declare function resolveProxyConfig(options?: ProxyResolutionOptions): ResolvedProxyConfig;
|
|
144
151
|
export declare function resolveProxyConfigAsync(options?: ProxyResolutionOptions): Promise<ResolvedProxyConfig>;
|
|
145
152
|
/**
|
package/dist/config/loader.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { readDiagnosticEnv } from "../runtime/diagnostic-env.js";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import path from "node:path";
|
|
6
|
+
import { BoundedExpiringMap } from "../runtime/bounded-expiring-map.js";
|
|
5
7
|
import { NODEMAVEN_DEFAULT_PROTOCOL, NODEMAVEN_FILTER_ENV, NODEMAVEN_MAX_POOL_SIZE, NODEMAVEN_PASSWORD_ENV, NODEMAVEN_USERNAME_ENV, nodemavenPoolSize, synthesizeNodemavenProxy, } from "../runtime/proxy-nodemaven.js";
|
|
6
8
|
// "smartproxy" here is api.smartproxy.org — a residential proxy with an IP
|
|
7
9
|
// extraction API (app_key → raw ip:port pool). It is NOT the company formerly
|
|
@@ -33,7 +35,13 @@ export class ProxyResolutionError extends Error {
|
|
|
33
35
|
this.protocol = options?.protocol;
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
|
-
|
|
38
|
+
// Keyed per (policy, affinity). Connection affinities are minted by callers, so
|
|
39
|
+
// without a cap a long-lived process accumulates one pool per affinity it ever
|
|
40
|
+
// saw. Evicting a still-fresh pool forces a fresh allocation (a different
|
|
41
|
+
// egress endpoint) for that affinity, so the cap is generous and every
|
|
42
|
+
// capacity eviction is reported as `diagnostics.poolCacheEvictions`.
|
|
43
|
+
const SMARTPROXY_POOL_CACHE_MAX_ENTRIES = 10_000;
|
|
44
|
+
let proxyCache = createSmartproxyPoolCache(SMARTPROXY_POOL_CACHE_MAX_ENTRIES);
|
|
37
45
|
const require = createRequire(import.meta.url);
|
|
38
46
|
const proxyInflight = new Map();
|
|
39
47
|
const invalidatedProxyKeys = new Map();
|
|
@@ -56,17 +64,17 @@ const SMARTPROXY_ALLOCATOR_RETRY_BASE_MS = 25;
|
|
|
56
64
|
const SMARTPROXY_EXTRACTION_CACHE_TTL_MS = 15_000;
|
|
57
65
|
const SMARTPROXY_EXTRACTION_SOFT_REFRESH_MS = 10_000;
|
|
58
66
|
function redisUrlFromEnv() {
|
|
59
|
-
return (
|
|
60
|
-
|
|
67
|
+
return (readDiagnosticEnv(PROVIDER_CACHE_REDIS_URL_ENV)?.trim() ||
|
|
68
|
+
readDiagnosticEnv(REDIS_URL_ENV)?.trim() ||
|
|
61
69
|
undefined);
|
|
62
70
|
}
|
|
63
71
|
export function providerCacheRedisUrlFromEnv() {
|
|
64
72
|
return redisUrlFromEnv();
|
|
65
73
|
}
|
|
66
74
|
export function providerStateRedisUrlFromEnv() {
|
|
67
|
-
return (
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
return (readDiagnosticEnv(PROVIDER_STATE_REDIS_URL_ENV)?.trim() ||
|
|
76
|
+
readDiagnosticEnv(PROVIDER_CACHE_REDIS_URL_ENV)?.trim() ||
|
|
77
|
+
readDiagnosticEnv(REDIS_URL_ENV)?.trim() ||
|
|
70
78
|
undefined);
|
|
71
79
|
}
|
|
72
80
|
/** @internal Test-only hook for exercising shared proxy-cache behavior. */
|
|
@@ -76,6 +84,17 @@ export function __setProxyRedisForTests(redis) {
|
|
|
76
84
|
export function __setSmartproxyAllocatorDeadlineMsForTests(deadlineMs) {
|
|
77
85
|
smartproxyAllocatorDeadlineMsForTests = deadlineMs;
|
|
78
86
|
}
|
|
87
|
+
/** @internal Test-only hook for exercising the pool-cache capacity bound at a small size. */
|
|
88
|
+
export function __setSmartproxyPoolCacheMaxEntriesForTests(maxEntries) {
|
|
89
|
+
proxyCache = createSmartproxyPoolCache(maxEntries ?? SMARTPROXY_POOL_CACHE_MAX_ENTRIES);
|
|
90
|
+
}
|
|
91
|
+
/** @internal Test-only view of the in-memory Smartproxy cache sizes. */
|
|
92
|
+
export function __getSmartproxyCacheSizesForTests() {
|
|
93
|
+
return { pools: proxyCache.size, tombstones: invalidatedProxyKeys.size };
|
|
94
|
+
}
|
|
95
|
+
function createSmartproxyPoolCache(maxEntries) {
|
|
96
|
+
return new BoundedExpiringMap(maxEntries, (pool) => pool.expiresAt);
|
|
97
|
+
}
|
|
79
98
|
function getProxyRedis() {
|
|
80
99
|
if (proxyRedisForTests)
|
|
81
100
|
return proxyRedisForTests;
|
|
@@ -389,10 +408,10 @@ export async function resolveWithVendor(vendor, policy, options, context) {
|
|
|
389
408
|
if (vendor === "nodemaven") {
|
|
390
409
|
const startedAt = Date.now();
|
|
391
410
|
const username = (context.credentials === undefined
|
|
392
|
-
?
|
|
411
|
+
? readDiagnosticEnv(NODEMAVEN_USERNAME_ENV)
|
|
393
412
|
: context.credentials[NODEMAVEN_USERNAME_ENV])?.trim();
|
|
394
413
|
const password = (context.credentials === undefined
|
|
395
|
-
?
|
|
414
|
+
? readDiagnosticEnv(NODEMAVEN_PASSWORD_ENV)
|
|
396
415
|
: context.credentials[NODEMAVEN_PASSWORD_ENV])?.trim();
|
|
397
416
|
const filter = context.credentials === undefined
|
|
398
417
|
? process.env[NODEMAVEN_FILTER_ENV]
|
|
@@ -435,7 +454,7 @@ export async function resolveWithVendor(vendor, policy, options, context) {
|
|
|
435
454
|
}
|
|
436
455
|
// smartproxy allocation-style vendor.
|
|
437
456
|
const appKey = (context.credentials === undefined
|
|
438
|
-
?
|
|
457
|
+
? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
|
|
439
458
|
: context.credentials[SMARTPROXY_APP_KEY_ENV])?.trim();
|
|
440
459
|
if (!appKey) {
|
|
441
460
|
// Guarded by vendorHasCredentials; treated as a vendor-internal failure.
|
|
@@ -457,6 +476,7 @@ export async function resolveWithVendor(vendor, policy, options, context) {
|
|
|
457
476
|
...allocated.pool.diagnostics,
|
|
458
477
|
poolSize: allocated.pool.urls.length,
|
|
459
478
|
poolIndex,
|
|
479
|
+
poolCacheEvictions: proxyCache.capacityEvictions,
|
|
460
480
|
},
|
|
461
481
|
};
|
|
462
482
|
}
|
|
@@ -501,14 +521,14 @@ function envDefaultProvider() {
|
|
|
501
521
|
function vendorHasCredentials(vendor, credentials) {
|
|
502
522
|
if (vendor === "nodemaven") {
|
|
503
523
|
return Boolean((credentials === undefined
|
|
504
|
-
?
|
|
524
|
+
? readDiagnosticEnv(NODEMAVEN_USERNAME_ENV)
|
|
505
525
|
: credentials[NODEMAVEN_USERNAME_ENV])?.trim() &&
|
|
506
526
|
(credentials === undefined
|
|
507
|
-
?
|
|
527
|
+
? readDiagnosticEnv(NODEMAVEN_PASSWORD_ENV)
|
|
508
528
|
: credentials[NODEMAVEN_PASSWORD_ENV])?.trim());
|
|
509
529
|
}
|
|
510
530
|
return Boolean((credentials === undefined
|
|
511
|
-
?
|
|
531
|
+
? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
|
|
512
532
|
: credentials[SMARTPROXY_APP_KEY_ENV])?.trim());
|
|
513
533
|
}
|
|
514
534
|
function missingCredentialEnv(vendor) {
|
|
@@ -695,8 +715,8 @@ async function allocateSmartproxy(policy, appKey, lifetimeMinutes, affinityKey,
|
|
|
695
715
|
const now = startedAt;
|
|
696
716
|
const invalidatedUntil = invalidatedProxyKeys.get(cacheKey) ?? 0;
|
|
697
717
|
const skipCached = invalidatedUntil > now;
|
|
698
|
-
const cached = proxyCache.get(cacheKey);
|
|
699
|
-
if (
|
|
718
|
+
const cached = skipCached ? undefined : proxyCache.get(cacheKey, now);
|
|
719
|
+
if (cached) {
|
|
700
720
|
if (shouldSoftRefresh(cached, now)) {
|
|
701
721
|
void refreshSmartproxyPool(cacheKey, policy, appKey, lifetimeMinutes, protocol, ambientDefaults, sharedCache);
|
|
702
722
|
return {
|
|
@@ -751,7 +771,7 @@ async function readSmartproxyRedisPool(cacheKey, startedAt) {
|
|
|
751
771
|
const now = Date.now();
|
|
752
772
|
if (!isFresh(pool, now))
|
|
753
773
|
return null;
|
|
754
|
-
proxyCache.set(cacheKey, pool);
|
|
774
|
+
proxyCache.set(cacheKey, pool, now);
|
|
755
775
|
return {
|
|
756
776
|
pool,
|
|
757
777
|
telemetry: telemetryForPool(pool, "redis_hit", startedAt, { redisReadMs }),
|
|
@@ -962,7 +982,7 @@ async function allocateAndStoreSmartproxyPool(cacheKey, policy, appKey, lifetime
|
|
|
962
982
|
rawConnect: true,
|
|
963
983
|
},
|
|
964
984
|
};
|
|
965
|
-
proxyCache.set(cacheKey, result);
|
|
985
|
+
proxyCache.set(cacheKey, result, allocatedAt);
|
|
966
986
|
if (options.redis && options.poolKey) {
|
|
967
987
|
const redis = options.redis;
|
|
968
988
|
const poolKey = options.poolKey;
|
|
@@ -1159,12 +1179,17 @@ function markSmartproxyCacheInvalidated(options = {}) {
|
|
|
1159
1179
|
}
|
|
1160
1180
|
const lifetimeMinutes = resolveSmartproxyLifetime(policy);
|
|
1161
1181
|
const appKey = (options.engineCredentials === undefined
|
|
1162
|
-
?
|
|
1182
|
+
? readDiagnosticEnv(SMARTPROXY_APP_KEY_ENV)
|
|
1163
1183
|
: options.engineCredentials[SMARTPROXY_APP_KEY_ENV])?.trim();
|
|
1164
1184
|
if (!appKey)
|
|
1165
1185
|
return undefined;
|
|
1166
1186
|
const cacheKey = buildSmartproxyCacheKey(policy, appKey, options.affinityKey, lifetimeMinutes, options.protocol ?? VENDOR_DEFAULT_PROTOCOL.smartproxy);
|
|
1167
|
-
|
|
1187
|
+
const now = Date.now();
|
|
1188
|
+
for (const [key, invalidatedUntil] of invalidatedProxyKeys) {
|
|
1189
|
+
if (invalidatedUntil <= now)
|
|
1190
|
+
invalidatedProxyKeys.delete(key);
|
|
1191
|
+
}
|
|
1192
|
+
invalidatedProxyKeys.set(cacheKey, now + SMARTPROXY_INVALIDATION_SKIP_REDIS_MS);
|
|
1168
1193
|
proxyCache.delete(cacheKey);
|
|
1169
1194
|
proxyInflight.delete(cacheKey);
|
|
1170
1195
|
return cacheKey;
|
|
@@ -13,6 +13,7 @@ export declare const DECLARATION_RULE_IDS: {
|
|
|
13
13
|
readonly proxySmartproxyGeo: "proxy-smartproxy-country-only";
|
|
14
14
|
readonly operationUpstreamProxy: "operation-upstream-proxy-unsupported";
|
|
15
15
|
readonly pinnedWireFieldPathValid: "public-schema-pinned-wire-field-valid";
|
|
16
|
+
readonly resolverClientProfileFamily: "resolver-client-profile-family";
|
|
16
17
|
};
|
|
17
18
|
export type DeclarationRuleId = (typeof DECLARATION_RULE_IDS)[keyof typeof DECLARATION_RULE_IDS];
|
|
18
19
|
export type DeclarationViolation = {
|
|
@@ -24,7 +25,7 @@ export type DeclarationViolation = {
|
|
|
24
25
|
export declare function declarationInvalidError(violations: readonly DeclarationViolation[]): ProviderError;
|
|
25
26
|
/** Enforces declaration rules whose runtime behavior would otherwise fail open. */
|
|
26
27
|
export declare function validateFailClosedDeclaration(provider: ProviderDefinition): void;
|
|
27
|
-
type ProviderDeclarationRulesInput = Pick<ProviderDefinition, "healthJourneys" | "meta" | "proxy">;
|
|
28
|
+
type ProviderDeclarationRulesInput = Pick<ProviderDefinition, "healthJourneys" | "meta" | "proxy" | "resolver" | "stealth">;
|
|
28
29
|
type OperationDeclarationRulesInput = Pick<ProviderDefinition, "operations">;
|
|
29
30
|
/** Enforces fail-closed rules that only depend on the provider declaration. */
|
|
30
31
|
export declare function validateFailClosedProviderDeclaration(provider: ProviderDeclarationRulesInput): void;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describeSchema } from "./contract-serialization.js";
|
|
2
2
|
import { ProviderError } from "./errors.js";
|
|
3
3
|
import { HealthScenarioSchema } from "./health-scenario.js";
|
|
4
|
+
import { kindRequiresClientProfile } from "./runtime/resolver-config.js";
|
|
5
|
+
import { resolverClientProfileFamily, resolveStealthProfileSelection } from "./stealth/profiles.js";
|
|
4
6
|
export const DECLARATION_INVALID_CODE = "DECLARATION_INVALID";
|
|
5
7
|
export const DECLARATION_RULE_IDS = {
|
|
6
8
|
challengeShape: "credentials-challenge-shape",
|
|
@@ -14,6 +16,7 @@ export const DECLARATION_RULE_IDS = {
|
|
|
14
16
|
proxySmartproxyGeo: "proxy-smartproxy-country-only",
|
|
15
17
|
operationUpstreamProxy: "operation-upstream-proxy-unsupported",
|
|
16
18
|
pinnedWireFieldPathValid: "public-schema-pinned-wire-field-valid",
|
|
19
|
+
resolverClientProfileFamily: "resolver-client-profile-family",
|
|
17
20
|
};
|
|
18
21
|
export function declarationInvalidError(violations) {
|
|
19
22
|
const summary = violations
|
|
@@ -32,6 +35,7 @@ export function validateFailClosedDeclaration(provider) {
|
|
|
32
35
|
validatePinnedWireFieldPaths(provider, violations);
|
|
33
36
|
validateSchemaDeclaration(provider, violations);
|
|
34
37
|
validateProxyDeclaration(provider, violations);
|
|
38
|
+
validateResolverClientProfileDeclaration(provider, violations);
|
|
35
39
|
validateOperationDeclaration(provider, violations);
|
|
36
40
|
if (violations.length > 0)
|
|
37
41
|
throw declarationInvalidError(violations);
|
|
@@ -54,6 +58,7 @@ function collectProviderDeclarationViolations(provider, violations) {
|
|
|
54
58
|
validateHealthDeclaration(provider, violations);
|
|
55
59
|
validatePinnedWireFieldPaths(provider, violations);
|
|
56
60
|
validateProxyDeclaration(provider, violations);
|
|
61
|
+
validateResolverClientProfileDeclaration(provider, violations);
|
|
57
62
|
}
|
|
58
63
|
function validatePinnedWireFieldPaths(provider, violations) {
|
|
59
64
|
const pins = provider.meta?.contract?.pinnedWireFieldPaths;
|
|
@@ -245,6 +250,40 @@ function validateProxyDeclaration(provider, violations) {
|
|
|
245
250
|
}
|
|
246
251
|
}
|
|
247
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* An akamai_sbsd solve runs on the provider's own stealth session, so the declared
|
|
255
|
+
* resolver.clientProfile must name the browser family that session emulates. The
|
|
256
|
+
* stealth runtime re-checks per request (a fetch may override the browser); this
|
|
257
|
+
* rule fails the declaration instead of the first challenged request.
|
|
258
|
+
*/
|
|
259
|
+
function validateResolverClientProfileDeclaration(provider, violations) {
|
|
260
|
+
const resolver = provider.resolver;
|
|
261
|
+
if (!resolver ||
|
|
262
|
+
!Array.isArray(resolver.kinds) ||
|
|
263
|
+
!resolver.kinds.some(kindRequiresClientProfile) ||
|
|
264
|
+
typeof resolver.clientProfile !== "string") {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
let sessionBrowser;
|
|
268
|
+
try {
|
|
269
|
+
sessionBrowser = resolveStealthProfileSelection(provider.stealth).browser;
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
// An unsupported stealth selection is reported by the stealth runtime itself.
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const family = resolverClientProfileFamily(resolver.clientProfile);
|
|
276
|
+
if (family === sessionBrowser)
|
|
277
|
+
return;
|
|
278
|
+
violations.push({
|
|
279
|
+
ruleId: DECLARATION_RULE_IDS.resolverClientProfileFamily,
|
|
280
|
+
path: "resolver.clientProfile",
|
|
281
|
+
message: family
|
|
282
|
+
? `"${resolver.clientProfile}" names ${family}, but the stealth session browser is ${sessionBrowser}.`
|
|
283
|
+
: `"${resolver.clientProfile}" does not name a supported browser family (chrome, firefox, safari).`,
|
|
284
|
+
fix: `Declare stealth.browser as the family resolver.clientProfile names, or set resolver.clientProfile to the ${sessionBrowser} profile the SBSD session runs on.`,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
248
287
|
function declaredProxyVendors(policy) {
|
|
249
288
|
const vendors = [...(policy.providers ?? [])];
|
|
250
289
|
if (policy.provider !== undefined)
|