@apifuse/provider-sdk 2.2.0-beta.14 → 2.2.0-beta.15
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/CHANGELOG.md +6 -0
- package/dist/define.js +9 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +19 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/native-egress-policy.d.ts +27 -0
- package/dist/native-egress-policy.js +225 -0
- package/dist/provider.d.ts +3 -3
- package/dist/provider.js +2 -2
- package/dist/runtime/http.js +189 -9
- package/dist/runtime/native-network.d.ts +39 -4
- package/dist/runtime/native-network.js +365 -20
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/stealth.js +16 -44
- package/dist/server/serve.js +37 -0
- package/dist/testing/run.js +32 -13
- package/dist/types.d.ts +23 -2
- package/package.json +1 -1
- package/src/define.ts +11 -0
- package/src/errors.ts +28 -0
- package/src/index.ts +6 -0
- package/src/native-egress-policy.ts +285 -0
- package/src/provider.ts +7 -0
- package/src/runtime/http.ts +217 -9
- package/src/runtime/native-network.ts +474 -22
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/stealth.ts +20 -47
- package/src/server/serve.ts +36 -0
- package/src/testing/run.ts +39 -14
- package/src/types.ts +32 -2
package/src/runtime/stealth.ts
CHANGED
|
@@ -50,6 +50,11 @@ import {
|
|
|
50
50
|
shouldRetryProxyTransportAttempt,
|
|
51
51
|
validateUnsafeProxyTransportRetryMethods,
|
|
52
52
|
} from "./proxy-retry-policy.js";
|
|
53
|
+
import {
|
|
54
|
+
evaluateRedirectHop,
|
|
55
|
+
isRedirectStatus,
|
|
56
|
+
resolveRedirectUrl,
|
|
57
|
+
} from "./redirects.js";
|
|
53
58
|
import {
|
|
54
59
|
isSensitiveKey,
|
|
55
60
|
redactSensitiveError,
|
|
@@ -731,16 +736,6 @@ function normalizeMethod(method: HttpMethod | string): StealthMethod {
|
|
|
731
736
|
}
|
|
732
737
|
}
|
|
733
738
|
|
|
734
|
-
function isRedirectStatus(status: number): boolean {
|
|
735
|
-
return [301, 302, 303, 307, 308].includes(status);
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function nextRedirectMethod(status: number, method: StealthMethod): StealthMethod {
|
|
739
|
-
if (status === 303 && method !== "HEAD") return "GET";
|
|
740
|
-
if ((status === 301 || status === 302) && method === "POST") return "GET";
|
|
741
|
-
return method;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
739
|
function locationHeader(headers: Record<string, string>): string | undefined {
|
|
745
740
|
for (const [name, value] of Object.entries(headers)) {
|
|
746
741
|
if (name.toLowerCase() === "location") return value;
|
|
@@ -1256,7 +1251,7 @@ function createSessionFetcher(
|
|
|
1256
1251
|
const redactedLocation = location ? redactRedirectUrl(location) : undefined;
|
|
1257
1252
|
let nextUrl: string | undefined;
|
|
1258
1253
|
try {
|
|
1259
|
-
nextUrl =
|
|
1254
|
+
nextUrl = resolveRedirectUrl(location, responseUrl);
|
|
1260
1255
|
} catch (error) {
|
|
1261
1256
|
throw redactSensitiveError(error, [...sensitiveValues], location, redactedLocation);
|
|
1262
1257
|
}
|
|
@@ -1297,51 +1292,29 @@ function createSessionFetcher(
|
|
|
1297
1292
|
throw sanitizedError;
|
|
1298
1293
|
}
|
|
1299
1294
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
if (!nextUrl) {
|
|
1311
|
-
return {
|
|
1312
|
-
final: response,
|
|
1313
|
-
hops,
|
|
1314
|
-
reason: "missing_location",
|
|
1315
|
-
cookies: cookieJar.snapshot(),
|
|
1316
|
-
cookieStore: cookieJar.serialize(),
|
|
1317
|
-
};
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
if (hops.length > maxHops) {
|
|
1295
|
+
const decision = evaluateRedirectHop({
|
|
1296
|
+
status: response.status,
|
|
1297
|
+
method,
|
|
1298
|
+
nextUrl,
|
|
1299
|
+
shouldStop,
|
|
1300
|
+
redirectCount: hops.length,
|
|
1301
|
+
maxHops,
|
|
1302
|
+
visitedRequests,
|
|
1303
|
+
});
|
|
1304
|
+
if (decision.kind === "stop") {
|
|
1321
1305
|
return {
|
|
1322
1306
|
final: response,
|
|
1323
1307
|
hops,
|
|
1324
|
-
reason:
|
|
1308
|
+
reason: decision.reason,
|
|
1325
1309
|
cookies: cookieJar.snapshot(),
|
|
1326
1310
|
cookieStore: cookieJar.serialize(),
|
|
1327
1311
|
};
|
|
1328
1312
|
}
|
|
1329
|
-
|
|
1330
|
-
const nextMethod = nextRedirectMethod(response.status, method);
|
|
1331
|
-
if (nextMethod !== method) {
|
|
1313
|
+
if (decision.nextMethod !== method) {
|
|
1332
1314
|
body = undefined;
|
|
1333
1315
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
final: response,
|
|
1337
|
-
hops,
|
|
1338
|
-
reason: "loop",
|
|
1339
|
-
cookies: cookieJar.snapshot(),
|
|
1340
|
-
cookieStore: cookieJar.serialize(),
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
1343
|
-
method = nextMethod;
|
|
1344
|
-
currentUrl = nextUrl;
|
|
1316
|
+
method = decision.nextMethod;
|
|
1317
|
+
currentUrl = decision.nextUrl;
|
|
1345
1318
|
}
|
|
1346
1319
|
|
|
1347
1320
|
if (!response) {
|
package/src/server/serve.ts
CHANGED
|
@@ -36,6 +36,7 @@ import { createEnvContext } from "../runtime/env.js";
|
|
|
36
36
|
import { executeOperation } from "../runtime/executor.js";
|
|
37
37
|
import { createHttpClient } from "../runtime/http.js";
|
|
38
38
|
import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
|
|
39
|
+
import { createNativeNetworkClient } from "../runtime/native-network.js";
|
|
39
40
|
import { getProviderBaseUrl } from "../runtime/provider.js";
|
|
40
41
|
import {
|
|
41
42
|
PROXY_AUTH_IP_DENIED_CODE,
|
|
@@ -82,6 +83,7 @@ import type {
|
|
|
82
83
|
OperationSseTransport,
|
|
83
84
|
ProviderContext,
|
|
84
85
|
ProviderDefinition,
|
|
86
|
+
ProviderProxyPolicy,
|
|
85
87
|
ProviderRuntimeState,
|
|
86
88
|
ProviderStreamEvent,
|
|
87
89
|
StealthClient,
|
|
@@ -287,6 +289,13 @@ function resolveOperationConnectionId(request: OperationRequest): string | undef
|
|
|
287
289
|
return request.connection?.id ?? request.connectionId;
|
|
288
290
|
}
|
|
289
291
|
|
|
292
|
+
function resolveNativeProxyPolicy(provider: ProviderDefinition): ProviderProxyPolicy | undefined {
|
|
293
|
+
if (typeof provider.proxy === "object") return provider.proxy;
|
|
294
|
+
if (provider.proxy === true) return { mode: "optional" };
|
|
295
|
+
if (provider.proxy === false) return { mode: "disabled" };
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
290
299
|
function createProviderContext(
|
|
291
300
|
provider: ProviderDefinition,
|
|
292
301
|
request: OperationRequest,
|
|
@@ -353,6 +362,17 @@ function createProviderContext(
|
|
|
353
362
|
engine: provider.browser?.engine,
|
|
354
363
|
})
|
|
355
364
|
: createBrowserStub(),
|
|
365
|
+
...(provider.native
|
|
366
|
+
? {
|
|
367
|
+
native: {
|
|
368
|
+
network: createNativeNetworkClient({
|
|
369
|
+
egress: provider.native.network,
|
|
370
|
+
proxyPolicy: resolveNativeProxyPolicy(provider),
|
|
371
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
372
|
+
}),
|
|
373
|
+
},
|
|
374
|
+
}
|
|
375
|
+
: {}),
|
|
356
376
|
trace: createTraceContext(),
|
|
357
377
|
auth: createAuthStub(),
|
|
358
378
|
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
|
@@ -455,6 +475,17 @@ function createAuthFlowContext(
|
|
|
455
475
|
? createStealthClient(stealthBaseUrl, stealthProfile.name, stealthClientOptions)
|
|
456
476
|
: createStealthClient(stealthBaseUrl, stealthClientOptions)
|
|
457
477
|
: createStealthStub(),
|
|
478
|
+
...(provider.native
|
|
479
|
+
? {
|
|
480
|
+
native: {
|
|
481
|
+
network: createNativeNetworkClient({
|
|
482
|
+
egress: provider.native.network,
|
|
483
|
+
proxyPolicy: resolveNativeProxyPolicy(provider),
|
|
484
|
+
affinityKey: proxyClientOptions.affinityKey,
|
|
485
|
+
}),
|
|
486
|
+
},
|
|
487
|
+
}
|
|
488
|
+
: {}),
|
|
458
489
|
env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
|
|
459
490
|
credential,
|
|
460
491
|
context: flowContextStore.context,
|
|
@@ -894,6 +925,11 @@ const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
|
|
|
894
925
|
"transport_stream_unavailable",
|
|
895
926
|
"transport_invalid_method",
|
|
896
927
|
"http_transport_override_unsupported",
|
|
928
|
+
"http_redirect_policy_invalid",
|
|
929
|
+
"http_redirect_stopped",
|
|
930
|
+
"http_redirect_max_hops",
|
|
931
|
+
"http_redirect_missing_location",
|
|
932
|
+
"http_redirect_loop",
|
|
897
933
|
"transport_invalid_url",
|
|
898
934
|
"retry_exhausted",
|
|
899
935
|
"auth_abort_unsafe_data",
|
package/src/testing/run.ts
CHANGED
|
@@ -4,6 +4,12 @@ import { createProviderCache } from "../runtime/cache.js";
|
|
|
4
4
|
import { createTestProviderChoiceContext } from "../runtime/choice.js";
|
|
5
5
|
import { createMemoryProviderRuntimeState } from "../runtime/state.js";
|
|
6
6
|
import { createUnsupportedSttClient } from "../runtime/stt.js";
|
|
7
|
+
import {
|
|
8
|
+
createNativeEgressAuthorization,
|
|
9
|
+
NativeNetworkError,
|
|
10
|
+
snapshotNativeConnectInput,
|
|
11
|
+
snapshotNativeGrantInput,
|
|
12
|
+
} from "../runtime/native-network.js";
|
|
7
13
|
import { safeParseSchemaSync } from "../schema.js";
|
|
8
14
|
import { requestPathForFixture } from "../fixture-sanitization.js";
|
|
9
15
|
import { findStreamCaptureGroup, replayStreamEvidence } from "../stream-evidence.js";
|
|
@@ -307,6 +313,17 @@ function createUpstreamContext(
|
|
|
307
313
|
};
|
|
308
314
|
const request = { headers: {} };
|
|
309
315
|
const state = createMemoryProviderRuntimeState();
|
|
316
|
+
const nativeEgress = provider.native
|
|
317
|
+
? createNativeEgressAuthorization({ egress: provider.native.network })
|
|
318
|
+
: undefined;
|
|
319
|
+
const requireNativeEgress = () => {
|
|
320
|
+
if (!nativeEgress)
|
|
321
|
+
throw new NativeNetworkError(
|
|
322
|
+
"Native egress authorization is unavailable",
|
|
323
|
+
"native_egress_policy_invalid",
|
|
324
|
+
);
|
|
325
|
+
return nativeEgress;
|
|
326
|
+
};
|
|
310
327
|
const dispatch = async (
|
|
311
328
|
call: Omit<StandardTestsUpstreamCall, "operationName">,
|
|
312
329
|
): Promise<NormalizedUpstreamResponse> => {
|
|
@@ -498,30 +515,38 @@ function createUpstreamContext(
|
|
|
498
515
|
...(provider.native
|
|
499
516
|
? {
|
|
500
517
|
native: {
|
|
501
|
-
|
|
502
|
-
connectTcp: async (options) =>
|
|
503
|
-
|
|
518
|
+
network: {
|
|
519
|
+
connectTcp: async (options) => {
|
|
520
|
+
const request = snapshotNativeConnectInput(options);
|
|
521
|
+
requireNativeEgress().assertConnect(request, "disabled");
|
|
522
|
+
return createNativeConnection(
|
|
504
523
|
await dispatch({
|
|
505
524
|
transport: "native",
|
|
506
525
|
method: "connectTcp",
|
|
507
|
-
url: `tcp://${
|
|
508
|
-
options,
|
|
526
|
+
url: `tcp://${request.host}:${request.port}`,
|
|
527
|
+
options: request,
|
|
509
528
|
}),
|
|
510
529
|
dispatch,
|
|
511
|
-
`tcp://${
|
|
512
|
-
)
|
|
513
|
-
|
|
514
|
-
|
|
530
|
+
`tcp://${request.host}:${request.port}`,
|
|
531
|
+
);
|
|
532
|
+
},
|
|
533
|
+
connectTls: async (options) => {
|
|
534
|
+
const request = snapshotNativeConnectInput(options);
|
|
535
|
+
requireNativeEgress().assertConnect(request, "required");
|
|
536
|
+
return createNativeConnection(
|
|
515
537
|
await dispatch({
|
|
516
538
|
transport: "native",
|
|
517
539
|
method: "connectTls",
|
|
518
|
-
url: `tls://${
|
|
519
|
-
options,
|
|
540
|
+
url: `tls://${request.host}:${request.port}`,
|
|
541
|
+
options: request,
|
|
520
542
|
}),
|
|
521
543
|
dispatch,
|
|
522
|
-
`tls://${
|
|
523
|
-
)
|
|
524
|
-
|
|
544
|
+
`tls://${request.host}:${request.port}`,
|
|
545
|
+
);
|
|
546
|
+
},
|
|
547
|
+
grantTcpEgress: (input) => {
|
|
548
|
+
return requireNativeEgress().grant(snapshotNativeGrantInput(input));
|
|
549
|
+
},
|
|
525
550
|
},
|
|
526
551
|
},
|
|
527
552
|
}
|
package/src/types.ts
CHANGED
|
@@ -1019,8 +1019,32 @@ export interface RequestOptions {
|
|
|
1019
1019
|
*/
|
|
1020
1020
|
throwOnHttpError?: boolean;
|
|
1021
1021
|
retry?: boolean | HttpRetryPreset | HttpRetryOptions;
|
|
1022
|
+
/**
|
|
1023
|
+
* Opt-in redirect-hop enforcement for ctx.http. When present, redirects are
|
|
1024
|
+
* evaluated before the next request is issued. Existing callers that omit
|
|
1025
|
+
* this policy retain the native fetch redirect behavior.
|
|
1026
|
+
*/
|
|
1027
|
+
redirectPolicy?: HttpRedirectPolicy;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
export type RedirectRunReason =
|
|
1031
|
+
| "completed"
|
|
1032
|
+
| "stopped"
|
|
1033
|
+
| "max_hops"
|
|
1034
|
+
| "missing_location"
|
|
1035
|
+
| "loop";
|
|
1036
|
+
|
|
1037
|
+
export type HttpRedirectPolicyMode = "same-origin";
|
|
1038
|
+
|
|
1039
|
+
export interface HttpRedirectPolicy {
|
|
1040
|
+
/** Only follow redirects whose canonical scheme, host, and port match the initial URL. */
|
|
1041
|
+
mode: HttpRedirectPolicyMode;
|
|
1042
|
+
/** Maximum number of redirect hops that may be followed. Must be an integer from 0 to 20. */
|
|
1043
|
+
maxHops: number;
|
|
1022
1044
|
}
|
|
1023
1045
|
|
|
1046
|
+
export type HttpRedirectFailureReason = Exclude<RedirectRunReason, "completed">;
|
|
1047
|
+
|
|
1024
1048
|
export type HttpMethod =
|
|
1025
1049
|
| "HEAD"
|
|
1026
1050
|
| "head"
|
|
@@ -1039,7 +1063,7 @@ export type HttpMethod =
|
|
|
1039
1063
|
| "PATCH"
|
|
1040
1064
|
| "patch";
|
|
1041
1065
|
|
|
1042
|
-
export interface StealthFetchOptions extends RequestOptions {
|
|
1066
|
+
export interface StealthFetchOptions extends Omit<RequestOptions, "redirectPolicy"> {
|
|
1043
1067
|
method?: HttpMethod;
|
|
1044
1068
|
body?: string | Buffer;
|
|
1045
1069
|
redirect?: "follow" | "manual" | "error";
|
|
@@ -1152,7 +1176,7 @@ export interface StealthRedirectRunOptions
|
|
|
1152
1176
|
export interface StealthRedirectRunResult {
|
|
1153
1177
|
final: StealthResponse;
|
|
1154
1178
|
hops: StealthRedirectHop[];
|
|
1155
|
-
reason:
|
|
1179
|
+
reason: RedirectRunReason;
|
|
1156
1180
|
/**
|
|
1157
1181
|
* Complete flat view across all redirect hosts. Attributes and duplicate names are lost.
|
|
1158
1182
|
* @deprecated Use cookieStore for lossless persistence.
|
|
@@ -1290,6 +1314,12 @@ export interface NativeTcpEgressRule {
|
|
|
1290
1314
|
/**
|
|
1291
1315
|
* Bounded native TCP egress discovered through a declared bootstrap endpoint.
|
|
1292
1316
|
* Host suffixes are exact DNS suffixes, not wildcard patterns.
|
|
1317
|
+
*
|
|
1318
|
+
* Dynamic rules are ordered. The first rule whose source, target, port, and TLS
|
|
1319
|
+
* selectors match exclusively owns the grant; its ttlMs and maxGrants bounds
|
|
1320
|
+
* apply, and an exhausted/shorter rule never falls through to a later overlap.
|
|
1321
|
+
* Every rule must declare a source host selector, source port list/range, and
|
|
1322
|
+
* target port list/range; omitted ttlMs/maxGrants remain unbounded.
|
|
1293
1323
|
*/
|
|
1294
1324
|
export interface NativeTcpDynamicEgressRule {
|
|
1295
1325
|
readonly sourceHost?: string;
|