@atproto-labs/fetch 0.3.1 → 0.3.2
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 +13 -0
- package/dist/fetch-request.js +4 -4
- package/dist/fetch-request.js.map +1 -1
- package/package.json +2 -2
- package/src/fetch-request.ts +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @atproto-labs/fetch
|
|
2
2
|
|
|
3
|
+
## 0.3.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#5148](https://github.com/bluesky-social/atproto/pull/5148) [`60e9b83`](https://github.com/bluesky-social/atproto/commit/60e9b8391f212c274b1f21991ee2a3a2d14f2f88) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix support of NodeJS 24 and before
|
|
8
|
+
|
|
9
|
+
- [#5148](https://github.com/bluesky-social/atproto/pull/5148) [`60e9b83`](https://github.com/bluesky-social/atproto/commit/60e9b8391f212c274b1f21991ee2a3a2d14f2f88) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Minor url parsing optimization
|
|
10
|
+
|
|
11
|
+
- [#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
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [[`a51c45d`](https://github.com/bluesky-social/atproto/commit/a51c45d38f6bd7b8765f640e564cf921d52162e7)]:
|
|
14
|
+
- @atproto-labs/pipe@0.2.2
|
|
15
|
+
|
|
3
16
|
## 0.3.1
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/dist/fetch-request.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FetchError } from './fetch-error.js';
|
|
2
2
|
import { asRequest } from './fetch.js';
|
|
3
|
-
import {
|
|
3
|
+
import { isIp } from './util.js';
|
|
4
4
|
export class FetchRequestError extends FetchError {
|
|
5
5
|
constructor(request, statusCode, message, options) {
|
|
6
6
|
if (statusCode == null || !message) {
|
|
@@ -75,8 +75,8 @@ function extractInfo(err) {
|
|
|
75
75
|
}
|
|
76
76
|
export function protocolCheckRequestTransform(protocols) {
|
|
77
77
|
return (input, init) => {
|
|
78
|
-
const { protocol, port } = extractUrl(input);
|
|
79
78
|
const request = asRequest(input, init);
|
|
79
|
+
const { protocol, port } = new URL(request.url);
|
|
80
80
|
const config = Object.hasOwn(protocols, protocol) ? protocols[protocol] : undefined;
|
|
81
81
|
if (!config) {
|
|
82
82
|
throw new FetchRequestError(request, 400, `Forbidden protocol "${protocol}"`);
|
|
@@ -111,8 +111,8 @@ export function requireHostHeaderTransform() {
|
|
|
111
111
|
return (input, init) => {
|
|
112
112
|
// Note that fetch() will automatically add the Host header from the URL and
|
|
113
113
|
// discard any Host header manually set in the request.
|
|
114
|
-
const { protocol, hostname } = extractUrl(input);
|
|
115
114
|
const request = asRequest(input, init);
|
|
115
|
+
const { protocol, hostname } = new URL(request.url);
|
|
116
116
|
// "Host" header only makes sense in the context of an HTTP request
|
|
117
117
|
if (protocol !== 'http:' && protocol !== 'https:') {
|
|
118
118
|
throw new FetchRequestError(request, 400, `"${protocol}" requests are not allowed`);
|
|
@@ -141,8 +141,8 @@ export function forbiddenDomainNameRequestTransform(denyList = DEFAULT_FORBIDDEN
|
|
|
141
141
|
return asRequest;
|
|
142
142
|
}
|
|
143
143
|
return async (input, init) => {
|
|
144
|
-
const { hostname } = extractUrl(input);
|
|
145
144
|
const request = asRequest(input, init);
|
|
145
|
+
const { hostname } = new URL(request.url);
|
|
146
146
|
// Full domain name check
|
|
147
147
|
if (denySet.has(hostname)) {
|
|
148
148
|
throw new FetchRequestError(request, 403, 'Forbidden hostname');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-request.js","sourceRoot":"","sources":["../src/fetch-request.ts"],"names":[],"mappings":"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","sourcesContent":["import { FetchError } from './fetch-error.js'\nimport { asRequest } from './fetch.js'\nimport { extractUrl, isIp } from './util.js'\n\nexport class FetchRequestError extends FetchError {\n constructor(\n public readonly request: Request,\n statusCode?: number,\n message?: string,\n options?: ErrorOptions,\n ) {\n if (statusCode == null || !message) {\n const info = extractInfo(extractRootCause(options?.cause))\n statusCode ??= info[0]\n message ||= info[1]\n }\n\n super(statusCode, message, options)\n }\n\n get expose() {\n // A 500 request error means that the request was not made due to an infra,\n // programming or server side issue. The message should no be exposed to\n // downstream clients.\n return this.statusCode !== 500\n }\n\n static from(request: Request, cause: unknown): FetchRequestError {\n if (cause instanceof FetchRequestError) return cause\n return new FetchRequestError(request, undefined, undefined, { cause })\n }\n}\n\nfunction extractRootCause(err: unknown): unknown {\n // Unwrap the Network error from undici (i.e. Node's internal fetch() implementation)\n // https://github.com/nodejs/undici/blob/3274c975947ce11a08508743df026f73598bfead/lib/web/fetch/index.js#L223-L228\n if (\n err instanceof TypeError &&\n err.message === 'fetch failed' &&\n err.cause !== undefined\n ) {\n return err.cause\n }\n\n return err\n}\n\nfunction extractInfo(err: unknown): [statusCode: number, message: string] {\n if (typeof err === 'string' && err.length > 0) {\n return [500, err]\n }\n\n if (!(err instanceof Error)) {\n return [500, 'Failed to fetch']\n }\n\n // Undici fetch() \"network\" errors\n switch (err.message) {\n case 'failed to fetch the data URL':\n return [400, err.message]\n case 'unexpected redirect':\n case 'cors failure':\n case 'blocked':\n case 'proxy authentication required':\n // These cases could be represented either as a 4xx user error (invalid\n // URL provided), or as a 5xx server error (server didn't behave as\n // expected).\n return [502, err.message]\n }\n\n // NodeJS errors\n const code = err['code']\n if (typeof code === 'string') {\n switch (true) {\n case code === 'ENOTFOUND':\n return [400, 'Invalid hostname']\n case code === 'ECONNREFUSED':\n return [502, 'Connection refused']\n case code === 'DEPTH_ZERO_SELF_SIGNED_CERT':\n return [502, 'Self-signed certificate']\n case code.startsWith('ERR_TLS'):\n return [502, 'TLS error']\n case code.startsWith('ECONN'):\n return [502, 'Connection error']\n default:\n return [500, `${code} error`]\n }\n }\n\n return [500, err.message]\n}\n\nexport function protocolCheckRequestTransform(protocols: {\n 'about:'?: boolean\n 'blob:'?: boolean\n 'data:'?: boolean\n 'file:'?: boolean\n 'http:'?: boolean | { allowCustomPort: boolean }\n 'https:'?: boolean | { allowCustomPort: boolean }\n}) {\n return (input: Request | string | URL, init?: RequestInit) => {\n const { protocol, port } = extractUrl(input)\n\n const request = asRequest(input, init)\n\n const config: undefined | boolean | { allowCustomPort?: boolean } =\n Object.hasOwn(protocols, protocol) ? protocols[protocol] : undefined\n\n if (!config) {\n throw new FetchRequestError(\n request,\n 400,\n `Forbidden protocol \"${protocol}\"`,\n )\n } else if (config === true) {\n // Safe to proceed\n } else if (!config['allowCustomPort'] && port !== '') {\n throw new FetchRequestError(\n request,\n 400,\n `Custom ${protocol} ports not allowed`,\n )\n }\n\n return request\n }\n}\n\nexport function explicitRedirectCheckRequestTransform() {\n return (input: Request | string | URL, init?: RequestInit): Request => {\n const request = asRequest(input, init)\n\n // We want to avoid the case where the user of this code forgot to explicit\n // a redirect strategy.\n if (init?.redirect != null) return request\n\n // Sadly, if the `input` is a request, and `init` was omitted, there is no\n // way to tell if the `redirect === 'follow'` value comes from the user, or\n // fetch's default. In order to prevent accidental omission, this case is\n // forbidden.\n if (request.redirect === 'follow') {\n throw new FetchRequestError(\n request,\n 500,\n 'Request redirect must be \"error\" or \"manual\"',\n )\n }\n\n return request\n }\n}\n\nexport function requireHostHeaderTransform() {\n return (input: Request | string | URL, init?: RequestInit) => {\n // Note that fetch() will automatically add the Host header from the URL and\n // discard any Host header manually set in the request.\n\n const { protocol, hostname } = extractUrl(input)\n\n const request = asRequest(input, init)\n\n // \"Host\" header only makes sense in the context of an HTTP request\n if (protocol !== 'http:' && protocol !== 'https:') {\n throw new FetchRequestError(\n request,\n 400,\n `\"${protocol}\" requests are not allowed`,\n )\n }\n\n if (!hostname || isIp(hostname)) {\n throw new FetchRequestError(request, 400, 'Invalid hostname')\n }\n\n return request\n }\n}\n\nexport const DEFAULT_FORBIDDEN_DOMAIN_NAMES = [\n 'example.com',\n '*.example.com',\n 'example.org',\n '*.example.org',\n 'example.net',\n '*.example.net',\n 'googleusercontent.com',\n '*.googleusercontent.com',\n]\n\nexport function forbiddenDomainNameRequestTransform(\n denyList: Iterable<string> = DEFAULT_FORBIDDEN_DOMAIN_NAMES,\n) {\n const denySet = new Set<string>(denyList)\n\n // Optimization: if no forbidden domain names are provided, we can skip the\n // check entirely.\n if (denySet.size === 0) {\n return asRequest\n }\n\n return async (input: Request | string | URL, init?: RequestInit) => {\n const { hostname } = extractUrl(input)\n\n const request = asRequest(input, init)\n\n // Full domain name check\n if (denySet.has(hostname)) {\n throw new FetchRequestError(request, 403, 'Forbidden hostname')\n }\n\n // Sub domain name check\n let curDot = hostname.indexOf('.')\n while (curDot !== -1) {\n const subdomain = hostname.slice(curDot + 1)\n if (denySet.has(`*.${subdomain}`)) {\n throw new FetchRequestError(request, 403, 'Forbidden hostname')\n }\n curDot = hostname.indexOf('.', curDot + 1)\n }\n\n return request\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fetch-request.js","sourceRoot":"","sources":["../src/fetch-request.ts"],"names":[],"mappings":"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","sourcesContent":["import { FetchError } from './fetch-error.js'\nimport { asRequest } from './fetch.js'\nimport { isIp } from './util.js'\n\nexport class FetchRequestError extends FetchError {\n constructor(\n public readonly request: Request,\n statusCode?: number,\n message?: string,\n options?: ErrorOptions,\n ) {\n if (statusCode == null || !message) {\n const info = extractInfo(extractRootCause(options?.cause))\n statusCode ??= info[0]\n message ||= info[1]\n }\n\n super(statusCode, message, options)\n }\n\n get expose() {\n // A 500 request error means that the request was not made due to an infra,\n // programming or server side issue. The message should no be exposed to\n // downstream clients.\n return this.statusCode !== 500\n }\n\n static from(request: Request, cause: unknown): FetchRequestError {\n if (cause instanceof FetchRequestError) return cause\n return new FetchRequestError(request, undefined, undefined, { cause })\n }\n}\n\nfunction extractRootCause(err: unknown): unknown {\n // Unwrap the Network error from undici (i.e. Node's internal fetch() implementation)\n // https://github.com/nodejs/undici/blob/3274c975947ce11a08508743df026f73598bfead/lib/web/fetch/index.js#L223-L228\n if (\n err instanceof TypeError &&\n err.message === 'fetch failed' &&\n err.cause !== undefined\n ) {\n return err.cause\n }\n\n return err\n}\n\nfunction extractInfo(err: unknown): [statusCode: number, message: string] {\n if (typeof err === 'string' && err.length > 0) {\n return [500, err]\n }\n\n if (!(err instanceof Error)) {\n return [500, 'Failed to fetch']\n }\n\n // Undici fetch() \"network\" errors\n switch (err.message) {\n case 'failed to fetch the data URL':\n return [400, err.message]\n case 'unexpected redirect':\n case 'cors failure':\n case 'blocked':\n case 'proxy authentication required':\n // These cases could be represented either as a 4xx user error (invalid\n // URL provided), or as a 5xx server error (server didn't behave as\n // expected).\n return [502, err.message]\n }\n\n // NodeJS errors\n const code = err['code']\n if (typeof code === 'string') {\n switch (true) {\n case code === 'ENOTFOUND':\n return [400, 'Invalid hostname']\n case code === 'ECONNREFUSED':\n return [502, 'Connection refused']\n case code === 'DEPTH_ZERO_SELF_SIGNED_CERT':\n return [502, 'Self-signed certificate']\n case code.startsWith('ERR_TLS'):\n return [502, 'TLS error']\n case code.startsWith('ECONN'):\n return [502, 'Connection error']\n default:\n return [500, `${code} error`]\n }\n }\n\n return [500, err.message]\n}\n\nexport function protocolCheckRequestTransform(protocols: {\n 'about:'?: boolean\n 'blob:'?: boolean\n 'data:'?: boolean\n 'file:'?: boolean\n 'http:'?: boolean | { allowCustomPort: boolean }\n 'https:'?: boolean | { allowCustomPort: boolean }\n}) {\n return (input: Request | string | URL, init?: RequestInit) => {\n const request = asRequest(input, init)\n\n const { protocol, port } = new URL(request.url)\n\n const config: undefined | boolean | { allowCustomPort?: boolean } =\n Object.hasOwn(protocols, protocol) ? protocols[protocol] : undefined\n\n if (!config) {\n throw new FetchRequestError(\n request,\n 400,\n `Forbidden protocol \"${protocol}\"`,\n )\n } else if (config === true) {\n // Safe to proceed\n } else if (!config['allowCustomPort'] && port !== '') {\n throw new FetchRequestError(\n request,\n 400,\n `Custom ${protocol} ports not allowed`,\n )\n }\n\n return request\n }\n}\n\nexport function explicitRedirectCheckRequestTransform() {\n return (input: Request | string | URL, init?: RequestInit): Request => {\n const request = asRequest(input, init)\n\n // We want to avoid the case where the user of this code forgot to explicit\n // a redirect strategy.\n if (init?.redirect != null) return request\n\n // Sadly, if the `input` is a request, and `init` was omitted, there is no\n // way to tell if the `redirect === 'follow'` value comes from the user, or\n // fetch's default. In order to prevent accidental omission, this case is\n // forbidden.\n if (request.redirect === 'follow') {\n throw new FetchRequestError(\n request,\n 500,\n 'Request redirect must be \"error\" or \"manual\"',\n )\n }\n\n return request\n }\n}\n\nexport function requireHostHeaderTransform() {\n return (input: Request | string | URL, init?: RequestInit) => {\n // Note that fetch() will automatically add the Host header from the URL and\n // discard any Host header manually set in the request.\n\n const request = asRequest(input, init)\n\n const { protocol, hostname } = new URL(request.url)\n\n // \"Host\" header only makes sense in the context of an HTTP request\n if (protocol !== 'http:' && protocol !== 'https:') {\n throw new FetchRequestError(\n request,\n 400,\n `\"${protocol}\" requests are not allowed`,\n )\n }\n\n if (!hostname || isIp(hostname)) {\n throw new FetchRequestError(request, 400, 'Invalid hostname')\n }\n\n return request\n }\n}\n\nexport const DEFAULT_FORBIDDEN_DOMAIN_NAMES = [\n 'example.com',\n '*.example.com',\n 'example.org',\n '*.example.org',\n 'example.net',\n '*.example.net',\n 'googleusercontent.com',\n '*.googleusercontent.com',\n]\n\nexport function forbiddenDomainNameRequestTransform(\n denyList: Iterable<string> = DEFAULT_FORBIDDEN_DOMAIN_NAMES,\n) {\n const denySet = new Set<string>(denyList)\n\n // Optimization: if no forbidden domain names are provided, we can skip the\n // check entirely.\n if (denySet.size === 0) {\n return asRequest\n }\n\n return async (input: Request | string | URL, init?: RequestInit) => {\n const request = asRequest(input, init)\n\n const { hostname } = new URL(request.url)\n\n // Full domain name check\n if (denySet.has(hostname)) {\n throw new FetchRequestError(request, 403, 'Forbidden hostname')\n }\n\n // Sub domain name check\n let curDot = hostname.indexOf('.')\n while (curDot !== -1) {\n const subdomain = hostname.slice(curDot + 1)\n if (denySet.has(`*.${subdomain}`)) {\n throw new FetchRequestError(request, 403, 'Forbidden hostname')\n }\n curDot = hostname.indexOf('.', curDot + 1)\n }\n\n return request\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto-labs/fetch",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22"
|
|
6
6
|
},
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@atproto-labs/pipe": "^0.2.
|
|
27
|
+
"@atproto-labs/pipe": "^0.2.2"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {},
|
|
30
30
|
"scripts": {
|
package/src/fetch-request.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FetchError } from './fetch-error.js'
|
|
2
2
|
import { asRequest } from './fetch.js'
|
|
3
|
-
import {
|
|
3
|
+
import { isIp } from './util.js'
|
|
4
4
|
|
|
5
5
|
export class FetchRequestError extends FetchError {
|
|
6
6
|
constructor(
|
|
@@ -99,10 +99,10 @@ export function protocolCheckRequestTransform(protocols: {
|
|
|
99
99
|
'https:'?: boolean | { allowCustomPort: boolean }
|
|
100
100
|
}) {
|
|
101
101
|
return (input: Request | string | URL, init?: RequestInit) => {
|
|
102
|
-
const { protocol, port } = extractUrl(input)
|
|
103
|
-
|
|
104
102
|
const request = asRequest(input, init)
|
|
105
103
|
|
|
104
|
+
const { protocol, port } = new URL(request.url)
|
|
105
|
+
|
|
106
106
|
const config: undefined | boolean | { allowCustomPort?: boolean } =
|
|
107
107
|
Object.hasOwn(protocols, protocol) ? protocols[protocol] : undefined
|
|
108
108
|
|
|
@@ -155,10 +155,10 @@ export function requireHostHeaderTransform() {
|
|
|
155
155
|
// Note that fetch() will automatically add the Host header from the URL and
|
|
156
156
|
// discard any Host header manually set in the request.
|
|
157
157
|
|
|
158
|
-
const { protocol, hostname } = extractUrl(input)
|
|
159
|
-
|
|
160
158
|
const request = asRequest(input, init)
|
|
161
159
|
|
|
160
|
+
const { protocol, hostname } = new URL(request.url)
|
|
161
|
+
|
|
162
162
|
// "Host" header only makes sense in the context of an HTTP request
|
|
163
163
|
if (protocol !== 'http:' && protocol !== 'https:') {
|
|
164
164
|
throw new FetchRequestError(
|
|
@@ -199,10 +199,10 @@ export function forbiddenDomainNameRequestTransform(
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
return async (input: Request | string | URL, init?: RequestInit) => {
|
|
202
|
-
const { hostname } = extractUrl(input)
|
|
203
|
-
|
|
204
202
|
const request = asRequest(input, init)
|
|
205
203
|
|
|
204
|
+
const { hostname } = new URL(request.url)
|
|
205
|
+
|
|
206
206
|
// Full domain name check
|
|
207
207
|
if (denySet.has(hostname)) {
|
|
208
208
|
throw new FetchRequestError(request, 403, 'Forbidden hostname')
|