@apifuse/provider-sdk 2.2.0-beta.11 → 2.2.0-beta.13
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 +238 -0
- package/CHANGELOG.md +14 -0
- package/README.md +44 -2
- package/bin/apifuse-pack-smoke.ts +14 -0
- package/bin/apifuse-pack-types.ts +40 -1
- package/bin/apifuse-record.ts +622 -57
- package/bin/apifuse-submit-check.ts +43 -10
- package/dist/config/loader.d.ts +9 -1
- package/dist/config/loader.js +9 -0
- package/dist/define.d.ts +2 -1
- package/dist/define.js +61 -3
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +15 -0
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/provider.d.ts +2 -1
- package/dist/provider.js +1 -0
- package/dist/runtime/http.js +86 -32
- package/dist/runtime/instrumentation.js +295 -9
- package/dist/runtime/native-network.d.ts +53 -0
- package/dist/runtime/native-network.js +477 -0
- package/dist/runtime/proxy-nodemaven.d.ts +14 -0
- package/dist/runtime/proxy-nodemaven.js +20 -2
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/stealth.d.ts +3 -1
- package/dist/runtime/stealth.js +352 -86
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +451 -19
- package/dist/types.d.ts +201 -7
- package/package.json +3 -1
- package/src/config/loader.ts +22 -1
- package/src/define.ts +81 -3
- package/src/errors.ts +15 -0
- package/src/fixture-sanitization.ts +247 -0
- package/src/index.ts +45 -1
- package/src/provider.ts +37 -0
- package/src/runtime/http.ts +144 -38
- package/src/runtime/instrumentation.ts +424 -8
- package/src/runtime/native-network.ts +600 -0
- package/src/runtime/proxy-nodemaven.ts +37 -2
- package/src/runtime/request-options.ts +680 -1
- package/src/runtime/stealth.ts +420 -88
- package/src/server/index.ts +4 -1
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/stream-evidence.ts +988 -0
- package/src/testing/index.ts +9 -1
- package/src/testing/run.ts +608 -12
- package/src/types.ts +235 -7
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { JsonValue } from "./contract-json.js";
|
|
2
|
+
|
|
3
|
+
export const REDACTED_FIXTURE_VALUE = "[REDACTED]";
|
|
4
|
+
|
|
5
|
+
const OPAQUE_TOKEN = /^[A-Za-z0-9_+/=.:~-]+$/;
|
|
6
|
+
const OPAQUE_TOKEN_RUN = /[A-Za-z0-9_+/=.:~-]{24,}/g;
|
|
7
|
+
const URL_RUN = /https?:\/\/[^\s"'<>]+/gi;
|
|
8
|
+
const PEM_PRIVATE_KEY =
|
|
9
|
+
/-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g;
|
|
10
|
+
|
|
11
|
+
/** Matches credential field names without treating benign prefixes such as `author` as `auth`. */
|
|
12
|
+
export function isSensitiveFixtureKey(key: string): boolean {
|
|
13
|
+
const normalized = key.replace(/[-_\s]/g, "").toLowerCase();
|
|
14
|
+
const candidates = [normalized, normalized.replace(/(?:value|payload|header)$/, "")];
|
|
15
|
+
return candidates.some(
|
|
16
|
+
(candidate) =>
|
|
17
|
+
/^(?:authorization|authentication|auth|bearer|cookie|credential|password|passwd|privatekey|secret|session|sessionid|token)$/.test(
|
|
18
|
+
candidate,
|
|
19
|
+
) ||
|
|
20
|
+
/^(?:api|client|service|access|consumer)(?:key|secret|token)$/.test(candidate) ||
|
|
21
|
+
/(?:authorization|credential|password|passwd|privatekey|secret|sessionid|token)$/.test(
|
|
22
|
+
candidate,
|
|
23
|
+
),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns JSON fixture data with credential-bearing keys and heuristic-confirmed string secrets
|
|
29
|
+
* replaced. Ordinary short prose and identifiers are retained.
|
|
30
|
+
*/
|
|
31
|
+
export function sanitizeFixture(value: JsonValue): JsonValue {
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
return value.map((item) => sanitizeFixture(item));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof value === "string") return sanitizeFixtureString(value);
|
|
37
|
+
if (value === null || typeof value !== "object") return value;
|
|
38
|
+
|
|
39
|
+
return Object.fromEntries(
|
|
40
|
+
Object.entries(value).map(([key, entryValue]) => [
|
|
41
|
+
key,
|
|
42
|
+
isSensitiveFixtureKey(key) ? REDACTED_FIXTURE_VALUE : sanitizeFixture(entryValue),
|
|
43
|
+
]),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Applies the shared credential-key policy to ordinary JSON fixtures. */
|
|
48
|
+
export function sanitizeOrdinaryFixture(value: JsonValue): JsonValue {
|
|
49
|
+
if (Array.isArray(value)) return value.map((item) => sanitizeOrdinaryFixture(item));
|
|
50
|
+
if (value === null || typeof value !== "object") return value;
|
|
51
|
+
return Object.fromEntries(
|
|
52
|
+
Object.entries(value).map(([key, entryValue]) => [
|
|
53
|
+
key,
|
|
54
|
+
isSensitiveFixtureKey(key) ? REDACTED_FIXTURE_VALUE : sanitizeOrdinaryFixture(entryValue),
|
|
55
|
+
]),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Sanitizes a primitive fixture string only when textual-secret heuristics match. */
|
|
60
|
+
export function sanitizeFixtureString(value: string): string {
|
|
61
|
+
let sanitized = value.replace(PEM_PRIVATE_KEY, REDACTED_FIXTURE_VALUE);
|
|
62
|
+
const retainedUrls: string[] = [];
|
|
63
|
+
sanitized = sanitized.replace(URL_RUN, (url) => {
|
|
64
|
+
const index =
|
|
65
|
+
retainedUrls.push(isCredentialBearingUrl(url) ? sanitizeUrlForLogs(url) : url) - 1;
|
|
66
|
+
return `APIFUSEURL${index}X`;
|
|
67
|
+
});
|
|
68
|
+
sanitized = redactSensitiveAssignments(sanitized);
|
|
69
|
+
sanitized = sanitized.replace(OPAQUE_TOKEN_RUN, (candidate) =>
|
|
70
|
+
isSensitiveFixtureValue(candidate) ? REDACTED_FIXTURE_VALUE : candidate,
|
|
71
|
+
);
|
|
72
|
+
sanitized = sanitized.replace(
|
|
73
|
+
/APIFUSEURL(\d+)X/g,
|
|
74
|
+
(_match, index: string) => retainedUrls[Number(index)] ?? REDACTED_FIXTURE_VALUE,
|
|
75
|
+
);
|
|
76
|
+
return sanitized;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** True for opaque values that are unsafe to retain in paths or unstructured text. */
|
|
80
|
+
export function isSensitiveFixtureValue(value: string): boolean {
|
|
81
|
+
const candidate = decodePathSegment(value);
|
|
82
|
+
if (/^bot(?:\d{6,}:)?[A-Za-z0-9_-]{16,}$/i.test(candidate)) return true;
|
|
83
|
+
if (/^\d{6,}:[A-Za-z0-9_-]{20,}$/.test(candidate)) return true;
|
|
84
|
+
if (/^(?:gh[opusr]_|sk[-_]|xox[baprs]-)[A-Za-z0-9_-]{16,}$/i.test(candidate)) return true;
|
|
85
|
+
if (!OPAQUE_TOKEN.test(candidate) || candidate.length < 24) return false;
|
|
86
|
+
if (/^[a-f0-9]{32,}$/i.test(candidate)) return true;
|
|
87
|
+
return shannonEntropy(candidate) >= 3.5;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Sanitizes every path segment and values following a credential-like segment name. */
|
|
91
|
+
export function sanitizePathname(pathname: string): string {
|
|
92
|
+
const segments = pathname.split("/");
|
|
93
|
+
return segments
|
|
94
|
+
.map((segment, index) => {
|
|
95
|
+
if (!segment) return segment;
|
|
96
|
+
const decoded = decodePathSegment(segment);
|
|
97
|
+
const previous = index > 0 ? decodePathSegment(segments[index - 1] as string) : "";
|
|
98
|
+
if (
|
|
99
|
+
isSensitivePathSegment(decoded) ||
|
|
100
|
+
isCredentialPathKey(previous) ||
|
|
101
|
+
isSensitiveFixtureValue(decoded)
|
|
102
|
+
) {
|
|
103
|
+
return REDACTED_FIXTURE_VALUE;
|
|
104
|
+
}
|
|
105
|
+
return segment;
|
|
106
|
+
})
|
|
107
|
+
.join("/");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isCredentialPathKey(key: string): boolean {
|
|
111
|
+
const finalPathPart = key.split("/").at(-1) ?? "";
|
|
112
|
+
const baseSegment = finalPathPart.split(";", 1)[0] ?? "";
|
|
113
|
+
return isSensitiveFixtureKey(baseSegment.split(/[=:]/, 1)[0] ?? "");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isSensitivePathSegment(segment: string): boolean {
|
|
117
|
+
return segment
|
|
118
|
+
.split(/[;/]/)
|
|
119
|
+
.some((part) => isSensitiveFixtureKey(part.split(/[=:]/, 1)[0] ?? ""));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Removes userinfo, query values, fragments, and credential-like path segments from log URLs. */
|
|
123
|
+
export function sanitizeUrlForLogs(value: string): string {
|
|
124
|
+
try {
|
|
125
|
+
const parsed = new URL(value, "https://fixture.invalid");
|
|
126
|
+
const queryMarker = parsed.search ? `?${REDACTED_FIXTURE_VALUE}` : "";
|
|
127
|
+
const path = sanitizePathname(parsed.pathname);
|
|
128
|
+
if (parsed.origin === "https://fixture.invalid" && !hasExplicitOrigin(value)) {
|
|
129
|
+
return `${path}${queryMarker}`;
|
|
130
|
+
}
|
|
131
|
+
return `${parsed.origin}${path}${queryMarker}`;
|
|
132
|
+
} catch {
|
|
133
|
+
return sanitizePathname(value.split(/[?#]/, 1)[0]);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Returns query-free request provenance with each path segment scrubbed for credential-like values.
|
|
139
|
+
* Origins, URL userinfo, query values, and fragments are never persisted in request provenance.
|
|
140
|
+
*/
|
|
141
|
+
export function requestPathForFixture(value: string): string {
|
|
142
|
+
try {
|
|
143
|
+
return sanitizePathname(new URL(value, "https://fixture.invalid").pathname);
|
|
144
|
+
} catch {
|
|
145
|
+
const path = value.split(/[?#]/, 1)[0];
|
|
146
|
+
return sanitizePathname(path.startsWith("/") ? path : `/${path}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Scrubs secrets and terminal/log control characters before diagnostic text is emitted. */
|
|
151
|
+
export function sanitizeDiagnosticText(value: string): string {
|
|
152
|
+
let sanitized = value
|
|
153
|
+
.replace(URL_RUN, (url) => sanitizeUrlForLogs(url))
|
|
154
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, `Bearer ${REDACTED_FIXTURE_VALUE}`);
|
|
155
|
+
sanitized = redactSensitiveAssignments(sanitized);
|
|
156
|
+
sanitized = sanitized.replace(OPAQUE_TOKEN_RUN, (candidate, offset: number, source: string) => {
|
|
157
|
+
if (/^(?:request|trace|correlation)[-_]?id[:=]/i.test(candidate)) return candidate;
|
|
158
|
+
const prefix = source.slice(Math.max(0, offset - 32), offset);
|
|
159
|
+
if (/(?:request|trace|correlation)[-_]?id\s*[:=]\s*$/i.test(prefix)) return candidate;
|
|
160
|
+
return isSensitiveFixtureValue(candidate) ? REDACTED_FIXTURE_VALUE : candidate;
|
|
161
|
+
});
|
|
162
|
+
return encodeDiagnosticControls(sanitized);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function redactSensitiveAssignments(value: string): string {
|
|
166
|
+
return value.replace(
|
|
167
|
+
/((["']?)([\w-]+)\2\s*[:=]\s*)("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;&]+)/gi,
|
|
168
|
+
(match, prefix: string, _quote: string, key: string, assignmentValue: string) => {
|
|
169
|
+
if (!isSensitiveFixtureKey(key) && key.toLowerCase() !== "key") return match;
|
|
170
|
+
const quote = assignmentValue.startsWith('"')
|
|
171
|
+
? '"'
|
|
172
|
+
: assignmentValue.startsWith("'")
|
|
173
|
+
? "'"
|
|
174
|
+
: "";
|
|
175
|
+
return `${prefix}${quote}${REDACTED_FIXTURE_VALUE}${quote}`;
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isCredentialBearingUrl(value: string): boolean {
|
|
181
|
+
try {
|
|
182
|
+
const parsed = new URL(value);
|
|
183
|
+
return (
|
|
184
|
+
parsed.username !== "" ||
|
|
185
|
+
parsed.password !== "" ||
|
|
186
|
+
parsed.hash !== "" ||
|
|
187
|
+
parsed.search !== "" ||
|
|
188
|
+
parsed.pathname.split("/").some((segment, index, segments) => {
|
|
189
|
+
const decoded = decodePathSegment(segment);
|
|
190
|
+
const previous = decodePathSegment(segments[index - 1] ?? "");
|
|
191
|
+
return (
|
|
192
|
+
isSensitiveFixtureKey(decoded) ||
|
|
193
|
+
isCredentialPathKey(previous) ||
|
|
194
|
+
isSensitiveFixtureValue(decoded)
|
|
195
|
+
);
|
|
196
|
+
})
|
|
197
|
+
);
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function encodeDiagnosticControls(value: string): string {
|
|
204
|
+
let result = "";
|
|
205
|
+
for (const character of value) {
|
|
206
|
+
const code = character.codePointAt(0) ?? 0;
|
|
207
|
+
if (code === 0x0a || code === 0x0d || code === 0x2028 || code === 0x2029) {
|
|
208
|
+
result += " ";
|
|
209
|
+
} else if (
|
|
210
|
+
(code >= 0 && code <= 0x1f) ||
|
|
211
|
+
(code >= 0x7f && code <= 0x9f) ||
|
|
212
|
+
code === 0x061c ||
|
|
213
|
+
code === 0x200e ||
|
|
214
|
+
code === 0x200f ||
|
|
215
|
+
(code >= 0x202a && code <= 0x202e) ||
|
|
216
|
+
(code >= 0x2066 && code <= 0x2069)
|
|
217
|
+
) {
|
|
218
|
+
result += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
219
|
+
} else {
|
|
220
|
+
result += character;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function decodePathSegment(value: string): string {
|
|
227
|
+
try {
|
|
228
|
+
return decodeURIComponent(value);
|
|
229
|
+
} catch {
|
|
230
|
+
return value;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function hasExplicitOrigin(value: string): boolean {
|
|
235
|
+
return /^[a-z][a-z\d+.-]*:\/\//i.test(value);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function shannonEntropy(value: string): number {
|
|
239
|
+
const counts = new Map<string, number>();
|
|
240
|
+
for (const character of value) counts.set(character, (counts.get(character) ?? 0) + 1);
|
|
241
|
+
let entropy = 0;
|
|
242
|
+
for (const count of counts.values()) {
|
|
243
|
+
const probability = count / value.length;
|
|
244
|
+
entropy -= probability * Math.log2(probability);
|
|
245
|
+
}
|
|
246
|
+
return entropy;
|
|
247
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,9 +7,14 @@ export type {
|
|
|
7
7
|
ApiFuseConfig,
|
|
8
8
|
BrowserConfig,
|
|
9
9
|
ProxyConfig,
|
|
10
|
+
ProxyProtocol,
|
|
11
|
+
ProxyResolutionOptions,
|
|
12
|
+
ProxyResolutionSource,
|
|
13
|
+
ProxyVendorName,
|
|
14
|
+
ResolvedProxyConfig,
|
|
10
15
|
SessionConfig,
|
|
11
16
|
} from "./config/loader.js";
|
|
12
|
-
export { defineConfig, loadApiFuseConfig } from "./config/loader.js";
|
|
17
|
+
export { defineConfig, loadApiFuseConfig, resolveProxy } from "./config/loader.js";
|
|
13
18
|
export {
|
|
14
19
|
canonicalJson,
|
|
15
20
|
digestProviderContract,
|
|
@@ -65,6 +70,20 @@ export {
|
|
|
65
70
|
export { createEnvContext } from "./runtime/env.js";
|
|
66
71
|
export { executeOperation } from "./runtime/executor.js";
|
|
67
72
|
export { createHttpClient } from "./runtime/http.js";
|
|
73
|
+
export {
|
|
74
|
+
createNativeNetworkClient,
|
|
75
|
+
deriveNativeCredentialAffinityKey,
|
|
76
|
+
NativeIdleTimeoutError,
|
|
77
|
+
NativeNetworkError,
|
|
78
|
+
NativeProxyExpiredError,
|
|
79
|
+
resolveNativeGatewayProxy,
|
|
80
|
+
type NativeGatewayProxy,
|
|
81
|
+
type NativeGatewayProxyResolutionInput,
|
|
82
|
+
type NativeGatewayProxySynthesizer,
|
|
83
|
+
type NativeGatewayProxySynthesisInput,
|
|
84
|
+
type NativeNetworkClientOptions,
|
|
85
|
+
type NativeNetworkErrorCode,
|
|
86
|
+
} from "./runtime/native-network.js";
|
|
68
87
|
export type { Insight, InsightSeverity } from "./runtime/insights.js";
|
|
69
88
|
export { generateInsights } from "./runtime/insights.js";
|
|
70
89
|
export {
|
|
@@ -174,6 +193,26 @@ export type {
|
|
|
174
193
|
Iso3166Alpha2CountryCode,
|
|
175
194
|
Iso4217CurrencyCode,
|
|
176
195
|
Iso8601Duration,
|
|
196
|
+
NativeContext,
|
|
197
|
+
NativeNetworkClient,
|
|
198
|
+
NativeNetworkCloseReason,
|
|
199
|
+
NativeNetworkConnection,
|
|
200
|
+
NativeNetworkConnectInput,
|
|
201
|
+
NativeNetworkConnectOptions,
|
|
202
|
+
NativeNetworkDynamicGrantOptions,
|
|
203
|
+
NativeNetworkEgressGrant,
|
|
204
|
+
NativeProviderConfig,
|
|
205
|
+
NativeProviderContext,
|
|
206
|
+
NativeProxyDrainHandler,
|
|
207
|
+
NativeProxyEgressInfo,
|
|
208
|
+
NativeProxyExpiringEvent,
|
|
209
|
+
NativeProxyExpiringReason,
|
|
210
|
+
NativeTcpDynamicEgressRule,
|
|
211
|
+
NativeTcpEgressGrant,
|
|
212
|
+
NativeTcpEgressRule,
|
|
213
|
+
NativeTcpPortRange,
|
|
214
|
+
NativeTcpTlsMode,
|
|
215
|
+
NativeTlsConnectOptions,
|
|
177
216
|
OperationAnnotations,
|
|
178
217
|
OperationApprovalPolicy,
|
|
179
218
|
OperationContractMetadata,
|
|
@@ -208,6 +247,8 @@ export type {
|
|
|
208
247
|
ProviderContext,
|
|
209
248
|
ProviderDefinition,
|
|
210
249
|
ProviderDeploymentOverrides,
|
|
250
|
+
ProviderFileRef,
|
|
251
|
+
ProviderFilesContext,
|
|
211
252
|
ProviderHealthMonitorConfig,
|
|
212
253
|
ProviderHealthProbeConfig,
|
|
213
254
|
ProviderLocale,
|
|
@@ -224,6 +265,7 @@ export type {
|
|
|
224
265
|
ProviderPublicConnectionMode,
|
|
225
266
|
ProviderPublicProfile,
|
|
226
267
|
ProviderReviewed,
|
|
268
|
+
ProviderResolvedFile,
|
|
227
269
|
ProviderRuntimeState,
|
|
228
270
|
ProviderSecretDeclaration,
|
|
229
271
|
ProviderStateDurationString,
|
|
@@ -245,6 +287,8 @@ export type {
|
|
|
245
287
|
StateValue,
|
|
246
288
|
StateWriteOptions,
|
|
247
289
|
StealthClient,
|
|
290
|
+
StealthCookieStore,
|
|
291
|
+
StealthCookieStoreV1,
|
|
248
292
|
StealthFetchOptions,
|
|
249
293
|
StealthPlatform,
|
|
250
294
|
StealthProfile,
|
package/src/provider.ts
CHANGED
|
@@ -94,6 +94,26 @@ export type {
|
|
|
94
94
|
HttpRetryOptions,
|
|
95
95
|
HttpRetrySummary,
|
|
96
96
|
InferSchemaOutput,
|
|
97
|
+
NativeContext,
|
|
98
|
+
NativeNetworkClient,
|
|
99
|
+
NativeNetworkCloseReason,
|
|
100
|
+
NativeNetworkConnection,
|
|
101
|
+
NativeNetworkConnectInput,
|
|
102
|
+
NativeNetworkConnectOptions,
|
|
103
|
+
NativeNetworkDynamicGrantOptions,
|
|
104
|
+
NativeNetworkEgressGrant,
|
|
105
|
+
NativeProviderConfig,
|
|
106
|
+
NativeProviderContext,
|
|
107
|
+
NativeProxyDrainHandler,
|
|
108
|
+
NativeProxyEgressInfo,
|
|
109
|
+
NativeProxyExpiringEvent,
|
|
110
|
+
NativeProxyExpiringReason,
|
|
111
|
+
NativeTcpDynamicEgressRule,
|
|
112
|
+
NativeTcpEgressGrant,
|
|
113
|
+
NativeTcpEgressRule,
|
|
114
|
+
NativeTcpPortRange,
|
|
115
|
+
NativeTcpTlsMode,
|
|
116
|
+
NativeTlsConnectOptions,
|
|
97
117
|
OperationApprovalPolicy,
|
|
98
118
|
OperationContractMetadata,
|
|
99
119
|
OperationDefinition,
|
|
@@ -116,6 +136,8 @@ export type {
|
|
|
116
136
|
ProviderContext,
|
|
117
137
|
ProviderDefinition,
|
|
118
138
|
ProviderDeploymentOverrides,
|
|
139
|
+
ProviderFileRef,
|
|
140
|
+
ProviderFilesContext,
|
|
119
141
|
ProviderLocale,
|
|
120
142
|
ProviderLocaleKey,
|
|
121
143
|
ProviderLocaleKeyInput,
|
|
@@ -123,6 +145,7 @@ export type {
|
|
|
123
145
|
ProviderProxyPolicy,
|
|
124
146
|
ProviderPublicConnectionMode,
|
|
125
147
|
ProviderPublicProfile,
|
|
148
|
+
ProviderResolvedFile,
|
|
126
149
|
ProviderRuntimeState,
|
|
127
150
|
ProviderStateDurationString,
|
|
128
151
|
ProviderStateNamespace,
|
|
@@ -135,6 +158,20 @@ export type {
|
|
|
135
158
|
StateValue,
|
|
136
159
|
StateWriteOptions,
|
|
137
160
|
} from "./types.js";
|
|
161
|
+
export {
|
|
162
|
+
createNativeNetworkClient,
|
|
163
|
+
deriveNativeCredentialAffinityKey,
|
|
164
|
+
NativeIdleTimeoutError,
|
|
165
|
+
NativeNetworkError,
|
|
166
|
+
NativeProxyExpiredError,
|
|
167
|
+
resolveNativeGatewayProxy,
|
|
168
|
+
type NativeGatewayProxy,
|
|
169
|
+
type NativeGatewayProxyResolutionInput,
|
|
170
|
+
type NativeGatewayProxySynthesizer,
|
|
171
|
+
type NativeGatewayProxySynthesisInput,
|
|
172
|
+
type NativeNetworkClientOptions,
|
|
173
|
+
type NativeNetworkErrorCode,
|
|
174
|
+
} from "./runtime/native-network.js";
|
|
138
175
|
export {
|
|
139
176
|
HttpRetryAfterPolicy,
|
|
140
177
|
HttpRetryDelayStrategy,
|
package/src/runtime/http.ts
CHANGED
|
@@ -28,7 +28,13 @@ import {
|
|
|
28
28
|
shouldRetryProxyTransportAttempt,
|
|
29
29
|
validateUnsafeProxyTransportRetryMethods,
|
|
30
30
|
} from "./proxy-retry-policy.js";
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
normalizeHttpRequestBody,
|
|
33
|
+
redactSensitiveError,
|
|
34
|
+
redactSensitiveRequestError,
|
|
35
|
+
type SerializedRequestUrl,
|
|
36
|
+
serializeRequestUrl,
|
|
37
|
+
} from "./request-options.js";
|
|
32
38
|
|
|
33
39
|
const DEFAULT_HTTP_BASE_URL = "http://localhost";
|
|
34
40
|
|
|
@@ -208,9 +214,58 @@ function requireNativeResponseBody(response: Response): ReadableStream<Uint8Arra
|
|
|
208
214
|
return response.body;
|
|
209
215
|
}
|
|
210
216
|
|
|
211
|
-
function
|
|
217
|
+
function sanitizeStreamErrors(
|
|
218
|
+
body: ReadableStream<Uint8Array>,
|
|
219
|
+
serializedUrl: SerializedRequestUrl,
|
|
220
|
+
): ReadableStream<Uint8Array> {
|
|
221
|
+
if (serializedUrl.sensitiveValues.length === 0) return body;
|
|
222
|
+
|
|
223
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
224
|
+
return new ReadableStream<Uint8Array>(
|
|
225
|
+
{
|
|
226
|
+
async pull(controller) {
|
|
227
|
+
try {
|
|
228
|
+
reader ??= body.getReader();
|
|
229
|
+
const chunk = await reader.read();
|
|
230
|
+
if (chunk.done) {
|
|
231
|
+
controller.close();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
controller.enqueue(chunk.value);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
controller.error(
|
|
237
|
+
redactSensitiveError(
|
|
238
|
+
error,
|
|
239
|
+
serializedUrl.sensitiveValues,
|
|
240
|
+
serializedUrl.requestUrl,
|
|
241
|
+
serializedUrl.redactedUrl,
|
|
242
|
+
),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
async cancel(reason) {
|
|
247
|
+
try {
|
|
248
|
+
await (reader ? reader.cancel(reason) : body.cancel(reason));
|
|
249
|
+
} catch (error) {
|
|
250
|
+
throw redactSensitiveError(
|
|
251
|
+
error,
|
|
252
|
+
serializedUrl.sensitiveValues,
|
|
253
|
+
serializedUrl.requestUrl,
|
|
254
|
+
serializedUrl.redactedUrl,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{ highWaterMark: 0 },
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function toNativeHttpStreamResponse(
|
|
264
|
+
response: Response,
|
|
265
|
+
serializedUrl: SerializedRequestUrl,
|
|
266
|
+
): HttpStreamResponse {
|
|
212
267
|
const headers = Object.fromEntries(response.headers.entries());
|
|
213
|
-
const body = requireNativeResponseBody(response);
|
|
268
|
+
const body = sanitizeStreamErrors(requireNativeResponseBody(response), serializedUrl);
|
|
214
269
|
return {
|
|
215
270
|
body,
|
|
216
271
|
headers,
|
|
@@ -305,6 +360,22 @@ function normalizeNativeFetchBody(body: unknown): string | ArrayBuffer | undefin
|
|
|
305
360
|
return copied.buffer;
|
|
306
361
|
}
|
|
307
362
|
|
|
363
|
+
function serializeHttpRequestUrl(
|
|
364
|
+
baseUrl: string | undefined,
|
|
365
|
+
url: string,
|
|
366
|
+
options: RequestOptions,
|
|
367
|
+
): SerializedRequestUrl {
|
|
368
|
+
try {
|
|
369
|
+
return serializeRequestUrl(
|
|
370
|
+
resolveHttpUrl(baseUrl, url),
|
|
371
|
+
options.params,
|
|
372
|
+
options.sensitiveParams,
|
|
373
|
+
);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
308
379
|
async function fetchNativeHttp(
|
|
309
380
|
baseUrl: string | undefined,
|
|
310
381
|
url: string,
|
|
@@ -316,7 +387,8 @@ async function fetchNativeHttp(
|
|
|
316
387
|
proxyAttemptOffset = 0,
|
|
317
388
|
dedupe?: { attempted: Set<string> },
|
|
318
389
|
): Promise<NativeHttpAttemptOutcome> {
|
|
319
|
-
const
|
|
390
|
+
const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
|
|
391
|
+
const { requestUrl } = serializedUrl;
|
|
320
392
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
321
393
|
const timeoutHandle = options.timeout
|
|
322
394
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
@@ -374,9 +446,19 @@ async function fetchNativeHttp(
|
|
|
374
446
|
return toNativeHttpResponse(response);
|
|
375
447
|
} catch (error) {
|
|
376
448
|
if (error instanceof SyntaxError) {
|
|
377
|
-
throw
|
|
449
|
+
throw redactSensitiveError(
|
|
450
|
+
error,
|
|
451
|
+
serializedUrl.sensitiveValues,
|
|
452
|
+
serializedUrl.requestUrl,
|
|
453
|
+
serializedUrl.redactedUrl,
|
|
454
|
+
);
|
|
378
455
|
}
|
|
379
|
-
const transportError =
|
|
456
|
+
const transportError: NativeHttpAttemptError = redactSensitiveError(
|
|
457
|
+
toHttpTransportError(error),
|
|
458
|
+
serializedUrl.sensitiveValues,
|
|
459
|
+
serializedUrl.requestUrl,
|
|
460
|
+
serializedUrl.redactedUrl,
|
|
461
|
+
);
|
|
380
462
|
transportError.proxyUsed = Boolean(proxy);
|
|
381
463
|
throw transportError;
|
|
382
464
|
} finally {
|
|
@@ -392,7 +474,8 @@ async function fetchNativeHttpStream(
|
|
|
392
474
|
clientOptions: HttpClientOptions,
|
|
393
475
|
warn: (message: string) => void,
|
|
394
476
|
): Promise<HttpStreamResponse> {
|
|
395
|
-
const
|
|
477
|
+
const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
|
|
478
|
+
const { requestUrl } = serializedUrl;
|
|
396
479
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
397
480
|
const timeoutHandle = options.timeout
|
|
398
481
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
@@ -421,12 +504,22 @@ async function fetchNativeHttpStream(
|
|
|
421
504
|
});
|
|
422
505
|
}
|
|
423
506
|
|
|
424
|
-
return toNativeHttpStreamResponse(response);
|
|
507
|
+
return toNativeHttpStreamResponse(response, serializedUrl);
|
|
425
508
|
} catch (error) {
|
|
426
509
|
if (error instanceof SyntaxError) {
|
|
427
|
-
throw
|
|
510
|
+
throw redactSensitiveError(
|
|
511
|
+
error,
|
|
512
|
+
serializedUrl.sensitiveValues,
|
|
513
|
+
serializedUrl.requestUrl,
|
|
514
|
+
serializedUrl.redactedUrl,
|
|
515
|
+
);
|
|
428
516
|
}
|
|
429
|
-
throw
|
|
517
|
+
throw redactSensitiveError(
|
|
518
|
+
toHttpTransportError(error),
|
|
519
|
+
serializedUrl.sensitiveValues,
|
|
520
|
+
serializedUrl.requestUrl,
|
|
521
|
+
serializedUrl.redactedUrl,
|
|
522
|
+
);
|
|
430
523
|
} finally {
|
|
431
524
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
432
525
|
}
|
|
@@ -451,22 +544,29 @@ export function createHttpClient(
|
|
|
451
544
|
method: string,
|
|
452
545
|
options: RequestOptions & { body?: unknown } = {},
|
|
453
546
|
): Promise<HttpResponse> {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
547
|
+
const { explicitRetry, headersOptions, methodName, retryOptions } = (() => {
|
|
548
|
+
try {
|
|
549
|
+
if (!baseUrl && !isAbsoluteUrl(url)) {
|
|
550
|
+
throw new TransportError(
|
|
551
|
+
"ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared",
|
|
552
|
+
{ code: "transport_invalid_url" },
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
assertNoHttpTransportOverrides(options);
|
|
556
|
+
const headersOptions = withClientHeaders(options, clientOptions, options.body);
|
|
557
|
+
const methodName = normalizeHttpMethod(method);
|
|
558
|
+
const explicitRetry = headersOptions.retry !== undefined;
|
|
559
|
+
const retryOptions =
|
|
560
|
+
normalizeProxyTransportRetryOptions(headersOptions.retry, {
|
|
561
|
+
label: "HTTP",
|
|
562
|
+
}) ??
|
|
563
|
+
(explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
|
|
564
|
+
if (retryOptions) validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
|
|
565
|
+
return { explicitRetry, headersOptions, methodName, retryOptions };
|
|
566
|
+
} catch (error) {
|
|
567
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
568
|
+
}
|
|
569
|
+
})();
|
|
470
570
|
const retryEnabled = Boolean(
|
|
471
571
|
retryOptions &&
|
|
472
572
|
retryOptions.attempts > 1 &&
|
|
@@ -531,9 +631,7 @@ export function createHttpClient(
|
|
|
531
631
|
explicitRetry,
|
|
532
632
|
method: methodName,
|
|
533
633
|
});
|
|
534
|
-
const dedupeContext = dedupeAllocatorEndpoints
|
|
535
|
-
? { attempted: new Set<string>() }
|
|
536
|
-
: undefined;
|
|
634
|
+
const dedupeContext = dedupeAllocatorEndpoints ? { attempted: new Set<string>() } : undefined;
|
|
537
635
|
|
|
538
636
|
const executeOnce = (proxyAttemptOffset = 0): Promise<NativeHttpAttemptOutcome> =>
|
|
539
637
|
fetchNativeHttp(
|
|
@@ -654,15 +752,23 @@ export function createHttpClient(
|
|
|
654
752
|
method: string,
|
|
655
753
|
options: RequestOptions & { body?: unknown } = {},
|
|
656
754
|
): Promise<HttpStreamResponse> {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
755
|
+
const { headersOptions, methodName } = (() => {
|
|
756
|
+
try {
|
|
757
|
+
if (!baseUrl && !isAbsoluteUrl(url)) {
|
|
758
|
+
throw new TransportError(
|
|
759
|
+
"ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared",
|
|
760
|
+
{ code: "transport_invalid_url" },
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
assertNoHttpTransportOverrides(options);
|
|
764
|
+
return {
|
|
765
|
+
headersOptions: withClientHeaders(options, clientOptions, options.body),
|
|
766
|
+
methodName: normalizeHttpMethod(method),
|
|
767
|
+
};
|
|
768
|
+
} catch (error) {
|
|
769
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
770
|
+
}
|
|
771
|
+
})();
|
|
666
772
|
return fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce);
|
|
667
773
|
}
|
|
668
774
|
|