@apifuse/provider-sdk 2.2.0-beta.17 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.2.0-beta.18
4
+
5
+ - Release candidate for main commit cebaf4b994918d2641c0fe6681fbffd3a3284c5d.
6
+
3
7
  ## 2.2.0-beta.17
4
8
 
5
9
  - Release candidate for main commit 5a0f9127a5861992d5c144b07ad379a085756544.
@@ -0,0 +1,43 @@
1
+ export type EgressHostKind = "ipv4" | "ipv6" | "ipv4-mapped-ipv6" | "numeric-ambiguous" | "dns";
2
+ export type EgressHostCanonicalizationFailure = "not-string" | "reserved-delimiter" | "control-character" | "whitespace" | "canonicalization-empty";
3
+ export type EgressHostCanonicalizationResult = {
4
+ readonly ok: true;
5
+ readonly host: string;
6
+ } | {
7
+ readonly ok: false;
8
+ readonly reason: EgressHostCanonicalizationFailure;
9
+ };
10
+ export type Ipv6CidrOverlap = "ipv4-mapped" | "ipv4-compatible";
11
+ export type Ipv6CidrParseResult = {
12
+ readonly ok: true;
13
+ readonly network: Uint8Array;
14
+ readonly prefix: number;
15
+ } | {
16
+ readonly ok: false;
17
+ readonly reason: "malformed";
18
+ readonly overlap?: Ipv6CidrOverlap;
19
+ } | {
20
+ readonly ok: false;
21
+ readonly reason: "non-canonical-network";
22
+ };
23
+ export declare function parseStrictIpv4(value: string): number | undefined;
24
+ /** Parse the RFC 4291 IPv6 text forms accepted at both policy and runtime boundaries. */
25
+ export declare function parseIpv6(value: string): Uint8Array | undefined;
26
+ export declare function parseIpv4Cidr(value: string): {
27
+ readonly ok: true;
28
+ readonly network: number;
29
+ readonly prefix: number;
30
+ } | {
31
+ readonly ok: false;
32
+ readonly reason: "malformed" | "non-canonical-network";
33
+ };
34
+ export declare function parseIpv6Cidr(value: string): Ipv6CidrParseResult;
35
+ export declare function ipv4InCidr(address: number, cidr: string): boolean;
36
+ export declare function ipv6InCidr(address: Uint8Array, cidr: string): boolean;
37
+ export declare function embeddedIpv4FromIpv6(address: Uint8Array): number | undefined;
38
+ export declare function formatIpv6(address: Uint8Array): string;
39
+ /** The single family and ambiguity classifier used by policy and runtime matching. */
40
+ export declare function classifyEgressHost(host: string): EgressHostKind;
41
+ export declare function hasReservedEgressHostDelimiter(value: string): boolean;
42
+ export declare function hasEgressHostControlCharacter(value: string): boolean;
43
+ export declare function canonicalizeEgressHost(value: unknown): EgressHostCanonicalizationResult;
@@ -0,0 +1,281 @@
1
+ import { domainToASCII } from "node:url";
2
+ const STRICT_IPV4_PATTERN = /^(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})\.(0|[1-9]\d{0,2})$/;
3
+ const DECIMAL_COMPONENT_PATTERN = /^\d+$/;
4
+ const HEX_COMPONENT_PATTERN = /^0[xX][\da-fA-F]+$/;
5
+ const IPV6_GROUP_PATTERN = /^[\da-fA-F]{1,4}$/;
6
+ const FORMAT_CONTROL_PATTERN = /\p{Cf}/u;
7
+ const RESERVED_EGRESS_HOST_DELIMITERS = ["/", "\\", "?", "#", "@", "[", "]", " ", "%"];
8
+ const IPV4_MAPPED_PREFIX = Uint8Array.of(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff);
9
+ const IPV4_COMPATIBLE_MIN = Uint8Array.of(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2);
10
+ const IPV4_COMPATIBLE_MAX = Uint8Array.of(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff);
11
+ export function parseStrictIpv4(value) {
12
+ const match = STRICT_IPV4_PATTERN.exec(value);
13
+ if (!match || match[0] !== value)
14
+ return undefined;
15
+ const [, first, second, third, fourth] = match;
16
+ if (first === undefined || second === undefined || third === undefined || fourth === undefined)
17
+ return undefined;
18
+ const octets = [Number(first), Number(second), Number(third), Number(fourth)];
19
+ if (octets.some((octet) => octet > 255))
20
+ return undefined;
21
+ const [a, b, c, d] = octets;
22
+ if (a === undefined || b === undefined || c === undefined || d === undefined)
23
+ return undefined;
24
+ return (a * 0x1000000 + b * 0x10000 + c * 0x100 + d) >>> 0;
25
+ }
26
+ function ipv4HexGroups(value) {
27
+ const address = parseStrictIpv4(value);
28
+ if (address === undefined)
29
+ return undefined;
30
+ return [((address >>> 16) & 0xffff).toString(16), (address & 0xffff).toString(16)];
31
+ }
32
+ /** Parse the RFC 4291 IPv6 text forms accepted at both policy and runtime boundaries. */
33
+ export function parseIpv6(value) {
34
+ if (!value || value.includes("%") || value.includes("[") || value.includes("]"))
35
+ return undefined;
36
+ if (value.indexOf("::") !== value.lastIndexOf("::"))
37
+ return undefined;
38
+ let text = value;
39
+ if (text.includes(".")) {
40
+ const finalColon = text.lastIndexOf(":");
41
+ if (finalColon < 0)
42
+ return undefined;
43
+ const groups = ipv4HexGroups(text.slice(finalColon + 1));
44
+ if (!groups)
45
+ return undefined;
46
+ text = `${text.slice(0, finalColon + 1)}${groups[0]}:${groups[1]}`;
47
+ }
48
+ const compression = text.indexOf("::");
49
+ let groups;
50
+ if (compression >= 0) {
51
+ const leftText = text.slice(0, compression);
52
+ const rightText = text.slice(compression + 2);
53
+ const left = leftText ? leftText.split(":") : [];
54
+ const right = rightText ? rightText.split(":") : [];
55
+ if (left.some((group) => !IPV6_GROUP_PATTERN.test(group)) ||
56
+ right.some((group) => !IPV6_GROUP_PATTERN.test(group)) ||
57
+ left.length + right.length >= 8)
58
+ return undefined;
59
+ groups = [...left, ...Array(8 - left.length - right.length).fill("0"), ...right];
60
+ }
61
+ else {
62
+ groups = text.split(":");
63
+ if (groups.length !== 8 || groups.some((group) => !IPV6_GROUP_PATTERN.test(group)))
64
+ return undefined;
65
+ }
66
+ const bytes = new Uint8Array(16);
67
+ for (let index = 0; index < groups.length; index += 1) {
68
+ const group = groups[index];
69
+ if (group === undefined)
70
+ return undefined;
71
+ const parsed = Number.parseInt(group, 16);
72
+ bytes[index * 2] = parsed >>> 8;
73
+ bytes[index * 2 + 1] = parsed & 0xff;
74
+ }
75
+ return bytes;
76
+ }
77
+ function prefixMatches(address, network, prefix) {
78
+ const wholeBytes = Math.floor(prefix / 8);
79
+ for (let index = 0; index < wholeBytes; index += 1) {
80
+ if (address[index] !== network[index])
81
+ return false;
82
+ }
83
+ const remainingBits = prefix % 8;
84
+ if (remainingBits === 0)
85
+ return true;
86
+ const mask = (0xff << (8 - remainingBits)) & 0xff;
87
+ return ((address[wholeBytes] ?? 0) & mask) === ((network[wholeBytes] ?? 0) & mask);
88
+ }
89
+ function compareIpv6(left, right) {
90
+ for (let index = 0; index < 16; index += 1) {
91
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
92
+ if (difference !== 0)
93
+ return difference;
94
+ }
95
+ return 0;
96
+ }
97
+ function cidrMaximum(network, prefix) {
98
+ const maximum = network.slice();
99
+ for (let bit = prefix; bit < 128; bit += 1) {
100
+ const byteIndex = Math.floor(bit / 8);
101
+ maximum[byteIndex] = (maximum[byteIndex] ?? 0) | (1 << (7 - (bit % 8)));
102
+ }
103
+ return maximum;
104
+ }
105
+ function rangesOverlap(leftMin, leftMax, rightMin, rightMax) {
106
+ return compareIpv6(leftMin, rightMax) <= 0 && compareIpv6(rightMin, leftMax) <= 0;
107
+ }
108
+ function ipv6CidrOverlap(network, prefix) {
109
+ const maximum = cidrMaximum(network, prefix);
110
+ const mappedMin = new Uint8Array(16);
111
+ mappedMin.set(IPV4_MAPPED_PREFIX);
112
+ const mappedMax = mappedMin.slice();
113
+ mappedMax.fill(0xff, 12);
114
+ if (rangesOverlap(network, maximum, mappedMin, mappedMax))
115
+ return "ipv4-mapped";
116
+ if (rangesOverlap(network, maximum, IPV4_COMPATIBLE_MIN, IPV4_COMPATIBLE_MAX))
117
+ return "ipv4-compatible";
118
+ return undefined;
119
+ }
120
+ export function parseIpv4Cidr(value) {
121
+ const separator = value.indexOf("/");
122
+ if (separator <= 0 || separator !== value.lastIndexOf("/"))
123
+ return { ok: false, reason: "malformed" };
124
+ const address = parseStrictIpv4(value.slice(0, separator));
125
+ const prefixText = value.slice(separator + 1);
126
+ const prefix = Number(prefixText);
127
+ if (address === undefined ||
128
+ !Number.isInteger(prefix) ||
129
+ prefix < 0 ||
130
+ prefix > 32 ||
131
+ String(prefix) !== prefixText)
132
+ return { ok: false, reason: "malformed" };
133
+ const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
134
+ const network = (address & mask) >>> 0;
135
+ if (address !== network)
136
+ return { ok: false, reason: "non-canonical-network" };
137
+ return { ok: true, network, prefix };
138
+ }
139
+ export function parseIpv6Cidr(value) {
140
+ const separator = value.indexOf("/");
141
+ if (separator <= 0 || separator !== value.lastIndexOf("/"))
142
+ return { ok: false, reason: "malformed" };
143
+ const address = parseIpv6(value.slice(0, separator));
144
+ const prefixText = value.slice(separator + 1);
145
+ const prefix = Number(prefixText);
146
+ if (address === undefined ||
147
+ !Number.isInteger(prefix) ||
148
+ prefix < 0 ||
149
+ prefix > 128 ||
150
+ String(prefix) !== prefixText)
151
+ return { ok: false, reason: "malformed" };
152
+ const network = address.slice();
153
+ const wholeBytes = Math.floor(prefix / 8);
154
+ const remainingBits = prefix % 8;
155
+ if (remainingBits > 0) {
156
+ const mask = (0xff << (8 - remainingBits)) & 0xff;
157
+ network[wholeBytes] = (network[wholeBytes] ?? 0) & mask;
158
+ }
159
+ network.fill(0, wholeBytes + (remainingBits > 0 ? 1 : 0));
160
+ if (compareIpv6(address, network) !== 0)
161
+ return { ok: false, reason: "non-canonical-network" };
162
+ const overlap = ipv6CidrOverlap(network, prefix);
163
+ if (overlap)
164
+ return { ok: false, reason: "malformed", overlap };
165
+ return { ok: true, network, prefix };
166
+ }
167
+ export function ipv4InCidr(address, cidr) {
168
+ const parsed = parseIpv4Cidr(cidr);
169
+ if (!parsed.ok)
170
+ return false;
171
+ return (parsed.prefix === 0 ||
172
+ address >>> (32 - parsed.prefix) === parsed.network >>> (32 - parsed.prefix));
173
+ }
174
+ export function ipv6InCidr(address, cidr) {
175
+ if (address.length !== 16)
176
+ return false;
177
+ const parsed = parseIpv6Cidr(cidr);
178
+ return parsed.ok && prefixMatches(address, parsed.network, parsed.prefix);
179
+ }
180
+ export function embeddedIpv4FromIpv6(address) {
181
+ if (address.length !== 16)
182
+ return undefined;
183
+ const mapped = IPV4_MAPPED_PREFIX.every((byte, index) => address[index] === byte);
184
+ const compatible = address.slice(0, 12).every((byte) => byte === 0);
185
+ const embedded = ((address[12] ?? 0) * 0x1000000 +
186
+ (address[13] ?? 0) * 0x10000 +
187
+ (address[14] ?? 0) * 0x100 +
188
+ (address[15] ?? 0)) >>>
189
+ 0;
190
+ if (mapped || (compatible && embedded !== 0 && embedded !== 1))
191
+ return embedded;
192
+ return undefined;
193
+ }
194
+ export function formatIpv6(address) {
195
+ if (address.length !== 16)
196
+ throw new TypeError("IPv6 addresses must contain exactly 16 bytes");
197
+ const groups = Array.from({ length: 8 }, (_, index) => (((address[index * 2] ?? 0) << 8) | (address[index * 2 + 1] ?? 0)).toString(16));
198
+ let bestStart = -1;
199
+ let bestLength = 0;
200
+ for (let index = 0; index < groups.length;) {
201
+ if (groups[index] !== "0") {
202
+ index += 1;
203
+ continue;
204
+ }
205
+ let end = index + 1;
206
+ while (end < groups.length && groups[end] === "0")
207
+ end += 1;
208
+ if (end - index > bestLength && end - index >= 2) {
209
+ bestStart = index;
210
+ bestLength = end - index;
211
+ }
212
+ index = end;
213
+ }
214
+ if (bestStart < 0)
215
+ return groups.join(":");
216
+ const left = groups.slice(0, bestStart).join(":");
217
+ const right = groups.slice(bestStart + bestLength).join(":");
218
+ return `${left}::${right}`;
219
+ }
220
+ /** The single family and ambiguity classifier used by policy and runtime matching. */
221
+ export function classifyEgressHost(host) {
222
+ if (hasReservedEgressHostDelimiter(host) ||
223
+ hasEgressHostControlCharacter(host) ||
224
+ /\s/u.test(host))
225
+ return "numeric-ambiguous";
226
+ if (parseStrictIpv4(host) !== undefined)
227
+ return "ipv4";
228
+ const ipv6 = parseIpv6(host);
229
+ if (ipv6)
230
+ return embeddedIpv4FromIpv6(ipv6) === undefined ? "ipv6" : "ipv4-mapped-ipv6";
231
+ if (host.includes(":"))
232
+ return "numeric-ambiguous";
233
+ const labels = host.split(".");
234
+ if (labels.every((label) => DECIMAL_COMPONENT_PATTERN.exec(label)?.[0] === label ||
235
+ HEX_COMPONENT_PATTERN.exec(label)?.[0] === label))
236
+ return "numeric-ambiguous";
237
+ return "dns";
238
+ }
239
+ export function hasReservedEgressHostDelimiter(value) {
240
+ return RESERVED_EGRESS_HOST_DELIMITERS.some((delimiter) => value.includes(delimiter));
241
+ }
242
+ export function hasEgressHostControlCharacter(value) {
243
+ for (let index = 0; index < value.length; index += 1) {
244
+ const code = value.charCodeAt(index);
245
+ if (code <= 31 || code === 127)
246
+ return true;
247
+ }
248
+ return FORMAT_CONTROL_PATTERN.test(value);
249
+ }
250
+ export function canonicalizeEgressHost(value) {
251
+ if (typeof value !== "string")
252
+ return { ok: false, reason: "not-string" };
253
+ if (hasReservedEgressHostDelimiter(value))
254
+ return { ok: false, reason: "reserved-delimiter" };
255
+ if (hasEgressHostControlCharacter(value))
256
+ return { ok: false, reason: "control-character" };
257
+ if (/\s/u.test(value))
258
+ return { ok: false, reason: "whitespace" };
259
+ const raw = value.trim();
260
+ if (!raw)
261
+ return { ok: false, reason: "canonicalization-empty" };
262
+ const ipv6 = parseIpv6(raw);
263
+ if (ipv6)
264
+ return { ok: true, host: formatIpv6(ipv6) };
265
+ // Classify the spelling with one optional root-label dot removed before IDNA.
266
+ // This keeps resolver-numeric ASCII forms from being widened into canonical IPv4.
267
+ const numericCandidate = raw.endsWith(".") ? raw.slice(0, -1) : raw;
268
+ const normalizedNumericCandidate = numericCandidate.toLowerCase();
269
+ if (classifyEgressHost(normalizedNumericCandidate) === "numeric-ambiguous")
270
+ return { ok: true, host: normalizedNumericCandidate };
271
+ let ascii;
272
+ try {
273
+ ascii = domainToASCII(raw).toLowerCase().replace(/\.$/, "");
274
+ }
275
+ catch {
276
+ return { ok: false, reason: "canonicalization-empty" };
277
+ }
278
+ if (!ascii || /^\.+$/.test(ascii))
279
+ return { ok: false, reason: "canonicalization-empty" };
280
+ return { ok: true, host: ascii };
281
+ }
@@ -7,10 +7,13 @@ export type StaticEgressRuleSnapshot = {
7
7
  export type DynamicEgressRuleSnapshot = {
8
8
  readonly sourceHost?: string;
9
9
  readonly sourceHostSuffixes: readonly string[];
10
+ readonly sourceIpv4Cidrs: readonly string[];
11
+ readonly sourceIpv6Cidrs: readonly string[];
10
12
  readonly sourcePorts: readonly number[];
11
13
  readonly sourcePortRanges: readonly NativeTcpPortRange[];
12
14
  readonly targetHostSuffixes: readonly string[];
13
15
  readonly targetIpv4Cidrs: readonly string[];
16
+ readonly targetIpv6Cidrs: readonly string[];
14
17
  readonly targetPorts: readonly number[];
15
18
  readonly targetPortRanges: readonly NativeTcpPortRange[];
16
19
  readonly tls: NativeTcpTlsMode;
@@ -1,4 +1,4 @@
1
- import { canonicalizeEgressHost, parseIpv4Cidr } from "./native-ipv4.js";
1
+ import { canonicalizeEgressHost, classifyEgressHost, formatIpv6, parseIpv4Cidr, parseIpv6Cidr, } from "./native-address.js";
2
2
  const NATIVE_PROVIDER_FIELD_RECORD = {
3
3
  network: true,
4
4
  };
@@ -14,10 +14,13 @@ const NATIVE_TCP_RULE_FIELD_RECORD = {
14
14
  const NATIVE_DYNAMIC_TCP_RULE_FIELD_RECORD = {
15
15
  sourceHost: true,
16
16
  sourceHostSuffixes: true,
17
+ sourceIpv4Cidrs: true,
18
+ sourceIpv6Cidrs: true,
17
19
  sourcePorts: true,
18
20
  sourcePortRanges: true,
19
21
  targetHostSuffixes: true,
20
22
  targetIpv4Cidrs: true,
23
+ targetIpv6Cidrs: true,
21
24
  targetPorts: true,
22
25
  targetPortRanges: true,
23
26
  tls: true,
@@ -89,6 +92,13 @@ function host(value, fieldPath, suffix = false) {
89
92
  const canonical = canonicalizeEgressHost(value);
90
93
  if (!canonical.ok)
91
94
  fail(`${fieldPath} must be a non-empty hostname`);
95
+ const kind = classifyEgressHost(canonical.host);
96
+ if (kind === "numeric-ambiguous")
97
+ fail(`${fieldPath} value is an unresolvable numeric-ambiguous spelling`);
98
+ if (suffix && kind !== "dns") {
99
+ const family = kind === "ipv4" ? "IPv4" : "IPv6";
100
+ fail(`${fieldPath} must be a DNS suffix, not an ${family} literal`);
101
+ }
92
102
  return canonical.host;
93
103
  }
94
104
  function port(value, fieldPath) {
@@ -121,6 +131,29 @@ function ipv4Cidrs(value, fieldPath) {
121
131
  return value;
122
132
  });
123
133
  }
134
+ function ipv6Cidrs(value, fieldPath) {
135
+ const seen = new Set();
136
+ return dataArray(value, fieldPath).map((value, index) => {
137
+ const cidrPath = `${fieldPath}[${index}]`;
138
+ if (typeof value !== "string")
139
+ fail(`${cidrPath} must be an IPv6 CIDR in address/nn form`);
140
+ const parsed = parseIpv6Cidr(value);
141
+ if (!parsed.ok) {
142
+ if (parsed.reason === "non-canonical-network")
143
+ fail(`${cidrPath} must use the canonical network address with no host bits set`);
144
+ if (parsed.overlap === "ipv4-mapped")
145
+ fail(`${cidrPath} overlaps IPv4-mapped ::ffff:0:0/96 address space`);
146
+ if (parsed.overlap === "ipv4-compatible")
147
+ fail(`${cidrPath} overlaps IPv4-compatible ::/96 address space`);
148
+ fail(`${cidrPath} must be an IPv6 CIDR in address/nn form`);
149
+ }
150
+ const duplicateKey = `${formatIpv6(parsed.network)}/${parsed.prefix}`;
151
+ if (seen.has(duplicateKey))
152
+ fail(`${fieldPath} must not contain duplicate CIDRs`);
153
+ seen.add(duplicateKey);
154
+ return value;
155
+ });
156
+ }
124
157
  function ranges(value, fieldPath) {
125
158
  return dataArray(value, fieldPath).map((value, index) => {
126
159
  const rangePath = `${fieldPath}[${index}]`;
@@ -170,8 +203,17 @@ export function parseNativeEgressPolicy(value) {
170
203
  const sourceHostSuffixes = rule.sourceHostSuffixes === undefined
171
204
  ? []
172
205
  : hostSuffixes(rule.sourceHostSuffixes, `${fieldPath}.sourceHostSuffixes`);
173
- if (sourceHost === undefined && sourceHostSuffixes.length === 0)
174
- fail(`${fieldPath} must declare sourceHost or a non-empty sourceHostSuffixes list`);
206
+ const sourceIpv4Cidrs = rule.sourceIpv4Cidrs === undefined
207
+ ? []
208
+ : ipv4Cidrs(rule.sourceIpv4Cidrs, `${fieldPath}.sourceIpv4Cidrs`);
209
+ const sourceIpv6Cidrs = rule.sourceIpv6Cidrs === undefined
210
+ ? []
211
+ : ipv6Cidrs(rule.sourceIpv6Cidrs, `${fieldPath}.sourceIpv6Cidrs`);
212
+ if (sourceHost === undefined &&
213
+ sourceHostSuffixes.length === 0 &&
214
+ sourceIpv4Cidrs.length === 0 &&
215
+ sourceIpv6Cidrs.length === 0)
216
+ fail(`${fieldPath} must declare sourceHost or a non-empty sourceHostSuffixes, sourceIpv4Cidrs, or sourceIpv6Cidrs list`);
175
217
  const sourcePorts = rule.sourcePorts === undefined
176
218
  ? []
177
219
  : ports(rule.sourcePorts, `${fieldPath}.sourcePorts`);
@@ -186,8 +228,13 @@ export function parseNativeEgressPolicy(value) {
186
228
  const targetIpv4Cidrs = rule.targetIpv4Cidrs === undefined
187
229
  ? []
188
230
  : ipv4Cidrs(rule.targetIpv4Cidrs, `${fieldPath}.targetIpv4Cidrs`);
189
- if (targetHostSuffixes.length === 0 && targetIpv4Cidrs.length === 0)
190
- fail(`${fieldPath} must declare a non-empty targetHostSuffixes or targetIpv4Cidrs list`);
231
+ const targetIpv6Cidrs = rule.targetIpv6Cidrs === undefined
232
+ ? []
233
+ : ipv6Cidrs(rule.targetIpv6Cidrs, `${fieldPath}.targetIpv6Cidrs`);
234
+ if (targetHostSuffixes.length === 0 &&
235
+ targetIpv4Cidrs.length === 0 &&
236
+ targetIpv6Cidrs.length === 0)
237
+ fail(`${fieldPath} must declare a non-empty targetHostSuffixes, targetIpv4Cidrs, or targetIpv6Cidrs list`);
191
238
  const targetPorts = rule.targetPorts === undefined
192
239
  ? []
193
240
  : ports(rule.targetPorts, `${fieldPath}.targetPorts`);
@@ -199,10 +246,13 @@ export function parseNativeEgressPolicy(value) {
199
246
  return {
200
247
  ...(sourceHost === undefined ? {} : { sourceHost }),
201
248
  sourceHostSuffixes,
249
+ sourceIpv4Cidrs,
250
+ sourceIpv6Cidrs,
202
251
  sourcePorts,
203
252
  sourcePortRanges,
204
253
  targetHostSuffixes,
205
254
  targetIpv4Cidrs,
255
+ targetIpv6Cidrs,
206
256
  targetPorts,
207
257
  targetPortRanges,
208
258
  tls: tls(rule.tls, `${fieldPath}.tls`),
@@ -2,6 +2,7 @@ import { Socket } from "node:net";
2
2
  import { type TLSSocket } from "node:tls";
3
3
  import { type ProxyProtocol } from "../config/loader.js";
4
4
  import { TransportError } from "../errors.js";
5
+ import { type DynamicEgressRuleSnapshot } from "../native-egress-policy.js";
5
6
  import type { NativeNetworkClient, NativeNetworkConnectInput, NativeNetworkConnection, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProxyEgressInfo, ProviderProxyPolicy, ProviderProxyProvider, EnvContext } from "../types.js";
6
7
  export type NativeNetworkErrorCode = "native_connection_aborted" | "native_connection_closed" | "native_connection_failed" | "native_connection_idle_timeout" | "native_connection_timeout" | "native_egress_authorization_failed" | "native_egress_grant_expired" | "native_egress_grant_invalid" | "native_egress_grant_limit_exceeded" | "native_egress_input_invalid" | "native_egress_not_declared" | "native_egress_policy_invalid" | "native_dynamic_egress_unsupported" | "native_proxy_expired" | "native_proxy_invalid";
7
8
  export declare class NativeNetworkError extends TransportError {
@@ -110,6 +111,8 @@ export declare function resolveNativeGatewayProxy(input: NativeGatewayProxyResol
110
111
  export declare function createNativeNetworkConnection(socket: Socket | TLSSocket, proxy: NativeGatewayProxy | undefined, options: NativeNetworkClientOptions, idleTimeoutMs?: number): NativeNetworkConnection;
111
112
  type NativeConnectTls = "required" | "disabled";
112
113
  export declare const NATIVE_EGRESS_EXPIRED_EVIDENCE_LIMIT = 256;
114
+ /** Internal validator-independent source selector matcher. */
115
+ export declare function matchesSourceHost(rule: DynamicEgressRuleSnapshot, host: string): boolean;
113
116
  /** Internal canonical snapshot shared by production and SDK transport test doubles. */
114
117
  export declare function snapshotNativeConnectInput(input: NativeNetworkConnectInput): NativeNetworkConnectInput;
115
118
  /** Internal canonical snapshot shared by production and SDK transport test doubles. */
@@ -5,7 +5,7 @@ import { SocksClient } from "socks";
5
5
  import { assertTunnelingScheme, ProxyResolutionError, SMARTPROXY_APP_KEY_ENV, VENDOR_DEFAULT_PROTOCOL, resolveWithVendor, } from "../config/loader.js";
6
6
  import { TransportError } from "../errors.js";
7
7
  import { NativeEgressPolicyValidationError, parseNativeEgressPolicy, } from "../native-egress-policy.js";
8
- import { canonicalizeEgressHost, classifyEgressTargetHost, ipv4InCidr, parseStrictIpv4, } from "../native-ipv4.js";
8
+ import { canonicalizeEgressHost, classifyEgressHost, embeddedIpv4FromIpv6, ipv4InCidr, ipv6InCidr, parseIpv6, parseStrictIpv4, } from "../native-address.js";
9
9
  import { createEnvContext } from "./env.js";
10
10
  import { NODEMAVEN_FILTER_ENV, NODEMAVEN_PASSWORD_ENV, NODEMAVEN_USERNAME_ENV, nodemavenSessionWindow, synthesizeNodemavenProxy, } from "./proxy-nodemaven.js";
11
11
  import { redactSensitiveError } from "./request-options.js";
@@ -916,12 +916,37 @@ function invalidPolicy(message) {
916
916
  function matchesDnsSuffix(host, suffix) {
917
917
  return host === suffix || host.endsWith(`.${suffix}`);
918
918
  }
919
- function matchesSourceHost(rule, host) {
920
- const hasSelector = rule.sourceHost !== undefined || rule.sourceHostSuffixes.length > 0;
919
+ /** Internal validator-independent source selector matcher. */
920
+ export function matchesSourceHost(rule, host) {
921
+ const kind = classifyEgressHost(host);
922
+ if (kind === "numeric-ambiguous")
923
+ return false;
924
+ const hasSelector = rule.sourceHost !== undefined ||
925
+ rule.sourceHostSuffixes.length > 0 ||
926
+ rule.sourceIpv4Cidrs.length > 0 ||
927
+ rule.sourceIpv6Cidrs.length > 0;
921
928
  if (!hasSelector)
922
929
  return false;
923
- return (host === rule.sourceHost ||
924
- rule.sourceHostSuffixes.some((suffix) => matchesDnsSuffix(host, suffix)));
930
+ if (host === rule.sourceHost)
931
+ return true;
932
+ return matchesHostFamilySelectors(host, rule.sourceHostSuffixes, rule.sourceIpv4Cidrs, rule.sourceIpv6Cidrs);
933
+ }
934
+ function matchesHostFamilySelectors(host, hostSuffixes, ipv4Cidrs, ipv6Cidrs) {
935
+ const kind = classifyEgressHost(host);
936
+ if (kind === "ipv4") {
937
+ const address = parseStrictIpv4(host);
938
+ return address !== undefined && ipv4Cidrs.some((cidr) => ipv4InCidr(address, cidr));
939
+ }
940
+ if (kind === "ipv4-mapped-ipv6") {
941
+ const address = parseIpv6(host);
942
+ const embedded = address && embeddedIpv4FromIpv6(address);
943
+ return embedded !== undefined && ipv4Cidrs.some((cidr) => ipv4InCidr(embedded, cidr));
944
+ }
945
+ if (kind === "ipv6") {
946
+ const address = parseIpv6(host);
947
+ return address !== undefined && ipv6Cidrs.some((cidr) => ipv6InCidr(address, cidr));
948
+ }
949
+ return kind === "dns" && hostSuffixes.some((suffix) => matchesDnsSuffix(host, suffix));
925
950
  }
926
951
  function matchesPortSelectors(port, ports, ranges) {
927
952
  if (ports.length === 0 && ranges.length === 0)
@@ -944,13 +969,7 @@ function matchesDynamicRuleSelectors(rule, input) {
944
969
  grantTlsFitsRule(input.tls, rule.tls));
945
970
  }
946
971
  function matchesDynamicTargetHost(rule, targetHost) {
947
- const targetKind = classifyEgressTargetHost(targetHost);
948
- if (targetKind === "ipv4") {
949
- const targetIp = parseStrictIpv4(targetHost);
950
- return (targetIp !== undefined && rule.targetIpv4Cidrs.some((cidr) => ipv4InCidr(targetIp, cidr)));
951
- }
952
- return (targetKind === "dns" &&
953
- rule.targetHostSuffixes.some((suffix) => matchesDnsSuffix(targetHost, suffix)));
972
+ return matchesHostFamilySelectors(targetHost, rule.targetHostSuffixes, rule.targetIpv4Cidrs, rule.targetIpv6Cidrs);
954
973
  }
955
974
  function invalidGrant(message) {
956
975
  return new NativeNetworkError(message, "native_egress_grant_invalid");
@@ -1129,7 +1148,7 @@ export function createNativeEgressAuthorization(options) {
1129
1148
  matchesPortSelectors(input.sourcePort, rule.sourcePorts, rule.sourcePortRanges)
1130
1149
  ? [index]
1131
1150
  : []);
1132
- const targetKind = classifyEgressTargetHost(diagnosticTargetHost);
1151
+ const targetKind = classifyEgressHost(diagnosticTargetHost);
1133
1152
  let selectorDetails;
1134
1153
  if (sourceMatchingRuleIndices.length > 0) {
1135
1154
  const failedByRule = [];
package/dist/types.d.ts CHANGED
@@ -1090,9 +1090,10 @@ export interface NativeTcpEgressRule {
1090
1090
  * Bounded native TCP egress discovered through a declared bootstrap endpoint.
1091
1091
  * Host suffixes are exact DNS suffixes, not wildcard patterns.
1092
1092
  * Dynamic rules must declare at least one target host selector through
1093
- * targetHostSuffixes and/or targetIpv4Cidrs. IPv4-literal grant targets match
1094
- * only targetIpv4Cidrs, while DNS-name targets match only targetHostSuffixes.
1095
- * CIDRs are exact IPv4 networks in a.b.c.d/nn form.
1093
+ * targetHostSuffixes, targetIpv4Cidrs, and/or targetIpv6Cidrs. Literal grant
1094
+ * sources and targets match only same-family selectors (except exact
1095
+ * sourceHost); DNS names match only host/suffix selectors. IPv4-mapped and
1096
+ * IPv4-compatible IPv6 literals are authorized only by IPv4 CIDRs.
1096
1097
  *
1097
1098
  * Dynamic rules are ordered. The first rule whose source, target, port, and TLS
1098
1099
  * selectors match exclusively owns the grant; its ttlMs and maxGrants bounds
@@ -1103,10 +1104,13 @@ export interface NativeTcpEgressRule {
1103
1104
  export interface NativeTcpDynamicEgressRule {
1104
1105
  readonly sourceHost?: string;
1105
1106
  readonly sourceHostSuffixes?: readonly string[];
1107
+ readonly sourceIpv4Cidrs?: readonly string[];
1108
+ readonly sourceIpv6Cidrs?: readonly string[];
1106
1109
  readonly sourcePorts?: readonly number[];
1107
1110
  readonly sourcePortRanges?: readonly NativeTcpPortRange[];
1108
1111
  readonly targetHostSuffixes?: readonly string[];
1109
1112
  readonly targetIpv4Cidrs?: readonly string[];
1113
+ readonly targetIpv6Cidrs?: readonly string[];
1110
1114
  readonly targetPorts?: readonly number[];
1111
1115
  readonly targetPortRanges?: readonly NativeTcpPortRange[];
1112
1116
  readonly tls: NativeTcpTlsMode;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.17",
2
+ "version": "2.2.0-beta.18",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",