@apifuse/provider-sdk 2.2.0-beta.12 → 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 +201 -0
- package/CHANGELOG.md +10 -0
- package/README.md +26 -2
- package/bin/apifuse-pack-types.ts +30 -1
- package/bin/apifuse-record.ts +622 -57
- package/bin/apifuse-submit-check.ts +43 -10
- package/dist/define.d.ts +2 -1
- package/dist/define.js +61 -3
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- 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 +239 -39
- 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 +162 -0
- package/package.json +2 -1
- package/src/define.ts +81 -3
- package/src/fixture-sanitization.ts +247 -0
- package/src/index.ts +37 -0
- 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 +293 -40
- 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 +194 -0
package/dist/types.d.ts
CHANGED
|
@@ -699,6 +699,12 @@ export interface ProviderProxyPolicy {
|
|
|
699
699
|
affinity?: ProviderProxySessionAffinity;
|
|
700
700
|
lifetimeMinutes?: number;
|
|
701
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;
|
|
702
708
|
};
|
|
703
709
|
}
|
|
704
710
|
export type ProviderProxyConfig = boolean | ProviderProxyPolicy;
|
|
@@ -840,6 +846,11 @@ export interface HttpRetrySummary {
|
|
|
840
846
|
export interface RequestOptions {
|
|
841
847
|
headers?: Record<string, string>;
|
|
842
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>;
|
|
843
854
|
proxy?: string;
|
|
844
855
|
timeout?: number;
|
|
845
856
|
/**
|
|
@@ -854,6 +865,12 @@ export interface StealthFetchOptions extends RequestOptions {
|
|
|
854
865
|
method?: HttpMethod;
|
|
855
866
|
body?: string | Buffer;
|
|
856
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;
|
|
857
874
|
/**
|
|
858
875
|
* Offsets policy-managed proxy pool selection for caller-managed retries.
|
|
859
876
|
* Use when a request receives an upstream challenge page rather than a
|
|
@@ -1020,6 +1037,139 @@ export interface HttpClient {
|
|
|
1020
1037
|
stream(url: string, options?: RequestWithMethodOptions): Promise<HttpStreamResponse>;
|
|
1021
1038
|
sse(url: string, options?: RequestWithMethodOptions): Promise<AsyncIterable<SseMessage>>;
|
|
1022
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
|
+
}
|
|
1023
1173
|
export interface ProviderCacheKeyOptions {
|
|
1024
1174
|
/**
|
|
1025
1175
|
* Additional field names to omit from stable key material. The SDK always
|
|
@@ -1351,6 +1501,8 @@ export interface FlowContext {
|
|
|
1351
1501
|
tenantId: string;
|
|
1352
1502
|
providerId: string;
|
|
1353
1503
|
http: HttpClient;
|
|
1504
|
+
/** Present when the selected runtime supplies native network capabilities. */
|
|
1505
|
+
readonly native?: NativeProviderContext;
|
|
1354
1506
|
stealth: StealthClient;
|
|
1355
1507
|
env: EnvContext;
|
|
1356
1508
|
credential?: CredentialContext;
|
|
@@ -1436,6 +1588,10 @@ export interface ProviderContext {
|
|
|
1436
1588
|
credential: CredentialContext;
|
|
1437
1589
|
request?: ProviderRequestContext;
|
|
1438
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;
|
|
1439
1595
|
cache: ProviderCache;
|
|
1440
1596
|
state: ProviderRuntimeState;
|
|
1441
1597
|
stealth: StealthClient;
|
|
@@ -1512,6 +1668,11 @@ export interface OperationDefinition<TInput extends SchemaLike = SchemaLike, TOu
|
|
|
1512
1668
|
fixtures?: {
|
|
1513
1669
|
request: InferSchemaOutput<TInput>;
|
|
1514
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;
|
|
1515
1676
|
};
|
|
1516
1677
|
upstream?: {
|
|
1517
1678
|
baseUrl?: string;
|
|
@@ -1570,6 +1731,7 @@ export interface ProviderDefinition {
|
|
|
1570
1731
|
*/
|
|
1571
1732
|
deployment?: ProviderDeploymentOverrides;
|
|
1572
1733
|
allowedHosts?: string[];
|
|
1734
|
+
native?: NativeProviderConfig;
|
|
1573
1735
|
stealth?: {
|
|
1574
1736
|
profile: string;
|
|
1575
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,7 @@
|
|
|
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",
|
|
117
118
|
"tough-cookie": "^6.0.2",
|
|
118
119
|
"zod": "^4.4.3"
|
|
119
120
|
},
|
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,
|
|
@@ -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
|
@@ -70,6 +70,20 @@ export {
|
|
|
70
70
|
export { createEnvContext } from "./runtime/env.js";
|
|
71
71
|
export { executeOperation } from "./runtime/executor.js";
|
|
72
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";
|
|
73
87
|
export type { Insight, InsightSeverity } from "./runtime/insights.js";
|
|
74
88
|
export { generateInsights } from "./runtime/insights.js";
|
|
75
89
|
export {
|
|
@@ -179,6 +193,26 @@ export type {
|
|
|
179
193
|
Iso3166Alpha2CountryCode,
|
|
180
194
|
Iso4217CurrencyCode,
|
|
181
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,
|
|
182
216
|
OperationAnnotations,
|
|
183
217
|
OperationApprovalPolicy,
|
|
184
218
|
OperationContractMetadata,
|
|
@@ -213,6 +247,8 @@ export type {
|
|
|
213
247
|
ProviderContext,
|
|
214
248
|
ProviderDefinition,
|
|
215
249
|
ProviderDeploymentOverrides,
|
|
250
|
+
ProviderFileRef,
|
|
251
|
+
ProviderFilesContext,
|
|
216
252
|
ProviderHealthMonitorConfig,
|
|
217
253
|
ProviderHealthProbeConfig,
|
|
218
254
|
ProviderLocale,
|
|
@@ -229,6 +265,7 @@ export type {
|
|
|
229
265
|
ProviderPublicConnectionMode,
|
|
230
266
|
ProviderPublicProfile,
|
|
231
267
|
ProviderReviewed,
|
|
268
|
+
ProviderResolvedFile,
|
|
232
269
|
ProviderRuntimeState,
|
|
233
270
|
ProviderSecretDeclaration,
|
|
234
271
|
ProviderStateDurationString,
|