@dereekb/discord 14.0.1 → 14.2.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.
@@ -0,0 +1,257 @@
1
+ import { type Factory, type IndexNumber, type Maybe, type Milliseconds, type PromiseOrValue } from '@dereekb/util';
2
+ import { type DiscordFetchMessagePageFetchFunction, type DiscordMessagePageFilter } from './discord.api.page';
3
+ import { type DiscordMessageId } from './discord.type';
4
+ /**
5
+ * Why a scan stopped walking messages.
6
+ *
7
+ * `stop_bound` and `channel_start` mean the scan is caught up; the rest mean it exhausted a budget
8
+ * and can be resumed from {@link DiscordScanMessagesResult.resumeBeforeMessageId}.
9
+ */
10
+ export type DiscordScanStopReason =
11
+ /**
12
+ * Reached the `afterMessageId` stop bound. Everything newer than it has been handled.
13
+ */
14
+ 'stop_bound'
15
+ /**
16
+ * Reached a short page, meaning no older messages exist in the channel.
17
+ */
18
+ | 'channel_start'
19
+ /**
20
+ * Loaded as many messages as the scan was budgeted for.
21
+ */
22
+ | 'max_messages'
23
+ /**
24
+ * Loaded as many pages as the scan was budgeted for.
25
+ */
26
+ | 'max_pages'
27
+ /**
28
+ * Ran out of the scan's time budget.
29
+ */
30
+ | 'time_budget';
31
+ /**
32
+ * A single page's worth of messages delivered to a {@link DiscordScanMessagesBatchHandler}.
33
+ *
34
+ * @typeParam T - The message type.
35
+ */
36
+ export interface DiscordScanMessagesBatch<T> {
37
+ /**
38
+ * The messages in this batch, newest-first, as Discord returns them.
39
+ *
40
+ * Never empty: a batch with no messages is not delivered to the handler at all.
41
+ */
42
+ readonly messages: T[];
43
+ /**
44
+ * The id of the newest message in this batch.
45
+ */
46
+ readonly newestMessageId: DiscordMessageId;
47
+ /**
48
+ * The id of the oldest message in this batch.
49
+ */
50
+ readonly oldestMessageId: DiscordMessageId;
51
+ /**
52
+ * The zero-based index of the page this batch came from within this scan.
53
+ */
54
+ readonly page: IndexNumber;
55
+ /**
56
+ * The total number of messages handed to the handler by this scan so far, including this batch.
57
+ */
58
+ readonly totalMessagesHandled: number;
59
+ }
60
+ /**
61
+ * Handles a single batch of messages during a scan.
62
+ *
63
+ * Batches arrive one at a time, newest page first, and the scan waits for each one before fetching
64
+ * the next page.
65
+ */
66
+ export type DiscordScanMessagesBatchHandler<T> = (batch: DiscordScanMessagesBatch<T>) => Promise<void>;
67
+ /**
68
+ * The work budget for a scan.
69
+ *
70
+ * Every value is optional; an unset value means that particular budget is unbounded. A scan with no
71
+ * budget at all walks back to the start of the channel or to its stop bound.
72
+ */
73
+ export interface DiscordScanMessagesBounds {
74
+ /**
75
+ * The number of messages to request per page.
76
+ *
77
+ * Defaults to {@link DEFAULT_DISCORD_MESSAGES_PER_PAGE}.
78
+ */
79
+ readonly messagesPerPage?: Maybe<number>;
80
+ /**
81
+ * The maximum number of pages to fetch.
82
+ */
83
+ readonly maxPages?: Maybe<number>;
84
+ /**
85
+ * The maximum number of messages to load, counted before filtering.
86
+ *
87
+ * Counted pre-filter so that a scan whose filter rejects everything still terminates.
88
+ */
89
+ readonly maxMessages?: Maybe<number>;
90
+ /**
91
+ * The maximum amount of time the scan may spend, checked after each page.
92
+ */
93
+ readonly maxDuration?: Maybe<Milliseconds>;
94
+ /**
95
+ * The amount of time to wait between page fetches.
96
+ */
97
+ readonly waitBetweenPages?: Maybe<Milliseconds>;
98
+ }
99
+ /**
100
+ * Configuration for {@link discordScanMessagesFactory}.
101
+ *
102
+ * @typeParam I - The fetch input filter type.
103
+ * @typeParam T - The message type.
104
+ */
105
+ export interface DiscordScanMessagesFactoryConfig<I extends DiscordMessagePageFilter, T extends {
106
+ id: string;
107
+ }> {
108
+ /**
109
+ * The Discord message fetch function to scan with.
110
+ */
111
+ readonly fetch: DiscordFetchMessagePageFetchFunction<I, T>;
112
+ /**
113
+ * Reads the snowflake id from a message. Defaults to reading the `id` property.
114
+ */
115
+ readonly readMessageId?: Maybe<(message: T) => DiscordMessageId>;
116
+ /**
117
+ * Default bounds applied to every scan, overridden per-scan by the scan input.
118
+ */
119
+ readonly defaults?: Maybe<DiscordScanMessagesBounds>;
120
+ /**
121
+ * Factory for the current time, used for the `maxDuration` budget.
122
+ *
123
+ * Exists as a seam so a spec can drive the time budget deterministically.
124
+ */
125
+ readonly nowFactory?: Maybe<Factory<Date>>;
126
+ }
127
+ /**
128
+ * Input for a single scan.
129
+ *
130
+ * @typeParam I - The fetch input filter type.
131
+ * @typeParam T - The message type.
132
+ */
133
+ export interface DiscordScanMessagesInput<I extends DiscordMessagePageFilter, T extends {
134
+ id: string;
135
+ }> extends DiscordScanMessagesBounds {
136
+ /**
137
+ * The non-pagination part of the fetch input, such as the channel id.
138
+ *
139
+ * The pagination fields are owned by the scan and are built from the bounds below.
140
+ */
141
+ readonly baseInput: Omit<I, keyof DiscordMessagePageFilter>;
142
+ /**
143
+ * The START bound, exclusive. The scan begins with the messages immediately older than this id.
144
+ *
145
+ * Leave undefined to start from the newest message in the channel. Resume an interrupted scan by
146
+ * passing its {@link DiscordScanMessagesResult.resumeBeforeMessageId} here.
147
+ */
148
+ readonly beforeMessageId?: Maybe<DiscordMessageId>;
149
+ /**
150
+ * The STOP bound, exclusive. The scan stops once it walks back to this id.
151
+ *
152
+ * Leave undefined to walk back to the start of the channel. This is typically the high-water mark
153
+ * from the previous completed scan.
154
+ */
155
+ readonly afterMessageId?: Maybe<DiscordMessageId>;
156
+ /**
157
+ * Optional filter applied to each page before the handler sees it.
158
+ *
159
+ * Filtered-out messages are still counted as loaded and still move the resume cursor past
160
+ * themselves, so they are never revisited.
161
+ */
162
+ readonly filterMessages?: Maybe<(messages: T[]) => PromiseOrValue<T[]>>;
163
+ /**
164
+ * Handles each batch of messages. Not invoked for an empty batch.
165
+ */
166
+ readonly handleMessages: DiscordScanMessagesBatchHandler<T>;
167
+ }
168
+ /**
169
+ * The outcome of a scan.
170
+ */
171
+ export interface DiscordScanMessagesResult {
172
+ /**
173
+ * Why the scan stopped.
174
+ */
175
+ readonly stopReason: DiscordScanStopReason;
176
+ /**
177
+ * Whether the scan reached its stop bound or the start of the channel.
178
+ *
179
+ * When true the caller has seen everything it asked for and should promote
180
+ * {@link newestMessageId} to its high-water mark. When false the scan ran out of budget and
181
+ * should be resumed from {@link resumeBeforeMessageId}.
182
+ */
183
+ readonly complete: boolean;
184
+ /**
185
+ * The id of the newest message the scan loaded, before filtering.
186
+ *
187
+ * Undefined when the scan loaded nothing.
188
+ */
189
+ readonly newestMessageId: Maybe<DiscordMessageId>;
190
+ /**
191
+ * The id to pass as `beforeMessageId` to resume this scan.
192
+ *
193
+ * This is the oldest message the scan LOADED, not the oldest it delivered, so filtered-out
194
+ * messages are not revisited. Only meaningful when {@link complete} is false.
195
+ */
196
+ readonly resumeBeforeMessageId: Maybe<DiscordMessageId>;
197
+ /**
198
+ * The number of pages fetched.
199
+ */
200
+ readonly totalPages: number;
201
+ /**
202
+ * The number of messages loaded, before filtering and before the stop bound truncated a page.
203
+ */
204
+ readonly totalMessagesLoaded: number;
205
+ /**
206
+ * The number of messages delivered to the handler.
207
+ */
208
+ readonly totalMessagesHandled: number;
209
+ /**
210
+ * When the scan started.
211
+ */
212
+ readonly startedAt: Date;
213
+ /**
214
+ * When the scan ended.
215
+ */
216
+ readonly endedAt: Date;
217
+ }
218
+ /**
219
+ * Scans a Discord channel's messages, handing each batch to the input handler.
220
+ *
221
+ * @typeParam I - The fetch input filter type.
222
+ * @typeParam T - The message type.
223
+ */
224
+ export type DiscordScanMessagesFunction<I extends DiscordMessagePageFilter, T extends {
225
+ id: string;
226
+ }> = (input: DiscordScanMessagesInput<I, T>) => Promise<DiscordScanMessagesResult>;
227
+ /**
228
+ * Creates a {@link DiscordScanMessagesFunction} that walks a channel's messages backwards in time
229
+ * and hands each page to a caller-supplied handler.
230
+ *
231
+ * The scan is persistence-agnostic: it knows nothing about where a cursor is stored. It walks from
232
+ * `beforeMessageId` (or the newest message) back towards `afterMessageId` (or the start of the
233
+ * channel), stops as soon as a budget is exhausted, and reports where it stopped so the caller can
234
+ * resume from exactly there.
235
+ *
236
+ * @param config - The fetch function and scan defaults.
237
+ * @returns A scan function.
238
+ *
239
+ * @example
240
+ * ```ts
241
+ * const scan = discordScanMessagesFactory({ fetch: fetchChannelMessages });
242
+ *
243
+ * const result = await scan({
244
+ * baseInput: { channelId },
245
+ * afterMessageId: lastScannedMessageId,
246
+ * maxMessages: 1000,
247
+ * handleMessages: async ({ messages }) => saveMessages(messages)
248
+ * });
249
+ *
250
+ * if (result.complete) {
251
+ * lastScannedMessageId = result.newestMessageId ?? lastScannedMessageId;
252
+ * }
253
+ * ```
254
+ */
255
+ export declare function discordScanMessagesFactory<I extends DiscordMessagePageFilter, T extends {
256
+ id: string;
257
+ }>(config: DiscordScanMessagesFactoryConfig<I, T>): DiscordScanMessagesFunction<I, T>;
@@ -0,0 +1,70 @@
1
+ import { type Milliseconds } from '@dereekb/util';
2
+ import { type DiscordSnowflake } from './discord.type';
3
+ /**
4
+ * The Discord epoch, the first second of 2015, expressed as a unix timestamp in milliseconds.
5
+ *
6
+ * Every snowflake encodes its creation time as an offset from this value.
7
+ */
8
+ export declare const DISCORD_EPOCH_MS: Milliseconds;
9
+ /**
10
+ * The number of low bits in a snowflake reserved for the worker id, process id, and increment.
11
+ *
12
+ * The timestamp occupies every bit above these.
13
+ */
14
+ export declare const DISCORD_SNOWFLAKE_TIMESTAMP_SHIFT = 22n;
15
+ /**
16
+ * Returns the creation time encoded in the input snowflake.
17
+ *
18
+ * The arithmetic is performed with BigInt: a snowflake exceeds 53 bits, so the naive
19
+ * `Number(snowflake) >> 22` coerces to a 32-bit integer and returns a time near the Discord
20
+ * epoch for every modern id.
21
+ *
22
+ * @param snowflake - The snowflake id to read the timestamp from.
23
+ * @returns The Date the snowflake was created at.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * discordSnowflakeToDate('1480401620608090182'); // 2026-03-09T03:07:30.885Z
28
+ * ```
29
+ */
30
+ export declare function discordSnowflakeToDate(snowflake: DiscordSnowflake): Date;
31
+ /**
32
+ * Returns the lowest snowflake id that could have been created at the input date.
33
+ *
34
+ * Useful as a `before`/`after` pagination bound: an id built this way sorts before every real
35
+ * message created in the same millisecond, so it can bound a scan by time without knowing any
36
+ * actual message id.
37
+ *
38
+ * @param date - The date to build a snowflake bound for.
39
+ * @returns The lowest snowflake id for that millisecond.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const twoWeeksAgo = discordSnowflakeForDate(addDays(new Date(), -14));
44
+ * ```
45
+ */
46
+ export declare function discordSnowflakeForDate(date: Date): DiscordSnowflake;
47
+ /**
48
+ * Compares two snowflakes by their numeric value.
49
+ *
50
+ * Compared as BigInt values rather than strings: a plain string comparison is only correct for
51
+ * ids of equal length, and snowflake ids grow a digit over time.
52
+ *
53
+ * @param a - The first snowflake.
54
+ * @param b - The second snowflake.
55
+ * @returns A negative number when a is older than b, a positive number when a is newer, and 0 when equal.
56
+ */
57
+ export declare function compareDiscordSnowflakes(a: DiscordSnowflake, b: DiscordSnowflake): number;
58
+ /**
59
+ * Returns true if snowflake a was created after snowflake b.
60
+ *
61
+ * @param a - The snowflake to test.
62
+ * @param b - The snowflake to test against.
63
+ * @returns True when a is strictly newer than b.
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * discordSnowflakeIsAfter('1480401620608090182', '1480401620608090181'); // true
68
+ * ```
69
+ */
70
+ export declare function discordSnowflakeIsAfter(a: DiscordSnowflake, b: DiscordSnowflake): boolean;
@@ -26,6 +26,10 @@ export type DiscordGuildId = DiscordSnowflake;
26
26
  * A Discord message id.
27
27
  */
28
28
  export type DiscordMessageId = DiscordSnowflake;
29
+ /**
30
+ * A Discord user id.
31
+ */
32
+ export type DiscordUserId = DiscordSnowflake;
29
33
  /**
30
34
  * Bot token used to authenticate the Discord bot with the gateway.
31
35
  *
@@ -1,4 +1,6 @@
1
1
  export * from './discord.api.page';
2
+ export * from './discord.api.scan';
2
3
  export * from './discord.config';
4
+ export * from './discord.snowflake';
3
5
  export * from './discord.type';
4
6
  export * from './oauth';
@@ -1,4 +1,4 @@
1
- import { type Maybe, type Seconds } from '@dereekb/util';
1
+ import { type EmailAddress, type Maybe, type Seconds } from '@dereekb/util';
2
2
  import { type DiscordSnowflake } from '../discord.type';
3
3
  import { type DiscordOAuthConfig, type DiscordOAuthContext } from './oauth.config';
4
4
  /**
@@ -6,11 +6,16 @@ import { type DiscordOAuthConfig, type DiscordOAuthContext } from './oauth.confi
6
6
  *
7
7
  * Discord rejects a JSON body outright, unlike Cal.com, which requires one.
8
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`.
9
+ * Discord IS an OIDC provider, contrary to what this file previously claimed. Verified directly:
10
+ * `https://discord.com/.well-known/openid-configuration` returns 200 with every required discovery
11
+ * field, `https://discord.com/api/oauth2/keys` serves a JWKS, and the `openid` scope yields an
12
+ * `id_token` on the authorization-code grant. The relying-party calls below therefore delegate to
13
+ * `@dereekb/util/oidc`, passing `clientAuth: 'client_secret_basic'` (Discord's discovery document
14
+ * omits `token_endpoint_auth_methods_supported`, whose OIDC Discovery default is Basic — which is
15
+ * what Discord in fact requires).
16
+ *
17
+ * Discovery itself is not performed: the endpoints are stable, and skipping the extra round trip
18
+ * keeps the per-request cost the same as before.
14
19
  */
15
20
  export declare const DISCORD_OAUTH_TOKEN_CONTENT_TYPE = "application/x-www-form-urlencoded";
16
21
  export interface DiscordOAuthExchangeAuthorizationCodeInput {
@@ -19,6 +24,23 @@ export interface DiscordOAuthExchangeAuthorizationCodeInput {
19
24
  * Must be byte-identical to the `redirect_uri` sent on the authorize request.
20
25
  */
21
26
  readonly redirectUri: string;
27
+ /**
28
+ * The PKCE code verifier whose challenge was sent on the authorize request.
29
+ *
30
+ * Optional, and must be omitted when the authorize request carried no `code_challenge` — Discord
31
+ * rejects a `code_verifier` for a code minted without one.
32
+ */
33
+ readonly codeVerifier?: Maybe<string>;
34
+ }
35
+ export interface DiscordOAuthRevokeTokenInput {
36
+ /**
37
+ * The access or refresh token to revoke.
38
+ */
39
+ readonly token: string;
40
+ /**
41
+ * Optional hint telling Discord which kind of token was passed.
42
+ */
43
+ readonly tokenTypeHint?: Maybe<'access_token' | 'refresh_token'>;
22
44
  }
23
45
  export interface DiscordOAuthRefreshTokenInput {
24
46
  readonly refreshToken: string;
@@ -61,15 +83,30 @@ export interface DiscordOAuthCurrentUser {
61
83
  readonly global_name?: Maybe<string>;
62
84
  readonly discriminator?: Maybe<string>;
63
85
  readonly avatar?: Maybe<string>;
86
+ /**
87
+ * The user's email. Present only when the `email` scope was granted.
88
+ *
89
+ * Never key an identity on this — it is mutable, and {@link DiscordOAuthCurrentUser.id} is the
90
+ * stable account identifier.
91
+ */
92
+ readonly email?: Maybe<EmailAddress>;
93
+ /**
94
+ * Whether Discord has verified {@link DiscordOAuthCurrentUser.email}. Present only when the
95
+ * `email` scope was granted.
96
+ *
97
+ * A sign-in must not adopt an existing account by email unless this is true.
98
+ */
99
+ readonly verified?: Maybe<boolean>;
64
100
  }
65
101
  /**
66
102
  * Builds the HTTP Basic `Authorization` header value that authenticates the OAuth client.
67
103
  *
68
- * Discord accepts the client credentials as Basic auth rather than in the request body, which is why
104
+ * Discord requires the client credentials as Basic auth rather than in the request body, which is why
69
105
  * `client_id` / `client_secret` are absent from the exchange body below.
70
106
  *
71
- * Uses `btoa()` rather than `Buffer`, so this package stays usable outside Node — the same choice
72
- * `@dereekb/util`'s PKCE helpers make.
107
+ * A thin alias of the generic {@link oidcClientSecretBasicAuthorizationHeader}, kept because the
108
+ * configured fetch bakes the header into its `baseRequest` and so needs it as a value, not as a
109
+ * per-request auth mode.
73
110
  *
74
111
  * @param config - The client credentials to encode.
75
112
  * @returns The `Authorization` header value, including the `Basic ` prefix.
@@ -80,9 +117,10 @@ export declare function discordOAuthBasicAuthorizationHeader(config: DiscordOAut
80
117
  /**
81
118
  * Exchanges an OAuth authorization code for access and refresh tokens.
82
119
  *
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.
120
+ * Delegates to `@dereekb/util/oidc`'s relying-party `exchangeAuthorizationCode` with
121
+ * `clientAuth: 'client_secret_basic'`, which produces exactly the form-encoded, Basic-authenticated
122
+ * request Discord requires. The context's configured fetch supplies the base URL and surfaces
123
+ * Discord's RFC-6749 error bodies as typed {@link DiscordOAuthError}s.
86
124
  *
87
125
  * @param context - The Discord OAuth context providing the authenticated fetch.
88
126
  * @returns Exchanges an authorization code for access and refresh tokens.
@@ -114,6 +152,18 @@ export declare function exchangeAuthorizationCode(context: DiscordOAuthContext):
114
152
  * @see https://docs.discord.com/developers/topics/oauth2
115
153
  */
116
154
  export declare function refreshAccessToken(context: DiscordOAuthContext): (input: DiscordOAuthRefreshTokenInput) => Promise<DiscordOAuthTokenResponse>;
155
+ /**
156
+ * Revokes an access or refresh token, ending Discord's side of the authorization.
157
+ *
158
+ * Called when a user disconnects their Discord account: deleting the stored credentials alone leaves
159
+ * the grant live on Discord, so the token stays usable by anyone who captured it.
160
+ *
161
+ * @param context - The Discord OAuth context providing the authenticated fetch.
162
+ * @returns Revokes the given token.
163
+ *
164
+ * @see https://docs.discord.com/developers/topics/oauth2
165
+ */
166
+ export declare function revokeToken(context: DiscordOAuthContext): (input: DiscordOAuthRevokeTokenInput) => Promise<void>;
117
167
  /**
118
168
  * Reads the user an access token belongs to. Requires the `identify` scope.
119
169
  *
@@ -6,12 +6,16 @@ import { type DiscordOAuthClientId } from '../discord.config';
6
6
  * A runtime list rather than a bare type union, so a configured scope can be validated instead of
7
7
  * being passed through to the consent screen and refused there.
8
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.
9
+ * Deliberately NOT Discord's full ~40-scope surface: only what a per-user account connect or sign-in
10
+ * can legitimately ask for. Add a scope here when code actually uses it.
11
+ *
12
+ * `openid` is included because Discord is an OIDC provider (its discovery document and JWKS are
13
+ * live) and requesting it yields an `id_token`. Nothing in this workspace consumes that token —
14
+ * identity is read server-side from `/users/@me` — but the scope is legal to request.
11
15
  *
12
16
  * @see https://docs.discord.com/developers/topics/oauth2
13
17
  */
14
- export declare const ALL_DISCORD_OAUTH_SCOPES: readonly ["identify", "email", "guilds", "connections"];
18
+ export declare const ALL_DISCORD_OAUTH_SCOPES: readonly ["openid", "identify", "email", "guilds", "connections"];
15
19
  /**
16
20
  * A Discord OAuth scope modeled by this package.
17
21
  */
@@ -34,6 +38,13 @@ export declare const DISCORD_OAUTH_SCOPE_DELIMITER = " ";
34
38
  * The `response_type` used by the authorization-code flow.
35
39
  */
36
40
  export declare const DISCORD_OAUTH_AUTHORIZE_RESPONSE_TYPE = "code";
41
+ /**
42
+ * The only PKCE challenge method this package emits.
43
+ *
44
+ * `plain` is not offered: it provides no protection against an attacker who can read the
45
+ * authorization request, which is the threat PKCE exists to address.
46
+ */
47
+ export declare const DISCORD_OAUTH_AUTHORIZE_CODE_CHALLENGE_METHOD = "S256";
37
48
  export interface DiscordOAuthAuthorizeUrlFactoryConfig {
38
49
  /**
39
50
  * The OAuth client id to authorize as.
@@ -63,6 +74,14 @@ export interface DiscordOAuthAuthorizeUrlParams {
63
74
  * short-lived.
64
75
  */
65
76
  readonly state?: Maybe<string>;
77
+ /**
78
+ * The PKCE code challenge — the base64url SHA-256 digest of a code verifier the caller retains.
79
+ *
80
+ * Discord supports PKCE S256. Optional because a state minted before PKCE was added carries no
81
+ * verifier, and sending a challenge the later exchange cannot answer would break that flow; every
82
+ * new authorization should set it.
83
+ */
84
+ readonly codeChallenge?: Maybe<string>;
66
85
  }
67
86
  export type DiscordOAuthAuthorizeUrlFactory = (params?: Maybe<DiscordOAuthAuthorizeUrlParams>) => WebsiteUrl;
68
87
  /**
@@ -85,7 +104,7 @@ export type DiscordOAuthAuthorizeUrlFactory = (params?: Maybe<DiscordOAuthAuthor
85
104
  * scopes: ['identify']
86
105
  * });
87
106
  *
88
- * const url = authorizeUrlFactory({ state: 'signed-state' });
107
+ * const url = authorizeUrlFactory({ state: 'signed-state', codeChallenge: 's256-challenge' });
89
108
  * ```
90
109
  *
91
110
  * @__NO_SIDE_EFFECTS__
@@ -1,4 +1,4 @@
1
- import { type FactoryWithRequiredInput } from '@dereekb/util';
1
+ import { type FactoryWithRequiredInput, type OidcClientAuthMethod } from '@dereekb/util';
2
2
  import { type ConfiguredFetch, type FetchJsonFunction } from '@dereekb/util/fetch';
3
3
  import { type DiscordOAuthClientId, type DiscordOAuthClientSecret } from '../discord.config';
4
4
  /**
@@ -17,6 +17,22 @@ export declare const DISCORD_OAUTH_TOKEN_PATH = "/oauth2/token";
17
17
  * The Discord OAuth2 token revocation endpoint path, relative to {@link DISCORD_API_URL}.
18
18
  */
19
19
  export declare const DISCORD_OAUTH_REVOKE_PATH = "/oauth2/token/revoke";
20
+ /**
21
+ * How this client authenticates itself at Discord's token and revocation endpoints.
22
+ *
23
+ * Discord's discovery document omits `token_endpoint_auth_methods_supported`, whose OIDC Discovery
24
+ * default is `client_secret_basic` — and Discord does in fact require Basic, rejecting the
25
+ * credentials-in-body form the OAuth relying-party layer otherwise defaults to.
26
+ */
27
+ export declare const DISCORD_OAUTH_CLIENT_AUTH_METHOD: OidcClientAuthMethod;
28
+ /**
29
+ * Discord's OIDC issuer.
30
+ *
31
+ * `https://discord.com/.well-known/openid-configuration` resolves against it, though this package
32
+ * does not perform discovery — the endpoint paths above are stable, so the extra round trip buys
33
+ * nothing. Exported for consumers that do want to discover.
34
+ */
35
+ export declare const DISCORD_OIDC_ISSUER = "https://discord.com";
20
36
  /**
21
37
  * Path of the endpoint returning the user an access token belongs to.
22
38
  *