@stratusagent/egress 0.6.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stratus Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @stratusagent/egress
2
+
3
+ Which addresses a tool may reach, enforced on the connection rather than on
4
+ the name it was asked for.
5
+
6
+ Used by [`@stratusagent/tool-web`](../tool-web) and
7
+ [`@stratusagent/tool-browser`](../tool-browser). It is one module because two
8
+ copies of this rule would not drift into a style difference — the stale one
9
+ would be an SSRF hole — and both packs are tested against `HOSTILE_URLS`, the
10
+ table exported from here, which is what fails if it is ever forked.
11
+
12
+ ## The rule
13
+
14
+ **Every globally routable address, and nothing else**, in both families. A
15
+ blacklist that stops at RFC 1918 still leaves:
16
+
17
+ - `169.254.169.254` — cloud instance metadata, and the credentials it serves
18
+ - `127.0.0.1` and `::1` — the daemon's own control API, among other things
19
+ - `fc00::/7` and `fe80::/10` — the same internal services over IPv6
20
+ - `::ffff:169.254.169.254`, `64:ff9b::a9fe:a9fe`, `2002:a9fe:a9fe::` — the
21
+ same IPv4 addresses again, written the ways an IPv4 check does not see.
22
+ The well-known NAT64 prefix is judged on the IPv4 it carries, so an
23
+ IPv6-only network still reaches the public internet through it; RFC 8215's
24
+ **local-use** prefix `64:ff9b:1::/48` is refused whole, because the
25
+ translator chooses the embedding length and there is no offset to read the
26
+ address from
27
+
28
+ Schemes are `http:` and `https:`. `file:`, `data:`, and `javascript:` are
29
+ refused before anything is fetched: the process doing the fetching runs as the
30
+ daemon user, and address validation cannot protect a filesystem.
31
+
32
+ ## Validation binds to the connection
33
+
34
+ Resolving a name to check it and then letting something else resolve it again
35
+ to connect is DNS rebinding with extra steps — the attacker's server answers
36
+ `93.184.216.34` for the check and `169.254.169.254` for the connection.
37
+
38
+ ```ts
39
+ import { createPinnedLookup, requestThroughPolicy, createEgressProxy } from '@stratusagent/egress';
40
+ ```
41
+
42
+ - `createPinnedLookup(policy)` is a `lookup` for Node's HTTP stack, so the
43
+ addresses the socket uses are the addresses this checked. There is no second
44
+ resolution to lose to.
45
+ - `requestThroughPolicy(url, …)` is one exchange over that lookup, with a byte
46
+ cap. It deliberately does **not** follow redirects: each hop is a new request
47
+ to a new host, so the caller loops and each hop faces the policy again.
48
+ Both of those pass `agent: false` alongside the lookup. Node's global agent
49
+ pools sockets by host and port and knows nothing about `lookup`, so a
50
+ connection opened under a permissive policy is one a stricter policy can be
51
+ handed — and a reused socket resolves nothing, so the pinned lookup that
52
+ would have refused it is never called.
53
+
54
+ - `createEgressProxy(policy)` is a loopback proxy for a browser, which resolves
55
+ names for itself whatever an interception handler decides. It resolves,
56
+ checks, and dials the checked address. `chromiumProxyOptions` /
57
+ `chromiumProxyArgs` include `<-loopback>` in the bypass list, without which
58
+ Chromium bypasses the proxy for exactly the addresses it exists to refuse.
59
+
60
+ ## Widening it
61
+
62
+ | Setting | What it does |
63
+ | --- | --- |
64
+ | `allowedHosts` | Exempts specific hosts, by name or literal address. The narrow override: one internal service an agent is meant to reach. |
65
+ | `allowPrivateAddresses` | Reaches any non-global address. The trusted-workstation posture — it turns the protection off rather than adjusting it. |
66
+ | `allowedSchemes` | Replaces the scheme allowlist. There is no good reason to add `file:`. |
67
+
68
+ Neither is a substitute for network-level egress rules in a VM or hosted
69
+ profile ([08](../../docs/roadmap/08-deployment-profiles.md)); this is the
70
+ in-process half.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Which addresses a tool may reach.
3
+ *
4
+ * The rule is "every globally routable address, and nothing else" rather
5
+ * than a blacklist of the ranges people remember. A blacklist that stops at
6
+ * RFC 1918 leaves cloud metadata at `169.254.169.254`, the gateway on
7
+ * `127.0.0.1`, and — on any dual-stack network — the same services again
8
+ * over IPv6 unique-local addresses and IPv4-mapped forms. Enumerating what
9
+ * is *allowed* fails closed on the range nobody thought of.
10
+ */
11
+ export interface AddressVerdict {
12
+ allowed: boolean;
13
+ /** One line naming what was refused, for the error the agent sees. */
14
+ reason?: string;
15
+ }
16
+ export interface EgressPolicy {
17
+ /**
18
+ * Schemes a tool may request. `http:`/`https:` by default — `file:`,
19
+ * `data:`, and the rest are rejected before anything is fetched, because
20
+ * a browser or fetcher runs as the daemon user and address validation
21
+ * cannot protect the filesystem.
22
+ */
23
+ allowedSchemes?: string[];
24
+ /**
25
+ * Reach addresses that are not globally routable. Off by default; on, it
26
+ * is the trusted-workstation posture — an agent that is meant to talk to
27
+ * a local development server. It turns off the SSRF protection, which is
28
+ * why it is one switch an operator flips rather than something a call can
29
+ * ask for.
30
+ */
31
+ allowPrivateAddresses?: boolean;
32
+ /**
33
+ * Hosts exempt from the address check, by hostname or literal address —
34
+ * `localhost`, `127.0.0.1`, `dev.internal`. Narrower than the switch
35
+ * above and the one to reach for: an agent that needs one internal
36
+ * service should not thereby be able to read the metadata endpoint.
37
+ */
38
+ allowedHosts?: string[];
39
+ }
40
+ /**
41
+ * Classify one literal address, in either family.
42
+ *
43
+ * Called on the address a connection is actually about to use, never on
44
+ * the one a name resolved to a moment earlier somewhere else — that gap is
45
+ * DNS rebinding, and it is the difference between a check and a ceremony.
46
+ */
47
+ export declare const classifyAddress: (address: string) => AddressVerdict;
48
+ /** Whether one resolved address may be connected to for this host. */
49
+ export declare const checkAddress: (policy: EgressPolicy, host: string, address: string) => AddressVerdict;
50
+ /** A URL this policy refuses, and why. */
51
+ export declare class EgressPolicyError extends Error {
52
+ constructor(message: string);
53
+ }
54
+ /**
55
+ * The check that can be made before anything is fetched: the scheme, and
56
+ * the address when the URL names one literally.
57
+ *
58
+ * A hostname cannot be judged here — that is what the connection-time
59
+ * check is for — so this returning without throwing is not permission to
60
+ * connect. It is permission to *try*, and it is where `file:///…` dies.
61
+ */
62
+ export declare const assertRequestAllowed: (rawUrl: string, policy?: EgressPolicy) => URL;
63
+ //# sourceMappingURL=addresses.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"addresses.d.ts","sourceRoot":"","sources":["../src/addresses.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAiKD;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,KAAG,cAUjD,CAAC;AAOF,sEAAsE;AACtE,eAAO,MAAM,YAAY,GAAI,QAAQ,YAAY,EAAE,MAAM,MAAM,EAAE,SAAS,MAAM,KAAG,cAKlF,CAAC;AAEF,0CAA0C;AAC1C,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,MAAM,EAAE,SAAQ,YAAiB,KAAG,GAyBhF,CAAC"}
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Which addresses a tool may reach.
3
+ *
4
+ * The rule is "every globally routable address, and nothing else" rather
5
+ * than a blacklist of the ranges people remember. A blacklist that stops at
6
+ * RFC 1918 leaves cloud metadata at `169.254.169.254`, the gateway on
7
+ * `127.0.0.1`, and — on any dual-stack network — the same services again
8
+ * over IPv6 unique-local addresses and IPv4-mapped forms. Enumerating what
9
+ * is *allowed* fails closed on the range nobody thought of.
10
+ */
11
+ const DEFAULT_SCHEMES = ['http:', 'https:'];
12
+ const parseIPv4 = (value) => {
13
+ const parts = value.split('.');
14
+ if (parts.length !== 4) {
15
+ return undefined;
16
+ }
17
+ const octets = [];
18
+ for (const part of parts) {
19
+ if (!/^\d{1,3}$/.test(part)) {
20
+ return undefined;
21
+ }
22
+ const octet = Number(part);
23
+ if (octet > 255) {
24
+ return undefined;
25
+ }
26
+ octets.push(octet);
27
+ }
28
+ return octets;
29
+ };
30
+ /** Every IPv4 range that is not globally routable, with the name of each. */
31
+ const classifyIPv4 = (octets) => {
32
+ const [a = 0, b = 0, c = 0, d = 0] = octets;
33
+ const address = octets.join('.');
34
+ const refuse = (what) => ({
35
+ allowed: false,
36
+ reason: `${address} is ${what}, which is not a public address`,
37
+ });
38
+ if (a === 0)
39
+ return refuse('the unspecified range (0.0.0.0/8)');
40
+ if (a === 10)
41
+ return refuse('a private address (10.0.0.0/8)');
42
+ if (a === 100 && b >= 64 && b <= 127)
43
+ return refuse('a carrier-NAT address (100.64.0.0/10)');
44
+ if (a === 127)
45
+ return refuse('loopback (127.0.0.0/8)');
46
+ if (a === 169 && b === 254) {
47
+ // The one that matters most in a hosted deployment: this is where every
48
+ // major cloud serves instance credentials.
49
+ return refuse('link-local (169.254.0.0/16) — cloud instance metadata lives here');
50
+ }
51
+ if (a === 172 && b >= 16 && b <= 31)
52
+ return refuse('a private address (172.16.0.0/12)');
53
+ if (a === 192 && b === 0 && c === 0)
54
+ return refuse('IETF protocol assignment space (192.0.0.0/24)');
55
+ if (a === 192 && b === 0 && c === 2)
56
+ return refuse('documentation space (192.0.2.0/24)');
57
+ if (a === 192 && b === 88 && c === 99)
58
+ return refuse('6to4 relay anycast (192.88.99.0/24)');
59
+ if (a === 192 && b === 168)
60
+ return refuse('a private address (192.168.0.0/16)');
61
+ if (a === 198 && (b === 18 || b === 19))
62
+ return refuse('benchmarking space (198.18.0.0/15)');
63
+ if (a === 198 && b === 51 && c === 100)
64
+ return refuse('documentation space (198.51.100.0/24)');
65
+ if (a === 203 && b === 0 && c === 113)
66
+ return refuse('documentation space (203.0.113.0/24)');
67
+ if (a >= 224 && a <= 239)
68
+ return refuse('multicast (224.0.0.0/4)');
69
+ if (a >= 240)
70
+ return refuse(d === 255 && a === 255 ? 'broadcast' : 'reserved space (240.0.0.0/4)');
71
+ return { allowed: true };
72
+ };
73
+ const expandIPv6 = (value) => {
74
+ const zoneless = value.split('%')[0] ?? value;
75
+ const [head, tail, ...extra] = zoneless.split('::');
76
+ if (extra.length > 0) {
77
+ return undefined;
78
+ }
79
+ const parseGroups = (part) => {
80
+ if (!part) {
81
+ return [];
82
+ }
83
+ const groups = [];
84
+ for (const piece of part.split(':')) {
85
+ if (piece.length === 0) {
86
+ return undefined;
87
+ }
88
+ if (piece.includes('.')) {
89
+ // A trailing dotted quad: `::ffff:169.254.169.254` and friends.
90
+ const octets = parseIPv4(piece);
91
+ if (!octets) {
92
+ return undefined;
93
+ }
94
+ groups.push(((octets[0] ?? 0) << 8) | (octets[1] ?? 0), ((octets[2] ?? 0) << 8) | (octets[3] ?? 0));
95
+ continue;
96
+ }
97
+ if (!/^[0-9a-fA-F]{1,4}$/.test(piece)) {
98
+ return undefined;
99
+ }
100
+ groups.push(Number.parseInt(piece, 16));
101
+ }
102
+ return groups;
103
+ };
104
+ const left = parseGroups(head);
105
+ const right = tail === undefined ? [] : parseGroups(tail);
106
+ if (!left || !right) {
107
+ return undefined;
108
+ }
109
+ if (tail === undefined) {
110
+ return left.length === 8 ? left : undefined;
111
+ }
112
+ const missing = 8 - left.length - right.length;
113
+ if (missing < 0) {
114
+ return undefined;
115
+ }
116
+ return [...left, ...Array.from({ length: missing }, () => 0), ...right];
117
+ };
118
+ const embeddedIPv4 = (groups) => {
119
+ const [g0 = 0, g1 = 0, g2 = 0, g3 = 0, g4 = 0, g5 = 0, g6 = 0, g7 = 0] = groups;
120
+ const asOctets = (high, low) => [high >> 8, high & 0xff, low >> 8, low & 0xff];
121
+ // IPv4-mapped (::ffff:a.b.c.d) and IPv4-compatible (::a.b.c.d). Both are
122
+ // ways of writing an IPv4 address, so both are classified as one — a
123
+ // check that only knew dotted quads would pass `::ffff:127.0.0.1`.
124
+ if (g0 === 0 && g1 === 0 && g2 === 0 && g3 === 0 && g4 === 0 && (g5 === 0xffff || g5 === 0)) {
125
+ if (g6 === 0 && g7 <= 1) {
126
+ return undefined; // :: and ::1 are classified as IPv6 below.
127
+ }
128
+ return asOctets(g6, g7);
129
+ }
130
+ // NAT64 (64:ff9b::/96) and 6to4 (2002::/16) both carry an IPv4 address
131
+ // that the network will actually route to.
132
+ if (g0 === 0x0064 && g1 === 0xff9b && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0) {
133
+ return asOctets(g6, g7);
134
+ }
135
+ if (g0 === 0x2002) {
136
+ return asOctets(g1, g2);
137
+ }
138
+ return undefined;
139
+ };
140
+ const classifyIPv6 = (groups, address) => {
141
+ const embedded = embeddedIPv4(groups);
142
+ if (embedded) {
143
+ const verdict = classifyIPv4(embedded);
144
+ return verdict.allowed
145
+ ? verdict
146
+ : { allowed: false, reason: `${address} embeds ${verdict.reason ?? 'a non-public address'}` };
147
+ }
148
+ const [g0 = 0] = groups;
149
+ const refuse = (what) => ({
150
+ allowed: false,
151
+ reason: `${address} is ${what}, which is not a public address`,
152
+ });
153
+ // RFC 8215's local-use NAT64 prefix. The well-known `64:ff9b::/96` above
154
+ // embeds its IPv4 at a fixed offset, so that one is extracted and judged
155
+ // on the address it carries — which is what lets an IPv6-only network
156
+ // reach the public IPv4 internet. This one may embed at any of RFC 6052's
157
+ // lengths, chosen by whoever runs the translator, so there is no offset
158
+ // to read it from. It is called local-use for a reason: refused whole,
159
+ // rather than guessed at and let through as public.
160
+ if (g0 === 0x0064 && groups[1] === 0xff9b && groups[2] === 0x0001) {
161
+ return refuse('the local-use NAT64 prefix (64:ff9b:1::/48), which can translate to any address');
162
+ }
163
+ if (groups.every((group) => group === 0))
164
+ return refuse('the unspecified address (::)');
165
+ if (groups.slice(0, 7).every((group) => group === 0) && groups[7] === 1)
166
+ return refuse('loopback (::1)');
167
+ if ((g0 & 0xfe00) === 0xfc00)
168
+ return refuse('a unique-local address (fc00::/7)');
169
+ if ((g0 & 0xffc0) === 0xfe80)
170
+ return refuse('link-local (fe80::/10)');
171
+ if ((g0 & 0xff00) === 0xff00)
172
+ return refuse('multicast (ff00::/8)');
173
+ if (g0 === 0x2001 && groups[1] === 0x0db8)
174
+ return refuse('documentation space (2001:db8::/32)');
175
+ if (g0 === 0x0100 && groups[1] === 0 && groups[2] === 0 && groups[3] === 0)
176
+ return refuse('the discard prefix (100::/64)');
177
+ return { allowed: true };
178
+ };
179
+ /**
180
+ * Classify one literal address, in either family.
181
+ *
182
+ * Called on the address a connection is actually about to use, never on
183
+ * the one a name resolved to a moment earlier somewhere else — that gap is
184
+ * DNS rebinding, and it is the difference between a check and a ceremony.
185
+ */
186
+ export const classifyAddress = (address) => {
187
+ const ipv4 = parseIPv4(address);
188
+ if (ipv4) {
189
+ return classifyIPv4(ipv4);
190
+ }
191
+ const ipv6 = expandIPv6(address);
192
+ if (ipv6) {
193
+ return classifyIPv6(ipv6, address);
194
+ }
195
+ return { allowed: false, reason: `${address} is not an IP address` };
196
+ };
197
+ const hostMatches = (policy, host) => {
198
+ const normalized = host.toLowerCase().replace(/^\[|\]$/g, '');
199
+ return (policy.allowedHosts ?? []).some((entry) => entry.toLowerCase() === normalized);
200
+ };
201
+ /** Whether one resolved address may be connected to for this host. */
202
+ export const checkAddress = (policy, host, address) => {
203
+ if (policy.allowPrivateAddresses || hostMatches(policy, host)) {
204
+ return { allowed: true };
205
+ }
206
+ return classifyAddress(address);
207
+ };
208
+ /** A URL this policy refuses, and why. */
209
+ export class EgressPolicyError extends Error {
210
+ constructor(message) {
211
+ super(message);
212
+ this.name = 'EgressPolicyError';
213
+ }
214
+ }
215
+ /**
216
+ * The check that can be made before anything is fetched: the scheme, and
217
+ * the address when the URL names one literally.
218
+ *
219
+ * A hostname cannot be judged here — that is what the connection-time
220
+ * check is for — so this returning without throwing is not permission to
221
+ * connect. It is permission to *try*, and it is where `file:///…` dies.
222
+ */
223
+ export const assertRequestAllowed = (rawUrl, policy = {}) => {
224
+ let url;
225
+ try {
226
+ url = new URL(rawUrl);
227
+ }
228
+ catch {
229
+ throw new EgressPolicyError(`Not a URL: ${rawUrl}`);
230
+ }
231
+ const schemes = policy.allowedSchemes ?? DEFAULT_SCHEMES;
232
+ if (!schemes.includes(url.protocol)) {
233
+ throw new EgressPolicyError(`Refusing ${url.protocol}//: only ${schemes.join(', ')} may be requested. `
234
+ + 'Local schemes are rejected before navigation — this process can read the files they name.');
235
+ }
236
+ const host = url.hostname.replace(/^\[|\]$/g, '');
237
+ const literal = parseIPv4(host) ? host : (expandIPv6(host) ? host : undefined);
238
+ if (literal) {
239
+ const verdict = checkAddress(policy, host, literal);
240
+ if (!verdict.allowed) {
241
+ throw new EgressPolicyError(`Refusing ${url.href}: ${verdict.reason}`);
242
+ }
243
+ }
244
+ return url;
245
+ };
246
+ //# sourceMappingURL=addresses.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"addresses.js","sourceRoot":"","sources":["../src/addresses.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ import http from 'node:http';
2
+ import net from 'node:net';
3
+ import { type EgressPolicy } from './addresses.ts';
4
+ /**
5
+ * One policy, as a key — sorted, because `{a,b}` and `{b,a}` are the same
6
+ * policy. Exported because anything that keeps a per-policy resource needs
7
+ * the same answer to "is this the same policy", and two spellings of that
8
+ * question would be two answers.
9
+ */
10
+ export declare const policyKeyFor: (policy: EgressPolicy) => string;
11
+ /**
12
+ * A DNS lookup that hands the socket only addresses the policy allows.
13
+ *
14
+ * This is the whole point of the module. Resolving a name in one place and
15
+ * connecting in another is a DNS rebinding race: the attacker's server
16
+ * answers `93.184.216.34` for the check and `169.254.169.254` a moment
17
+ * later for the connection, and a checker that looked the name up itself
18
+ * would have validated an address nothing ever connected to. Node lets the
19
+ * connection's *own* resolution be replaced, so the addresses this returns
20
+ * are the addresses the socket uses — there is no second lookup to lose to.
21
+ */
22
+ export declare const createPinnedLookup: (policy: EgressPolicy) => net.LookupFunction;
23
+ export interface PolicyRequestOptions {
24
+ policy?: EgressPolicy;
25
+ headers?: Record<string, string>;
26
+ /** Give up on the whole exchange after this long. */
27
+ timeoutMs?: number;
28
+ /** Stop reading here. A tool's output is bounded; so is what it downloads. */
29
+ maxBytes?: number;
30
+ signal?: AbortSignal;
31
+ }
32
+ export interface PolicyResponse {
33
+ status: number;
34
+ headers: http.IncomingHttpHeaders;
35
+ body: string;
36
+ /** True when `maxBytes` cut the body short. */
37
+ truncated: boolean;
38
+ }
39
+ /**
40
+ * One HTTP exchange, pinned to a validated address.
41
+ *
42
+ * Redirects are deliberately **not** followed here: each hop is a new
43
+ * request to a new host, so each one has to face the policy again. A
44
+ * caller loops and re-enters this, which is what makes "an approved public
45
+ * URL that redirects into the metadata endpoint" a refusal rather than a
46
+ * fetch.
47
+ */
48
+ export declare const requestThroughPolicy: (rawUrl: string, options?: PolicyRequestOptions) => Promise<PolicyResponse>;
49
+ export interface EgressProxy {
50
+ /** `http://127.0.0.1:<port>` — what a browser is launched pointing at. */
51
+ url: string;
52
+ port: number;
53
+ /** Requests this proxy refused, newest last. Surfaced to the agent as the reason a page is blank. */
54
+ refusals: string[];
55
+ close(): Promise<void>;
56
+ }
57
+ export declare const createEgressProxy: (policy?: EgressPolicy) => Promise<EgressProxy>;
58
+ /**
59
+ * What a Chromium launch needs to be actually pinned to a proxy.
60
+ *
61
+ * `--proxy-bypass-list=<-loopback>` is not optional: Chromium bypasses the
62
+ * proxy for loopback and `localhost` by default, so a browser pointed at
63
+ * this proxy would still reach `http://127.0.0.1:8080` directly — which is
64
+ * one of the exact destinations the proxy exists to refuse.
65
+ */
66
+ export declare const chromiumProxyOptions: (proxy: Pick<EgressProxy, "url">) => {
67
+ server: string;
68
+ bypass: string;
69
+ };
70
+ export declare const chromiumProxyArgs: (proxy: Pick<EgressProxy, "url">) => string[];
71
+ //# sourceMappingURL=connect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,GAAG,MAAM,UAAU,CAAC;AAG3B,OAAO,EAAyD,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE1G;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAI,QAAQ,YAAY,KAAG,MAIlD,CAAC;AAqBH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,YAAY,KAAG,GAAG,CAAC,cAqC7D,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,IAAI,CAAC,mBAAmB,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,MAAM,EACd,UAAS,oBAAyB,KACjC,OAAO,CAAC,cAAc,CAuFxB,CAAC;AAEF,MAAM,WAAW,WAAW;IAC1B,0EAA0E;IAC1E,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,qGAAqG;IACrG,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAqCD,eAAO,MAAM,iBAAiB,GAAU,SAAQ,YAAiB,KAAG,OAAO,CAAC,WAAW,CA+GtF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,KAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAIrG,CAAC;AAEH,eAAO,MAAM,iBAAiB,GAAI,OAAO,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,KAAG,MAAM,EAGzE,CAAC"}
@@ -0,0 +1,309 @@
1
+ import dns from 'node:dns';
2
+ import http from 'node:http';
3
+ import https from 'node:https';
4
+ import net from 'node:net';
5
+ import { assertRequestAllowed, checkAddress, EgressPolicyError } from "./addresses.js";
6
+ /**
7
+ * One policy, as a key — sorted, because `{a,b}` and `{b,a}` are the same
8
+ * policy. Exported because anything that keeps a per-policy resource needs
9
+ * the same answer to "is this the same policy", and two spellings of that
10
+ * question would be two answers.
11
+ */
12
+ export const policyKeyFor = (policy) => JSON.stringify({
13
+ allowPrivateAddresses: policy.allowPrivateAddresses ?? false,
14
+ allowedHosts: [...(policy.allowedHosts ?? [])].sort(),
15
+ allowedSchemes: [...(policy.allowedSchemes ?? [])].sort(),
16
+ });
17
+ /**
18
+ * Connection options that keep a policy's sockets to itself.
19
+ *
20
+ * `agent: false` is the load-bearing half. Node's global agent pools
21
+ * sockets by host and port and knows nothing about `lookup`, so a
22
+ * connection opened under a permissive policy is a connection a stricter
23
+ * one can be handed — no DNS resolution happens on a reused socket, and the
24
+ * pinned lookup that would have refused it is never called. That is a
25
+ * policy bypass that gets *more* likely the busier the daemon is, and it
26
+ * cost us a browser test that passed alone and failed in company.
27
+ *
28
+ * The price is no keep-alive for tool traffic, which for a fetch an agent
29
+ * makes a few times a turn is not a price worth a shared pool.
30
+ */
31
+ const pinnedConnection = (policy) => ({
32
+ agent: false,
33
+ lookup: createPinnedLookup(policy),
34
+ });
35
+ /**
36
+ * A DNS lookup that hands the socket only addresses the policy allows.
37
+ *
38
+ * This is the whole point of the module. Resolving a name in one place and
39
+ * connecting in another is a DNS rebinding race: the attacker's server
40
+ * answers `93.184.216.34` for the check and `169.254.169.254` a moment
41
+ * later for the connection, and a checker that looked the name up itself
42
+ * would have validated an address nothing ever connected to. Node lets the
43
+ * connection's *own* resolution be replaced, so the addresses this returns
44
+ * are the addresses the socket uses — there is no second lookup to lose to.
45
+ */
46
+ export const createPinnedLookup = (policy) => {
47
+ const lookup = (hostname, options, callback) => {
48
+ dns.lookup(hostname, { all: true, verbatim: true }, (error, addresses) => {
49
+ if (error) {
50
+ callback(error, '', 0);
51
+ return;
52
+ }
53
+ const permitted = addresses.filter((entry) => checkAddress(policy, hostname, entry.address).allowed);
54
+ if (permitted.length === 0) {
55
+ const first = addresses[0];
56
+ const verdict = first ? checkAddress(policy, hostname, first.address) : undefined;
57
+ callback(new EgressPolicyError(`Refusing to connect to ${hostname}: ${verdict?.reason ?? 'it resolves to no public address'}`), '', 0);
58
+ return;
59
+ }
60
+ if (options.all) {
61
+ callback(null, permitted);
62
+ return;
63
+ }
64
+ const [chosen] = permitted;
65
+ callback(null, chosen?.address ?? '', chosen?.family ?? 4);
66
+ });
67
+ };
68
+ return lookup;
69
+ };
70
+ /**
71
+ * One HTTP exchange, pinned to a validated address.
72
+ *
73
+ * Redirects are deliberately **not** followed here: each hop is a new
74
+ * request to a new host, so each one has to face the policy again. A
75
+ * caller loops and re-enters this, which is what makes "an approved public
76
+ * URL that redirects into the metadata endpoint" a refusal rather than a
77
+ * fetch.
78
+ */
79
+ export const requestThroughPolicy = async (rawUrl, options = {}) => {
80
+ const policy = options.policy ?? {};
81
+ const url = assertRequestAllowed(rawUrl, policy);
82
+ const transport = url.protocol === 'https:' ? https : http;
83
+ const maxBytes = options.maxBytes ?? 2_000_000;
84
+ return new Promise((resolve, reject) => {
85
+ let deadline;
86
+ const settle = (finish) => (value) => {
87
+ if (deadline) {
88
+ clearTimeout(deadline);
89
+ deadline = undefined;
90
+ }
91
+ finish(value);
92
+ };
93
+ const succeed = settle(resolve);
94
+ const fail = settle(reject);
95
+ const request = transport.request(url, {
96
+ method: 'GET',
97
+ headers: options.headers ?? {},
98
+ ...pinnedConnection(policy),
99
+ ...(options.signal ? { signal: options.signal } : {}),
100
+ }, (response) => {
101
+ let body = '';
102
+ let bytes = 0;
103
+ let truncated = false;
104
+ response.setEncoding('utf8');
105
+ response.on('data', (chunk) => {
106
+ if (truncated) {
107
+ return;
108
+ }
109
+ const size = Buffer.byteLength(chunk, 'utf8');
110
+ if (bytes + size > maxBytes) {
111
+ truncated = true;
112
+ // Kept to the cap, not to the end of whichever chunk crossed
113
+ // it: a single response can arrive in one piece, and "stop
114
+ // after the chunk that went over" is no cap at all.
115
+ body += chunk.slice(0, Math.max(0, maxBytes - bytes));
116
+ bytes = maxBytes;
117
+ // Stop the transfer rather than reading a gigabyte into a
118
+ // string we have already decided not to keep.
119
+ response.destroy();
120
+ return;
121
+ }
122
+ bytes += size;
123
+ body += chunk;
124
+ });
125
+ response.on('error', (error) => {
126
+ if (truncated) {
127
+ succeed({ status: response.statusCode ?? 0, headers: response.headers, body, truncated });
128
+ return;
129
+ }
130
+ fail(error);
131
+ });
132
+ response.on('end', () => {
133
+ succeed({ status: response.statusCode ?? 0, headers: response.headers, body, truncated });
134
+ });
135
+ response.on('close', () => {
136
+ if (truncated) {
137
+ succeed({ status: response.statusCode ?? 0, headers: response.headers, body, truncated });
138
+ }
139
+ });
140
+ });
141
+ if (options.timeoutMs) {
142
+ // A deadline for the whole exchange, not for a quiet socket.
143
+ // `setTimeout` on a request fires on *inactivity*, so a server that
144
+ // sends one byte before each interval keeps a fetch alive forever —
145
+ // which for a one-shot `stratus run` is a command that never returns.
146
+ // Both are armed: the wall clock bounds the exchange, and the
147
+ // inactivity timer still ends a connection that has simply stalled.
148
+ deadline = setTimeout(() => {
149
+ request.destroy(new Error(`Timed out after ${options.timeoutMs}ms: ${url.href}`));
150
+ }, options.timeoutMs);
151
+ deadline.unref?.();
152
+ request.setTimeout(options.timeoutMs, () => {
153
+ request.destroy(new Error(`Timed out after ${options.timeoutMs}ms of silence: ${url.href}`));
154
+ });
155
+ }
156
+ request.on('error', fail);
157
+ request.end();
158
+ });
159
+ };
160
+ /**
161
+ * A proxy that owns every connection a browser makes, so the browser's own
162
+ * resolver never decides where a request goes.
163
+ *
164
+ * Playwright can intercept requests, but interception happens in the
165
+ * browser and the browser resolves the name itself afterwards — validating
166
+ * there is the rebinding race again, one process further away. Routing
167
+ * Chromium through a proxy that resolves, validates, and connects means the
168
+ * socket the page gets is the socket this policy opened, for the address it
169
+ * checked. Redirects and subresources come back through here too, because
170
+ * every request does.
171
+ */
172
+ /**
173
+ * The `host:port` a CONNECT carries, which is not a plain split on colons.
174
+ *
175
+ * A literal IPv6 host arrives bracketed — `[2606:4700::1111]:443` — and
176
+ * splitting that on every colon yields a host of `[`, which is then looked
177
+ * up in DNS and fails. Every IPv6-literal HTTPS page a browser is pointed
178
+ * at would be unreachable, and the failure would read as a policy refusal
179
+ * rather than a parse bug.
180
+ */
181
+ const parseAuthority = (authority) => {
182
+ const bracketed = /^\[(?<host>[^\]]+)\](?::(?<port>\d+))?$/.exec(authority);
183
+ const plain = /^(?<host>[^:]+)(?::(?<port>\d+))?$/.exec(authority);
184
+ const groups = (bracketed ?? plain)?.groups;
185
+ if (!groups?.host) {
186
+ return undefined;
187
+ }
188
+ const port = groups.port === undefined ? 443 : Number(groups.port);
189
+ if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
190
+ return undefined;
191
+ }
192
+ return { host: groups.host, port };
193
+ };
194
+ export const createEgressProxy = async (policy = {}) => {
195
+ const refusals = [];
196
+ const sockets = new Set();
197
+ const dial = async (host, port) => {
198
+ const addresses = await dns.promises.lookup(host, { all: true, verbatim: true });
199
+ const permitted = addresses.find((entry) => checkAddress(policy, host, entry.address).allowed);
200
+ if (!permitted) {
201
+ const first = addresses[0];
202
+ const verdict = first ? checkAddress(policy, host, first.address) : undefined;
203
+ throw new EgressPolicyError(`Refusing to connect to ${host}: ${verdict?.reason ?? 'it resolves to no public address'}`);
204
+ }
205
+ // Connected to the checked address itself, not to the name: a second
206
+ // resolution is a second answer, and the attacker chooses when it
207
+ // changes.
208
+ return net.connect({ host: permitted.address, port });
209
+ };
210
+ const server = http.createServer();
211
+ server.on('connect', (request, clientSocket, head) => {
212
+ sockets.add(clientSocket);
213
+ clientSocket.on('close', () => sockets.delete(clientSocket));
214
+ const target = parseAuthority(request.url ?? '');
215
+ if (!target) {
216
+ clientSocket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
217
+ return;
218
+ }
219
+ const { host, port } = target;
220
+ dial(host, port).then((upstream) => {
221
+ sockets.add(upstream);
222
+ upstream.on('close', () => sockets.delete(upstream));
223
+ clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
224
+ if (head.length > 0) {
225
+ upstream.write(head);
226
+ }
227
+ upstream.pipe(clientSocket);
228
+ clientSocket.pipe(upstream);
229
+ upstream.on('error', () => clientSocket.destroy());
230
+ clientSocket.on('error', () => upstream.destroy());
231
+ }, (error) => {
232
+ const reason = error instanceof Error ? error.message : String(error);
233
+ refusals.push(reason);
234
+ // 403 rather than a dropped socket: the page sees a refusal it can
235
+ // render, and the reason reaches the agent through `refusals`.
236
+ clientSocket.end(`HTTP/1.1 403 Forbidden\r\n\r\n${reason}`);
237
+ });
238
+ });
239
+ server.on('request', (request, response) => {
240
+ // Plain HTTP arrives as an absolute-URI request rather than a CONNECT.
241
+ let target;
242
+ try {
243
+ target = assertRequestAllowed(request.url ?? '', policy);
244
+ }
245
+ catch (error) {
246
+ const reason = error instanceof Error ? error.message : String(error);
247
+ refusals.push(reason);
248
+ response.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' });
249
+ response.end(reason);
250
+ return;
251
+ }
252
+ const upstream = http.request({
253
+ host: target.hostname,
254
+ port: target.port ? Number(target.port) : 80,
255
+ path: `${target.pathname}${target.search}`,
256
+ method: request.method ?? 'GET',
257
+ headers: request.headers,
258
+ ...pinnedConnection(policy),
259
+ }, (upstreamResponse) => {
260
+ response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
261
+ upstreamResponse.pipe(response);
262
+ });
263
+ upstream.on('error', (error) => {
264
+ refusals.push(error.message);
265
+ if (!response.headersSent) {
266
+ response.writeHead(403, { 'content-type': 'text/plain; charset=utf-8' });
267
+ }
268
+ response.end(error.message);
269
+ });
270
+ request.pipe(upstream);
271
+ });
272
+ await new Promise((resolve) => {
273
+ // Loopback only: this proxy exists to narrow what one process may
274
+ // reach, and a proxy on a shared interface is an open relay.
275
+ server.listen(0, '127.0.0.1', resolve);
276
+ });
277
+ const port = server.address().port;
278
+ return {
279
+ url: `http://127.0.0.1:${port}`,
280
+ port,
281
+ refusals,
282
+ async close() {
283
+ for (const socket of sockets) {
284
+ socket.destroy();
285
+ }
286
+ await new Promise((resolve) => {
287
+ server.close(() => resolve());
288
+ });
289
+ },
290
+ };
291
+ };
292
+ /**
293
+ * What a Chromium launch needs to be actually pinned to a proxy.
294
+ *
295
+ * `--proxy-bypass-list=<-loopback>` is not optional: Chromium bypasses the
296
+ * proxy for loopback and `localhost` by default, so a browser pointed at
297
+ * this proxy would still reach `http://127.0.0.1:8080` directly — which is
298
+ * one of the exact destinations the proxy exists to refuse.
299
+ */
300
+ export const chromiumProxyOptions = (proxy) => ({
301
+ server: proxy.url,
302
+ // Same reason as the flag below, in the form Playwright takes.
303
+ bypass: '<-loopback>',
304
+ });
305
+ export const chromiumProxyArgs = (proxy) => [
306
+ `--proxy-server=${proxy.url}`,
307
+ '--proxy-bypass-list=<-loopback>',
308
+ ];
309
+ //# sourceMappingURL=connect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.js","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The table every tool that reaches the network is tested against.
3
+ *
4
+ * Shipped in `src` rather than kept in one package's tests because two
5
+ * packages must be held to it, and the acceptance criterion for that is
6
+ * exact: `web.fetch` refuses the same addresses and schemes as `browser.*`,
7
+ * *proven by a test that runs the same table through both*. If the shared
8
+ * module is ever forked, this is what fails — in whichever package forked
9
+ * it, because both suites read this list rather than their own.
10
+ */
11
+ export interface HostileUrl {
12
+ url: string;
13
+ /** What it is, for a failing assertion that names the hole rather than an index. */
14
+ what: string;
15
+ }
16
+ export declare const HOSTILE_URLS: HostileUrl[];
17
+ //# sourceMappingURL=fixtures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../src/fixtures.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,oFAAoF;IACpF,IAAI,EAAE,MAAM,CAAC;CACd;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,EAwBpC,CAAC"}
@@ -0,0 +1,26 @@
1
+ export const HOSTILE_URLS = [
2
+ { url: 'file:///home/ada/.stratus/credentials.json', what: 'the credential store, by file: URL' },
3
+ { url: 'file:///etc/passwd', what: 'a local file' },
4
+ { url: 'data:text/html,<script>fetch("http://169.254.169.254")</script>', what: 'an inline document' },
5
+ { url: 'javascript:fetch("http://127.0.0.1")', what: 'a javascript: URL' },
6
+ { url: 'ftp://example.com/secrets', what: 'a scheme nobody asked for' },
7
+ { url: 'http://169.254.169.254/latest/meta-data/', what: 'cloud instance metadata' },
8
+ { url: 'http://[fd00::1]/', what: 'an IPv6 unique-local address' },
9
+ { url: 'http://[::ffff:169.254.169.254]/', what: 'metadata again, as an IPv4-mapped IPv6 address' },
10
+ { url: 'http://[::ffff:127.0.0.1]/', what: 'loopback, as an IPv4-mapped IPv6 address' },
11
+ { url: 'http://[64:ff9b::a9fe:a9fe]/', what: 'metadata again, through NAT64' },
12
+ { url: 'http://[64:ff9b:1::a9fe:a9fe]/', what: 'metadata again, through the local-use NAT64 prefix' },
13
+ { url: 'http://[2002:a9fe:a9fe::]/', what: 'metadata again, through 6to4' },
14
+ { url: 'http://127.0.0.1:4123/api/v1/credentials', what: 'the daemon’s own control API' },
15
+ { url: 'http://[::1]:4123/api/v1/credentials', what: 'the control API over IPv6 loopback' },
16
+ { url: 'http://localhost:4123/api/v1/credentials', what: 'the control API by name' },
17
+ { url: 'http://10.0.0.5/', what: 'an RFC 1918 address' },
18
+ { url: 'http://172.16.4.4/', what: 'an RFC 1918 address in the /12' },
19
+ { url: 'http://192.168.1.1/', what: 'the home router' },
20
+ { url: 'http://100.100.100.200/', what: 'a carrier-NAT address' },
21
+ { url: 'http://[fe80::1]/', what: 'an IPv6 link-local address' },
22
+ { url: 'http://0.0.0.0/', what: 'the unspecified address' },
23
+ { url: 'http://192.0.0.1/', what: 'IETF protocol assignment space' },
24
+ { url: 'http://224.0.0.1/', what: 'multicast' },
25
+ ];
26
+ //# sourceMappingURL=fixtures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixtures.js","sourceRoot":"","sources":["../src/fixtures.ts"],"names":[],"mappings":"AAgBA,MAAM,CAAC,MAAM,YAAY,GAAiB;IACxC,EAAE,GAAG,EAAE,4CAA4C,EAAE,IAAI,EAAE,oCAAoC,EAAE;IACjG,EAAE,GAAG,EAAE,oBAAoB,EAAE,IAAI,EAAE,cAAc,EAAE;IACnD,EAAE,GAAG,EAAE,iEAAiE,EAAE,IAAI,EAAE,oBAAoB,EAAE;IACtG,EAAE,GAAG,EAAE,sCAAsC,EAAE,IAAI,EAAE,mBAAmB,EAAE;IAC1E,EAAE,GAAG,EAAE,2BAA2B,EAAE,IAAI,EAAE,2BAA2B,EAAE;IACvE,EAAE,GAAG,EAAE,0CAA0C,EAAE,IAAI,EAAE,yBAAyB,EAAE;IACpF,EAAE,GAAG,EAAE,mBAAmB,EAAE,IAAI,EAAE,8BAA8B,EAAE;IAClE,EAAE,GAAG,EAAE,kCAAkC,EAAE,IAAI,EAAE,gDAAgD,EAAE;IACnG,EAAE,GAAG,EAAE,4BAA4B,EAAE,IAAI,EAAE,0CAA0C,EAAE;IACvF,EAAE,GAAG,EAAE,8BAA8B,EAAE,IAAI,EAAE,+BAA+B,EAAE;IAC9E,EAAE,GAAG,EAAE,gCAAgC,EAAE,IAAI,EAAE,oDAAoD,EAAE;IACrG,EAAE,GAAG,EAAE,4BAA4B,EAAE,IAAI,EAAE,8BAA8B,EAAE;IAC3E,EAAE,GAAG,EAAE,0CAA0C,EAAE,IAAI,EAAE,8BAA8B,EAAE;IACzF,EAAE,GAAG,EAAE,sCAAsC,EAAE,IAAI,EAAE,oCAAoC,EAAE;IAC3F,EAAE,GAAG,EAAE,0CAA0C,EAAE,IAAI,EAAE,yBAAyB,EAAE;IACpF,EAAE,GAAG,EAAE,kBAAkB,EAAE,IAAI,EAAE,qBAAqB,EAAE;IACxD,EAAE,GAAG,EAAE,oBAAoB,EAAE,IAAI,EAAE,gCAAgC,EAAE;IACrE,EAAE,GAAG,EAAE,qBAAqB,EAAE,IAAI,EAAE,iBAAiB,EAAE;IACvD,EAAE,GAAG,EAAE,yBAAyB,EAAE,IAAI,EAAE,uBAAuB,EAAE;IACjE,EAAE,GAAG,EAAE,mBAAmB,EAAE,IAAI,EAAE,4BAA4B,EAAE;IAChE,EAAE,GAAG,EAAE,iBAAiB,EAAE,IAAI,EAAE,yBAAyB,EAAE;IAC3D,EAAE,GAAG,EAAE,mBAAmB,EAAE,IAAI,EAAE,gCAAgC,EAAE;IACpE,EAAE,GAAG,EAAE,mBAAmB,EAAE,IAAI,EAAE,WAAW,EAAE;CAChD,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * One implementation of the address policy, shared.
3
+ *
4
+ * `tool-web` and `tool-browser` both reach the network on an agent's
5
+ * instruction, and a second hand-rolled copy of "which addresses may a tool
6
+ * reach" would not be a drift — it would be an SSRF hole in whichever copy
7
+ * was not updated. So the rule lives here, once, and the packages that
8
+ * enforce it import it.
9
+ */
10
+ export { assertRequestAllowed, checkAddress, classifyAddress, EgressPolicyError, type AddressVerdict, type EgressPolicy, } from './addresses.ts';
11
+ export { chromiumProxyArgs, chromiumProxyOptions, createEgressProxy, createPinnedLookup, policyKeyFor, requestThroughPolicy, type EgressProxy, type PolicyRequestOptions, type PolicyResponse, } from './connect.ts';
12
+ export { HOSTILE_URLS, type HostileUrl } from './fixtures.ts';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,eAAe,EACf,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,YAAY,GAClB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,EACpB,KAAK,WAAW,EAChB,KAAK,oBAAoB,EACzB,KAAK,cAAc,GACpB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * One implementation of the address policy, shared.
3
+ *
4
+ * `tool-web` and `tool-browser` both reach the network on an agent's
5
+ * instruction, and a second hand-rolled copy of "which addresses may a tool
6
+ * reach" would not be a drift — it would be an SSRF hole in whichever copy
7
+ * was not updated. So the rule lives here, once, and the packages that
8
+ * enforce it import it.
9
+ */
10
+ export { assertRequestAllowed, checkAddress, classifyAddress, EgressPolicyError, } from "./addresses.js";
11
+ export { chromiumProxyArgs, chromiumProxyOptions, createEgressProxy, createPinnedLookup, policyKeyFor, requestThroughPolicy, } from "./connect.js";
12
+ export { HOSTILE_URLS } from "./fixtures.js";
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,eAAe,EACf,iBAAiB,GAGlB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,GAIrB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,YAAY,EAAmB,MAAM,eAAe,CAAC"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@stratusagent/egress",
3
+ "version": "0.6.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "engines": {
18
+ "node": ">=22.13 <23 || >=23.4"
19
+ },
20
+ "dependencies": {},
21
+ "devDependencies": {
22
+ "@types/node": "^24.6.0",
23
+ "typescript": "^5.9.3"
24
+ },
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/stratuslabs/agent.git",
29
+ "directory": "packages/egress"
30
+ },
31
+ "description": "The Stratus Agent egress policy: which addresses a tool may reach, enforced on the connection rather than on the name it was asked for",
32
+ "keywords": [
33
+ "stratus",
34
+ "agent",
35
+ "ai",
36
+ "llm",
37
+ "ssrf",
38
+ "security"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc -b",
45
+ "typecheck": "tsc --noEmit --project tsconfig.test.json",
46
+ "test": "node --test --experimental-strip-types"
47
+ }
48
+ }