@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.
- package/AUTHORING.md +40 -0
- package/CHANGELOG.md +9 -0
- package/dist/config/loader.d.ts +19 -0
- package/dist/config/loader.js +59 -28
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +4 -0
- package/dist/native-egress-policy.js +86 -23
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +1 -1
- package/dist/runtime/native-network.d.ts +46 -7
- package/dist/runtime/native-network.js +590 -114
- package/dist/runtime/proxy-nodemaven.d.ts +7 -0
- package/dist/runtime/proxy-nodemaven.js +5 -5
- package/dist/server/serve.js +3 -1
- package/dist/types.d.ts +10 -1
- package/package.json +1 -1
- package/src/config/loader.ts +110 -18
- package/src/index.ts +5 -0
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +105 -32
- package/src/provider.ts +5 -0
- package/src/runtime/native-network.ts +770 -136
- package/src/runtime/proxy-nodemaven.ts +13 -5
- package/src/server/serve.ts +8 -1
- package/src/types.ts +10 -1
|
@@ -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
|
|
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
|
|
102
|
-
if (!
|
|
92
|
+
const canonical = canonicalizeEgressHost(value);
|
|
93
|
+
if (!canonical.ok)
|
|
103
94
|
fail(`${fieldPath} must be a non-empty hostname`);
|
|
104
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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 =
|
|
177
|
-
|
|
178
|
-
|
|
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`),
|
package/dist/provider.d.ts
CHANGED
|
@@ -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
|
|
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) =>
|
|
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
|
|
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. */
|