@atproto-labs/fetch-node 0.1.10 → 0.3.0-next.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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @atproto-labs/fetch-node
2
2
 
3
+ ## 0.3.0-next.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#4929](https://github.com/bluesky-social/atproto/pull/4929) [`bb7491c`](https://github.com/bluesky-social/atproto/commit/bb7491c29e06181e1d2f8cf6eb454f9bb8ab961b) Thanks [@devinivy](https://github.com/devinivy)! - **BREAKING:** Drop support for Node.js 18 and 20. Node.js 22 is now the minimum supported version. Docker images now use Node.js 24.
8
+
9
+ - [#4943](https://github.com/bluesky-social/atproto/pull/4943) [`07ae5d4`](https://github.com/bluesky-social/atproto/commit/07ae5d4452df51e045e0239da7a04cf0bc154028) Thanks [@devinivy](https://github.com/devinivy)! - **BREAKING:** Convert to pure ESM. All packages now ship `"type": "module"` with ES module output and Node16 module resolution.
10
+
11
+ Node.js 22's `require()` compatibility layer can still load these packages in CommonJS code.
12
+
13
+ - [#4943](https://github.com/bluesky-social/atproto/pull/4943) [`8e40db6`](https://github.com/bluesky-social/atproto/commit/8e40db65aab7ba5f9414c26dbe1ab658e3bffeca) Thanks [@devinivy](https://github.com/devinivy)! - **BREAKING:** `unicastFetchWrap` and `safeFetchWrap` no longer accept `dangerouslyForceKeepAliveAgent` — on Node.js 22+ the keep-alive dispatcher is always used via `new Request(input, { dispatcher })`, so the option was a no-op.
14
+
15
+ - [#4930](https://github.com/bluesky-social/atproto/pull/4930) [`042df15`](https://github.com/bluesky-social/atproto/commit/042df15087c0e62cd1e715fcbf58852fab875af9) Thanks [@devinivy](https://github.com/devinivy)! - Build with TypeScript 6.0. Emitted `.d.ts` files now use TypeScript 6's stricter `Uint8Array<ArrayBuffer>` typing in places where Web/Node APIs require buffer-backed (not shared-memory) byte arrays. Consumers compiling against these types on older TypeScript should see no runtime impact, but may need to widen or cast in spots that previously relied on `Uint8Array` defaulting to `<ArrayBufferLike>`.
16
+
17
+ Internal: tsconfig `moduleResolution: "node"` is silenced via `ignoreDeprecations: "6.0"` for now; the proper migration to `node16`/`bundler` resolution is deferred.
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies [[`bb7491c`](https://github.com/bluesky-social/atproto/commit/bb7491c29e06181e1d2f8cf6eb454f9bb8ab961b), [`07ae5d4`](https://github.com/bluesky-social/atproto/commit/07ae5d4452df51e045e0239da7a04cf0bc154028), [`042df15`](https://github.com/bluesky-social/atproto/commit/042df15087c0e62cd1e715fcbf58852fab875af9)]:
22
+ - @atproto-labs/fetch@0.3.0-next.0
23
+ - @atproto-labs/pipe@0.2.0-next.0
24
+
25
+ ## 0.2.0
26
+
27
+ ### Minor Changes
28
+
29
+ - [#4289](https://github.com/bluesky-social/atproto/pull/4289) [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `isLocalHostname` export
30
+
3
31
  ## 0.1.10
4
32
 
5
33
  ### Patch Changes
package/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  Dual MIT/Apache-2.0 License
2
2
 
3
- Copyright (c) 2022-2025 Bluesky Social PBC, and Contributors
3
+ Copyright (c) 2022-2026 Bluesky Social PBC, and Contributors
4
4
 
5
5
  Except as otherwise noted in individual files, this software is licensed under the MIT license (<http://opensource.org/licenses/MIT>), or the Apache License, Version 2.0 (<http://www.apache.org/licenses/LICENSE-2.0>).
6
6
 
package/dist/index.js CHANGED
@@ -1,21 +1,5 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
- };
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("@atproto-labs/fetch"), exports);
18
- __exportStar(require("./safe.js"), exports);
19
- __exportStar(require("./unicast.js"), exports);
20
- __exportStar(require("./util.js"), exports);
1
+ export * from '@atproto-labs/fetch';
2
+ export * from './safe.js';
3
+ export * from './unicast.js';
4
+ export * from './util.js';
21
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,sDAAmC;AAEnC,4CAAyB;AACzB,+CAA4B;AAC5B,4CAAyB"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AAEnC,cAAc,WAAW,CAAA;AACzB,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA","sourcesContent":["export * from '@atproto-labs/fetch'\n\nexport * from './safe.js'\nexport * from './unicast.js'\nexport * from './util.js'\n"]}
package/dist/safe.d.ts CHANGED
@@ -36,6 +36,6 @@ export type SafeFetchWrapOptions<C> = UnicastFetchWrapOptions<C> & {
36
36
  * {@link Request} object before passing it to the function (as a e.g. a logging
37
37
  * function would).
38
38
  */
39
- export declare function safeFetchWrap<C>({ fetch, dangerouslyForceKeepAliveAgent, responseMaxSize, // 512kB
39
+ export declare function safeFetchWrap<C>({ fetch, responseMaxSize, // 512kB
40
40
  ssrfProtection, allowCustomPort, allowData, allowHttp, allowIpHost, allowPrivateIps, timeout, forbiddenDomainNames, allowImplicitRedirect, }?: SafeFetchWrapOptions<C>): (input: string | URL | Request, init?: RequestInit | undefined) => Promise<Response>;
41
41
  //# sourceMappingURL=safe.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"safe.d.ts","sourceRoot":"","sources":["../src/safe.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,uBAAuB,EAAoB,MAAM,cAAc,CAAA;AAExE,MAAM,MAAM,oBAAoB,CAAC,CAAC,IAAI,uBAAuB,CAAC,CAAC,CAAC,GAAG;IACjE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,oBAAoB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IACvC;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,EAC/B,KAAoC,EACpC,8BAAsC,EACtC,eAA4B,EAAE,QAAQ;AACtC,cAAqB,EACrB,eAAiC,EACjC,SAAiB,EACjB,SAA2B,EAC3B,WAAkB,EAClB,eAAiC,EACjC,OAAc,EACd,oBAAyE,EACzE,qBAA6B,GAC9B,GAAE,oBAAoB,CAAC,CAAC,CAAM,wFAsD9B"}
1
+ {"version":3,"file":"safe.d.ts","sourceRoot":"","sources":["../src/safe.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,uBAAuB,EAAoB,MAAM,cAAc,CAAA;AAExE,MAAM,MAAM,oBAAoB,CAAC,CAAC,IAAI,uBAAuB,CAAC,CAAC,CAAC,GAAG;IACjE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,oBAAoB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IACvC;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,EAC/B,KAAoC,EACpC,eAA4B,EAAE,QAAQ;AACtC,cAAqB,EACrB,eAAiC,EACjC,SAAiB,EACjB,SAA2B,EAC3B,WAAkB,EAClB,eAAiC,EACjC,OAAc,EACd,oBAAyE,EACzE,qBAA6B,GAC9B,GAAE,oBAAoB,CAAC,CAAC,CAAM,wFAoD9B"}
package/dist/safe.js CHANGED
@@ -1,9 +1,6 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.safeFetchWrap = safeFetchWrap;
4
- const fetch_1 = require("@atproto-labs/fetch");
5
- const pipe_1 = require("@atproto-labs/pipe");
6
- const unicast_js_1 = require("./unicast.js");
1
+ import { DEFAULT_FORBIDDEN_DOMAIN_NAMES, asRequest, explicitRedirectCheckRequestTransform, fetchMaxSizeProcessor, forbiddenDomainNameRequestTransform, protocolCheckRequestTransform, requireHostHeaderTransform, timedFetch, } from '@atproto-labs/fetch';
2
+ import { pipe } from '@atproto-labs/pipe';
3
+ import { unicastFetchWrap } from './unicast.js';
7
4
  /**
8
5
  * Wrap a fetch function with safety checks so that it can be safely used
9
6
  * with user provided input (URL).
@@ -23,21 +20,21 @@ const unicast_js_1 = require("./unicast.js");
23
20
  * {@link Request} object before passing it to the function (as a e.g. a logging
24
21
  * function would).
25
22
  */
26
- function safeFetchWrap({ fetch = globalThis.fetch, dangerouslyForceKeepAliveAgent = false, responseMaxSize = 512 * 1024, // 512kB
27
- ssrfProtection = true, allowCustomPort = !ssrfProtection, allowData = false, allowHttp = !ssrfProtection, allowIpHost = true, allowPrivateIps = !ssrfProtection, timeout = 10e3, forbiddenDomainNames = fetch_1.DEFAULT_FORBIDDEN_DOMAIN_NAMES, allowImplicitRedirect = false, } = {}) {
28
- return (0, pipe_1.pipe)(
23
+ export function safeFetchWrap({ fetch = globalThis.fetch, responseMaxSize = 512 * 1024, // 512kB
24
+ ssrfProtection = true, allowCustomPort = !ssrfProtection, allowData = false, allowHttp = !ssrfProtection, allowIpHost = true, allowPrivateIps = !ssrfProtection, timeout = 10e3, forbiddenDomainNames = DEFAULT_FORBIDDEN_DOMAIN_NAMES, allowImplicitRedirect = false, } = {}) {
25
+ return pipe(
29
26
  /**
30
27
  * Require explicit {@link RequestInit['redirect']} mode
31
28
  */
32
- allowImplicitRedirect ? fetch_1.asRequest : (0, fetch_1.explicitRedirectCheckRequestTransform)(),
29
+ allowImplicitRedirect ? asRequest : explicitRedirectCheckRequestTransform(),
33
30
  /**
34
31
  * Only requests that will be issued with a "Host" header are allowed.
35
32
  */
36
- allowIpHost ? fetch_1.asRequest : (0, fetch_1.requireHostHeaderTransform)(),
33
+ allowIpHost ? asRequest : requireHostHeaderTransform(),
37
34
  /**
38
35
  * Prevent using http:, file: or data: protocols.
39
36
  */
40
- (0, fetch_1.protocolCheckRequestTransform)({
37
+ protocolCheckRequestTransform({
41
38
  'about:': false,
42
39
  'data:': allowData,
43
40
  'file:': false,
@@ -50,24 +47,22 @@ ssrfProtection = true, allowCustomPort = !ssrfProtection, allowData = false, all
50
47
  * custom fetch function combined with another
51
48
  * forbiddenDomainNameRequestTransform.
52
49
  */
53
- (0, fetch_1.forbiddenDomainNameRequestTransform)(forbiddenDomainNames),
50
+ forbiddenDomainNameRequestTransform(forbiddenDomainNames),
54
51
  /**
55
52
  * Since we will be fetching from the network based on user provided
56
53
  * input, let's mitigate resource exhaustion attacks by setting a timeout.
57
54
  */
58
- (0, fetch_1.timedFetch)(timeout,
55
+ timedFetch(timeout,
59
56
  /**
60
57
  * Since we will be fetching from the network based on user provided
61
58
  * input, we need to make sure that the request is not vulnerable to SSRF
62
59
  * attacks.
63
60
  */
64
- allowPrivateIps
65
- ? fetch
66
- : (0, unicast_js_1.unicastFetchWrap)({ fetch, dangerouslyForceKeepAliveAgent })),
61
+ allowPrivateIps ? fetch : unicastFetchWrap({ fetch })),
67
62
  /**
68
63
  * Since we will be fetching user owned data, we need to make sure that an
69
64
  * attacker cannot force us to download a large amounts of data.
70
65
  */
71
- (0, fetch_1.fetchMaxSizeProcessor)(responseMaxSize));
66
+ fetchMaxSizeProcessor(responseMaxSize));
72
67
  }
73
68
  //# sourceMappingURL=safe.js.map
package/dist/safe.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"safe.js","sourceRoot":"","sources":["../src/safe.ts"],"names":[],"mappings":";;AAoDA,sCAmEC;AAvHD,+CAU4B;AAC5B,6CAAyC;AACzC,6CAAwE;AAqBxE;;;;;;;;;;;;;;;;;;GAkBG;AACH,SAAgB,aAAa,CAAI,EAC/B,KAAK,GAAG,UAAU,CAAC,KAAiB,EACpC,8BAA8B,GAAG,KAAK,EACtC,eAAe,GAAG,GAAG,GAAG,IAAI,EAAE,QAAQ;AACtC,cAAc,GAAG,IAAI,EACrB,eAAe,GAAG,CAAC,cAAc,EACjC,SAAS,GAAG,KAAK,EACjB,SAAS,GAAG,CAAC,cAAc,EAC3B,WAAW,GAAG,IAAI,EAClB,eAAe,GAAG,CAAC,cAAc,EACjC,OAAO,GAAG,IAAI,EACd,oBAAoB,GAAG,sCAAkD,EACzE,qBAAqB,GAAG,KAAK,MACF,EAAE;IAC7B,OAAO,IAAA,WAAI;IACT;;OAEG;IACH,qBAAqB,CAAC,CAAC,CAAC,iBAAS,CAAC,CAAC,CAAC,IAAA,6CAAqC,GAAE;IAE3E;;OAEG;IACH,WAAW,CAAC,CAAC,CAAC,iBAAS,CAAC,CAAC,CAAC,IAAA,kCAA0B,GAAE;IAEtD;;OAEG;IACH,IAAA,qCAA6B,EAAC;QAC5B,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,SAAS;QAClB,OAAO,EAAE,KAAK;QACd,OAAO,EAAE,SAAS,IAAI,EAAE,eAAe,EAAE;QACzC,QAAQ,EAAE,EAAE,eAAe,EAAE;KAC9B,CAAC;IAEF;;;;;OAKG;IACH,IAAA,2CAAmC,EAAC,oBAAoB,CAAC;IAEzD;;;OAGG;IACH,IAAA,kBAAU,EACR,OAAO;IAEP;;;;OAIG;IACH,eAAe;QACb,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,IAAA,6BAAgB,EAAC,EAAE,KAAK,EAAE,8BAA8B,EAAE,CAAC,CAChE;IAED;;;OAGG;IACH,IAAA,6BAAqB,EAAC,eAAe,CAAC,CACd,CAAA;AAC5B,CAAC"}
1
+ {"version":3,"file":"safe.js","sourceRoot":"","sources":["../src/safe.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,8BAA8B,EAE9B,SAAS,EACT,qCAAqC,EACrC,qBAAqB,EACrB,mCAAmC,EACnC,6BAA6B,EAC7B,0BAA0B,EAC1B,UAAU,GACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAA;AACzC,OAAO,EAA2B,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAqBxE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa,CAAI,EAC/B,KAAK,GAAG,UAAU,CAAC,KAAiB,EACpC,eAAe,GAAG,GAAG,GAAG,IAAI,EAAE,QAAQ;AACtC,cAAc,GAAG,IAAI,EACrB,eAAe,GAAG,CAAC,cAAc,EACjC,SAAS,GAAG,KAAK,EACjB,SAAS,GAAG,CAAC,cAAc,EAC3B,WAAW,GAAG,IAAI,EAClB,eAAe,GAAG,CAAC,cAAc,EACjC,OAAO,GAAG,IAAI,EACd,oBAAoB,GAAG,8BAAkD,EACzE,qBAAqB,GAAG,KAAK,MACF,EAAE;IAC7B,OAAO,IAAI;IACT;;OAEG;IACH,qBAAqB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,qCAAqC,EAAE;IAE3E;;OAEG;IACH,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,0BAA0B,EAAE;IAEtD;;OAEG;IACH,6BAA6B,CAAC;QAC5B,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,SAAS;QAClB,OAAO,EAAE,KAAK;QACd,OAAO,EAAE,SAAS,IAAI,EAAE,eAAe,EAAE;QACzC,QAAQ,EAAE,EAAE,eAAe,EAAE;KAC9B,CAAC;IAEF;;;;;OAKG;IACH,mCAAmC,CAAC,oBAAoB,CAAC;IAEzD;;;OAGG;IACH,UAAU,CACR,OAAO;IAEP;;;;OAIG;IACH,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,gBAAgB,CAAC,EAAE,KAAK,EAAE,CAAC,CACtD;IAED;;;OAGG;IACH,qBAAqB,CAAC,eAAe,CAAC,CACd,CAAA;AAC5B,CAAC","sourcesContent":["import {\n DEFAULT_FORBIDDEN_DOMAIN_NAMES,\n Fetch,\n asRequest,\n explicitRedirectCheckRequestTransform,\n fetchMaxSizeProcessor,\n forbiddenDomainNameRequestTransform,\n protocolCheckRequestTransform,\n requireHostHeaderTransform,\n timedFetch,\n} from '@atproto-labs/fetch'\nimport { pipe } from '@atproto-labs/pipe'\nimport { UnicastFetchWrapOptions, unicastFetchWrap } from './unicast.js'\n\nexport type SafeFetchWrapOptions<C> = UnicastFetchWrapOptions<C> & {\n responseMaxSize?: number\n ssrfProtection?: boolean\n allowCustomPort?: boolean\n allowData?: boolean\n allowHttp?: boolean\n allowIpHost?: boolean\n allowPrivateIps?: boolean\n timeout?: number\n forbiddenDomainNames?: Iterable<string>\n /**\n * When `false`, a {@link RequestInit['redirect']} value must be explicitly\n * provided as second argument to the returned function or requests will fail.\n *\n * @default false\n */\n allowImplicitRedirect?: boolean\n}\n\n/**\n * Wrap a fetch function with safety checks so that it can be safely used\n * with user provided input (URL).\n *\n * @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html}\n *\n * @note When {@link SafeFetchWrapOptions.allowImplicitRedirect} is `false`\n * (default), then the returned function **must** be called setting the second\n * argument's `redirect` property to one of the allowed values. Otherwise, if\n * the returned fetch function is called with a `Request` object (and no\n * explicit `redirect` init object), then the verification code will not be able\n * to determine if the `redirect` property was explicitly set or based on the\n * default value (`follow`), causing it to preventively block the request (throw\n * an error). For this reason, unless you set\n * {@link SafeFetchWrapOptions.allowImplicitRedirect} to `true`, you should\n * **not** wrap the returned function into another function that creates a\n * {@link Request} object before passing it to the function (as a e.g. a logging\n * function would).\n */\nexport function safeFetchWrap<C>({\n fetch = globalThis.fetch as Fetch<C>,\n responseMaxSize = 512 * 1024, // 512kB\n ssrfProtection = true,\n allowCustomPort = !ssrfProtection,\n allowData = false,\n allowHttp = !ssrfProtection,\n allowIpHost = true,\n allowPrivateIps = !ssrfProtection,\n timeout = 10e3,\n forbiddenDomainNames = DEFAULT_FORBIDDEN_DOMAIN_NAMES as Iterable<string>,\n allowImplicitRedirect = false,\n}: SafeFetchWrapOptions<C> = {}) {\n return pipe(\n /**\n * Require explicit {@link RequestInit['redirect']} mode\n */\n allowImplicitRedirect ? asRequest : explicitRedirectCheckRequestTransform(),\n\n /**\n * Only requests that will be issued with a \"Host\" header are allowed.\n */\n allowIpHost ? asRequest : requireHostHeaderTransform(),\n\n /**\n * Prevent using http:, file: or data: protocols.\n */\n protocolCheckRequestTransform({\n 'about:': false,\n 'data:': allowData,\n 'file:': false,\n 'http:': allowHttp && { allowCustomPort },\n 'https:': { allowCustomPort },\n }),\n\n /**\n * Disallow fetching from domains we know are not atproto/OIDC client\n * implementation. Note that other domains can be blocked by providing a\n * custom fetch function combined with another\n * forbiddenDomainNameRequestTransform.\n */\n forbiddenDomainNameRequestTransform(forbiddenDomainNames),\n\n /**\n * Since we will be fetching from the network based on user provided\n * input, let's mitigate resource exhaustion attacks by setting a timeout.\n */\n timedFetch(\n timeout,\n\n /**\n * Since we will be fetching from the network based on user provided\n * input, we need to make sure that the request is not vulnerable to SSRF\n * attacks.\n */\n allowPrivateIps ? fetch : unicastFetchWrap({ fetch }),\n ),\n\n /**\n * Since we will be fetching user owned data, we need to make sure that an\n * attacker cannot force us to download a large amounts of data.\n */\n fetchMaxSizeProcessor(responseMaxSize),\n ) satisfies Fetch<unknown>\n}\n"]}
package/dist/unicast.d.ts CHANGED
@@ -3,44 +3,10 @@ import { LookupFunction } from 'node:net';
3
3
  import { Fetch, FetchContext } from '@atproto-labs/fetch';
4
4
  export type UnicastFetchWrapOptions<C = FetchContext> = {
5
5
  fetch?: Fetch<C>;
6
- /**
7
- * ## ‼️ important security feature use with care
8
- *
9
- * On older NodeJS version, the `dispatcher` init option is ignored when
10
- * creating a new Request instance. It can only be passed through the fetch
11
- * function directly.
12
- *
13
- * Since this is a security feature, we need to ensure that the unicastLookup
14
- * function is called to resolve the hostname to a unicast IP address.
15
- *
16
- * However, in the case a custom "fetch" function is passed here (fetch !==
17
- * globalThis.fetch), we have no guarantee that the dispatcher will be used to
18
- * make the request. Because of this, in such a case, we will use a one-time
19
- * use dispatcher that checks that the provided fetch function indeed made use
20
- * of the "unicastLookup" when a custom dispatch init function is used.
21
- *
22
- * Sadly, this means that we cannot use "keepAlive" connections, as the method
23
- * used to ensure that "unicastLookup" gets called requires to create a new
24
- * dispatcher for each request.
25
- *
26
- * If you can guarantee that the provided fetch function will make use of the
27
- * "dispatcher" init option, you can set this flag to true, which will enable
28
- * the use of a single agent (with keep-alive) for all requests.
29
- *
30
- * @default false
31
- * @note This option has no effect on Node.js versions >= 20
32
- */
33
- dangerouslyForceKeepAliveAgent?: boolean;
34
6
  };
35
7
  /**
36
8
  * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}
37
9
  */
38
- export declare function unicastFetchWrap<C = FetchContext>({ fetch, dangerouslyForceKeepAliveAgent, }: UnicastFetchWrapOptions<C>): Fetch<C>;
10
+ export declare function unicastFetchWrap<C = FetchContext>({ fetch, }: UnicastFetchWrapOptions<C>): Fetch<C>;
39
11
  export declare function unicastLookup(hostname: string, options: dns.LookupOptions, callback: Parameters<LookupFunction>[2]): void;
40
- /**
41
- * @param hostname - a syntactically valid hostname
42
- * @returns whether the hostname is a name typically used for on locale area networks.
43
- * @note **DO NOT** use for security reasons. Only as heuristic.
44
- */
45
- export declare function isLocalHostname(hostname: string): boolean;
46
12
  //# sourceMappingURL=unicast.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"unicast.d.ts","sourceRoot":"","sources":["../src/unicast.ts"],"names":[],"mappings":"AAAA,OAAO,GAAsB,MAAM,UAAU,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAGzC,OAAO,EACL,KAAK,EACL,YAAY,EAIb,MAAM,qBAAqB,CAAA;AAK5B,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,YAAY,IAAI;IACtD,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;IAEhB;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,8BAA8B,CAAC,EAAE,OAAO,CAAA;CACzC,CAAA;AAMD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,GAAG,YAAY,EAAE,EACjD,KAAwB,EACxB,8BAAsC,GACvC,EAAE,uBAAuB,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAuIvC;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,GAAG,CAAC,aAAa,EAC1B,QAAQ,EAAE,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,QA0BxC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAYzD"}
1
+ {"version":3,"file":"unicast.d.ts","sourceRoot":"","sources":["../src/unicast.ts"],"names":[],"mappings":"AAAA,OAAO,GAAsB,MAAM,UAAU,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAGzC,OAAO,EACL,KAAK,EACL,YAAY,EAIb,MAAM,qBAAqB,CAAA;AAK5B,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,YAAY,IAAI;IACtD,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;CACjB,CAAA;AAKD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,GAAG,YAAY,EAAE,EACjD,KAAwB,GACzB,EAAE,uBAAuB,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAkCvC;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,GAAG,CAAC,aAAa,EAC1B,QAAQ,EAAE,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,QA0BxC"}
package/dist/unicast.js CHANGED
@@ -1,129 +1,39 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.unicastFetchWrap = unicastFetchWrap;
7
- exports.unicastLookup = unicastLookup;
8
- exports.isLocalHostname = isLocalHostname;
9
- const node_dns_1 = __importDefault(require("node:dns"));
10
- const ipaddr_js_1 = __importDefault(require("ipaddr.js"));
11
- const undici_1 = require("undici");
12
- const fetch_1 = require("@atproto-labs/fetch");
13
- const util_js_1 = require("./util.js");
14
- const { IPv4, IPv6 } = ipaddr_js_1.default;
15
- // @TODO support other runtimes ?
1
+ import dns from 'node:dns';
2
+ import ipaddr from 'ipaddr.js';
3
+ import { Agent } from 'undici';
4
+ import { FetchRequestError, asRequest, extractUrl, } from '@atproto-labs/fetch';
5
+ import { isUnicastIp } from './util.js';
6
+ const { IPv4, IPv6 } = ipaddr;
16
7
  const SUPPORTS_REQUEST_INIT_DISPATCHER = Number(process.versions.node.split('.')[0]) >= 20;
17
8
  /**
18
9
  * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}
19
10
  */
20
- function unicastFetchWrap({ fetch = globalThis.fetch, dangerouslyForceKeepAliveAgent = false, }) {
21
- if (SUPPORTS_REQUEST_INIT_DISPATCHER ||
22
- dangerouslyForceKeepAliveAgent ||
23
- fetch === globalThis.fetch) {
24
- const dispatcher = new undici_1.Agent({
25
- connect: { lookup: unicastLookup },
26
- });
27
- return async function (input, init) {
28
- if (init?.dispatcher) {
29
- throw new fetch_1.FetchRequestError((0, fetch_1.asRequest)(input, init), 500, 'SSRF protection cannot be used with a custom request dispatcher');
30
- }
31
- const url = (0, fetch_1.extractUrl)(input);
32
- if (url.hostname && (0, util_js_1.isUnicastIp)(url.hostname) === false) {
33
- throw new fetch_1.FetchRequestError((0, fetch_1.asRequest)(input, init), 400, 'Hostname is a non-unicast address');
34
- }
35
- if (SUPPORTS_REQUEST_INIT_DISPATCHER) {
36
- // @ts-expect-error non-standard option
37
- const request = new Request(input, { ...init, dispatcher });
38
- return fetch.call(this, request);
39
- }
40
- else {
41
- // @ts-expect-error non-standard option
42
- return fetch.call(this, input, { ...init, dispatcher });
43
- }
44
- };
45
- }
46
- else {
47
- return async function (input, init) {
48
- if (init?.dispatcher) {
49
- throw new fetch_1.FetchRequestError((0, fetch_1.asRequest)(input, init), 500, 'SSRF protection cannot be used with a custom request dispatcher');
50
- }
51
- const url = (0, fetch_1.extractUrl)(input);
52
- if (!url.hostname) {
53
- return fetch.call(this, input, init);
54
- }
55
- switch ((0, util_js_1.isUnicastIp)(url.hostname)) {
56
- case true: {
57
- // hostname is a unicast address, safe to proceed.
58
- return fetch.call(this, input, init);
59
- }
60
- case false: {
61
- throw new fetch_1.FetchRequestError((0, fetch_1.asRequest)(input, init), 400, 'Hostname is a non-unicast address');
62
- }
63
- case undefined: {
64
- // hostname is a domain name, let's create a new dispatcher that
65
- // will 1) use the unicastLookup function to resolve the hostname
66
- // and 2) allow us to check that the lookup function was indeed
67
- // called.
68
- let didLookup = false;
69
- const dispatcher = new undici_1.Client(url.origin, {
70
- // Do *not* enable H2 here, as it will cause an error (the
71
- // client will terminate the connection before the response is
72
- // consumed).
73
- // https://github.com/nodejs/undici/issues/3671
74
- connect: {
75
- keepAlive: false, // Client will be used once
76
- lookup(...args) {
77
- didLookup = true;
78
- unicastLookup(...args);
79
- },
80
- },
81
- });
82
- try {
83
- const headers = new Headers(init?.headers);
84
- headers.set('connection', 'close'); // Proactively close the connection
85
- const response = await fetch.call(this, input, {
86
- ...init,
87
- headers,
88
- // @ts-expect-error non-standard option
89
- dispatcher,
90
- });
91
- if (!didLookup) {
92
- // We need to ensure that the body is discarded. We can either
93
- // consume the whole body (for await loop) in order to keep the
94
- // socket alive, or cancel the request. Since we sent "connection:
95
- // close", there is no point in consuming the whole response
96
- // (which would cause un-necessary bandwidth).
97
- //
98
- // https://undici.nodejs.org/#/?id=garbage-collection
99
- await response.body?.cancel();
100
- // If you encounter this error, either upgrade to Node.js >=21 or
101
- // make sure that the dispatcher passed through the requestInit
102
- // object ends up being used to make the request.
103
- // eslint-disable-next-line no-unsafe-finally
104
- throw new fetch_1.FetchRequestError((0, fetch_1.asRequest)(input, init), 500, 'Unable to enforce SSRF protection');
105
- }
106
- return response;
107
- }
108
- finally {
109
- // Free resources (we cannot await here since the response was not
110
- // consumed yet).
111
- void dispatcher.close().catch((err) => {
112
- // No biggie, but let's still log it
113
- console.warn('Failed to close dispatcher', err);
114
- });
115
- }
116
- }
117
- }
118
- };
11
+ export function unicastFetchWrap({ fetch = globalThis.fetch, }) {
12
+ if (!SUPPORTS_REQUEST_INIT_DISPATCHER) {
13
+ throw new Error('Unicast SSRF protection unavailable on your platform. Update to Node.js 22+.');
119
14
  }
15
+ const dispatcher = new Agent({
16
+ connect: { lookup: unicastLookup },
17
+ });
18
+ return async function (input, init) {
19
+ if (init?.dispatcher) {
20
+ throw new FetchRequestError(asRequest(input, init), 500, 'SSRF protection cannot be used with a custom request dispatcher');
21
+ }
22
+ const url = extractUrl(input);
23
+ if (url.hostname && isUnicastIp(url.hostname) === false) {
24
+ throw new FetchRequestError(asRequest(input, init), 400, 'Hostname is a non-unicast address');
25
+ }
26
+ // @ts-expect-error non-standard option
27
+ const request = new Request(input, { ...init, dispatcher });
28
+ return fetch.call(this, request);
29
+ };
120
30
  }
121
- function unicastLookup(hostname, options, callback) {
31
+ export function unicastLookup(hostname, options, callback) {
122
32
  if (isLocalHostname(hostname)) {
123
33
  callback(new Error('Hostname is not a public domain'), []);
124
34
  return;
125
35
  }
126
- node_dns_1.default.lookup(hostname, options, (err, address, family) => {
36
+ dns.lookup(hostname, options, (err, address, family) => {
127
37
  if (err) {
128
38
  callback(err, address, family);
129
39
  }
@@ -1 +1 @@
1
- {"version":3,"file":"unicast.js","sourceRoot":"","sources":["../src/unicast.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"unicast.js","sourceRoot":"","sources":["../src/unicast.ts"],"names":[],"mappings":"AAAA,OAAO,GAAsB,MAAM,UAAU,CAAA;AAE7C,OAAO,MAAM,MAAM,WAAW,CAAA;AAC9B,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAC9B,OAAO,EAGL,iBAAiB,EACjB,SAAS,EACT,UAAU,GACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAEvC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,CAAA;AAM7B,MAAM,gCAAgC,GACpC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;AAEnD;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAmB,EACjD,KAAK,GAAG,UAAU,CAAC,KAAK,GACG;IAC3B,IAAI,CAAC,gCAAgC,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CACb,8EAA8E,CAC/E,CAAA;IACH,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,KAAK,CAAC;QAC3B,OAAO,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE;KACnC,CAAC,CAAA;IAEF,OAAO,KAAK,WAAW,KAAK,EAAE,IAAI;QAChC,IAAI,IAAI,EAAE,UAAU,EAAE,CAAC;YACrB,MAAM,IAAI,iBAAiB,CACzB,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,EACtB,GAAG,EACH,iEAAiE,CAClE,CAAA;QACH,CAAC;QAED,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,CAAA;QAE7B,IAAI,GAAG,CAAC,QAAQ,IAAI,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,KAAK,EAAE,CAAC;YACxD,MAAM,IAAI,iBAAiB,CACzB,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,EACtB,GAAG,EACH,mCAAmC,CACpC,CAAA;QACH,CAAC;QAED,uCAAuC;QACvC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,CAAC,CAAA;QAC3D,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IAClC,CAAC,CAAA;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,QAAgB,EAChB,OAA0B,EAC1B,QAAuC;IAEvC,IAAI,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9B,QAAQ,CAAC,IAAI,KAAK,CAAC,iCAAiC,CAAC,EAAE,EAAE,CAAC,CAAA;QAC1D,OAAM;IACR,CAAC;IAED,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;QACrD,IAAI,GAAG,EAAE,CAAC;YACR,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;QAChC,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;gBAChC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;gBACjC,CAAC,CAAC,CAAC,kBAAkB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;YAE7C,IAAI,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;gBAC3B,QAAQ,CACN,IAAI,KAAK,CAAC,0CAA0C,CAAC,EACrD,OAAO,EACP,MAAM,CACP,CAAA;YACH,CAAC;iBAAM,CAAC;gBACN,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;YACjC,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,QAAgB;IACvC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACjC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IAEjC,MAAM,GAAG,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,CAAA;IACvC,OAAO,CACL,GAAG,KAAK,MAAM;QACd,GAAG,KAAK,OAAO;QACf,GAAG,KAAK,WAAW;QACnB,GAAG,KAAK,SAAS;QACjB,GAAG,KAAK,SAAS,CAClB,CAAA;AACH,CAAC;AAED,SAAS,YAAY,CAAC,EAA6B;IACjD,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,SAAS,CAAA;AACjC,CAAC;AAED,SAAS,kBAAkB,CAAC,EAC1B,OAAO,EACP,MAAM,GACQ;IACd,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAEnE,IAAI,EAAE,YAAY,IAAI,IAAI,EAAE,CAAC,mBAAmB,EAAE,EAAE,CAAC;QACnD,OAAO,EAAE,CAAC,aAAa,EAAE,CAAA;IAC3B,CAAC;SAAM,CAAC;QACN,OAAO,EAAE,CAAA;IACX,CAAC;AACH,CAAC","sourcesContent":["import dns, { LookupAddress } from 'node:dns'\nimport { LookupFunction } from 'node:net'\nimport ipaddr from 'ipaddr.js'\nimport { Agent } from 'undici'\nimport {\n Fetch,\n FetchContext,\n FetchRequestError,\n asRequest,\n extractUrl,\n} from '@atproto-labs/fetch'\nimport { isUnicastIp } from './util.js'\n\nconst { IPv4, IPv6 } = ipaddr\n\nexport type UnicastFetchWrapOptions<C = FetchContext> = {\n fetch?: Fetch<C>\n}\n\nconst SUPPORTS_REQUEST_INIT_DISPATCHER =\n Number(process.versions.node.split('.')[0]) >= 20\n\n/**\n * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}\n */\nexport function unicastFetchWrap<C = FetchContext>({\n fetch = globalThis.fetch,\n}: UnicastFetchWrapOptions<C>): Fetch<C> {\n if (!SUPPORTS_REQUEST_INIT_DISPATCHER) {\n throw new Error(\n 'Unicast SSRF protection unavailable on your platform. Update to Node.js 22+.',\n )\n }\n\n const dispatcher = new Agent({\n connect: { lookup: unicastLookup },\n })\n\n return async function (input, init): Promise<Response> {\n if (init?.dispatcher) {\n throw new FetchRequestError(\n asRequest(input, init),\n 500,\n 'SSRF protection cannot be used with a custom request dispatcher',\n )\n }\n\n const url = extractUrl(input)\n\n if (url.hostname && isUnicastIp(url.hostname) === false) {\n throw new FetchRequestError(\n asRequest(input, init),\n 400,\n 'Hostname is a non-unicast address',\n )\n }\n\n // @ts-expect-error non-standard option\n const request = new Request(input, { ...init, dispatcher })\n return fetch.call(this, request)\n }\n}\n\nexport function unicastLookup(\n hostname: string,\n options: dns.LookupOptions,\n callback: Parameters<LookupFunction>[2],\n) {\n if (isLocalHostname(hostname)) {\n callback(new Error('Hostname is not a public domain'), [])\n return\n }\n\n dns.lookup(hostname, options, (err, address, family) => {\n if (err) {\n callback(err, address, family)\n } else {\n const ips = Array.isArray(address)\n ? address.map(parseLookupAddress)\n : [parseLookupAddress({ address, family })]\n\n if (ips.some(isNotUnicast)) {\n callback(\n new Error('Hostname resolved to non-unicast address'),\n address,\n family,\n )\n } else {\n callback(null, address, family)\n }\n }\n })\n}\n\n/**\n * @param hostname - a syntactically valid hostname\n * @returns whether the hostname is a name typically used for on locale area networks.\n * @note **DO NOT** use for security reasons. Only as heuristic.\n */\nfunction isLocalHostname(hostname: string): boolean {\n const parts = hostname.split('.')\n if (parts.length < 2) return true\n\n const tld = parts.at(-1)!.toLowerCase()\n return (\n tld === 'test' ||\n tld === 'local' ||\n tld === 'localhost' ||\n tld === 'invalid' ||\n tld === 'example'\n )\n}\n\nfunction isNotUnicast(ip: ipaddr.IPv4 | ipaddr.IPv6): boolean {\n return ip.range() !== 'unicast'\n}\n\nfunction parseLookupAddress({\n address,\n family,\n}: LookupAddress): ipaddr.IPv4 | ipaddr.IPv6 {\n const ip = family === 4 ? IPv4.parse(address) : IPv6.parse(address)\n\n if (ip instanceof IPv6 && ip.isIPv4MappedAddress()) {\n return ip.toIPv4Address()\n } else {\n return ip\n }\n}\n"]}
package/dist/util.js CHANGED
@@ -1,11 +1,5 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.isUnicastIp = isUnicastIp;
7
- const ipaddr_js_1 = __importDefault(require("ipaddr.js"));
8
- const { IPv4, IPv6 } = ipaddr_js_1.default;
1
+ import ipaddr from 'ipaddr.js';
2
+ const { IPv4, IPv6 } = ipaddr;
9
3
  function parseIpHostname(hostname) {
10
4
  if (IPv4.isIPv4(hostname)) {
11
5
  return IPv4.parse(hostname);
@@ -15,7 +9,7 @@ function parseIpHostname(hostname) {
15
9
  }
16
10
  return undefined;
17
11
  }
18
- function isUnicastIp(hostname) {
12
+ export function isUnicastIp(hostname) {
19
13
  const ip = parseIpHostname(hostname);
20
14
  return ip ? ip.range() === 'unicast' : undefined;
21
15
  }
package/dist/util.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;;;;AAkBA,kCAGC;AArBD,0DAA8B;AAE9B,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,mBAAM,CAAA;AAE7B,SAAS,eAAe,CACtB,QAAgB;IAEhB,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IAC7B,CAAC;IAED,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACvD,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1C,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,SAAgB,WAAW,CAAC,QAAgB;IAC1C,MAAM,EAAE,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAA;IACpC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAA;AAClD,CAAC"}
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,WAAW,CAAA;AAE9B,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,CAAA;AAE7B,SAAS,eAAe,CACtB,QAAgB;IAEhB,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IAC7B,CAAC;IAED,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACvD,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1C,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,MAAM,EAAE,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAA;IACpC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAA;AAClD,CAAC","sourcesContent":["import ipaddr from 'ipaddr.js'\n\nconst { IPv4, IPv6 } = ipaddr\n\nfunction parseIpHostname(\n hostname: string,\n): ipaddr.IPv4 | ipaddr.IPv6 | undefined {\n if (IPv4.isIPv4(hostname)) {\n return IPv4.parse(hostname)\n }\n\n if (hostname.startsWith('[') && hostname.endsWith(']')) {\n return IPv6.parse(hostname.slice(1, -1))\n }\n\n return undefined\n}\n\nexport function isUnicastIp(hostname: string): boolean | undefined {\n const ip = parseIpHostname(hostname)\n return ip ? ip.range() === 'unicast' : undefined\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto-labs/fetch-node",
3
- "version": "0.1.10",
3
+ "version": "0.3.0-next.0",
4
4
  "license": "MIT",
5
5
  "description": "SSRF protection for fetch() in Node.js",
6
6
  "keywords": [
@@ -14,9 +14,7 @@
14
14
  "url": "https://github.com/bluesky-social/atproto",
15
15
  "directory": "packages/internal/fetch-node"
16
16
  },
17
- "type": "commonjs",
18
- "main": "dist/index.js",
19
- "types": "dist/index.d.ts",
17
+ "type": "module",
20
18
  "exports": {
21
19
  ".": {
22
20
  "types": "./dist/index.d.ts",
@@ -24,16 +22,16 @@
24
22
  }
25
23
  },
26
24
  "engines": {
27
- "node": ">=18.7.0"
25
+ "node": ">=22"
28
26
  },
29
27
  "dependencies": {
30
28
  "ipaddr.js": "^2.1.0",
31
29
  "undici": "^6.14.1",
32
- "@atproto-labs/fetch": "0.2.3",
33
- "@atproto-labs/pipe": "0.1.1"
30
+ "@atproto-labs/pipe": "^0.2.0-next.0",
31
+ "@atproto-labs/fetch": "^0.3.0-next.0"
34
32
  },
35
33
  "devDependencies": {
36
- "typescript": "^5.6.3"
34
+ "typescript": "^6.0.3"
37
35
  },
38
36
  "scripts": {
39
37
  "build": "tsc --build tsconfig.json"
package/src/safe.ts CHANGED
@@ -52,7 +52,6 @@ export type SafeFetchWrapOptions<C> = UnicastFetchWrapOptions<C> & {
52
52
  */
53
53
  export function safeFetchWrap<C>({
54
54
  fetch = globalThis.fetch as Fetch<C>,
55
- dangerouslyForceKeepAliveAgent = false,
56
55
  responseMaxSize = 512 * 1024, // 512kB
57
56
  ssrfProtection = true,
58
57
  allowCustomPort = !ssrfProtection,
@@ -106,9 +105,7 @@ export function safeFetchWrap<C>({
106
105
  * input, we need to make sure that the request is not vulnerable to SSRF
107
106
  * attacks.
108
107
  */
109
- allowPrivateIps
110
- ? fetch
111
- : unicastFetchWrap({ fetch, dangerouslyForceKeepAliveAgent }),
108
+ allowPrivateIps ? fetch : unicastFetchWrap({ fetch }),
112
109
  ),
113
110
 
114
111
  /**
package/src/unicast.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import dns, { LookupAddress } from 'node:dns'
2
2
  import { LookupFunction } from 'node:net'
3
3
  import ipaddr from 'ipaddr.js'
4
- import { Agent, Client } from 'undici'
4
+ import { Agent } from 'undici'
5
5
  import {
6
6
  Fetch,
7
7
  FetchContext,
@@ -15,38 +15,8 @@ const { IPv4, IPv6 } = ipaddr
15
15
 
16
16
  export type UnicastFetchWrapOptions<C = FetchContext> = {
17
17
  fetch?: Fetch<C>
18
-
19
- /**
20
- * ## ‼️ important security feature use with care
21
- *
22
- * On older NodeJS version, the `dispatcher` init option is ignored when
23
- * creating a new Request instance. It can only be passed through the fetch
24
- * function directly.
25
- *
26
- * Since this is a security feature, we need to ensure that the unicastLookup
27
- * function is called to resolve the hostname to a unicast IP address.
28
- *
29
- * However, in the case a custom "fetch" function is passed here (fetch !==
30
- * globalThis.fetch), we have no guarantee that the dispatcher will be used to
31
- * make the request. Because of this, in such a case, we will use a one-time
32
- * use dispatcher that checks that the provided fetch function indeed made use
33
- * of the "unicastLookup" when a custom dispatch init function is used.
34
- *
35
- * Sadly, this means that we cannot use "keepAlive" connections, as the method
36
- * used to ensure that "unicastLookup" gets called requires to create a new
37
- * dispatcher for each request.
38
- *
39
- * If you can guarantee that the provided fetch function will make use of the
40
- * "dispatcher" init option, you can set this flag to true, which will enable
41
- * the use of a single agent (with keep-alive) for all requests.
42
- *
43
- * @default false
44
- * @note This option has no effect on Node.js versions >= 20
45
- */
46
- dangerouslyForceKeepAliveAgent?: boolean
47
18
  }
48
19
 
49
- // @TODO support other runtimes ?
50
20
  const SUPPORTS_REQUEST_INIT_DISPATCHER =
51
21
  Number(process.versions.node.split('.')[0]) >= 20
52
22
 
@@ -55,141 +25,39 @@ const SUPPORTS_REQUEST_INIT_DISPATCHER =
55
25
  */
56
26
  export function unicastFetchWrap<C = FetchContext>({
57
27
  fetch = globalThis.fetch,
58
- dangerouslyForceKeepAliveAgent = false,
59
28
  }: UnicastFetchWrapOptions<C>): Fetch<C> {
60
- if (
61
- SUPPORTS_REQUEST_INIT_DISPATCHER ||
62
- dangerouslyForceKeepAliveAgent ||
63
- fetch === globalThis.fetch
64
- ) {
65
- const dispatcher = new Agent({
66
- connect: { lookup: unicastLookup },
67
- })
68
-
69
- return async function (input, init): Promise<Response> {
70
- if (init?.dispatcher) {
71
- throw new FetchRequestError(
72
- asRequest(input, init),
73
- 500,
74
- 'SSRF protection cannot be used with a custom request dispatcher',
75
- )
76
- }
77
-
78
- const url = extractUrl(input)
29
+ if (!SUPPORTS_REQUEST_INIT_DISPATCHER) {
30
+ throw new Error(
31
+ 'Unicast SSRF protection unavailable on your platform. Update to Node.js 22+.',
32
+ )
33
+ }
79
34
 
80
- if (url.hostname && isUnicastIp(url.hostname) === false) {
81
- throw new FetchRequestError(
82
- asRequest(input, init),
83
- 400,
84
- 'Hostname is a non-unicast address',
85
- )
86
- }
35
+ const dispatcher = new Agent({
36
+ connect: { lookup: unicastLookup },
37
+ })
87
38
 
88
- if (SUPPORTS_REQUEST_INIT_DISPATCHER) {
89
- // @ts-expect-error non-standard option
90
- const request = new Request(input, { ...init, dispatcher })
91
- return fetch.call(this, request)
92
- } else {
93
- // @ts-expect-error non-standard option
94
- return fetch.call(this, input, { ...init, dispatcher })
95
- }
39
+ return async function (input, init): Promise<Response> {
40
+ if (init?.dispatcher) {
41
+ throw new FetchRequestError(
42
+ asRequest(input, init),
43
+ 500,
44
+ 'SSRF protection cannot be used with a custom request dispatcher',
45
+ )
96
46
  }
97
- } else {
98
- return async function (input, init): Promise<Response> {
99
- if (init?.dispatcher) {
100
- throw new FetchRequestError(
101
- asRequest(input, init),
102
- 500,
103
- 'SSRF protection cannot be used with a custom request dispatcher',
104
- )
105
- }
106
47
 
107
- const url = extractUrl(input)
48
+ const url = extractUrl(input)
108
49
 
109
- if (!url.hostname) {
110
- return fetch.call(this, input, init)
111
- }
112
-
113
- switch (isUnicastIp(url.hostname)) {
114
- case true: {
115
- // hostname is a unicast address, safe to proceed.
116
- return fetch.call(this, input, init)
117
- }
118
-
119
- case false: {
120
- throw new FetchRequestError(
121
- asRequest(input, init),
122
- 400,
123
- 'Hostname is a non-unicast address',
124
- )
125
- }
126
-
127
- case undefined: {
128
- // hostname is a domain name, let's create a new dispatcher that
129
- // will 1) use the unicastLookup function to resolve the hostname
130
- // and 2) allow us to check that the lookup function was indeed
131
- // called.
132
-
133
- let didLookup = false
134
- const dispatcher = new Client(url.origin, {
135
- // Do *not* enable H2 here, as it will cause an error (the
136
- // client will terminate the connection before the response is
137
- // consumed).
138
- // https://github.com/nodejs/undici/issues/3671
139
- connect: {
140
- keepAlive: false, // Client will be used once
141
- lookup(...args) {
142
- didLookup = true
143
- unicastLookup(...args)
144
- },
145
- },
146
- })
147
-
148
- try {
149
- const headers = new Headers(init?.headers)
150
- headers.set('connection', 'close') // Proactively close the connection
151
-
152
- const response = await fetch.call(this, input, {
153
- ...init,
154
- headers,
155
- // @ts-expect-error non-standard option
156
- dispatcher,
157
- })
158
-
159
- if (!didLookup) {
160
- // We need to ensure that the body is discarded. We can either
161
- // consume the whole body (for await loop) in order to keep the
162
- // socket alive, or cancel the request. Since we sent "connection:
163
- // close", there is no point in consuming the whole response
164
- // (which would cause un-necessary bandwidth).
165
- //
166
- // https://undici.nodejs.org/#/?id=garbage-collection
167
- await response.body?.cancel()
168
-
169
- // If you encounter this error, either upgrade to Node.js >=21 or
170
- // make sure that the dispatcher passed through the requestInit
171
- // object ends up being used to make the request.
172
-
173
- // eslint-disable-next-line no-unsafe-finally
174
- throw new FetchRequestError(
175
- asRequest(input, init),
176
- 500,
177
- 'Unable to enforce SSRF protection',
178
- )
179
- }
180
-
181
- return response
182
- } finally {
183
- // Free resources (we cannot await here since the response was not
184
- // consumed yet).
185
- void dispatcher.close().catch((err) => {
186
- // No biggie, but let's still log it
187
- console.warn('Failed to close dispatcher', err)
188
- })
189
- }
190
- }
191
- }
50
+ if (url.hostname && isUnicastIp(url.hostname) === false) {
51
+ throw new FetchRequestError(
52
+ asRequest(input, init),
53
+ 400,
54
+ 'Hostname is a non-unicast address',
55
+ )
192
56
  }
57
+
58
+ // @ts-expect-error non-standard option
59
+ const request = new Request(input, { ...init, dispatcher })
60
+ return fetch.call(this, request)
193
61
  }
194
62
  }
195
63
 
@@ -229,7 +97,7 @@ export function unicastLookup(
229
97
  * @returns whether the hostname is a name typically used for on locale area networks.
230
98
  * @note **DO NOT** use for security reasons. Only as heuristic.
231
99
  */
232
- export function isLocalHostname(hostname: string): boolean {
100
+ function isLocalHostname(hostname: string): boolean {
233
101
  const parts = hostname.split('.')
234
102
  if (parts.length < 2) return true
235
103
 
@@ -1 +1 @@
1
- {"root":["./src/index.ts","./src/safe.ts","./src/unicast.ts","./src/util.ts"],"version":"5.8.2"}
1
+ {"root":["./src/index.ts","./src/safe.ts","./src/unicast.ts","./src/util.ts"],"version":"6.0.3"}