@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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/index.cjs.default.js +1 -0
  4. package/index.cjs.js +817 -0
  5. package/index.cjs.mjs +2 -0
  6. package/index.d.ts +1 -0
  7. package/index.esm.js +789 -0
  8. package/nestjs/index.cjs.default.js +1 -0
  9. package/nestjs/index.cjs.js +1928 -0
  10. package/nestjs/index.cjs.mjs +2 -0
  11. package/nestjs/index.d.ts +1 -0
  12. package/nestjs/index.esm.js +1905 -0
  13. package/nestjs/package.json +26 -0
  14. package/nestjs/src/index.d.ts +1 -0
  15. package/nestjs/src/lib/discord/discord.api.d.ts +69 -0
  16. package/nestjs/src/lib/discord/discord.api.spec.client.d.ts +20 -0
  17. package/nestjs/src/lib/discord/discord.config.d.ts +58 -0
  18. package/nestjs/src/lib/discord/discord.module.d.ts +20 -0
  19. package/nestjs/src/lib/discord/discord.util.d.ts +28 -0
  20. package/nestjs/src/lib/discord/index.d.ts +4 -0
  21. package/nestjs/src/lib/index.d.ts +3 -0
  22. package/nestjs/src/lib/oauth/index.d.ts +3 -0
  23. package/nestjs/src/lib/oauth/oauth.api.d.ts +74 -0
  24. package/nestjs/src/lib/oauth/oauth.config.d.ts +55 -0
  25. package/nestjs/src/lib/oauth/oauth.module.d.ts +22 -0
  26. package/nestjs/src/lib/webhook/index.d.ts +6 -0
  27. package/nestjs/src/lib/webhook/webhook.discord.config.d.ts +22 -0
  28. package/nestjs/src/lib/webhook/webhook.discord.controller.d.ts +8 -0
  29. package/nestjs/src/lib/webhook/webhook.discord.d.ts +59 -0
  30. package/nestjs/src/lib/webhook/webhook.discord.module.d.ts +17 -0
  31. package/nestjs/src/lib/webhook/webhook.discord.service.d.ts +18 -0
  32. package/nestjs/src/lib/webhook/webhook.discord.verify.d.ts +46 -0
  33. package/package.json +33 -0
  34. package/src/index.d.ts +1 -0
  35. package/src/lib/discord.api.page.d.ts +108 -0
  36. package/src/lib/discord.config.d.ts +19 -0
  37. package/src/lib/discord.type.d.ts +41 -0
  38. package/src/lib/index.d.ts +4 -0
  39. package/src/lib/oauth/index.d.ts +6 -0
  40. package/src/lib/oauth/oauth.api.d.ts +130 -0
  41. package/src/lib/oauth/oauth.authorize.d.ts +93 -0
  42. package/src/lib/oauth/oauth.config.d.ts +56 -0
  43. package/src/lib/oauth/oauth.d.ts +42 -0
  44. package/src/lib/oauth/oauth.error.api.d.ts +65 -0
  45. package/src/lib/oauth/oauth.factory.d.ts +36 -0
@@ -0,0 +1,108 @@
1
+ import { type Maybe } from '@dereekb/util';
2
+ import { type FetchPageFactory, type FetchPageFactoryConfigDefaults } from '@dereekb/util/fetch';
3
+ import { type DiscordMessageId } from './discord.type';
4
+ /**
5
+ * Default number of messages per page when fetching Discord channel messages.
6
+ */
7
+ export declare const DEFAULT_DISCORD_MESSAGES_PER_PAGE = 100;
8
+ /**
9
+ * Base pagination parameters for Discord channel message endpoints.
10
+ *
11
+ * Discord uses cursor-based pagination via snowflake IDs rather than page numbers.
12
+ * Only one of `before`, `after`, or `around` should be specified per request.
13
+ */
14
+ export interface DiscordMessagePageFilter {
15
+ /**
16
+ * Fetch messages before this message ID.
17
+ */
18
+ readonly before?: Maybe<DiscordMessageId>;
19
+ /**
20
+ * Fetch messages after this message ID.
21
+ */
22
+ readonly after?: Maybe<DiscordMessageId>;
23
+ /**
24
+ * Fetch messages around this message ID.
25
+ */
26
+ readonly around?: Maybe<DiscordMessageId>;
27
+ /**
28
+ * Maximum number of messages to return per page (1-100).
29
+ *
30
+ * Defaults to {@link DEFAULT_DISCORD_MESSAGES_PER_PAGE}.
31
+ */
32
+ readonly limit?: Maybe<number>;
33
+ }
34
+ /**
35
+ * Result of a paginated Discord message fetch containing the array of messages.
36
+ *
37
+ * @typeParam T - The message type. Structural, so it fits a raw REST response as readily as a
38
+ * discord.js `Message`; this entry point carries no discord.js dependency of its own.
39
+ */
40
+ export interface DiscordMessagePageResult<T> {
41
+ /**
42
+ * Array of messages returned.
43
+ */
44
+ readonly data: T[];
45
+ }
46
+ /**
47
+ * A fetch function that accepts {@link DiscordMessagePageFilter} input and returns a {@link DiscordMessagePageResult}.
48
+ * Used as the underlying data source for {@link discordFetchMessagePageFactory}.
49
+ */
50
+ export type DiscordFetchMessagePageFetchFunction<I extends DiscordMessagePageFilter, T> = (input: I) => Promise<DiscordMessagePageResult<T>>;
51
+ /**
52
+ * Configuration for {@link discordFetchMessagePageFactory}.
53
+ *
54
+ * @typeParam T - The message type
55
+ */
56
+ export interface DiscordFetchMessagePageFactoryConfig<T> {
57
+ /**
58
+ * Extracts the snowflake ID from a message object. Used to determine the cursor for the next page.
59
+ *
60
+ * Defaults to reading the `id` property on the message.
61
+ */
62
+ readonly readMessageId?: (message: T) => DiscordMessageId;
63
+ }
64
+ /**
65
+ * Configuration for {@link discordFetchMessagePageFactory}.
66
+ *
67
+ * @typeParam I - The input filter type
68
+ * @typeParam T - The message type
69
+ */
70
+ export interface DiscordFetchMessagePageFactoryInput<I extends DiscordMessagePageFilter, T> {
71
+ /**
72
+ * The Discord fetch function to paginate over.
73
+ */
74
+ readonly fetch: DiscordFetchMessagePageFetchFunction<I, T>;
75
+ /**
76
+ * Optional config for reading message IDs.
77
+ */
78
+ readonly config?: Maybe<DiscordFetchMessagePageFactoryConfig<T>>;
79
+ /**
80
+ * Optional default configuration for the page factory.
81
+ */
82
+ readonly defaults?: Maybe<FetchPageFactoryConfigDefaults>;
83
+ }
84
+ /**
85
+ * Creates a page factory that wraps a Discord message fetch function with automatic cursor-based pagination.
86
+ *
87
+ * Discord paginates via `before`/`after` snowflake IDs. This factory automatically reads the last
88
+ * message's ID from each response and sets it as the `before` cursor for the next request.
89
+ * When the number of returned messages is less than the requested limit, pagination stops.
90
+ *
91
+ * @param input - The factory input configuration.
92
+ * @returns A page factory that produces iterable page fetchers.
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const pageFactory = discordFetchMessagePageFactory({ fetch: fetchChannelMessages });
97
+ *
98
+ * const fetchPage = pageFactory({ limit: 50 });
99
+ * const firstPage = await fetchPage.fetchNext();
100
+ *
101
+ * if (firstPage.hasNext) {
102
+ * const secondPage = await firstPage.fetchNext();
103
+ * }
104
+ * ```
105
+ */
106
+ export declare function discordFetchMessagePageFactory<I extends DiscordMessagePageFilter, T extends {
107
+ id: string;
108
+ }>(input: DiscordFetchMessagePageFactoryInput<I, T>): FetchPageFactory<I, DiscordMessagePageResult<T>>;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * OAuth2 client id of a Discord application.
3
+ *
4
+ * Identical to the value the developer portal labels "Application ID".
5
+ */
6
+ export type DiscordOAuthClientId = string;
7
+ /**
8
+ * OAuth2 client secret of a Discord application.
9
+ */
10
+ export type DiscordOAuthClientSecret = string;
11
+ /**
12
+ * The Discord REST API base, pinned to a version.
13
+ *
14
+ * Endpoint paths are appended to this base, so it intentionally carries no endpoint segment of its
15
+ * own. Discord requires an explicit version in the path; an unversioned base resolves to the oldest
16
+ * still-supported version.
17
+ */
18
+ export declare const DISCORD_API_URL = "https://discord.com/api/v10";
19
+ export type DiscordApiUrl = typeof DISCORD_API_URL;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * A Discord snowflake id.
3
+ *
4
+ * The canonical id type for this package, declared in the fetch-only core so both entry points can
5
+ * share one declaration. `@dereekb/discord/nestjs` imports these from here directly rather than
6
+ * re-exporting them (the `dereekb-util/no-sister-re-export` rule), so the dependency only ever points
7
+ * core -> nestjs: the core stays free of discord.js and of anything that asserts a bot token.
8
+ */
9
+ export type DiscordSnowflake = string;
10
+ /**
11
+ * A Discord snowflake id, when the kind of object it identifies is not significant.
12
+ *
13
+ * Prefer the specific alias ({@link DiscordChannelId}, {@link DiscordGuildId},
14
+ * {@link DiscordMessageId}) where one applies.
15
+ */
16
+ export type DiscordId = DiscordSnowflake;
17
+ /**
18
+ * A Discord channel id.
19
+ */
20
+ export type DiscordChannelId = DiscordSnowflake;
21
+ /**
22
+ * A Discord guild (server) id.
23
+ */
24
+ export type DiscordGuildId = DiscordSnowflake;
25
+ /**
26
+ * A Discord message id.
27
+ */
28
+ export type DiscordMessageId = DiscordSnowflake;
29
+ /**
30
+ * Bot token used to authenticate the Discord bot with the gateway.
31
+ *
32
+ * Declared here alongside the other scalars for one place to look, though only
33
+ * `@dereekb/discord/nestjs` consumes it — the core never authenticates as a bot.
34
+ */
35
+ export type DiscordBotToken = string;
36
+ /**
37
+ * The Ed25519 public key of your Discord application, used to verify interaction webhooks.
38
+ *
39
+ * Found in the Discord Developer Portal under your application's General Information page.
40
+ */
41
+ export type DiscordPublicKey = string;
@@ -0,0 +1,4 @@
1
+ export * from './discord.api.page';
2
+ export * from './discord.config';
3
+ export * from './discord.type';
4
+ export * from './oauth';
@@ -0,0 +1,6 @@
1
+ export * from './oauth';
2
+ export * from './oauth.api';
3
+ export * from './oauth.authorize';
4
+ export * from './oauth.config';
5
+ export * from './oauth.error.api';
6
+ export * from './oauth.factory';
@@ -0,0 +1,130 @@
1
+ import { type Maybe, type Seconds } from '@dereekb/util';
2
+ import { type DiscordSnowflake } from '../discord.type';
3
+ import { type DiscordOAuthConfig, type DiscordOAuthContext } from './oauth.config';
4
+ /**
5
+ * The `Content-Type` Discord's token endpoint requires.
6
+ *
7
+ * Discord rejects a JSON body outright, unlike Cal.com, which requires one.
8
+ *
9
+ * `@dereekb/util/oidc`'s `postTokenEndpoint` is the in-workspace precedent for this form-encoded
10
+ * shape and is deliberately NOT reused: its `exchangeAuthorizationCode` requires a PKCE
11
+ * `code_verifier`, it authenticates with `client_secret_post` rather than Basic, and it is
12
+ * discovery-driven. Discord is not an OIDC provider — there is no discovery document and no
13
+ * `id_token`.
14
+ */
15
+ export declare const DISCORD_OAUTH_TOKEN_CONTENT_TYPE = "application/x-www-form-urlencoded";
16
+ export interface DiscordOAuthExchangeAuthorizationCodeInput {
17
+ readonly code: string;
18
+ /**
19
+ * Must be byte-identical to the `redirect_uri` sent on the authorize request.
20
+ */
21
+ readonly redirectUri: string;
22
+ }
23
+ export interface DiscordOAuthRefreshTokenInput {
24
+ readonly refreshToken: string;
25
+ }
26
+ export interface DiscordOAuthReadCurrentUserInput {
27
+ /**
28
+ * The user's access token. NOT the client credentials.
29
+ */
30
+ readonly accessToken: string;
31
+ }
32
+ export interface DiscordOAuthTokenResponse {
33
+ readonly access_token: string;
34
+ readonly token_type: 'Bearer';
35
+ /**
36
+ * Seconds until expiry.
37
+ *
38
+ * Discord issues 604800 (7 days), far longer than most providers. Nothing structural depends on
39
+ * that, but it does mean an expiry bug surfaces a week after it is introduced rather than an hour.
40
+ */
41
+ readonly expires_in: Seconds;
42
+ readonly refresh_token: string;
43
+ /**
44
+ * The granted scopes, space-delimited.
45
+ */
46
+ readonly scope?: Maybe<string>;
47
+ }
48
+ /**
49
+ * The subset of Discord's user object this package reads.
50
+ *
51
+ * @see https://docs.discord.com/developers/resources/user
52
+ */
53
+ export interface DiscordOAuthCurrentUser {
54
+ readonly id: DiscordSnowflake;
55
+ readonly username: string;
56
+ /**
57
+ * The user's chosen display name, which supersedes the legacy `username#discriminator` pair.
58
+ *
59
+ * Null for accounts that have not migrated.
60
+ */
61
+ readonly global_name?: Maybe<string>;
62
+ readonly discriminator?: Maybe<string>;
63
+ readonly avatar?: Maybe<string>;
64
+ }
65
+ /**
66
+ * Builds the HTTP Basic `Authorization` header value that authenticates the OAuth client.
67
+ *
68
+ * Discord accepts the client credentials as Basic auth rather than in the request body, which is why
69
+ * `client_id` / `client_secret` are absent from the exchange body below.
70
+ *
71
+ * Uses `btoa()` rather than `Buffer`, so this package stays usable outside Node — the same choice
72
+ * `@dereekb/util`'s PKCE helpers make.
73
+ *
74
+ * @param config - The client credentials to encode.
75
+ * @returns The `Authorization` header value, including the `Basic ` prefix.
76
+ *
77
+ * @__NO_SIDE_EFFECTS__
78
+ */
79
+ export declare function discordOAuthBasicAuthorizationHeader(config: DiscordOAuthConfig): string;
80
+ /**
81
+ * Exchanges an OAuth authorization code for access and refresh tokens.
82
+ *
83
+ * Discord requires `application/x-www-form-urlencoded` — a JSON body is rejected — and authenticates
84
+ * the client with HTTP Basic rather than credentials in the body. Both differ from Cal.com, which
85
+ * posts JSON with the credentials inline. The Basic header rides on the context's configured fetch.
86
+ *
87
+ * @param context - The Discord OAuth context providing the authenticated fetch.
88
+ * @returns Exchanges an authorization code for access and refresh tokens.
89
+ *
90
+ * @see https://docs.discord.com/developers/topics/oauth2
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const response = await exchangeAuthorizationCode(context)({
95
+ * code: 'auth-code-from-redirect',
96
+ * redirectUri: 'http://localhost:9901/oauth/discord/callback'
97
+ * });
98
+ * ```
99
+ */
100
+ export declare function exchangeAuthorizationCode(context: DiscordOAuthContext): (input: DiscordOAuthExchangeAuthorizationCodeInput) => Promise<DiscordOAuthTokenResponse>;
101
+ /**
102
+ * Refreshes an access token.
103
+ *
104
+ * Discord's refresh response carries a `refresh_token` of its own, so persist whatever comes back
105
+ * rather than assuming the sent one stays valid. That is correct for rotating and non-rotating
106
+ * providers alike.
107
+ *
108
+ * Reached through `DiscordUserExternalConnectionOAuthService.refreshCredentials`, which the external
109
+ * connection reader dispatches to when a user's stored Discord credentials are near expiration.
110
+ *
111
+ * @param context - The Discord OAuth context providing the authenticated fetch.
112
+ * @returns Refreshes an access token using the given refresh token.
113
+ *
114
+ * @see https://docs.discord.com/developers/topics/oauth2
115
+ */
116
+ export declare function refreshAccessToken(context: DiscordOAuthContext): (input: DiscordOAuthRefreshTokenInput) => Promise<DiscordOAuthTokenResponse>;
117
+ /**
118
+ * Reads the user an access token belongs to. Requires the `identify` scope.
119
+ *
120
+ * Bearer-authenticated with the USER's token, not Basic-authenticated with the client credentials, so
121
+ * the `Authorization` header is passed per-request to override the one on the context's fetch. A
122
+ * per-request header wins over the base header of the same name, which is what makes one configured
123
+ * fetch enough for both shapes.
124
+ *
125
+ * @param context - The Discord OAuth context providing the authenticated fetch.
126
+ * @returns Reads the Discord user the given access token belongs to.
127
+ *
128
+ * @see https://docs.discord.com/developers/resources/user
129
+ */
130
+ export declare function readCurrentUser(context: DiscordOAuthContext): (input: DiscordOAuthReadCurrentUserInput) => Promise<DiscordOAuthCurrentUser>;
@@ -0,0 +1,93 @@
1
+ import { type Maybe, type WebsiteUrl } from '@dereekb/util';
2
+ import { type DiscordOAuthClientId } from '../discord.config';
3
+ /**
4
+ * The Discord OAuth scopes this package models.
5
+ *
6
+ * A runtime list rather than a bare type union, so a configured scope can be validated instead of
7
+ * being passed through to the consent screen and refused there.
8
+ *
9
+ * Deliberately NOT Discord's full ~40-scope surface: only what a per-user account connect can
10
+ * legitimately ask for. Add a scope here when code actually uses it.
11
+ *
12
+ * @see https://docs.discord.com/developers/topics/oauth2
13
+ */
14
+ export declare const ALL_DISCORD_OAUTH_SCOPES: readonly ["identify", "email", "guilds", "connections"];
15
+ /**
16
+ * A Discord OAuth scope modeled by this package.
17
+ */
18
+ export type DiscordOAuthScope = (typeof ALL_DISCORD_OAUTH_SCOPES)[number];
19
+ /**
20
+ * Returns whether the input is a known {@link DiscordOAuthScope}.
21
+ *
22
+ * @param value - The value to check.
23
+ * @returns True when the value is a Discord OAuth scope this package models.
24
+ */
25
+ export declare function isDiscordOAuthScope(value: string): value is DiscordOAuthScope;
26
+ /**
27
+ * The delimiter used to join scopes in the `scope` query parameter.
28
+ *
29
+ * OAuth2 specifies a space-delimited list and Discord follows it. `URL.searchParams.set` handles the
30
+ * percent-encoding, so this stays a literal space.
31
+ */
32
+ export declare const DISCORD_OAUTH_SCOPE_DELIMITER = " ";
33
+ /**
34
+ * The `response_type` used by the authorization-code flow.
35
+ */
36
+ export declare const DISCORD_OAUTH_AUTHORIZE_RESPONSE_TYPE = "code";
37
+ export interface DiscordOAuthAuthorizeUrlFactoryConfig {
38
+ /**
39
+ * The OAuth client id to authorize as.
40
+ */
41
+ readonly clientId: DiscordOAuthClientId;
42
+ /**
43
+ * The redirect URI to return to after the user consents.
44
+ *
45
+ * Must match a URI registered on the Discord application byte-for-byte, including the port, and
46
+ * must be identical to the `redirectUri` later passed to the token exchange.
47
+ */
48
+ readonly redirectUri: WebsiteUrl;
49
+ /**
50
+ * The scopes to request.
51
+ */
52
+ readonly scopes: readonly DiscordOAuthScope[];
53
+ /**
54
+ * Optional override of the authorize URL. Defaults to {@link DISCORD_OAUTH_AUTHORIZE_URL}.
55
+ */
56
+ readonly authorizeUrl?: Maybe<WebsiteUrl>;
57
+ }
58
+ export interface DiscordOAuthAuthorizeUrlParams {
59
+ /**
60
+ * Opaque state echoed back to the redirect URI.
61
+ *
62
+ * Carries the acting user and is the CSRF defense for the handoff, so it should be signed and
63
+ * short-lived.
64
+ */
65
+ readonly state?: Maybe<string>;
66
+ }
67
+ export type DiscordOAuthAuthorizeUrlFactory = (params?: Maybe<DiscordOAuthAuthorizeUrlParams>) => WebsiteUrl;
68
+ /**
69
+ * Creates a {@link DiscordOAuthAuthorizeUrlFactory} that composes the Discord authorize URL a user's
70
+ * browser is redirected to in order to begin the authorization-code flow.
71
+ *
72
+ * The client id, redirect URI, and scopes are fixed by the config, since a consumer holds those
73
+ * constant and varies only the per-request `state`.
74
+ *
75
+ * @param config - The client id, redirect URI, and scopes to request.
76
+ * @returns A factory that builds an authorize URL for the given params.
77
+ *
78
+ * @see https://docs.discord.com/developers/topics/oauth2
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * const authorizeUrlFactory = discordOAuthAuthorizeUrlFactory({
83
+ * clientId: 'client-id',
84
+ * redirectUri: 'http://localhost:9901/oauth/discord/callback',
85
+ * scopes: ['identify']
86
+ * });
87
+ *
88
+ * const url = authorizeUrlFactory({ state: 'signed-state' });
89
+ * ```
90
+ *
91
+ * @__NO_SIDE_EFFECTS__
92
+ */
93
+ export declare function discordOAuthAuthorizeUrlFactory(config: DiscordOAuthAuthorizeUrlFactoryConfig): DiscordOAuthAuthorizeUrlFactory;
@@ -0,0 +1,56 @@
1
+ import { type FactoryWithRequiredInput } from '@dereekb/util';
2
+ import { type ConfiguredFetch, type FetchJsonFunction } from '@dereekb/util/fetch';
3
+ import { type DiscordOAuthClientId, type DiscordOAuthClientSecret } from '../discord.config';
4
+ /**
5
+ * The Discord OAuth2 authorize URL the user's browser is redirected to.
6
+ *
7
+ * Note this is NOT under `/api`: Discord serves the consent screen from the site root, while the
8
+ * token endpoint lives under {@link DISCORD_API_URL}. It is therefore a full URL rather than a path
9
+ * relative to the API base.
10
+ */
11
+ export declare const DISCORD_OAUTH_AUTHORIZE_URL = "https://discord.com/oauth2/authorize";
12
+ /**
13
+ * The Discord OAuth2 token endpoint path, relative to {@link DISCORD_API_URL}.
14
+ */
15
+ export declare const DISCORD_OAUTH_TOKEN_PATH = "/oauth2/token";
16
+ /**
17
+ * The Discord OAuth2 token revocation endpoint path, relative to {@link DISCORD_API_URL}.
18
+ */
19
+ export declare const DISCORD_OAUTH_REVOKE_PATH = "/oauth2/token/revoke";
20
+ /**
21
+ * Path of the endpoint returning the user an access token belongs to.
22
+ *
23
+ * Requires the `identify` scope.
24
+ */
25
+ export declare const DISCORD_OAUTH_CURRENT_USER_PATH = "/users/@me";
26
+ /**
27
+ * Configuration for a Discord OAuth client.
28
+ *
29
+ * Both values are required: unlike Cal.com, Discord has no api-key alternative to the
30
+ * client-credentials pair, so there is no valid partially-configured state.
31
+ */
32
+ export interface DiscordOAuthConfig {
33
+ readonly clientId: DiscordOAuthClientId;
34
+ readonly clientSecret: DiscordOAuthClientSecret;
35
+ }
36
+ export interface DiscordOAuthFetchFactoryInput {
37
+ /**
38
+ * The client credentials the produced fetch authenticates the token endpoint with.
39
+ *
40
+ * Passed in rather than closed over, because the Basic authorization header is part of the fetch's
41
+ * baseRequest and so cannot be composed before the credentials are known.
42
+ */
43
+ readonly config: DiscordOAuthConfig;
44
+ }
45
+ export type DiscordOAuthFetchFactory = FactoryWithRequiredInput<ConfiguredFetch, DiscordOAuthFetchFactoryInput>;
46
+ /**
47
+ * Context used for performing fetch() and fetchJson() calls with a configured fetch instance.
48
+ */
49
+ export interface DiscordOAuthContext {
50
+ readonly fetch: ConfiguredFetch;
51
+ readonly fetchJson: FetchJsonFunction;
52
+ readonly config: DiscordOAuthConfig;
53
+ }
54
+ export interface DiscordOAuthContextRef {
55
+ readonly oauthContext: DiscordOAuthContext;
56
+ }
@@ -0,0 +1,42 @@
1
+ import { type Seconds } from '@dereekb/util';
2
+ import { type DiscordOAuthTokenResponse } from './oauth.api';
3
+ /**
4
+ * Access token string issued by Discord's token endpoint.
5
+ */
6
+ export type DiscordAccessTokenString = string;
7
+ /**
8
+ * Space-separated scopes string for a {@link DiscordAccessToken}.
9
+ */
10
+ export type DiscordAccessTokenScopesString = string;
11
+ /**
12
+ * Refresh token issued alongside a Discord access token.
13
+ *
14
+ * Discord's refresh response returns a refresh token of its own, so always persist the latest one
15
+ * rather than assuming the sent value survives.
16
+ */
17
+ export type DiscordRefreshToken = string;
18
+ /**
19
+ * A normalized Discord account access token.
20
+ */
21
+ export interface DiscordAccessToken {
22
+ readonly accessToken: DiscordAccessTokenString;
23
+ readonly refreshToken: DiscordRefreshToken;
24
+ readonly scope: DiscordAccessTokenScopesString;
25
+ /**
26
+ * Length of time the token is valid for. Discord issues 7 days.
27
+ */
28
+ readonly expiresIn: Seconds;
29
+ /**
30
+ * Date the token expires at.
31
+ */
32
+ readonly expiresAt: Date;
33
+ }
34
+ /**
35
+ * Maps a {@link DiscordOAuthTokenResponse} to a {@link DiscordAccessToken}.
36
+ *
37
+ * @param response - The token response returned by the Discord token endpoint.
38
+ * @returns The equivalent DiscordAccessToken, with `expiresAt` resolved against the current time.
39
+ *
40
+ * @__NO_SIDE_EFFECTS__
41
+ */
42
+ export declare function discordAccessTokenFromTokenResponse(response: DiscordOAuthTokenResponse): DiscordAccessToken;
@@ -0,0 +1,65 @@
1
+ import { type Maybe } from '@dereekb/util';
2
+ import { type ConfiguredFetch, type FetchRequestFactoryError, FetchResponseError } from '@dereekb/util/fetch';
3
+ import { BaseError } from 'make-error';
4
+ /**
5
+ * Error code returned when a code or refresh token is invalid, expired, or was already spent.
6
+ */
7
+ export declare const DISCORD_OAUTH_INVALID_GRANT_ERROR_CODE = "invalid_grant";
8
+ /**
9
+ * Error code returned when the requested scope set is not one the application may ask for.
10
+ */
11
+ export declare const DISCORD_OAUTH_INVALID_SCOPE_ERROR_CODE = "invalid_scope";
12
+ /**
13
+ * The error body Discord's OAuth endpoints return.
14
+ *
15
+ * Discord follows RFC 6749's `error` / `error_description` shape rather than the `code` / `message`
16
+ * shape its own REST API uses, so this is deliberately not the REST error type.
17
+ */
18
+ export interface DiscordOAuthErrorData {
19
+ readonly error: string;
20
+ readonly error_description?: Maybe<string>;
21
+ }
22
+ /**
23
+ * An error reported by a Discord OAuth endpoint.
24
+ */
25
+ export declare class DiscordOAuthError<D extends DiscordOAuthErrorData = DiscordOAuthErrorData> extends BaseError {
26
+ readonly error: D;
27
+ get code(): string;
28
+ constructor(error: D);
29
+ }
30
+ /**
31
+ * A {@link DiscordOAuthError} that retains the HTTP response it was parsed from.
32
+ */
33
+ export declare class DiscordOAuthFetchResponseError<D extends DiscordOAuthErrorData = DiscordOAuthErrorData> extends DiscordOAuthError<D> {
34
+ readonly data: D;
35
+ readonly responseError: FetchResponseError;
36
+ constructor(data: D, responseError: FetchResponseError);
37
+ }
38
+ export type LogDiscordOAuthErrorFunction = (error: FetchRequestFactoryError | DiscordOAuthError) => void;
39
+ /**
40
+ * Creates a {@link LogDiscordOAuthErrorFunction} that logs the error to the console.
41
+ *
42
+ * @param discordApiNamePrefix - Prefix to use when logging, e.g. `DiscordOAuth`.
43
+ * @returns A log function that prefixes each logged error.
44
+ *
45
+ * @__NO_SIDE_EFFECTS__
46
+ */
47
+ export declare function logDiscordOAuthErrorFunction(discordApiNamePrefix: string): LogDiscordOAuthErrorFunction;
48
+ export declare const logDiscordOAuthErrorToConsole: LogDiscordOAuthErrorFunction;
49
+ /**
50
+ * Parses a {@link FetchResponseError} from a Discord OAuth call into a typed error.
51
+ *
52
+ * @param responseError - The fetch response error to parse.
53
+ * @returns The parsed error, or undefined when the body carried no OAuth error to parse.
54
+ */
55
+ export declare function parseDiscordOAuthError(responseError: FetchResponseError): Promise<Maybe<DiscordOAuthFetchResponseError>>;
56
+ /**
57
+ * Wraps a {@link ConfiguredFetch} so that Discord OAuth error responses surface as typed errors.
58
+ *
59
+ * @param fetch - The fetch to wrap.
60
+ * @param logError - Optional override of the error logging function.
61
+ * @returns The wrapped fetch.
62
+ *
63
+ * @__NO_SIDE_EFFECTS__
64
+ */
65
+ export declare function handleDiscordOAuthErrorFetch(fetch: ConfiguredFetch, logError?: LogDiscordOAuthErrorFunction): ConfiguredFetch;
@@ -0,0 +1,36 @@
1
+ import { type Maybe } from '@dereekb/util';
2
+ import { type FetchHandler } from '@dereekb/util/fetch';
3
+ import { type DiscordOAuthConfig, type DiscordOAuthContextRef, type DiscordOAuthFetchFactory } from './oauth.config';
4
+ import { type LogDiscordOAuthErrorFunction } from './oauth.error.api';
5
+ export type DiscordOAuth = DiscordOAuthContextRef;
6
+ export interface DiscordOAuthFactoryConfig {
7
+ /**
8
+ * Creates a new fetch instance to use when making calls.
9
+ */
10
+ readonly fetchFactory?: DiscordOAuthFetchFactory;
11
+ /**
12
+ * Custom FetchHandler to use with the default fetchFactory.
13
+ *
14
+ * This is the seam specs use to intercept requests before they leave the process. Ignored when a
15
+ * `fetchFactory` is provided.
16
+ */
17
+ readonly fetchHandler?: Maybe<FetchHandler>;
18
+ /**
19
+ * Custom log error function.
20
+ */
21
+ readonly logDiscordOAuthErrorFunction?: LogDiscordOAuthErrorFunction;
22
+ }
23
+ export type DiscordOAuthFactory = (config: DiscordOAuthConfig) => DiscordOAuth;
24
+ /**
25
+ * Creates a {@link DiscordOAuthFactory} that produces configured Discord OAuth instances.
26
+ *
27
+ * There is no access-token cache or per-user token factory here, unlike `calcomOAuthFactory`: the
28
+ * external-connection framework stores each user's credentials itself, so the client only ever needs
29
+ * to make the calls it is asked to make.
30
+ *
31
+ * @param factoryConfig - Configuration including an optional fetch factory, fetch handler, and error logging.
32
+ * @returns A factory accepting a DiscordOAuthConfig and producing a DiscordOAuth instance.
33
+ *
34
+ * @__NO_SIDE_EFFECTS__
35
+ */
36
+ export declare function discordOAuthFactory(factoryConfig: DiscordOAuthFactoryConfig): DiscordOAuthFactory;