@theholocron/discord-client 1.44.0 → 1.46.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/README.md +27 -6
- package/dist/index.d.mts +237 -1
- package/dist/index.mjs +251 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -99,12 +99,13 @@ the resource, look it up by name before creating it again. A webhook created
|
|
|
99
99
|
that way cannot have its URL read back, since Discord returns the token only on
|
|
100
100
|
the create response.
|
|
101
101
|
|
|
102
|
-
| Resource
|
|
103
|
-
|
|
|
104
|
-
| `client.users`
|
|
105
|
-
| `client.
|
|
106
|
-
| `client.
|
|
107
|
-
| `client.
|
|
102
|
+
| Resource | Methods |
|
|
103
|
+
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
104
|
+
| `client.users` | `me()` — `GET /users/@me`, a cheap token check; `get(id)` — a user's name |
|
|
105
|
+
| `client.application` | `get`, `modify` (the bot's own application: description, tags, install settings, URLs) |
|
|
106
|
+
| `client.guilds` | `get`, `modify`, `channels`, `createChannel`, `roles`, `createRole`, `modifyRole`, `create`, `delete`, `emojis`, `member`, `members`, `membersWithRole`, `addMemberRole`, `removeMemberRole`, `welcomeScreen`, `modifyWelcomeScreen`, `onboarding`, `modifyOnboarding` |
|
|
107
|
+
| `client.channels` | `modify`, `webhooks`, `createWebhook`, `createMessage`, `createInvite`, `messages`, `pin`, `deleteMessage` |
|
|
108
|
+
| `client.webhooks` | `modify` (name, avatar), `delete` |
|
|
108
109
|
|
|
109
110
|
`permissions([...names])` builds the decimal-string bitfield Discord expects, and
|
|
110
111
|
`permissionNames(bitfield)` reads one back; `Permission` holds the bits themselves.
|
|
@@ -114,6 +115,26 @@ features on, and `modifyOnboarding` is a `PUT` that replaces the whole config.
|
|
|
114
115
|
otherwise.
|
|
115
116
|
|
|
116
117
|
The bot needs Manage Channels, Manage Roles, Manage Webhooks and (for server settings, welcome screen and onboarding) Manage Server in the server, plus View Channel and Send Messages in any channel it posts to with `channels.createMessage`.
|
|
118
|
+
|
|
119
|
+
### Linking members to the role
|
|
120
|
+
|
|
121
|
+
`createDiscordOAuthClient({ clientId, clientSecret, redirectUri })` is "sign in with Discord": `authorizeUrl({ state })`
|
|
122
|
+
builds the link a member opens, `exchangeCode(code)` trades what Discord sends back for a token, and `user(token)` and
|
|
123
|
+
`connections(token)` read who they are and which accounts they have linked. `findVerifiedConnection(connections, "github")`
|
|
124
|
+
picks the GitHub account Discord has confirmed they own (an unverified name proves nothing). `revoke(token)` discards the
|
|
125
|
+
token once it has been read; nothing needs to keep it. `signOAuthState(secret)` and `verifyOAuthState(secret, state)` make
|
|
126
|
+
and check a `state` that expires and cannot be forged, so a server with no storage can still refuse a forged callback.
|
|
127
|
+
|
|
128
|
+
`guilds.membersWithRole(guildId, roleId)` lists who holds a role; like `guilds.members` it needs the privileged **Server
|
|
129
|
+
Members Intent**, switched on in the developer portal. `guilds.member(guildId, userId)` reads one member and needs no intent.
|
|
130
|
+
|
|
131
|
+
### Servers and checks
|
|
132
|
+
|
|
133
|
+
`guilds.create` makes a server the bot owns (only a bot in fewer than ten servers may), `guilds.modify(id, { owner_id })`
|
|
134
|
+
hands it over, `channels.createInvite` makes the link to join it, and `guilds.delete` removes it. On the webhook resource,
|
|
135
|
+
`executeAndWait` posts and returns the message id and `deleteMessage` removes it, so a check can post a line, read it back
|
|
136
|
+
with `channels.messages`, and clean up. `channels.pin` pins a message.
|
|
137
|
+
|
|
117
138
|
Discord only returns a webhook's `token`/`url` for webhooks the same
|
|
118
139
|
application created, so a pre-existing webhook can be found by name but its URL
|
|
119
140
|
cannot be read back.
|
package/dist/index.d.mts
CHANGED
|
@@ -67,6 +67,8 @@ interface DiscordGuild {
|
|
|
67
67
|
}
|
|
68
68
|
interface ModifyGuildInput {
|
|
69
69
|
name?: string;
|
|
70
|
+
/** Hands the server to another user; only the current owner (never a bot that does not own it) may. */
|
|
71
|
+
owner_id?: string;
|
|
70
72
|
verification_level?: number;
|
|
71
73
|
default_message_notifications?: number;
|
|
72
74
|
explicit_content_filter?: number;
|
|
@@ -164,6 +166,16 @@ interface ModifyChannelInput {
|
|
|
164
166
|
parent_id?: string | null;
|
|
165
167
|
permission_overwrites?: DiscordPermissionOverwrite[];
|
|
166
168
|
}
|
|
169
|
+
/** A member of a server. Only what provisioning needs: who they are and which roles they hold. */
|
|
170
|
+
interface DiscordGuildMember {
|
|
171
|
+
user?: {
|
|
172
|
+
id: string;
|
|
173
|
+
username?: string;
|
|
174
|
+
};
|
|
175
|
+
/** Ids of the roles the member holds. */
|
|
176
|
+
roles: string[];
|
|
177
|
+
nick?: string | null;
|
|
178
|
+
}
|
|
167
179
|
interface DiscordRole {
|
|
168
180
|
id: string;
|
|
169
181
|
name: string;
|
|
@@ -219,11 +231,93 @@ interface DiscordMessage {
|
|
|
219
231
|
channel_id: string;
|
|
220
232
|
content: string;
|
|
221
233
|
}
|
|
234
|
+
/** How the bot is added to a server or an account. */
|
|
235
|
+
interface DiscordInstallParams {
|
|
236
|
+
/** OAuth scopes, e.g. `bot`, `applications.commands`. */
|
|
237
|
+
scopes: string[];
|
|
238
|
+
/** Permission bitfield the default install link asks for, as a string. */
|
|
239
|
+
permissions: string;
|
|
240
|
+
}
|
|
241
|
+
/** The bot's own application, as `GET /applications/@me` returns it. Fields Discord leaves out are absent or null. */
|
|
242
|
+
interface DiscordApplication {
|
|
243
|
+
id: string;
|
|
244
|
+
name: string;
|
|
245
|
+
description?: string | null;
|
|
246
|
+
/** Up to five short labels shown on the application's page. */
|
|
247
|
+
tags?: string[];
|
|
248
|
+
/** The default install link's scopes and permissions. */
|
|
249
|
+
install_params?: DiscordInstallParams;
|
|
250
|
+
/** Used instead of the default install link when set. */
|
|
251
|
+
custom_install_url?: string | null;
|
|
252
|
+
role_connections_verification_url?: string | null;
|
|
253
|
+
terms_of_service_url?: string | null;
|
|
254
|
+
privacy_policy_url?: string | null;
|
|
255
|
+
interactions_endpoint_url?: string | null;
|
|
256
|
+
/** Whether anyone can add the bot (`true`) or only its owner. */
|
|
257
|
+
bot_public?: boolean;
|
|
258
|
+
bot_require_code_grant?: boolean;
|
|
259
|
+
flags?: number;
|
|
260
|
+
}
|
|
261
|
+
/** What `PATCH /applications/@me` takes; a field left out is left alone, and `null` clears the ones that can be cleared. */
|
|
262
|
+
interface ModifyApplicationInput {
|
|
263
|
+
description?: string;
|
|
264
|
+
tags?: string[];
|
|
265
|
+
install_params?: DiscordInstallParams;
|
|
266
|
+
custom_install_url?: string | null;
|
|
267
|
+
role_connections_verification_url?: string | null;
|
|
268
|
+
terms_of_service_url?: string | null;
|
|
269
|
+
privacy_policy_url?: string | null;
|
|
270
|
+
interactions_endpoint_url?: string | null;
|
|
271
|
+
}
|
|
222
272
|
interface ModifyWebhookInput {
|
|
223
273
|
name?: string;
|
|
224
274
|
/** A data URI (`data:image/png;base64,...`); `null` clears it. */
|
|
225
275
|
avatar?: string | null;
|
|
226
276
|
}
|
|
277
|
+
/** What `POST /guilds` takes. Only a bot in fewer than ten servers may create one, and it becomes the owner. */
|
|
278
|
+
interface CreateGuildInput {
|
|
279
|
+
name: string;
|
|
280
|
+
/** A data URI (`data:image/png;base64,...`). */
|
|
281
|
+
icon?: string;
|
|
282
|
+
verification_level?: number;
|
|
283
|
+
default_message_notifications?: number;
|
|
284
|
+
explicit_content_filter?: number;
|
|
285
|
+
}
|
|
286
|
+
/** Page options for `guilds.members`; Discord caps `limit` at 1000. */
|
|
287
|
+
interface ListMembersInput {
|
|
288
|
+
limit?: number;
|
|
289
|
+
/** Return members after this user id. */
|
|
290
|
+
after?: string;
|
|
291
|
+
}
|
|
292
|
+
interface DiscordEmoji {
|
|
293
|
+
id: string | null;
|
|
294
|
+
name: string | null;
|
|
295
|
+
animated?: boolean;
|
|
296
|
+
available?: boolean;
|
|
297
|
+
}
|
|
298
|
+
interface CreateInviteInput {
|
|
299
|
+
/** Seconds until it expires; `0` never. Discord's default is 86400. */
|
|
300
|
+
max_age?: number;
|
|
301
|
+
/** Uses before it is revoked; `0` unlimited. */
|
|
302
|
+
max_uses?: number;
|
|
303
|
+
temporary?: boolean;
|
|
304
|
+
unique?: boolean;
|
|
305
|
+
}
|
|
306
|
+
interface DiscordInvite {
|
|
307
|
+
code: string;
|
|
308
|
+
channel_id?: string;
|
|
309
|
+
uses?: number;
|
|
310
|
+
}
|
|
311
|
+
/** A message listed back from a channel, with who wrote it. */
|
|
312
|
+
interface DiscordChannelMessage extends DiscordMessage {
|
|
313
|
+
author?: {
|
|
314
|
+
id: string;
|
|
315
|
+
username?: string;
|
|
316
|
+
bot?: boolean;
|
|
317
|
+
};
|
|
318
|
+
webhook_id?: string;
|
|
319
|
+
pinned?: boolean;
|
|
320
|
+
}
|
|
227
321
|
//#endregion
|
|
228
322
|
//#region ../../../../../src/bot/bot.d.ts
|
|
229
323
|
/**
|
|
@@ -241,9 +335,15 @@ declare function createDiscordBotClient(opts: DiscordBotClientOptions): {
|
|
|
241
335
|
users: {
|
|
242
336
|
/** `GET /users/@me` — the bot's own user; a cheap token check. */
|
|
243
337
|
me: () => Promise<DiscordUser>;
|
|
338
|
+
/** `GET /users/:id` — a user's name, for reports that would otherwise show a bare id. */
|
|
339
|
+
get: (userId: string) => Promise<DiscordUser>;
|
|
244
340
|
};
|
|
245
341
|
guilds: {
|
|
246
342
|
get: (guildId: string) => Promise<DiscordGuild>;
|
|
343
|
+
/** `POST /guilds` — a new server the bot owns. Not idempotent: look one up by name before retrying. */
|
|
344
|
+
create: (body: CreateGuildInput) => Promise<DiscordGuild>;
|
|
345
|
+
/** `DELETE /guilds/:id` — only the owner may; a 404 counts as done. */
|
|
346
|
+
delete: (guildId: string) => Promise<void>;
|
|
247
347
|
/** `PATCH /guilds/:id` — verification, notification defaults, content filter, system channels. */
|
|
248
348
|
modify: (guildId: string, body: ModifyGuildInput) => Promise<DiscordGuild>;
|
|
249
349
|
welcomeScreen: (guildId: string) => Promise<DiscordWelcomeScreen>;
|
|
@@ -260,16 +360,44 @@ declare function createDiscordBotClient(opts: DiscordBotClientOptions): {
|
|
|
260
360
|
roles: (guildId: string) => Promise<DiscordRole[]>;
|
|
261
361
|
createRole: (guildId: string, body: CreateRoleInput) => Promise<DiscordRole>;
|
|
262
362
|
modifyRole: (guildId: string, roleId: string, body: ModifyRoleInput) => Promise<DiscordRole>;
|
|
363
|
+
/** `GET /guilds/:id/members/:id` — one member and the roles they hold. A 404 means either they are not in the server or the guild id is unknown; read the error body to tell them apart. */
|
|
364
|
+
member: (guildId: string, userId: string) => Promise<DiscordGuildMember>;
|
|
365
|
+
/**
|
|
366
|
+
* `GET /guilds/:id/members` — one page. Needs the privileged Server Members Intent, switched on in the
|
|
367
|
+
* developer portal; without it Discord answers 403.
|
|
368
|
+
*/
|
|
369
|
+
members: (guildId: string, opts?: ListMembersInput) => Promise<DiscordGuildMember[]>;
|
|
370
|
+
/** Every member holding the role, read page by page; needs the same intent as `members`. */
|
|
371
|
+
membersWithRole: (guildId: string, roleId: string) => Promise<DiscordGuildMember[]>;
|
|
372
|
+
/** `GET /guilds/:id/emojis` — custom emoji, for welcome screen and onboarding ids. */
|
|
373
|
+
emojis: (guildId: string) => Promise<DiscordEmoji[]>;
|
|
374
|
+
/** `PUT /guilds/:id/members/:id/roles/:id` — gives the member the role; giving one they have is a no-op. */
|
|
375
|
+
addMemberRole: (guildId: string, userId: string, roleId: string) => Promise<void>;
|
|
376
|
+
/** `DELETE /guilds/:id/members/:id/roles/:id` — takes the role away; taking one they lack is a no-op. */
|
|
377
|
+
removeMemberRole: (guildId: string, userId: string, roleId: string) => Promise<void>;
|
|
263
378
|
};
|
|
264
379
|
channels: {
|
|
265
380
|
modify: (channelId: string, body: ModifyChannelInput) => Promise<DiscordChannel>;
|
|
266
381
|
webhooks: (channelId: string) => Promise<DiscordChannelWebhook[]>;
|
|
267
382
|
/** `POST /channels/:id/messages` as the bot. Mentions are off unless `allowed_mentions` asks for them. */
|
|
268
383
|
createMessage: (channelId: string, body: CreateMessageInput) => Promise<DiscordMessage>;
|
|
384
|
+
/** `POST /channels/:id/invites` — a join link for the server, e.g. to hand over a newly created one. */
|
|
385
|
+
createInvite: (channelId: string, body?: CreateInviteInput) => Promise<DiscordInvite>;
|
|
386
|
+
/** `GET /channels/:id/messages` — the newest messages first, to read a post back. */
|
|
387
|
+
messages: (channelId: string, limit?: number) => Promise<DiscordChannelMessage[]>;
|
|
388
|
+
/** `PUT /channels/:id/pins/:id` — pins a message; one already pinned is a no-op. */
|
|
389
|
+
pin: (channelId: string, messageId: string) => Promise<void>;
|
|
390
|
+
deleteMessage: (channelId: string, messageId: string) => Promise<void>;
|
|
269
391
|
createWebhook: (channelId: string, body: {
|
|
270
392
|
name: string;
|
|
271
393
|
}) => Promise<DiscordChannelWebhook>;
|
|
272
394
|
};
|
|
395
|
+
application: {
|
|
396
|
+
/** `GET /applications/@me` — the bot's own application: description, tags, install settings, URLs. */
|
|
397
|
+
get: () => Promise<DiscordApplication>;
|
|
398
|
+
/** `PATCH /applications/@me` — only what is in the body changes. */
|
|
399
|
+
modify: (body: ModifyApplicationInput) => Promise<DiscordApplication>;
|
|
400
|
+
};
|
|
273
401
|
webhooks: {
|
|
274
402
|
/** `PATCH /webhooks/:id` — name and avatar of a webhook this bot manages. */
|
|
275
403
|
modify: (webhookId: string, body: ModifyWebhookInput) => Promise<DiscordChannelWebhook>;
|
|
@@ -344,13 +472,121 @@ declare function permissions(names: readonly PermissionName[]): string;
|
|
|
344
472
|
/** The permission names set in a bitfield, in declaration order (unknown bits are ignored). */
|
|
345
473
|
declare function permissionNames(bitfield: string): PermissionName[];
|
|
346
474
|
//#endregion
|
|
475
|
+
//#region ../../../../../src/oauth/oauth.type.d.ts
|
|
476
|
+
interface DiscordOAuthClientOptions {
|
|
477
|
+
/** The application's client id (the same as its application id). Not a secret. */
|
|
478
|
+
clientId: string;
|
|
479
|
+
/** The application's client secret, from the developer portal (OAuth2 → Reset Secret). */
|
|
480
|
+
clientSecret: string;
|
|
481
|
+
/** One of the redirect URIs registered on the application; Discord refuses any other. */
|
|
482
|
+
redirectUri: string;
|
|
483
|
+
/** Override the API base for testing. Defaults to https://discord.com/api/v10. */
|
|
484
|
+
baseUrl?: string;
|
|
485
|
+
/** Override the authorize page for testing. Defaults to https://discord.com/oauth2/authorize. */
|
|
486
|
+
authorizeUrl?: string;
|
|
487
|
+
/** Override fetch for testing. Defaults to globalThis.fetch. */
|
|
488
|
+
fetch?: typeof fetch;
|
|
489
|
+
/** Per-request deadline in milliseconds. Defaults to 15000. */
|
|
490
|
+
timeoutMs?: number;
|
|
491
|
+
}
|
|
492
|
+
/** What a member's token may do: `identify` reads who they are, `connections` reads their linked accounts. */
|
|
493
|
+
type DiscordOAuthScope = "identify" | "connections";
|
|
494
|
+
interface DiscordOAuthToken {
|
|
495
|
+
access_token: string;
|
|
496
|
+
token_type: string;
|
|
497
|
+
/** Seconds until `access_token` stops working. */
|
|
498
|
+
expires_in: number;
|
|
499
|
+
refresh_token?: string;
|
|
500
|
+
/** The scopes granted, space separated. */
|
|
501
|
+
scope: string;
|
|
502
|
+
}
|
|
503
|
+
/** A linked account on a member's profile, as `GET /users/@me/connections` returns it. */
|
|
504
|
+
interface DiscordConnection {
|
|
505
|
+
/** The id on the other service; for GitHub, the numeric user id. */
|
|
506
|
+
id: string;
|
|
507
|
+
/** The name on the other service; for GitHub, the login. */
|
|
508
|
+
name: string;
|
|
509
|
+
/** The service, lower case: `github`, `twitch`, … */
|
|
510
|
+
type: string;
|
|
511
|
+
/** Whether Discord confirmed the member owns the account. */
|
|
512
|
+
verified: boolean;
|
|
513
|
+
visibility?: number;
|
|
514
|
+
}
|
|
515
|
+
/** The member who signed in, as `GET /users/@me` returns it with their token. */
|
|
516
|
+
interface DiscordOAuthUser {
|
|
517
|
+
id: string;
|
|
518
|
+
username: string;
|
|
519
|
+
global_name?: string | null;
|
|
520
|
+
}
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region ../../../../../src/oauth/oauth.d.ts
|
|
523
|
+
/**
|
|
524
|
+
* Discord OAuth2 for "sign in with Discord": build the link a member follows, trade the code Discord sends
|
|
525
|
+
* back for a token, and read who they are and which accounts they have linked. Used to learn a member's
|
|
526
|
+
* Discord id together with their GitHub account, which Discord has verified.
|
|
527
|
+
*
|
|
528
|
+
* The token endpoints take a form body and the client's own credentials, which `createDiscordBotClient`'s
|
|
529
|
+
* JSON + bot-token client cannot send, so this has its own small transport.
|
|
530
|
+
*/
|
|
531
|
+
declare function createDiscordOAuthClient(opts: DiscordOAuthClientOptions): {
|
|
532
|
+
/**
|
|
533
|
+
* The page a member opens to approve the application. `state` must be unguessable and checked when
|
|
534
|
+
* Discord sends the member back (see {@link signOAuthState}). `prompt: "none"` skips the approval
|
|
535
|
+
* screen for a member who already approved.
|
|
536
|
+
*/
|
|
537
|
+
authorizeUrl: (input: {
|
|
538
|
+
state: string;
|
|
539
|
+
scopes?: readonly DiscordOAuthScope[];
|
|
540
|
+
prompt?: "consent" | "none";
|
|
541
|
+
}) => string;
|
|
542
|
+
/** `POST /oauth2/token` — trades the one-use `code` from the redirect for a token. */
|
|
543
|
+
exchangeCode: (code: string) => Promise<DiscordOAuthToken>;
|
|
544
|
+
/** `POST /oauth2/token` — a fresh token from a `refresh_token`. */
|
|
545
|
+
refresh: (refreshToken: string) => Promise<DiscordOAuthToken>;
|
|
546
|
+
/** `POST /oauth2/token/revoke` — discards a token once it has been used; nothing needs to keep it. */
|
|
547
|
+
revoke: (token: string) => Promise<void>;
|
|
548
|
+
/** `GET /users/@me` as the member (needs `identify`). */
|
|
549
|
+
user: (accessToken: string) => Promise<DiscordOAuthUser>;
|
|
550
|
+
/** `GET /users/@me/connections` as the member (needs `connections`). */
|
|
551
|
+
connections: (accessToken: string) => Promise<DiscordConnection[]>;
|
|
552
|
+
};
|
|
553
|
+
type DiscordOAuthClient = ReturnType<typeof createDiscordOAuthClient>;
|
|
554
|
+
/**
|
|
555
|
+
* The verified connection of one service, or `undefined`. Unverified ones are ignored: anyone can type a
|
|
556
|
+
* GitHub name into their profile, and only a verified link proves they own it. When a member has several
|
|
557
|
+
* of the service, the first verified one wins.
|
|
558
|
+
*/
|
|
559
|
+
declare function findVerifiedConnection(connections: readonly DiscordConnection[], type: string): DiscordConnection | undefined;
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region ../../../../../src/oauth/state.d.ts
|
|
562
|
+
/** How long a sign-in link stays valid by default, in seconds. */
|
|
563
|
+
declare const DEFAULT_STATE_TTL_SECONDS = 600;
|
|
564
|
+
/**
|
|
565
|
+
* An OAuth `state` that a stateless server can check without storing anything: a random nonce and an
|
|
566
|
+
* expiry, signed with a server-side secret. Tampering with either, or using a different secret, fails
|
|
567
|
+
* {@link verifyOAuthState}. It does not bind the link to one browser: set the nonce in a cookie as well
|
|
568
|
+
* when that matters.
|
|
569
|
+
*/
|
|
570
|
+
declare function signOAuthState(secret: string, opts?: {
|
|
571
|
+
now?: number;
|
|
572
|
+
ttlSeconds?: number;
|
|
573
|
+
}): string;
|
|
574
|
+
/** Whether `state` was signed with `secret` and has not expired. Never throws on malformed input. */
|
|
575
|
+
declare function verifyOAuthState(secret: string, state: string, opts?: {
|
|
576
|
+
now?: number;
|
|
577
|
+
}): boolean;
|
|
578
|
+
//#endregion
|
|
347
579
|
//#region ../../../../../src/index.d.ts
|
|
348
580
|
declare function createDiscordClient(opts?: DiscordClientOptions): {
|
|
349
581
|
webhooks: {
|
|
350
582
|
get: (id: string, token: string) => Promise<DiscordWebhookInfo>;
|
|
351
583
|
execute: (id: string, token: string, content: string) => Promise<void>;
|
|
584
|
+
executeAndWait: (id: string, token: string, content: string) => Promise<{
|
|
585
|
+
id: string;
|
|
586
|
+
}>;
|
|
587
|
+
deleteMessage: (id: string, token: string, messageId: string) => Promise<void>;
|
|
352
588
|
};
|
|
353
589
|
};
|
|
354
590
|
type DiscordClient = ReturnType<typeof createDiscordClient>;
|
|
355
591
|
//#endregion
|
|
356
|
-
export { ChannelType, CreateChannelInput, CreateMessageInput, CreateRoleInput, DiscordBotClient, DiscordBotClientOptions, DiscordChannel, DiscordChannelWebhook, DiscordClient, type DiscordClientOptions, DiscordEmoji, DiscordGuild, DiscordMessage, DiscordOnboarding, DiscordOnboardingOption, DiscordOnboardingPrompt, DiscordPermissionOverwrite, DiscordRole, DiscordUser, type DiscordWebhookInfo, DiscordWelcomeChannel, DiscordWelcomeScreen, ModifyChannelInput, ModifyGuildInput, ModifyRoleInput, ModifyWebhookInput, ModifyWelcomeScreenInput, Permission, PermissionName, createDiscordBotClient, createDiscordClient, parseWebhookUrl, permissionNames, permissions };
|
|
592
|
+
export { ChannelType, CreateChannelInput, CreateGuildInput, CreateInviteInput, CreateMessageInput, CreateRoleInput, DEFAULT_STATE_TTL_SECONDS, DiscordApplication, DiscordBotClient, DiscordBotClientOptions, DiscordChannel, DiscordChannelMessage, DiscordChannelWebhook, DiscordClient, type DiscordClientOptions, DiscordConnection, DiscordEmoji, DiscordGuild, DiscordGuildMember, DiscordInstallParams, DiscordInvite, DiscordMessage, DiscordOAuthClient, DiscordOAuthClientOptions, DiscordOAuthScope, DiscordOAuthToken, DiscordOAuthUser, DiscordOnboarding, DiscordOnboardingOption, DiscordOnboardingPrompt, DiscordPermissionOverwrite, DiscordRole, DiscordUser, type DiscordWebhookInfo, DiscordWelcomeChannel, DiscordWelcomeScreen, ListMembersInput, ModifyApplicationInput, ModifyChannelInput, ModifyGuildInput, ModifyRoleInput, ModifyWebhookInput, ModifyWelcomeScreenInput, Permission, PermissionName, createDiscordBotClient, createDiscordClient, createDiscordOAuthClient, findVerifiedConnection, parseWebhookUrl, permissionNames, permissions, signOAuthState, verifyOAuthState };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ProviderApiError, createRestClient } from "@theholocron/http-client";
|
|
2
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
3
|
//#endregion
|
|
3
4
|
//#region ../../../../../src/webhooks/webhooks.ts
|
|
4
5
|
/**
|
|
@@ -32,6 +33,36 @@ function webhooks(base, f) {
|
|
|
32
33
|
throw new ProviderApiError(`Discord POST /webhooks failed: ${detail}`, 0, void 0);
|
|
33
34
|
}
|
|
34
35
|
if (!res.ok) throw new ProviderApiError(`Discord webhook POST failed (${res.status})`, res.status, void 0);
|
|
36
|
+
},
|
|
37
|
+
/** Posts and waits for Discord to answer with the message, so a check can read it back or delete it. */
|
|
38
|
+
executeAndWait: async (id, token, content) => {
|
|
39
|
+
let res;
|
|
40
|
+
try {
|
|
41
|
+
res = await f(`${base}/webhooks/${id}/${token}?wait=true`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: { "content-type": "application/json" },
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
content,
|
|
46
|
+
allowed_mentions: { parse: [] }
|
|
47
|
+
})
|
|
48
|
+
});
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const detail = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
51
|
+
throw new ProviderApiError(`Discord POST /webhooks failed: ${detail}`, 0, void 0);
|
|
52
|
+
}
|
|
53
|
+
if (!res.ok) throw new ProviderApiError(`Discord webhook POST failed (${res.status})`, res.status, void 0);
|
|
54
|
+
return res.json();
|
|
55
|
+
},
|
|
56
|
+
/** Removes a message this webhook posted, e.g. a smoke-test line. A 404 counts as done. */
|
|
57
|
+
deleteMessage: async (id, token, messageId) => {
|
|
58
|
+
let res;
|
|
59
|
+
try {
|
|
60
|
+
res = await f(`${base}/webhooks/${id}/${token}/messages/${messageId}`, { method: "DELETE" });
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const detail = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
63
|
+
throw new ProviderApiError(`Discord DELETE /webhooks failed: ${detail}`, 0, void 0);
|
|
64
|
+
}
|
|
65
|
+
if (!res.ok && res.status !== 404) throw new ProviderApiError(`Discord webhook DELETE failed (${res.status})`, res.status, void 0);
|
|
35
66
|
}
|
|
36
67
|
};
|
|
37
68
|
}
|
|
@@ -46,10 +77,12 @@ function parseWebhookUrl(url) {
|
|
|
46
77
|
}
|
|
47
78
|
//#endregion
|
|
48
79
|
//#region ../../../../../src/bot/bot.ts
|
|
49
|
-
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
80
|
+
const DEFAULT_TIMEOUT_MS$1 = 15e3;
|
|
50
81
|
const DEFAULT_MAX_RETRIES = 3;
|
|
51
82
|
/** A rate-limit wait longer than this fails fast instead of stalling the caller. */
|
|
52
83
|
const MAX_RETRY_WAIT_MS = 1e4;
|
|
84
|
+
/** A guard on `membersWithRole`, so a runaway cursor cannot loop forever. */
|
|
85
|
+
const MAX_MEMBER_PAGES = 50;
|
|
53
86
|
/** Discord ids are snowflakes; anything else would be spliced into a URL path. */
|
|
54
87
|
function id(value) {
|
|
55
88
|
if (!/^\d{1,20}$/.test(value)) throw new Error(`invalid Discord id: ${JSON.stringify(value)}`);
|
|
@@ -115,20 +148,40 @@ function createDiscordBotClient(opts) {
|
|
|
115
148
|
apiKeyHeader: "authorization",
|
|
116
149
|
vendor: "Discord",
|
|
117
150
|
fetch: guardedFetch(opts.fetch ?? globalThis.fetch, {
|
|
118
|
-
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
151
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS$1,
|
|
119
152
|
maxRetries: opts.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
120
153
|
sleep: opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)))
|
|
121
154
|
})
|
|
122
155
|
});
|
|
123
156
|
return {
|
|
124
|
-
users: {
|
|
125
|
-
|
|
126
|
-
me: () => rest.request("/users/@me")
|
|
157
|
+
users: {
|
|
158
|
+
/** `GET /users/@me` — the bot's own user; a cheap token check. */
|
|
159
|
+
me: () => rest.request("/users/@me"),
|
|
160
|
+
/** `GET /users/:id` — a user's name, for reports that would otherwise show a bare id. */
|
|
161
|
+
get: (userId) => rest.request(`/users/${id(userId)}`)
|
|
162
|
+
},
|
|
127
163
|
guilds: {
|
|
128
164
|
get: (guildId) => rest.request(`/guilds/${id(guildId)}`),
|
|
165
|
+
/** `POST /guilds` — a new server the bot owns. Not idempotent: look one up by name before retrying. */
|
|
166
|
+
create: (body) => rest.request("/guilds", {
|
|
167
|
+
method: "POST",
|
|
168
|
+
body
|
|
169
|
+
}),
|
|
170
|
+
/** `DELETE /guilds/:id` — only the owner may; a 404 counts as done. */
|
|
171
|
+
delete: async (guildId) => {
|
|
172
|
+
try {
|
|
173
|
+
await rest.request(`/guilds/${id(guildId)}`, {
|
|
174
|
+
method: "DELETE",
|
|
175
|
+
expectNoContent: true
|
|
176
|
+
});
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (err instanceof ProviderApiError && err.status === 404) return;
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
},
|
|
129
182
|
/** `PATCH /guilds/:id` — verification, notification defaults, content filter, system channels. */
|
|
130
183
|
modify: (guildId, body) => {
|
|
131
|
-
bodyIds(body.system_channel_id, body.rules_channel_id, body.public_updates_channel_id);
|
|
184
|
+
bodyIds(body.owner_id, body.system_channel_id, body.rules_channel_id, body.public_updates_channel_id);
|
|
132
185
|
return rest.request(`/guilds/${id(guildId)}`, {
|
|
133
186
|
method: "PATCH",
|
|
134
187
|
body
|
|
@@ -169,6 +222,46 @@ me: () => rest.request("/users/@me") },
|
|
|
169
222
|
modifyRole: (guildId, roleId, body) => rest.request(`/guilds/${id(guildId)}/roles/${id(roleId)}`, {
|
|
170
223
|
method: "PATCH",
|
|
171
224
|
body
|
|
225
|
+
}),
|
|
226
|
+
/** `GET /guilds/:id/members/:id` — one member and the roles they hold. A 404 means either they are not in the server or the guild id is unknown; read the error body to tell them apart. */
|
|
227
|
+
member: (guildId, userId) => rest.request(`/guilds/${id(guildId)}/members/${id(userId)}`),
|
|
228
|
+
/**
|
|
229
|
+
* `GET /guilds/:id/members` — one page. Needs the privileged Server Members Intent, switched on in the
|
|
230
|
+
* developer portal; without it Discord answers 403.
|
|
231
|
+
*/
|
|
232
|
+
members: (guildId, opts = {}) => {
|
|
233
|
+
const query = { limit: String(opts.limit ?? 1e3) };
|
|
234
|
+
if (opts.after !== void 0) query.after = id(opts.after);
|
|
235
|
+
return rest.request(`/guilds/${id(guildId)}/members`, { query });
|
|
236
|
+
},
|
|
237
|
+
/** Every member holding the role, read page by page; needs the same intent as `members`. */
|
|
238
|
+
membersWithRole: async (guildId, roleId) => {
|
|
239
|
+
const wanted = id(roleId);
|
|
240
|
+
const found = [];
|
|
241
|
+
let after;
|
|
242
|
+
for (let page = 0; page < MAX_MEMBER_PAGES; page++) {
|
|
243
|
+
const batch = await rest.request(`/guilds/${id(guildId)}/members`, { query: {
|
|
244
|
+
limit: "1000",
|
|
245
|
+
...after === void 0 ? {} : { after }
|
|
246
|
+
} });
|
|
247
|
+
for (const m of batch) if (m.roles.includes(wanted)) found.push(m);
|
|
248
|
+
const last = batch[batch.length - 1]?.user?.id;
|
|
249
|
+
if (batch.length < 1e3 || last === void 0) return found;
|
|
250
|
+
after = last;
|
|
251
|
+
}
|
|
252
|
+
throw new Error(`more than ${MAX_MEMBER_PAGES * 1e3} members; refusing to read them all`);
|
|
253
|
+
},
|
|
254
|
+
/** `GET /guilds/:id/emojis` — custom emoji, for welcome screen and onboarding ids. */
|
|
255
|
+
emojis: (guildId) => rest.request(`/guilds/${id(guildId)}/emojis`),
|
|
256
|
+
/** `PUT /guilds/:id/members/:id/roles/:id` — gives the member the role; giving one they have is a no-op. */
|
|
257
|
+
addMemberRole: (guildId, userId, roleId) => rest.request(`/guilds/${id(guildId)}/members/${id(userId)}/roles/${id(roleId)}`, {
|
|
258
|
+
method: "PUT",
|
|
259
|
+
expectNoContent: true
|
|
260
|
+
}),
|
|
261
|
+
/** `DELETE /guilds/:id/members/:id/roles/:id` — takes the role away; taking one they lack is a no-op. */
|
|
262
|
+
removeMemberRole: (guildId, userId, roleId) => rest.request(`/guilds/${id(guildId)}/members/${id(userId)}/roles/${id(roleId)}`, {
|
|
263
|
+
method: "DELETE",
|
|
264
|
+
expectNoContent: true
|
|
172
265
|
})
|
|
173
266
|
},
|
|
174
267
|
channels: {
|
|
@@ -188,11 +281,36 @@ me: () => rest.request("/users/@me") },
|
|
|
188
281
|
}
|
|
189
282
|
});
|
|
190
283
|
},
|
|
284
|
+
/** `POST /channels/:id/invites` — a join link for the server, e.g. to hand over a newly created one. */
|
|
285
|
+
createInvite: (channelId, body = {}) => rest.request(`/channels/${id(channelId)}/invites`, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
body
|
|
288
|
+
}),
|
|
289
|
+
/** `GET /channels/:id/messages` — the newest messages first, to read a post back. */
|
|
290
|
+
messages: (channelId, limit = 50) => rest.request(`/channels/${id(channelId)}/messages`, { query: { limit: String(limit) } }),
|
|
291
|
+
/** `PUT /channels/:id/pins/:id` — pins a message; one already pinned is a no-op. */
|
|
292
|
+
pin: (channelId, messageId) => rest.request(`/channels/${id(channelId)}/pins/${id(messageId)}`, {
|
|
293
|
+
method: "PUT",
|
|
294
|
+
expectNoContent: true
|
|
295
|
+
}),
|
|
296
|
+
deleteMessage: (channelId, messageId) => rest.request(`/channels/${id(channelId)}/messages/${id(messageId)}`, {
|
|
297
|
+
method: "DELETE",
|
|
298
|
+
expectNoContent: true
|
|
299
|
+
}),
|
|
191
300
|
createWebhook: async (channelId, body) => withUrl(await rest.request(`/channels/${id(channelId)}/webhooks`, {
|
|
192
301
|
method: "POST",
|
|
193
302
|
body
|
|
194
303
|
}))
|
|
195
304
|
},
|
|
305
|
+
application: {
|
|
306
|
+
/** `GET /applications/@me` — the bot's own application: description, tags, install settings, URLs. */
|
|
307
|
+
get: () => rest.request("/applications/@me"),
|
|
308
|
+
/** `PATCH /applications/@me` — only what is in the body changes. */
|
|
309
|
+
modify: (body) => rest.request("/applications/@me", {
|
|
310
|
+
method: "PATCH",
|
|
311
|
+
body
|
|
312
|
+
})
|
|
313
|
+
},
|
|
196
314
|
webhooks: {
|
|
197
315
|
/** `PATCH /webhooks/:id` — name and avatar of a webhook this bot manages. */
|
|
198
316
|
modify: async (webhookId, body) => withUrl(await rest.request(`/webhooks/${id(webhookId)}`, {
|
|
@@ -300,9 +418,135 @@ function permissionNames(bitfield) {
|
|
|
300
418
|
return Object.keys(Permission).filter((name) => (bits & Permission[name]) !== 0n);
|
|
301
419
|
}
|
|
302
420
|
//#endregion
|
|
421
|
+
//#region ../../../../../src/oauth/oauth.ts
|
|
422
|
+
const DEFAULT_AUTHORIZE_URL = "https://discord.com/oauth2/authorize";
|
|
423
|
+
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
424
|
+
/**
|
|
425
|
+
* Discord OAuth2 for "sign in with Discord": build the link a member follows, trade the code Discord sends
|
|
426
|
+
* back for a token, and read who they are and which accounts they have linked. Used to learn a member's
|
|
427
|
+
* Discord id together with their GitHub account, which Discord has verified.
|
|
428
|
+
*
|
|
429
|
+
* The token endpoints take a form body and the client's own credentials, which `createDiscordBotClient`'s
|
|
430
|
+
* JSON + bot-token client cannot send, so this has its own small transport.
|
|
431
|
+
*/
|
|
432
|
+
function createDiscordOAuthClient(opts) {
|
|
433
|
+
let base = opts.baseUrl ?? "https://discord.com/api/v10";
|
|
434
|
+
while (base.endsWith("/")) base = base.slice(0, -1);
|
|
435
|
+
const f = opts.fetch ?? globalThis.fetch;
|
|
436
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
437
|
+
const basic = `Basic ${btoa(`${opts.clientId}:${opts.clientSecret}`)}`;
|
|
438
|
+
async function call(path, init) {
|
|
439
|
+
const tag = `Discord ${init.method ?? "GET"} ${path}`;
|
|
440
|
+
let res;
|
|
441
|
+
try {
|
|
442
|
+
res = await f(`${base}${path}`, {
|
|
443
|
+
...init,
|
|
444
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
445
|
+
});
|
|
446
|
+
} catch (err) {
|
|
447
|
+
const detail = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
|
|
448
|
+
throw new ProviderApiError(`${tag} failed: ${detail}`, 0, void 0);
|
|
449
|
+
}
|
|
450
|
+
if (!res.ok) {
|
|
451
|
+
const body = await res.text().catch(() => "");
|
|
452
|
+
throw new ProviderApiError(`${tag} → ${res.status}`, res.status, body);
|
|
453
|
+
}
|
|
454
|
+
return await res.json();
|
|
455
|
+
}
|
|
456
|
+
function form(fields) {
|
|
457
|
+
return {
|
|
458
|
+
method: "POST",
|
|
459
|
+
headers: {
|
|
460
|
+
authorization: basic,
|
|
461
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
462
|
+
accept: "application/json"
|
|
463
|
+
},
|
|
464
|
+
body: new URLSearchParams(fields).toString()
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function bearer(accessToken) {
|
|
468
|
+
return { headers: {
|
|
469
|
+
authorization: `Bearer ${accessToken}`,
|
|
470
|
+
accept: "application/json"
|
|
471
|
+
} };
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
/**
|
|
475
|
+
* The page a member opens to approve the application. `state` must be unguessable and checked when
|
|
476
|
+
* Discord sends the member back (see {@link signOAuthState}). `prompt: "none"` skips the approval
|
|
477
|
+
* screen for a member who already approved.
|
|
478
|
+
*/
|
|
479
|
+
authorizeUrl: (input) => {
|
|
480
|
+
const url = new URL(opts.authorizeUrl ?? DEFAULT_AUTHORIZE_URL);
|
|
481
|
+
url.searchParams.set("client_id", opts.clientId);
|
|
482
|
+
url.searchParams.set("response_type", "code");
|
|
483
|
+
url.searchParams.set("redirect_uri", opts.redirectUri);
|
|
484
|
+
url.searchParams.set("scope", (input.scopes ?? ["identify", "connections"]).join(" "));
|
|
485
|
+
url.searchParams.set("state", input.state);
|
|
486
|
+
if (input.prompt) url.searchParams.set("prompt", input.prompt);
|
|
487
|
+
return url.toString();
|
|
488
|
+
},
|
|
489
|
+
/** `POST /oauth2/token` — trades the one-use `code` from the redirect for a token. */
|
|
490
|
+
exchangeCode: (code) => call("/oauth2/token", form({
|
|
491
|
+
grant_type: "authorization_code",
|
|
492
|
+
code,
|
|
493
|
+
redirect_uri: opts.redirectUri
|
|
494
|
+
})),
|
|
495
|
+
/** `POST /oauth2/token` — a fresh token from a `refresh_token`. */
|
|
496
|
+
refresh: (refreshToken) => call("/oauth2/token", form({
|
|
497
|
+
grant_type: "refresh_token",
|
|
498
|
+
refresh_token: refreshToken
|
|
499
|
+
})),
|
|
500
|
+
/** `POST /oauth2/token/revoke` — discards a token once it has been used; nothing needs to keep it. */
|
|
501
|
+
revoke: async (token) => {
|
|
502
|
+
await call("/oauth2/token/revoke", form({ token }));
|
|
503
|
+
},
|
|
504
|
+
/** `GET /users/@me` as the member (needs `identify`). */
|
|
505
|
+
user: (accessToken) => call("/users/@me", bearer(accessToken)),
|
|
506
|
+
/** `GET /users/@me/connections` as the member (needs `connections`). */
|
|
507
|
+
connections: (accessToken) => call("/users/@me/connections", bearer(accessToken))
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* The verified connection of one service, or `undefined`. Unverified ones are ignored: anyone can type a
|
|
512
|
+
* GitHub name into their profile, and only a verified link proves they own it. When a member has several
|
|
513
|
+
* of the service, the first verified one wins.
|
|
514
|
+
*/
|
|
515
|
+
function findVerifiedConnection(connections, type) {
|
|
516
|
+
return connections.find((c) => c.type === type && c.verified);
|
|
517
|
+
}
|
|
518
|
+
//#endregion
|
|
519
|
+
//#region ../../../../../src/oauth/state.ts
|
|
520
|
+
function mac(secret, message) {
|
|
521
|
+
return createHmac("sha256", secret).update(message).digest("hex");
|
|
522
|
+
}
|
|
523
|
+
/** How long a sign-in link stays valid by default, in seconds. */
|
|
524
|
+
const DEFAULT_STATE_TTL_SECONDS = 600;
|
|
525
|
+
/**
|
|
526
|
+
* An OAuth `state` that a stateless server can check without storing anything: a random nonce and an
|
|
527
|
+
* expiry, signed with a server-side secret. Tampering with either, or using a different secret, fails
|
|
528
|
+
* {@link verifyOAuthState}. It does not bind the link to one browser: set the nonce in a cookie as well
|
|
529
|
+
* when that matters.
|
|
530
|
+
*/
|
|
531
|
+
function signOAuthState(secret, opts = {}) {
|
|
532
|
+
const body = `${Math.floor((opts.now ?? Date.now()) / 1e3) + (opts.ttlSeconds ?? 600)}.${randomBytes(16).toString("hex")}`;
|
|
533
|
+
return `${body}.${mac(secret, body)}`;
|
|
534
|
+
}
|
|
535
|
+
/** Whether `state` was signed with `secret` and has not expired. Never throws on malformed input. */
|
|
536
|
+
function verifyOAuthState(secret, state, opts = {}) {
|
|
537
|
+
const parts = state.split(".");
|
|
538
|
+
if (parts.length !== 3) return false;
|
|
539
|
+
const [expires, nonce, sig] = parts;
|
|
540
|
+
if (!/^\d+$/.test(expires) || !/^[0-9a-f]{32}$/.test(nonce)) return false;
|
|
541
|
+
const want = Buffer.from(mac(secret, `${expires}.${nonce}`));
|
|
542
|
+
const got = Buffer.from(sig);
|
|
543
|
+
if (got.length !== want.length || !timingSafeEqual(got, want)) return false;
|
|
544
|
+
return Number(expires) >= Math.floor((opts.now ?? Date.now()) / 1e3);
|
|
545
|
+
}
|
|
546
|
+
//#endregion
|
|
303
547
|
//#region ../../../../../src/index.ts
|
|
304
548
|
function createDiscordClient(opts = {}) {
|
|
305
549
|
return { webhooks: webhooks(opts.baseUrl ?? "https://discord.com/api/v10", opts.fetch ?? globalThis.fetch) };
|
|
306
550
|
}
|
|
307
551
|
//#endregion
|
|
308
|
-
export { ChannelType, Permission, createDiscordBotClient, createDiscordClient, parseWebhookUrl, permissionNames, permissions };
|
|
552
|
+
export { ChannelType, DEFAULT_STATE_TTL_SECONDS, Permission, createDiscordBotClient, createDiscordClient, createDiscordOAuthClient, findVerifiedConnection, parseWebhookUrl, permissionNames, permissions, signOAuthState, verifyOAuthState };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/discord-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.46.0",
|
|
4
4
|
"description": "A TypeScript client for the Discord webhooks API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"discord",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"dist"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@theholocron/http-client": "^1.
|
|
35
|
+
"@theholocron/http-client": "^1.46.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@theholocron/cli": "5.0.0-alpha.79",
|