@discordkit/core 3.1.0 → 4.0.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.
- package/CHANGELOG.md +416 -0
- package/README.md +52 -0
- package/dist/index.d.mts +23 -0
- package/dist/index.mjs +23 -0
- package/dist/requests/DiscordSession.d.mts +30 -0
- package/dist/requests/DiscordSession.mjs +202 -0
- package/dist/requests/addParams.d.mts +15 -0
- package/dist/requests/addParams.mjs +24 -0
- package/dist/requests/buildURL.d.mts +7 -0
- package/dist/requests/buildURL.mjs +7 -0
- package/dist/requests/getAsset.d.mts +7 -0
- package/dist/requests/getAsset.mjs +6 -0
- package/dist/requests/index.d.mts +9 -0
- package/dist/requests/index.mjs +9 -0
- package/dist/requests/methods.d.mts +63 -0
- package/dist/requests/methods.mjs +10 -0
- package/dist/requests/request.d.mts +27 -0
- package/dist/requests/request.mjs +29 -0
- package/dist/requests/toProcedure.d.mts +40 -0
- package/dist/requests/toProcedure.mjs +27 -0
- package/dist/requests/toQuery.d.mts +36 -0
- package/dist/requests/toQuery.mjs +17 -0
- package/dist/requests/toValidated.d.mts +16 -0
- package/dist/requests/toValidated.mjs +25 -0
- package/dist/requests/verifyKey.d.mts +13 -0
- package/dist/requests/verifyKey.mjs +63 -0
- package/dist/utils/isBetween.d.mts +4 -0
- package/dist/utils/isBetween.mjs +4 -0
- package/dist/utils/{isNonNullable.d.ts → isNonNullable.d.mts} +5 -2
- package/dist/utils/isNonNullable.mjs +22 -0
- package/dist/utils/isNumericString.d.mts +4 -0
- package/dist/utils/isNumericString.mjs +4 -0
- package/dist/utils/isObject.d.mts +4 -0
- package/dist/utils/isObject.mjs +4 -0
- package/dist/utils/sleep.d.mts +7 -0
- package/dist/utils/sleep.mjs +7 -0
- package/dist/utils/toCamelCase.d.mts +4 -0
- package/dist/utils/toCamelCase.mjs +4 -0
- package/dist/utils/toCamelKeys.d.mts +6 -0
- package/dist/utils/toCamelKeys.mjs +13 -0
- package/dist/utils/toSnakeCase.d.mts +4 -0
- package/dist/utils/toSnakeCase.mjs +4 -0
- package/dist/utils/toSnakeKeys.d.mts +6 -0
- package/dist/utils/toSnakeKeys.mjs +13 -0
- package/dist/validations/asDigits.d.mts +12 -0
- package/dist/validations/asDigits.mjs +10 -0
- package/dist/validations/asInteger.d.mts +12 -0
- package/dist/validations/asInteger.mjs +10 -0
- package/dist/validations/bitfield.d.mts +23 -0
- package/dist/validations/bitfield.mjs +20 -0
- package/dist/validations/boundedArray.d.mts +13 -0
- package/dist/validations/boundedArray.mjs +9 -0
- package/dist/validations/boundedInteger.d.mts +13 -0
- package/dist/validations/boundedInteger.mjs +9 -0
- package/dist/validations/boundedString.d.mts +14 -0
- package/dist/validations/boundedString.mjs +10 -0
- package/dist/validations/datauri.d.mts +24 -0
- package/dist/validations/datauri.mjs +17 -0
- package/dist/validations/fileUpload.d.mts +129 -0
- package/dist/validations/fileUpload.mjs +114 -0
- package/dist/validations/hasMimeType.d.mts +16 -0
- package/dist/validations/hasMimeType.mjs +16 -0
- package/dist/validations/hasSize.d.mts +10 -0
- package/dist/validations/hasSize.mjs +12 -0
- package/dist/validations/index.d.mts +15 -0
- package/dist/validations/index.mjs +15 -0
- package/dist/validations/schema.d.mts +102 -0
- package/dist/validations/schema.mjs +109 -0
- package/dist/validations/{snowflake.d.ts → snowflake.d.mts} +9 -7
- package/dist/validations/snowflake.mjs +28 -0
- package/dist/validations/{timestamp.d.ts → timestamp.d.mts} +5 -1
- package/dist/validations/timestamp.mjs +6 -0
- package/dist/validations/toBlob.d.mts +9 -0
- package/dist/validations/toBlob.mjs +17 -0
- package/dist/validations/url.d.mts +6 -0
- package/dist/validations/url.mjs +5 -0
- package/package.json +13 -23
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -3
- package/dist/index.js.map +0 -1
- package/dist/requests/DiscordSession.d.ts +0 -16
- package/dist/requests/DiscordSession.js +0 -42
- package/dist/requests/DiscordSession.js.map +0 -1
- package/dist/requests/addParams.d.ts +0 -2
- package/dist/requests/addParams.js +0 -11
- package/dist/requests/addParams.js.map +0 -1
- package/dist/requests/buildURL.d.ts +0 -2
- package/dist/requests/buildURL.js +0 -4
- package/dist/requests/buildURL.js.map +0 -1
- package/dist/requests/getAsset.d.ts +0 -2
- package/dist/requests/getAsset.js +0 -3
- package/dist/requests/getAsset.js.map +0 -1
- package/dist/requests/index.d.ts +0 -7
- package/dist/requests/index.js +0 -8
- package/dist/requests/index.js.map +0 -1
- package/dist/requests/methods.d.ts +0 -13
- package/dist/requests/methods.js +0 -8
- package/dist/requests/methods.js.map +0 -1
- package/dist/requests/request.d.ts +0 -2
- package/dist/requests/request.js +0 -35
- package/dist/requests/request.js.map +0 -1
- package/dist/requests/toProcedure.d.ts +0 -31
- package/dist/requests/toProcedure.js +0 -36
- package/dist/requests/toProcedure.js.map +0 -1
- package/dist/requests/toQuery.d.ts +0 -28
- package/dist/requests/toQuery.js +0 -9
- package/dist/requests/toQuery.js.map +0 -1
- package/dist/requests/toValidated.d.ts +0 -13
- package/dist/requests/toValidated.js +0 -29
- package/dist/requests/toValidated.js.map +0 -1
- package/dist/utils/isBetween.d.ts +0 -1
- package/dist/utils/isBetween.js +0 -2
- package/dist/utils/isBetween.js.map +0 -1
- package/dist/utils/isNonNullable.js +0 -20
- package/dist/utils/isNonNullable.js.map +0 -1
- package/dist/utils/isNumericString.d.ts +0 -1
- package/dist/utils/isNumericString.js +0 -2
- package/dist/utils/isNumericString.js.map +0 -1
- package/dist/utils/isObject.d.ts +0 -1
- package/dist/utils/isObject.js +0 -2
- package/dist/utils/isObject.js.map +0 -1
- package/dist/utils/toCamelCase.d.ts +0 -1
- package/dist/utils/toCamelCase.js +0 -2
- package/dist/utils/toCamelCase.js.map +0 -1
- package/dist/utils/toCamelKeys.d.ts +0 -2
- package/dist/utils/toCamelKeys.js +0 -16
- package/dist/utils/toCamelKeys.js.map +0 -1
- package/dist/utils/toSnakeCase.d.ts +0 -1
- package/dist/utils/toSnakeCase.js +0 -4
- package/dist/utils/toSnakeCase.js.map +0 -1
- package/dist/utils/toSnakeKeys.d.ts +0 -2
- package/dist/utils/toSnakeKeys.js +0 -16
- package/dist/utils/toSnakeKeys.js.map +0 -1
- package/dist/validations/asDigits.d.ts +0 -6
- package/dist/validations/asDigits.js +0 -6
- package/dist/validations/asDigits.js.map +0 -1
- package/dist/validations/asInteger.d.ts +0 -6
- package/dist/validations/asInteger.js +0 -6
- package/dist/validations/asInteger.js.map +0 -1
- package/dist/validations/bitfield.d.ts +0 -17
- package/dist/validations/bitfield.js +0 -37
- package/dist/validations/bitfield.js.map +0 -1
- package/dist/validations/boundedArray.d.ts +0 -6
- package/dist/validations/boundedArray.js +0 -8
- package/dist/validations/boundedArray.js.map +0 -1
- package/dist/validations/boundedInteger.d.ts +0 -6
- package/dist/validations/boundedInteger.js +0 -8
- package/dist/validations/boundedInteger.js.map +0 -1
- package/dist/validations/boundedString.d.ts +0 -6
- package/dist/validations/boundedString.js +0 -8
- package/dist/validations/boundedString.js.map +0 -1
- package/dist/validations/datauri.d.ts +0 -20
- package/dist/validations/datauri.js +0 -20
- package/dist/validations/datauri.js.map +0 -1
- package/dist/validations/hasMimeType.d.ts +0 -10
- package/dist/validations/hasMimeType.js +0 -18
- package/dist/validations/hasMimeType.js.map +0 -1
- package/dist/validations/hasSize.d.ts +0 -5
- package/dist/validations/hasSize.js +0 -13
- package/dist/validations/hasSize.js.map +0 -1
- package/dist/validations/index.d.ts +0 -12
- package/dist/validations/index.js +0 -13
- package/dist/validations/index.js.map +0 -1
- package/dist/validations/snowflake.js +0 -39
- package/dist/validations/snowflake.js.map +0 -1
- package/dist/validations/timestamp.js +0 -4
- package/dist/validations/timestamp.js.map +0 -1
- package/dist/validations/toBlob.d.ts +0 -4
- package/dist/validations/toBlob.js +0 -19
- package/dist/validations/toBlob.js.map +0 -1
- package/dist/validations/url.d.ts +0 -2
- package/dist/validations/url.js +0 -3
- package/dist/validations/url.js.map +0 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { sleep } from "../utils/sleep.mjs";
|
|
2
|
+
//#region src/requests/DiscordSession.ts
|
|
3
|
+
const endpoint = `https://discord.com/api/v10/`;
|
|
4
|
+
/** @internal */
|
|
5
|
+
var DiscordSession = class {
|
|
6
|
+
endpoint = endpoint;
|
|
7
|
+
maxRetries = 5;
|
|
8
|
+
#authToken = null;
|
|
9
|
+
#buckets = /* @__PURE__ */ new Map();
|
|
10
|
+
#globalReset = 0;
|
|
11
|
+
#requestQueue = [];
|
|
12
|
+
#processingQueue = false;
|
|
13
|
+
#globalLimit = 50;
|
|
14
|
+
#globalWindow = 1e3;
|
|
15
|
+
#globalRequestTimestamps = [];
|
|
16
|
+
#invalidRequests = {
|
|
17
|
+
count: 0,
|
|
18
|
+
windowStart: Date.now()
|
|
19
|
+
};
|
|
20
|
+
#invalidRequestLimit = 1e4;
|
|
21
|
+
#invalidRequestWindow = 600 * 1e3;
|
|
22
|
+
get ready() {
|
|
23
|
+
return Boolean(this.#authToken);
|
|
24
|
+
}
|
|
25
|
+
constructor(authToken) {
|
|
26
|
+
if (authToken) this.setToken(authToken);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Clears the current session details, then attempts to set
|
|
30
|
+
* new session values
|
|
31
|
+
*/
|
|
32
|
+
setToken = (token) => {
|
|
33
|
+
this.#authToken = null;
|
|
34
|
+
if (token.length === 0) throw new Error(`Must provide a non-empty string to set Auth Token`);
|
|
35
|
+
if (!token.startsWith(`Bot `) && !token.startsWith(`Bearer `)) throw new Error(`Token must begin with either "Bot " or "Bearer ", received: ${token}`);
|
|
36
|
+
this.#authToken = token;
|
|
37
|
+
return this;
|
|
38
|
+
};
|
|
39
|
+
clearSession = () => {
|
|
40
|
+
this.#authToken = null;
|
|
41
|
+
this.#buckets.clear();
|
|
42
|
+
this.#globalReset = 0;
|
|
43
|
+
this.#requestQueue = [];
|
|
44
|
+
this.#globalRequestTimestamps = [];
|
|
45
|
+
this.#invalidRequests = {
|
|
46
|
+
count: 0,
|
|
47
|
+
windowStart: Date.now()
|
|
48
|
+
};
|
|
49
|
+
return this;
|
|
50
|
+
};
|
|
51
|
+
getSession = () => {
|
|
52
|
+
const token = this.#authToken;
|
|
53
|
+
if (!token) throw new Error(`Auth Token must be set before requests can be made.`);
|
|
54
|
+
return token;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Queue a request to be processed with rate limiting
|
|
58
|
+
*/
|
|
59
|
+
queueRequest = async (resource, method, body, options) => {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
this.#requestQueue.push({
|
|
62
|
+
resource,
|
|
63
|
+
method,
|
|
64
|
+
body,
|
|
65
|
+
options,
|
|
66
|
+
resolve,
|
|
67
|
+
reject
|
|
68
|
+
});
|
|
69
|
+
this.#processQueue();
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Process the request queue with rate limiting
|
|
74
|
+
*/
|
|
75
|
+
#processQueue = async () => {
|
|
76
|
+
if (this.#processingQueue) return;
|
|
77
|
+
this.#processingQueue = true;
|
|
78
|
+
while (this.#requestQueue.length > 0) {
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
if (this.#isTemporarilyBanned()) {
|
|
81
|
+
const waitTime = this.#invalidRequestWindow - (now - this.#invalidRequests.windowStart);
|
|
82
|
+
console.warn(`Temporarily banned from Discord API. Waiting ${Math.ceil(waitTime / 1e3)}s`);
|
|
83
|
+
await sleep(waitTime);
|
|
84
|
+
this.#resetInvalidRequestCounter();
|
|
85
|
+
}
|
|
86
|
+
await this.#enforceGlobalRateLimit();
|
|
87
|
+
if (this.#globalReset > now / 1e3) {
|
|
88
|
+
await sleep((this.#globalReset - now / 1e3) * 1e3);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const request = this.#requestQueue[0];
|
|
92
|
+
if (typeof request === `undefined`) break;
|
|
93
|
+
const bucket = this.#buckets.get(`${request.method}:${request.resource}`);
|
|
94
|
+
if (bucket?.remaining === 0) {
|
|
95
|
+
const resetTime = bucket.reset * 1e3;
|
|
96
|
+
if (resetTime > now) await sleep(resetTime - now);
|
|
97
|
+
}
|
|
98
|
+
this.#requestQueue.shift();
|
|
99
|
+
try {
|
|
100
|
+
const response = await this.#executeRequest(request);
|
|
101
|
+
request.resolve(response);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error instanceof Error) request.reject(error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
this.#processingQueue = false;
|
|
107
|
+
if (this.#requestQueue.length > 0) this.#processQueue();
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Execute a single request and handle rate limit headers
|
|
111
|
+
*/
|
|
112
|
+
#executeRequest = async (request, retryCount = 0) => {
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
this.#globalRequestTimestamps.push(now);
|
|
115
|
+
const headers = {};
|
|
116
|
+
if (!request.options?.anonymous) headers.Authorization = this.getSession();
|
|
117
|
+
if (request.options?.reason) headers[`X-Audit-Log-Reason`] = encodeURIComponent(request.options.reason);
|
|
118
|
+
if (request.body && !(request.body instanceof FormData)) headers[`Content-Type`] = `application/json`;
|
|
119
|
+
const response = await fetch(request.resource.toString(), {
|
|
120
|
+
method: request.method,
|
|
121
|
+
body: request.body,
|
|
122
|
+
headers
|
|
123
|
+
});
|
|
124
|
+
this.#updateRateLimits(response);
|
|
125
|
+
if (response.status === 429) {
|
|
126
|
+
if (retryCount >= this.maxRetries) {
|
|
127
|
+
console.error(`Max retries (${this.maxRetries}) exceeded for ${request.resource}`);
|
|
128
|
+
return response;
|
|
129
|
+
}
|
|
130
|
+
const retryAfter = response.headers.get(`Retry-After`) ?? `1`;
|
|
131
|
+
const isGlobal = response.headers.get(`X-RateLimit-Global`) === `true`;
|
|
132
|
+
const scope = response.headers.get(`X-RateLimit-Scope`);
|
|
133
|
+
if (isGlobal || scope === `global`) {
|
|
134
|
+
console.warn(`Hit global rate limit`);
|
|
135
|
+
this.#globalReset = now / 1e3 + parseFloat(retryAfter);
|
|
136
|
+
}
|
|
137
|
+
await sleep(parseFloat(retryAfter) * 1e3);
|
|
138
|
+
return this.#executeRequest(request, retryCount + 1);
|
|
139
|
+
}
|
|
140
|
+
if ([401, 403].includes(response.status) || response.status === 429 && response.headers.get(`X-RateLimit-Scope`) !== `shared`) this.#trackInvalidRequest();
|
|
141
|
+
return response;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Update rate limit buckets from response headers
|
|
145
|
+
*/
|
|
146
|
+
#updateRateLimits = (response) => {
|
|
147
|
+
const bucket = response.headers.get(`X-RateLimit-Bucket`);
|
|
148
|
+
const limit = response.headers.get(`X-RateLimit-Limit`);
|
|
149
|
+
const remaining = response.headers.get(`X-RateLimit-Remaining`);
|
|
150
|
+
const reset = response.headers.get(`X-RateLimit-Reset`);
|
|
151
|
+
const resetAfter = response.headers.get(`X-RateLimit-Reset-After`);
|
|
152
|
+
if (bucket && limit && remaining && reset && resetAfter) this.#buckets.set(bucket, {
|
|
153
|
+
limit: parseInt(limit, 10),
|
|
154
|
+
remaining: parseInt(remaining, 10),
|
|
155
|
+
reset: parseFloat(reset),
|
|
156
|
+
resetAfter: parseFloat(resetAfter)
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* Enforce global rate limit of 50 requests per second
|
|
161
|
+
*/
|
|
162
|
+
#enforceGlobalRateLimit = async () => {
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
this.#globalRequestTimestamps = this.#globalRequestTimestamps.filter((timestamp) => now - timestamp < this.#globalWindow);
|
|
165
|
+
if (this.#globalRequestTimestamps.length >= this.#globalLimit) {
|
|
166
|
+
const oldestTimestamp = this.#globalRequestTimestamps[0];
|
|
167
|
+
const waitTime = this.#globalWindow - (now - oldestTimestamp);
|
|
168
|
+
if (waitTime > 0) await sleep(waitTime);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Track invalid requests for Cloudflare ban prevention
|
|
173
|
+
*/
|
|
174
|
+
#trackInvalidRequest = () => {
|
|
175
|
+
if (Date.now() - this.#invalidRequests.windowStart >= this.#invalidRequestWindow) this.#resetInvalidRequestCounter();
|
|
176
|
+
this.#invalidRequests.count++;
|
|
177
|
+
if (this.#invalidRequests.count >= this.#invalidRequestLimit) console.error(`Approaching invalid request limit! Bot may be temporarily banned.`);
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Check if we're temporarily banned
|
|
181
|
+
*/
|
|
182
|
+
#isTemporarilyBanned = () => {
|
|
183
|
+
if (Date.now() - this.#invalidRequests.windowStart < this.#invalidRequestWindow) return this.#invalidRequests.count >= this.#invalidRequestLimit;
|
|
184
|
+
return false;
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Reset invalid request counter
|
|
188
|
+
*/
|
|
189
|
+
#resetInvalidRequestCounter = () => {
|
|
190
|
+
this.#invalidRequests = {
|
|
191
|
+
count: 0,
|
|
192
|
+
windowStart: Date.now()
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* Get current queue size (useful for monitoring)
|
|
197
|
+
*/
|
|
198
|
+
getQueueSize = () => this.#requestQueue.length;
|
|
199
|
+
};
|
|
200
|
+
const discord = new DiscordSession();
|
|
201
|
+
//#endregion
|
|
202
|
+
export { DiscordSession, discord, endpoint };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/requests/addParams.d.ts
|
|
2
|
+
type RequestParams = Partial<Record<string, number[] | string[] | boolean | number | string | null | undefined>>;
|
|
3
|
+
/**
|
|
4
|
+
* Append a params object to a URL.
|
|
5
|
+
*
|
|
6
|
+
* Array values are emitted as repeated query keys (`?id=1&id=2`), which is
|
|
7
|
+
* how Discord's HTTP API documents array query strings. Scalars are
|
|
8
|
+
* stringified once. Keys are converted from camelCase to snake_case to
|
|
9
|
+
* match Discord's convention.
|
|
10
|
+
*
|
|
11
|
+
* @__NO_SIDE_EFFECTS__
|
|
12
|
+
*/
|
|
13
|
+
declare const addParams: (url: URL, params: RequestParams) => URL;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { RequestParams, addParams };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isNonNullable } from "../utils/isNonNullable.mjs";
|
|
2
|
+
import { toSnakeCase } from "../utils/toSnakeCase.mjs";
|
|
3
|
+
//#region src/requests/addParams.ts
|
|
4
|
+
/**
|
|
5
|
+
* Append a params object to a URL.
|
|
6
|
+
*
|
|
7
|
+
* Array values are emitted as repeated query keys (`?id=1&id=2`), which is
|
|
8
|
+
* how Discord's HTTP API documents array query strings. Scalars are
|
|
9
|
+
* stringified once. Keys are converted from camelCase to snake_case to
|
|
10
|
+
* match Discord's convention.
|
|
11
|
+
*
|
|
12
|
+
* @__NO_SIDE_EFFECTS__
|
|
13
|
+
*/
|
|
14
|
+
const addParams = (url, params) => {
|
|
15
|
+
for (const [key, value] of Object.entries(params)) {
|
|
16
|
+
if (!isNonNullable(value)) continue;
|
|
17
|
+
const snake = toSnakeCase(key);
|
|
18
|
+
if (Array.isArray(value)) for (const item of value) url.searchParams.append(snake, String(item));
|
|
19
|
+
else url.searchParams.set(snake, String(value));
|
|
20
|
+
}
|
|
21
|
+
return url;
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { addParams };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { endpoint } from "./DiscordSession.mjs";
|
|
2
|
+
import { addParams } from "./addParams.mjs";
|
|
3
|
+
//#region src/requests/buildURL.ts
|
|
4
|
+
/** @__NO_SIDE_EFFECTS__ */
|
|
5
|
+
const buildURL = (resource, params, base) => addParams(new URL(resource.replace(/^\//, ``), base ?? endpoint), params ?? {});
|
|
6
|
+
//#endregion
|
|
7
|
+
export { buildURL };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { discord, endpoint } from "./DiscordSession.mjs";
|
|
2
|
+
import { buildURL } from "./buildURL.mjs";
|
|
3
|
+
import { getAsset } from "./getAsset.mjs";
|
|
4
|
+
import { Fetcher, FetcherCapabilities, RequestOptionsFor, get, patch, post, put, remove } from "./methods.mjs";
|
|
5
|
+
import { toProcedure } from "./toProcedure.mjs";
|
|
6
|
+
import { QueryFunction, toQuery } from "./toQuery.mjs";
|
|
7
|
+
import { toValidated } from "./toValidated.mjs";
|
|
8
|
+
import { verifyKey } from "./verifyKey.mjs";
|
|
9
|
+
export { Fetcher, FetcherCapabilities, QueryFunction, RequestOptionsFor, buildURL, discord, endpoint, get, getAsset, patch, post, put, remove, toProcedure, toQuery, toValidated, verifyKey };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { discord, endpoint } from "./DiscordSession.mjs";
|
|
2
|
+
import { buildURL } from "./buildURL.mjs";
|
|
3
|
+
import { getAsset } from "./getAsset.mjs";
|
|
4
|
+
import { get, patch, post, put, remove } from "./methods.mjs";
|
|
5
|
+
import { toProcedure } from "./toProcedure.mjs";
|
|
6
|
+
import { toQuery } from "./toQuery.mjs";
|
|
7
|
+
import { toValidated } from "./toValidated.mjs";
|
|
8
|
+
import { verifyKey } from "./verifyKey.mjs";
|
|
9
|
+
export { buildURL, discord, endpoint, get, getAsset, patch, post, put, remove, toProcedure, toQuery, toValidated, verifyKey };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { RequestBody, RequestOptions } from "./request.mjs";
|
|
2
|
+
import { RequestParams } from "./addParams.mjs";
|
|
3
|
+
import { GenericSchema, GenericSchemaAsync, InferOutput } from "valibot";
|
|
4
|
+
|
|
5
|
+
//#region src/requests/methods.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Capability flags advertised by a {@link Fetcher}. Each flag describes a
|
|
8
|
+
* property of the underlying Discord endpoint — not a per-call preference.
|
|
9
|
+
*
|
|
10
|
+
* The flags drive both the `options` argument the fetcher accepts and the
|
|
11
|
+
* headers/auth the request layer emits at runtime.
|
|
12
|
+
*/
|
|
13
|
+
interface FetcherCapabilities {
|
|
14
|
+
/**
|
|
15
|
+
* `true` if the Discord endpoint MUST be called without an `Authorization`
|
|
16
|
+
* header. Forces callers to acknowledge the unauthenticated path via
|
|
17
|
+
* `{ anonymous: true }` and tells the request layer to skip the session token.
|
|
18
|
+
*/
|
|
19
|
+
readonly anonymous?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* `true` if the Discord endpoint accepts the `X-Audit-Log-Reason` header.
|
|
22
|
+
* Allows callers to pass `{ reason: "…" }`; the header is URL-encoded and
|
|
23
|
+
* attached by the request layer.
|
|
24
|
+
*/
|
|
25
|
+
readonly auditLogReason?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The shape of the per-call `options` argument a {@link Fetcher} accepts,
|
|
29
|
+
* derived from the endpoint's {@link FetcherCapabilities}.
|
|
30
|
+
*
|
|
31
|
+
* - When `anonymous: true` is declared on the capability, the option is
|
|
32
|
+
* *required* on every call. When omitted, `anonymous` is forbidden.
|
|
33
|
+
* - When `auditLogReason: true` is declared, `reason` is permitted; otherwise
|
|
34
|
+
* it is forbidden.
|
|
35
|
+
*/
|
|
36
|
+
type RequestOptionsFor<C extends FetcherCapabilities> = (C extends {
|
|
37
|
+
anonymous: true;
|
|
38
|
+
} ? {
|
|
39
|
+
anonymous: true;
|
|
40
|
+
} : {
|
|
41
|
+
anonymous?: never;
|
|
42
|
+
}) & (C extends {
|
|
43
|
+
auditLogReason: true;
|
|
44
|
+
} ? {
|
|
45
|
+
reason?: string;
|
|
46
|
+
} : {
|
|
47
|
+
reason?: never;
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* Decide whether the per-call `options` argument is required or optional
|
|
51
|
+
* based on whether any capability forces a value to be passed.
|
|
52
|
+
*/
|
|
53
|
+
type RequiresOptions<C extends FetcherCapabilities> = C extends {
|
|
54
|
+
anonymous: true;
|
|
55
|
+
} ? true : false;
|
|
56
|
+
type Fetcher< /** A schema to validate the input arguments of a fetch call */S extends GenericSchema | GenericSchemaAsync | null = null, /** The return value expected from the fetch call */R = void, /** Endpoint capabilities; shapes the `options` argument */C extends FetcherCapabilities = {}> = S extends null ? RequiresOptions<C> extends true ? (options: RequestOptionsFor<C>) => Promise<R> : (options?: RequestOptionsFor<C>) => Promise<R> : RequiresOptions<C> extends true ? (config: InferOutput<NonNullable<S>>, options: RequestOptionsFor<C>) => Promise<R> : (config: InferOutput<NonNullable<S>>, options?: RequestOptionsFor<C>) => Promise<R>;
|
|
57
|
+
declare const get: <T>(url: string, params?: RequestParams, options?: RequestOptions) => Promise<T>;
|
|
58
|
+
declare const post: <T>(url: string, body?: RequestBody, options?: RequestOptions) => Promise<T>;
|
|
59
|
+
declare const put: <T>(url: string, body?: RequestBody, options?: RequestOptions) => Promise<T>;
|
|
60
|
+
declare const patch: <T>(url: string, body?: RequestBody, options?: RequestOptions) => Promise<T>;
|
|
61
|
+
declare const remove: <T = void>(url: string, options?: RequestOptions) => Promise<T>;
|
|
62
|
+
//#endregion
|
|
63
|
+
export { Fetcher, FetcherCapabilities, RequestOptionsFor, get, patch, post, put, remove };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { buildURL } from "./buildURL.mjs";
|
|
2
|
+
import { request } from "./request.mjs";
|
|
3
|
+
//#region src/requests/methods.ts
|
|
4
|
+
const get = async (url, params, options) => request(buildURL(url, params), `GET`, void 0, options);
|
|
5
|
+
const post = async (url, body, options) => request(buildURL(url), `POST`, body, options);
|
|
6
|
+
const put = async (url, body, options) => request(buildURL(url), `PUT`, body, options);
|
|
7
|
+
const patch = async (url, body, options) => request(buildURL(url), `PATCH`, body, options);
|
|
8
|
+
const remove = async (url, options) => request(buildURL(url), `DELETE`, void 0, options);
|
|
9
|
+
//#endregion
|
|
10
|
+
export { get, patch, post, put, remove };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/requests/request.d.ts
|
|
2
|
+
type RequestBody = object | null | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Per-call request options forwarded by the typed method helpers
|
|
5
|
+
* (`get`/`post`/`put`/`patch`/`remove`) into the request layer.
|
|
6
|
+
*
|
|
7
|
+
* Endpoints surface these via the `Fetcher` type so the call site is
|
|
8
|
+
* type-checked against the endpoint's declared capabilities.
|
|
9
|
+
*/
|
|
10
|
+
interface RequestOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Skip the `Authorization` header for this request. Used by webhook
|
|
13
|
+
* and interaction endpoints whose path tokens act as authorization.
|
|
14
|
+
* When set, `discord.getSession()` is NOT consulted, so the session
|
|
15
|
+
* is allowed to be unset.
|
|
16
|
+
*/
|
|
17
|
+
anonymous?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Audit-log reason. Forwarded to Discord as the `X-Audit-Log-Reason`
|
|
20
|
+
* header. The value is URL-encoded by the request layer so non-ASCII
|
|
21
|
+
* characters survive transit.
|
|
22
|
+
*/
|
|
23
|
+
reason?: string;
|
|
24
|
+
}
|
|
25
|
+
declare const request: <T>(resource: URL, method?: string, body?: RequestBody, options?: RequestOptions) => Promise<T>;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { RequestBody, RequestOptions, request };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { discord } from "./DiscordSession.mjs";
|
|
2
|
+
import { toCamelKeys } from "../utils/toCamelKeys.mjs";
|
|
3
|
+
import { toSnakeKeys } from "../utils/toSnakeKeys.mjs";
|
|
4
|
+
import { shouldSerializeAsMultipart, toMultipartBody } from "../validations/fileUpload.mjs";
|
|
5
|
+
//#region src/requests/request.ts
|
|
6
|
+
const request = async (resource, method = `GET`, body, options) => {
|
|
7
|
+
if (!options?.anonymous) discord.getSession();
|
|
8
|
+
/**
|
|
9
|
+
* Serialize the body. The `multipart()` schema wrapper stamps a
|
|
10
|
+
* sentinel on the parsed body when {@link FileUpload}s are present,
|
|
11
|
+
* which switches serialization from JSON to `multipart/form-data`.
|
|
12
|
+
*/
|
|
13
|
+
const serializeBody = () => {
|
|
14
|
+
if (!body) return body;
|
|
15
|
+
try {
|
|
16
|
+
if (shouldSerializeAsMultipart(body)) return toMultipartBody(body, toSnakeKeys);
|
|
17
|
+
return JSON.stringify(toSnakeKeys(body));
|
|
18
|
+
} catch (cause) {
|
|
19
|
+
console.error(`Received malformed request body:\n\n`, { body });
|
|
20
|
+
throw new Error(`Failed to serialize request body!`, { cause });
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const res = await discord.queueRequest(resource, method, serializeBody(), options);
|
|
24
|
+
if (!res.ok) throw new Error(`Request to resource '${resource.toString()}' failed:\n\n${res.statusText}`);
|
|
25
|
+
if (res.status === 204) return;
|
|
26
|
+
return toCamelKeys(await res.json());
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { request };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Fetcher } from "./methods.mjs";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
import { AnyProcedureBuilder, MutationProcedure, ProcedureType, QueryProcedure, SubscriptionProcedure } from "@trpc/server/unstable-core-do-not-import";
|
|
4
|
+
|
|
5
|
+
//#region src/requests/toProcedure.d.ts
|
|
6
|
+
type Result<T = void> = T extends v.GenericSchema | v.GenericSchemaAsync ? v.InferOutput<T> : T;
|
|
7
|
+
type Schema = v.GenericSchema | v.GenericSchemaAsync;
|
|
8
|
+
type ProvedureDef<I extends Schema | null = null, O extends Schema | null = null> = I extends Schema ? O extends Schema ? {
|
|
9
|
+
input: v.InferInput<I>;
|
|
10
|
+
output: v.InferOutput<O>;
|
|
11
|
+
meta: unknown;
|
|
12
|
+
} : {
|
|
13
|
+
input: v.InferInput<I>;
|
|
14
|
+
output: undefined;
|
|
15
|
+
meta: unknown;
|
|
16
|
+
} : O extends Schema ? {
|
|
17
|
+
input: undefined;
|
|
18
|
+
output: v.InferOutput<O>;
|
|
19
|
+
meta: unknown;
|
|
20
|
+
} : {
|
|
21
|
+
input: undefined;
|
|
22
|
+
output: undefined;
|
|
23
|
+
meta: unknown;
|
|
24
|
+
};
|
|
25
|
+
type BaseProcedure<T extends ProcedureType, I extends Schema | null = null, O extends Schema | null = null> = T extends `query` ? QueryProcedure<ProvedureDef<I, O>> : T extends `mutation` ? MutationProcedure<ProvedureDef<I, O>> : SubscriptionProcedure<ProvedureDef<I, O>>;
|
|
26
|
+
/**
|
|
27
|
+
* Given a {@link Fetcher | Fetcher} function and it's associated input and
|
|
28
|
+
* output Zod schemas, this produces a function which accepts a tRPC procedure
|
|
29
|
+
* builder of the given procedure type. This can then be used in a tRPC router
|
|
30
|
+
* to scaffold an API route to forward a request to Discord's API.
|
|
31
|
+
*
|
|
32
|
+
* Capability-free fetchers only — endpoints that require `{ anonymous: true }`
|
|
33
|
+
* or accept `{ reason: string }` cannot currently be wrapped via this helper,
|
|
34
|
+
* because tRPC has no natural channel for those per-call options.
|
|
35
|
+
*
|
|
36
|
+
* @__NO_SIDE_EFFECTS__
|
|
37
|
+
*/
|
|
38
|
+
declare const toProcedure: <T extends ProcedureType, I extends Schema | null = null, O extends Schema | null = null>(type: T, fn: Fetcher<I extends Schema ? I : null, Result<O>>, input?: I, output?: O) => (procedure: AnyProcedureBuilder, errorHandler?: (error: unknown) => void) => BaseProcedure<T, I, O>;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { toProcedure };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { isNonNullable } from "../utils/isNonNullable.mjs";
|
|
2
|
+
//#region src/requests/toProcedure.ts
|
|
3
|
+
/**
|
|
4
|
+
* Given a {@link Fetcher | Fetcher} function and it's associated input and
|
|
5
|
+
* output Zod schemas, this produces a function which accepts a tRPC procedure
|
|
6
|
+
* builder of the given procedure type. This can then be used in a tRPC router
|
|
7
|
+
* to scaffold an API route to forward a request to Discord's API.
|
|
8
|
+
*
|
|
9
|
+
* Capability-free fetchers only — endpoints that require `{ anonymous: true }`
|
|
10
|
+
* or accept `{ reason: string }` cannot currently be wrapped via this helper,
|
|
11
|
+
* because tRPC has no natural channel for those per-call options.
|
|
12
|
+
*
|
|
13
|
+
* @__NO_SIDE_EFFECTS__
|
|
14
|
+
*/
|
|
15
|
+
const toProcedure = (type, fn, input, output) => (procedure, errorHandler) => {
|
|
16
|
+
try {
|
|
17
|
+
if (isNonNullable(input) && isNonNullable(output)) return procedure.input(input).output(output)[type](async (opts) => fn(opts.input));
|
|
18
|
+
if (isNonNullable(input) && !isNonNullable(output)) return procedure.input(input)[type](async (opts) => fn(opts.input));
|
|
19
|
+
if (!isNonNullable(input) && isNonNullable(output)) return procedure.output(output)[type](async () => fn());
|
|
20
|
+
return procedure[type](async () => fn());
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (typeof errorHandler === `function`) errorHandler(error);
|
|
23
|
+
throw new Error(`Unhandled Procedure Error!`, { cause: error });
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { toProcedure };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Fetcher } from "./methods.mjs";
|
|
2
|
+
import { GenericSchema } from "valibot";
|
|
3
|
+
|
|
4
|
+
//#region src/requests/toQuery.d.ts
|
|
5
|
+
interface Register {}
|
|
6
|
+
type QueryKey = readonly unknown[];
|
|
7
|
+
type QueryMeta = Register extends {
|
|
8
|
+
queryMeta: infer TQueryMeta;
|
|
9
|
+
} ? TQueryMeta extends Record<string, unknown> ? TQueryMeta : Record<string, unknown> : Record<string, unknown>;
|
|
10
|
+
type FetchDirection = `backward` | `forward`;
|
|
11
|
+
type QueryFunctionContext<TQueryKey extends QueryKey = QueryKey, TPageParam = never> = [TPageParam] extends [never] ? {
|
|
12
|
+
queryKey: TQueryKey;
|
|
13
|
+
signal: AbortSignal;
|
|
14
|
+
meta: QueryMeta | undefined;
|
|
15
|
+
} : {
|
|
16
|
+
queryKey: TQueryKey;
|
|
17
|
+
signal: AbortSignal;
|
|
18
|
+
pageParam: TPageParam;
|
|
19
|
+
direction: FetchDirection;
|
|
20
|
+
meta: QueryMeta | undefined;
|
|
21
|
+
};
|
|
22
|
+
type QueryFunction<T = unknown, TQueryKey extends QueryKey = QueryKey, TPageParam = never> = (context: QueryFunctionContext<TQueryKey, TPageParam>) => Promise<T> | T;
|
|
23
|
+
/**
|
|
24
|
+
* Given a {@link Fetcher | Fetcher} function, transforms it into a curried function
|
|
25
|
+
* which can then be used with React-Query as a query function without
|
|
26
|
+
* the need for any additional boilerplate.
|
|
27
|
+
*
|
|
28
|
+
* Capability-free fetchers only — endpoints that require `{ anonymous: true }`
|
|
29
|
+
* or accept `{ reason: string }` cannot currently be wrapped via this helper,
|
|
30
|
+
* because react-query has no natural channel for those per-call options.
|
|
31
|
+
*
|
|
32
|
+
* @__NO_SIDE_EFFECTS__
|
|
33
|
+
*/
|
|
34
|
+
declare const toQuery: <S extends GenericSchema | null, R, T extends Fetcher<S, R>>(fn: T) => Parameters<T>[`length`] extends 0 ? () => QueryFunction<Awaited<ReturnType<T>>> : (config: Parameters<T>[0]) => QueryFunction<Awaited<ReturnType<T>>>;
|
|
35
|
+
//#endregion
|
|
36
|
+
export { QueryFunction, toQuery };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/requests/toQuery.ts
|
|
2
|
+
/**
|
|
3
|
+
* Given a {@link Fetcher | Fetcher} function, transforms it into a curried function
|
|
4
|
+
* which can then be used with React-Query as a query function without
|
|
5
|
+
* the need for any additional boilerplate.
|
|
6
|
+
*
|
|
7
|
+
* Capability-free fetchers only — endpoints that require `{ anonymous: true }`
|
|
8
|
+
* or accept `{ reason: string }` cannot currently be wrapped via this helper,
|
|
9
|
+
* because react-query has no natural channel for those per-call options.
|
|
10
|
+
*
|
|
11
|
+
* @__NO_SIDE_EFFECTS__
|
|
12
|
+
*/
|
|
13
|
+
const toQuery = (fn) => (...config) => async () => {
|
|
14
|
+
return fn(...config);
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { toQuery };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Fetcher, FetcherCapabilities } from "./methods.mjs";
|
|
2
|
+
import { GenericSchema, GenericSchemaAsync } from "valibot";
|
|
3
|
+
|
|
4
|
+
//#region src/requests/toValidated.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Given a {@link Fetcher | Fetcher} function and it's associated input
|
|
7
|
+
* and output Zod schemas, this returns a new validated {@link Fetcher | Fetcher} function which will validate it's input and result at runtime.
|
|
8
|
+
* This is useful in contexts where you want strong guarantees on runtime
|
|
9
|
+
* type-safety when dealing with raw user input in a framework agnostic
|
|
10
|
+
* environment.
|
|
11
|
+
*
|
|
12
|
+
* @__NO_SIDE_EFFECTS__
|
|
13
|
+
*/
|
|
14
|
+
declare const toValidated: <S extends GenericSchema | GenericSchemaAsync | null = null, R = void, C extends FetcherCapabilities = {}>(fn: Fetcher<S, R, C>, input?: S | null, output?: GenericSchema<unknown, R> | GenericSchemaAsync<unknown, R>) => Fetcher<S, R, C>;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { toValidated };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isOfKind, safeParseAsync, summarize } from "valibot";
|
|
2
|
+
//#region src/requests/toValidated.ts
|
|
3
|
+
/**
|
|
4
|
+
* Given a {@link Fetcher | Fetcher} function and it's associated input
|
|
5
|
+
* and output Zod schemas, this returns a new validated {@link Fetcher | Fetcher} function which will validate it's input and result at runtime.
|
|
6
|
+
* This is useful in contexts where you want strong guarantees on runtime
|
|
7
|
+
* type-safety when dealing with raw user input in a framework agnostic
|
|
8
|
+
* environment.
|
|
9
|
+
*
|
|
10
|
+
* @__NO_SIDE_EFFECTS__
|
|
11
|
+
*/
|
|
12
|
+
const toValidated = (fn, input, output) => new Proxy(fn, { async apply(target, _, [config, options]) {
|
|
13
|
+
if (input && isOfKind(`schema`, input)) {
|
|
14
|
+
const { issues } = await safeParseAsync(input, config);
|
|
15
|
+
if (issues) throw new Error(`Failed to parse input schema: ${input.reference.name}\n\n${summarize(issues)}`);
|
|
16
|
+
}
|
|
17
|
+
const result = await target(config, options);
|
|
18
|
+
if (output && isOfKind(`schema`, output)) {
|
|
19
|
+
const { issues } = await safeParseAsync(output, result);
|
|
20
|
+
if (issues) throw new Error(`Failed to parse input schema: ${output.reference.name}\n\n${summarize(issues)}`);
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
} });
|
|
24
|
+
//#endregion
|
|
25
|
+
export { toValidated };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/requests/verifyKey.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Validates a payload from Discord against its signature and key.
|
|
4
|
+
*
|
|
5
|
+
* @param rawBody - The raw payload data
|
|
6
|
+
* @param signature - The signature from the `X-Signature-Ed25519` header
|
|
7
|
+
* @param timestamp - The timestamp from the `X-Signature-Timestamp` header
|
|
8
|
+
* @param clientPublicKey - The public key from the Discord developer dashboard
|
|
9
|
+
* @returns Whether or not validation was successful
|
|
10
|
+
*/
|
|
11
|
+
declare function verifyKey(rawBody: Uint8Array | ArrayBuffer | Buffer | string, signature: string, timestamp: string, clientPublicKey: string | CryptoKey): Promise<boolean>;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { verifyKey };
|