@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
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type ms from "ms";
|
|
2
|
+
import type { SerializedCookieJar } from "tough-cookie";
|
|
2
3
|
import type { infer as ZodInfer, ZodType } from "zod";
|
|
3
4
|
/** Minimal Standard Schema v1 shape accepted by provider operations. */
|
|
4
5
|
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
@@ -698,6 +699,12 @@ export interface ProviderProxyPolicy {
|
|
|
698
699
|
affinity?: ProviderProxySessionAffinity;
|
|
699
700
|
lifetimeMinutes?: number;
|
|
700
701
|
poolSize?: number;
|
|
702
|
+
/**
|
|
703
|
+
* Seconds before hard sticky expiry at which native connections receive
|
|
704
|
+
* the `expiring` event so the provider can drain and reconnect cleanly.
|
|
705
|
+
* Declared by the provider; the SDK does not assume a default cut point.
|
|
706
|
+
*/
|
|
707
|
+
drainLeadSeconds?: number;
|
|
701
708
|
};
|
|
702
709
|
}
|
|
703
710
|
export type ProviderProxyConfig = boolean | ProviderProxyPolicy;
|
|
@@ -839,6 +846,11 @@ export interface HttpRetrySummary {
|
|
|
839
846
|
export interface RequestOptions {
|
|
840
847
|
headers?: Record<string, string>;
|
|
841
848
|
params?: RequestParams;
|
|
849
|
+
/**
|
|
850
|
+
* Query parameters whose values contain credentials or other secret material.
|
|
851
|
+
* They are sent like `params`, but redacted from SDK errors, traces, and recorded fixtures.
|
|
852
|
+
*/
|
|
853
|
+
sensitiveParams?: Record<string, string>;
|
|
842
854
|
proxy?: string;
|
|
843
855
|
timeout?: number;
|
|
844
856
|
/**
|
|
@@ -853,6 +865,12 @@ export interface StealthFetchOptions extends RequestOptions {
|
|
|
853
865
|
method?: HttpMethod;
|
|
854
866
|
body?: string | Buffer;
|
|
855
867
|
redirect?: "follow" | "manual" | "error";
|
|
868
|
+
/**
|
|
869
|
+
* Maximum decoded response-body bytes to buffer. When set, the stealth
|
|
870
|
+
* transport aborts the response and throws `response_too_large` if the
|
|
871
|
+
* declared or streamed body exceeds this limit.
|
|
872
|
+
*/
|
|
873
|
+
maxBodyBytes?: number;
|
|
856
874
|
/**
|
|
857
875
|
* Offsets policy-managed proxy pool selection for caller-managed retries.
|
|
858
876
|
* Use when a request receives an upstream challenge page rather than a
|
|
@@ -872,17 +890,42 @@ export interface StealthFetchOptions extends RequestOptions {
|
|
|
872
890
|
};
|
|
873
891
|
}
|
|
874
892
|
export interface CookieJar {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
893
|
+
/** URL-less reads use the jar's response URL or session base URL. */
|
|
894
|
+
get(name: string, url?: string): string | undefined;
|
|
895
|
+
getAll(url?: string): Record<string, string>;
|
|
896
|
+
toString(url?: string): string;
|
|
897
|
+
find?(predicate: (cookie: string) => boolean, url?: string): string | undefined;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Version 1 of the JSON-safe, attribute-preserving stealth cookie store.
|
|
901
|
+
* The nested jar is tough-cookie's serialized form and retains cookie origin,
|
|
902
|
+
* Path, Secure, expiry, host-only, and other RFC attributes.
|
|
903
|
+
*/
|
|
904
|
+
export interface StealthCookieStoreV1 {
|
|
905
|
+
readonly version: 1;
|
|
906
|
+
readonly jar: SerializedCookieJar;
|
|
879
907
|
}
|
|
908
|
+
/** Cookie persistence formats understood by this SDK version. */
|
|
909
|
+
export type StealthCookieStore = StealthCookieStoreV1;
|
|
880
910
|
export interface StealthSessionCookies extends CookieJar {
|
|
881
|
-
has(name: string): boolean;
|
|
882
|
-
|
|
883
|
-
|
|
911
|
+
has(name: string, url?: string): boolean;
|
|
912
|
+
/** URL-less writes are scoped to the session base URL. */
|
|
913
|
+
setFromCookieStrings(cookieStrings: readonly string[], url?: string): void;
|
|
914
|
+
toHeader(url?: string): string;
|
|
915
|
+
/**
|
|
916
|
+
* Returns every cookie as a flat name/value map, collapsing duplicate names.
|
|
917
|
+
* @deprecated Use serialize() for lossless, attribute-preserving persistence.
|
|
918
|
+
*/
|
|
884
919
|
snapshot(): Record<string, string>;
|
|
920
|
+
/**
|
|
921
|
+
* Restores flat values as host-only, Path=/ cookies on the session base URL.
|
|
922
|
+
* @deprecated Use deserialize() with state produced by serialize().
|
|
923
|
+
*/
|
|
885
924
|
restore(cookies: Record<string, string>): void;
|
|
925
|
+
/** Returns a versioned, JSON-safe, attribute-preserving representation of every cookie. */
|
|
926
|
+
serialize(): StealthCookieStoreV1;
|
|
927
|
+
/** Replaces the jar with a previously serialized, attribute-preserving cookie store. */
|
|
928
|
+
deserialize(state: StealthCookieStore): void;
|
|
886
929
|
clear(): void;
|
|
887
930
|
}
|
|
888
931
|
export interface DeclarativeStealthResponse {
|
|
@@ -925,7 +968,13 @@ export interface StealthRedirectRunResult {
|
|
|
925
968
|
final: StealthResponse;
|
|
926
969
|
hops: StealthRedirectHop[];
|
|
927
970
|
reason: "completed" | "stopped" | "max_hops" | "missing_location" | "loop";
|
|
971
|
+
/**
|
|
972
|
+
* Complete flat view across all redirect hosts. Attributes and duplicate names are lost.
|
|
973
|
+
* @deprecated Use cookieStore for lossless persistence.
|
|
974
|
+
*/
|
|
928
975
|
cookies: Record<string, string>;
|
|
976
|
+
/** Versioned, attribute-preserving cookie state accumulated across the redirect chain. */
|
|
977
|
+
cookieStore: StealthCookieStoreV1;
|
|
929
978
|
}
|
|
930
979
|
export interface StealthSession {
|
|
931
980
|
fetch(url: string, options?: StealthFetchOptions): Promise<StealthResponse>;
|
|
@@ -988,6 +1037,139 @@ export interface HttpClient {
|
|
|
988
1037
|
stream(url: string, options?: RequestWithMethodOptions): Promise<HttpStreamResponse>;
|
|
989
1038
|
sse(url: string, options?: RequestWithMethodOptions): Promise<AsyncIterable<SseMessage>>;
|
|
990
1039
|
}
|
|
1040
|
+
/** Request-scoped file reference accepted by provider operation inputs. */
|
|
1041
|
+
export interface ProviderFileRef {
|
|
1042
|
+
readonly type: "request_file";
|
|
1043
|
+
readonly id: string;
|
|
1044
|
+
readonly filename: string;
|
|
1045
|
+
readonly mime_type?: string;
|
|
1046
|
+
readonly size: number;
|
|
1047
|
+
readonly sha256?: string;
|
|
1048
|
+
}
|
|
1049
|
+
/** File body resolved from a request-scoped {@link ProviderFileRef}. */
|
|
1050
|
+
export type ProviderResolvedFile = Omit<ProviderFileRef, "mime_type"> & {
|
|
1051
|
+
readonly mimeType?: string;
|
|
1052
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
1053
|
+
bytes(): Promise<Uint8Array>;
|
|
1054
|
+
stream(): ReadableStream<Uint8Array>;
|
|
1055
|
+
};
|
|
1056
|
+
/** Resolver supplied by runtimes that accept request-scoped file inputs. */
|
|
1057
|
+
export interface ProviderFilesContext {
|
|
1058
|
+
has(input: string | ProviderFileRef): boolean;
|
|
1059
|
+
resolve(input: string | ProviderFileRef): Promise<ProviderResolvedFile>;
|
|
1060
|
+
}
|
|
1061
|
+
export type NativeTcpTlsMode = "required" | "allowed" | "disabled";
|
|
1062
|
+
export interface NativeTcpPortRange {
|
|
1063
|
+
readonly start: number;
|
|
1064
|
+
readonly end: number;
|
|
1065
|
+
}
|
|
1066
|
+
/** Static native TCP egress declared by a provider. */
|
|
1067
|
+
export interface NativeTcpEgressRule {
|
|
1068
|
+
readonly host: string;
|
|
1069
|
+
readonly ports: readonly number[];
|
|
1070
|
+
readonly tls: NativeTcpTlsMode;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Bounded native TCP egress discovered through a declared bootstrap endpoint.
|
|
1074
|
+
* Host suffixes are exact DNS suffixes, not wildcard patterns.
|
|
1075
|
+
*/
|
|
1076
|
+
export interface NativeTcpDynamicEgressRule {
|
|
1077
|
+
readonly sourceHost?: string;
|
|
1078
|
+
readonly sourceHostSuffixes?: readonly string[];
|
|
1079
|
+
readonly sourcePorts?: readonly number[];
|
|
1080
|
+
readonly sourcePortRanges?: readonly NativeTcpPortRange[];
|
|
1081
|
+
readonly targetHostSuffixes: readonly string[];
|
|
1082
|
+
readonly targetPorts?: readonly number[];
|
|
1083
|
+
readonly targetPortRanges?: readonly NativeTcpPortRange[];
|
|
1084
|
+
readonly tls: NativeTcpTlsMode;
|
|
1085
|
+
readonly ttlMs?: number;
|
|
1086
|
+
readonly maxGrants?: number;
|
|
1087
|
+
}
|
|
1088
|
+
/** Common TCP/TLS connection input supported by the native runtime. */
|
|
1089
|
+
export interface NativeNetworkConnectInput {
|
|
1090
|
+
readonly host: string;
|
|
1091
|
+
readonly port: number;
|
|
1092
|
+
readonly serverName?: string;
|
|
1093
|
+
readonly rejectUnauthorized?: boolean;
|
|
1094
|
+
/**
|
|
1095
|
+
* Maximum time without a successful socket read before the connection is
|
|
1096
|
+
* closed. Opt-in; when absent, reads can remain pending indefinitely.
|
|
1097
|
+
*/
|
|
1098
|
+
readonly idleTimeoutMs?: number;
|
|
1099
|
+
/** Maximum time allowed to establish the TCP/SOCKS/TLS connection. */
|
|
1100
|
+
readonly timeoutMs?: number;
|
|
1101
|
+
readonly signal?: AbortSignal;
|
|
1102
|
+
/** Overrides the credential-derived sticky affinity key. */
|
|
1103
|
+
readonly affinityKey?: string;
|
|
1104
|
+
}
|
|
1105
|
+
export type NativeNetworkConnectOptions = Omit<NativeNetworkConnectInput, "serverName" | "rejectUnauthorized">;
|
|
1106
|
+
export type NativeTlsConnectOptions = NativeNetworkConnectInput;
|
|
1107
|
+
export interface NativeNetworkDynamicGrantOptions {
|
|
1108
|
+
readonly sourceHost: string;
|
|
1109
|
+
readonly sourcePort: number;
|
|
1110
|
+
readonly host: string;
|
|
1111
|
+
readonly port: number;
|
|
1112
|
+
readonly tls: NativeTcpTlsMode;
|
|
1113
|
+
readonly ttlMs?: number;
|
|
1114
|
+
}
|
|
1115
|
+
export interface NativeNetworkEgressGrant {
|
|
1116
|
+
revoke(): void;
|
|
1117
|
+
}
|
|
1118
|
+
/** Consumer-facing alias used by native TCP providers. */
|
|
1119
|
+
export type NativeTcpEgressGrant = NativeNetworkEgressGrant;
|
|
1120
|
+
/** Resolved egress identity for a native connection routed through a proxy. */
|
|
1121
|
+
export interface NativeProxyEgressInfo {
|
|
1122
|
+
readonly vendor: ProviderProxyProvider;
|
|
1123
|
+
readonly sticky: boolean;
|
|
1124
|
+
/** Vendor sticky session id (sid). Absent for rotating sessions. */
|
|
1125
|
+
readonly sessionId?: string;
|
|
1126
|
+
/** Hard expiry of the sticky binding, ISO 8601. */
|
|
1127
|
+
readonly expiresAt?: string;
|
|
1128
|
+
}
|
|
1129
|
+
export type NativeProxyExpiringReason = "sticky_expiry";
|
|
1130
|
+
export interface NativeProxyExpiringEvent {
|
|
1131
|
+
readonly expiresAt: string;
|
|
1132
|
+
readonly leadSeconds: number;
|
|
1133
|
+
readonly reason: NativeProxyExpiringReason;
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Cooperative drain handler. The SDK awaits this before closing a socket whose
|
|
1137
|
+
* sticky proxy binding is about to expire, then force-closes at hard expiry.
|
|
1138
|
+
*/
|
|
1139
|
+
export type NativeProxyDrainHandler = (event: NativeProxyExpiringEvent) => void | Promise<void>;
|
|
1140
|
+
/** Typed reason recorded when the SDK closes a native connection intentionally. */
|
|
1141
|
+
export interface NativeNetworkCloseReason {
|
|
1142
|
+
readonly code: string;
|
|
1143
|
+
readonly message: string;
|
|
1144
|
+
}
|
|
1145
|
+
/** Byte-oriented connection returned by the native TCP/TLS runtime. */
|
|
1146
|
+
export interface NativeNetworkConnection {
|
|
1147
|
+
/** Present when the connection was routed through a proxy. */
|
|
1148
|
+
readonly proxy?: NativeProxyEgressInfo;
|
|
1149
|
+
/** Present after an SDK-planned close, such as sticky proxy expiry. */
|
|
1150
|
+
readonly closeReason?: NativeNetworkCloseReason;
|
|
1151
|
+
/** Register a cooperative drain handler for sticky-expiry reconnects. */
|
|
1152
|
+
onExpiring?(handler: NativeProxyDrainHandler): void;
|
|
1153
|
+
read(): Promise<Uint8Array | null>;
|
|
1154
|
+
write(data: Uint8Array): Promise<void>;
|
|
1155
|
+
close(): Promise<void>;
|
|
1156
|
+
}
|
|
1157
|
+
export interface NativeNetworkClient {
|
|
1158
|
+
connectTcp(input: NativeNetworkConnectOptions): Promise<NativeNetworkConnection>;
|
|
1159
|
+
connectTls(input: NativeTlsConnectOptions): Promise<NativeNetworkConnection>;
|
|
1160
|
+
grantTcpEgress(input: NativeNetworkDynamicGrantOptions): NativeNetworkEgressGrant;
|
|
1161
|
+
}
|
|
1162
|
+
export interface NativeContext {
|
|
1163
|
+
readonly network: NativeNetworkClient;
|
|
1164
|
+
}
|
|
1165
|
+
/** Consumer-facing alias for the native capability on provider contexts. */
|
|
1166
|
+
export type NativeProviderContext = NativeContext;
|
|
1167
|
+
export interface NativeProviderConfig {
|
|
1168
|
+
readonly network?: {
|
|
1169
|
+
readonly tcp?: readonly NativeTcpEgressRule[];
|
|
1170
|
+
readonly dynamicTcp?: readonly NativeTcpDynamicEgressRule[];
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
991
1173
|
export interface ProviderCacheKeyOptions {
|
|
992
1174
|
/**
|
|
993
1175
|
* Additional field names to omit from stable key material. The SDK always
|
|
@@ -1319,6 +1501,8 @@ export interface FlowContext {
|
|
|
1319
1501
|
tenantId: string;
|
|
1320
1502
|
providerId: string;
|
|
1321
1503
|
http: HttpClient;
|
|
1504
|
+
/** Present when the selected runtime supplies native network capabilities. */
|
|
1505
|
+
readonly native?: NativeProviderContext;
|
|
1322
1506
|
stealth: StealthClient;
|
|
1323
1507
|
env: EnvContext;
|
|
1324
1508
|
credential?: CredentialContext;
|
|
@@ -1404,6 +1588,10 @@ export interface ProviderContext {
|
|
|
1404
1588
|
credential: CredentialContext;
|
|
1405
1589
|
request?: ProviderRequestContext;
|
|
1406
1590
|
http: HttpClient;
|
|
1591
|
+
/** Present for requests carrying runtime-resolvable file references. */
|
|
1592
|
+
readonly files?: ProviderFilesContext;
|
|
1593
|
+
/** Present when the selected runtime supplies native network capabilities. */
|
|
1594
|
+
readonly native?: NativeProviderContext;
|
|
1407
1595
|
cache: ProviderCache;
|
|
1408
1596
|
state: ProviderRuntimeState;
|
|
1409
1597
|
stealth: StealthClient;
|
|
@@ -1480,6 +1668,11 @@ export interface OperationDefinition<TInput extends SchemaLike = SchemaLike, TOu
|
|
|
1480
1668
|
fixtures?: {
|
|
1481
1669
|
request: InferSchemaOutput<TInput>;
|
|
1482
1670
|
response: InferSchemaOutput<TOutput>;
|
|
1671
|
+
/**
|
|
1672
|
+
* KST calendar date when `response` evidence was captured. Date fields in
|
|
1673
|
+
* the response align with this date, not a resolved relative request date.
|
|
1674
|
+
*/
|
|
1675
|
+
recordedAt?: string;
|
|
1483
1676
|
};
|
|
1484
1677
|
upstream?: {
|
|
1485
1678
|
baseUrl?: string;
|
|
@@ -1538,6 +1731,7 @@ export interface ProviderDefinition {
|
|
|
1538
1731
|
*/
|
|
1539
1732
|
deployment?: ProviderDeploymentOverrides;
|
|
1540
1733
|
allowedHosts?: string[];
|
|
1734
|
+
native?: NativeProviderConfig;
|
|
1541
1735
|
stealth?: {
|
|
1542
1736
|
profile: string;
|
|
1543
1737
|
platform: StealthPlatform;
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.2.0-beta.
|
|
2
|
+
"version": "2.2.0-beta.13",
|
|
3
3
|
"name": "@apifuse/provider-sdk",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
@@ -114,6 +114,8 @@
|
|
|
114
114
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
115
115
|
"re2-wasm": "^1.0",
|
|
116
116
|
"safe-regex": "^2.1",
|
|
117
|
+
"socks": "^2.8.9",
|
|
118
|
+
"tough-cookie": "^6.0.2",
|
|
117
119
|
"zod": "^4.4.3"
|
|
118
120
|
},
|
|
119
121
|
"repository": {
|
package/src/config/loader.ts
CHANGED
|
@@ -156,10 +156,19 @@ export type ProxyTelemetrySink = {
|
|
|
156
156
|
recordProxyVendorFailover?(event: ProxyVendorFailoverTelemetryEvent): void;
|
|
157
157
|
};
|
|
158
158
|
|
|
159
|
+
export type ProxyResolutionSource =
|
|
160
|
+
| "explicit"
|
|
161
|
+
| "env"
|
|
162
|
+
| "config"
|
|
163
|
+
| "smartproxy-allocator"
|
|
164
|
+
| "nodemaven-gateway";
|
|
165
|
+
|
|
159
166
|
export type ResolvedProxyConfig = {
|
|
160
167
|
shouldWarn: boolean;
|
|
161
168
|
url?: string;
|
|
162
|
-
|
|
169
|
+
/** SDK-native vendor that supplied the URL, when applicable. */
|
|
170
|
+
vendor?: ProxyVendorName;
|
|
171
|
+
source?: ProxyResolutionSource;
|
|
163
172
|
protocol?: ProxyProtocol;
|
|
164
173
|
diagnostics?: Record<string, string | number | boolean>;
|
|
165
174
|
};
|
|
@@ -656,6 +665,18 @@ export async function resolveProxyConfigAsync(
|
|
|
656
665
|
return { shouldWarn: true };
|
|
657
666
|
}
|
|
658
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Resolve the proxy URL for a provider-owned consumer such as a CAPTCHA solver.
|
|
670
|
+
* Vendor allocation and failover remain owned by the SDK.
|
|
671
|
+
*/
|
|
672
|
+
export async function resolveProxy(
|
|
673
|
+
options: ProxyResolutionOptions = {},
|
|
674
|
+
): Promise<ResolvedProxyConfig> {
|
|
675
|
+
const resolved = await resolveProxyConfigAsync(options);
|
|
676
|
+
const vendor = vendorFromResolvedSource(resolved.source);
|
|
677
|
+
return vendor ? { ...resolved, vendor } : resolved;
|
|
678
|
+
}
|
|
679
|
+
|
|
659
680
|
/**
|
|
660
681
|
* Each vendor's default egress protocol, chosen from live KR benchmarks. HTTP
|
|
661
682
|
* CONNECT wins for nodemaven (socks5 adds ~500ms through the gateway) and ties
|
package/src/define.ts
CHANGED
|
@@ -2,6 +2,7 @@ import ms from "ms";
|
|
|
2
2
|
|
|
3
3
|
import { ProviderError, ValidationError } from "./errors.js";
|
|
4
4
|
import { safeParseSchemaSync } from "./schema.js";
|
|
5
|
+
import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
|
|
5
6
|
import type {
|
|
6
7
|
AuthConfig,
|
|
7
8
|
BrowserEngine,
|
|
@@ -20,6 +21,7 @@ import type {
|
|
|
20
21
|
OperationSseTransport,
|
|
21
22
|
OperationTransport,
|
|
22
23
|
OperationWebSocketTransport,
|
|
24
|
+
NativeProviderConfig,
|
|
23
25
|
ProviderAccessConfig,
|
|
24
26
|
ProviderDefinition,
|
|
25
27
|
ProviderDeploymentOverrides,
|
|
@@ -205,6 +207,7 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
|
|
|
205
207
|
*/
|
|
206
208
|
deployment?: ProviderDeploymentOverrides;
|
|
207
209
|
allowedHosts?: string[];
|
|
210
|
+
native?: NativeProviderConfig;
|
|
208
211
|
stealth?: {
|
|
209
212
|
profile: string;
|
|
210
213
|
platform: StealthPlatform;
|
|
@@ -437,7 +440,7 @@ function validateProviderProxy(config: {
|
|
|
437
440
|
}
|
|
438
441
|
rejectUnknownFields(
|
|
439
442
|
proxy.session,
|
|
440
|
-
new Set(["affinity", "lifetimeMinutes", "poolSize"]),
|
|
443
|
+
new Set(["affinity", "lifetimeMinutes", "poolSize", "drainLeadSeconds"]),
|
|
441
444
|
"proxy.session",
|
|
442
445
|
);
|
|
443
446
|
if (proxy.session.affinity !== undefined) {
|
|
@@ -460,6 +463,34 @@ function validateProviderProxy(config: {
|
|
|
460
463
|
`Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`,
|
|
461
464
|
);
|
|
462
465
|
}
|
|
466
|
+
const drainLeadSeconds = proxy.session.drainLeadSeconds;
|
|
467
|
+
if (
|
|
468
|
+
drainLeadSeconds !== undefined &&
|
|
469
|
+
(!Number.isFinite(drainLeadSeconds) || drainLeadSeconds <= 0)
|
|
470
|
+
) {
|
|
471
|
+
throw new ValidationError(
|
|
472
|
+
`Provider "${config.id}" has invalid proxy.session.drainLeadSeconds: must be a positive number of seconds.`,
|
|
473
|
+
{
|
|
474
|
+
fix: `Use proxy.session.drainLeadSeconds: 120 to receive the sticky-expiry drain event 120s before hard expiry.`,
|
|
475
|
+
},
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
// A drain lead longer than the sticky lifetime would fire the expiring
|
|
479
|
+
// event before the session is even established, so the provider would
|
|
480
|
+
// never get a usable window. Reject the contradiction at build time.
|
|
481
|
+
if (
|
|
482
|
+
drainLeadSeconds !== undefined &&
|
|
483
|
+
lifetime !== undefined &&
|
|
484
|
+
Number.isFinite(lifetime) &&
|
|
485
|
+
drainLeadSeconds >= lifetime * 60
|
|
486
|
+
) {
|
|
487
|
+
throw new ValidationError(
|
|
488
|
+
`Provider "${config.id}" has proxy.session.drainLeadSeconds (${drainLeadSeconds}s) greater than or equal to proxy.session.lifetimeMinutes (${lifetime}m).`,
|
|
489
|
+
{
|
|
490
|
+
fix: `Lower drainLeadSeconds below the sticky lifetime so the drain event leaves a usable session window.`,
|
|
491
|
+
},
|
|
492
|
+
);
|
|
493
|
+
}
|
|
463
494
|
}
|
|
464
495
|
// Every credentialed vendor in a required-mode chain must declare its
|
|
465
496
|
// provider secret(s) so a missing credential fails at build/validation time,
|
|
@@ -2040,6 +2071,26 @@ function validateOperationFixtures(
|
|
|
2040
2071
|
fix: `Add operations.${operationName}.handler as an async function with signature (ctx, input) => Promise<output>`,
|
|
2041
2072
|
},
|
|
2042
2073
|
);
|
|
2074
|
+
if (operation.fixtures?.recordedAt !== undefined) {
|
|
2075
|
+
const recordedAt = operation.fixtures.recordedAt;
|
|
2076
|
+
const parsed =
|
|
2077
|
+
typeof recordedAt === "string"
|
|
2078
|
+
? new Date(`${recordedAt}T00:00:00.000Z`)
|
|
2079
|
+
: new Date(Number.NaN);
|
|
2080
|
+
const isCalendarDate =
|
|
2081
|
+
typeof recordedAt === "string" &&
|
|
2082
|
+
/^\d{4}-\d{2}-\d{2}$/.test(recordedAt) &&
|
|
2083
|
+
!Number.isNaN(parsed.getTime()) &&
|
|
2084
|
+
parsed.toISOString().slice(0, 10) === recordedAt;
|
|
2085
|
+
const kstToday = new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
2086
|
+
if (!isCalendarDate || recordedAt > kstToday)
|
|
2087
|
+
throw new ValidationError(
|
|
2088
|
+
`Fixture recordedAt must be a valid, non-future KST calendar date for provider "${providerId}" operation "${operationName}"`,
|
|
2089
|
+
{
|
|
2090
|
+
fix: `Set operations.${operationName}.fixtures.recordedAt to the KST capture date in YYYY-MM-DD format; it must not be in the future.`,
|
|
2091
|
+
},
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2043
2094
|
if (operation.fixtures?.request !== undefined) {
|
|
2044
2095
|
const result = safeParseSchemaSync(
|
|
2045
2096
|
operation.input,
|
|
@@ -2073,6 +2124,31 @@ function validateOperationFixtures(
|
|
|
2073
2124
|
}
|
|
2074
2125
|
}
|
|
2075
2126
|
|
|
2127
|
+
function resolveOperationFixtureRequests<TOperations extends Record<string, ProviderOperation>>(
|
|
2128
|
+
operations: TOperations,
|
|
2129
|
+
): TOperations {
|
|
2130
|
+
let changed = false;
|
|
2131
|
+
const resolvedOperations = Object.fromEntries(
|
|
2132
|
+
Object.entries(operations).map(([operationName, operation]) => {
|
|
2133
|
+
if (operation.fixtures?.request === undefined) return [operationName, operation];
|
|
2134
|
+
const request = resolveHealthCheckInputDateTokens(operation.fixtures.request);
|
|
2135
|
+
if (request === operation.fixtures.request) return [operationName, operation];
|
|
2136
|
+
changed = true;
|
|
2137
|
+
return [
|
|
2138
|
+
operationName,
|
|
2139
|
+
{
|
|
2140
|
+
...operation,
|
|
2141
|
+
fixtures: {
|
|
2142
|
+
...operation.fixtures,
|
|
2143
|
+
request,
|
|
2144
|
+
},
|
|
2145
|
+
},
|
|
2146
|
+
];
|
|
2147
|
+
}),
|
|
2148
|
+
) as TOperations;
|
|
2149
|
+
return changed ? resolvedOperations : operations;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2076
2152
|
/**
|
|
2077
2153
|
* Shallow shape guard only: the `deployment` object is passed through
|
|
2078
2154
|
* verbatim and deliberately not deep-validated by the SDK — the APIFuse
|
|
@@ -2093,6 +2169,7 @@ export function defineProvider<
|
|
|
2093
2169
|
config: TConfig & AuthStartNoInputGuard<TConfig>,
|
|
2094
2170
|
): ProviderDefinition & { operations: OperationMapConfig<TOperations> } {
|
|
2095
2171
|
validateProviderShape(config);
|
|
2172
|
+
const operations = resolveOperationFixtureRequests(config.operations);
|
|
2096
2173
|
if (!CONNECTOR_ID_REGEX.test(config.id))
|
|
2097
2174
|
throw new ProviderError(`Invalid provider id: "${config.id}"`, {
|
|
2098
2175
|
fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
|
|
@@ -2125,7 +2202,7 @@ export function defineProvider<
|
|
|
2125
2202
|
config.healthProbe ?? config.healthMonitor,
|
|
2126
2203
|
config.healthProbe !== undefined ? "healthProbe" : "healthMonitor",
|
|
2127
2204
|
);
|
|
2128
|
-
validateOperationFixtures(config.id,
|
|
2205
|
+
validateOperationFixtures(config.id, operations);
|
|
2129
2206
|
validateProviderDeployment(config.id, config.deployment);
|
|
2130
2207
|
validateProviderProxy(config);
|
|
2131
2208
|
validateProviderStt(config);
|
|
@@ -2149,6 +2226,7 @@ export function defineProvider<
|
|
|
2149
2226
|
// are owned by the APIFuse registry builder, not the SDK.
|
|
2150
2227
|
deployment: config.deployment,
|
|
2151
2228
|
allowedHosts: config.allowedHosts,
|
|
2229
|
+
native: config.native,
|
|
2152
2230
|
stealth: config.stealth,
|
|
2153
2231
|
proxy: config.proxy,
|
|
2154
2232
|
stt: config.stt,
|
|
@@ -2160,7 +2238,7 @@ export function defineProvider<
|
|
|
2160
2238
|
credential: config.credential,
|
|
2161
2239
|
context: config.context,
|
|
2162
2240
|
meta: config.meta,
|
|
2163
|
-
operations
|
|
2241
|
+
operations,
|
|
2164
2242
|
// Transitional healthMonitor → healthProbe alias: mirror whichever field
|
|
2165
2243
|
// was declared onto both so old and new consumers keep working.
|
|
2166
2244
|
healthMonitor: config.healthMonitor ?? config.healthProbe,
|
package/src/errors.ts
CHANGED
|
@@ -79,6 +79,21 @@ export class SDKError extends ProviderError {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** Raised when persisted stealth cookies use a store version this SDK cannot read. */
|
|
83
|
+
export class StealthCookieStoreVersionError extends SDKError {
|
|
84
|
+
constructor(public readonly version: unknown) {
|
|
85
|
+
const displayedVersion =
|
|
86
|
+
typeof version === "string" || typeof version === "number"
|
|
87
|
+
? String(version)
|
|
88
|
+
: "missing or invalid";
|
|
89
|
+
super(`Unsupported stealth cookie store version: ${displayedVersion}`, {
|
|
90
|
+
code: "unsupported_stealth_cookie_store_version",
|
|
91
|
+
details: { receivedVersion: version, supportedVersions: [1] },
|
|
92
|
+
});
|
|
93
|
+
this.name = "StealthCookieStoreVersionError";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
export class AuthError extends ProviderError {
|
|
83
98
|
constructor(message: string, options?: ProviderErrorOptions) {
|
|
84
99
|
super(message, options);
|