@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.
Files changed (52) hide show
  1. package/AUTHORING.md +201 -0
  2. package/CHANGELOG.md +10 -0
  3. package/README.md +26 -2
  4. package/bin/apifuse-pack-types.ts +30 -1
  5. package/bin/apifuse-record.ts +622 -57
  6. package/bin/apifuse-submit-check.ts +43 -10
  7. package/dist/define.d.ts +2 -1
  8. package/dist/define.js +61 -3
  9. package/dist/fixture-sanitization.d.ts +26 -0
  10. package/dist/fixture-sanitization.js +216 -0
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.js +1 -0
  13. package/dist/provider.d.ts +2 -1
  14. package/dist/provider.js +1 -0
  15. package/dist/runtime/http.js +86 -32
  16. package/dist/runtime/instrumentation.js +295 -9
  17. package/dist/runtime/native-network.d.ts +53 -0
  18. package/dist/runtime/native-network.js +477 -0
  19. package/dist/runtime/proxy-nodemaven.d.ts +14 -0
  20. package/dist/runtime/proxy-nodemaven.js +20 -2
  21. package/dist/runtime/request-options.d.ts +68 -1
  22. package/dist/runtime/request-options.js +548 -0
  23. package/dist/runtime/stealth.d.ts +3 -1
  24. package/dist/runtime/stealth.js +239 -39
  25. package/dist/server/index.d.ts +1 -1
  26. package/dist/server/index.js +1 -1
  27. package/dist/server/self-test-input-tokens.d.ts +2 -1
  28. package/dist/server/self-test-input-tokens.js +18 -14
  29. package/dist/stream-evidence.d.ts +74 -0
  30. package/dist/stream-evidence.js +785 -0
  31. package/dist/testing/index.d.ts +1 -1
  32. package/dist/testing/index.js +1 -1
  33. package/dist/testing/run.d.ts +32 -2
  34. package/dist/testing/run.js +451 -19
  35. package/dist/types.d.ts +162 -0
  36. package/package.json +2 -1
  37. package/src/define.ts +81 -3
  38. package/src/fixture-sanitization.ts +247 -0
  39. package/src/index.ts +37 -0
  40. package/src/provider.ts +37 -0
  41. package/src/runtime/http.ts +144 -38
  42. package/src/runtime/instrumentation.ts +424 -8
  43. package/src/runtime/native-network.ts +600 -0
  44. package/src/runtime/proxy-nodemaven.ts +37 -2
  45. package/src/runtime/request-options.ts +680 -1
  46. package/src/runtime/stealth.ts +293 -40
  47. package/src/server/index.ts +4 -1
  48. package/src/server/self-test-input-tokens.ts +29 -14
  49. package/src/stream-evidence.ts +988 -0
  50. package/src/testing/index.ts +9 -1
  51. package/src/testing/run.ts +608 -12
  52. 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.12",
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, config.operations);
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: config.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,