@dereekb/discord 13.33.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/LICENSE +21 -0
- package/README.md +11 -0
- package/index.cjs.default.js +1 -0
- package/index.cjs.js +817 -0
- package/index.cjs.mjs +2 -0
- package/index.d.ts +1 -0
- package/index.esm.js +789 -0
- package/nestjs/index.cjs.default.js +1 -0
- package/nestjs/index.cjs.js +1928 -0
- package/nestjs/index.cjs.mjs +2 -0
- package/nestjs/index.d.ts +1 -0
- package/nestjs/index.esm.js +1905 -0
- package/nestjs/package.json +26 -0
- package/nestjs/src/index.d.ts +1 -0
- package/nestjs/src/lib/discord/discord.api.d.ts +69 -0
- package/nestjs/src/lib/discord/discord.api.spec.client.d.ts +20 -0
- package/nestjs/src/lib/discord/discord.config.d.ts +58 -0
- package/nestjs/src/lib/discord/discord.module.d.ts +20 -0
- package/nestjs/src/lib/discord/discord.util.d.ts +28 -0
- package/nestjs/src/lib/discord/index.d.ts +4 -0
- package/nestjs/src/lib/index.d.ts +3 -0
- package/nestjs/src/lib/oauth/index.d.ts +3 -0
- package/nestjs/src/lib/oauth/oauth.api.d.ts +74 -0
- package/nestjs/src/lib/oauth/oauth.config.d.ts +55 -0
- package/nestjs/src/lib/oauth/oauth.module.d.ts +22 -0
- package/nestjs/src/lib/webhook/index.d.ts +6 -0
- package/nestjs/src/lib/webhook/webhook.discord.config.d.ts +22 -0
- package/nestjs/src/lib/webhook/webhook.discord.controller.d.ts +8 -0
- package/nestjs/src/lib/webhook/webhook.discord.d.ts +59 -0
- package/nestjs/src/lib/webhook/webhook.discord.module.d.ts +17 -0
- package/nestjs/src/lib/webhook/webhook.discord.service.d.ts +18 -0
- package/nestjs/src/lib/webhook/webhook.discord.verify.d.ts +46 -0
- package/package.json +33 -0
- package/src/index.d.ts +1 -0
- package/src/lib/discord.api.page.d.ts +108 -0
- package/src/lib/discord.config.d.ts +19 -0
- package/src/lib/discord.type.d.ts +41 -0
- package/src/lib/index.d.ts +4 -0
- package/src/lib/oauth/index.d.ts +6 -0
- package/src/lib/oauth/oauth.api.d.ts +130 -0
- package/src/lib/oauth/oauth.authorize.d.ts +93 -0
- package/src/lib/oauth/oauth.config.d.ts +56 -0
- package/src/lib/oauth/oauth.d.ts +42 -0
- package/src/lib/oauth/oauth.error.api.d.ts +65 -0
- package/src/lib/oauth/oauth.factory.d.ts +36 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dereekb/discord/nestjs",
|
|
3
|
+
"version": "13.33.0",
|
|
4
|
+
"peerDependencies": {
|
|
5
|
+
"@dereekb/discord": "13.33.0",
|
|
6
|
+
"@dereekb/nestjs": "13.33.0",
|
|
7
|
+
"@dereekb/rxjs": "13.33.0",
|
|
8
|
+
"@dereekb/util": "13.33.0",
|
|
9
|
+
"@nestjs/common": "^11.1.19",
|
|
10
|
+
"@nestjs/config": "^4.0.4",
|
|
11
|
+
"discord.js": "^14.26.3",
|
|
12
|
+
"express": "^5.2.1"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
"./package.json": "./package.json",
|
|
16
|
+
".": {
|
|
17
|
+
"module": "./index.esm.js",
|
|
18
|
+
"types": "./index.d.ts",
|
|
19
|
+
"import": "./index.cjs.mjs",
|
|
20
|
+
"default": "./index.cjs.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"module": "./index.esm.js",
|
|
24
|
+
"main": "./index.cjs.js",
|
|
25
|
+
"types": "./index.d.ts"
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Client, type Message } from 'discord.js';
|
|
2
|
+
import { type OnModuleDestroy, type OnModuleInit } from '@nestjs/common';
|
|
3
|
+
import { DiscordServiceConfig } from './discord.config';
|
|
4
|
+
import { type DiscordChannelId } from '@dereekb/discord';
|
|
5
|
+
/**
|
|
6
|
+
* Injectable service that wraps the discord.js Client for bot operations.
|
|
7
|
+
*
|
|
8
|
+
* Automatically logs in on module init and destroys the client on module destroy
|
|
9
|
+
* when autoLogin is enabled (default).
|
|
10
|
+
*/
|
|
11
|
+
export declare class DiscordApi implements OnModuleInit, OnModuleDestroy {
|
|
12
|
+
readonly config: DiscordServiceConfig;
|
|
13
|
+
private readonly logger;
|
|
14
|
+
/**
|
|
15
|
+
* The underlying discord.js Client instance.
|
|
16
|
+
*/
|
|
17
|
+
readonly client: Client;
|
|
18
|
+
constructor(config: DiscordServiceConfig);
|
|
19
|
+
onModuleInit(): Promise<void>;
|
|
20
|
+
onModuleDestroy(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Sends a text message to a Discord channel.
|
|
23
|
+
*
|
|
24
|
+
* @param channelId - target channel's snowflake ID
|
|
25
|
+
* @param content - message text to send
|
|
26
|
+
*
|
|
27
|
+
* @throws {Error} When the channel is not found or is not a text channel.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const message = await discordApi.sendMessage('123456789', 'Hello from the bot!');
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Sends a text message to the specified Discord channel.
|
|
36
|
+
*
|
|
37
|
+
* @param channelId - Target channel's snowflake ID.
|
|
38
|
+
* @param content - Message text to send.
|
|
39
|
+
* @returns The sent Discord Message.
|
|
40
|
+
* @throws {Error} When the channel is not found or is not a text channel.
|
|
41
|
+
*/
|
|
42
|
+
sendMessage(channelId: DiscordChannelId, content: string): Promise<Message>;
|
|
43
|
+
/**
|
|
44
|
+
* Registers a handler for the MessageCreate event (incoming messages).
|
|
45
|
+
*
|
|
46
|
+
* Returns an unsubscribe function to remove the handler.
|
|
47
|
+
*
|
|
48
|
+
* @param handler - callback invoked for each incoming message
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* const unsubscribe = discordApi.onMessage((message) => {
|
|
53
|
+
* if (!message.author.bot) {
|
|
54
|
+
* console.log(`${message.author.tag}: ${message.content}`);
|
|
55
|
+
* }
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* // Later, to stop listening:
|
|
59
|
+
* unsubscribe();
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
/**
|
|
63
|
+
* Registers a handler for incoming Discord messages (MessageCreate event).
|
|
64
|
+
*
|
|
65
|
+
* @param handler - Callback invoked for each incoming Message.
|
|
66
|
+
* @returns An unsubscribe function that removes the registered handler.
|
|
67
|
+
*/
|
|
68
|
+
onMessage(handler: (message: Message) => void): () => void;
|
|
69
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DiscordApi } from './discord.api';
|
|
2
|
+
/**
|
|
3
|
+
* Shared, process-singleton Discord test client.
|
|
4
|
+
*
|
|
5
|
+
* Integration spec files import {@link getSharedDiscordTestClient} so that at most one
|
|
6
|
+
* `client.login()` call occurs per vitest worker process, regardless of how many spec
|
|
7
|
+
* files participate. This keeps the bot's daily gateway-session quota intact during
|
|
8
|
+
* normal development.
|
|
9
|
+
*/
|
|
10
|
+
export interface SharedDiscordTestClient {
|
|
11
|
+
readonly discordApi: DiscordApi;
|
|
12
|
+
readonly testChannelId: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Returns the shared logged-in test client, creating it on first call and reusing it
|
|
16
|
+
* on subsequent calls within the same process.
|
|
17
|
+
*
|
|
18
|
+
* @returns The cached {@link SharedDiscordTestClient}.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getSharedDiscordTestClient(): Promise<SharedDiscordTestClient>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type Maybe } from '@dereekb/util';
|
|
2
|
+
import { type ClientOptions, GatewayIntentBits } from 'discord.js';
|
|
3
|
+
import { type DiscordBotToken } from '@dereekb/discord';
|
|
4
|
+
/**
|
|
5
|
+
* Default environment variable for the Discord bot token.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DISCORD_BOT_TOKEN_ENV_VAR = "DISCORD_BOT_TOKEN";
|
|
8
|
+
/**
|
|
9
|
+
* Placeholder bot token value used in development and CI environments where a real Discord
|
|
10
|
+
* login should not be attempted. Mirrors the value used in the workspace's .env file.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DISCORD_BOT_TOKEN_PLACEHOLDER = "placeholder";
|
|
13
|
+
/**
|
|
14
|
+
* Returns true if the input is a real, usable Discord bot token.
|
|
15
|
+
*
|
|
16
|
+
* A token is usable when it is non-empty and is not the shared placeholder value, allowing
|
|
17
|
+
* non-production environments to skip a real gateway login that would always fail.
|
|
18
|
+
*
|
|
19
|
+
* @param botToken - The bot token read from configuration, if any.
|
|
20
|
+
* @returns True when the token should be used to log in.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* isUsableDiscordBotToken('placeholder'); // false
|
|
25
|
+
* isUsableDiscordBotToken('real-token'); // true
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function isUsableDiscordBotToken(botToken: Maybe<DiscordBotToken>): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Default gateway intents for a bot that reads guild messages.
|
|
31
|
+
*
|
|
32
|
+
* Includes Guilds, GuildMessages, and MessageContent.
|
|
33
|
+
* Note: MessageContent is a privileged intent and must be enabled in the Discord Developer Portal.
|
|
34
|
+
*/
|
|
35
|
+
export declare const DEFAULT_DISCORD_INTENTS: GatewayIntentBits[];
|
|
36
|
+
export interface DiscordServiceApiConfig {
|
|
37
|
+
/**
|
|
38
|
+
* The bot token used to authenticate with the Discord gateway.
|
|
39
|
+
*/
|
|
40
|
+
readonly botToken: DiscordBotToken;
|
|
41
|
+
/**
|
|
42
|
+
* discord.js Client options. Intents default to DEFAULT_DISCORD_INTENTS if not provided.
|
|
43
|
+
*/
|
|
44
|
+
readonly clientOptions?: Partial<ClientOptions>;
|
|
45
|
+
/**
|
|
46
|
+
* Whether to automatically call client.login() during module initialization.
|
|
47
|
+
*
|
|
48
|
+
* Defaults to true.
|
|
49
|
+
*/
|
|
50
|
+
readonly autoLogin?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Configuration for the DiscordApi service.
|
|
54
|
+
*/
|
|
55
|
+
export declare abstract class DiscordServiceConfig {
|
|
56
|
+
readonly discord: DiscordServiceApiConfig;
|
|
57
|
+
static assertValidConfig(config: DiscordServiceConfig): void;
|
|
58
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ConfigService } from '@nestjs/config';
|
|
2
|
+
import { DiscordServiceConfig } from './discord.config';
|
|
3
|
+
/**
|
|
4
|
+
* Factory that creates a DiscordServiceConfig from environment variables.
|
|
5
|
+
*
|
|
6
|
+
* autoLogin is enabled only when a real bot token is configured and the process is not running
|
|
7
|
+
* under a test environment, so development and CI runs (which use a placeholder token) never
|
|
8
|
+
* attempt a real gateway login that would fail.
|
|
9
|
+
*
|
|
10
|
+
* @param configService - The NestJS config service used to read Discord environment variables.
|
|
11
|
+
* @returns A validated DiscordServiceConfig populated from environment variables.
|
|
12
|
+
*/
|
|
13
|
+
export declare function discordServiceConfigFactory(configService: ConfigService): DiscordServiceConfig;
|
|
14
|
+
/**
|
|
15
|
+
* NestJS module that provides the DiscordApi service.
|
|
16
|
+
*
|
|
17
|
+
* Reads the bot token from the DISCORD_BOT_TOKEN environment variable.
|
|
18
|
+
*/
|
|
19
|
+
export declare class DiscordModule {
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type GatewayIntentBits, type ClientOptions } from 'discord.js';
|
|
2
|
+
/**
|
|
3
|
+
* Returns default ClientOptions for a bot that reads guild messages.
|
|
4
|
+
*
|
|
5
|
+
* Includes Guilds, GuildMessages, and MessageContent intents.
|
|
6
|
+
*
|
|
7
|
+
* @returns Partial ClientOptions with the default bot intents set.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const options = discordDefaultClientOptions();
|
|
12
|
+
* // options.intents === [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent]
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function discordDefaultClientOptions(): Partial<ClientOptions>;
|
|
16
|
+
/**
|
|
17
|
+
* Returns ClientOptions with additional intents merged with the defaults.
|
|
18
|
+
*
|
|
19
|
+
* @param additionalIntents - Extra intents to include beyond the defaults.
|
|
20
|
+
* @returns Partial ClientOptions with the merged intent list.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const options = discordClientOptionsWithIntents([GatewayIntentBits.DirectMessages]);
|
|
25
|
+
* // options.intents includes Guilds, GuildMessages, MessageContent, and DirectMessages
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function discordClientOptionsWithIntents(additionalIntents: GatewayIntentBits[]): Partial<ClientOptions>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type DiscordAccessToken, type DiscordOAuth, type DiscordOAuthAuthorizeUrlFactory, type DiscordOAuthAuthorizeUrlFactoryConfig, type DiscordOAuthClientId, type DiscordOAuthContext, type DiscordOAuthExchangeAuthorizationCodeInput, type DiscordOAuthRefreshTokenInput } from '@dereekb/discord';
|
|
2
|
+
import { DiscordOAuthServiceConfig } from './oauth.config';
|
|
3
|
+
/**
|
|
4
|
+
* Provides the app's configured Discord OAuth client.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately thinner than `CalcomOAuthApi`, which owns an access-token cache service and memoizes a
|
|
7
|
+
* token factory per user. Discord needs neither: the external-connection framework persists each
|
|
8
|
+
* user's credentials itself, so this api holds one client authenticated with the *application's*
|
|
9
|
+
* credentials and hands out the three calls that run through it.
|
|
10
|
+
*
|
|
11
|
+
* Note this is not the bot. The app acting as itself against Discord goes through `DiscordApi` and
|
|
12
|
+
* discord.js with a bot token; that token never reaches this client.
|
|
13
|
+
*/
|
|
14
|
+
export declare class DiscordOAuthApi {
|
|
15
|
+
readonly config: DiscordOAuthServiceConfig;
|
|
16
|
+
readonly discordOAuth: DiscordOAuth;
|
|
17
|
+
get oauthContext(): DiscordOAuthContext;
|
|
18
|
+
/**
|
|
19
|
+
* The OAuth client id the api authorizes as. Read from the context so there is a single source.
|
|
20
|
+
*
|
|
21
|
+
* @returns The configured Discord OAuth client id.
|
|
22
|
+
*/
|
|
23
|
+
get clientId(): DiscordOAuthClientId;
|
|
24
|
+
constructor(config: DiscordOAuthServiceConfig);
|
|
25
|
+
/**
|
|
26
|
+
* Configured pass-through for {@link exchangeAuthorizationCode}.
|
|
27
|
+
*
|
|
28
|
+
* @returns Function to exchange an OAuth authorization code for tokens.
|
|
29
|
+
*/
|
|
30
|
+
get exchangeAuthorizationCode(): (input: DiscordOAuthExchangeAuthorizationCodeInput) => Promise<import("@dereekb/discord").DiscordOAuthTokenResponse>;
|
|
31
|
+
/**
|
|
32
|
+
* Configured pass-through for {@link refreshAccessToken}.
|
|
33
|
+
*
|
|
34
|
+
* @returns Function to refresh an access token using a stored refresh token.
|
|
35
|
+
*/
|
|
36
|
+
get refreshAccessToken(): (input: DiscordOAuthRefreshTokenInput) => Promise<import("@dereekb/discord").DiscordOAuthTokenResponse>;
|
|
37
|
+
/**
|
|
38
|
+
* Configured pass-through for {@link readCurrentUser}.
|
|
39
|
+
*
|
|
40
|
+
* Bearer-authenticated with the user's own access token, unlike the two token-endpoint calls above,
|
|
41
|
+
* which authenticate with the application's Basic credentials.
|
|
42
|
+
*
|
|
43
|
+
* @returns Function to read the Discord user an access token belongs to.
|
|
44
|
+
*/
|
|
45
|
+
get readCurrentUser(): (input: import("@dereekb/discord").DiscordOAuthReadCurrentUserInput) => Promise<import("@dereekb/discord").DiscordOAuthCurrentUser>;
|
|
46
|
+
/**
|
|
47
|
+
* Builds a {@link DiscordOAuthAuthorizeUrlFactory} for the app's client id.
|
|
48
|
+
*
|
|
49
|
+
* The redirect URI and scopes stay the caller's, since they are properties of the flow being
|
|
50
|
+
* mounted rather than of the client, but the client id comes from this api so a caller never has to
|
|
51
|
+
* read it out of the config itself.
|
|
52
|
+
*
|
|
53
|
+
* @param config - The redirect URI, requested scopes, and optional authorize URL override.
|
|
54
|
+
* @returns A factory composing the authorize URL to redirect a user's browser to.
|
|
55
|
+
*/
|
|
56
|
+
authorizeUrlFactory(config: Omit<DiscordOAuthAuthorizeUrlFactoryConfig, 'clientId'>): DiscordOAuthAuthorizeUrlFactory;
|
|
57
|
+
/**
|
|
58
|
+
* Exchanges an OAuth authorization code and maps the response to a {@link DiscordAccessToken}.
|
|
59
|
+
*
|
|
60
|
+
* @param input - The authorization code and the exact redirect URI it was issued for.
|
|
61
|
+
* @returns The exchanged access token.
|
|
62
|
+
*/
|
|
63
|
+
exchangeAuthorizationCodeToAccessToken(input: DiscordOAuthExchangeAuthorizationCodeInput): Promise<DiscordAccessToken>;
|
|
64
|
+
/**
|
|
65
|
+
* Refreshes an access token and maps the response to a {@link DiscordAccessToken}.
|
|
66
|
+
*
|
|
67
|
+
* The returned `refreshToken` is the one to persist: Discord's refresh responses carry a refresh
|
|
68
|
+
* token of their own, so storing whatever came back is correct whether or not it rotated.
|
|
69
|
+
*
|
|
70
|
+
* @param input - The stored refresh token.
|
|
71
|
+
* @returns The refreshed access token.
|
|
72
|
+
*/
|
|
73
|
+
refreshToAccessToken(input: DiscordOAuthRefreshTokenInput): Promise<DiscordAccessToken>;
|
|
74
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type DiscordOAuthClientId, type DiscordOAuthClientSecret, type DiscordOAuthConfig, type DiscordOAuthFactoryConfig } from '@dereekb/discord';
|
|
2
|
+
import { type ConfigService } from '@nestjs/config';
|
|
3
|
+
export declare const DISCORD_SERVICE_NAME = "discord";
|
|
4
|
+
export declare const DISCORD_CLIENT_ID_CONFIG_KEY = "DISCORD_CLIENT_ID";
|
|
5
|
+
export declare const DISCORD_CLIENT_SECRET_CONFIG_KEY = "DISCORD_CLIENT_SECRET";
|
|
6
|
+
/**
|
|
7
|
+
* The environment-facing shape of the Discord OAuth client credentials.
|
|
8
|
+
*
|
|
9
|
+
* Both values are optional here, mirroring the `DISCORD_*` variables they are read from, so a missing
|
|
10
|
+
* variable is reported by {@link DiscordOAuthServiceConfig.assertValidConfig} rather than surfacing as
|
|
11
|
+
* `undefined` further down.
|
|
12
|
+
*/
|
|
13
|
+
export interface DiscordOAuthServiceApiConfig {
|
|
14
|
+
readonly clientId?: DiscordOAuthClientId;
|
|
15
|
+
readonly clientSecret?: DiscordOAuthClientSecret;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Configuration for the {@link DiscordOAuthApi}.
|
|
19
|
+
*
|
|
20
|
+
* Carries no server-level token or access-token cache, unlike `CalcomOAuthServiceConfig`: Discord has
|
|
21
|
+
* no api-key alternative to the client-credentials pair, and the app acting as *itself* against
|
|
22
|
+
* Discord authenticates with a bot token through `DiscordServiceConfig` and discord.js instead. This
|
|
23
|
+
* config only ever describes the OAuth client that performs the per-user authorization-code handoff.
|
|
24
|
+
*/
|
|
25
|
+
export declare abstract class DiscordOAuthServiceConfig {
|
|
26
|
+
readonly discordOAuth: DiscordOAuthServiceApiConfig;
|
|
27
|
+
/**
|
|
28
|
+
* Optional configuration for the Discord OAuth client the api builds — a custom fetch handler or
|
|
29
|
+
* error logger.
|
|
30
|
+
*/
|
|
31
|
+
readonly factoryConfig?: DiscordOAuthFactoryConfig;
|
|
32
|
+
static assertValidConfig(config: DiscordOAuthServiceConfig): void;
|
|
33
|
+
/**
|
|
34
|
+
* Narrows the environment-facing config to the fully-populated {@link DiscordOAuthConfig} the core
|
|
35
|
+
* factory requires, asserting both credentials are present.
|
|
36
|
+
*
|
|
37
|
+
* @param config - The service configuration to read.
|
|
38
|
+
* @returns The validated OAuth client configuration.
|
|
39
|
+
* @throws {Error} When either client credential is missing.
|
|
40
|
+
*/
|
|
41
|
+
static assertedDiscordOAuthConfig(config: DiscordOAuthServiceConfig): DiscordOAuthConfig;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Factory function that creates a {@link DiscordOAuthServiceConfig} from NestJS ConfigService
|
|
45
|
+
* environment variables.
|
|
46
|
+
*
|
|
47
|
+
* Fails at startup rather than at the consent screen: a missing client id otherwise composes an
|
|
48
|
+
* authorize URL carrying `client_id=undefined`, and a missing secret fails the exchange only after the
|
|
49
|
+
* user has already consented.
|
|
50
|
+
*
|
|
51
|
+
* @param configService - The NestJS ConfigService instance.
|
|
52
|
+
* @returns A validated DiscordOAuthServiceConfig.
|
|
53
|
+
* @throws {Error} When either client credential is missing.
|
|
54
|
+
*/
|
|
55
|
+
export declare function discordOAuthServiceConfigFactory(configService: ConfigService): DiscordOAuthServiceConfig;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ModuleMetadata } from '@nestjs/common';
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
3
|
+
import { DiscordOAuthServiceConfig } from './oauth.config';
|
|
4
|
+
export type DiscordOAuthServiceConfigFactory = (configService: ConfigService) => DiscordOAuthServiceConfig;
|
|
5
|
+
export interface ProvideAppDiscordOAuthMetadataConfig extends Pick<ModuleMetadata, 'imports' | 'exports' | 'providers'> {
|
|
6
|
+
/**
|
|
7
|
+
* Optional override for the DiscordOAuthServiceConfigFactory.
|
|
8
|
+
*
|
|
9
|
+
* @default discordOAuthServiceConfigFactory
|
|
10
|
+
*/
|
|
11
|
+
readonly discordOAuthServiceConfigFactory?: DiscordOAuthServiceConfigFactory;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Convenience function used to generate ModuleMetadata for an app's DiscordOAuthModule.
|
|
15
|
+
*
|
|
16
|
+
* Takes no `dependencyModule`, unlike the Cal.com equivalent: {@link DiscordOAuthApi} depends only on
|
|
17
|
+
* its own config, since Discord's connect flow needs no access-token cache service.
|
|
18
|
+
*
|
|
19
|
+
* @param config - The module metadata configuration including an optional config factory.
|
|
20
|
+
* @returns NestJS ModuleMetadata for registering the DiscordOAuthModule.
|
|
21
|
+
*/
|
|
22
|
+
export declare function appDiscordOAuthModuleMetadata(config: ProvideAppDiscordOAuthMetadataConfig): ModuleMetadata;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type DiscordPublicKey } from '@dereekb/discord';
|
|
2
|
+
/**
|
|
3
|
+
* Default environment variable for the Discord application public key.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DISCORD_PUBLIC_KEY_ENV_VAR = "DISCORD_PUBLIC_KEY";
|
|
6
|
+
/**
|
|
7
|
+
* The byte length of a Discord Ed25519 public key (32 bytes = 64 hex characters).
|
|
8
|
+
*/
|
|
9
|
+
export declare const DISCORD_ED25519_PUBLIC_KEY_BYTE_LENGTH = 32;
|
|
10
|
+
export interface DiscordWebhookConfig {
|
|
11
|
+
/**
|
|
12
|
+
* The Ed25519 public key used to verify incoming interaction webhook signatures.
|
|
13
|
+
*/
|
|
14
|
+
readonly publicKey: DiscordPublicKey;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Configuration for the DiscordWebhookService.
|
|
18
|
+
*/
|
|
19
|
+
export declare abstract class DiscordWebhookServiceConfig {
|
|
20
|
+
readonly discordWebhook: DiscordWebhookConfig;
|
|
21
|
+
static assertValidConfig(config: DiscordWebhookServiceConfig): void;
|
|
22
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type RawBodyBuffer } from '@dereekb/nestjs';
|
|
2
|
+
import { type Request } from 'express';
|
|
3
|
+
import { DiscordWebhookService } from './webhook.discord.service';
|
|
4
|
+
export declare class DiscordWebhookController {
|
|
5
|
+
private readonly _discordWebhookService;
|
|
6
|
+
constructor(discordWebhookService: DiscordWebhookService);
|
|
7
|
+
handleDiscordWebhook(req: Request, rawBody: RawBodyBuffer): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type HandlerBindAccessor, type HandlerMappedSetFunction, type Handler } from '@dereekb/util';
|
|
2
|
+
import { InteractionType, type Interaction } from 'discord.js';
|
|
3
|
+
/**
|
|
4
|
+
* Discord interaction type numeric key, used for handler dispatch.
|
|
5
|
+
*/
|
|
6
|
+
export type DiscordInteractionType = InteractionType;
|
|
7
|
+
/**
|
|
8
|
+
* An untyped Discord interaction received via webhook.
|
|
9
|
+
*/
|
|
10
|
+
export type UntypedDiscordInteraction = Interaction;
|
|
11
|
+
/**
|
|
12
|
+
* A typed Discord interaction, narrowed from the base Interaction type.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const interaction: DiscordWebhookInteraction<ChatInputCommandInteraction> = discordWebhookInteraction(rawInteraction);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export type DiscordWebhookInteraction<T extends Interaction = Interaction> = T;
|
|
20
|
+
/**
|
|
21
|
+
* Casts an untyped Discord interaction to a typed one.
|
|
22
|
+
*
|
|
23
|
+
* @param interaction - The raw interaction to cast.
|
|
24
|
+
* @returns The interaction cast to the specified typed DiscordWebhookInteraction.
|
|
25
|
+
*/
|
|
26
|
+
export declare function discordWebhookInteraction<T extends Interaction = Interaction>(interaction: UntypedDiscordInteraction): DiscordWebhookInteraction<T>;
|
|
27
|
+
export type DiscordInteractionHandler = Handler<UntypedDiscordInteraction, DiscordInteractionType>;
|
|
28
|
+
export declare const discordInteractionHandlerFactory: import("@dereekb/util").HandlerFactory<UntypedDiscordInteraction, InteractionType, boolean>;
|
|
29
|
+
export type DiscordHandlerMappedSetFunction<T extends Interaction = Interaction> = HandlerMappedSetFunction<DiscordWebhookInteraction<T>>;
|
|
30
|
+
/**
|
|
31
|
+
* Configurer for Discord interaction handlers.
|
|
32
|
+
*
|
|
33
|
+
* Handlers are keyed on InteractionType. Use discord.js type guards
|
|
34
|
+
* (e.g., interaction.isChatInputCommand(), interaction.isButton()) within
|
|
35
|
+
* your handler callback for sub-type refinement.
|
|
36
|
+
*/
|
|
37
|
+
export interface DiscordInteractionHandlerConfigurer extends HandlerBindAccessor<UntypedDiscordInteraction, DiscordInteractionType> {
|
|
38
|
+
/**
|
|
39
|
+
* Handles application commands (slash commands + context menu commands).
|
|
40
|
+
*
|
|
41
|
+
* Use interaction.isChatInputCommand() or interaction.isContextMenuCommand() to narrow the type within your handler.
|
|
42
|
+
*/
|
|
43
|
+
readonly handleApplicationCommand: DiscordHandlerMappedSetFunction;
|
|
44
|
+
/**
|
|
45
|
+
* Handles message component interactions (buttons + select menus).
|
|
46
|
+
*
|
|
47
|
+
* Use interaction.isButton() or interaction.isStringSelectMenu() to narrow the type within your handler.
|
|
48
|
+
*/
|
|
49
|
+
readonly handleMessageComponent: DiscordHandlerMappedSetFunction;
|
|
50
|
+
/**
|
|
51
|
+
* Handles modal submit interactions.
|
|
52
|
+
*/
|
|
53
|
+
readonly handleModalSubmit: DiscordHandlerMappedSetFunction;
|
|
54
|
+
/**
|
|
55
|
+
* Handles autocomplete interactions for slash command options.
|
|
56
|
+
*/
|
|
57
|
+
readonly handleAutocomplete: DiscordHandlerMappedSetFunction;
|
|
58
|
+
}
|
|
59
|
+
export declare const discordInteractionHandlerConfigurerFactory: import("@dereekb/util").HandlerConfigurerFactory<DiscordInteractionHandlerConfigurer, UntypedDiscordInteraction, InteractionType, boolean>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ConfigService } from '@nestjs/config';
|
|
2
|
+
import { DiscordWebhookServiceConfig } from './webhook.discord.config';
|
|
3
|
+
/**
|
|
4
|
+
* Factory that creates a DiscordWebhookServiceConfig from environment variables.
|
|
5
|
+
*
|
|
6
|
+
* @param configService - The NestJS config service used to read the Discord public key environment variable.
|
|
7
|
+
* @returns A validated DiscordWebhookServiceConfig populated from environment variables.
|
|
8
|
+
*/
|
|
9
|
+
export declare function discordWebhookServiceConfigFactory(configService: ConfigService): DiscordWebhookServiceConfig;
|
|
10
|
+
/**
|
|
11
|
+
* NestJS module that provides Discord interaction webhook handling.
|
|
12
|
+
*
|
|
13
|
+
* Standalone — does not depend on DiscordModule (no bot token needed).
|
|
14
|
+
* Reads the application public key from the DISCORD_PUBLIC_KEY environment variable.
|
|
15
|
+
*/
|
|
16
|
+
export declare class DiscordWebhookModule {
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Request } from 'express';
|
|
2
|
+
import { type DiscordInteractionType, type UntypedDiscordInteraction } from './webhook.discord';
|
|
3
|
+
import { type Handler } from '@dereekb/util';
|
|
4
|
+
import { DiscordWebhookServiceConfig } from './webhook.discord.config';
|
|
5
|
+
/**
|
|
6
|
+
* Service that handles Discord interaction webhook events.
|
|
7
|
+
*
|
|
8
|
+
* Verifies incoming webhook signatures and dispatches interactions to registered handlers.
|
|
9
|
+
*/
|
|
10
|
+
export declare class DiscordWebhookService {
|
|
11
|
+
private readonly logger;
|
|
12
|
+
private readonly _verifier;
|
|
13
|
+
readonly handler: Handler<UntypedDiscordInteraction, DiscordInteractionType>;
|
|
14
|
+
readonly configure: import("@dereekb/util").HandlerConfigurer<import("./webhook.discord").DiscordInteractionHandlerConfigurer, UntypedDiscordInteraction, import("discord.js").InteractionType, boolean>;
|
|
15
|
+
constructor(discordWebhookServiceConfig: DiscordWebhookServiceConfig);
|
|
16
|
+
updateForWebhook(req: Request, rawBody: Buffer): Promise<void>;
|
|
17
|
+
updateForDiscordInteraction(interaction: UntypedDiscordInteraction): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type Request } from 'express';
|
|
2
|
+
import { type DiscordPublicKey } from '@dereekb/discord';
|
|
3
|
+
export interface DiscordWebhookEventVerificationConfig {
|
|
4
|
+
/**
|
|
5
|
+
* The Ed25519 public key from the Discord Developer Portal.
|
|
6
|
+
*/
|
|
7
|
+
readonly publicKey: DiscordPublicKey;
|
|
8
|
+
}
|
|
9
|
+
export type DiscordWebhookEventVerificationResult = DiscordWebhookEventVerificationSuccessResult | DiscordWebhookEventVerificationErrorResult;
|
|
10
|
+
export interface DiscordWebhookEventVerificationSuccessResult {
|
|
11
|
+
readonly valid: true;
|
|
12
|
+
/**
|
|
13
|
+
* The parsed JSON body of the verified interaction.
|
|
14
|
+
*/
|
|
15
|
+
readonly body: unknown;
|
|
16
|
+
}
|
|
17
|
+
export interface DiscordWebhookEventVerificationErrorResult {
|
|
18
|
+
readonly valid: false;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Function that verifies a Discord interaction webhook request using Ed25519 signatures.
|
|
22
|
+
*/
|
|
23
|
+
export type DiscordWebhookEventVerifier = (req: Request, rawBody: Buffer) => Promise<DiscordWebhookEventVerificationResult>;
|
|
24
|
+
/**
|
|
25
|
+
* Creates a verifier for Discord interaction webhook requests.
|
|
26
|
+
*
|
|
27
|
+
* Discord signs interaction webhook requests with Ed25519. The signed message is
|
|
28
|
+
* the concatenation of the x-signature-timestamp header and the raw request body.
|
|
29
|
+
* The signature is provided in the x-signature-ed25519 header as a hex string.
|
|
30
|
+
*
|
|
31
|
+
* Uses Node.js built-in crypto with JWK key import — no external dependencies required.
|
|
32
|
+
*
|
|
33
|
+
* @param config - Verification config containing the application's public key.
|
|
34
|
+
* @returns A DiscordWebhookEventVerifier function that validates Ed25519-signed requests.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const verifier = discordWebhookEventVerifier({ publicKey: 'your-hex-public-key' });
|
|
39
|
+
* const result = await verifier(req, rawBody);
|
|
40
|
+
*
|
|
41
|
+
* if (result.valid) {
|
|
42
|
+
* // result.body contains the parsed interaction
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare function discordWebhookEventVerifier(config: DiscordWebhookEventVerificationConfig): DiscordWebhookEventVerifier;
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dereekb/discord",
|
|
3
|
+
"version": "13.33.0",
|
|
4
|
+
"sideEffects": false,
|
|
5
|
+
"exports": {
|
|
6
|
+
"./nestjs": {
|
|
7
|
+
"module": "./nestjs/index.esm.js",
|
|
8
|
+
"types": "./nestjs/index.d.ts",
|
|
9
|
+
"import": "./nestjs/index.cjs.mjs",
|
|
10
|
+
"default": "./nestjs/index.cjs.js"
|
|
11
|
+
},
|
|
12
|
+
"./package.json": "./package.json",
|
|
13
|
+
".": {
|
|
14
|
+
"module": "./index.esm.js",
|
|
15
|
+
"types": "./index.d.ts",
|
|
16
|
+
"import": "./index.cjs.mjs",
|
|
17
|
+
"default": "./index.cjs.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@dereekb/nestjs": "13.33.0",
|
|
22
|
+
"@dereekb/rxjs": "13.33.0",
|
|
23
|
+
"@dereekb/util": "13.33.0",
|
|
24
|
+
"@nestjs/common": "^11.1.19",
|
|
25
|
+
"@nestjs/config": "^4.0.4",
|
|
26
|
+
"discord.js": "^14.26.3",
|
|
27
|
+
"express": "^5.2.1",
|
|
28
|
+
"make-error": "^1.3.6"
|
|
29
|
+
},
|
|
30
|
+
"module": "./index.esm.js",
|
|
31
|
+
"main": "./index.cjs.js",
|
|
32
|
+
"types": "./index.d.ts"
|
|
33
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib';
|