Skip to content

Commit ae0265e

Browse files
authored
feat(rest)!: allow passing tokens per request (discordjs#10682)
BREAKING CHANGE: `RequestData.authPrefix` has been removed in favor of `RequestData.auth.prefix`
1 parent 11438c2 commit ae0265e

File tree

8 files changed

+71
-22
lines changed

8 files changed

+71
-22
lines changed

packages/rest/__tests__/REST.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ test('getAuth', async () => {
184184
(from) => ({ auth: (from.headers as unknown as Record<string, string | undefined>).Authorization ?? null }),
185185
responseOptions,
186186
)
187-
.times(3);
187+
.times(5);
188188

189189
// default
190190
expect(await api.get('/getAuth')).toStrictEqual({ auth: 'Bot A-Very-Fake-Token' });
@@ -202,6 +202,20 @@ test('getAuth', async () => {
202202
auth: true,
203203
}),
204204
).toStrictEqual({ auth: 'Bot A-Very-Fake-Token' });
205+
206+
// Custom Bot Auth
207+
expect(
208+
await api.get('/getAuth', {
209+
auth: { token: 'A-Very-Different-Fake-Token' },
210+
}),
211+
).toStrictEqual({ auth: 'Bot A-Very-Different-Fake-Token' });
212+
213+
// Custom Bearer Auth
214+
expect(
215+
await api.get('/getAuth', {
216+
auth: { token: 'A-Bearer-Fake-Token', prefix: 'Bearer' },
217+
}),
218+
).toStrictEqual({ auth: 'Bearer A-Bearer-Fake-Token' });
205219
});
206220

207221
test('getReason', async () => {

packages/rest/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@
9191
"discord-api-types": "^0.37.114",
9292
"magic-bytes.js": "^1.10.0",
9393
"tslib": "^2.8.1",
94-
"undici": "6.21.0"
94+
"undici": "6.21.0",
95+
"uuid": "^11.0.3"
9596
},
9697
"devDependencies": {
9798
"@discordjs/api-extractor": "workspace:^",

packages/rest/src/lib/REST.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { DiscordSnowflake } from '@sapphire/snowflake';
33
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
44
import { filetypeinfo } from 'magic-bytes.js';
55
import type { RequestInit, BodyInit, Dispatcher } from 'undici';
6+
import { v5 as uuidV5 } from 'uuid';
67
import { CDN } from './CDN.js';
78
import { BurstHandler } from './handlers/BurstHandler.js';
89
import { SequentialHandler } from './handlers/SequentialHandler.js';
910
import type { IHandler } from './interfaces/Handler.js';
1011
import {
12+
AUTH_UUID_NAMESPACE,
1113
BurstHandlerMajorIdKey,
1214
DefaultRestOptions,
1315
DefaultUserAgent,
@@ -25,6 +27,7 @@ import type {
2527
RequestHeaders,
2628
RouteData,
2729
RequestData,
30+
AuthData,
2831
} from './utils/types.js';
2932
import { isBufferLike, parseResponse } from './utils/utils.js';
3033

@@ -240,9 +243,11 @@ export class REST extends AsyncEventEmitter<RestEvents> {
240243
public async queueRequest(request: InternalRequest): Promise<ResponseLike> {
241244
// Generalize the endpoint to its route data
242245
const routeId = REST.generateRouteData(request.fullRoute, request.method);
246+
const customAuth = typeof request.auth === 'object' && request.auth.token !== this.#token;
247+
const auth = customAuth ? uuidV5((request.auth as AuthData).token, AUTH_UUID_NAMESPACE) : request.auth !== false;
243248
// Get the bucket hash for the generic route, or point to a global route otherwise
244-
const hash = this.hashes.get(`${request.method}:${routeId.bucketRoute}`) ?? {
245-
value: `Global(${request.method}:${routeId.bucketRoute})`,
249+
const hash = this.hashes.get(`${request.method}:${routeId.bucketRoute}${customAuth ? `:${auth}` : ''}`) ?? {
250+
value: `Global(${request.method}:${routeId.bucketRoute}${customAuth ? `:${auth}` : ''})`,
246251
lastAccess: -1,
247252
};
248253

@@ -258,7 +263,7 @@ export class REST extends AsyncEventEmitter<RestEvents> {
258263
return handler.queueRequest(routeId, url, fetchOptions, {
259264
body: request.body,
260265
files: request.files,
261-
auth: request.auth !== false,
266+
auth,
262267
signal: request.signal,
263268
});
264269
}
@@ -308,12 +313,16 @@ export class REST extends AsyncEventEmitter<RestEvents> {
308313

309314
// If this request requires authorization (allowing non-"authorized" requests for webhooks)
310315
if (request.auth !== false) {
311-
// If we haven't received a token, throw an error
312-
if (!this.#token) {
313-
throw new Error('Expected token to be set for this request, but none was present');
314-
}
316+
if (typeof request.auth === 'object') {
317+
headers.Authorization = `${request.auth.prefix ?? this.options.authPrefix} ${request.auth.token}`;
318+
} else {
319+
// If we haven't received a token, throw an error
320+
if (!this.#token) {
321+
throw new Error('Expected token to be set for this request, but none was present');
322+
}
315323

316-
headers.Authorization = `${request.authPrefix ?? this.options.authPrefix} ${this.#token}`;
324+
headers.Authorization = `${this.options.authPrefix} ${this.#token}`;
325+
}
317326
}
318327

319328
// If a reason was set, set its appropriate header

packages/rest/src/lib/handlers/SequentialHandler.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,16 @@ export class SequentialHandler implements IHandler {
304304
// Let library users know when rate limit buckets have been updated
305305
this.debug(['Received bucket hash update', ` Old Hash : ${this.hash}`, ` New Hash : ${hash}`].join('\n'));
306306
// This queue will eventually be eliminated via attrition
307-
this.manager.hashes.set(`${method}:${routeId.bucketRoute}`, { value: hash, lastAccess: Date.now() });
307+
this.manager.hashes.set(
308+
`${method}:${routeId.bucketRoute}${typeof requestData.auth === 'string' ? `:${requestData.auth}` : ''}`,
309+
{ value: hash, lastAccess: Date.now() },
310+
);
308311
} else if (hash) {
309312
// Handle the case where hash value doesn't change
310313
// Fetch the hash data from the manager
311-
const hashData = this.manager.hashes.get(`${method}:${routeId.bucketRoute}`);
314+
const hashData = this.manager.hashes.get(
315+
`${method}:${routeId.bucketRoute}${typeof requestData.auth === 'string' ? `:${requestData.auth}` : ''}`,
316+
);
312317

313318
// When fetched, update the last access of the hash
314319
if (hashData) {

packages/rest/src/lib/handlers/Shared.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export async function handleErrors(
138138
// Handle possible malformed requests
139139
if (status >= 400 && status < 500) {
140140
// If we receive this status code, it means the token we had is no longer valid.
141-
if (status === 401 && requestData.auth) {
141+
if (status === 401 && requestData.auth === true) {
142142
manager.setToken(null!);
143143
}
144144

packages/rest/src/lib/utils/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,5 @@ export const OverwrittenMimeTypes = {
6060
} as const satisfies Readonly<Record<string, string>>;
6161

6262
export const BurstHandlerMajorIdKey = 'burst';
63+
64+
export const AUTH_UUID_NAMESPACE = 'acc82a4c-f887-417b-a69c-f74096ff7e59';

packages/rest/src/lib/utils/types.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,19 @@ export interface RawFile {
269269
name: string;
270270
}
271271

272+
export interface AuthData {
273+
/**
274+
* The authorization prefix to use for this request, useful if you use this with bearer tokens
275+
*
276+
* @defaultValue `REST.options.authPrefix`
277+
*/
278+
prefix?: 'Bearer' | 'Bot';
279+
/**
280+
* The authorization token to use for this request
281+
*/
282+
token: string;
283+
}
284+
272285
/**
273286
* Represents possible data to be given to an endpoint
274287
*/
@@ -278,17 +291,11 @@ export interface RequestData {
278291
*/
279292
appendToFormData?: boolean;
280293
/**
281-
* If this request needs the `Authorization` header
294+
* Alternate authorization data to use for this request only, or `false` to disable the Authorization header
282295
*
283296
* @defaultValue `true`
284297
*/
285-
auth?: boolean;
286-
/**
287-
* The authorization prefix to use for this request, useful if you use this with bearer tokens
288-
*
289-
* @defaultValue `'Bot'`
290-
*/
291-
authPrefix?: 'Bearer' | 'Bot';
298+
auth?: AuthData | boolean;
292299
/**
293300
* The body to send to this request.
294301
* If providing as BodyInit, set `passThroughBody: true`
@@ -363,7 +370,9 @@ export interface InternalRequest extends RequestData {
363370
method: RequestMethod;
364371
}
365372

366-
export type HandlerRequestData = Pick<InternalRequest, 'auth' | 'body' | 'files' | 'signal'>;
373+
export interface HandlerRequestData extends Pick<InternalRequest, 'body' | 'files' | 'signal'> {
374+
auth: boolean | string;
375+
}
367376

368377
/**
369378
* Parsed route data for an endpoint

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy