@apifuse/provider-sdk 2.2.0-beta.16 → 2.2.0-beta.18

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.
@@ -0,0 +1,340 @@
1
+ import { domainToASCII } from "node:url";
2
+
3
+ const STRICT_IPV4_PATTERN =
4
+ /^(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})$/;
5
+ const DECIMAL_COMPONENT_PATTERN = /^\d+$/;
6
+ const HEX_COMPONENT_PATTERN = /^0[xX][\da-fA-F]+$/;
7
+ const IPV6_GROUP_PATTERN = /^[\da-fA-F]{1,4}$/;
8
+ const FORMAT_CONTROL_PATTERN = /\p{Cf}/u;
9
+ const RESERVED_EGRESS_HOST_DELIMITERS = ["/", "\\", "?", "#", "@", "[", "]", " ", "%"];
10
+
11
+ const IPV4_MAPPED_PREFIX = Uint8Array.of(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff);
12
+ const IPV4_COMPATIBLE_MIN = Uint8Array.of(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2);
13
+ const IPV4_COMPATIBLE_MAX = Uint8Array.of(
14
+ 0,
15
+ 0,
16
+ 0,
17
+ 0,
18
+ 0,
19
+ 0,
20
+ 0,
21
+ 0,
22
+ 0,
23
+ 0,
24
+ 0,
25
+ 0,
26
+ 0xff,
27
+ 0xff,
28
+ 0xff,
29
+ 0xff,
30
+ );
31
+
32
+ export type EgressHostKind = "ipv4" | "ipv6" | "ipv4-mapped-ipv6" | "numeric-ambiguous" | "dns";
33
+
34
+ export type EgressHostCanonicalizationFailure =
35
+ | "not-string"
36
+ | "reserved-delimiter"
37
+ | "control-character"
38
+ | "whitespace"
39
+ | "canonicalization-empty";
40
+
41
+ export type EgressHostCanonicalizationResult =
42
+ | { readonly ok: true; readonly host: string }
43
+ | { readonly ok: false; readonly reason: EgressHostCanonicalizationFailure };
44
+
45
+ export type Ipv6CidrOverlap = "ipv4-mapped" | "ipv4-compatible";
46
+
47
+ export type Ipv6CidrParseResult =
48
+ | { readonly ok: true; readonly network: Uint8Array; readonly prefix: number }
49
+ | {
50
+ readonly ok: false;
51
+ readonly reason: "malformed";
52
+ readonly overlap?: Ipv6CidrOverlap;
53
+ }
54
+ | { readonly ok: false; readonly reason: "non-canonical-network" };
55
+
56
+ export function parseStrictIpv4(value: string): number | undefined {
57
+ const match = STRICT_IPV4_PATTERN.exec(value);
58
+ if (!match || match[0] !== value) return undefined;
59
+ const [, first, second, third, fourth] = match;
60
+ if (first === undefined || second === undefined || third === undefined || fourth === undefined)
61
+ return undefined;
62
+ const octets = [Number(first), Number(second), Number(third), Number(fourth)];
63
+ if (octets.some((octet) => octet > 255)) return undefined;
64
+ const [a, b, c, d] = octets;
65
+ if (a === undefined || b === undefined || c === undefined || d === undefined) return undefined;
66
+ return (a * 0x1000000 + b * 0x10000 + c * 0x100 + d) >>> 0;
67
+ }
68
+
69
+ function ipv4HexGroups(value: string): readonly [string, string] | undefined {
70
+ const address = parseStrictIpv4(value);
71
+ if (address === undefined) return undefined;
72
+ return [((address >>> 16) & 0xffff).toString(16), (address & 0xffff).toString(16)];
73
+ }
74
+
75
+ /** Parse the RFC 4291 IPv6 text forms accepted at both policy and runtime boundaries. */
76
+ export function parseIpv6(value: string): Uint8Array | undefined {
77
+ if (!value || value.includes("%") || value.includes("[") || value.includes("]")) return undefined;
78
+ if (value.indexOf("::") !== value.lastIndexOf("::")) return undefined;
79
+
80
+ let text = value;
81
+ if (text.includes(".")) {
82
+ const finalColon = text.lastIndexOf(":");
83
+ if (finalColon < 0) return undefined;
84
+ const groups = ipv4HexGroups(text.slice(finalColon + 1));
85
+ if (!groups) return undefined;
86
+ text = `${text.slice(0, finalColon + 1)}${groups[0]}:${groups[1]}`;
87
+ }
88
+
89
+ const compression = text.indexOf("::");
90
+ let groups: string[];
91
+ if (compression >= 0) {
92
+ const leftText = text.slice(0, compression);
93
+ const rightText = text.slice(compression + 2);
94
+ const left = leftText ? leftText.split(":") : [];
95
+ const right = rightText ? rightText.split(":") : [];
96
+ if (
97
+ left.some((group) => !IPV6_GROUP_PATTERN.test(group)) ||
98
+ right.some((group) => !IPV6_GROUP_PATTERN.test(group)) ||
99
+ left.length + right.length >= 8
100
+ )
101
+ return undefined;
102
+ groups = [...left, ...Array<string>(8 - left.length - right.length).fill("0"), ...right];
103
+ } else {
104
+ groups = text.split(":");
105
+ if (groups.length !== 8 || groups.some((group) => !IPV6_GROUP_PATTERN.test(group)))
106
+ return undefined;
107
+ }
108
+
109
+ const bytes = new Uint8Array(16);
110
+ for (let index = 0; index < groups.length; index += 1) {
111
+ const group = groups[index];
112
+ if (group === undefined) return undefined;
113
+ const parsed = Number.parseInt(group, 16);
114
+ bytes[index * 2] = parsed >>> 8;
115
+ bytes[index * 2 + 1] = parsed & 0xff;
116
+ }
117
+ return bytes;
118
+ }
119
+
120
+ function prefixMatches(address: Uint8Array, network: Uint8Array, prefix: number): boolean {
121
+ const wholeBytes = Math.floor(prefix / 8);
122
+ for (let index = 0; index < wholeBytes; index += 1) {
123
+ if (address[index] !== network[index]) return false;
124
+ }
125
+ const remainingBits = prefix % 8;
126
+ if (remainingBits === 0) return true;
127
+ const mask = (0xff << (8 - remainingBits)) & 0xff;
128
+ return ((address[wholeBytes] ?? 0) & mask) === ((network[wholeBytes] ?? 0) & mask);
129
+ }
130
+
131
+ function compareIpv6(left: Uint8Array, right: Uint8Array): number {
132
+ for (let index = 0; index < 16; index += 1) {
133
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
134
+ if (difference !== 0) return difference;
135
+ }
136
+ return 0;
137
+ }
138
+
139
+ function cidrMaximum(network: Uint8Array, prefix: number): Uint8Array {
140
+ const maximum = network.slice();
141
+ for (let bit = prefix; bit < 128; bit += 1) {
142
+ const byteIndex = Math.floor(bit / 8);
143
+ maximum[byteIndex] = (maximum[byteIndex] ?? 0) | (1 << (7 - (bit % 8)));
144
+ }
145
+ return maximum;
146
+ }
147
+
148
+ function rangesOverlap(
149
+ leftMin: Uint8Array,
150
+ leftMax: Uint8Array,
151
+ rightMin: Uint8Array,
152
+ rightMax: Uint8Array,
153
+ ): boolean {
154
+ return compareIpv6(leftMin, rightMax) <= 0 && compareIpv6(rightMin, leftMax) <= 0;
155
+ }
156
+
157
+ function ipv6CidrOverlap(network: Uint8Array, prefix: number): Ipv6CidrOverlap | undefined {
158
+ const maximum = cidrMaximum(network, prefix);
159
+ const mappedMin = new Uint8Array(16);
160
+ mappedMin.set(IPV4_MAPPED_PREFIX);
161
+ const mappedMax = mappedMin.slice();
162
+ mappedMax.fill(0xff, 12);
163
+ if (rangesOverlap(network, maximum, mappedMin, mappedMax)) return "ipv4-mapped";
164
+ if (rangesOverlap(network, maximum, IPV4_COMPATIBLE_MIN, IPV4_COMPATIBLE_MAX))
165
+ return "ipv4-compatible";
166
+ return undefined;
167
+ }
168
+
169
+ export function parseIpv4Cidr(
170
+ value: string,
171
+ ):
172
+ | { readonly ok: true; readonly network: number; readonly prefix: number }
173
+ | { readonly ok: false; readonly reason: "malformed" | "non-canonical-network" } {
174
+ const separator = value.indexOf("/");
175
+ if (separator <= 0 || separator !== value.lastIndexOf("/"))
176
+ return { ok: false, reason: "malformed" };
177
+ const address = parseStrictIpv4(value.slice(0, separator));
178
+ const prefixText = value.slice(separator + 1);
179
+ const prefix = Number(prefixText);
180
+ if (
181
+ address === undefined ||
182
+ !Number.isInteger(prefix) ||
183
+ prefix < 0 ||
184
+ prefix > 32 ||
185
+ String(prefix) !== prefixText
186
+ )
187
+ return { ok: false, reason: "malformed" };
188
+ const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
189
+ const network = (address & mask) >>> 0;
190
+ if (address !== network) return { ok: false, reason: "non-canonical-network" };
191
+ return { ok: true, network, prefix };
192
+ }
193
+
194
+ export function parseIpv6Cidr(value: string): Ipv6CidrParseResult {
195
+ const separator = value.indexOf("/");
196
+ if (separator <= 0 || separator !== value.lastIndexOf("/"))
197
+ return { ok: false, reason: "malformed" };
198
+ const address = parseIpv6(value.slice(0, separator));
199
+ const prefixText = value.slice(separator + 1);
200
+ const prefix = Number(prefixText);
201
+ if (
202
+ address === undefined ||
203
+ !Number.isInteger(prefix) ||
204
+ prefix < 0 ||
205
+ prefix > 128 ||
206
+ String(prefix) !== prefixText
207
+ )
208
+ return { ok: false, reason: "malformed" };
209
+ const network = address.slice();
210
+ const wholeBytes = Math.floor(prefix / 8);
211
+ const remainingBits = prefix % 8;
212
+ if (remainingBits > 0) {
213
+ const mask = (0xff << (8 - remainingBits)) & 0xff;
214
+ network[wholeBytes] = (network[wholeBytes] ?? 0) & mask;
215
+ }
216
+ network.fill(0, wholeBytes + (remainingBits > 0 ? 1 : 0));
217
+ if (compareIpv6(address, network) !== 0) return { ok: false, reason: "non-canonical-network" };
218
+ const overlap = ipv6CidrOverlap(network, prefix);
219
+ if (overlap) return { ok: false, reason: "malformed", overlap };
220
+ return { ok: true, network, prefix };
221
+ }
222
+
223
+ export function ipv4InCidr(address: number, cidr: string): boolean {
224
+ const parsed = parseIpv4Cidr(cidr);
225
+ if (!parsed.ok) return false;
226
+ return (
227
+ parsed.prefix === 0 ||
228
+ address >>> (32 - parsed.prefix) === parsed.network >>> (32 - parsed.prefix)
229
+ );
230
+ }
231
+
232
+ export function ipv6InCidr(address: Uint8Array, cidr: string): boolean {
233
+ if (address.length !== 16) return false;
234
+ const parsed = parseIpv6Cidr(cidr);
235
+ return parsed.ok && prefixMatches(address, parsed.network, parsed.prefix);
236
+ }
237
+
238
+ export function embeddedIpv4FromIpv6(address: Uint8Array): number | undefined {
239
+ if (address.length !== 16) return undefined;
240
+ const mapped = IPV4_MAPPED_PREFIX.every((byte, index) => address[index] === byte);
241
+ const compatible = address.slice(0, 12).every((byte) => byte === 0);
242
+ const embedded =
243
+ ((address[12] ?? 0) * 0x1000000 +
244
+ (address[13] ?? 0) * 0x10000 +
245
+ (address[14] ?? 0) * 0x100 +
246
+ (address[15] ?? 0)) >>>
247
+ 0;
248
+ if (mapped || (compatible && embedded !== 0 && embedded !== 1)) return embedded;
249
+ return undefined;
250
+ }
251
+
252
+ export function formatIpv6(address: Uint8Array): string {
253
+ if (address.length !== 16) throw new TypeError("IPv6 addresses must contain exactly 16 bytes");
254
+ const groups = Array.from({ length: 8 }, (_, index) =>
255
+ (((address[index * 2] ?? 0) << 8) | (address[index * 2 + 1] ?? 0)).toString(16),
256
+ );
257
+ let bestStart = -1;
258
+ let bestLength = 0;
259
+ for (let index = 0; index < groups.length; ) {
260
+ if (groups[index] !== "0") {
261
+ index += 1;
262
+ continue;
263
+ }
264
+ let end = index + 1;
265
+ while (end < groups.length && groups[end] === "0") end += 1;
266
+ if (end - index > bestLength && end - index >= 2) {
267
+ bestStart = index;
268
+ bestLength = end - index;
269
+ }
270
+ index = end;
271
+ }
272
+ if (bestStart < 0) return groups.join(":");
273
+ const left = groups.slice(0, bestStart).join(":");
274
+ const right = groups.slice(bestStart + bestLength).join(":");
275
+ return `${left}::${right}`;
276
+ }
277
+
278
+ /** The single family and ambiguity classifier used by policy and runtime matching. */
279
+ export function classifyEgressHost(host: string): EgressHostKind {
280
+ if (
281
+ hasReservedEgressHostDelimiter(host) ||
282
+ hasEgressHostControlCharacter(host) ||
283
+ /\s/u.test(host)
284
+ )
285
+ return "numeric-ambiguous";
286
+ if (parseStrictIpv4(host) !== undefined) return "ipv4";
287
+ const ipv6 = parseIpv6(host);
288
+ if (ipv6) return embeddedIpv4FromIpv6(ipv6) === undefined ? "ipv6" : "ipv4-mapped-ipv6";
289
+ if (host.includes(":")) return "numeric-ambiguous";
290
+ const labels = host.split(".");
291
+ if (
292
+ labels.every(
293
+ (label) =>
294
+ DECIMAL_COMPONENT_PATTERN.exec(label)?.[0] === label ||
295
+ HEX_COMPONENT_PATTERN.exec(label)?.[0] === label,
296
+ )
297
+ )
298
+ return "numeric-ambiguous";
299
+ return "dns";
300
+ }
301
+
302
+ export function hasReservedEgressHostDelimiter(value: string): boolean {
303
+ return RESERVED_EGRESS_HOST_DELIMITERS.some((delimiter) => value.includes(delimiter));
304
+ }
305
+
306
+ export function hasEgressHostControlCharacter(value: string): boolean {
307
+ for (let index = 0; index < value.length; index += 1) {
308
+ const code = value.charCodeAt(index);
309
+ if (code <= 31 || code === 127) return true;
310
+ }
311
+ return FORMAT_CONTROL_PATTERN.test(value);
312
+ }
313
+
314
+ export function canonicalizeEgressHost(value: unknown): EgressHostCanonicalizationResult {
315
+ if (typeof value !== "string") return { ok: false, reason: "not-string" };
316
+ if (hasReservedEgressHostDelimiter(value)) return { ok: false, reason: "reserved-delimiter" };
317
+ if (hasEgressHostControlCharacter(value)) return { ok: false, reason: "control-character" };
318
+ if (/\s/u.test(value)) return { ok: false, reason: "whitespace" };
319
+ const raw = value.trim();
320
+ if (!raw) return { ok: false, reason: "canonicalization-empty" };
321
+
322
+ const ipv6 = parseIpv6(raw);
323
+ if (ipv6) return { ok: true, host: formatIpv6(ipv6) };
324
+
325
+ // Classify the spelling with one optional root-label dot removed before IDNA.
326
+ // This keeps resolver-numeric ASCII forms from being widened into canonical IPv4.
327
+ const numericCandidate = raw.endsWith(".") ? raw.slice(0, -1) : raw;
328
+ const normalizedNumericCandidate = numericCandidate.toLowerCase();
329
+ if (classifyEgressHost(normalizedNumericCandidate) === "numeric-ambiguous")
330
+ return { ok: true, host: normalizedNumericCandidate };
331
+
332
+ let ascii: string;
333
+ try {
334
+ ascii = domainToASCII(raw).toLowerCase().replace(/\.$/, "");
335
+ } catch {
336
+ return { ok: false, reason: "canonicalization-empty" };
337
+ }
338
+ if (!ascii || /^\.+$/.test(ascii)) return { ok: false, reason: "canonicalization-empty" };
339
+ return { ok: true, host: ascii };
340
+ }
@@ -5,6 +5,13 @@ import type {
5
5
  NativeTcpPortRange,
6
6
  NativeTcpTlsMode,
7
7
  } from "./types.js";
8
+ import {
9
+ canonicalizeEgressHost,
10
+ classifyEgressHost,
11
+ formatIpv6,
12
+ parseIpv4Cidr,
13
+ parseIpv6Cidr,
14
+ } from "./native-address.js";
8
15
 
9
16
  type NativeNetworkDeclaration = NonNullable<NativeProviderConfig["network"]>;
10
17
 
@@ -23,9 +30,13 @@ const NATIVE_TCP_RULE_FIELD_RECORD = {
23
30
  const NATIVE_DYNAMIC_TCP_RULE_FIELD_RECORD = {
24
31
  sourceHost: true,
25
32
  sourceHostSuffixes: true,
33
+ sourceIpv4Cidrs: true,
34
+ sourceIpv6Cidrs: true,
26
35
  sourcePorts: true,
27
36
  sourcePortRanges: true,
28
37
  targetHostSuffixes: true,
38
+ targetIpv4Cidrs: true,
39
+ targetIpv6Cidrs: true,
29
40
  targetPorts: true,
30
41
  targetPortRanges: true,
31
42
  tls: true,
@@ -52,9 +63,13 @@ export type StaticEgressRuleSnapshot = {
52
63
  export type DynamicEgressRuleSnapshot = {
53
64
  readonly sourceHost?: string;
54
65
  readonly sourceHostSuffixes: readonly string[];
66
+ readonly sourceIpv4Cidrs: readonly string[];
67
+ readonly sourceIpv6Cidrs: readonly string[];
55
68
  readonly sourcePorts: readonly number[];
56
69
  readonly sourcePortRanges: readonly NativeTcpPortRange[];
57
70
  readonly targetHostSuffixes: readonly string[];
71
+ readonly targetIpv4Cidrs: readonly string[];
72
+ readonly targetIpv6Cidrs: readonly string[];
58
73
  readonly targetPorts: readonly number[];
59
74
  readonly targetPortRanges: readonly NativeTcpPortRange[];
60
75
  readonly tls: NativeTcpTlsMode;
@@ -117,28 +132,19 @@ function dataArray(value: unknown, fieldPath: string): readonly unknown[] {
117
132
  return result;
118
133
  }
119
134
 
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
135
  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("*"))
136
+ if (typeof value === "string" && value.includes("*"))
138
137
  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;
138
+ const canonical = canonicalizeEgressHost(value);
139
+ if (!canonical.ok) fail(`${fieldPath} must be a non-empty hostname`);
140
+ const kind = classifyEgressHost(canonical.host);
141
+ if (kind === "numeric-ambiguous")
142
+ fail(`${fieldPath} value is an unresolvable numeric-ambiguous spelling`);
143
+ if (suffix && kind !== "dns") {
144
+ const family = kind === "ipv4" ? "IPv4" : "IPv6";
145
+ fail(`${fieldPath} must be a DNS suffix, not an ${family} literal`);
146
+ }
147
+ return canonical.host;
142
148
  }
143
149
 
144
150
  function port(value: unknown, fieldPath: string): number {
@@ -157,6 +163,46 @@ function hostSuffixes(value: unknown, fieldPath: string): readonly string[] {
157
163
  );
158
164
  }
159
165
 
166
+ function ipv4Cidrs(value: unknown, fieldPath: string): readonly string[] {
167
+ const seen = new Set<string>();
168
+ return dataArray(value, fieldPath).map((value, index) => {
169
+ const cidrPath = `${fieldPath}[${index}]`;
170
+ if (typeof value !== "string") fail(`${cidrPath} must be an IPv4 CIDR in a.b.c.d/nn form`);
171
+ const parsed = parseIpv4Cidr(value);
172
+ if (!parsed.ok) {
173
+ if (parsed.reason === "non-canonical-network")
174
+ fail(`${cidrPath} must use the canonical network address with no host bits set`);
175
+ fail(`${cidrPath} must be an IPv4 CIDR in a.b.c.d/nn form`);
176
+ }
177
+ const duplicateKey = `${parsed.network}/${parsed.prefix}`;
178
+ if (seen.has(duplicateKey)) fail(`${fieldPath} must not contain duplicate CIDRs`);
179
+ seen.add(duplicateKey);
180
+ return value;
181
+ });
182
+ }
183
+
184
+ function ipv6Cidrs(value: unknown, fieldPath: string): readonly string[] {
185
+ const seen = new Set<string>();
186
+ return dataArray(value, fieldPath).map((value, index) => {
187
+ const cidrPath = `${fieldPath}[${index}]`;
188
+ if (typeof value !== "string") fail(`${cidrPath} must be an IPv6 CIDR in address/nn form`);
189
+ const parsed = parseIpv6Cidr(value);
190
+ if (!parsed.ok) {
191
+ if (parsed.reason === "non-canonical-network")
192
+ fail(`${cidrPath} must use the canonical network address with no host bits set`);
193
+ if (parsed.overlap === "ipv4-mapped")
194
+ fail(`${cidrPath} overlaps IPv4-mapped ::ffff:0:0/96 address space`);
195
+ if (parsed.overlap === "ipv4-compatible")
196
+ fail(`${cidrPath} overlaps IPv4-compatible ::/96 address space`);
197
+ fail(`${cidrPath} must be an IPv6 CIDR in address/nn form`);
198
+ }
199
+ const duplicateKey = `${formatIpv6(parsed.network)}/${parsed.prefix}`;
200
+ if (seen.has(duplicateKey)) fail(`${fieldPath} must not contain duplicate CIDRs`);
201
+ seen.add(duplicateKey);
202
+ return value;
203
+ });
204
+ }
205
+
160
206
  function ranges(value: unknown, fieldPath: string): readonly NativeTcpPortRange[] {
161
207
  return dataArray(value, fieldPath).map((value, index) => {
162
208
  const rangePath = `${fieldPath}[${index}]`;
@@ -211,9 +257,22 @@ export function parseNativeEgressPolicy(value: unknown): NativeEgressPolicySnaps
211
257
  rule.sourceHostSuffixes === undefined
212
258
  ? []
213
259
  : hostSuffixes(rule.sourceHostSuffixes, `${fieldPath}.sourceHostSuffixes`);
214
- if (sourceHost === undefined && sourceHostSuffixes.length === 0)
260
+ const sourceIpv4Cidrs =
261
+ rule.sourceIpv4Cidrs === undefined
262
+ ? []
263
+ : ipv4Cidrs(rule.sourceIpv4Cidrs, `${fieldPath}.sourceIpv4Cidrs`);
264
+ const sourceIpv6Cidrs =
265
+ rule.sourceIpv6Cidrs === undefined
266
+ ? []
267
+ : ipv6Cidrs(rule.sourceIpv6Cidrs, `${fieldPath}.sourceIpv6Cidrs`);
268
+ if (
269
+ sourceHost === undefined &&
270
+ sourceHostSuffixes.length === 0 &&
271
+ sourceIpv4Cidrs.length === 0 &&
272
+ sourceIpv6Cidrs.length === 0
273
+ )
215
274
  fail(
216
- `${fieldPath} must declare sourceHost or a non-empty sourceHostSuffixes list`,
275
+ `${fieldPath} must declare sourceHost or a non-empty sourceHostSuffixes, sourceIpv4Cidrs, or sourceIpv6Cidrs list`,
217
276
  );
218
277
  const sourcePorts =
219
278
  rule.sourcePorts === undefined
@@ -224,15 +283,27 @@ export function parseNativeEgressPolicy(value: unknown): NativeEgressPolicySnaps
224
283
  ? []
225
284
  : ranges(rule.sourcePortRanges, `${fieldPath}.sourcePortRanges`);
226
285
  if (sourcePorts.length === 0 && sourcePortRanges.length === 0)
286
+ fail(`${fieldPath} must declare a non-empty sourcePorts or sourcePortRanges list`);
287
+ const targetHostSuffixes =
288
+ rule.targetHostSuffixes === undefined
289
+ ? []
290
+ : hostSuffixes(rule.targetHostSuffixes, `${fieldPath}.targetHostSuffixes`);
291
+ const targetIpv4Cidrs =
292
+ rule.targetIpv4Cidrs === undefined
293
+ ? []
294
+ : ipv4Cidrs(rule.targetIpv4Cidrs, `${fieldPath}.targetIpv4Cidrs`);
295
+ const targetIpv6Cidrs =
296
+ rule.targetIpv6Cidrs === undefined
297
+ ? []
298
+ : ipv6Cidrs(rule.targetIpv6Cidrs, `${fieldPath}.targetIpv6Cidrs`);
299
+ if (
300
+ targetHostSuffixes.length === 0 &&
301
+ targetIpv4Cidrs.length === 0 &&
302
+ targetIpv6Cidrs.length === 0
303
+ )
227
304
  fail(
228
- `${fieldPath} must declare a non-empty sourcePorts or sourcePortRanges list`,
305
+ `${fieldPath} must declare a non-empty targetHostSuffixes, targetIpv4Cidrs, or targetIpv6Cidrs list`,
229
306
  );
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
307
  const targetPorts =
237
308
  rule.targetPorts === undefined
238
309
  ? []
@@ -242,15 +313,17 @@ export function parseNativeEgressPolicy(value: unknown): NativeEgressPolicySnaps
242
313
  ? []
243
314
  : ranges(rule.targetPortRanges, `${fieldPath}.targetPortRanges`);
244
315
  if (targetPorts.length === 0 && targetPortRanges.length === 0)
245
- fail(
246
- `${fieldPath} must declare a non-empty targetPorts or targetPortRanges list`,
247
- );
316
+ fail(`${fieldPath} must declare a non-empty targetPorts or targetPortRanges list`);
248
317
  return {
249
318
  ...(sourceHost === undefined ? {} : { sourceHost }),
250
319
  sourceHostSuffixes,
320
+ sourceIpv4Cidrs,
321
+ sourceIpv6Cidrs,
251
322
  sourcePorts,
252
323
  sourcePortRanges,
253
324
  targetHostSuffixes,
325
+ targetIpv4Cidrs,
326
+ targetIpv6Cidrs,
254
327
  targetPorts,
255
328
  targetPortRanges,
256
329
  tls: tls(rule.tls, `${fieldPath}.tls`),
package/src/provider.ts CHANGED
@@ -166,6 +166,7 @@ export type {
166
166
  } from "./types.js";
167
167
  export {
168
168
  createNativeNetworkClient,
169
+ createEnvVendorCredentialResolver,
169
170
  deriveNativeCredentialAffinityKey,
170
171
  NativeEgressGrantExpiredError,
171
172
  NativeEgressNotDeclaredError,
@@ -175,10 +176,14 @@ export {
175
176
  resolveNativeGatewayProxy,
176
177
  type NativeGatewayProxy,
177
178
  type NativeGatewayProxyResolutionInput,
179
+ type NativeGatewayProxySkipReason,
178
180
  type NativeGatewayProxySynthesizer,
181
+ type NativeGatewayProxySynthesisResult,
179
182
  type NativeGatewayProxySynthesisInput,
180
183
  type NativeNetworkClientOptions,
181
184
  type NativeNetworkErrorCode,
185
+ type VendorCredentialLookup,
186
+ type VendorCredentialResolver,
182
187
  } from "./runtime/native-network.js";
183
188
  export {
184
189
  HttpRetryAfterPolicy,