@atproto-labs/fetch 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,10 +15,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.TransformedResponse = void 0;
16
16
  class TransformedResponse extends Response {
17
17
  constructor(response, transform) {
18
- if (response.body && response.bodyUsed) {
18
+ if (!response.body) {
19
+ throw new TypeError('Response body is not available');
20
+ }
21
+ if (response.bodyUsed) {
19
22
  throw new TypeError('Response body is already used');
20
23
  }
21
- super(response.body?.pipeThrough(transform) ?? null, {
24
+ super(response.body.pipeThrough(transform), {
22
25
  status: response.status,
23
26
  statusText: response.statusText,
24
27
  headers: response.headers,
@@ -1 +1 @@
1
- {"version":3,"file":"transformed-response.js","sourceRoot":"","sources":["../src/transformed-response.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,MAAa,mBAAoB,SAAQ,QAAQ;IAG/C,YAAY,QAAkB,EAAE,SAA0B;QACxD,IAAI,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;YACvC,MAAM,IAAI,SAAS,CAAC,+BAA+B,CAAC,CAAA;QACtD,CAAC;QAED,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,SAAS,CAAC,IAAI,IAAI,EAAE;YACnD,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;YAC/B,OAAO,EAAE,QAAQ,CAAC,OAAO;SAC1B,CAAC,CAAA;QAXJ,gDAAmB;QAajB,uBAAA,IAAI,iCAAa,QAAQ,MAAA,CAAA;IAC3B,CAAC;IAED;;OAEG;IACH,IAAI,GAAG;QACL,OAAO,uBAAA,IAAI,qCAAU,CAAC,GAAG,CAAA;IAC3B,CAAC;IACD,IAAI,UAAU;QACZ,OAAO,uBAAA,IAAI,qCAAU,CAAC,UAAU,CAAA;IAClC,CAAC;IACD,IAAI,IAAI;QACN,OAAO,uBAAA,IAAI,qCAAU,CAAC,IAAI,CAAA;IAC5B,CAAC;IACD,IAAI,UAAU;QACZ,OAAO,uBAAA,IAAI,qCAAU,CAAC,UAAU,CAAA;IAClC,CAAC;CACF;AAhCD,kDAgCC"}
1
+ {"version":3,"file":"transformed-response.js","sourceRoot":"","sources":["../src/transformed-response.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,MAAa,mBAAoB,SAAQ,QAAQ;IAG/C,YAAY,QAAkB,EAAE,SAA0B;QACxD,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACnB,MAAM,IAAI,SAAS,CAAC,gCAAgC,CAAC,CAAA;QACvD,CAAC;QACD,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;YACtB,MAAM,IAAI,SAAS,CAAC,+BAA+B,CAAC,CAAA;QACtD,CAAC;QAED,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE;YAC1C,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;YAC/B,OAAO,EAAE,QAAQ,CAAC,OAAO;SAC1B,CAAC,CAAA;QAdJ,gDAAmB;QAgBjB,uBAAA,IAAI,iCAAa,QAAQ,MAAA,CAAA;IAC3B,CAAC;IAED;;OAEG;IACH,IAAI,GAAG;QACL,OAAO,uBAAA,IAAI,qCAAU,CAAC,GAAG,CAAA;IAC3B,CAAC;IACD,IAAI,UAAU;QACZ,OAAO,uBAAA,IAAI,qCAAU,CAAC,UAAU,CAAA;IAClC,CAAC;IACD,IAAI,IAAI;QACN,OAAO,uBAAA,IAAI,qCAAU,CAAC,IAAI,CAAA;IAC5B,CAAC;IACD,IAAI,UAAU;QACZ,OAAO,uBAAA,IAAI,qCAAU,CAAC,UAAU,CAAA;IAClC,CAAC;CACF;AAnCD,kDAmCC"}
package/dist/util.d.ts CHANGED
@@ -6,19 +6,51 @@ export type JsonObject = {
6
6
  [key: string]: Json;
7
7
  };
8
8
  export type JsonArray = Json[];
9
- declare global {
10
- interface JSON {
11
- parse(text: string, reviver?: (key: any, value: any) => any): Json;
12
- }
13
- }
9
+ export type ThisParameterOverride<C, Fn extends (...a: any) => any> = Fn extends (...args: infer P) => infer R ? ((this: C, ...args: P) => R) & {
10
+ bind(context: C): (...args: P) => R;
11
+ } : never;
14
12
  export declare function isIp(hostname: string): boolean;
15
- export declare const ifObject: <V>(v?: V) => (V extends symbol | Function | JsonScalar | JsonArray ? never : V extends Json ? V : {
16
- [key: string]: unknown;
17
- }) | undefined;
18
- export declare const ifArray: <V>(v?: V) => (V & any[]) | undefined;
19
- export declare const ifScalar: <V>(v?: V) => (V & string) | (V & number) | (V & boolean) | (V & null) | (V extends JsonScalar ? never : undefined);
20
- export declare const ifBoolean: <V>(v?: V) => (V & boolean) | undefined;
21
- export declare const ifString: <V>(v?: V) => (V & string) | undefined;
22
- export declare const ifNumber: <V>(v?: V) => (V & number) | undefined;
23
- export declare const ifNull: <V>(v?: V) => V | undefined;
13
+ export declare const ifObject: <V>(v: V) => (V extends symbol | Function | JsonScalar | JsonArray ? never : V extends Json ? V : Record<string, unknown>) | undefined;
14
+ export declare const ifString: <V>(v: V) => (V & string) | undefined;
15
+ export declare class MaxBytesTransformStream extends TransformStream<Uint8Array, Uint8Array> {
16
+ constructor(maxBytes: number);
17
+ }
18
+ export declare function padLines(input: string, pad: string): string;
19
+ /**
20
+ * @param [onCancellationError] - Callback that will trigger to asynchronously
21
+ * handle any error that occurs while cancelling the response body. Providing
22
+ * this will speed up the process and avoid potential deadlocks. Defaults to
23
+ * awaiting the cancellation operation. use `"log"` to log the error.
24
+ * @see {@link https://undici.nodejs.org/#/?id=garbage-collection}
25
+ * @note awaiting this function's result, when no `onCancellationError` is
26
+ * provided, might result in a dead lock. Indeed, if the response was cloned(),
27
+ * the response.body.cancel() method will not resolve until the other response's
28
+ * body is consumed/cancelled.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * // Make sure response was not cloned, or that every cloned response was
33
+ * // consumed/cancelled before awaiting this function's result.
34
+ * await cancelBody(response)
35
+ * ```
36
+ * @example
37
+ * ```ts
38
+ * await cancelBody(response, (err) => {
39
+ * // No biggie, let's just log the error
40
+ * console.warn('Failed to cancel response body', err)
41
+ * })
42
+ * ```
43
+ * @example
44
+ * ```ts
45
+ * // Will generate an "unhandledRejection" if an error occurs while cancelling
46
+ * // the response body. This will likely crash the process.
47
+ * await cancelBody(response, (err) => { throw err })
48
+ * ```
49
+ */
50
+ export declare function cancelBody(body: Body, onCancellationError?: 'log' | ((err: unknown) => void)): Promise<void>;
51
+ export declare function logCancellationError(err: unknown): void;
52
+ export declare function stringifyMessage(input: Body & {
53
+ headers: Headers;
54
+ }): Promise<string>;
55
+ export declare const extractUrl: (input: Request | string | URL) => URL;
24
56
  //# sourceMappingURL=util.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;AACzD,MAAM,MAAM,IAAI,GAAG,UAAU,GAAG,IAAI,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;CAAE,CAAA;AAC5E,MAAM,MAAM,UAAU,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAA;AAChD,MAAM,MAAM,SAAS,GAAG,IAAI,EAAE,CAAA;AAE9B,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,IAAI;QACZ,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,KAAK,GAAG,GAAG,IAAI,CAAA;KACnE;CACF;AAED,wBAAgB,IAAI,CAAC,QAAQ,EAAE,MAAM,WAQpC;AAKD,eAAO,MAAM,QAAQ,UAAW,CAAC;;cAehC,CAAA;AAED,eAAO,MAAM,OAAO,UAAW,CAAC,4BAAuC,CAAA;AACvE,eAAO,MAAM,QAAQ,UAAW,CAAC,0GAUhC,CAAA;AACD,eAAO,MAAM,SAAS,UAAW,CAAC,8BAA6C,CAAA;AAC/E,eAAO,MAAM,QAAQ,UAAW,CAAC,6BAA4C,CAAA;AAC7E,eAAO,MAAM,QAAQ,UAAW,CAAC,6BAA4C,CAAA;AAC7E,eAAO,MAAM,MAAM,UAAW,CAAC,kBAAiC,CAAA"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;AACzD,MAAM,MAAM,IAAI,GAAG,UAAU,GAAG,IAAI,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;CAAE,CAAA;AAC5E,MAAM,MAAM,UAAU,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAA;AAChD,MAAM,MAAM,SAAS,GAAG,IAAI,EAAE,CAAA;AAE9B,MAAM,MAAM,qBAAqB,CAC/B,CAAC,EACD,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,GAAG,KAAK,GAAG,IAC3B,EAAE,SAAS,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,GACxC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG;IAC7B,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,CAAC,CAAA;CACpC,GACD,KAAK,CAAA;AAET,wBAAgB,IAAI,CAAC,QAAQ,EAAE,MAAM,WAQpC;AAGD,eAAO,MAAM,QAAQ,SAAU,CAAC,8HAe/B,CAAA;AAED,eAAO,MAAM,QAAQ,SAAU,CAAC,6BAA4C,CAAA;AAE5E,qBAAa,uBAAwB,SAAQ,eAAe,CAC1D,UAAU,EACV,UAAU,CACX;gBACa,QAAQ,EAAE,MAAM;CAqB7B;AAGD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,UAGlD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,IAAI,EACV,mBAAmB,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC,GACrD,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAEvD;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,IAAI,GAAG;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,mBAQxE;AA2BD,eAAO,MAAM,UAAU,UAAW,OAAO,GAAG,MAAM,GAAG,GAAG,QAK9B,CAAA"}
package/dist/util.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
- // TODO: Move to a shared package ?
2
+ // @TODO: Move some of these to a shared package ?
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.ifNull = exports.ifNumber = exports.ifString = exports.ifBoolean = exports.ifScalar = exports.ifArray = exports.ifObject = exports.isIp = void 0;
4
+ exports.extractUrl = exports.stringifyMessage = exports.logCancellationError = exports.cancelBody = exports.padLines = exports.MaxBytesTransformStream = exports.ifString = exports.ifObject = exports.isIp = void 0;
5
5
  function isIp(hostname) {
6
6
  // IPv4
7
7
  if (hostname.match(/^\d+\.\d+\.\d+\.\d+$/))
@@ -12,7 +12,6 @@ function isIp(hostname) {
12
12
  return false;
13
13
  }
14
14
  exports.isIp = isIp;
15
- // TODO: Move to a shared package ?
16
15
  const plainObjectProto = Object.prototype;
17
16
  const ifObject = (v) => {
18
17
  if (typeof v === 'object' && v != null && !Array.isArray(v)) {
@@ -25,27 +24,125 @@ const ifObject = (v) => {
25
24
  return undefined;
26
25
  };
27
26
  exports.ifObject = ifObject;
28
- const ifArray = (v) => (Array.isArray(v) ? v : undefined);
29
- exports.ifArray = ifArray;
30
- const ifScalar = (v) => {
31
- switch (typeof v) {
32
- case 'string':
33
- case 'number':
34
- case 'boolean':
35
- return v;
36
- default:
37
- if (v === null)
38
- return null;
39
- return undefined;
40
- }
41
- };
42
- exports.ifScalar = ifScalar;
43
- const ifBoolean = (v) => (typeof v === 'boolean' ? v : undefined);
44
- exports.ifBoolean = ifBoolean;
45
27
  const ifString = (v) => (typeof v === 'string' ? v : undefined);
46
28
  exports.ifString = ifString;
47
- const ifNumber = (v) => (typeof v === 'number' ? v : undefined);
48
- exports.ifNumber = ifNumber;
49
- const ifNull = (v) => (v === null ? v : undefined);
50
- exports.ifNull = ifNull;
29
+ class MaxBytesTransformStream extends TransformStream {
30
+ constructor(maxBytes) {
31
+ // Note: negation accounts for invalid value types (NaN, non numbers)
32
+ if (!(maxBytes >= 0)) {
33
+ throw new TypeError('maxBytes must be a non-negative number');
34
+ }
35
+ let bytesRead = 0;
36
+ super({
37
+ transform: (chunk, ctrl) => {
38
+ if ((bytesRead += chunk.length) <= maxBytes) {
39
+ ctrl.enqueue(chunk);
40
+ }
41
+ else {
42
+ ctrl.error(new Error('Response too large'));
43
+ }
44
+ },
45
+ });
46
+ }
47
+ }
48
+ exports.MaxBytesTransformStream = MaxBytesTransformStream;
49
+ const LINE_BREAK = /\r?\n/g;
50
+ function padLines(input, pad) {
51
+ if (!input)
52
+ return input;
53
+ return pad + input.replace(LINE_BREAK, `$&${pad}`);
54
+ }
55
+ exports.padLines = padLines;
56
+ /**
57
+ * @param [onCancellationError] - Callback that will trigger to asynchronously
58
+ * handle any error that occurs while cancelling the response body. Providing
59
+ * this will speed up the process and avoid potential deadlocks. Defaults to
60
+ * awaiting the cancellation operation. use `"log"` to log the error.
61
+ * @see {@link https://undici.nodejs.org/#/?id=garbage-collection}
62
+ * @note awaiting this function's result, when no `onCancellationError` is
63
+ * provided, might result in a dead lock. Indeed, if the response was cloned(),
64
+ * the response.body.cancel() method will not resolve until the other response's
65
+ * body is consumed/cancelled.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * // Make sure response was not cloned, or that every cloned response was
70
+ * // consumed/cancelled before awaiting this function's result.
71
+ * await cancelBody(response)
72
+ * ```
73
+ * @example
74
+ * ```ts
75
+ * await cancelBody(response, (err) => {
76
+ * // No biggie, let's just log the error
77
+ * console.warn('Failed to cancel response body', err)
78
+ * })
79
+ * ```
80
+ * @example
81
+ * ```ts
82
+ * // Will generate an "unhandledRejection" if an error occurs while cancelling
83
+ * // the response body. This will likely crash the process.
84
+ * await cancelBody(response, (err) => { throw err })
85
+ * ```
86
+ */
87
+ async function cancelBody(body, onCancellationError) {
88
+ if (body.body &&
89
+ !body.bodyUsed &&
90
+ !body.body.locked &&
91
+ // Support for alternative fetch implementations
92
+ typeof body.body.cancel === 'function') {
93
+ if (typeof onCancellationError === 'function') {
94
+ void body.body.cancel().catch(onCancellationError);
95
+ }
96
+ else if (onCancellationError === 'log') {
97
+ void body.body.cancel().catch(logCancellationError);
98
+ }
99
+ else {
100
+ await body.body.cancel();
101
+ }
102
+ }
103
+ }
104
+ exports.cancelBody = cancelBody;
105
+ function logCancellationError(err) {
106
+ console.warn('Failed to cancel response body', err);
107
+ }
108
+ exports.logCancellationError = logCancellationError;
109
+ async function stringifyMessage(input) {
110
+ try {
111
+ const headers = stringifyHeaders(input.headers);
112
+ const payload = await stringifyBody(input);
113
+ return headers && payload ? `${headers}\n${payload}` : headers || payload;
114
+ }
115
+ finally {
116
+ void cancelBody(input, 'log');
117
+ }
118
+ }
119
+ exports.stringifyMessage = stringifyMessage;
120
+ function stringifyHeaders(headers) {
121
+ return Array.from(headers)
122
+ .map(([name, value]) => `${name}: ${value}`)
123
+ .join('\n');
124
+ }
125
+ async function stringifyBody(body) {
126
+ try {
127
+ const blob = await body.blob();
128
+ if (blob.type?.startsWith('text/')) {
129
+ const text = await blob.text();
130
+ return JSON.stringify(text);
131
+ }
132
+ if (/application\/(?:\w+\+)?json/.test(blob.type)) {
133
+ const text = await blob.text();
134
+ return text.includes('\n') ? JSON.stringify(JSON.parse(text)) : text;
135
+ }
136
+ return `[Body size: ${blob.size}, type: ${JSON.stringify(blob.type)} ]`;
137
+ }
138
+ catch {
139
+ return '[Body could not be read]';
140
+ }
141
+ }
142
+ const extractUrl = (input) => typeof input === 'string'
143
+ ? new URL(input)
144
+ : input instanceof URL
145
+ ? input
146
+ : new URL(input.url);
147
+ exports.extractUrl = extractUrl;
51
148
  //# 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,mCAAmC;;;AAanC,SAAgB,IAAI,CAAC,QAAgB;IACnC,OAAO;IACP,IAAI,QAAQ,CAAC,KAAK,CAAC,sBAAsB,CAAC;QAAE,OAAO,IAAI,CAAA;IAEvD,OAAO;IACP,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IAEnE,OAAO,KAAK,CAAA;AACd,CAAC;AARD,oBAQC;AAED,mCAAmC;AAEnC,MAAM,gBAAgB,GAAG,MAAM,CAAC,SAAS,CAAA;AAClC,MAAM,QAAQ,GAAG,CAAI,CAAK,EAAE,EAAE;IACnC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAA;QACtC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,gBAAgB,EAAE,CAAC;YACjD,wDAAwD;YACxD,OAAO,CAKyB,CAAA;QAClC,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC,CAAA;AAfY,QAAA,QAAQ,YAepB;AAEM,MAAM,OAAO,GAAG,CAAI,CAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AAA1D,QAAA,OAAO,WAAmD;AAChE,MAAM,QAAQ,GAAG,CAAI,CAAK,EAAE,EAAE;IACnC,QAAQ,OAAO,CAAC,EAAE,CAAC;QACjB,KAAK,QAAQ,CAAC;QACd,KAAK,QAAQ,CAAC;QACd,KAAK,SAAS;YACZ,OAAO,CAAC,CAAA;QACV;YACE,IAAI,CAAC,KAAK,IAAI;gBAAE,OAAO,IAAgB,CAAA;YACvC,OAAO,SAAqD,CAAA;IAChE,CAAC;AACH,CAAC,CAAA;AAVY,QAAA,QAAQ,YAUpB;AACM,MAAM,SAAS,GAAG,CAAI,CAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AAAlE,QAAA,SAAS,aAAyD;AACxE,MAAM,QAAQ,GAAG,CAAI,CAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AAAhE,QAAA,QAAQ,YAAwD;AACtE,MAAM,QAAQ,GAAG,CAAI,CAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AAAhE,QAAA,QAAQ,YAAwD;AACtE,MAAM,MAAM,GAAG,CAAI,CAAK,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AAAnD,QAAA,MAAM,UAA6C"}
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto-labs/fetch",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
4
4
  "license": "MIT",
5
5
  "description": "Isomorphic wrapper utilities for fetch API",
6
6
  "keywords": [
@@ -11,7 +11,7 @@
11
11
  "repository": {
12
12
  "type": "git",
13
13
  "url": "https://github.com/bluesky-social/atproto",
14
- "directory": "packages/fetch"
14
+ "directory": "packages/internal/fetch"
15
15
  },
16
16
  "type": "commonjs",
17
17
  "main": "dist/index.js",
@@ -23,13 +23,14 @@
23
23
  }
24
24
  },
25
25
  "dependencies": {
26
- "tslib": "^2.6.2",
27
- "zod": "^3.22.4",
28
- "@atproto-labs/transformer": "0.0.1"
26
+ "@atproto-labs/pipe": "0.1.0"
29
27
  },
30
28
  "devDependencies": {
31
29
  "typescript": "^5.3.3"
32
30
  },
31
+ "optionalDependencies": {
32
+ "zod": "^3.23.8"
33
+ },
33
34
  "scripts": {
34
35
  "build": "tsc --build tsconfig.json"
35
36
  }
@@ -1,44 +1,26 @@
1
- import { Transformer } from '@atproto-labs/transformer'
2
-
3
- export type FetchErrorOptions = {
4
- cause?: unknown
5
- request?: Request
6
- response?: Response
7
- }
8
-
9
1
  export class FetchError extends Error {
10
- public readonly request?: Request
11
- public readonly response?: Response
2
+ public readonly statusCode: number
12
3
 
13
- constructor(
14
- public readonly statusCode: number,
15
- message?: string,
16
- { cause, request, response }: FetchErrorOptions = {},
17
- ) {
18
- super(message, { cause })
19
- this.request = request
20
- this.response = response
21
- }
4
+ constructor(statusCode?: number, message?: string, options?: ErrorOptions) {
5
+ if (statusCode == null || !message) {
6
+ const info = extractInfo(extractRootCause(options?.cause))
7
+ statusCode = statusCode ?? info[0]
8
+ message = message || info[1]
9
+ }
22
10
 
23
- static async from(err: unknown) {
24
- const cause = extractCause(err)
25
- return new FetchError(...extractInfo(cause), { cause })
26
- }
27
- }
11
+ super(message, options)
28
12
 
29
- export const fetchFailureHandler: Transformer<unknown, never> = async (
30
- err: unknown,
31
- ) => {
32
- throw await FetchError.from(err)
13
+ this.statusCode = statusCode
14
+ }
33
15
  }
34
16
 
35
- function extractCause(err: unknown): unknown {
17
+ function extractRootCause(err: unknown): unknown {
36
18
  // Unwrap the Network error from undici (i.e. Node's internal fetch() implementation)
37
19
  // https://github.com/nodejs/undici/blob/3274c975947ce11a08508743df026f73598bfead/lib/web/fetch/index.js#L223-L228
38
20
  if (
39
21
  err instanceof TypeError &&
40
22
  err.message === 'fetch failed' &&
41
- err.cause instanceof Error
23
+ err.cause !== undefined
42
24
  ) {
43
25
  return err.cause
44
26
  }
@@ -46,32 +28,32 @@ function extractCause(err: unknown): unknown {
46
28
  return err
47
29
  }
48
30
 
49
- export function extractInfo(
50
- err: unknown,
51
- ): [statusCode: number, message: string] {
31
+ function extractInfo(err: unknown): [statusCode: number, message: string] {
52
32
  if (typeof err === 'string' && err.length > 0) {
53
- return [502, err]
33
+ return [500, err]
54
34
  }
55
35
 
56
36
  if (!(err instanceof Error)) {
57
- return [502, 'Unable to fetch']
37
+ return [500, 'Failed to fetch']
58
38
  }
59
39
 
60
- if ('code' in err && typeof err.code === 'string') {
40
+ const code = err['code']
41
+ if (typeof code === 'string') {
61
42
  switch (true) {
62
- case err.code === 'ENOTFOUND':
63
- return [404, 'Invalid hostname']
64
- case err.code === 'ECONNREFUSED':
43
+ case code === 'ENOTFOUND':
44
+ return [400, 'Invalid hostname']
45
+ case code === 'ECONNREFUSED':
65
46
  return [502, 'Connection refused']
66
- case err.code === 'DEPTH_ZERO_SELF_SIGNED_CERT':
47
+ case code === 'DEPTH_ZERO_SELF_SIGNED_CERT':
67
48
  return [502, 'Self-signed certificate']
68
- case err.code.startsWith('ERR_TLS'):
49
+ case code.startsWith('ERR_TLS'):
69
50
  return [502, 'TLS error']
70
- case err.code.startsWith('ECONN'):
51
+ case code.startsWith('ECONN'):
71
52
  return [502, 'Connection error']
53
+ default:
54
+ return [500, `${code} error`]
72
55
  }
73
56
  }
74
57
 
75
- // Let's assume that other errors are "bad gateway" errors
76
- return [502, err.message]
58
+ return [500, err.message]
77
59
  }
@@ -1,40 +1,95 @@
1
- import { Transformer } from '@atproto-labs/transformer'
2
-
3
1
  import { FetchError } from './fetch-error.js'
4
- import { isIp } from './util.js'
2
+ import { asRequest } from './fetch.js'
3
+ import { extractUrl, isIp } from './util.js'
4
+
5
+ export class FetchRequestError extends FetchError {
6
+ constructor(
7
+ public readonly request: Request,
8
+ statusCode?: number,
9
+ message?: string,
10
+ options?: ErrorOptions,
11
+ ) {
12
+ super(statusCode, message, options)
13
+ }
5
14
 
6
- export type RequestTranformer = Transformer<Request>
15
+ static from(request: Request, cause: unknown): FetchRequestError {
16
+ if (cause instanceof FetchRequestError) return cause
17
+ return new FetchRequestError(request, undefined, undefined, { cause })
18
+ }
19
+ }
7
20
 
8
- export function protocolCheckRequestTransform(
9
- protocols: Iterable<string>,
10
- ): RequestTranformer {
11
- const allowedProtocols = new Set<string>(protocols)
21
+ export function protocolCheckRequestTransform(protocols: {
22
+ 'about:'?: boolean
23
+ 'blob:'?: boolean
24
+ 'data:'?: boolean
25
+ 'file:'?: boolean
26
+ 'http:'?: boolean | { allowCustomPort: boolean }
27
+ 'https:'?: boolean | { allowCustomPort: boolean }
28
+ }) {
29
+ return (input: Request | string | URL, init?: RequestInit) => {
30
+ const { protocol, port } = extractUrl(input)
31
+
32
+ const request = asRequest(input, init)
33
+
34
+ const config: undefined | boolean | { allowCustomPort?: boolean } =
35
+ Object.hasOwn(protocols, protocol) ? protocols[protocol] : undefined
36
+
37
+ if (!config) {
38
+ throw new FetchRequestError(
39
+ request,
40
+ 400,
41
+ `Forbidden protocol "${protocol}"`,
42
+ )
43
+ } else if (config === true) {
44
+ // Safe to proceed
45
+ } else if (!config['allowCustomPort'] && port !== '') {
46
+ throw new FetchRequestError(
47
+ request,
48
+ 400,
49
+ `Custom ${protocol} ports not allowed`,
50
+ )
51
+ }
12
52
 
13
- return async (request) => {
14
- const { protocol } = new URL(request.url)
53
+ return request
54
+ }
55
+ }
56
+
57
+ export function redirectCheckRequestTransform() {
58
+ return (input: Request | string | URL, init?: RequestInit) => {
59
+ const request = asRequest(input, init)
15
60
 
16
- if (!allowedProtocols.has(protocol)) {
17
- throw new FetchError(400, `${protocol} is not allowed`, { request })
61
+ if (request.redirect === 'follow') {
62
+ throw new FetchRequestError(
63
+ request,
64
+ 500,
65
+ 'Request redirect must be "error" or "manual"',
66
+ )
18
67
  }
19
68
 
20
69
  return request
21
70
  }
22
71
  }
23
72
 
24
- export function requireHostHeaderTranform(): RequestTranformer {
25
- return async (request) => {
73
+ export function requireHostHeaderTransform() {
74
+ return (input: Request | string | URL, init?: RequestInit) => {
26
75
  // Note that fetch() will automatically add the Host header from the URL and
27
76
  // discard any Host header manually set in the request.
28
77
 
29
- const { protocol, hostname } = new URL(request.url)
78
+ const { protocol, hostname } = extractUrl(input)
79
+
80
+ const request = asRequest(input, init)
30
81
 
31
82
  // "Host" header only makes sense in the context of an HTTP request
32
- if (protocol !== 'http:' && protocol !== 'https') {
33
- throw new FetchError(400, `Forbidden protocol ${protocol}`, { request })
83
+ if (protocol !== 'http:' && protocol !== 'https:') {
84
+ throw new FetchRequestError(
85
+ request,
86
+ 400,
87
+ `"${protocol}" requests are not allowed`,
88
+ )
34
89
  }
35
90
 
36
91
  if (!hostname || isIp(hostname)) {
37
- throw new FetchError(400, 'Invalid hostname', { request })
92
+ throw new FetchRequestError(request, 400, 'Invalid hostname')
38
93
  }
39
94
 
40
95
  return request
@@ -54,21 +109,23 @@ export const DEFAULT_FORBIDDEN_DOMAIN_NAMES = [
54
109
 
55
110
  export function forbiddenDomainNameRequestTransform(
56
111
  denyList: Iterable<string> = DEFAULT_FORBIDDEN_DOMAIN_NAMES,
57
- ): RequestTranformer {
112
+ ) {
58
113
  const denySet = new Set<string>(denyList)
59
114
 
60
115
  // Optimization: if no forbidden domain names are provided, we can skip the
61
116
  // check entirely.
62
117
  if (denySet.size === 0) {
63
- return async (request) => request
118
+ return asRequest
64
119
  }
65
120
 
66
- return async (request) => {
67
- const { hostname } = new URL(request.url)
121
+ return async (input: Request | string | URL, init?: RequestInit) => {
122
+ const { hostname } = extractUrl(input)
123
+
124
+ const request = asRequest(input, init)
68
125
 
69
126
  // Full domain name check
70
127
  if (denySet.has(hostname)) {
71
- throw new FetchError(403, 'Forbidden hostname', { request })
128
+ throw new FetchRequestError(request, 403, 'Forbidden hostname')
72
129
  }
73
130
 
74
131
  // Sub domain name check
@@ -76,7 +133,7 @@ export function forbiddenDomainNameRequestTransform(
76
133
  while (curDot !== -1) {
77
134
  const subdomain = hostname.slice(curDot + 1)
78
135
  if (denySet.has(`*.${subdomain}`)) {
79
- throw new FetchError(403, 'Forbidden hostname', { request })
136
+ throw new FetchRequestError(request, 403, 'Forbidden hostname')
80
137
  }
81
138
  curDot = hostname.indexOf('.', curDot + 1)
82
139
  }