@atproto-labs/fetch-node 0.3.2 → 0.3.4
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 +32 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/unicast.d.ts +4 -4
- package/dist/unicast.d.ts.map +1 -1
- package/dist/unicast.js +52 -18
- package/dist/unicast.js.map +1 -1
- package/dist/util.d.ts +4 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +16 -1
- package/dist/util.js.map +1 -1
- package/package.json +15 -6
- package/src/index.ts +0 -5
- package/src/safe.ts +0 -117
- package/src/unicast.ts +0 -129
- package/src/util.ts +0 -22
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @atproto-labs/fetch-node
|
|
2
2
|
|
|
3
|
+
## 0.3.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update TypeScript build to rely on references to composite internal projects
|
|
8
|
+
|
|
9
|
+
- [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Bundle only necessary files in the NPM tarball, including the `CHANGELOG.md` and `README.md` files (if present).
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07)]:
|
|
12
|
+
- @atproto-labs/fetch@0.3.3
|
|
13
|
+
- @atproto-labs/pipe@0.2.3
|
|
14
|
+
|
|
15
|
+
## 0.3.3
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [#5148](https://github.com/bluesky-social/atproto/pull/5148) [`60e9b83`](https://github.com/bluesky-social/atproto/commit/60e9b8391f212c274b1f21991ee2a3a2d14f2f88) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Wrap all errors thrown through `unicastFetchWrap` in Whatwg fetch like `TypeError`.
|
|
20
|
+
|
|
21
|
+
- [#5148](https://github.com/bluesky-social/atproto/pull/5148) [`60e9b83`](https://github.com/bluesky-social/atproto/commit/60e9b8391f212c274b1f21991ee2a3a2d14f2f88) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow `unicastFetchWrap` to be called without options object
|
|
22
|
+
|
|
23
|
+
- [#5148](https://github.com/bluesky-social/atproto/pull/5148) [`60e9b83`](https://github.com/bluesky-social/atproto/commit/60e9b8391f212c274b1f21991ee2a3a2d14f2f88) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Rename `isUnicastIp` to `isUnicastIpHostname` (`isUnicastIp` is still exported as deprecated for backwards compatibility).
|
|
24
|
+
|
|
25
|
+
- [#5148](https://github.com/bluesky-social/atproto/pull/5148) [`60e9b83`](https://github.com/bluesky-social/atproto/commit/60e9b8391f212c274b1f21991ee2a3a2d14f2f88) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Add tests for unicast ip lookup and utilities
|
|
26
|
+
|
|
27
|
+
- [#5151](https://github.com/bluesky-social/atproto/pull/5151) [`a51c45d`](https://github.com/bluesky-social/atproto/commit/a51c45d38f6bd7b8765f640e564cf921d52162e7) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update dependencies
|
|
28
|
+
|
|
29
|
+
- [#5148](https://github.com/bluesky-social/atproto/pull/5148) [`60e9b83`](https://github.com/bluesky-social/atproto/commit/60e9b8391f212c274b1f21991ee2a3a2d14f2f88) Thanks [@matthieusieben](https://github.com/matthieusieben)! - `unicastFetchWrap` now cancels request bodies when throwing an error (to avoid resource leak)
|
|
30
|
+
|
|
31
|
+
- Updated dependencies [[`60e9b83`](https://github.com/bluesky-social/atproto/commit/60e9b8391f212c274b1f21991ee2a3a2d14f2f88), [`60e9b83`](https://github.com/bluesky-social/atproto/commit/60e9b8391f212c274b1f21991ee2a3a2d14f2f88), [`a51c45d`](https://github.com/bluesky-social/atproto/commit/a51c45d38f6bd7b8765f640e564cf921d52162e7)]:
|
|
32
|
+
- @atproto-labs/fetch@0.3.2
|
|
33
|
+
- @atproto-labs/pipe@0.2.2
|
|
34
|
+
|
|
3
35
|
## 0.3.2
|
|
4
36
|
|
|
5
37
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export * from '@atproto-labs/fetch';
|
|
2
2
|
export * from './safe.js';
|
|
3
3
|
export * from './unicast.js';
|
|
4
|
-
export
|
|
4
|
+
export { isUnicastIpHostname,
|
|
5
|
+
/** @deprecated use {@link isUnicastIpHostname} instead */
|
|
6
|
+
isUnicastIpHostname as isUnicastIp, } from './util.js';
|
|
5
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AAEnC,cAAc,WAAW,CAAA;AACzB,cAAc,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AAEnC,cAAc,WAAW,CAAA;AACzB,cAAc,cAAc,CAAA;AAE5B,OAAO,EACL,mBAAmB;AACnB,0DAA0D;AAC1D,mBAAmB,IAAI,WAAW,GACnC,MAAM,WAAW,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export * from '@atproto-labs/fetch';
|
|
2
2
|
export * from './safe.js';
|
|
3
3
|
export * from './unicast.js';
|
|
4
|
-
export
|
|
4
|
+
export { isUnicastIpHostname,
|
|
5
|
+
/** @deprecated use {@link isUnicastIpHostname} instead */
|
|
6
|
+
isUnicastIpHostname as isUnicastIp, } from './util.js';
|
|
5
7
|
//# 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,cAAc,qBAAqB,CAAA;AAEnC,cAAc,WAAW,CAAA;AACzB,cAAc,cAAc,CAAA;
|
|
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;AAE5B,OAAO,EACL,mBAAmB;AACnB,0DAA0D;AAC1D,mBAAmB,IAAI,WAAW,GACnC,MAAM,WAAW,CAAA","sourcesContent":["export * from '@atproto-labs/fetch'\n\nexport * from './safe.js'\nexport * from './unicast.js'\n\nexport {\n isUnicastIpHostname,\n /** @deprecated use {@link isUnicastIpHostname} instead */\n isUnicastIpHostname as isUnicastIp,\n} from './util.js'\n"]}
|
package/dist/unicast.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { LookupFunction } from 'node:net';
|
|
1
|
+
import type { LookupOptions } from 'node:dns';
|
|
2
|
+
import type { 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>;
|
|
@@ -7,6 +7,6 @@ export type UnicastFetchWrapOptions<C = FetchContext> = {
|
|
|
7
7
|
/**
|
|
8
8
|
* @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}
|
|
9
9
|
*/
|
|
10
|
-
export declare function unicastFetchWrap<C = FetchContext>({ fetch, }
|
|
11
|
-
export declare function unicastLookup(hostname: string, options:
|
|
10
|
+
export declare function unicastFetchWrap<C = FetchContext>({ fetch, }?: UnicastFetchWrapOptions<C>): Fetch<C>;
|
|
11
|
+
export declare function unicastLookup(hostname: string, options: LookupOptions, callback: Parameters<LookupFunction>[2]): void;
|
|
12
12
|
//# sourceMappingURL=unicast.d.ts.map
|
package/dist/unicast.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unicast.d.ts","sourceRoot":"","sources":["../src/unicast.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"unicast.d.ts","sourceRoot":"","sources":["../src/unicast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,aAAa,EAAE,MAAM,UAAU,CAAA;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAK9C,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;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,GAAG,YAAY,EAAE,EACjD,KAAwB,GACzB,GAAE,uBAAuB,CAAC,CAAC,CAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAwE5C;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,QA0BxC"}
|
package/dist/unicast.js
CHANGED
|
@@ -1,31 +1,65 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { lookup } from 'node:dns';
|
|
2
2
|
import ipaddr from 'ipaddr.js';
|
|
3
|
-
import { Agent } from '
|
|
3
|
+
import { Agent as Undici6Agent } from 'undici_v6'; // NodeJS 22
|
|
4
|
+
import { Agent as Undici7Agent } from 'undici_v7'; // NodeJS 24
|
|
5
|
+
import { Agent as Undici8Agent } from 'undici_v8'; // NodeJS 26
|
|
4
6
|
import { FetchRequestError, asRequest, extractUrl, } from '@atproto-labs/fetch';
|
|
5
|
-
import {
|
|
7
|
+
import { compareVersions, isUnicastIpHostname, parseVersion } from './util.js';
|
|
6
8
|
const { IPv4, IPv6 } = ipaddr;
|
|
7
|
-
const SUPPORTS_REQUEST_INIT_DISPATCHER = Number(process.versions.node.split('.')[0]) >= 20;
|
|
8
9
|
/**
|
|
9
10
|
* @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}
|
|
10
11
|
*/
|
|
11
|
-
export function unicastFetchWrap({ fetch = globalThis.fetch, }) {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
export function unicastFetchWrap({ fetch = globalThis.fetch, } = {}) {
|
|
13
|
+
// @NOTE we parse here instead of top-level to allow version mocking in tests.
|
|
14
|
+
const nodeUndiciVersion = parseVersion(process.versions.undici);
|
|
15
|
+
if (!nodeUndiciVersion ||
|
|
16
|
+
// https://github.com/nodejs/undici/pull/2928
|
|
17
|
+
compareVersions(nodeUndiciVersion, [6, 11, 1]) < 0) {
|
|
18
|
+
throw new Error('Unicast SSRF protection requires Node.js 20.6+');
|
|
19
|
+
}
|
|
20
|
+
// @NOTE Since major versions of undici are not guaranteed to be backwards
|
|
21
|
+
// compatible, we need to check the major version and use the appropriate
|
|
22
|
+
// Agent class for that version, to ensure that the dispatcher interface is
|
|
23
|
+
// compatible with the version of undici being used.
|
|
24
|
+
const dispatcher = nodeUndiciVersion[0] === 6
|
|
25
|
+
? new Undici6Agent({ connect: { lookup: unicastLookup } })
|
|
26
|
+
: nodeUndiciVersion[0] === 7
|
|
27
|
+
? new Undici7Agent({ connect: { lookup: unicastLookup } })
|
|
28
|
+
: nodeUndiciVersion[0] === 8
|
|
29
|
+
? new Undici8Agent({ connect: { lookup: unicastLookup } })
|
|
30
|
+
: null;
|
|
31
|
+
// @NOTE Because this is a security feature, we don't want to fallback to
|
|
32
|
+
// using Agent8 to "future proof" this package. Although future version of
|
|
33
|
+
// undici may have a backwards compatible dispatcher interface, we don't want
|
|
34
|
+
// to assume that and risk a security issue.
|
|
35
|
+
if (!dispatcher) {
|
|
36
|
+
throw new Error('This version of @atproto-labs/fetch-node does not support your version of undici. Please upgrade @atproto-labs/fetch-node, and use a version of NodeJS that uses undici 6, 7, or 8 internally.');
|
|
14
37
|
}
|
|
15
|
-
const dispatcher = new Agent({
|
|
16
|
-
connect: { lookup: unicastLookup },
|
|
17
|
-
});
|
|
18
38
|
return async function (input, init) {
|
|
19
|
-
if (init
|
|
20
|
-
|
|
39
|
+
if (init != null && 'dispatcher' in init && init.dispatcher != null) {
|
|
40
|
+
const request = asRequest(input, init);
|
|
41
|
+
await request.body?.cancel();
|
|
42
|
+
const cause = new FetchRequestError(request, 500, 'SSRF protection cannot be used with a custom request dispatcher');
|
|
43
|
+
// @NOTE Use Whatwg style errors so that the error is similar whether
|
|
44
|
+
// caused by this line of an error caused by the lookup function
|
|
45
|
+
throw new TypeError('fetch failed', { cause });
|
|
21
46
|
}
|
|
22
47
|
const url = extractUrl(input);
|
|
23
|
-
if (url.hostname &&
|
|
24
|
-
|
|
48
|
+
if (url.hostname && isUnicastIpHostname(url.hostname) === false) {
|
|
49
|
+
const request = asRequest(input, init);
|
|
50
|
+
await request.body?.cancel();
|
|
51
|
+
const cause = new FetchRequestError(request, 400, 'Hostname is a non-unicast address');
|
|
52
|
+
// @NOTE Use Whatwg style errors so that the error is similar whether
|
|
53
|
+
// caused by this line of an error caused by the lookup function
|
|
54
|
+
throw new TypeError('fetch failed', { cause });
|
|
25
55
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
56
|
+
return fetch.call(this, input, {
|
|
57
|
+
...init,
|
|
58
|
+
// @ts-ignore There is a type mismatch because of undici version
|
|
59
|
+
// differences, but we know this is safe because we are using the correct
|
|
60
|
+
// Agent class for the undici version.
|
|
61
|
+
dispatcher,
|
|
62
|
+
});
|
|
29
63
|
};
|
|
30
64
|
}
|
|
31
65
|
export function unicastLookup(hostname, options, callback) {
|
|
@@ -33,7 +67,7 @@ export function unicastLookup(hostname, options, callback) {
|
|
|
33
67
|
callback(new Error('Hostname is not a public domain'), []);
|
|
34
68
|
return;
|
|
35
69
|
}
|
|
36
|
-
|
|
70
|
+
lookup(hostname, options, (err, address, family) => {
|
|
37
71
|
if (err) {
|
|
38
72
|
callback(err, address, family);
|
|
39
73
|
}
|
package/dist/unicast.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unicast.js","sourceRoot":"","sources":["../src/unicast.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"unicast.js","sourceRoot":"","sources":["../src/unicast.ts"],"names":[],"mappings":"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","sourcesContent":["import type { LookupAddress, LookupOptions } from 'node:dns'\nimport { lookup } from 'node:dns'\nimport type { LookupFunction } from 'node:net'\nimport ipaddr from 'ipaddr.js'\nimport { Agent as Undici6Agent } from 'undici_v6' // NodeJS 22\nimport { Agent as Undici7Agent } from 'undici_v7' // NodeJS 24\nimport { Agent as Undici8Agent } from 'undici_v8' // NodeJS 26\nimport {\n Fetch,\n FetchContext,\n FetchRequestError,\n asRequest,\n extractUrl,\n} from '@atproto-labs/fetch'\nimport { compareVersions, isUnicastIpHostname, parseVersion } from './util.js'\n\nconst { IPv4, IPv6 } = ipaddr\n\nexport type UnicastFetchWrapOptions<C = FetchContext> = {\n fetch?: Fetch<C>\n}\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 // @NOTE we parse here instead of top-level to allow version mocking in tests.\n const nodeUndiciVersion = parseVersion(process.versions.undici)\n\n if (\n !nodeUndiciVersion ||\n // https://github.com/nodejs/undici/pull/2928\n compareVersions(nodeUndiciVersion, [6, 11, 1]) < 0\n ) {\n throw new Error('Unicast SSRF protection requires Node.js 20.6+')\n }\n\n // @NOTE Since major versions of undici are not guaranteed to be backwards\n // compatible, we need to check the major version and use the appropriate\n // Agent class for that version, to ensure that the dispatcher interface is\n // compatible with the version of undici being used.\n const dispatcher =\n nodeUndiciVersion[0] === 6\n ? new Undici6Agent({ connect: { lookup: unicastLookup } })\n : nodeUndiciVersion[0] === 7\n ? new Undici7Agent({ connect: { lookup: unicastLookup } })\n : nodeUndiciVersion[0] === 8\n ? new Undici8Agent({ connect: { lookup: unicastLookup } })\n : null\n\n // @NOTE Because this is a security feature, we don't want to fallback to\n // using Agent8 to \"future proof\" this package. Although future version of\n // undici may have a backwards compatible dispatcher interface, we don't want\n // to assume that and risk a security issue.\n if (!dispatcher) {\n throw new Error(\n 'This version of @atproto-labs/fetch-node does not support your version of undici. Please upgrade @atproto-labs/fetch-node, and use a version of NodeJS that uses undici 6, 7, or 8 internally.',\n )\n }\n\n return async function (this: C, input, init): Promise<Response> {\n if (init != null && 'dispatcher' in init && init.dispatcher != null) {\n const request = asRequest(input, init)\n await request.body?.cancel()\n const cause = new FetchRequestError(\n request,\n 500,\n 'SSRF protection cannot be used with a custom request dispatcher',\n )\n // @NOTE Use Whatwg style errors so that the error is similar whether\n // caused by this line of an error caused by the lookup function\n throw new TypeError('fetch failed', { cause })\n }\n\n const url = extractUrl(input)\n\n if (url.hostname && isUnicastIpHostname(url.hostname) === false) {\n const request = asRequest(input, init)\n await request.body?.cancel()\n const cause = new FetchRequestError(\n request,\n 400,\n 'Hostname is a non-unicast address',\n )\n // @NOTE Use Whatwg style errors so that the error is similar whether\n // caused by this line of an error caused by the lookup function\n throw new TypeError('fetch failed', { cause })\n }\n\n return fetch.call(this, input, {\n ...init,\n // @ts-ignore There is a type mismatch because of undici version\n // differences, but we know this is safe because we are using the correct\n // Agent class for the undici version.\n dispatcher,\n })\n }\n}\n\nexport function unicastLookup(\n hostname: string,\n options: 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 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.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
export declare function
|
|
1
|
+
export declare function isUnicastIpHostname(hostname: string): boolean | undefined;
|
|
2
|
+
export type Version = [major: number, minor: number, patch: number];
|
|
3
|
+
export declare function parseVersion(version?: string): Version | undefined;
|
|
4
|
+
export declare function compareVersions(a: Version, b: Version): number;
|
|
2
5
|
//# sourceMappingURL=util.d.ts.map
|
package/dist/util.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAkBA,wBAAgB,
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAkBA,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAGzE;AAED,MAAM,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;AACnE,wBAAgB,YAAY,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAIlE;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,MAAM,CAM9D"}
|
package/dist/util.js
CHANGED
|
@@ -9,8 +9,23 @@ function parseIpHostname(hostname) {
|
|
|
9
9
|
}
|
|
10
10
|
return undefined;
|
|
11
11
|
}
|
|
12
|
-
export function
|
|
12
|
+
export function isUnicastIpHostname(hostname) {
|
|
13
13
|
const ip = parseIpHostname(hostname);
|
|
14
14
|
return ip ? ip.range() === 'unicast' : undefined;
|
|
15
15
|
}
|
|
16
|
+
export function parseVersion(version) {
|
|
17
|
+
const match = version?.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
18
|
+
if (!match)
|
|
19
|
+
return undefined;
|
|
20
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
21
|
+
}
|
|
22
|
+
export function compareVersions(a, b) {
|
|
23
|
+
for (let i = 0; i < 3; i++) {
|
|
24
|
+
if (a[i] < b[i])
|
|
25
|
+
return -1;
|
|
26
|
+
if (a[i] > b[i])
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
16
31
|
//# sourceMappingURL=util.js.map
|
package/dist/util.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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,
|
|
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,mBAAmB,CAAC,QAAgB;IAClD,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;AAGD,MAAM,UAAU,YAAY,CAAC,OAAgB;IAC3C,MAAM,KAAK,GAAG,OAAO,EAAE,KAAK,CAAC,uBAAuB,CAAC,CAAA;IACrD,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAA;IAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAC/D,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,CAAU,EAAE,CAAU;IACpD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,CAAC,CAAA;QAC1B,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,CAAA;IAC3B,CAAC;IACD,OAAO,CAAC,CAAA;AACV,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 isUnicastIpHostname(hostname: string): boolean | undefined {\n const ip = parseIpHostname(hostname)\n return ip ? ip.range() === 'unicast' : undefined\n}\n\nexport type Version = [major: number, minor: number, patch: number]\nexport function parseVersion(version?: string): Version | undefined {\n const match = version?.match(/^(\\d+)\\.(\\d+)\\.(\\d+)$/)\n if (!match) return undefined\n return [Number(match[1]), Number(match[2]), Number(match[3])]\n}\n\nexport function compareVersions(a: Version, b: Version): number {\n for (let i = 0; i < 3; i++) {\n if (a[i] < b[i]) return -1\n if (a[i] > b[i]) return 1\n }\n return 0\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto-labs/fetch-node",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "SSRF protection for fetch() in Node.js",
|
|
6
6
|
"keywords": [
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"url": "https://github.com/bluesky-social/atproto",
|
|
15
15
|
"directory": "packages/internal/fetch-node"
|
|
16
16
|
},
|
|
17
|
+
"files": [
|
|
18
|
+
"./dist",
|
|
19
|
+
"./CHANGELOG.md"
|
|
20
|
+
],
|
|
17
21
|
"type": "module",
|
|
18
22
|
"exports": {
|
|
19
23
|
".": {
|
|
@@ -26,12 +30,17 @@
|
|
|
26
30
|
},
|
|
27
31
|
"dependencies": {
|
|
28
32
|
"ipaddr.js": "^2.1.0",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
33
|
+
"undici_v6": "npm:undici@^6.x",
|
|
34
|
+
"undici_v7": "npm:undici@^7.x",
|
|
35
|
+
"undici_v8": "npm:undici@^8.x",
|
|
36
|
+
"@atproto-labs/fetch": "^0.3.3",
|
|
37
|
+
"@atproto-labs/pipe": "^0.2.3"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"vitest": "^4.0.16"
|
|
32
41
|
},
|
|
33
|
-
"devDependencies": {},
|
|
34
42
|
"scripts": {
|
|
35
|
-
"build": "tsgo --build tsconfig.json"
|
|
43
|
+
"build": "tsgo --build tsconfig.json",
|
|
44
|
+
"test": "vitest run"
|
|
36
45
|
}
|
|
37
46
|
}
|
package/src/index.ts
DELETED
package/src/safe.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_FORBIDDEN_DOMAIN_NAMES,
|
|
3
|
-
Fetch,
|
|
4
|
-
asRequest,
|
|
5
|
-
explicitRedirectCheckRequestTransform,
|
|
6
|
-
fetchMaxSizeProcessor,
|
|
7
|
-
forbiddenDomainNameRequestTransform,
|
|
8
|
-
protocolCheckRequestTransform,
|
|
9
|
-
requireHostHeaderTransform,
|
|
10
|
-
timedFetch,
|
|
11
|
-
} from '@atproto-labs/fetch'
|
|
12
|
-
import { pipe } from '@atproto-labs/pipe'
|
|
13
|
-
import { UnicastFetchWrapOptions, unicastFetchWrap } from './unicast.js'
|
|
14
|
-
|
|
15
|
-
export type SafeFetchWrapOptions<C> = UnicastFetchWrapOptions<C> & {
|
|
16
|
-
responseMaxSize?: number
|
|
17
|
-
ssrfProtection?: boolean
|
|
18
|
-
allowCustomPort?: boolean
|
|
19
|
-
allowData?: boolean
|
|
20
|
-
allowHttp?: boolean
|
|
21
|
-
allowIpHost?: boolean
|
|
22
|
-
allowPrivateIps?: boolean
|
|
23
|
-
timeout?: number
|
|
24
|
-
forbiddenDomainNames?: Iterable<string>
|
|
25
|
-
/**
|
|
26
|
-
* When `false`, a {@link RequestInit['redirect']} value must be explicitly
|
|
27
|
-
* provided as second argument to the returned function or requests will fail.
|
|
28
|
-
*
|
|
29
|
-
* @default false
|
|
30
|
-
*/
|
|
31
|
-
allowImplicitRedirect?: boolean
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Wrap a fetch function with safety checks so that it can be safely used
|
|
36
|
-
* with user provided input (URL).
|
|
37
|
-
*
|
|
38
|
-
* @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html}
|
|
39
|
-
*
|
|
40
|
-
* @note When {@link SafeFetchWrapOptions.allowImplicitRedirect} is `false`
|
|
41
|
-
* (default), then the returned function **must** be called setting the second
|
|
42
|
-
* argument's `redirect` property to one of the allowed values. Otherwise, if
|
|
43
|
-
* the returned fetch function is called with a `Request` object (and no
|
|
44
|
-
* explicit `redirect` init object), then the verification code will not be able
|
|
45
|
-
* to determine if the `redirect` property was explicitly set or based on the
|
|
46
|
-
* default value (`follow`), causing it to preventively block the request (throw
|
|
47
|
-
* an error). For this reason, unless you set
|
|
48
|
-
* {@link SafeFetchWrapOptions.allowImplicitRedirect} to `true`, you should
|
|
49
|
-
* **not** wrap the returned function into another function that creates a
|
|
50
|
-
* {@link Request} object before passing it to the function (as a e.g. a logging
|
|
51
|
-
* function would).
|
|
52
|
-
*/
|
|
53
|
-
export function safeFetchWrap<C>({
|
|
54
|
-
fetch = globalThis.fetch as Fetch<C>,
|
|
55
|
-
responseMaxSize = 512 * 1024, // 512kB
|
|
56
|
-
ssrfProtection = true,
|
|
57
|
-
allowCustomPort = !ssrfProtection,
|
|
58
|
-
allowData = false,
|
|
59
|
-
allowHttp = !ssrfProtection,
|
|
60
|
-
allowIpHost = true,
|
|
61
|
-
allowPrivateIps = !ssrfProtection,
|
|
62
|
-
timeout = 10e3,
|
|
63
|
-
forbiddenDomainNames = DEFAULT_FORBIDDEN_DOMAIN_NAMES as Iterable<string>,
|
|
64
|
-
allowImplicitRedirect = false,
|
|
65
|
-
}: SafeFetchWrapOptions<C> = {}) {
|
|
66
|
-
return pipe(
|
|
67
|
-
/**
|
|
68
|
-
* Require explicit {@link RequestInit['redirect']} mode
|
|
69
|
-
*/
|
|
70
|
-
allowImplicitRedirect ? asRequest : explicitRedirectCheckRequestTransform(),
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Only requests that will be issued with a "Host" header are allowed.
|
|
74
|
-
*/
|
|
75
|
-
allowIpHost ? asRequest : requireHostHeaderTransform(),
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Prevent using http:, file: or data: protocols.
|
|
79
|
-
*/
|
|
80
|
-
protocolCheckRequestTransform({
|
|
81
|
-
'about:': false,
|
|
82
|
-
'data:': allowData,
|
|
83
|
-
'file:': false,
|
|
84
|
-
'http:': allowHttp && { allowCustomPort },
|
|
85
|
-
'https:': { allowCustomPort },
|
|
86
|
-
}),
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Disallow fetching from domains we know are not atproto/OIDC client
|
|
90
|
-
* implementation. Note that other domains can be blocked by providing a
|
|
91
|
-
* custom fetch function combined with another
|
|
92
|
-
* forbiddenDomainNameRequestTransform.
|
|
93
|
-
*/
|
|
94
|
-
forbiddenDomainNameRequestTransform(forbiddenDomainNames),
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Since we will be fetching from the network based on user provided
|
|
98
|
-
* input, let's mitigate resource exhaustion attacks by setting a timeout.
|
|
99
|
-
*/
|
|
100
|
-
timedFetch(
|
|
101
|
-
timeout,
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Since we will be fetching from the network based on user provided
|
|
105
|
-
* input, we need to make sure that the request is not vulnerable to SSRF
|
|
106
|
-
* attacks.
|
|
107
|
-
*/
|
|
108
|
-
allowPrivateIps ? fetch : unicastFetchWrap({ fetch }),
|
|
109
|
-
),
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Since we will be fetching user owned data, we need to make sure that an
|
|
113
|
-
* attacker cannot force us to download a large amounts of data.
|
|
114
|
-
*/
|
|
115
|
-
fetchMaxSizeProcessor(responseMaxSize),
|
|
116
|
-
) satisfies Fetch<unknown>
|
|
117
|
-
}
|
package/src/unicast.ts
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import dns, { LookupAddress } from 'node:dns'
|
|
2
|
-
import { LookupFunction } from 'node:net'
|
|
3
|
-
import ipaddr from 'ipaddr.js'
|
|
4
|
-
import { Agent } from 'undici'
|
|
5
|
-
import {
|
|
6
|
-
Fetch,
|
|
7
|
-
FetchContext,
|
|
8
|
-
FetchRequestError,
|
|
9
|
-
asRequest,
|
|
10
|
-
extractUrl,
|
|
11
|
-
} from '@atproto-labs/fetch'
|
|
12
|
-
import { isUnicastIp } from './util.js'
|
|
13
|
-
|
|
14
|
-
const { IPv4, IPv6 } = ipaddr
|
|
15
|
-
|
|
16
|
-
export type UnicastFetchWrapOptions<C = FetchContext> = {
|
|
17
|
-
fetch?: Fetch<C>
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const SUPPORTS_REQUEST_INIT_DISPATCHER =
|
|
21
|
-
Number(process.versions.node.split('.')[0]) >= 20
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}
|
|
25
|
-
*/
|
|
26
|
-
export function unicastFetchWrap<C = FetchContext>({
|
|
27
|
-
fetch = globalThis.fetch,
|
|
28
|
-
}: UnicastFetchWrapOptions<C>): Fetch<C> {
|
|
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
|
-
}
|
|
34
|
-
|
|
35
|
-
const dispatcher = new Agent({
|
|
36
|
-
connect: { lookup: unicastLookup },
|
|
37
|
-
})
|
|
38
|
-
|
|
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
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const url = extractUrl(input)
|
|
49
|
-
|
|
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
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// @ts-expect-error non-standard option
|
|
59
|
-
const request = new Request(input, { ...init, dispatcher })
|
|
60
|
-
return fetch.call(this, request)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function unicastLookup(
|
|
65
|
-
hostname: string,
|
|
66
|
-
options: dns.LookupOptions,
|
|
67
|
-
callback: Parameters<LookupFunction>[2],
|
|
68
|
-
) {
|
|
69
|
-
if (isLocalHostname(hostname)) {
|
|
70
|
-
callback(new Error('Hostname is not a public domain'), [])
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
dns.lookup(hostname, options, (err, address, family) => {
|
|
75
|
-
if (err) {
|
|
76
|
-
callback(err, address, family)
|
|
77
|
-
} else {
|
|
78
|
-
const ips = Array.isArray(address)
|
|
79
|
-
? address.map(parseLookupAddress)
|
|
80
|
-
: [parseLookupAddress({ address, family })]
|
|
81
|
-
|
|
82
|
-
if (ips.some(isNotUnicast)) {
|
|
83
|
-
callback(
|
|
84
|
-
new Error('Hostname resolved to non-unicast address'),
|
|
85
|
-
address,
|
|
86
|
-
family,
|
|
87
|
-
)
|
|
88
|
-
} else {
|
|
89
|
-
callback(null, address, family)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* @param hostname - a syntactically valid hostname
|
|
97
|
-
* @returns whether the hostname is a name typically used for on locale area networks.
|
|
98
|
-
* @note **DO NOT** use for security reasons. Only as heuristic.
|
|
99
|
-
*/
|
|
100
|
-
function isLocalHostname(hostname: string): boolean {
|
|
101
|
-
const parts = hostname.split('.')
|
|
102
|
-
if (parts.length < 2) return true
|
|
103
|
-
|
|
104
|
-
const tld = parts.at(-1)!.toLowerCase()
|
|
105
|
-
return (
|
|
106
|
-
tld === 'test' ||
|
|
107
|
-
tld === 'local' ||
|
|
108
|
-
tld === 'localhost' ||
|
|
109
|
-
tld === 'invalid' ||
|
|
110
|
-
tld === 'example'
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function isNotUnicast(ip: ipaddr.IPv4 | ipaddr.IPv6): boolean {
|
|
115
|
-
return ip.range() !== 'unicast'
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function parseLookupAddress({
|
|
119
|
-
address,
|
|
120
|
-
family,
|
|
121
|
-
}: LookupAddress): ipaddr.IPv4 | ipaddr.IPv6 {
|
|
122
|
-
const ip = family === 4 ? IPv4.parse(address) : IPv6.parse(address)
|
|
123
|
-
|
|
124
|
-
if (ip instanceof IPv6 && ip.isIPv4MappedAddress()) {
|
|
125
|
-
return ip.toIPv4Address()
|
|
126
|
-
} else {
|
|
127
|
-
return ip
|
|
128
|
-
}
|
|
129
|
-
}
|
package/src/util.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import ipaddr from 'ipaddr.js'
|
|
2
|
-
|
|
3
|
-
const { IPv4, IPv6 } = ipaddr
|
|
4
|
-
|
|
5
|
-
function parseIpHostname(
|
|
6
|
-
hostname: string,
|
|
7
|
-
): ipaddr.IPv4 | ipaddr.IPv6 | undefined {
|
|
8
|
-
if (IPv4.isIPv4(hostname)) {
|
|
9
|
-
return IPv4.parse(hostname)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (hostname.startsWith('[') && hostname.endsWith(']')) {
|
|
13
|
-
return IPv6.parse(hostname.slice(1, -1))
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return undefined
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function isUnicastIp(hostname: string): boolean | undefined {
|
|
20
|
-
const ip = parseIpHostname(hostname)
|
|
21
|
-
return ip ? ip.range() === 'unicast' : undefined
|
|
22
|
-
}
|
package/tsconfig.build.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":"7.0.0-dev.20260614.1","root":["./src/index.ts","./src/safe.ts","./src/unicast.ts","./src/util.ts"]}
|
package/tsconfig.json
DELETED