@intentface/latch-net 0.9.1

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 Intentface
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,31 @@
1
+ # @intentface/latch-net
2
+
3
+ Network-safety primitives for Latch — SSRF guards and a DNS-pinned egress `fetch`, shared by every host that opens an outbound connection on a model's behalf.
4
+
5
+ The root export has **zero third-party dependencies** (Node built-ins only), so any package can depend on it without pulling weight in. The pinned fetch, which needs `undici`, sits behind the `/pinned` subpath.
6
+
7
+ ## What it does
8
+
9
+ - **`assertPublicUrl`** — the front-line check. Parses the URL and rejects any host that is, or resolves to, a private, loopback, link-local or otherwise internal address. Returns the parsed `URL` so the caller can keep using it.
10
+ - **`isBlockedHost` / `isBlockedResolved` / `isPrivateV4`** — the predicates behind it. The string-level one is deliberately conservative, covering `.localhost` and `.local` names, IPv6 literals in any spelling, and the integer and hex encodings of an IPv4 address. The resolving one is the rebinding backstop.
11
+ - **`isHostAllowed`** — positive allowlist matching (an exact host, or `.suffix` for subdomains), used to bound which hosts a tenant may reach at all. It complements the blocklist rather than replacing it.
12
+ - **`createPinnedFetch` / `pinnedFetch`** (`@intentface/latch-net/pinned`) — a `fetch` that vets addresses **inside** the lookup the connection itself uses.
13
+
14
+ ## Why the pinned fetch exists
15
+
16
+ Checking a hostname and then handing it to `fetch` leaves a gap: `fetch` performs its own second DNS lookup, so a name that resolves public during the check and private when the socket opens defeats the guard. That is DNS rebinding, and it is a time-of-check/time-of-use bug, not a weak blocklist.
17
+
18
+ `createPinnedFetch` closes the gap by validating in the undici connector's `lookup`, so there is no distance between validating and connecting. TLS is untouched: SNI and certificate validation stay against the original hostname, and redirect hops dispatch through the same agent, so every hop is connect-vetted too.
19
+
20
+ ```ts
21
+ import { assertPublicUrl } from "@intentface/latch-net";
22
+ import { createPinnedFetch } from "@intentface/latch-net/pinned";
23
+
24
+ await assertPublicUrl(url); // throws on private / non-public targets
25
+ const fetch = createPinnedFetch();
26
+ const response = await fetch(url, { redirect: "follow" });
27
+ ```
28
+
29
+ ## Where it fits
30
+
31
+ An optional peer of `@intentface/latch-core`, required by its `web_fetch` harness tool. `latch-mcp` uses the allowlist to bound MCP server hosts, and any host tool that fetches a user-supplied URL should route through it.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Positive host allowlist shared by hosts that gate outbound connections on a
3
+ * per-tenant list (MCP servers, OpenAPI backends). Complements the SSRF
4
+ * blocklist in ./ssrf.ts: the allowlist says which hosts a tenant may reach at
5
+ * all; the blocklist rejects private/internal addresses regardless.
6
+ */
7
+ /** True if `host` is permitted by the allowlist (exact, or `.suffix` subdomain). */
8
+ export declare function isHostAllowed(host: string, allowed: string[]): boolean;
9
+ //# sourceMappingURL=allowlist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"allowlist.d.ts","sourceRoot":"","sources":["../src/allowlist.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,oFAAoF;AACpF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAStE"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Positive host allowlist shared by hosts that gate outbound connections on a
3
+ * per-tenant list (MCP servers, OpenAPI backends). Complements the SSRF
4
+ * blocklist in ./ssrf.ts: the allowlist says which hosts a tenant may reach at
5
+ * all; the blocklist rejects private/internal addresses regardless.
6
+ */
7
+ /** True if `host` is permitted by the allowlist (exact, or `.suffix` subdomain). */
8
+ export function isHostAllowed(host, allowed) {
9
+ // Strip one trailing DNS dot: `URL.hostname` preserves it for a FQDN like
10
+ // `api.example.com.`, which must match the same allowlist entries.
11
+ const h = host.toLowerCase().replace(/\.$/, "");
12
+ return allowed.some((entry) => {
13
+ const a = entry.toLowerCase();
14
+ if (a.startsWith("."))
15
+ return h === a.slice(1) || h.endsWith(a);
16
+ return h === a;
17
+ });
18
+ }
19
+ //# sourceMappingURL=allowlist.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"allowlist.js","sourceRoot":"","sources":["../src/allowlist.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,oFAAoF;AACpF,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,OAAiB;IAC3D,0EAA0E;IAC1E,mEAAmE;IACnE,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAChD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;QAC5B,MAAM,CAAC,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;QAC9B,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QAChE,OAAO,CAAC,KAAK,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @intentface/latch-net
3
+ *
4
+ * Network-safety primitives shared across the Latch stack. This root export
5
+ * has zero third-party dependencies (Node built-ins only), so any host
6
+ * package can depend on it without pulling heavier packages in; the
7
+ * DNS-pinned egress fetch, which pulls in `undici`, lives behind the
8
+ * `@intentface/latch-net/pinned` subpath.
9
+ */
10
+ export { isPrivateV4, isBlockedHost, isBlockedResolved, assertPublicUrl, } from "./ssrf.js";
11
+ export { isHostAllowed } from "./allowlist.js";
12
+ //# 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,WAAW,EACX,aAAa,EACb,iBAAiB,EACjB,eAAe,GAChB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @intentface/latch-net
3
+ *
4
+ * Network-safety primitives shared across the Latch stack. This root export
5
+ * has zero third-party dependencies (Node built-ins only), so any host
6
+ * package can depend on it without pulling heavier packages in; the
7
+ * DNS-pinned egress fetch, which pulls in `undici`, lives behind the
8
+ * `@intentface/latch-net/pinned` subpath.
9
+ */
10
+ export { isPrivateV4, isBlockedHost, isBlockedResolved, assertPublicUrl, } from "./ssrf.js";
11
+ export { isHostAllowed } from "./allowlist.js";
12
+ //# 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,WAAW,EACX,aAAa,EACb,iBAAiB,EACjB,eAAe,GAChB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,72 @@
1
+ import { buildConnector } from "undici";
2
+ /**
3
+ * DNS-pinned SSRF-safe fetch.
4
+ *
5
+ * The guards in `ssrf.ts` validate a hostname and then hand it to `fetch`,
6
+ * which performs its OWN second DNS lookup — a name that resolves public
7
+ * during the check and private when the connection opens (DNS rebinding /
8
+ * TOCTOU) defeats them. This module closes that gap by vetting addresses
9
+ * INSIDE the lookup the connection itself uses (undici `Agent` →
10
+ * `buildConnector` → `net`/`tls.connect` `lookup`), so there is no
11
+ * validate-vs-connect distance at all. TLS is untouched: undici sets
12
+ * `servername` from the URL host before the lookup runs, so SNI and
13
+ * certificate validation stay against the original hostname and
14
+ * `rejectUnauthorized` keeps its default.
15
+ *
16
+ * Lives behind the `@intentface/latch-net/pinned` subpath (not the root
17
+ * export) because it pulls in `undici` — the root entry stays third-party-free.
18
+ *
19
+ * Redirect hops dispatch through the same agent, so with `redirect: "follow"`
20
+ * every hop is connect-vetted too (call sites here still use "error"/"manual"
21
+ * for better messages and header hygiene).
22
+ */
23
+ /**
24
+ * Input is `string | URL` only — undici's `fetch` rejects `Request` objects
25
+ * built by another fetch implementation (cross-realm brand check), so the
26
+ * type must not admit them.
27
+ */
28
+ export type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;
29
+ export interface PinnedFetch extends FetchLike {
30
+ /** Close the underlying Agent (keep-alive sockets would hold a test's event loop). */
31
+ close(): Promise<void>;
32
+ }
33
+ type Addr = {
34
+ address: string;
35
+ family: number;
36
+ };
37
+ /**
38
+ * Callback-style resolver, injectable for tests. Always invoked with
39
+ * `all: true`; other options (`family`, `hints`) pass through from `net`.
40
+ */
41
+ export type BaseLookup = (hostname: string, options: {
42
+ all: true;
43
+ family?: number;
44
+ hints?: number;
45
+ }, callback: (err: NodeJS.ErrnoException | null, addresses?: Addr[]) => void) => void;
46
+ export interface PinnedFetchOptions {
47
+ /**
48
+ * Skip the private-address vetting (trusted local backend, tests against
49
+ * 127.0.0.1 servers). The pinning path itself is unchanged.
50
+ */
51
+ allowPrivate?: boolean;
52
+ /** Test seam; defaults to `node:dns` `lookup`. */
53
+ lookup?: BaseLookup;
54
+ /**
55
+ * Extra socket/TLS options for the connector (tests: a fixture `ca`).
56
+ * `autoSelectFamily` is widened in explicitly: it IS typed in the union's
57
+ * `TcpNetConnectOpts` member, but that member requires `port`, so a partial
58
+ * options object can never select it.
59
+ */
60
+ connect?: buildConnector.BuildOptions & {
61
+ autoSelectFamily?: boolean;
62
+ };
63
+ }
64
+ /**
65
+ * Build a fetch whose connections resolve through the vetting lookup above.
66
+ * One shared `Agent` per instance — pooled keep-alive sockets are safe to
67
+ * reuse because they were vetted when they connected.
68
+ */
69
+ export declare function createPinnedFetch(options?: PinnedFetchOptions): PinnedFetch;
70
+ export declare const pinnedFetch: FetchLike;
71
+ export {};
72
+ //# sourceMappingURL=pinned-fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pinned-fetch.d.ts","sourceRoot":"","sources":["../src/pinned-fetch.ts"],"names":[],"mappings":"AAEA,OAAO,EAAS,cAAc,EAAwB,MAAM,QAAQ,CAAC;AAIrE;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,CACtB,KAAK,EAAE,MAAM,GAAG,GAAG,EACnB,IAAI,CAAC,EAAE,WAAW,KACf,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvB,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,sFAAsF;IACtF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,KAAK,IAAI,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhD;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,CACvB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;IAAE,GAAG,EAAE,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,EACvD,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,cAAc,GAAG,IAAI,EAAE,SAAS,CAAC,EAAE,IAAI,EAAE,KAAK,IAAI,KACtE,IAAI,CAAC;AAEV,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kDAAkD;IAClD,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC,YAAY,GAAG;QAAE,gBAAgB,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACxE;AA0DD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,WAAW,CA6B/E;AASD,eAAO,MAAM,WAAW,EAAE,SACqB,CAAC"}
@@ -0,0 +1,91 @@
1
+ import { lookup as dnsLookup } from "node:dns";
2
+ import { isIP } from "node:net";
3
+ import { Agent, buildConnector, fetch as undiciFetch } from "undici";
4
+ import { isBlockedHost, isPrivateV4, isPrivateV6, parseIPv6 } from "./ssrf.js";
5
+ function isPrivateAddress({ address, family }) {
6
+ if (family === 4)
7
+ return isPrivateV4(address);
8
+ const bytes = parseIPv6(address);
9
+ return bytes ? isPrivateV6(bytes) : true; // unparseable resolver output → fail closed
10
+ }
11
+ /**
12
+ * Wrap a resolver so every address it returns is classified before the socket
13
+ * ever sees it: error (fail closed) on lookup failure, an empty answer, or —
14
+ * unless `allowPrivate` — any private/internal address. Node calls the lookup
15
+ * in two shapes depending on `options.all` (`autoSelectFamily` uses the array
16
+ * form); both are honored.
17
+ */
18
+ function vettingLookup(base, allowPrivate) {
19
+ return (hostname, options, callback) => {
20
+ base(hostname, { ...options, all: true }, (err, addresses) => {
21
+ if (err)
22
+ return callback(err);
23
+ const addrs = addresses ?? [];
24
+ if (addrs.length === 0) {
25
+ return callback(new Error(`lookup returned no addresses: ${hostname}`));
26
+ }
27
+ if (!allowPrivate) {
28
+ const bad = addrs.find(isPrivateAddress);
29
+ if (bad) {
30
+ return callback(new Error(`Blocked private/internal address: ${hostname} -> ${bad.address}`));
31
+ }
32
+ }
33
+ if (options.all)
34
+ return callback(null, addrs);
35
+ // Scalar shape: honor a requested family even if the base resolver
36
+ // ignored the forwarded option (real dns.lookup honors it; the contract
37
+ // on BaseLookup doesn't require it). No cross-family fallback — handing
38
+ // back the wrong family is worse than failing, and real dns.lookup
39
+ // reports ENOTFOUND for a family with no answers.
40
+ const pick = options.family
41
+ ? addrs.find((a) => a.family === options.family)
42
+ : addrs[0];
43
+ if (!pick) {
44
+ return callback(new Error(`lookup returned no addresses for family ${options.family}: ${hostname}`));
45
+ }
46
+ callback(null, pick.address, pick.family);
47
+ });
48
+ };
49
+ }
50
+ /**
51
+ * Build a fetch whose connections resolve through the vetting lookup above.
52
+ * One shared `Agent` per instance — pooled keep-alive sockets are safe to
53
+ * reuse because they were vetted when they connected.
54
+ */
55
+ export function createPinnedFetch(options = {}) {
56
+ const { allowPrivate = false, lookup = dnsLookup } = options;
57
+ const base = buildConnector({
58
+ ...options.connect,
59
+ // Passed through to net/tls.connect; spread AFTER options.connect so a
60
+ // caller can never displace the vetting resolver. (Cast for the same
61
+ // BuildOptions-union reason as `autoSelectFamily` above.)
62
+ lookup: vettingLookup(lookup, allowPrivate),
63
+ });
64
+ // net.connect skips `lookup` entirely for IP-literal hosts — vet those here.
65
+ const connector = (opts, cb) => {
66
+ const host = opts.hostname.replace(/^\[|\]$/g, "");
67
+ if (!allowPrivate && isIP(host) !== 0 && isBlockedHost(host)) {
68
+ cb(new Error(`Blocked private/internal address: ${host}`), null);
69
+ return;
70
+ }
71
+ base(opts, cb);
72
+ };
73
+ const agent = new Agent({ connect: connector });
74
+ // undici's fetch, not the global: mixing this Agent into Node's bundled
75
+ // fetch is cross-version undici and unsupported. Types differ from lib.dom
76
+ // but the runtime objects are the same implementation Node bundles.
77
+ const fn = (input, init) => undiciFetch(input, {
78
+ ...init,
79
+ dispatcher: agent,
80
+ });
81
+ return Object.assign(fn, { close: () => agent.close() });
82
+ }
83
+ /**
84
+ * Shared default instance, constructed lazily (no Agent at import time).
85
+ * Deliberately exposed WITHOUT `close()` — it is shared, so no caller may
86
+ * tear it down under the others; a long-lived server never closes it, and
87
+ * idle keep-alive sockets expire on undici's own timeout.
88
+ */
89
+ let shared;
90
+ export const pinnedFetch = (input, init) => (shared ??= createPinnedFetch())(input, init);
91
+ //# sourceMappingURL=pinned-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pinned-fetch.js","sourceRoot":"","sources":["../src/pinned-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AAErE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAoE/E,SAAS,gBAAgB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAQ;IACjD,IAAI,MAAM,KAAK,CAAC;QAAE,OAAO,WAAW,CAAC,OAAO,CAAC,CAAC;IAC9C,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IACjC,OAAO,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,4CAA4C;AACxF,CAAC;AAED;;;;;;GAMG;AACH,SAAS,aAAa,CAAC,IAAgB,EAAE,YAAqB;IAC5D,OAAO,CACL,QAAgB,EAChB,OAA2C,EAC3C,QAAyE,EACnE,EAAE;QACR,IAAI,CAAC,QAAQ,EAAE,EAAE,GAAG,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE;YAC3D,IAAI,GAAG;gBAAE,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC;YAC9B,MAAM,KAAK,GAAG,SAAS,IAAI,EAAE,CAAC;YAC9B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACvB,OAAO,QAAQ,CAAC,IAAI,KAAK,CAAC,iCAAiC,QAAQ,EAAE,CAAC,CAAC,CAAC;YAC1E,CAAC;YACD,IAAI,CAAC,YAAY,EAAE,CAAC;gBAClB,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;gBACzC,IAAI,GAAG,EAAE,CAAC;oBACR,OAAO,QAAQ,CACb,IAAI,KAAK,CACP,qCAAqC,QAAQ,OAAO,GAAG,CAAC,OAAO,EAAE,CAClE,CACF,CAAC;gBACJ,CAAC;YACH,CAAC;YACD,IAAI,OAAO,CAAC,GAAG;gBAAE,OAAO,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAC9C,mEAAmE;YACnE,wEAAwE;YACxE,wEAAwE;YACxE,mEAAmE;YACnE,kDAAkD;YAClD,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM;gBACzB,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC;gBAChD,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YACb,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO,QAAQ,CACb,IAAI,KAAK,CACP,2CAA2C,OAAO,CAAC,MAAM,KAAK,QAAQ,EAAE,CACzE,CACF,CAAC;YACJ,CAAC;YACD,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAA8B,EAAE;IAChE,MAAM,EAAE,YAAY,GAAG,KAAK,EAAE,MAAM,GAAG,SAAkC,EAAE,GACzE,OAAO,CAAC;IACV,MAAM,IAAI,GAAG,cAAc,CAAC;QAC1B,GAAG,OAAO,CAAC,OAAO;QAClB,uEAAuE;QACvE,qEAAqE;QACrE,0DAA0D;QAC1D,MAAM,EAAE,aAAa,CAAC,MAAM,EAAE,YAAY,CAAC;KACb,CAAC,CAAC;IAClC,6EAA6E;IAC7E,MAAM,SAAS,GAA6B,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE;QACvD,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QACnD,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7D,EAAE,CAAC,IAAI,KAAK,CAAC,qCAAqC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;YACjE,OAAO;QACT,CAAC;QACD,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACjB,CAAC,CAAC;IACF,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;IAChD,wEAAwE;IACxE,2EAA2E;IAC3E,oEAAoE;IACpE,MAAM,EAAE,GAAc,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CACpC,WAAW,CAAC,KAA0C,EAAE;QACtD,GAAI,IAAuE;QAC3E,UAAU,EAAE,KAAK;KAClB,CAAiC,CAAC;IACrC,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;GAKG;AACH,IAAI,MAA+B,CAAC;AACpC,MAAM,CAAC,MAAM,WAAW,GAAc,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CACpD,CAAC,MAAM,KAAK,iBAAiB,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC"}
package/dist/ssrf.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * SSRF guards for any Latch host that opens an outbound connection to a
3
+ * config- or agent-supplied URL (MCP servers, OpenAPI backends, the renderer's
4
+ * fetch service). A URL could point at internal/cloud-metadata addresses, so
5
+ * there are two layers: a string-level blocklist for encoded/literal private
6
+ * hosts, and a DNS-resolution backstop for public names that resolve private.
7
+ *
8
+ * These are best-effort and deliberately conservative (they err toward
9
+ * blocking). They complement, but do not replace, a positive allowlist — see
10
+ * `@intentface/latch-mcp`'s per-tenant `allowedHosts`.
11
+ *
12
+ * These checks validate at CHECK time, not CONNECT time — on their own they
13
+ * do not close DNS rebinding (a name that resolves public here but private
14
+ * when the request opens). For that, route the request through the pinned
15
+ * fetch in `pinned-fetch.ts` (`@intentface/latch-net/pinned`), which vets
16
+ * addresses inside the lookup the connection itself uses; the checks here
17
+ * remain useful as early, well-messaged pre-flight gates.
18
+ */
19
+ /** True if a dotted-decimal IPv4 (or a short form like 127.1) is private/loopback/link-local. */
20
+ export declare function isPrivateV4(ip: string): boolean;
21
+ /**
22
+ * Parse an IPv6 literal into its 16 bytes — compressed (`::1`), fully expanded
23
+ * (`0:0:0:0:0:0:0:1`), or with an embedded IPv4 tail (`::ffff:127.0.0.1`).
24
+ * Returns null for anything that isn't a valid IPv6 literal. One parser so every
25
+ * spelling (not just `::`-prefixed hex) is classified the same way.
26
+ */
27
+ export declare function parseIPv6(input: string): number[] | null;
28
+ /**
29
+ * True if a 16-byte IPv6 address is unspecified/loopback/ULA/link-local, or an
30
+ * IPv4-mapped/compatible address whose embedded IPv4 is private.
31
+ */
32
+ export declare function isPrivateV6(b: number[]): boolean;
33
+ /**
34
+ * String-level blocklist: trailing-dot, bracketed IPv6 (loopback/ULA/link-local
35
+ * + IPv4-mapped/compatible, dotted or hex), bare integer / hex encodings of
36
+ * IPv4 (2130706433, 0x7f000001), short dotted forms (127.1). Best-effort and
37
+ * conservative — strictly more blocking. `isBlockedResolved` backs this up to
38
+ * defeat DNS rebinding.
39
+ */
40
+ export declare function isBlockedHost(host: string): boolean;
41
+ /**
42
+ * DNS-rebinding backstop: resolve the hostname and reject if ANY resolved
43
+ * address is private/loopback/link-local. The string check above cannot catch
44
+ * `http://public-name/` that resolves to 169.254.169.254 — this does.
45
+ */
46
+ export declare function isBlockedResolved(hostname: string): Promise<boolean>;
47
+ /**
48
+ * Convenience for callers that want one hard gate: parse `input`, then reject a
49
+ * private/internal literal (`isBlockedHost`) or a name that resolves private
50
+ * (`isBlockedResolved`). Returns the parsed `URL` on success; throws otherwise.
51
+ */
52
+ export declare function assertPublicUrl(input: string | URL): Promise<URL>;
53
+ //# sourceMappingURL=ssrf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssrf.d.ts","sourceRoot":"","sources":["../src/ssrf.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;GAiBG;AAEH,iGAAiG;AACjG,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAa/C;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAgCxD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAUhD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAanD;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa1E;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAYvE"}
package/dist/ssrf.js ADDED
@@ -0,0 +1,173 @@
1
+ import { lookup } from "node:dns/promises";
2
+ import { isIP } from "node:net";
3
+ /**
4
+ * SSRF guards for any Latch host that opens an outbound connection to a
5
+ * config- or agent-supplied URL (MCP servers, OpenAPI backends, the renderer's
6
+ * fetch service). A URL could point at internal/cloud-metadata addresses, so
7
+ * there are two layers: a string-level blocklist for encoded/literal private
8
+ * hosts, and a DNS-resolution backstop for public names that resolve private.
9
+ *
10
+ * These are best-effort and deliberately conservative (they err toward
11
+ * blocking). They complement, but do not replace, a positive allowlist — see
12
+ * `@intentface/latch-mcp`'s per-tenant `allowedHosts`.
13
+ *
14
+ * These checks validate at CHECK time, not CONNECT time — on their own they
15
+ * do not close DNS rebinding (a name that resolves public here but private
16
+ * when the request opens). For that, route the request through the pinned
17
+ * fetch in `pinned-fetch.ts` (`@intentface/latch-net/pinned`), which vets
18
+ * addresses inside the lookup the connection itself uses; the checks here
19
+ * remain useful as early, well-messaged pre-flight gates.
20
+ */
21
+ /** True if a dotted-decimal IPv4 (or a short form like 127.1) is private/loopback/link-local. */
22
+ export function isPrivateV4(ip) {
23
+ const parts = ip.split(".");
24
+ if (parts.length < 2 || parts.length > 4 || !parts.every((p) => /^\d{1,3}$/.test(p)))
25
+ return false;
26
+ const a = Number(parts[0]);
27
+ const b = parts.length >= 2 ? Number(parts[1]) : 0;
28
+ if (a === 127 || a === 10 || a === 0)
29
+ return true; // loopback, private, "this host"
30
+ if (a === 192 && b === 168)
31
+ return true;
32
+ if (a === 169 && b === 254)
33
+ return true; // link-local incl. 169.254.169.254 (cloud metadata)
34
+ if (a === 172 && b >= 16 && b <= 31)
35
+ return true;
36
+ if (a === 100 && b >= 64 && b <= 127)
37
+ return true; // CGNAT 100.64.0.0/10 (carrier / internal)
38
+ if (a === 198 && (b === 18 || b === 19))
39
+ return true; // benchmarking 198.18.0.0/15
40
+ if (a >= 224)
41
+ return true; // multicast 224/4 + reserved/broadcast 240/4 (incl. 255.255.255.255)
42
+ return false;
43
+ }
44
+ /**
45
+ * Parse an IPv6 literal into its 16 bytes — compressed (`::1`), fully expanded
46
+ * (`0:0:0:0:0:0:0:1`), or with an embedded IPv4 tail (`::ffff:127.0.0.1`).
47
+ * Returns null for anything that isn't a valid IPv6 literal. One parser so every
48
+ * spelling (not just `::`-prefixed hex) is classified the same way.
49
+ */
50
+ export function parseIPv6(input) {
51
+ let h = input.toLowerCase().split("%")[0]; // drop any zone id
52
+ // Rewrite an embedded IPv4 tail (::ffff:127.0.0.1) as its two hextets so the
53
+ // rest parses uniformly as hex groups.
54
+ const dotted = h.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
55
+ if (dotted) {
56
+ const o = dotted[1].split(".").map(Number);
57
+ if (o.some((x) => x > 255))
58
+ return null;
59
+ const hextets = `${(((o[0] << 8) | o[1]) >>> 0).toString(16)}:${(((o[2] << 8) | o[3]) >>> 0).toString(16)}`;
60
+ h = h.slice(0, h.length - dotted[1].length) + hextets;
61
+ }
62
+ const halves = h.split("::");
63
+ if (halves.length > 2)
64
+ return null;
65
+ const toBytes = (s) => {
66
+ if (s === "")
67
+ return [];
68
+ const out = [];
69
+ for (const g of s.split(":")) {
70
+ if (!/^[0-9a-f]{1,4}$/.test(g))
71
+ return null;
72
+ const n = Number.parseInt(g, 16);
73
+ out.push((n >> 8) & 255, n & 255);
74
+ }
75
+ return out;
76
+ };
77
+ const head = toBytes(halves[0]);
78
+ const rest = toBytes(halves[1] ?? "");
79
+ if (!head || !rest)
80
+ return null;
81
+ const known = head.length + rest.length;
82
+ if (halves.length === 2) {
83
+ const gap = 16 - known; // "::" expands to this many zero bytes
84
+ return gap < 0 ? null : [...head, ...Array(gap).fill(0), ...rest];
85
+ }
86
+ return known === 16 ? [...head, ...rest] : null;
87
+ }
88
+ /**
89
+ * True if a 16-byte IPv6 address is unspecified/loopback/ULA/link-local, or an
90
+ * IPv4-mapped/compatible address whose embedded IPv4 is private.
91
+ */
92
+ export function isPrivateV6(b) {
93
+ if (b.every((x) => x === 0))
94
+ return true; // :: unspecified
95
+ if (b.slice(0, 15).every((x) => x === 0) && b[15] === 1)
96
+ return true; // ::1 loopback
97
+ // IPv4-mapped ::ffff:0:0/96 and IPv4-compatible ::/96 → classify embedded v4.
98
+ const mapped = b.slice(0, 10).every((x) => x === 0) && b[10] === 0xff && b[11] === 0xff;
99
+ const compat = b.slice(0, 12).every((x) => x === 0);
100
+ if (mapped || compat)
101
+ return isPrivateV4(b.slice(12).join("."));
102
+ if ((b[0] & 0xfe) === 0xfc)
103
+ return true; // ULA fc00::/7
104
+ if (b[0] === 0xfe && (b[1] & 0xc0) === 0x80)
105
+ return true; // link-local fe80::/10
106
+ return false;
107
+ }
108
+ /**
109
+ * String-level blocklist: trailing-dot, bracketed IPv6 (loopback/ULA/link-local
110
+ * + IPv4-mapped/compatible, dotted or hex), bare integer / hex encodings of
111
+ * IPv4 (2130706433, 0x7f000001), short dotted forms (127.1). Best-effort and
112
+ * conservative — strictly more blocking. `isBlockedResolved` backs this up to
113
+ * defeat DNS rebinding.
114
+ */
115
+ export function isBlockedHost(host) {
116
+ let h = host.toLowerCase().replace(/\.$/, "");
117
+ if (h.startsWith("[") && h.endsWith("]"))
118
+ h = h.slice(1, -1); // unwrap IPv6 literal
119
+ // `.localhost` subdomains are reserved to resolve loopback (RFC 6761).
120
+ if (h === "localhost" || h.endsWith(".localhost") || h.endsWith(".local") || h === "::1" || h === "::")
121
+ return true;
122
+ if (h.includes(":")) {
123
+ // Parse the full IPv6 literal (any spelling) and classify its bytes. A
124
+ // non-parseable colon-host falls through to false (names hit the resolver).
125
+ const bytes = parseIPv6(h);
126
+ return bytes ? isPrivateV6(bytes) : false;
127
+ }
128
+ if (/^\d+$/.test(h) || /^0x[0-9a-f]+$/.test(h))
129
+ return true; // bare int / hex = encoded IPv4
130
+ return isPrivateV4(h);
131
+ }
132
+ /**
133
+ * DNS-rebinding backstop: resolve the hostname and reject if ANY resolved
134
+ * address is private/loopback/link-local. The string check above cannot catch
135
+ * `http://public-name/` that resolves to 169.254.169.254 — this does.
136
+ */
137
+ export async function isBlockedResolved(hostname) {
138
+ if (isIP(hostname))
139
+ return false; // literal IPs already handled by isBlockedHost
140
+ try {
141
+ const addrs = await lookup(hostname, { all: true });
142
+ return addrs.some(({ address, family }) => {
143
+ if (family === 4)
144
+ return isPrivateV4(address);
145
+ const bytes = parseIPv6(address); // same classifier as the literal path
146
+ return bytes ? isPrivateV6(bytes) : false;
147
+ });
148
+ }
149
+ catch {
150
+ return true; // fail closed — a lookup error must not open the gate (a
151
+ // rebinding/flaky resolver could otherwise slip a private target through)
152
+ }
153
+ }
154
+ /**
155
+ * Convenience for callers that want one hard gate: parse `input`, then reject a
156
+ * private/internal literal (`isBlockedHost`) or a name that resolves private
157
+ * (`isBlockedResolved`). Returns the parsed `URL` on success; throws otherwise.
158
+ */
159
+ export async function assertPublicUrl(input) {
160
+ let url;
161
+ try {
162
+ url = typeof input === "string" ? new URL(input) : input;
163
+ }
164
+ catch {
165
+ throw new Error(`Invalid URL: ${String(input)}`);
166
+ }
167
+ const host = url.hostname;
168
+ if (isBlockedHost(host) || (await isBlockedResolved(host))) {
169
+ throw new Error(`Blocked private/internal address: ${host}`);
170
+ }
171
+ return url;
172
+ }
173
+ //# sourceMappingURL=ssrf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssrf.js","sourceRoot":"","sources":["../src/ssrf.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@intentface/latch-net",
3
+ "version": "0.9.1",
4
+ "description": "Network-safety primitives for Latch — SSRF guards (private-IP blocklist + DNS-rebinding backstop) and a DNS-pinned egress fetch, shared by every host that opens an outbound connection.",
5
+ "keywords": [
6
+ "ai",
7
+ "ai-sdk",
8
+ "agents",
9
+ "llm",
10
+ "ssrf",
11
+ "fetch",
12
+ "security"
13
+ ],
14
+ "license": "MIT",
15
+ "type": "module",
16
+ "main": "./dist/index.js",
17
+ "module": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ },
24
+ "./pinned": {
25
+ "types": "./dist/pinned-fetch.d.ts",
26
+ "import": "./dist/pinned-fetch.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "sideEffects": false,
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "registry": "https://registry.npmjs.org"
36
+ },
37
+ "engines": {
38
+ "node": ">=20.18.1"
39
+ },
40
+ "dependencies": {
41
+ "undici": "^7.28.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22",
45
+ "typescript": "^5.7.0",
46
+ "vitest": "^4"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/Intentface/intentface-latch.git",
51
+ "directory": "packages/net"
52
+ },
53
+ "scripts": {
54
+ "build": "tsc -p tsconfig.json",
55
+ "typecheck": "tsc -p tsconfig.json --noEmit",
56
+ "test": "vitest run"
57
+ }
58
+ }