mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
feat(rest)!: allow passing tokens per request (#10682)
BREAKING CHANGE: `RequestData.authPrefix` has been removed in favor of `RequestData.auth.prefix`
This commit is contained in:
@@ -184,7 +184,7 @@ test('getAuth', async () => {
|
||||
(from) => ({ auth: (from.headers as unknown as Record<string, string | undefined>).Authorization ?? null }),
|
||||
responseOptions,
|
||||
)
|
||||
.times(3);
|
||||
.times(5);
|
||||
|
||||
// default
|
||||
expect(await api.get('/getAuth')).toStrictEqual({ auth: 'Bot A-Very-Fake-Token' });
|
||||
@@ -202,6 +202,20 @@ test('getAuth', async () => {
|
||||
auth: true,
|
||||
}),
|
||||
).toStrictEqual({ auth: 'Bot A-Very-Fake-Token' });
|
||||
|
||||
// Custom Bot Auth
|
||||
expect(
|
||||
await api.get('/getAuth', {
|
||||
auth: { token: 'A-Very-Different-Fake-Token' },
|
||||
}),
|
||||
).toStrictEqual({ auth: 'Bot A-Very-Different-Fake-Token' });
|
||||
|
||||
// Custom Bearer Auth
|
||||
expect(
|
||||
await api.get('/getAuth', {
|
||||
auth: { token: 'A-Bearer-Fake-Token', prefix: 'Bearer' },
|
||||
}),
|
||||
).toStrictEqual({ auth: 'Bearer A-Bearer-Fake-Token' });
|
||||
});
|
||||
|
||||
test('getReason', async () => {
|
||||
|
||||
@@ -91,7 +91,8 @@
|
||||
"discord-api-types": "^0.37.114",
|
||||
"magic-bytes.js": "^1.10.0",
|
||||
"tslib": "^2.8.1",
|
||||
"undici": "6.21.0"
|
||||
"undici": "6.21.0",
|
||||
"uuid": "^11.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@discordjs/api-extractor": "workspace:^",
|
||||
|
||||
@@ -3,11 +3,13 @@ import { DiscordSnowflake } from '@sapphire/snowflake';
|
||||
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
|
||||
import { filetypeinfo } from 'magic-bytes.js';
|
||||
import type { RequestInit, BodyInit, Dispatcher } from 'undici';
|
||||
import { v5 as uuidV5 } from 'uuid';
|
||||
import { CDN } from './CDN.js';
|
||||
import { BurstHandler } from './handlers/BurstHandler.js';
|
||||
import { SequentialHandler } from './handlers/SequentialHandler.js';
|
||||
import type { IHandler } from './interfaces/Handler.js';
|
||||
import {
|
||||
AUTH_UUID_NAMESPACE,
|
||||
BurstHandlerMajorIdKey,
|
||||
DefaultRestOptions,
|
||||
DefaultUserAgent,
|
||||
@@ -25,6 +27,7 @@ import type {
|
||||
RequestHeaders,
|
||||
RouteData,
|
||||
RequestData,
|
||||
AuthData,
|
||||
} from './utils/types.js';
|
||||
import { isBufferLike, parseResponse } from './utils/utils.js';
|
||||
|
||||
@@ -240,9 +243,11 @@ export class REST extends AsyncEventEmitter<RestEvents> {
|
||||
public async queueRequest(request: InternalRequest): Promise<ResponseLike> {
|
||||
// Generalize the endpoint to its route data
|
||||
const routeId = REST.generateRouteData(request.fullRoute, request.method);
|
||||
const customAuth = typeof request.auth === 'object' && request.auth.token !== this.#token;
|
||||
const auth = customAuth ? uuidV5((request.auth as AuthData).token, AUTH_UUID_NAMESPACE) : request.auth !== false;
|
||||
// Get the bucket hash for the generic route, or point to a global route otherwise
|
||||
const hash = this.hashes.get(`${request.method}:${routeId.bucketRoute}`) ?? {
|
||||
value: `Global(${request.method}:${routeId.bucketRoute})`,
|
||||
const hash = this.hashes.get(`${request.method}:${routeId.bucketRoute}${customAuth ? `:${auth}` : ''}`) ?? {
|
||||
value: `Global(${request.method}:${routeId.bucketRoute}${customAuth ? `:${auth}` : ''})`,
|
||||
lastAccess: -1,
|
||||
};
|
||||
|
||||
@@ -258,7 +263,7 @@ export class REST extends AsyncEventEmitter<RestEvents> {
|
||||
return handler.queueRequest(routeId, url, fetchOptions, {
|
||||
body: request.body,
|
||||
files: request.files,
|
||||
auth: request.auth !== false,
|
||||
auth,
|
||||
signal: request.signal,
|
||||
});
|
||||
}
|
||||
@@ -308,12 +313,16 @@ export class REST extends AsyncEventEmitter<RestEvents> {
|
||||
|
||||
// If this request requires authorization (allowing non-"authorized" requests for webhooks)
|
||||
if (request.auth !== false) {
|
||||
// If we haven't received a token, throw an error
|
||||
if (!this.#token) {
|
||||
throw new Error('Expected token to be set for this request, but none was present');
|
||||
}
|
||||
if (typeof request.auth === 'object') {
|
||||
headers.Authorization = `${request.auth.prefix ?? this.options.authPrefix} ${request.auth.token}`;
|
||||
} else {
|
||||
// If we haven't received a token, throw an error
|
||||
if (!this.#token) {
|
||||
throw new Error('Expected token to be set for this request, but none was present');
|
||||
}
|
||||
|
||||
headers.Authorization = `${request.authPrefix ?? this.options.authPrefix} ${this.#token}`;
|
||||
headers.Authorization = `${this.options.authPrefix} ${this.#token}`;
|
||||
}
|
||||
}
|
||||
|
||||
// If a reason was set, set its appropriate header
|
||||
|
||||
@@ -304,11 +304,16 @@ export class SequentialHandler implements IHandler {
|
||||
// Let library users know when rate limit buckets have been updated
|
||||
this.debug(['Received bucket hash update', ` Old Hash : ${this.hash}`, ` New Hash : ${hash}`].join('\n'));
|
||||
// This queue will eventually be eliminated via attrition
|
||||
this.manager.hashes.set(`${method}:${routeId.bucketRoute}`, { value: hash, lastAccess: Date.now() });
|
||||
this.manager.hashes.set(
|
||||
`${method}:${routeId.bucketRoute}${typeof requestData.auth === 'string' ? `:${requestData.auth}` : ''}`,
|
||||
{ value: hash, lastAccess: Date.now() },
|
||||
);
|
||||
} else if (hash) {
|
||||
// Handle the case where hash value doesn't change
|
||||
// Fetch the hash data from the manager
|
||||
const hashData = this.manager.hashes.get(`${method}:${routeId.bucketRoute}`);
|
||||
const hashData = this.manager.hashes.get(
|
||||
`${method}:${routeId.bucketRoute}${typeof requestData.auth === 'string' ? `:${requestData.auth}` : ''}`,
|
||||
);
|
||||
|
||||
// When fetched, update the last access of the hash
|
||||
if (hashData) {
|
||||
|
||||
@@ -138,7 +138,7 @@ export async function handleErrors(
|
||||
// Handle possible malformed requests
|
||||
if (status >= 400 && status < 500) {
|
||||
// If we receive this status code, it means the token we had is no longer valid.
|
||||
if (status === 401 && requestData.auth) {
|
||||
if (status === 401 && requestData.auth === true) {
|
||||
manager.setToken(null!);
|
||||
}
|
||||
|
||||
|
||||
@@ -60,3 +60,5 @@ export const OverwrittenMimeTypes = {
|
||||
} as const satisfies Readonly<Record<string, string>>;
|
||||
|
||||
export const BurstHandlerMajorIdKey = 'burst';
|
||||
|
||||
export const AUTH_UUID_NAMESPACE = 'acc82a4c-f887-417b-a69c-f74096ff7e59';
|
||||
|
||||
@@ -269,6 +269,19 @@ export interface RawFile {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AuthData {
|
||||
/**
|
||||
* The authorization prefix to use for this request, useful if you use this with bearer tokens
|
||||
*
|
||||
* @defaultValue `REST.options.authPrefix`
|
||||
*/
|
||||
prefix?: 'Bearer' | 'Bot';
|
||||
/**
|
||||
* The authorization token to use for this request
|
||||
*/
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents possible data to be given to an endpoint
|
||||
*/
|
||||
@@ -278,17 +291,11 @@ export interface RequestData {
|
||||
*/
|
||||
appendToFormData?: boolean;
|
||||
/**
|
||||
* If this request needs the `Authorization` header
|
||||
* Alternate authorization data to use for this request only, or `false` to disable the Authorization header
|
||||
*
|
||||
* @defaultValue `true`
|
||||
*/
|
||||
auth?: boolean;
|
||||
/**
|
||||
* The authorization prefix to use for this request, useful if you use this with bearer tokens
|
||||
*
|
||||
* @defaultValue `'Bot'`
|
||||
*/
|
||||
authPrefix?: 'Bearer' | 'Bot';
|
||||
auth?: AuthData | boolean;
|
||||
/**
|
||||
* The body to send to this request.
|
||||
* If providing as BodyInit, set `passThroughBody: true`
|
||||
@@ -363,7 +370,9 @@ export interface InternalRequest extends RequestData {
|
||||
method: RequestMethod;
|
||||
}
|
||||
|
||||
export type HandlerRequestData = Pick<InternalRequest, 'auth' | 'body' | 'files' | 'signal'>;
|
||||
export interface HandlerRequestData extends Pick<InternalRequest, 'body' | 'files' | 'signal'> {
|
||||
auth: boolean | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsed route data for an endpoint
|
||||
|
||||
Reference in New Issue
Block a user