@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
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { isSensitiveFixtureKey, REDACTED_FIXTURE_VALUE } from "../fixture-sanitization.js";
|
|
2
|
+
import {
|
|
3
|
+
type DiagnosticRedactor,
|
|
4
|
+
redactedKeyAllocator,
|
|
5
|
+
isRedactedKey,
|
|
6
|
+
REDACTION_FAILED,
|
|
7
|
+
redactDiagnosticText,
|
|
8
|
+
} from "../runtime/diagnostic-redactor.js";
|
|
2
9
|
import {
|
|
3
10
|
OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
4
11
|
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
|
|
@@ -22,6 +29,16 @@ import type { TraceConfig } from "../types.js";
|
|
|
22
29
|
|
|
23
30
|
type EnvLike = Record<string, string | undefined>;
|
|
24
31
|
|
|
32
|
+
const TRACE_RESOURCE_ATTRIBUTE_SANITIZER = Symbol.for(
|
|
33
|
+
"@apifuse/provider-sdk/runtime/trace-resource-attribute-sanitizer",
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
type ResourceAttributesWithSanitizer = Record<string, string> & {
|
|
37
|
+
[TRACE_RESOURCE_ATTRIBUTE_SANITIZER]?: (
|
|
38
|
+
attributes: Record<string, string>,
|
|
39
|
+
) => Record<string, string>;
|
|
40
|
+
};
|
|
41
|
+
|
|
25
42
|
// Warn once per environment object: process.env in production, each injected env in tests.
|
|
26
43
|
const warnedEnvironments = new WeakSet<EnvLike>();
|
|
27
44
|
const warnedResourceEnvironments = new WeakSet<EnvLike>();
|
|
@@ -73,22 +90,52 @@ function resolveServerOTLPExportOptions(
|
|
|
73
90
|
function resolveExportResourceAttributes(
|
|
74
91
|
requestAttributes: Record<string, string>,
|
|
75
92
|
env: EnvLike,
|
|
76
|
-
|
|
93
|
+
redact?: DiagnosticRedactor,
|
|
94
|
+
): {
|
|
95
|
+
attributes: Record<string, string>;
|
|
96
|
+
sanitize: (attributes: Record<string, string>) => Record<string, string>;
|
|
97
|
+
} {
|
|
77
98
|
const resolution = resolveOTLPResourceAttributes({}, env);
|
|
78
99
|
warnDiscardedResourceAttributes(env, resolution);
|
|
79
|
-
const operatorAttributes =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
const operatorAttributes = resolution.attributes;
|
|
101
|
+
const requestKeys = new Set(Object.keys(requestAttributes));
|
|
102
|
+
return {
|
|
103
|
+
// Keep the raw snapshot private until export. A credential registered after
|
|
104
|
+
// trace creation must be matched before any key or value is bounded.
|
|
105
|
+
attributes: { ...operatorAttributes, ...requestAttributes },
|
|
106
|
+
sanitize(attributes) {
|
|
107
|
+
const nextKey = redactedKeyAllocator(Object.keys(attributes));
|
|
108
|
+
const sanitizedOperatorAttributes: Record<string, string> = {};
|
|
109
|
+
const rawRequestAttributes: Record<string, string> = {};
|
|
110
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
111
|
+
const unchangedOperatorValue = !requestKeys.has(key) && operatorAttributes[key] === value;
|
|
112
|
+
if (unchangedOperatorValue) {
|
|
113
|
+
const redactedKey = isRedactedKey(key) ? key : redactDiagnosticText(key, redact);
|
|
114
|
+
const keyChanged = redactedKey !== key;
|
|
115
|
+
const sanitizedKey = keyChanged ? nextKey() : sanitizeSpanNameForOutput(key);
|
|
116
|
+
sanitizedOperatorAttributes[sanitizedKey] = keyChanged
|
|
117
|
+
? redactedKey === REDACTION_FAILED
|
|
118
|
+
? REDACTION_FAILED
|
|
119
|
+
: REDACTED_FIXTURE_VALUE
|
|
120
|
+
: isSensitiveFixtureKey(key)
|
|
121
|
+
? REDACTED_FIXTURE_VALUE
|
|
122
|
+
: sanitizeSpanNameForOutput(value, redact);
|
|
123
|
+
} else {
|
|
124
|
+
rawRequestAttributes[key] = value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const sanitizedRequestAttributes = Object.fromEntries(
|
|
128
|
+
Object.entries(
|
|
129
|
+
sanitizeTraceAttributes(
|
|
130
|
+
rawRequestAttributes,
|
|
131
|
+
redact,
|
|
132
|
+
Object.keys(sanitizedOperatorAttributes),
|
|
133
|
+
),
|
|
134
|
+
).map(([key, value]) => [key, String(value)]),
|
|
135
|
+
);
|
|
136
|
+
return { ...sanitizedOperatorAttributes, ...sanitizedRequestAttributes };
|
|
137
|
+
},
|
|
138
|
+
};
|
|
92
139
|
}
|
|
93
140
|
|
|
94
141
|
/** Server-only trace output policy. Shared programmatic trace callers stay in-memory. */
|
|
@@ -96,12 +143,14 @@ export function resolveServerTraceContextOptions(
|
|
|
96
143
|
config: TraceConfig,
|
|
97
144
|
resourceAttributes: Record<string, string>,
|
|
98
145
|
env: EnvLike = process.env,
|
|
146
|
+
redact?: DiagnosticRedactor,
|
|
99
147
|
): CreateTraceContextOptions {
|
|
100
148
|
const resolved = resolveTraceContextOptions(config);
|
|
101
149
|
const outputEnabled = config.enabled !== false && config.exporter !== "none";
|
|
102
150
|
const consoleHook =
|
|
103
151
|
outputEnabled && (config.exporter === "console" || config.exporter === "json")
|
|
104
|
-
? (span: Span) =>
|
|
152
|
+
? (span: Span) =>
|
|
153
|
+
console.log(JSON.stringify(sanitizeSpanForOutput(span, resourceAttributes, redact)))
|
|
105
154
|
: undefined;
|
|
106
155
|
const onSpan =
|
|
107
156
|
consoleHook && resolved.onSpan
|
|
@@ -114,15 +163,26 @@ export function resolveServerTraceContextOptions(
|
|
|
114
163
|
outputEnabled && config.exporter === "otlp"
|
|
115
164
|
? resolveServerOTLPExportOptions(config, env)
|
|
116
165
|
: undefined;
|
|
166
|
+
const exportResources = exportOptions
|
|
167
|
+
? resolveExportResourceAttributes(resourceAttributes, env, redact)
|
|
168
|
+
: undefined;
|
|
169
|
+
if (exportResources) {
|
|
170
|
+
Object.defineProperty(
|
|
171
|
+
exportResources.attributes as ResourceAttributesWithSanitizer,
|
|
172
|
+
TRACE_RESOURCE_ATTRIBUTE_SANITIZER,
|
|
173
|
+
{ value: exportResources.sanitize },
|
|
174
|
+
);
|
|
175
|
+
}
|
|
117
176
|
|
|
118
177
|
return {
|
|
119
178
|
maxSpans: resolved.maxSpans,
|
|
120
179
|
onSpan,
|
|
180
|
+
...(redact ? { redact } : {}),
|
|
121
181
|
...(exportOptions
|
|
122
182
|
? {
|
|
123
183
|
exportOptions,
|
|
124
|
-
resourceAttributes:
|
|
125
|
-
sanitizeSpanForExport: (span: Span) => sanitizeSpanForOutput(span),
|
|
184
|
+
resourceAttributes: exportResources?.attributes,
|
|
185
|
+
sanitizeSpanForExport: (span: Span) => sanitizeSpanForOutput(span, undefined, redact),
|
|
126
186
|
}
|
|
127
187
|
: {}),
|
|
128
188
|
};
|
package/src/server/types.ts
CHANGED
|
@@ -93,12 +93,26 @@ export const AuthFlowRequestSchema = z.object({
|
|
|
93
93
|
headers: z.record(z.string(), z.string()).optional(),
|
|
94
94
|
input: z.record(z.string(), z.unknown()).optional(),
|
|
95
95
|
context: z.record(z.string(), z.unknown()).optional(),
|
|
96
|
+
/** Opaque engine state. This is never projected into FlowContext or provider contextPatch. */
|
|
97
|
+
engine: z
|
|
98
|
+
.object({
|
|
99
|
+
egressLease: z.string().min(1).max(16_384).optional(),
|
|
100
|
+
})
|
|
101
|
+
.strict()
|
|
102
|
+
.optional(),
|
|
96
103
|
connection: OperationConnectionSchema.optional(),
|
|
97
104
|
});
|
|
98
105
|
|
|
99
106
|
export const AuthFlowSuccessResponseSchema = z.object({
|
|
100
107
|
data: z.unknown(),
|
|
101
108
|
contextPatch: z.record(z.string(), z.unknown().nullable()).optional(),
|
|
109
|
+
/** Opaque engine state to carry verbatim to the next auth turn. */
|
|
110
|
+
engine: z
|
|
111
|
+
.object({
|
|
112
|
+
egressLease: z.string().min(1).max(16_384).optional(),
|
|
113
|
+
})
|
|
114
|
+
.strict()
|
|
115
|
+
.optional(),
|
|
102
116
|
});
|
|
103
117
|
|
|
104
118
|
export const AuthFlowErrorResponseSchema = OperationErrorResponseSchema;
|
package/src/stealth/profiles.ts
CHANGED
|
@@ -258,6 +258,16 @@ export function getStealthProfile(selection: StealthProfileSelection = {}): Stea
|
|
|
258
258
|
};
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
/**
|
|
262
|
+
* The transport owns the browser version (Chromium follows the pinned wreq-js build), so a
|
|
263
|
+
* resolver `clientProfile` such as `safari17_0` or `chrome_149` only has to name the same
|
|
264
|
+
* browser family as the stealth session it runs on.
|
|
265
|
+
*/
|
|
266
|
+
export function resolverClientProfileFamily(clientProfile: string): StealthBrowser | undefined {
|
|
267
|
+
const family = /^[a-z]+/u.exec(clientProfile.trim().toLowerCase())?.[0];
|
|
268
|
+
return family === "chrome" || family === "firefox" || family === "safari" ? family : undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
261
271
|
export function listStealthProfiles(): StealthProfileDescriptor[] {
|
|
262
272
|
return SUPPORTED_STEALTH_PROFILES.map((profile) => ({ ...profile }));
|
|
263
273
|
}
|
package/src/testing/run.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import { ProviderError } from "../errors.js";
|
|
3
4
|
import { createProviderCache } from "../runtime/cache.js";
|
|
4
5
|
import { createTestProviderChoiceContext } from "../runtime/choice.js";
|
|
5
6
|
import { createMemoryProviderRuntimeState } from "../runtime/state.js";
|
|
@@ -425,6 +426,11 @@ function createUpstreamContext(
|
|
|
425
426
|
|
|
426
427
|
const createStealthSession = (): ReturnType<ProviderContext["stealth"]["createSession"]> => ({
|
|
427
428
|
fetch: stealthCall,
|
|
429
|
+
async replayChallenged() {
|
|
430
|
+
throw new ProviderError("Recorded transports cannot replay an engine-owned challenge", {
|
|
431
|
+
code: "REPLAY_SESSION_MISMATCH",
|
|
432
|
+
});
|
|
433
|
+
},
|
|
428
434
|
cookies: {
|
|
429
435
|
...emptyCookieJar,
|
|
430
436
|
has: () => false,
|
|
@@ -4,20 +4,33 @@ import {
|
|
|
4
4
|
REDACTED_FIXTURE_VALUE,
|
|
5
5
|
sanitizeDiagnosticText,
|
|
6
6
|
} from "./fixture-sanitization.js";
|
|
7
|
+
import {
|
|
8
|
+
type DiagnosticRedactor,
|
|
9
|
+
diagnosticStructuredRedactor,
|
|
10
|
+
redactDiagnosticAttributeKey,
|
|
11
|
+
copyDiagnosticAttributeTypes,
|
|
12
|
+
isDiagnosticBigintAttribute,
|
|
13
|
+
redactedKeyAllocator,
|
|
14
|
+
isRedactedKey,
|
|
15
|
+
redactCriticalDiagnosticText,
|
|
16
|
+
diagnosticAttributeRedactor,
|
|
17
|
+
REDACTION_FAILED,
|
|
18
|
+
redactDiagnosticText,
|
|
19
|
+
} from "./runtime/diagnostic-redactor.js";
|
|
7
20
|
import type { TraceSpan } from "./types.js";
|
|
8
21
|
|
|
9
22
|
const MAX_TRACE_TEXT_LENGTH = 300;
|
|
10
23
|
|
|
11
|
-
function sanitizeTraceText(value: string): string {
|
|
12
|
-
const encoded = sanitizeDiagnosticText(value);
|
|
24
|
+
function sanitizeTraceText(value: string, redact?: DiagnosticRedactor): string {
|
|
25
|
+
const encoded = sanitizeDiagnosticText(redactDiagnosticText(value, redact));
|
|
13
26
|
return encoded.length > MAX_TRACE_TEXT_LENGTH
|
|
14
27
|
? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
|
|
15
28
|
: encoded;
|
|
16
29
|
}
|
|
17
30
|
|
|
18
31
|
/** Span names are SDK-authored identifiers; retain them while preventing log injection. */
|
|
19
|
-
export function sanitizeSpanNameForOutput(value: string): string {
|
|
20
|
-
const encoded = encodeDiagnosticControls(value);
|
|
32
|
+
export function sanitizeSpanNameForOutput(value: string, redact?: DiagnosticRedactor): string {
|
|
33
|
+
const encoded = encodeDiagnosticControls(redactDiagnosticText(value, redact));
|
|
21
34
|
return encoded.length > MAX_TRACE_TEXT_LENGTH
|
|
22
35
|
? `${encoded.slice(0, MAX_TRACE_TEXT_LENGTH)}… [truncated]`
|
|
23
36
|
: encoded;
|
|
@@ -25,39 +38,73 @@ export function sanitizeSpanNameForOutput(value: string): string {
|
|
|
25
38
|
|
|
26
39
|
export function sanitizeTraceAttributes(
|
|
27
40
|
attributes: Record<string, unknown>,
|
|
41
|
+
redact?: DiagnosticRedactor,
|
|
42
|
+
reservedKeys: Iterable<string> = [],
|
|
28
43
|
): Record<string, string | number | boolean> {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
const nextKey = redactedKeyAllocator([...Object.keys(attributes), ...reservedKeys]);
|
|
45
|
+
return copyDiagnosticAttributeTypes(
|
|
46
|
+
attributes,
|
|
47
|
+
Object.fromEntries(
|
|
48
|
+
Object.entries(attributes).map(([key, value]) => {
|
|
49
|
+
const redactedKey = isRedactedKey(key) ? key : redactDiagnosticAttributeKey(key, redact);
|
|
50
|
+
const keyChanged = redactedKey !== key;
|
|
51
|
+
const sanitizedKey = keyChanged ? nextKey() : sanitizeTraceText(key);
|
|
52
|
+
const valueRedactor =
|
|
53
|
+
typeof value === "bigint" || isDiagnosticBigintAttribute(attributes, key)
|
|
54
|
+
? diagnosticStructuredRedactor(redact)
|
|
55
|
+
: diagnosticAttributeRedactor(key, redact);
|
|
56
|
+
const sanitizedValue = keyChanged
|
|
57
|
+
? redactedKey === REDACTION_FAILED
|
|
58
|
+
? REDACTION_FAILED
|
|
59
|
+
: REDACTED_FIXTURE_VALUE
|
|
60
|
+
: isSensitiveFixtureKey(key)
|
|
61
|
+
? REDACTED_FIXTURE_VALUE
|
|
62
|
+
: typeof value === "bigint" || isDiagnosticBigintAttribute(attributes, key)
|
|
63
|
+
? String(value).length < 8
|
|
64
|
+
? String(value)
|
|
65
|
+
: sanitizeTraceText(String(value), valueRedactor)
|
|
66
|
+
: typeof value === "string"
|
|
67
|
+
? sanitizeTraceText(value, valueRedactor)
|
|
68
|
+
: typeof value === "number" || typeof value === "boolean"
|
|
69
|
+
? (() => {
|
|
70
|
+
if (String(value).length < 8) return value;
|
|
71
|
+
const redacted = redactDiagnosticText(
|
|
72
|
+
String(value),
|
|
73
|
+
diagnosticStructuredRedactor(valueRedactor),
|
|
74
|
+
);
|
|
75
|
+
return redacted === String(value) ? value : sanitizeTraceText(redacted);
|
|
76
|
+
})()
|
|
77
|
+
: sanitizeTraceText(String(value), valueRedactor);
|
|
78
|
+
return [sanitizedKey, sanitizedValue];
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
40
81
|
);
|
|
41
82
|
}
|
|
42
83
|
|
|
43
84
|
export function sanitizeSpanForOutput(
|
|
44
85
|
span: TraceSpan,
|
|
45
86
|
additionalAttributes?: Record<string, string>,
|
|
87
|
+
redact?: DiagnosticRedactor,
|
|
46
88
|
): TraceSpan {
|
|
47
89
|
// Keep this schema explicit so future fields are not silently added to a
|
|
48
90
|
// process output path before their trust boundary has been reviewed.
|
|
49
91
|
return {
|
|
50
|
-
id: span.id,
|
|
51
|
-
name: sanitizeSpanNameForOutput(span.name),
|
|
92
|
+
id: redactCriticalDiagnosticText(span.id, redact),
|
|
93
|
+
name: sanitizeSpanNameForOutput(redactCriticalDiagnosticText(span.name, redact)),
|
|
52
94
|
startedAt: span.startedAt,
|
|
53
95
|
endedAt: span.endedAt,
|
|
54
96
|
duration_ms: span.duration_ms,
|
|
55
97
|
status: span.status,
|
|
56
|
-
attributes: sanitizeTraceAttributes(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
98
|
+
attributes: sanitizeTraceAttributes(
|
|
99
|
+
copyDiagnosticAttributeTypes(span.attributes, {
|
|
100
|
+
...span.attributes,
|
|
101
|
+
...(additionalAttributes ?? {}),
|
|
102
|
+
}),
|
|
103
|
+
redact,
|
|
104
|
+
),
|
|
105
|
+
...(span.error !== undefined ? { error: sanitizeTraceText(span.error, redact) } : {}),
|
|
106
|
+
...(span.parentId !== undefined
|
|
107
|
+
? { parentId: redactCriticalDiagnosticText(span.parentId, redact) }
|
|
108
|
+
: {}),
|
|
62
109
|
};
|
|
63
110
|
}
|
package/src/types.ts
CHANGED
|
@@ -278,6 +278,7 @@ export type ProviderResolverVendor =
|
|
|
278
278
|
| "capsolver"
|
|
279
279
|
| "capmonster"
|
|
280
280
|
| "2captcha"
|
|
281
|
+
| "hypersolutions"
|
|
281
282
|
| "custom";
|
|
282
283
|
|
|
283
284
|
/**
|
|
@@ -343,6 +344,23 @@ export type ProviderChallenge =
|
|
|
343
344
|
readonly bmsz?: string;
|
|
344
345
|
/** Bot Manager major version when known ("3" measured on zozo.jp). */
|
|
345
346
|
readonly version?: string;
|
|
347
|
+
}
|
|
348
|
+
| {
|
|
349
|
+
readonly kind: "akamai_sbsd";
|
|
350
|
+
readonly pageUrl: string;
|
|
351
|
+
/**
|
|
352
|
+
* Discovered SBSD script URL. `?v=&t=` is a hard challenge and `?v=`
|
|
353
|
+
* is passive or a remembered script. A later challenge token never
|
|
354
|
+
* changes this URL.
|
|
355
|
+
*/
|
|
356
|
+
readonly scriptUrl: string;
|
|
357
|
+
readonly stateCookieName: "sbsd_o" | "bm_so";
|
|
358
|
+
/**
|
|
359
|
+
* Token returned by a later `cpr_chlge` response. When present with a
|
|
360
|
+
* remembered v-only script, it selects index 0 and is appended only to
|
|
361
|
+
* the payload POST URL.
|
|
362
|
+
*/
|
|
363
|
+
readonly challengeToken?: string;
|
|
346
364
|
};
|
|
347
365
|
|
|
348
366
|
export type ProviderChallengeKind = ProviderChallenge["kind"];
|
|
@@ -352,7 +370,9 @@ export type ProviderChallengeKind = ProviderChallenge["kind"];
|
|
|
352
370
|
* per challenge kind: `aws_waf` was measured portable across residential leases
|
|
353
371
|
* on buyee, while `cf_clearance` is unmeasured here and treated as scoped to the
|
|
354
372
|
* identity that produced it. The provider attaches the returned cookies to its
|
|
355
|
-
* own requests.
|
|
373
|
+
* own requests. `cookie_state` solutions carry no values: SBSD state lives only
|
|
374
|
+
* in the engine-owned bound jar, and a 2xx payload POST is not proof of a solve;
|
|
375
|
+
* only the next protected GET can verify it (the safe refetch is Phase 2).
|
|
356
376
|
*/
|
|
357
377
|
export type ChallengeSolution =
|
|
358
378
|
| { readonly form: "token"; readonly token: string }
|
|
@@ -367,21 +387,44 @@ export type ChallengeSolution =
|
|
|
367
387
|
* the upstream cookie's expiry. `expires` takes precedence when both are present.
|
|
368
388
|
*/
|
|
369
389
|
readonly sdkEstimatedExpires?: number;
|
|
390
|
+
}
|
|
391
|
+
| {
|
|
392
|
+
readonly form: "cookie_state";
|
|
393
|
+
readonly kind: "akamai_sbsd";
|
|
394
|
+
readonly outcome: "payload_accepted";
|
|
395
|
+
/** Always false until a later protected GET verifies the bound jar (Phase 2). */
|
|
396
|
+
readonly verified: false;
|
|
397
|
+
/** Name only; the identity-bound cookie value remains in the engine-owned jar. */
|
|
398
|
+
readonly stateCookieName: "sbsd_o" | "bm_so";
|
|
399
|
+
/** Epoch seconds observed on the updated state cookie, when supplied upstream. */
|
|
400
|
+
readonly expires?: number;
|
|
370
401
|
};
|
|
371
402
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
403
|
+
/**
|
|
404
|
+
* `akamai_sbsd` declarations require a transport-owned client profile because the
|
|
405
|
+
* solve runs on the provider's admitted session. Other kinds may omit it.
|
|
406
|
+
*/
|
|
407
|
+
export type ProviderResolverConfig =
|
|
408
|
+
| {
|
|
409
|
+
/** Optional ordered override for the SDK-owned vendor fallback chain. */
|
|
410
|
+
readonly vendors?: readonly ProviderResolverVendor[];
|
|
411
|
+
/** Challenge kinds this provider is permitted to request. */
|
|
412
|
+
readonly kinds: readonly Exclude<ProviderChallengeKind, "akamai_sbsd">[];
|
|
413
|
+
readonly clientProfile?: string;
|
|
414
|
+
}
|
|
415
|
+
| {
|
|
416
|
+
/** Optional ordered override for the SDK-owned vendor fallback chain. */
|
|
417
|
+
readonly vendors?: readonly ProviderResolverVendor[];
|
|
418
|
+
/** Challenge kinds this provider is permitted to request. */
|
|
419
|
+
readonly kinds: readonly ProviderChallengeKind[];
|
|
420
|
+
/**
|
|
421
|
+
* Client fingerprint profile the SDK must use when reaching this upstream.
|
|
422
|
+
* Measured on zozo.jp: Chrome/Firefox profiles are refused 403 before any
|
|
423
|
+
* challenge is served, while a Safari profile is admitted. Provider-declared
|
|
424
|
+
* because only the provider knows its upstream's admission rule.
|
|
425
|
+
*/
|
|
426
|
+
readonly clientProfile: string;
|
|
427
|
+
};
|
|
385
428
|
|
|
386
429
|
export type SttAudioInput = {
|
|
387
430
|
kind: "base64";
|
|
@@ -905,6 +948,14 @@ export type StealthProfileSelection =
|
|
|
905
948
|
| { browser: "firefox"; os?: "windows" | "macos" | "linux" }
|
|
906
949
|
| { browser: "safari"; os?: "macos" | "ios" };
|
|
907
950
|
|
|
951
|
+
/** Provider-owned opt-ins layered onto the transport-owned browser/OS profile. */
|
|
952
|
+
export type ProviderStealthConfig = StealthProfileSelection & {
|
|
953
|
+
readonly challengeDetection?: {
|
|
954
|
+
/** Classify SBSD responses even when no resolver is declared (detect/report mode). */
|
|
955
|
+
readonly akamaiSbsd?: boolean;
|
|
956
|
+
};
|
|
957
|
+
};
|
|
958
|
+
|
|
908
959
|
export type BrowserEngine = "playwright-stealth" | "nodriver" | "selenium-uc";
|
|
909
960
|
export interface BrowserOptions {
|
|
910
961
|
headless?: boolean;
|
|
@@ -1238,9 +1289,25 @@ export type HttpMethod =
|
|
|
1238
1289
|
| "patch";
|
|
1239
1290
|
|
|
1240
1291
|
export interface StealthFetchOptions extends Omit<RequestOptions, "redirectPolicy" | "headers"> {
|
|
1292
|
+
/**
|
|
1293
|
+
* Defaults to true. Set to false when callers need to inspect upstream
|
|
1294
|
+
* non-2xx bodies themselves instead of converting them to TransportError.
|
|
1295
|
+
*
|
|
1296
|
+
* Documented exception: a non-2xx response that an opted-in SDK challenge
|
|
1297
|
+
* detector classified (`response.challenge` is set) is returned, not thrown,
|
|
1298
|
+
* whatever this flag says, so the classification stays observable. Callers
|
|
1299
|
+
* that need the failure semantics check `response.challenge` first.
|
|
1300
|
+
*/
|
|
1301
|
+
throwOnHttpError?: boolean;
|
|
1241
1302
|
/**
|
|
1242
1303
|
* Request headers. Array values and case-insensitive duplicate names are
|
|
1243
1304
|
* combined in caller order using `", "`, matching Chrome's Fetch behavior.
|
|
1305
|
+
*
|
|
1306
|
+
* The stealth transport owns `host`, `connection`, `user-agent`, `sec-ch-ua`,
|
|
1307
|
+
* `sec-ch-ua-mobile`, `sec-ch-ua-platform`, `accept-encoding`, and every
|
|
1308
|
+
* `sec-fetch-*` header. Supplying any of them throws
|
|
1309
|
+
* `STEALTH_HEADER_OVERRIDE_UNSUPPORTED` at request time; declare the request
|
|
1310
|
+
* shape with `stealth.requestClass` instead.
|
|
1244
1311
|
*/
|
|
1245
1312
|
headers?: Record<string, string | string[]>;
|
|
1246
1313
|
method?: HttpMethod;
|
|
@@ -1260,13 +1327,26 @@ export interface StealthFetchOptions extends Omit<RequestOptions, "redirectPolic
|
|
|
1260
1327
|
*/
|
|
1261
1328
|
proxyAttemptOffset?: number;
|
|
1262
1329
|
/**
|
|
1263
|
-
* SDK-specific stealth controls
|
|
1264
|
-
* referrer, and content
|
|
1330
|
+
* SDK-specific stealth controls, including a per-request browser and/or OS
|
|
1331
|
+
* override. Standard HTTP metadata such as language, referrer, and content
|
|
1332
|
+
* type belongs in `headers`.
|
|
1265
1333
|
*/
|
|
1266
1334
|
stealth?: StealthProfileSelection & {
|
|
1267
|
-
/**
|
|
1268
|
-
|
|
1335
|
+
/**
|
|
1336
|
+
* Declare the Chrome request class when it cannot be inferred from the
|
|
1337
|
+
* method (`POST` infers `post`, everything else `navigation`). This is the
|
|
1338
|
+
* replacement for hand-set `Sec-Fetch-*` headers, which the transport
|
|
1339
|
+
* rejects; it drives `Sec-Fetch-Mode`/`Dest`/`User`, `Accept`, `Priority`,
|
|
1340
|
+
* and `Upgrade-Insecure-Requests`.
|
|
1341
|
+
*/
|
|
1269
1342
|
requestClass?: "navigation" | "xhr" | "post";
|
|
1343
|
+
/**
|
|
1344
|
+
* Whether a `navigation` request carries a user gesture and therefore
|
|
1345
|
+
* `Sec-Fetch-User: ?1` (default `true`). Set `false` for script-driven
|
|
1346
|
+
* navigations such as `location.replace`, which Chrome sends without it.
|
|
1347
|
+
* Ignored for the `xhr` and `post` request classes.
|
|
1348
|
+
*/
|
|
1349
|
+
userActivation?: boolean;
|
|
1270
1350
|
/**
|
|
1271
1351
|
* Use only for proxy products that terminate CONNECT with a private CA
|
|
1272
1352
|
* instead of tunneling the origin certificate chain.
|
|
@@ -1326,6 +1406,8 @@ export interface DeclarativeStealthResponse {
|
|
|
1326
1406
|
headers: Record<string, string>;
|
|
1327
1407
|
rawHeaders: [string, string][];
|
|
1328
1408
|
body: string;
|
|
1409
|
+
/** Present only when an opted-in SDK detector returned an unresolved challenge response. */
|
|
1410
|
+
challenge?: StealthChallengeClassification;
|
|
1329
1411
|
httpVersion?: string;
|
|
1330
1412
|
tlsInfo?: { protocol?: string; cipher?: string; [key: string]: unknown };
|
|
1331
1413
|
cookies: CookieJar;
|
|
@@ -1336,6 +1418,23 @@ export interface DeclarativeStealthResponse {
|
|
|
1336
1418
|
|
|
1337
1419
|
export type StealthResponse = DeclarativeStealthResponse;
|
|
1338
1420
|
|
|
1421
|
+
/** Why a detected challenge response was returned to the caller instead of being solved and refetched. */
|
|
1422
|
+
export type StealthChallengeClassification = {
|
|
1423
|
+
readonly challenge: Extract<ProviderChallenge, { readonly kind: "akamai_sbsd" }>;
|
|
1424
|
+
/**
|
|
1425
|
+
* `resolver_unavailable`: detect-only provider or no solver configured.
|
|
1426
|
+
* `replay_required`: the request is not a plain GET, so it is never solved or replayed
|
|
1427
|
+
* automatically. `solve_failed`: a concurrent request on the same session owned the
|
|
1428
|
+
* solve and it rejected (that owner receives the error). `challenge_persisted`: the
|
|
1429
|
+
* single refetch after a solve was challenged again.
|
|
1430
|
+
*/
|
|
1431
|
+
readonly outcome:
|
|
1432
|
+
| "resolver_unavailable"
|
|
1433
|
+
| "replay_required"
|
|
1434
|
+
| "solve_failed"
|
|
1435
|
+
| "challenge_persisted";
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1339
1438
|
export type RequestWithMethodOptions = RequestOptions & {
|
|
1340
1439
|
method?: string;
|
|
1341
1440
|
body?: unknown;
|
|
@@ -1371,6 +1470,12 @@ export interface StealthRedirectRunResult {
|
|
|
1371
1470
|
|
|
1372
1471
|
export interface StealthSession {
|
|
1373
1472
|
fetch(url: string, options?: StealthFetchOptions): Promise<StealthResponse>;
|
|
1473
|
+
/**
|
|
1474
|
+
* Explicitly solve and replay one unsafe request previously classified as
|
|
1475
|
+
* `replay_required`. The original method, URL, headers, and body bytes are
|
|
1476
|
+
* retained by this session and may be replayed exactly once.
|
|
1477
|
+
*/
|
|
1478
|
+
replayChallenged(response: StealthResponse): Promise<StealthResponse>;
|
|
1374
1479
|
cookies: StealthSessionCookies;
|
|
1375
1480
|
redirects: {
|
|
1376
1481
|
run(options: StealthRedirectRunOptions): Promise<StealthRedirectRunResult>;
|
|
@@ -2474,7 +2579,7 @@ export interface ProviderDefinition<TContext = ProviderContext> {
|
|
|
2474
2579
|
http?: Record<string, never> | true;
|
|
2475
2580
|
allowedHosts?: string[];
|
|
2476
2581
|
native?: NativeProviderConfig;
|
|
2477
|
-
stealth?:
|
|
2582
|
+
stealth?: ProviderStealthConfig;
|
|
2478
2583
|
proxy?: ProviderProxyConfig;
|
|
2479
2584
|
ocr?: ProviderOcrConfig;
|
|
2480
2585
|
stt?: ProviderSttConfig;
|