@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,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,9 +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[];
15
+ readonly targetIpv4Cidrs: readonly string[];
16
+ readonly targetIpv6Cidrs: readonly string[];
13
17
  readonly targetPorts: readonly number[];
14
18
  readonly targetPortRanges: readonly NativeTcpPortRange[];
15
19
  readonly tls: NativeTcpTlsMode;
@@ -1,3 +1,4 @@
1
+ import { canonicalizeEgressHost, classifyEgressHost, formatIpv6, parseIpv4Cidr, parseIpv6Cidr, } from "./native-address.js";
1
2
  const NATIVE_PROVIDER_FIELD_RECORD = {
2
3
  network: true,
3
4
  };
@@ -13,9 +14,13 @@ const NATIVE_TCP_RULE_FIELD_RECORD = {
13
14
  const NATIVE_DYNAMIC_TCP_RULE_FIELD_RECORD = {
14
15
  sourceHost: true,
15
16
  sourceHostSuffixes: true,
17
+ sourceIpv4Cidrs: true,
18
+ sourceIpv6Cidrs: true,
16
19
  sourcePorts: true,
17
20
  sourcePortRanges: true,
18
21
  targetHostSuffixes: true,
22
+ targetIpv4Cidrs: true,
23
+ targetIpv6Cidrs: true,
19
24
  targetPorts: true,
20
25
  targetPortRanges: true,
21
26
  tls: true,
@@ -81,27 +86,20 @@ function dataArray(value, fieldPath) {
81
86
  }
82
87
  return result;
83
88
  }
84
- function hasControlCharacter(value) {
85
- for (let index = 0; index < value.length; index += 1) {
86
- const code = value.charCodeAt(index);
87
- if (code <= 31 || code === 127)
88
- return true;
89
- }
90
- return false;
91
- }
92
89
  function host(value, fieldPath, suffix = false) {
93
- if (typeof value !== "string" ||
94
- !value.trim() ||
95
- hasControlCharacter(value) ||
96
- /\s/.test(value) ||
97
- value.includes("://"))
98
- fail(`${fieldPath} must be a non-empty hostname`);
99
- if (value.includes("*"))
90
+ if (typeof value === "string" && value.includes("*"))
100
91
  fail(`${fieldPath} must be an exact ${suffix ? "DNS suffix" : "hostname"}, not a wildcard`);
101
- const normalized = value.trim().toLowerCase().replace(/\.$/, "");
102
- if (!normalized)
92
+ const canonical = canonicalizeEgressHost(value);
93
+ if (!canonical.ok)
103
94
  fail(`${fieldPath} must be a non-empty hostname`);
104
- return normalized;
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
+ }
102
+ return canonical.host;
105
103
  }
106
104
  function port(value, fieldPath) {
107
105
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > 65_535)
@@ -114,6 +112,48 @@ function ports(value, fieldPath) {
114
112
  function hostSuffixes(value, fieldPath) {
115
113
  return dataArray(value, fieldPath).map((value, index) => host(value, `${fieldPath}[${index}]`, true));
116
114
  }
115
+ function ipv4Cidrs(value, fieldPath) {
116
+ const seen = new Set();
117
+ return dataArray(value, fieldPath).map((value, index) => {
118
+ const cidrPath = `${fieldPath}[${index}]`;
119
+ if (typeof value !== "string")
120
+ fail(`${cidrPath} must be an IPv4 CIDR in a.b.c.d/nn form`);
121
+ const parsed = parseIpv4Cidr(value);
122
+ if (!parsed.ok) {
123
+ if (parsed.reason === "non-canonical-network")
124
+ fail(`${cidrPath} must use the canonical network address with no host bits set`);
125
+ fail(`${cidrPath} must be an IPv4 CIDR in a.b.c.d/nn form`);
126
+ }
127
+ const duplicateKey = `${parsed.network}/${parsed.prefix}`;
128
+ if (seen.has(duplicateKey))
129
+ fail(`${fieldPath} must not contain duplicate CIDRs`);
130
+ seen.add(duplicateKey);
131
+ return value;
132
+ });
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
+ }
117
157
  function ranges(value, fieldPath) {
118
158
  return dataArray(value, fieldPath).map((value, index) => {
119
159
  const rangePath = `${fieldPath}[${index}]`;
@@ -163,8 +203,17 @@ export function parseNativeEgressPolicy(value) {
163
203
  const sourceHostSuffixes = rule.sourceHostSuffixes === undefined
164
204
  ? []
165
205
  : hostSuffixes(rule.sourceHostSuffixes, `${fieldPath}.sourceHostSuffixes`);
166
- if (sourceHost === undefined && sourceHostSuffixes.length === 0)
167
- 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`);
168
217
  const sourcePorts = rule.sourcePorts === undefined
169
218
  ? []
170
219
  : ports(rule.sourcePorts, `${fieldPath}.sourcePorts`);
@@ -173,9 +222,19 @@ export function parseNativeEgressPolicy(value) {
173
222
  : ranges(rule.sourcePortRanges, `${fieldPath}.sourcePortRanges`);
174
223
  if (sourcePorts.length === 0 && sourcePortRanges.length === 0)
175
224
  fail(`${fieldPath} must declare a non-empty sourcePorts or sourcePortRanges list`);
176
- const targetHostSuffixes = hostSuffixes(rule.targetHostSuffixes, `${fieldPath}.targetHostSuffixes`);
177
- if (targetHostSuffixes.length === 0)
178
- fail(`${fieldPath}.targetHostSuffixes must not be empty`);
225
+ const targetHostSuffixes = rule.targetHostSuffixes === undefined
226
+ ? []
227
+ : hostSuffixes(rule.targetHostSuffixes, `${fieldPath}.targetHostSuffixes`);
228
+ const targetIpv4Cidrs = rule.targetIpv4Cidrs === undefined
229
+ ? []
230
+ : ipv4Cidrs(rule.targetIpv4Cidrs, `${fieldPath}.targetIpv4Cidrs`);
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`);
179
238
  const targetPorts = rule.targetPorts === undefined
180
239
  ? []
181
240
  : ports(rule.targetPorts, `${fieldPath}.targetPorts`);
@@ -187,9 +246,13 @@ export function parseNativeEgressPolicy(value) {
187
246
  return {
188
247
  ...(sourceHost === undefined ? {} : { sourceHost }),
189
248
  sourceHostSuffixes,
249
+ sourceIpv4Cidrs,
250
+ sourceIpv6Cidrs,
190
251
  sourcePorts,
191
252
  sourcePortRanges,
192
253
  targetHostSuffixes,
254
+ targetIpv4Cidrs,
255
+ targetIpv6Cidrs,
193
256
  targetPorts,
194
257
  targetPortRanges,
195
258
  tls: tls(rule.tls, `${fieldPath}.tls`),
@@ -8,5 +8,5 @@ export { getProviderLocalePath, providerLocaleKey, qualifyProviderLocaleKey, } f
8
8
  export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
9
9
  export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, collectSensitivePaths, describeKey, field, fields, isSensitiveSchema, redactPayload, type SensitiveFieldKind, type SensitiveFieldOptions, type SensitivePath, sensitive, z, } from "./schema.js";
10
10
  export type { AuthAbortData, AuthAbortRetry, AuthFlowTerminalContext, AuthMode, AuthSafeData, AuthSafeJson, FlowContext, HealthCheckAssertionContext, HealthCheckCase, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthScheduleRandomization, HttpRedirectFailureReason, HttpRedirectPolicy, HttpRedirectPolicyMode, HttpRetryOptions, HttpRetrySummary, InferSchemaOutput, NativeContext, NativeNetworkClient, NativeNetworkCloseReason, NativeNetworkConnection, NativeNetworkConnectInput, NativeNetworkConnectOptions, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProviderContext, NativeProxyDrainHandler, NativeProxyEgressInfo, NativeProxyExpiringEvent, NativeProxyExpiringReason, NativeTcpDynamicEgressRule, NativeTcpEgressGrant, NativeTcpEgressRule, NativeTcpPortRange, NativeTcpTlsMode, NativeTlsConnectOptions, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDocMeta, OperationErrorCode, ProviderErrorStatus, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, ProviderAccessVisibility, ProviderChoiceBindingOptions, ProviderChoiceContext, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderFileRef, ProviderFilesContext, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderProxyPolicy, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderResolvedFile, ProviderRuntimeState, ProviderStateDurationString, ProviderStateNamespace, ProviderSupportLevel, RedirectRunReason, SchemaLike, SmsOtpMatcherDefinition, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateValue, StateWriteOptions, } from "./types.js";
11
- export { createNativeNetworkClient, deriveNativeCredentialAffinityKey, NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, resolveNativeGatewayProxy, type NativeGatewayProxy, type NativeGatewayProxyResolutionInput, type NativeGatewayProxySynthesizer, type NativeGatewayProxySynthesisInput, type NativeNetworkClientOptions, type NativeNetworkErrorCode, } from "./runtime/native-network.js";
11
+ export { createNativeNetworkClient, createEnvVendorCredentialResolver, deriveNativeCredentialAffinityKey, NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, resolveNativeGatewayProxy, type NativeGatewayProxy, type NativeGatewayProxyResolutionInput, type NativeGatewayProxySkipReason, type NativeGatewayProxySynthesizer, type NativeGatewayProxySynthesisResult, type NativeGatewayProxySynthesisInput, type NativeNetworkClientOptions, type NativeNetworkErrorCode, type VendorCredentialLookup, type VendorCredentialResolver, } from "./runtime/native-network.js";
12
12
  export { HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, } from "./types.js";
package/dist/provider.js CHANGED
@@ -6,5 +6,5 @@ export { AuthError, HttpRedirectError, isProviderError, isSessionExpiredError, i
6
6
  export { getProviderLocalePath, providerLocaleKey, qualifyProviderLocaleKey, } from "./i18n/index.js";
7
7
  export { createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
8
8
  export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, collectSensitivePaths, describeKey, field, fields, isSensitiveSchema, redactPayload, sensitive, z, } from "./schema.js";
9
- export { createNativeNetworkClient, deriveNativeCredentialAffinityKey, NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, resolveNativeGatewayProxy, } from "./runtime/native-network.js";
9
+ export { createNativeNetworkClient, createEnvVendorCredentialResolver, deriveNativeCredentialAffinityKey, NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, resolveNativeGatewayProxy, } from "./runtime/native-network.js";
10
10
  export { HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, } from "./types.js";
@@ -1,10 +1,12 @@
1
1
  import { Socket } from "node:net";
2
2
  import { type TLSSocket } from "node:tls";
3
+ import { type ProxyProtocol } from "../config/loader.js";
3
4
  import { TransportError } from "../errors.js";
4
- import type { NativeNetworkClient, NativeNetworkConnection, NativeNetworkConnectInput, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProxyEgressInfo, ProviderProxyPolicy, ProviderProxyProvider } from "../types.js";
5
+ import { type DynamicEgressRuleSnapshot } from "../native-egress-policy.js";
6
+ import type { NativeNetworkClient, NativeNetworkConnectInput, NativeNetworkConnection, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProxyEgressInfo, ProviderProxyPolicy, ProviderProxyProvider, EnvContext } from "../types.js";
5
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";
6
8
  export declare class NativeNetworkError extends TransportError {
7
- constructor(message: string, code: NativeNetworkErrorCode);
9
+ constructor(message: string, code: NativeNetworkErrorCode, cause?: Error);
8
10
  get code(): NativeNetworkErrorCode;
9
11
  }
10
12
  export declare class NativeProxyExpiredError extends NativeNetworkError {
@@ -13,9 +15,9 @@ export declare class NativeProxyExpiredError extends NativeNetworkError {
13
15
  }
14
16
  /** Raised before transport setup when a native destination is not authorized. */
15
17
  export declare class NativeEgressNotDeclaredError extends NativeNetworkError {
16
- readonly host: string;
17
18
  readonly port: number;
18
19
  readonly tls: "required" | "disabled";
20
+ readonly host: string;
19
21
  constructor(host: string, port: number, tls: "required" | "disabled");
20
22
  }
21
23
  /**
@@ -23,10 +25,10 @@ export declare class NativeEgressNotDeclaredError extends NativeNetworkError {
23
25
  * its expiry remains in the client's bounded recent-expiry evidence window.
24
26
  */
25
27
  export declare class NativeEgressGrantExpiredError extends NativeNetworkError {
26
- readonly host: string;
27
28
  readonly port: number;
28
29
  readonly tls: "required" | "disabled";
29
30
  readonly expiresAt: string;
31
+ readonly host: string;
30
32
  constructor(host: string, port: number, tls: "required" | "disabled", expiresAt: string);
31
33
  }
32
34
  /** Raised when an established connection exceeds its opt-in read-idle window. */
@@ -41,13 +43,42 @@ export type NativeGatewayProxySynthesisInput = {
41
43
  readonly policy: ProviderProxyPolicy;
42
44
  readonly affinityKey?: string;
43
45
  readonly now: number;
46
+ readonly protocol: ProxyProtocol;
47
+ readonly credentials: VendorCredentialResolver;
48
+ };
49
+ export type VendorCredentialLookup = {
50
+ readonly kind: "present";
51
+ readonly values: Readonly<Record<string, string>>;
52
+ } | {
53
+ readonly kind: "absent";
54
+ readonly missing: readonly string[];
55
+ };
56
+ export type VendorCredentialResolver = (vendor: ProviderProxyProvider) => VendorCredentialLookup;
57
+ export type NativeGatewayProxySkipReason = {
58
+ readonly kind: "credentials_absent";
59
+ readonly missing: readonly string[];
60
+ } | {
61
+ readonly kind: "protocol_unsupported";
62
+ readonly protocol: string;
63
+ } | {
64
+ readonly kind: "allocation_failed";
65
+ readonly cause: Error;
66
+ } | {
67
+ readonly kind: "credential_lookup_failed";
68
+ readonly cause: Error;
44
69
  };
70
+ export type NativeGatewayProxySynthesisResult = NativeGatewayProxy | {
71
+ readonly kind: "skipped";
72
+ readonly reason: NativeGatewayProxySkipReason;
73
+ } | undefined;
45
74
  /** A vendor adapter in the ordered native gateway resolution chain. */
46
- export type NativeGatewayProxySynthesizer = (input: NativeGatewayProxySynthesisInput) => NativeGatewayProxy | undefined;
75
+ export type NativeGatewayProxySynthesizer = (input: NativeGatewayProxySynthesisInput) => NativeGatewayProxySynthesisResult | Promise<NativeGatewayProxySynthesisResult>;
47
76
  export type NativeGatewayProxyResolutionInput = {
48
77
  readonly policy: ProviderProxyPolicy;
49
78
  readonly affinityKey?: string;
50
79
  readonly now?: number;
80
+ readonly protocol?: ProxyProtocol;
81
+ readonly credentials?: VendorCredentialResolver;
51
82
  readonly gatewaySynthesizers?: readonly NativeGatewayProxySynthesizer[];
52
83
  };
53
84
  export type NativeNetworkClientOptions = {
@@ -55,6 +86,10 @@ export type NativeNetworkClientOptions = {
55
86
  readonly affinityKey?: string;
56
87
  /** Stable credential/account identity; hashed before vendor synthesis. */
57
88
  readonly credentialIdentity?: string;
89
+ /** Vendor credential lookup; defaults to the process EnvContext. */
90
+ readonly credentials?: VendorCredentialResolver;
91
+ /** Explicit CONNECT/SOCKS5 override; vendors otherwise choose their default. */
92
+ readonly proxyProtocol?: ProxyProtocol;
58
93
  /** Vendor adapters in priority order within each policy vendor slot. */
59
94
  readonly gatewaySynthesizers?: readonly NativeGatewayProxySynthesizer[];
60
95
  /** Warning-level lifecycle diagnostic sink. */
@@ -67,13 +102,17 @@ export type NativeNetworkClientOptions = {
67
102
  /** Additional deployment authorization layered on top of SDK enforcement. */
68
103
  readonly grantTcpEgress?: (input: NativeNetworkDynamicGrantOptions) => NativeNetworkEgressGrant;
69
104
  };
105
+ /** Build a resolver over the SDK's existing injectable environment context. */
106
+ export declare function createEnvVendorCredentialResolver(env?: EnvContext): VendorCredentialResolver;
70
107
  /** Domain-separated, process-independent affinity derived from credential identity. */
71
108
  export declare function deriveNativeCredentialAffinityKey(credentialIdentity: string): string;
72
- /** Resolve the first configured native gateway without invoking an allocator API. */
73
- export declare function resolveNativeGatewayProxy(input: NativeGatewayProxyResolutionInput): NativeGatewayProxy | undefined;
109
+ /** Resolve the first configured native gateway, including allocation vendors. */
110
+ export declare function resolveNativeGatewayProxy(input: NativeGatewayProxyResolutionInput): Promise<NativeGatewayProxy | undefined>;
74
111
  export declare function createNativeNetworkConnection(socket: Socket | TLSSocket, proxy: NativeGatewayProxy | undefined, options: NativeNetworkClientOptions, idleTimeoutMs?: number): NativeNetworkConnection;
75
112
  type NativeConnectTls = "required" | "disabled";
76
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;
77
116
  /** Internal canonical snapshot shared by production and SDK transport test doubles. */
78
117
  export declare function snapshotNativeConnectInput(input: NativeNetworkConnectInput): NativeNetworkConnectInput;
79
118
  /** Internal canonical snapshot shared by production and SDK transport test doubles. */