@ovencord/rest 2.5.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.
@@ -0,0 +1,406 @@
1
+ import type { Collection } from '@ovencord/collection';
2
+ import type { Awaitable, RawFile } from '@ovencord/util';
3
+ import type { IHandler } from '../interfaces/Handler.js';
4
+
5
+ export interface RestEvents {
6
+ handlerSweep: [sweptHandlers: Collection<string, IHandler>];
7
+ hashSweep: [sweptHashes: Collection<string, HashData>];
8
+ invalidRequestWarning: [invalidRequestInfo: InvalidRequestWarningData];
9
+ rateLimited: [rateLimitInfo: RateLimitData];
10
+ response: [request: APIRequest, response: ResponseLike];
11
+ restDebug: [info: string];
12
+ }
13
+
14
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
15
+ export interface RestEventsMap extends RestEvents {}
16
+
17
+ /**
18
+ * Options to be passed when creating the REST instance
19
+ */
20
+ export interface RESTOptions {
21
+ /**
22
+ * The agent to set globally
23
+ *
24
+ * @deprecated This property is deprecated and has no effect when using Bun native fetch.
25
+ * It is kept for API compatibility but will always be `null`.
26
+ */
27
+ agent: null;
28
+ /**
29
+ * The base api path, without version
30
+ *
31
+ * @defaultValue `'https://discord.com/api'`
32
+ */
33
+ api: string;
34
+ /**
35
+ * The authorization prefix to use for requests, useful if you want to use
36
+ * bearer tokens
37
+ *
38
+ * @defaultValue `'Bot'`
39
+ */
40
+ authPrefix: 'Bearer' | 'Bot';
41
+ /**
42
+ * The cdn path
43
+ *
44
+ * @defaultValue `'https://cdn.discordapp.com'`
45
+ */
46
+ cdn: string;
47
+ /**
48
+ * How many requests to allow sending per second (Infinity for unlimited, 50 for the standard global limit used by Discord)
49
+ *
50
+ * @defaultValue `50`
51
+ */
52
+ globalRequestsPerSecond: number;
53
+ /**
54
+ * The amount of time in milliseconds that passes between each hash sweep. (defaults to 1h)
55
+ *
56
+ * @defaultValue `3_600_000`
57
+ */
58
+ handlerSweepInterval: number;
59
+ /**
60
+ * The maximum amount of time a hash can exist in milliseconds without being hit with a request (defaults to 24h)
61
+ *
62
+ * @defaultValue `86_400_000`
63
+ */
64
+ hashLifetime: number;
65
+ /**
66
+ * The amount of time in milliseconds that passes between each hash sweep. (defaults to 4h)
67
+ *
68
+ * @defaultValue `14_400_000`
69
+ */
70
+ hashSweepInterval: number;
71
+ /**
72
+ * Additional headers to send for all API requests
73
+ *
74
+ * @defaultValue `{}`
75
+ */
76
+ headers: Record<string, string>;
77
+ /**
78
+ * The number of invalid REST requests (those that return 401, 403, or 429) in a 10 minute window between emitted warnings (0 for no warnings).
79
+ * That is, if set to 500, warnings will be emitted at invalid request number 500, 1000, 1500, and so on.
80
+ *
81
+ * @defaultValue `0`
82
+ */
83
+ invalidRequestWarningInterval: number;
84
+ /**
85
+ * The method called to perform the actual HTTP request given a url and web `fetch` options
86
+ * For example, to use global fetch, simply provide `makeRequest: fetch`
87
+ */
88
+ makeRequest(url: string, init: RequestInit): Promise<ResponseLike>;
89
+ /**
90
+ * The media proxy path
91
+ *
92
+ * @defaultValue `'https://media.discordapp.net'`
93
+ */
94
+ mediaProxy: string;
95
+ /**
96
+ * The extra offset to add to rate limits in milliseconds
97
+ *
98
+ * @defaultValue `50`
99
+ */
100
+ offset: GetRateLimitOffsetFunction | number;
101
+ /**
102
+ * Determines how rate limiting and pre-emptive throttling should be handled.
103
+ * When an array of strings, each element is treated as a prefix for the request route
104
+ * (e.g. `/channels` to match any route starting with `/channels` such as `/channels/:id/messages`)
105
+ * for which to throw {@link RateLimitError}s. All other request routes will be queued normally
106
+ *
107
+ * @defaultValue `null`
108
+ */
109
+ rejectOnRateLimit: RateLimitQueueFilter | string[] | null;
110
+ /**
111
+ * The number of retries for errors with the 500 code, or errors
112
+ * that timeout
113
+ *
114
+ * @defaultValue `3`
115
+ */
116
+ retries: number;
117
+ /**
118
+ * The time to exponentially add before retrying a 5xx or aborted request
119
+ *
120
+ * @defaultValue `0`
121
+ */
122
+ retryBackoff: GetRetryBackoffFunction | number;
123
+ /**
124
+ * The time to wait in milliseconds before a request is aborted
125
+ *
126
+ * @defaultValue `15_000`
127
+ */
128
+ timeout: GetTimeoutFunction | number;
129
+ /**
130
+ * Extra information to add to the user agent
131
+ *
132
+ * @defaultValue DefaultUserAgentAppendix
133
+ */
134
+ userAgentAppendix: string;
135
+ /**
136
+ * The version of the API to use
137
+ *
138
+ * @defaultValue `'10'`
139
+ */
140
+ version: string;
141
+ }
142
+
143
+ /**
144
+ * Data emitted on `RESTEvents.RateLimited`
145
+ */
146
+ export interface RateLimitData {
147
+ /**
148
+ * Whether the rate limit that was reached was the global limit
149
+ */
150
+ global: boolean;
151
+ /**
152
+ * The bucket hash for this request
153
+ */
154
+ hash: string;
155
+ /**
156
+ * The amount of requests we can perform before locking requests
157
+ */
158
+ limit: number;
159
+ /**
160
+ * The major parameter of the route
161
+ *
162
+ * For example, in `/channels/x`, this will be `x`.
163
+ * If there is no major parameter (e.g: `/bot/gateway`) this will be `global`.
164
+ */
165
+ majorParameter: string;
166
+ /**
167
+ * The HTTP method being performed
168
+ */
169
+ method: string;
170
+ /**
171
+ * The time, in milliseconds, that will need to pass before this specific request can be retried
172
+ */
173
+ retryAfter: number;
174
+ /**
175
+ * The route being hit in this request
176
+ */
177
+ route: string;
178
+ /**
179
+ * The scope of the rate limit that was hit.
180
+ *
181
+ * This can be `user` for rate limits that are per client, `global` for rate limits that affect all clients or `shared` for rate limits that
182
+ * are shared per resource.
183
+ */
184
+ scope: 'global' | 'shared' | 'user';
185
+ /**
186
+ * The time, in milliseconds, that will need to pass before the sublimit lock for the route resets, and requests that fall under a sublimit
187
+ * can be retried
188
+ *
189
+ * This is only present on certain sublimits, and `0` otherwise
190
+ */
191
+ sublimitTimeout: number;
192
+ /**
193
+ * The time, in milliseconds, until the route's request-lock is reset
194
+ */
195
+ timeToReset: number;
196
+ /**
197
+ * The full URL for this request
198
+ */
199
+ url: string;
200
+ }
201
+
202
+ /**
203
+ * A function that determines whether the rate limit hit should throw an Error
204
+ */
205
+ export type RateLimitQueueFilter = (rateLimitData: RateLimitData) => Awaitable<boolean>;
206
+
207
+ /**
208
+ * A function that determines the rate limit offset for a given request.
209
+ */
210
+ export type GetRateLimitOffsetFunction = (route: string) => number;
211
+
212
+ /**
213
+ * A function that determines the backoff for a retry for a given request.
214
+ *
215
+ * @param route - The route that has encountered a server-side error
216
+ * @param statusCode - The status code received or `null` if aborted
217
+ * @param retryCount - The number of retries that have been attempted so far. The first call will be `0`
218
+ * @param requestBody - The body that was sent with the request
219
+ * @returns The delay for the current request or `null` to throw an error instead of retrying
220
+ */
221
+ export type GetRetryBackoffFunction = (
222
+ route: string,
223
+ statusCode: number | null,
224
+ retryCount: number,
225
+ requestBody: unknown,
226
+ ) => number | null;
227
+
228
+ /**
229
+ * A function that determines the timeout for a given request.
230
+ *
231
+ * @param route - The route that is being processed
232
+ * @param body - The body that will be sent with the request
233
+ */
234
+ export type GetTimeoutFunction = (route: string, body: unknown) => number;
235
+
236
+ export interface APIRequest {
237
+ /**
238
+ * The data that was used to form the body of this request
239
+ */
240
+ data: HandlerRequestData;
241
+ /**
242
+ * The HTTP method used in this request
243
+ */
244
+ method: string;
245
+ /**
246
+ * Additional HTTP options for this request
247
+ */
248
+ options: RequestInit;
249
+ /**
250
+ * The full path used to make the request
251
+ */
252
+ path: RouteLike;
253
+ /**
254
+ * The number of times this request has been attempted
255
+ */
256
+ retries: number;
257
+ /**
258
+ * The API route identifying the ratelimit for this request
259
+ */
260
+ route: string;
261
+ }
262
+
263
+ export interface ResponseLike extends Pick<
264
+ globalThis.Response,
265
+ 'arrayBuffer' | 'bodyUsed' | 'headers' | 'json' | 'ok' | 'status' | 'statusText' | 'text'
266
+ > {
267
+ body: ReadableStream | null;
268
+ }
269
+
270
+ export interface InvalidRequestWarningData {
271
+ /**
272
+ * Number of invalid requests that have been made in the window
273
+ */
274
+ count: number;
275
+ /**
276
+ * Time in milliseconds remaining before the count resets
277
+ */
278
+ remainingTime: number;
279
+ }
280
+
281
+ export type { RawFile } from '@ovencord/util';
282
+
283
+ export interface AuthData {
284
+ /**
285
+ * The authorization prefix to use for this request, useful if you use this with bearer tokens
286
+ *
287
+ * @defaultValue `REST.options.authPrefix`
288
+ */
289
+ prefix?: 'Bearer' | 'Bot';
290
+ /**
291
+ * The authorization token to use for this request
292
+ */
293
+ token: string;
294
+ }
295
+
296
+ /**
297
+ * Represents possible data to be given to an endpoint
298
+ */
299
+ export interface RequestData {
300
+ /**
301
+ * Whether to append JSON data to form data instead of `payload_json` when sending files
302
+ */
303
+ appendToFormData?: boolean;
304
+ /**
305
+ * Alternate authorization data to use for this request only, or `false` to disable the Authorization header.
306
+ * When making a request to a route that includes a token (such as interactions or webhooks), set to `false`
307
+ * to avoid accidentally unsetting the instance token if a 401 is encountered.
308
+ *
309
+ * @defaultValue `true`
310
+ */
311
+ auth?: AuthData | boolean | undefined;
312
+ /**
313
+ * The body to send to this request.
314
+ * If providing as BodyInit, set `passThroughBody: true`
315
+ */
316
+ body?: BodyInit | unknown;
317
+ /**
318
+ * The {@link https://undici.nodejs.org/#/docs/api/Agent | Agent} to use for the request.
319
+ *
320
+ * @deprecated This property is deprecated and has no effect when using Bun native fetch.
321
+ * It is kept for API compatibility but will be ignored.
322
+ */
323
+ dispatcher?: never;
324
+ /**
325
+ * Files to be attached to this request
326
+ */
327
+ files?: RawFile[] | undefined;
328
+ /**
329
+ * Additional headers to add to this request
330
+ */
331
+ headers?: Record<string, string>;
332
+ /**
333
+ * Whether to pass-through the body property directly to `fetch()`.
334
+ * <warn>This only applies when files is NOT present</warn>
335
+ */
336
+ passThroughBody?: boolean;
337
+ /**
338
+ * Query string parameters to append to the called endpoint
339
+ */
340
+ query?: URLSearchParams;
341
+ /**
342
+ * Reason to show in the audit logs
343
+ */
344
+ reason?: string | undefined;
345
+ /**
346
+ * The signal to abort the queue entry or the REST call, where applicable
347
+ */
348
+ signal?: AbortSignal | undefined;
349
+ /**
350
+ * If this request should be versioned
351
+ *
352
+ * @defaultValue `true`
353
+ */
354
+ versioned?: boolean;
355
+ }
356
+
357
+ /**
358
+ * Possible headers for an API call
359
+ */
360
+ export interface RequestHeaders {
361
+ Authorization?: string;
362
+ 'User-Agent': string;
363
+ 'X-Audit-Log-Reason'?: string;
364
+ }
365
+
366
+ /**
367
+ * Possible API methods to be used when doing requests
368
+ */
369
+ export enum RequestMethod {
370
+ Delete = 'DELETE',
371
+ Get = 'GET',
372
+ Patch = 'PATCH',
373
+ Post = 'POST',
374
+ Put = 'PUT',
375
+ }
376
+
377
+ export type RouteLike = `/${string}`;
378
+
379
+ /**
380
+ * Internal request options
381
+ */
382
+ export interface InternalRequest extends RequestData {
383
+ fullRoute: RouteLike;
384
+ method: RequestMethod;
385
+ }
386
+
387
+ export interface HandlerRequestData extends Pick<InternalRequest, 'body' | 'files' | 'signal'> {
388
+ auth: boolean | string;
389
+ }
390
+
391
+ /**
392
+ * Parsed route data for an endpoint
393
+ */
394
+ export interface RouteData {
395
+ bucketRoute: string;
396
+ majorParameter: string;
397
+ original: RouteLike;
398
+ }
399
+
400
+ /**
401
+ * Represents a hash and its associated fields
402
+ */
403
+ export interface HashData {
404
+ lastAccess: number;
405
+ value: string;
406
+ }
@@ -0,0 +1,248 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type { RESTPatchAPIChannelJSONBody, Snowflake } from 'discord-api-types/v10';
3
+ import type { REST } from '../REST.js';
4
+ import { RateLimitError } from '../errors/RateLimitError.js';
5
+ import { RequestMethod } from './types.js';
6
+ import type {
7
+ GetRateLimitOffsetFunction,
8
+ GetRetryBackoffFunction,
9
+ GetTimeoutFunction,
10
+ RateLimitData,
11
+ ResponseLike,
12
+ } from './types.js';
13
+
14
+ function serializeSearchParam(value: unknown): string | null {
15
+ switch (typeof value) {
16
+ case 'string':
17
+ return value;
18
+ case 'number':
19
+ case 'bigint':
20
+ case 'boolean':
21
+ return value.toString();
22
+ case 'object':
23
+ if (value === null) return null;
24
+ if (value instanceof Date) {
25
+ return Number.isNaN(value.getTime()) ? null : value.toISOString();
26
+ }
27
+
28
+ if (typeof value.toString === 'function' && value.toString !== Object.prototype.toString) return value.toString();
29
+ return null;
30
+ default:
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Creates and populates an URLSearchParams instance from an object, stripping
37
+ * out null and undefined values, while also coercing non-strings to strings.
38
+ *
39
+ * @param options - The options to use
40
+ * @returns A populated URLSearchParams instance
41
+ */
42
+ export function makeURLSearchParams<OptionsType extends object>(options?: Readonly<OptionsType>) {
43
+ const params = new URLSearchParams();
44
+ if (!options) return params;
45
+
46
+ for (const [key, value] of Object.entries(options)) {
47
+ const serialized = serializeSearchParam(value);
48
+ if (serialized !== null) params.append(key, serialized);
49
+ }
50
+
51
+ return params;
52
+ }
53
+
54
+ /**
55
+ * Converts the response to usable data
56
+ *
57
+ * @param res - The fetch response
58
+ */
59
+ export async function parseResponse(res: ResponseLike): Promise<unknown> {
60
+ if (res.headers.get('Content-Type')?.startsWith('application/json')) {
61
+ return res.json();
62
+ }
63
+
64
+ return res.arrayBuffer();
65
+ }
66
+
67
+ /**
68
+ * Check whether a request falls under a sublimit
69
+ *
70
+ * @param bucketRoute - The buckets route identifier
71
+ * @param body - The options provided as JSON data
72
+ * @param method - The HTTP method that will be used to make the request
73
+ * @returns Whether the request falls under a sublimit
74
+ */
75
+ export function hasSublimit(bucketRoute: string, body?: unknown, method?: string): boolean {
76
+ // TODO: Update for new sublimits
77
+ // Currently known sublimits:
78
+ // Editing channel `name` or `topic`
79
+ if (bucketRoute === '/channels/:id') {
80
+ if (typeof body !== 'object' || body === null) return false;
81
+ // This should never be a POST body, but just in case
82
+ if (method !== RequestMethod.Patch) return false;
83
+ const castedBody = body as RESTPatchAPIChannelJSONBody;
84
+ return ['name', 'topic'].some((key) => Reflect.has(castedBody, key));
85
+ }
86
+
87
+ // If we are checking if a request has a sublimit on a route not checked above, sublimit all requests to avoid a flood of 429s
88
+ return true;
89
+ }
90
+
91
+ /**
92
+ * Check whether an error indicates that a retry can be attempted
93
+ *
94
+ * @param error - The error thrown by the network request
95
+ * @returns Whether the error indicates a retry should be attempted
96
+ */
97
+ export function shouldRetry(error: Error & { code?: string }) {
98
+ // Retry for possible timed out requests
99
+ if (error.name === 'AbortError') return true;
100
+ // Downlevel ECONNRESET to retry as it may be recoverable
101
+ return ('code' in error && error.code === 'ECONNRESET') || error.message.includes('ECONNRESET');
102
+ }
103
+
104
+ /**
105
+ * Determines whether the request should be queued or whether a RateLimitError should be thrown
106
+ *
107
+ * @internal
108
+ */
109
+ export async function onRateLimit(manager: REST, rateLimitData: RateLimitData) {
110
+ const { options } = manager;
111
+ if (!options.rejectOnRateLimit) return;
112
+
113
+ const shouldThrow =
114
+ typeof options.rejectOnRateLimit === 'function'
115
+ ? await options.rejectOnRateLimit(rateLimitData)
116
+ : options.rejectOnRateLimit.some((route) => rateLimitData.route.startsWith(route.toLowerCase()));
117
+ if (shouldThrow) {
118
+ throw new RateLimitError(rateLimitData);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Calculates the default avatar index for a given user id.
124
+ *
125
+ * @param userId - The user id to calculate the default avatar index for
126
+ */
127
+ export function calculateUserDefaultAvatarIndex(userId: Snowflake) {
128
+ return Number(BigInt(userId) >> 22n) % 6;
129
+ }
130
+
131
+ /**
132
+ * Sleeps for a given amount of time.
133
+ *
134
+ * @param ms - The amount of time (in milliseconds) to sleep for
135
+ */
136
+ export async function sleep(ms: number): Promise<void> {
137
+ return new Promise<void>((resolve) => {
138
+ setTimeout(() => resolve(), ms);
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Verifies that a value is a buffer-like object.
144
+ *
145
+ * @param value - The value to check
146
+ */
147
+ export function isBufferLike(value: unknown): value is ArrayBuffer | Uint8Array | Uint8ClampedArray {
148
+ return value instanceof ArrayBuffer || value instanceof Uint8Array || value instanceof Uint8ClampedArray;
149
+ }
150
+
151
+ /**
152
+ * Normalizes the offset for rate limits. Applies a Math.max(0, N) to prevent negative offsets,
153
+ * also deals with callbacks.
154
+ *
155
+ * @internal
156
+ */
157
+ export function normalizeRateLimitOffset(offset: GetRateLimitOffsetFunction | number, route: string): number {
158
+ if (typeof offset === 'number') {
159
+ return Math.max(0, offset);
160
+ }
161
+
162
+ const result = offset(route);
163
+ return Math.max(0, result);
164
+ }
165
+
166
+ /**
167
+ * Normalizes the retry backoff used to add delay to retrying 5xx and aborted requests.
168
+ * Applies a Math.max(0, N) to prevent negative backoffs, also deals with callbacks.
169
+ *
170
+ * @internal
171
+ */
172
+ export function normalizeRetryBackoff(
173
+ retryBackoff: GetRetryBackoffFunction | number,
174
+ route: string,
175
+ statusCode: number | null,
176
+ retryCount: number,
177
+ requestBody: unknown,
178
+ ): number | null {
179
+ if (typeof retryBackoff === 'number') {
180
+ return Math.max(0, retryBackoff) * (1 << retryCount);
181
+ }
182
+
183
+ // No need to Math.max as we'll only set the sleep timer if the value is > 0 (and not equal)
184
+ return retryBackoff(route, statusCode, retryCount, requestBody);
185
+ }
186
+
187
+ /**
188
+ * Normalizes the timeout for aborting requests. Applies a Math.max(0, N) to prevent negative timeouts,
189
+ * also deals with callbacks.
190
+ *
191
+ * @internal
192
+ */
193
+ export function normalizeTimeout(timeout: GetTimeoutFunction | number, route: string, requestBody: unknown): number {
194
+ if (typeof timeout === 'number') {
195
+ return Math.max(0, timeout);
196
+ }
197
+ const result = timeout(route, requestBody);
198
+ return Math.max(0, result);
199
+ }
200
+
201
+ /**
202
+ * Generates a UUID v5 according to RFC 4122
203
+ *
204
+ * @param value - The value to hash
205
+ * @param namespace - The namespace UUID
206
+ */
207
+ export function uuidv5(value: string | Uint8Array, namespace: string): string {
208
+ // 1. Verify namespace is a valid UUID
209
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(namespace)) {
210
+ throw new TypeError('Invalid namespace UUID');
211
+ }
212
+
213
+ // 2. Parse namespace UUID into bytes
214
+ const namespaceBytes = new Uint8Array(16);
215
+ const hex = namespace.replace(/-/g, '');
216
+ for (let i = 0; i < 16; i++) {
217
+ namespaceBytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
218
+ }
219
+
220
+ // 3. Convert value to bytes if string
221
+ const valueBytes = typeof value === 'string' ? new TextEncoder().encode(value) : value;
222
+
223
+ // 4. Concatenate namespace and value
224
+ const data = new Uint8Array(namespaceBytes.length + valueBytes.length);
225
+ data.set(namespaceBytes);
226
+ data.set(valueBytes, namespaceBytes.length);
227
+
228
+ // 5. Hash with SHA-1
229
+ const buffer = createHash('sha1').update(data).digest();
230
+ const hash = new Uint8Array(buffer);
231
+
232
+ // 6. Set version to 5 (0101)
233
+ hash[6] = (hash[6]! & 0x0f) | 0x50;
234
+
235
+ // 7. Set variant to RFC 4122 (10xx)
236
+ hash[8] = (hash[8]! & 0x3f) | 0x80;
237
+
238
+ // 8. Convert to hex string with dashes
239
+ const hexHash = Array.from(hash, (byte) => byte.toString(16).padStart(2, '0'));
240
+
241
+ return [
242
+ hexHash.slice(0, 4).join(''),
243
+ hexHash.slice(4, 6).join(''),
244
+ hexHash.slice(6, 8).join(''),
245
+ hexHash.slice(8, 10).join(''),
246
+ hexHash.slice(10, 16).join('')
247
+ ].join('-');
248
+ }
package/src/shared.ts ADDED
@@ -0,0 +1,16 @@
1
+ export * from './lib/CDN.js';
2
+ export * from './lib/errors/DiscordAPIError.js';
3
+ export * from './lib/errors/HTTPError.js';
4
+ export * from './lib/errors/RateLimitError.js';
5
+ export type * from './lib/interfaces/Handler.js';
6
+ export * from './lib/REST.js';
7
+ export * from './lib/utils/constants.js';
8
+ export * from './lib/utils/types.js';
9
+ export { calculateUserDefaultAvatarIndex, makeURLSearchParams, parseResponse } from './lib/utils/utils.js';
10
+
11
+ /**
12
+ * The {@link https://github.com/ovencord/ovencord/blob/main/packages/rest#readme | @ovencord/rest} version
13
+ * that you are currently using.
14
+ */
15
+ // This needs to explicitly be `string` so it is not typed as a "const string" that gets injected by esbuild
16
+ export const version = '[VI]{{inject}}[/VI]' as string;
@@ -0,0 +1,32 @@
1
+ import type { ResponseLike } from '../shared.js';
2
+
3
+ /**
4
+ * Makes an HTTP request using Bun's native fetch API
5
+ *
6
+ * @param url - The URL to request
7
+ * @param init - Fetch options
8
+ * @returns A response-like object compatible with the REST manager
9
+ */
10
+ export async function makeRequest(url: string, init: RequestInit): Promise<ResponseLike> {
11
+ const res = await fetch(url, init);
12
+
13
+ return {
14
+ body: res.body,
15
+ async arrayBuffer() {
16
+ return res.arrayBuffer();
17
+ },
18
+ async json() {
19
+ return res.json();
20
+ },
21
+ async text() {
22
+ return res.text();
23
+ },
24
+ get bodyUsed() {
25
+ return res.bodyUsed;
26
+ },
27
+ headers: res.headers,
28
+ status: res.status,
29
+ statusText: res.statusText,
30
+ ok: res.ok,
31
+ };
32
+ }