@apifuse/provider-sdk 2.2.0-beta.14 → 2.2.0-beta.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/define.js +9 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +19 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/native-egress-policy.d.ts +27 -0
- package/dist/native-egress-policy.js +225 -0
- package/dist/provider.d.ts +3 -3
- package/dist/provider.js +2 -2
- package/dist/runtime/http.js +189 -9
- package/dist/runtime/native-network.d.ts +39 -4
- package/dist/runtime/native-network.js +365 -20
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/stealth.js +16 -44
- package/dist/server/serve.js +37 -0
- package/dist/testing/run.js +32 -13
- package/dist/types.d.ts +23 -2
- package/package.json +1 -1
- package/src/define.ts +11 -0
- package/src/errors.ts +28 -0
- package/src/index.ts +6 -0
- package/src/native-egress-policy.ts +285 -0
- package/src/provider.ts +7 -0
- package/src/runtime/http.ts +217 -9
- package/src/runtime/native-network.ts +474 -22
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/stealth.ts +20 -47
- package/src/server/serve.ts +36 -0
- package/src/testing/run.ts +39 -14
- package/src/types.ts +32 -2
package/dist/testing/run.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createProviderCache } from "../runtime/cache.js";
|
|
|
3
3
|
import { createTestProviderChoiceContext } from "../runtime/choice.js";
|
|
4
4
|
import { createMemoryProviderRuntimeState } from "../runtime/state.js";
|
|
5
5
|
import { createUnsupportedSttClient } from "../runtime/stt.js";
|
|
6
|
+
import { createNativeEgressAuthorization, NativeNetworkError, snapshotNativeConnectInput, snapshotNativeGrantInput, } from "../runtime/native-network.js";
|
|
6
7
|
import { safeParseSchemaSync } from "../schema.js";
|
|
7
8
|
import { requestPathForFixture } from "../fixture-sanitization.js";
|
|
8
9
|
import { findStreamCaptureGroup, replayStreamEvidence } from "../stream-evidence.js";
|
|
@@ -183,6 +184,14 @@ function createUpstreamContext(provider, operationName, upstreamStub) {
|
|
|
183
184
|
};
|
|
184
185
|
const request = { headers: {} };
|
|
185
186
|
const state = createMemoryProviderRuntimeState();
|
|
187
|
+
const nativeEgress = provider.native
|
|
188
|
+
? createNativeEgressAuthorization({ egress: provider.native.network })
|
|
189
|
+
: undefined;
|
|
190
|
+
const requireNativeEgress = () => {
|
|
191
|
+
if (!nativeEgress)
|
|
192
|
+
throw new NativeNetworkError("Native egress authorization is unavailable", "native_egress_policy_invalid");
|
|
193
|
+
return nativeEgress;
|
|
194
|
+
};
|
|
186
195
|
const dispatch = async (call) => {
|
|
187
196
|
const canned = await upstreamStub({ operationName, ...call });
|
|
188
197
|
if (canned === undefined) {
|
|
@@ -353,19 +362,29 @@ function createUpstreamContext(provider, operationName, upstreamStub) {
|
|
|
353
362
|
? {
|
|
354
363
|
native: {
|
|
355
364
|
network: {
|
|
356
|
-
connectTcp: async (options) =>
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
365
|
+
connectTcp: async (options) => {
|
|
366
|
+
const request = snapshotNativeConnectInput(options);
|
|
367
|
+
requireNativeEgress().assertConnect(request, "disabled");
|
|
368
|
+
return createNativeConnection(await dispatch({
|
|
369
|
+
transport: "native",
|
|
370
|
+
method: "connectTcp",
|
|
371
|
+
url: `tcp://${request.host}:${request.port}`,
|
|
372
|
+
options: request,
|
|
373
|
+
}), dispatch, `tcp://${request.host}:${request.port}`);
|
|
374
|
+
},
|
|
375
|
+
connectTls: async (options) => {
|
|
376
|
+
const request = snapshotNativeConnectInput(options);
|
|
377
|
+
requireNativeEgress().assertConnect(request, "required");
|
|
378
|
+
return createNativeConnection(await dispatch({
|
|
379
|
+
transport: "native",
|
|
380
|
+
method: "connectTls",
|
|
381
|
+
url: `tls://${request.host}:${request.port}`,
|
|
382
|
+
options: request,
|
|
383
|
+
}), dispatch, `tls://${request.host}:${request.port}`);
|
|
384
|
+
},
|
|
385
|
+
grantTcpEgress: (input) => {
|
|
386
|
+
return requireNativeEgress().grant(snapshotNativeGrantInput(input));
|
|
387
|
+
},
|
|
369
388
|
},
|
|
370
389
|
},
|
|
371
390
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -859,9 +859,24 @@ export interface RequestOptions {
|
|
|
859
859
|
*/
|
|
860
860
|
throwOnHttpError?: boolean;
|
|
861
861
|
retry?: boolean | HttpRetryPreset | HttpRetryOptions;
|
|
862
|
+
/**
|
|
863
|
+
* Opt-in redirect-hop enforcement for ctx.http. When present, redirects are
|
|
864
|
+
* evaluated before the next request is issued. Existing callers that omit
|
|
865
|
+
* this policy retain the native fetch redirect behavior.
|
|
866
|
+
*/
|
|
867
|
+
redirectPolicy?: HttpRedirectPolicy;
|
|
862
868
|
}
|
|
869
|
+
export type RedirectRunReason = "completed" | "stopped" | "max_hops" | "missing_location" | "loop";
|
|
870
|
+
export type HttpRedirectPolicyMode = "same-origin";
|
|
871
|
+
export interface HttpRedirectPolicy {
|
|
872
|
+
/** Only follow redirects whose canonical scheme, host, and port match the initial URL. */
|
|
873
|
+
mode: HttpRedirectPolicyMode;
|
|
874
|
+
/** Maximum number of redirect hops that may be followed. Must be an integer from 0 to 20. */
|
|
875
|
+
maxHops: number;
|
|
876
|
+
}
|
|
877
|
+
export type HttpRedirectFailureReason = Exclude<RedirectRunReason, "completed">;
|
|
863
878
|
export type HttpMethod = "HEAD" | "head" | "GET" | "get" | "POST" | "post" | "PUT" | "put" | "DELETE" | "delete" | "OPTIONS" | "options" | "TRACE" | "trace" | "PATCH" | "patch";
|
|
864
|
-
export interface StealthFetchOptions extends RequestOptions {
|
|
879
|
+
export interface StealthFetchOptions extends Omit<RequestOptions, "redirectPolicy"> {
|
|
865
880
|
method?: HttpMethod;
|
|
866
881
|
body?: string | Buffer;
|
|
867
882
|
redirect?: "follow" | "manual" | "error";
|
|
@@ -967,7 +982,7 @@ export interface StealthRedirectRunOptions extends Omit<StealthFetchOptions, "re
|
|
|
967
982
|
export interface StealthRedirectRunResult {
|
|
968
983
|
final: StealthResponse;
|
|
969
984
|
hops: StealthRedirectHop[];
|
|
970
|
-
reason:
|
|
985
|
+
reason: RedirectRunReason;
|
|
971
986
|
/**
|
|
972
987
|
* Complete flat view across all redirect hosts. Attributes and duplicate names are lost.
|
|
973
988
|
* @deprecated Use cookieStore for lossless persistence.
|
|
@@ -1072,6 +1087,12 @@ export interface NativeTcpEgressRule {
|
|
|
1072
1087
|
/**
|
|
1073
1088
|
* Bounded native TCP egress discovered through a declared bootstrap endpoint.
|
|
1074
1089
|
* Host suffixes are exact DNS suffixes, not wildcard patterns.
|
|
1090
|
+
*
|
|
1091
|
+
* Dynamic rules are ordered. The first rule whose source, target, port, and TLS
|
|
1092
|
+
* selectors match exclusively owns the grant; its ttlMs and maxGrants bounds
|
|
1093
|
+
* apply, and an exhausted/shorter rule never falls through to a later overlap.
|
|
1094
|
+
* Every rule must declare a source host selector, source port list/range, and
|
|
1095
|
+
* target port list/range; omitted ttlMs/maxGrants remain unbounded.
|
|
1075
1096
|
*/
|
|
1076
1097
|
export interface NativeTcpDynamicEgressRule {
|
|
1077
1098
|
readonly sourceHost?: string;
|
package/package.json
CHANGED
package/src/define.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import ms from "ms";
|
|
2
2
|
|
|
3
3
|
import { ProviderError, ValidationError } from "./errors.js";
|
|
4
|
+
import {
|
|
5
|
+
NativeEgressPolicyValidationError,
|
|
6
|
+
validateNativeProviderConfig,
|
|
7
|
+
} from "./native-egress-policy.js";
|
|
4
8
|
import { safeParseSchemaSync } from "./schema.js";
|
|
5
9
|
import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
|
|
6
10
|
import type {
|
|
@@ -2204,6 +2208,13 @@ export function defineProvider<
|
|
|
2204
2208
|
);
|
|
2205
2209
|
validateOperationFixtures(config.id, operations);
|
|
2206
2210
|
validateProviderDeployment(config.id, config.deployment);
|
|
2211
|
+
try {
|
|
2212
|
+
validateNativeProviderConfig(config.native);
|
|
2213
|
+
} catch (error) {
|
|
2214
|
+
if (error instanceof NativeEgressPolicyValidationError)
|
|
2215
|
+
throw new ValidationError(error.message);
|
|
2216
|
+
throw error;
|
|
2217
|
+
}
|
|
2207
2218
|
validateProviderProxy(config);
|
|
2208
2219
|
validateProviderStt(config);
|
|
2209
2220
|
if (config.runtime === "browser" && !config.browser)
|
package/src/errors.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ProviderErrorCategory } from "./observability.js";
|
|
2
|
+
import type { HttpRedirectFailureReason } from "./types.js";
|
|
2
3
|
|
|
3
4
|
// Versioned, cross-realm brands. `Symbol.for` resolves to the same symbol in
|
|
4
5
|
// any copy/entrypoint of this SDK major version, so an error created by a
|
|
@@ -148,6 +149,33 @@ export class TransportError extends ProviderError {
|
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
export type HttpRedirectErrorOptions = TransportErrorOptions & {
|
|
153
|
+
reason: HttpRedirectFailureReason;
|
|
154
|
+
/** Redacted redirect target suitable for provider diagnostics. */
|
|
155
|
+
target?: string;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** Raised when an opt-in ctx.http redirect policy refuses or cannot resolve a hop. */
|
|
159
|
+
export class HttpRedirectError extends TransportError {
|
|
160
|
+
readonly reason: HttpRedirectFailureReason;
|
|
161
|
+
readonly target?: string;
|
|
162
|
+
|
|
163
|
+
constructor(message: string, options: HttpRedirectErrorOptions) {
|
|
164
|
+
const { reason, target, ...transportOptions } = options;
|
|
165
|
+
super(message, {
|
|
166
|
+
...transportOptions,
|
|
167
|
+
code: `http_redirect_${reason}`,
|
|
168
|
+
details: {
|
|
169
|
+
reason,
|
|
170
|
+
...(target ? { target } : {}),
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
this.name = "HttpRedirectError";
|
|
174
|
+
this.reason = reason;
|
|
175
|
+
this.target = target;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
151
179
|
// Cross-module type guards. Prefer these over `instanceof` at any boundary that
|
|
152
180
|
// may receive an error from a different copy/entrypoint of the SDK (see the HTTP
|
|
153
181
|
// server error boundary). They recognize branded errors regardless of which
|
package/src/index.ts
CHANGED
|
@@ -73,6 +73,8 @@ export { createHttpClient } from "./runtime/http.js";
|
|
|
73
73
|
export {
|
|
74
74
|
createNativeNetworkClient,
|
|
75
75
|
deriveNativeCredentialAffinityKey,
|
|
76
|
+
NativeEgressGrantExpiredError,
|
|
77
|
+
NativeEgressNotDeclaredError,
|
|
76
78
|
NativeIdleTimeoutError,
|
|
77
79
|
NativeNetworkError,
|
|
78
80
|
NativeProxyExpiredError,
|
|
@@ -189,6 +191,9 @@ export type {
|
|
|
189
191
|
HealthJourneyStep,
|
|
190
192
|
HttpClient,
|
|
191
193
|
HttpMethod,
|
|
194
|
+
HttpRedirectFailureReason,
|
|
195
|
+
HttpRedirectPolicy,
|
|
196
|
+
HttpRedirectPolicyMode,
|
|
192
197
|
HttpResponse,
|
|
193
198
|
HttpRetryOptions,
|
|
194
199
|
HttpRetrySummary,
|
|
@@ -280,6 +285,7 @@ export type {
|
|
|
280
285
|
ProviderSttMode,
|
|
281
286
|
ProviderSupportLevel,
|
|
282
287
|
RequestOptions,
|
|
288
|
+
RedirectRunReason,
|
|
283
289
|
Rfc3339Instant,
|
|
284
290
|
SchemaLike,
|
|
285
291
|
SmsOrigin,
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NativeProviderConfig,
|
|
3
|
+
NativeTcpDynamicEgressRule,
|
|
4
|
+
NativeTcpEgressRule,
|
|
5
|
+
NativeTcpPortRange,
|
|
6
|
+
NativeTcpTlsMode,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
|
|
9
|
+
type NativeNetworkDeclaration = NonNullable<NativeProviderConfig["network"]>;
|
|
10
|
+
|
|
11
|
+
const NATIVE_PROVIDER_FIELD_RECORD = {
|
|
12
|
+
network: true,
|
|
13
|
+
} satisfies { readonly [K in keyof Required<NativeProviderConfig>]: true };
|
|
14
|
+
const NATIVE_NETWORK_FIELD_RECORD = {
|
|
15
|
+
tcp: true,
|
|
16
|
+
dynamicTcp: true,
|
|
17
|
+
} satisfies { readonly [K in keyof Required<NativeNetworkDeclaration>]: true };
|
|
18
|
+
const NATIVE_TCP_RULE_FIELD_RECORD = {
|
|
19
|
+
host: true,
|
|
20
|
+
ports: true,
|
|
21
|
+
tls: true,
|
|
22
|
+
} satisfies { readonly [K in keyof Required<NativeTcpEgressRule>]: true };
|
|
23
|
+
const NATIVE_DYNAMIC_TCP_RULE_FIELD_RECORD = {
|
|
24
|
+
sourceHost: true,
|
|
25
|
+
sourceHostSuffixes: true,
|
|
26
|
+
sourcePorts: true,
|
|
27
|
+
sourcePortRanges: true,
|
|
28
|
+
targetHostSuffixes: true,
|
|
29
|
+
targetPorts: true,
|
|
30
|
+
targetPortRanges: true,
|
|
31
|
+
tls: true,
|
|
32
|
+
ttlMs: true,
|
|
33
|
+
maxGrants: true,
|
|
34
|
+
} satisfies { readonly [K in keyof Required<NativeTcpDynamicEgressRule>]: true };
|
|
35
|
+
const NATIVE_TCP_PORT_RANGE_FIELD_RECORD = {
|
|
36
|
+
start: true,
|
|
37
|
+
end: true,
|
|
38
|
+
} satisfies { readonly [K in keyof Required<NativeTcpPortRange>]: true };
|
|
39
|
+
|
|
40
|
+
const NATIVE_PROVIDER_FIELDS = Object.keys(NATIVE_PROVIDER_FIELD_RECORD);
|
|
41
|
+
const NATIVE_NETWORK_FIELDS = Object.keys(NATIVE_NETWORK_FIELD_RECORD);
|
|
42
|
+
const NATIVE_TCP_RULE_FIELDS = Object.keys(NATIVE_TCP_RULE_FIELD_RECORD);
|
|
43
|
+
const NATIVE_DYNAMIC_TCP_RULE_FIELDS = Object.keys(NATIVE_DYNAMIC_TCP_RULE_FIELD_RECORD);
|
|
44
|
+
const NATIVE_TCP_PORT_RANGE_FIELDS = Object.keys(NATIVE_TCP_PORT_RANGE_FIELD_RECORD);
|
|
45
|
+
|
|
46
|
+
export type StaticEgressRuleSnapshot = {
|
|
47
|
+
readonly host: string;
|
|
48
|
+
readonly ports: readonly number[];
|
|
49
|
+
readonly tls: NativeTcpTlsMode;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type DynamicEgressRuleSnapshot = {
|
|
53
|
+
readonly sourceHost?: string;
|
|
54
|
+
readonly sourceHostSuffixes: readonly string[];
|
|
55
|
+
readonly sourcePorts: readonly number[];
|
|
56
|
+
readonly sourcePortRanges: readonly NativeTcpPortRange[];
|
|
57
|
+
readonly targetHostSuffixes: readonly string[];
|
|
58
|
+
readonly targetPorts: readonly number[];
|
|
59
|
+
readonly targetPortRanges: readonly NativeTcpPortRange[];
|
|
60
|
+
readonly tls: NativeTcpTlsMode;
|
|
61
|
+
readonly ttlMs?: number;
|
|
62
|
+
readonly maxGrants?: number;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type NativeEgressPolicySnapshot = {
|
|
66
|
+
readonly staticRules: readonly StaticEgressRuleSnapshot[];
|
|
67
|
+
readonly dynamicRules: readonly DynamicEgressRuleSnapshot[];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export class NativeEgressPolicyValidationError extends Error {
|
|
71
|
+
constructor(message: string) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = "NativeEgressPolicyValidationError";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function fail(message: string): never {
|
|
78
|
+
throw new NativeEgressPolicyValidationError(message);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function dataRecord(
|
|
82
|
+
value: unknown,
|
|
83
|
+
fieldPath: string,
|
|
84
|
+
allowed: readonly string[],
|
|
85
|
+
): Record<string, unknown> {
|
|
86
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
87
|
+
fail(`${fieldPath} must be an object`);
|
|
88
|
+
const prototype = Reflect.getPrototypeOf(value);
|
|
89
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
90
|
+
fail(`${fieldPath} must be a plain object`);
|
|
91
|
+
const record: Record<string, unknown> = {};
|
|
92
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
93
|
+
if (typeof key !== "string") fail(`${fieldPath} must not contain symbol fields`);
|
|
94
|
+
if (!allowed.includes(key)) fail(`Unknown field ${fieldPath}.${key}`);
|
|
95
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
|
|
96
|
+
if (!descriptor || !("value" in descriptor)) fail(`${fieldPath}.${key} must be a data field`);
|
|
97
|
+
record[key] = descriptor.value;
|
|
98
|
+
}
|
|
99
|
+
return record;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function dataArray(value: unknown, fieldPath: string): readonly unknown[] {
|
|
103
|
+
if (!Array.isArray(value)) fail(`${fieldPath} must be an array`);
|
|
104
|
+
const result: unknown[] = [];
|
|
105
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
106
|
+
if (key === "length") continue;
|
|
107
|
+
if (typeof key !== "string" || !/^(?:0|[1-9]\d*)$/.test(key))
|
|
108
|
+
fail(`${fieldPath} must not contain non-index fields`);
|
|
109
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
|
|
110
|
+
if (!descriptor || !("value" in descriptor)) fail(`${fieldPath}[${key}] must be a data field`);
|
|
111
|
+
result[Number(key)] = descriptor.value;
|
|
112
|
+
}
|
|
113
|
+
if (result.length !== value.length) fail(`${fieldPath} must not be sparse`);
|
|
114
|
+
for (let index = 0; index < result.length; index += 1) {
|
|
115
|
+
if (!(index in result)) fail(`${fieldPath} must not be sparse`);
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function hasControlCharacter(value: string): boolean {
|
|
121
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
122
|
+
const code = value.charCodeAt(index);
|
|
123
|
+
if (code <= 31 || code === 127) return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function host(value: unknown, fieldPath: string, suffix = false): string {
|
|
129
|
+
if (
|
|
130
|
+
typeof value !== "string" ||
|
|
131
|
+
!value.trim() ||
|
|
132
|
+
hasControlCharacter(value) ||
|
|
133
|
+
/\s/.test(value) ||
|
|
134
|
+
value.includes("://")
|
|
135
|
+
)
|
|
136
|
+
fail(`${fieldPath} must be a non-empty hostname`);
|
|
137
|
+
if (value.includes("*"))
|
|
138
|
+
fail(`${fieldPath} must be an exact ${suffix ? "DNS suffix" : "hostname"}, not a wildcard`);
|
|
139
|
+
const normalized = value.trim().toLowerCase().replace(/\.$/, "");
|
|
140
|
+
if (!normalized) fail(`${fieldPath} must be a non-empty hostname`);
|
|
141
|
+
return normalized;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function port(value: unknown, fieldPath: string): number {
|
|
145
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > 65_535)
|
|
146
|
+
fail(`${fieldPath} must be an integer from 1 to 65535`);
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function ports(value: unknown, fieldPath: string): readonly number[] {
|
|
151
|
+
return dataArray(value, fieldPath).map((value, index) => port(value, `${fieldPath}[${index}]`));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function hostSuffixes(value: unknown, fieldPath: string): readonly string[] {
|
|
155
|
+
return dataArray(value, fieldPath).map((value, index) =>
|
|
156
|
+
host(value, `${fieldPath}[${index}]`, true),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function ranges(value: unknown, fieldPath: string): readonly NativeTcpPortRange[] {
|
|
161
|
+
return dataArray(value, fieldPath).map((value, index) => {
|
|
162
|
+
const rangePath = `${fieldPath}[${index}]`;
|
|
163
|
+
const record = dataRecord(value, rangePath, NATIVE_TCP_PORT_RANGE_FIELDS);
|
|
164
|
+
const start = port(record.start, `${rangePath}.start`);
|
|
165
|
+
const end = port(record.end, `${rangePath}.end`);
|
|
166
|
+
if (start > end) fail(`${rangePath}.start must not exceed end`);
|
|
167
|
+
return { start, end };
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function tls(value: unknown, fieldPath: string): NativeTcpTlsMode {
|
|
172
|
+
if (value !== "required" && value !== "allowed" && value !== "disabled")
|
|
173
|
+
fail(`${fieldPath} must be required, allowed, or disabled`);
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function positiveInteger(value: unknown, fieldPath: string): number {
|
|
178
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0)
|
|
179
|
+
fail(`${fieldPath} must be a positive integer`);
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function parseNativeEgressPolicy(value: unknown): NativeEgressPolicySnapshot {
|
|
184
|
+
try {
|
|
185
|
+
const policy = dataRecord(value, "native.network", NATIVE_NETWORK_FIELDS);
|
|
186
|
+
const staticRules =
|
|
187
|
+
policy.tcp === undefined
|
|
188
|
+
? []
|
|
189
|
+
: dataArray(policy.tcp, "native.network.tcp").map((value, index) => {
|
|
190
|
+
const fieldPath = `native.network.tcp[${index}]`;
|
|
191
|
+
const rule = dataRecord(value, fieldPath, NATIVE_TCP_RULE_FIELDS);
|
|
192
|
+
const declaredPorts = ports(rule.ports, `${fieldPath}.ports`);
|
|
193
|
+
if (declaredPorts.length === 0) fail(`${fieldPath}.ports must not be empty`);
|
|
194
|
+
return {
|
|
195
|
+
host: host(rule.host, `${fieldPath}.host`),
|
|
196
|
+
ports: declaredPorts,
|
|
197
|
+
tls: tls(rule.tls, `${fieldPath}.tls`),
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
const dynamicRules =
|
|
201
|
+
policy.dynamicTcp === undefined
|
|
202
|
+
? []
|
|
203
|
+
: dataArray(policy.dynamicTcp, "native.network.dynamicTcp").map((value, index) => {
|
|
204
|
+
const fieldPath = `native.network.dynamicTcp[${index}]`;
|
|
205
|
+
const rule = dataRecord(value, fieldPath, NATIVE_DYNAMIC_TCP_RULE_FIELDS);
|
|
206
|
+
const sourceHost =
|
|
207
|
+
rule.sourceHost === undefined
|
|
208
|
+
? undefined
|
|
209
|
+
: host(rule.sourceHost, `${fieldPath}.sourceHost`);
|
|
210
|
+
const sourceHostSuffixes =
|
|
211
|
+
rule.sourceHostSuffixes === undefined
|
|
212
|
+
? []
|
|
213
|
+
: hostSuffixes(rule.sourceHostSuffixes, `${fieldPath}.sourceHostSuffixes`);
|
|
214
|
+
if (sourceHost === undefined && sourceHostSuffixes.length === 0)
|
|
215
|
+
fail(
|
|
216
|
+
`${fieldPath} must declare sourceHost or a non-empty sourceHostSuffixes list`,
|
|
217
|
+
);
|
|
218
|
+
const sourcePorts =
|
|
219
|
+
rule.sourcePorts === undefined
|
|
220
|
+
? []
|
|
221
|
+
: ports(rule.sourcePorts, `${fieldPath}.sourcePorts`);
|
|
222
|
+
const sourcePortRanges =
|
|
223
|
+
rule.sourcePortRanges === undefined
|
|
224
|
+
? []
|
|
225
|
+
: ranges(rule.sourcePortRanges, `${fieldPath}.sourcePortRanges`);
|
|
226
|
+
if (sourcePorts.length === 0 && sourcePortRanges.length === 0)
|
|
227
|
+
fail(
|
|
228
|
+
`${fieldPath} must declare a non-empty sourcePorts or sourcePortRanges list`,
|
|
229
|
+
);
|
|
230
|
+
const targetHostSuffixes = hostSuffixes(
|
|
231
|
+
rule.targetHostSuffixes,
|
|
232
|
+
`${fieldPath}.targetHostSuffixes`,
|
|
233
|
+
);
|
|
234
|
+
if (targetHostSuffixes.length === 0)
|
|
235
|
+
fail(`${fieldPath}.targetHostSuffixes must not be empty`);
|
|
236
|
+
const targetPorts =
|
|
237
|
+
rule.targetPorts === undefined
|
|
238
|
+
? []
|
|
239
|
+
: ports(rule.targetPorts, `${fieldPath}.targetPorts`);
|
|
240
|
+
const targetPortRanges =
|
|
241
|
+
rule.targetPortRanges === undefined
|
|
242
|
+
? []
|
|
243
|
+
: ranges(rule.targetPortRanges, `${fieldPath}.targetPortRanges`);
|
|
244
|
+
if (targetPorts.length === 0 && targetPortRanges.length === 0)
|
|
245
|
+
fail(
|
|
246
|
+
`${fieldPath} must declare a non-empty targetPorts or targetPortRanges list`,
|
|
247
|
+
);
|
|
248
|
+
return {
|
|
249
|
+
...(sourceHost === undefined ? {} : { sourceHost }),
|
|
250
|
+
sourceHostSuffixes,
|
|
251
|
+
sourcePorts,
|
|
252
|
+
sourcePortRanges,
|
|
253
|
+
targetHostSuffixes,
|
|
254
|
+
targetPorts,
|
|
255
|
+
targetPortRanges,
|
|
256
|
+
tls: tls(rule.tls, `${fieldPath}.tls`),
|
|
257
|
+
...(rule.ttlMs === undefined
|
|
258
|
+
? {}
|
|
259
|
+
: { ttlMs: positiveInteger(rule.ttlMs, `${fieldPath}.ttlMs`) }),
|
|
260
|
+
...(rule.maxGrants === undefined
|
|
261
|
+
? {}
|
|
262
|
+
: { maxGrants: positiveInteger(rule.maxGrants, `${fieldPath}.maxGrants`) }),
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
return { staticRules, dynamicRules };
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (error instanceof NativeEgressPolicyValidationError) throw error;
|
|
268
|
+
throw new NativeEgressPolicyValidationError(
|
|
269
|
+
"Native egress policy could not be inspected safely",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function validateNativeProviderConfig(value: unknown): void {
|
|
275
|
+
if (value === undefined) return;
|
|
276
|
+
try {
|
|
277
|
+
const native = dataRecord(value, "native", NATIVE_PROVIDER_FIELDS);
|
|
278
|
+
if (native.network !== undefined) parseNativeEgressPolicy(native.network);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (error instanceof NativeEgressPolicyValidationError) throw error;
|
|
281
|
+
throw new NativeEgressPolicyValidationError(
|
|
282
|
+
"Native provider config could not be inspected safely",
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
package/src/provider.ts
CHANGED
|
@@ -37,6 +37,7 @@ export {
|
|
|
37
37
|
} from "./define.js";
|
|
38
38
|
export {
|
|
39
39
|
AuthError,
|
|
40
|
+
HttpRedirectError,
|
|
40
41
|
isProviderError,
|
|
41
42
|
isSessionExpiredError,
|
|
42
43
|
isTransportError,
|
|
@@ -91,6 +92,9 @@ export type {
|
|
|
91
92
|
HealthJourneyRunContext,
|
|
92
93
|
HealthJourneyRunResult,
|
|
93
94
|
HealthScheduleRandomization,
|
|
95
|
+
HttpRedirectFailureReason,
|
|
96
|
+
HttpRedirectPolicy,
|
|
97
|
+
HttpRedirectPolicyMode,
|
|
94
98
|
HttpRetryOptions,
|
|
95
99
|
HttpRetrySummary,
|
|
96
100
|
InferSchemaOutput,
|
|
@@ -150,6 +154,7 @@ export type {
|
|
|
150
154
|
ProviderStateDurationString,
|
|
151
155
|
ProviderStateNamespace,
|
|
152
156
|
ProviderSupportLevel,
|
|
157
|
+
RedirectRunReason,
|
|
153
158
|
SchemaLike,
|
|
154
159
|
SmsOtpMatcherDefinition,
|
|
155
160
|
StandardSchemaV1,
|
|
@@ -161,6 +166,8 @@ export type {
|
|
|
161
166
|
export {
|
|
162
167
|
createNativeNetworkClient,
|
|
163
168
|
deriveNativeCredentialAffinityKey,
|
|
169
|
+
NativeEgressGrantExpiredError,
|
|
170
|
+
NativeEgressNotDeclaredError,
|
|
164
171
|
NativeIdleTimeoutError,
|
|
165
172
|
NativeNetworkError,
|
|
166
173
|
NativeProxyExpiredError,
|