@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.
package/index.esm.js CHANGED
@@ -1,8 +1,9 @@
1
- import { lastValue, MS_IN_SECOND } from '@dereekb/util';
2
- import { fetchPageFactory, FetchResponseError, fetchJsonFunction, returnNullHandleFetchJsonParseErrorFunction, fetchApiFetchService } from '@dereekb/util/fetch';
1
+ import { lastValue, MS_IN_SECOND, oidcClientSecretBasicAuthorizationHeader } from '@dereekb/util';
2
+ import { fetchPageFactory, iterateFetchPages, FetchResponseError, fetchJsonFunction, returnNullHandleFetchJsonParseErrorFunction, fetchApiFetchService } from '@dereekb/util/fetch';
3
+ import { revokeToken as revokeToken$1, exchangeAuthorizationCode as exchangeAuthorizationCode$1, refreshAccessToken as refreshAccessToken$1 } from '@dereekb/util/oidc';
3
4
  import { BaseError } from 'make-error';
4
5
 
5
- function _define_property$1(obj, key, value) {
6
+ function _define_property$2(obj, key, value) {
6
7
  if (key in obj) {
7
8
  Object.defineProperty(obj, key, {
8
9
  value: value,
@@ -13,7 +14,7 @@ function _define_property$1(obj, key, value) {
13
14
  } else obj[key] = value;
14
15
  return obj;
15
16
  }
16
- function _object_spread(target) {
17
+ function _object_spread$1(target) {
17
18
  for(var i = 1; i < arguments.length; i++){
18
19
  var source = arguments[i] != null ? arguments[i] : {};
19
20
  var ownKeys = Object.keys(source);
@@ -23,12 +24,12 @@ function _object_spread(target) {
23
24
  }));
24
25
  }
25
26
  ownKeys.forEach(function(key) {
26
- _define_property$1(target, key, source[key]);
27
+ _define_property$2(target, key, source[key]);
27
28
  });
28
29
  }
29
30
  return target;
30
31
  }
31
- function ownKeys(object, enumerableOnly) {
32
+ function ownKeys$1(object, enumerableOnly) {
32
33
  var keys = Object.keys(object);
33
34
  if (Object.getOwnPropertySymbols) {
34
35
  var symbols = Object.getOwnPropertySymbols(object);
@@ -36,11 +37,11 @@ function ownKeys(object, enumerableOnly) {
36
37
  }
37
38
  return keys;
38
39
  }
39
- function _object_spread_props(target, source) {
40
+ function _object_spread_props$1(target, source) {
40
41
  source = source != null ? source : {};
41
42
  if (Object.getOwnPropertyDescriptors) Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
42
43
  else {
43
- ownKeys(Object(source)).forEach(function(key) {
44
+ ownKeys$1(Object(source)).forEach(function(key) {
44
45
  Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
45
46
  });
46
47
  }
@@ -76,13 +77,16 @@ function _object_spread_props(target, source) {
76
77
  var readMessageId = (_ref = config === null || config === void 0 ? void 0 : config.readMessageId) !== null && _ref !== void 0 ? _ref : function(message) {
77
78
  return message.id;
78
79
  };
79
- return fetchPageFactory(_object_spread_props(_object_spread({}, defaults), {
80
+ return fetchPageFactory(_object_spread_props$1(_object_spread$1({}, defaults), {
80
81
  fetch: fetch,
81
- readFetchPageResultInfo: function readFetchPageResultInfo(result) {
82
+ readFetchPageResultInfo: function readFetchPageResultInfo(result, input, options) {
83
+ var _ref, _options_maxItemsPerPage;
82
84
  var count = result.data.length;
83
85
  var nextCursor = count > 0 ? readMessageId(lastValue(result.data)) : undefined;
86
+ // read the effective limit the same way buildInputForNextPage does, so a short page reports hasNext: false
87
+ var effectiveLimit = (_ref = (_options_maxItemsPerPage = options.maxItemsPerPage) !== null && _options_maxItemsPerPage !== void 0 ? _options_maxItemsPerPage : input.limit) !== null && _ref !== void 0 ? _ref : DEFAULT_DISCORD_MESSAGES_PER_PAGE;
84
88
  return {
85
- hasNext: count > 0,
89
+ hasNext: count >= effectiveLimit,
86
90
  nextPageCursor: nextCursor
87
91
  };
88
92
  },
@@ -97,7 +101,7 @@ function _object_spread_props(target, source) {
97
101
  if (!nextCursor || resultCount < effectiveLimit) {
98
102
  nextInput = undefined;
99
103
  } else {
100
- nextInput = _object_spread_props(_object_spread({}, input), {
104
+ nextInput = _object_spread_props$1(_object_spread$1({}, input), {
101
105
  before: nextCursor,
102
106
  after: undefined,
103
107
  around: undefined,
@@ -109,6 +113,451 @@ function _object_spread_props(target, source) {
109
113
  }));
110
114
  }
111
115
 
116
+ /**
117
+ * The Discord epoch, the first second of 2015, expressed as a unix timestamp in milliseconds.
118
+ *
119
+ * Every snowflake encodes its creation time as an offset from this value.
120
+ */ var DISCORD_EPOCH_MS = 1420070400000;
121
+ /**
122
+ * The number of low bits in a snowflake reserved for the worker id, process id, and increment.
123
+ *
124
+ * The timestamp occupies every bit above these.
125
+ */ var DISCORD_SNOWFLAKE_TIMESTAMP_SHIFT = 22n;
126
+ /**
127
+ * Returns the creation time encoded in the input snowflake.
128
+ *
129
+ * The arithmetic is performed with BigInt: a snowflake exceeds 53 bits, so the naive
130
+ * `Number(snowflake) >> 22` coerces to a 32-bit integer and returns a time near the Discord
131
+ * epoch for every modern id.
132
+ *
133
+ * @param snowflake - The snowflake id to read the timestamp from.
134
+ * @returns The Date the snowflake was created at.
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * discordSnowflakeToDate('1480401620608090182'); // 2026-03-09T03:07:30.885Z
139
+ * ```
140
+ */ function discordSnowflakeToDate(snowflake) {
141
+ var timestamp = (BigInt(snowflake) >> DISCORD_SNOWFLAKE_TIMESTAMP_SHIFT) + BigInt(DISCORD_EPOCH_MS);
142
+ return new Date(Number(timestamp));
143
+ }
144
+ /**
145
+ * Returns the lowest snowflake id that could have been created at the input date.
146
+ *
147
+ * Useful as a `before`/`after` pagination bound: an id built this way sorts before every real
148
+ * message created in the same millisecond, so it can bound a scan by time without knowing any
149
+ * actual message id.
150
+ *
151
+ * @param date - The date to build a snowflake bound for.
152
+ * @returns The lowest snowflake id for that millisecond.
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * const twoWeeksAgo = discordSnowflakeForDate(addDays(new Date(), -14));
157
+ * ```
158
+ */ function discordSnowflakeForDate(date) {
159
+ var offset = BigInt(date.getTime()) - BigInt(DISCORD_EPOCH_MS);
160
+ var snowflake = offset > 0n ? offset << DISCORD_SNOWFLAKE_TIMESTAMP_SHIFT : 0n;
161
+ return snowflake.toString();
162
+ }
163
+ /**
164
+ * Compares two snowflakes by their numeric value.
165
+ *
166
+ * Compared as BigInt values rather than strings: a plain string comparison is only correct for
167
+ * ids of equal length, and snowflake ids grow a digit over time.
168
+ *
169
+ * @param a - The first snowflake.
170
+ * @param b - The second snowflake.
171
+ * @returns A negative number when a is older than b, a positive number when a is newer, and 0 when equal.
172
+ */ function compareDiscordSnowflakes(a, b) {
173
+ var aValue = BigInt(a);
174
+ var bValue = BigInt(b);
175
+ var result;
176
+ if (aValue < bValue) {
177
+ result = -1;
178
+ } else if (aValue > bValue) {
179
+ result = 1;
180
+ } else {
181
+ result = 0;
182
+ }
183
+ return result;
184
+ }
185
+ /**
186
+ * Returns true if snowflake a was created after snowflake b.
187
+ *
188
+ * @param a - The snowflake to test.
189
+ * @param b - The snowflake to test against.
190
+ * @returns True when a is strictly newer than b.
191
+ *
192
+ * @example
193
+ * ```ts
194
+ * discordSnowflakeIsAfter('1480401620608090182', '1480401620608090181'); // true
195
+ * ```
196
+ */ function discordSnowflakeIsAfter(a, b) {
197
+ return BigInt(a) > BigInt(b);
198
+ }
199
+
200
+ function asyncGeneratorStep$2(gen, resolve, reject, _next, _throw, key, arg) {
201
+ try {
202
+ var info = gen[key](arg);
203
+ var value = info.value;
204
+ } catch (error) {
205
+ reject(error);
206
+ return;
207
+ }
208
+ if (info.done) resolve(value);
209
+ else Promise.resolve(value).then(_next, _throw);
210
+ }
211
+ function _async_to_generator$2(fn) {
212
+ return function() {
213
+ var self = this, args = arguments;
214
+ return new Promise(function(resolve, reject) {
215
+ var gen = fn.apply(self, args);
216
+ function _next(value) {
217
+ asyncGeneratorStep$2(gen, resolve, reject, _next, _throw, "next", value);
218
+ }
219
+ function _throw(err) {
220
+ asyncGeneratorStep$2(gen, resolve, reject, _next, _throw, "throw", err);
221
+ }
222
+ _next(undefined);
223
+ });
224
+ };
225
+ }
226
+ function _define_property$1(obj, key, value) {
227
+ if (key in obj) {
228
+ Object.defineProperty(obj, key, {
229
+ value: value,
230
+ enumerable: true,
231
+ configurable: true,
232
+ writable: true
233
+ });
234
+ } else obj[key] = value;
235
+ return obj;
236
+ }
237
+ function _object_spread(target) {
238
+ for(var i = 1; i < arguments.length; i++){
239
+ var source = arguments[i] != null ? arguments[i] : {};
240
+ var ownKeys = Object.keys(source);
241
+ if (typeof Object.getOwnPropertySymbols === "function") {
242
+ ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
243
+ return Object.getOwnPropertyDescriptor(source, sym).enumerable;
244
+ }));
245
+ }
246
+ ownKeys.forEach(function(key) {
247
+ _define_property$1(target, key, source[key]);
248
+ });
249
+ }
250
+ return target;
251
+ }
252
+ function ownKeys(object, enumerableOnly) {
253
+ var keys = Object.keys(object);
254
+ if (Object.getOwnPropertySymbols) {
255
+ var symbols = Object.getOwnPropertySymbols(object);
256
+ keys.push.apply(keys, symbols);
257
+ }
258
+ return keys;
259
+ }
260
+ function _object_spread_props(target, source) {
261
+ source = source != null ? source : {};
262
+ if (Object.getOwnPropertyDescriptors) Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
263
+ else {
264
+ ownKeys(Object(source)).forEach(function(key) {
265
+ Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
266
+ });
267
+ }
268
+ return target;
269
+ }
270
+ function _ts_generator$2(thisArg, body) {
271
+ var f, y, t, _ = {
272
+ label: 0,
273
+ sent: function() {
274
+ if (t[0] & 1) throw t[1];
275
+ return t[1];
276
+ },
277
+ trys: [],
278
+ ops: []
279
+ }, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype), d = Object.defineProperty;
280
+ return d(g, "next", {
281
+ value: verb(0)
282
+ }), d(g, "throw", {
283
+ value: verb(1)
284
+ }), d(g, "return", {
285
+ value: verb(2)
286
+ }), typeof Symbol === "function" && d(g, Symbol.iterator, {
287
+ value: function() {
288
+ return this;
289
+ }
290
+ }), g;
291
+ function verb(n) {
292
+ return function(v) {
293
+ return step([
294
+ n,
295
+ v
296
+ ]);
297
+ };
298
+ }
299
+ function step(op) {
300
+ if (f) throw new TypeError("Generator is already executing.");
301
+ while(g && (g = 0, op[0] && (_ = 0)), _)try {
302
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
303
+ if (y = 0, t) op = [
304
+ op[0] & 2,
305
+ t.value
306
+ ];
307
+ switch(op[0]){
308
+ case 0:
309
+ case 1:
310
+ t = op;
311
+ break;
312
+ case 4:
313
+ _.label++;
314
+ return {
315
+ value: op[1],
316
+ done: false
317
+ };
318
+ case 5:
319
+ _.label++;
320
+ y = op[1];
321
+ op = [
322
+ 0
323
+ ];
324
+ continue;
325
+ case 7:
326
+ op = _.ops.pop();
327
+ _.trys.pop();
328
+ continue;
329
+ default:
330
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {
331
+ _ = 0;
332
+ continue;
333
+ }
334
+ if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {
335
+ _.label = op[1];
336
+ break;
337
+ }
338
+ if (op[0] === 6 && _.label < t[1]) {
339
+ _.label = t[1];
340
+ t = op;
341
+ break;
342
+ }
343
+ if (t && _.label < t[2]) {
344
+ _.label = t[2];
345
+ _.ops.push(op);
346
+ break;
347
+ }
348
+ if (t[2]) _.ops.pop();
349
+ _.trys.pop();
350
+ continue;
351
+ }
352
+ op = body.call(thisArg, _);
353
+ } catch (e) {
354
+ op = [
355
+ 6,
356
+ e
357
+ ];
358
+ y = 0;
359
+ } finally{
360
+ f = t = 0;
361
+ }
362
+ if (op[0] & 5) throw op[1];
363
+ return {
364
+ value: op[0] ? op[1] : void 0,
365
+ done: true
366
+ };
367
+ }
368
+ }
369
+ /**
370
+ * Creates a {@link DiscordScanMessagesFunction} that walks a channel's messages backwards in time
371
+ * and hands each page to a caller-supplied handler.
372
+ *
373
+ * The scan is persistence-agnostic: it knows nothing about where a cursor is stored. It walks from
374
+ * `beforeMessageId` (or the newest message) back towards `afterMessageId` (or the start of the
375
+ * channel), stops as soon as a budget is exhausted, and reports where it stopped so the caller can
376
+ * resume from exactly there.
377
+ *
378
+ * @param config - The fetch function and scan defaults.
379
+ * @returns A scan function.
380
+ *
381
+ * @example
382
+ * ```ts
383
+ * const scan = discordScanMessagesFactory({ fetch: fetchChannelMessages });
384
+ *
385
+ * const result = await scan({
386
+ * baseInput: { channelId },
387
+ * afterMessageId: lastScannedMessageId,
388
+ * maxMessages: 1000,
389
+ * handleMessages: async ({ messages }) => saveMessages(messages)
390
+ * });
391
+ *
392
+ * if (result.complete) {
393
+ * lastScannedMessageId = result.newestMessageId ?? lastScannedMessageId;
394
+ * }
395
+ * ```
396
+ */ function discordScanMessagesFactory(config) {
397
+ var fetch = config.fetch, inputReadMessageId = config.readMessageId, defaults = config.defaults, inputNowFactory = config.nowFactory;
398
+ var readMessageId = inputReadMessageId !== null && inputReadMessageId !== void 0 ? inputReadMessageId : function(message) {
399
+ return message.id;
400
+ };
401
+ var nowFactory = inputNowFactory !== null && inputNowFactory !== void 0 ? inputNowFactory : function() {
402
+ return new Date();
403
+ };
404
+ return function(input) {
405
+ return _async_to_generator$2(function() {
406
+ var _ref, _input_messagesPerPage, _input_maxPages, _input_maxMessages, _input_maxDuration, _input_waitBetweenPages, baseInput, beforeMessageId, afterMessageId, filterMessages, handleMessages, messagesPerPage, maxPages, maxMessages, maxDuration, waitBetweenPages, fetchPageFactory, startedAt, totalPages, totalMessagesLoaded, totalMessagesHandled, newestMessageId, resumeBeforeMessageId, reachedStopBound, reachedChannelStart, reachedMaxMessages, reachedTimeBudget, pageInput, iterateResult, stopReason, result;
407
+ return _ts_generator$2(this, function(_state) {
408
+ switch(_state.label){
409
+ case 0:
410
+ baseInput = input.baseInput, beforeMessageId = input.beforeMessageId, afterMessageId = input.afterMessageId, filterMessages = input.filterMessages, handleMessages = input.handleMessages;
411
+ messagesPerPage = (_ref = (_input_messagesPerPage = input.messagesPerPage) !== null && _input_messagesPerPage !== void 0 ? _input_messagesPerPage : defaults === null || defaults === void 0 ? void 0 : defaults.messagesPerPage) !== null && _ref !== void 0 ? _ref : DEFAULT_DISCORD_MESSAGES_PER_PAGE;
412
+ maxPages = (_input_maxPages = input.maxPages) !== null && _input_maxPages !== void 0 ? _input_maxPages : defaults === null || defaults === void 0 ? void 0 : defaults.maxPages;
413
+ maxMessages = (_input_maxMessages = input.maxMessages) !== null && _input_maxMessages !== void 0 ? _input_maxMessages : defaults === null || defaults === void 0 ? void 0 : defaults.maxMessages;
414
+ maxDuration = (_input_maxDuration = input.maxDuration) !== null && _input_maxDuration !== void 0 ? _input_maxDuration : defaults === null || defaults === void 0 ? void 0 : defaults.maxDuration;
415
+ waitBetweenPages = (_input_waitBetweenPages = input.waitBetweenPages) !== null && _input_waitBetweenPages !== void 0 ? _input_waitBetweenPages : defaults === null || defaults === void 0 ? void 0 : defaults.waitBetweenPages;
416
+ // the page factory reads the same messagesPerPage everywhere, so the first page and every page
417
+ // after it use one limit and short-page detection stays honest
418
+ fetchPageFactory = discordFetchMessagePageFactory({
419
+ fetch: fetch,
420
+ config: {
421
+ readMessageId: readMessageId
422
+ },
423
+ defaults: {
424
+ defaultMaxItemsPerPage: messagesPerPage
425
+ }
426
+ });
427
+ startedAt = nowFactory();
428
+ totalPages = 0;
429
+ totalMessagesLoaded = 0;
430
+ totalMessagesHandled = 0;
431
+ reachedStopBound = false;
432
+ reachedChannelStart = false;
433
+ reachedMaxMessages = false;
434
+ reachedTimeBudget = false;
435
+ pageInput = _object_spread_props(_object_spread({}, baseInput), {
436
+ before: beforeMessageId !== null && beforeMessageId !== void 0 ? beforeMessageId : undefined,
437
+ after: undefined,
438
+ around: undefined,
439
+ limit: messagesPerPage
440
+ });
441
+ return [
442
+ 4,
443
+ iterateFetchPages({
444
+ input: pageInput,
445
+ fetchPageFactory: fetchPageFactory,
446
+ // maxPage is the max page INDEX, so a page count of N is an index of N - 1
447
+ maxPage: maxPages == null ? null : Math.max(0, maxPages - 1),
448
+ maxItemsPerPage: messagesPerPage,
449
+ maxParallelPages: 1,
450
+ waitBetweenPages: waitBetweenPages !== null && waitBetweenPages !== void 0 ? waitBetweenPages : undefined,
451
+ iteratePage: function iteratePage(fetchPageResult) {
452
+ return _async_to_generator$2(function() {
453
+ var raw, _ref, bounded, stopBoundIndex, messages;
454
+ return _ts_generator$2(this, function(_state) {
455
+ switch(_state.label){
456
+ case 0:
457
+ raw = fetchPageResult.result.data;
458
+ totalPages += 1;
459
+ totalMessagesLoaded += raw.length;
460
+ if (raw.length < messagesPerPage) {
461
+ reachedChannelStart = true; // a short page means there is nothing older to load
462
+ }
463
+ if (!(raw.length > 0)) return [
464
+ 3,
465
+ 3
466
+ ];
467
+ newestMessageId = newestMessageId !== null && newestMessageId !== void 0 ? newestMessageId : readMessageId(raw[0]);
468
+ // the OLDEST RAW message, so filtered-out messages are stepped past rather than revisited
469
+ resumeBeforeMessageId = readMessageId(lastValue(raw));
470
+ bounded = raw;
471
+ if (afterMessageId != null) {
472
+ stopBoundIndex = raw.findIndex(function(message) {
473
+ return !discordSnowflakeIsAfter(readMessageId(message), afterMessageId);
474
+ });
475
+ if (stopBoundIndex >= 0) {
476
+ reachedStopBound = true;
477
+ bounded = raw.slice(0, stopBoundIndex);
478
+ }
479
+ }
480
+ return [
481
+ 4,
482
+ filterMessages === null || filterMessages === void 0 ? void 0 : filterMessages(bounded)
483
+ ];
484
+ case 1:
485
+ messages = (_ref = _state.sent()) !== null && _ref !== void 0 ? _ref : bounded;
486
+ if (!(messages.length > 0)) return [
487
+ 3,
488
+ 3
489
+ ];
490
+ totalMessagesHandled += messages.length;
491
+ return [
492
+ 4,
493
+ handleMessages({
494
+ messages: messages,
495
+ newestMessageId: readMessageId(messages[0]),
496
+ oldestMessageId: readMessageId(lastValue(messages)),
497
+ page: fetchPageResult.page,
498
+ totalMessagesHandled: totalMessagesHandled
499
+ })
500
+ ];
501
+ case 2:
502
+ _state.sent();
503
+ _state.label = 3;
504
+ case 3:
505
+ if (maxMessages != null && totalMessagesLoaded >= maxMessages) {
506
+ reachedMaxMessages = true;
507
+ }
508
+ if (maxDuration != null && nowFactory().getTime() - startedAt.getTime() >= maxDuration) {
509
+ reachedTimeBudget = true;
510
+ }
511
+ return [
512
+ 2
513
+ ];
514
+ }
515
+ });
516
+ })();
517
+ },
518
+ // ordered by priority so the reported stopReason is deterministic. channel_start is what keeps
519
+ // the iteration from asking a cursor-based source for a page that does not exist.
520
+ endEarly: function endEarly() {
521
+ return reachedStopBound || reachedChannelStart || reachedMaxMessages || reachedTimeBudget;
522
+ }
523
+ })
524
+ ];
525
+ case 1:
526
+ iterateResult = _state.sent();
527
+ if (reachedStopBound) {
528
+ stopReason = 'stop_bound';
529
+ } else if (reachedChannelStart) {
530
+ stopReason = 'channel_start';
531
+ } else if (reachedMaxMessages) {
532
+ stopReason = 'max_messages';
533
+ } else if (reachedTimeBudget) {
534
+ stopReason = 'time_budget';
535
+ } else if (iterateResult.totalPagesLimitReached) {
536
+ stopReason = 'max_pages';
537
+ } else {
538
+ stopReason = 'channel_start'; // the page source reported it had no further pages
539
+ }
540
+ result = {
541
+ stopReason: stopReason,
542
+ complete: stopReason === 'stop_bound' || stopReason === 'channel_start',
543
+ newestMessageId: newestMessageId,
544
+ resumeBeforeMessageId: resumeBeforeMessageId,
545
+ totalPages: totalPages,
546
+ totalMessagesLoaded: totalMessagesLoaded,
547
+ totalMessagesHandled: totalMessagesHandled,
548
+ startedAt: startedAt,
549
+ endedAt: nowFactory()
550
+ };
551
+ return [
552
+ 2,
553
+ result
554
+ ];
555
+ }
556
+ });
557
+ })();
558
+ };
559
+ }
560
+
112
561
  /**
113
562
  * The Discord REST API base, pinned to a version.
114
563
  *
@@ -150,46 +599,191 @@ function _object_spread_props(target, source) {
150
599
  /**
151
600
  * The Discord OAuth2 token revocation endpoint path, relative to {@link DISCORD_API_URL}.
152
601
  */ var DISCORD_OAUTH_REVOKE_PATH = '/oauth2/token/revoke';
602
+ /**
603
+ * How this client authenticates itself at Discord's token and revocation endpoints.
604
+ *
605
+ * Discord's discovery document omits `token_endpoint_auth_methods_supported`, whose OIDC Discovery
606
+ * default is `client_secret_basic` — and Discord does in fact require Basic, rejecting the
607
+ * credentials-in-body form the OAuth relying-party layer otherwise defaults to.
608
+ */ var DISCORD_OAUTH_CLIENT_AUTH_METHOD = 'client_secret_basic';
609
+ /**
610
+ * Discord's OIDC issuer.
611
+ *
612
+ * `https://discord.com/.well-known/openid-configuration` resolves against it, though this package
613
+ * does not perform discovery — the endpoint paths above are stable, so the extra round trip buys
614
+ * nothing. Exported for consumers that do want to discover.
615
+ */ var DISCORD_OIDC_ISSUER = 'https://discord.com';
153
616
  /**
154
617
  * Path of the endpoint returning the user an access token belongs to.
155
618
  *
156
619
  * Requires the `identify` scope.
157
620
  */ var DISCORD_OAUTH_CURRENT_USER_PATH = '/users/@me';
158
621
 
622
+ function asyncGeneratorStep$1(gen, resolve, reject, _next, _throw, key, arg) {
623
+ try {
624
+ var info = gen[key](arg);
625
+ var value = info.value;
626
+ } catch (error) {
627
+ reject(error);
628
+ return;
629
+ }
630
+ if (info.done) resolve(value);
631
+ else Promise.resolve(value).then(_next, _throw);
632
+ }
633
+ function _async_to_generator$1(fn) {
634
+ return function() {
635
+ var self = this, args = arguments;
636
+ return new Promise(function(resolve, reject) {
637
+ var gen = fn.apply(self, args);
638
+ function _next(value) {
639
+ asyncGeneratorStep$1(gen, resolve, reject, _next, _throw, "next", value);
640
+ }
641
+ function _throw(err) {
642
+ asyncGeneratorStep$1(gen, resolve, reject, _next, _throw, "throw", err);
643
+ }
644
+ _next(undefined);
645
+ });
646
+ };
647
+ }
648
+ function _ts_generator$1(thisArg, body) {
649
+ var f, y, t, _ = {
650
+ label: 0,
651
+ sent: function() {
652
+ if (t[0] & 1) throw t[1];
653
+ return t[1];
654
+ },
655
+ trys: [],
656
+ ops: []
657
+ }, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype), d = Object.defineProperty;
658
+ return d(g, "next", {
659
+ value: verb(0)
660
+ }), d(g, "throw", {
661
+ value: verb(1)
662
+ }), d(g, "return", {
663
+ value: verb(2)
664
+ }), typeof Symbol === "function" && d(g, Symbol.iterator, {
665
+ value: function() {
666
+ return this;
667
+ }
668
+ }), g;
669
+ function verb(n) {
670
+ return function(v) {
671
+ return step([
672
+ n,
673
+ v
674
+ ]);
675
+ };
676
+ }
677
+ function step(op) {
678
+ if (f) throw new TypeError("Generator is already executing.");
679
+ while(g && (g = 0, op[0] && (_ = 0)), _)try {
680
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
681
+ if (y = 0, t) op = [
682
+ op[0] & 2,
683
+ t.value
684
+ ];
685
+ switch(op[0]){
686
+ case 0:
687
+ case 1:
688
+ t = op;
689
+ break;
690
+ case 4:
691
+ _.label++;
692
+ return {
693
+ value: op[1],
694
+ done: false
695
+ };
696
+ case 5:
697
+ _.label++;
698
+ y = op[1];
699
+ op = [
700
+ 0
701
+ ];
702
+ continue;
703
+ case 7:
704
+ op = _.ops.pop();
705
+ _.trys.pop();
706
+ continue;
707
+ default:
708
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {
709
+ _ = 0;
710
+ continue;
711
+ }
712
+ if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {
713
+ _.label = op[1];
714
+ break;
715
+ }
716
+ if (op[0] === 6 && _.label < t[1]) {
717
+ _.label = t[1];
718
+ t = op;
719
+ break;
720
+ }
721
+ if (t && _.label < t[2]) {
722
+ _.label = t[2];
723
+ _.ops.push(op);
724
+ break;
725
+ }
726
+ if (t[2]) _.ops.pop();
727
+ _.trys.pop();
728
+ continue;
729
+ }
730
+ op = body.call(thisArg, _);
731
+ } catch (e) {
732
+ op = [
733
+ 6,
734
+ e
735
+ ];
736
+ y = 0;
737
+ } finally{
738
+ f = t = 0;
739
+ }
740
+ if (op[0] & 5) throw op[1];
741
+ return {
742
+ value: op[0] ? op[1] : void 0,
743
+ done: true
744
+ };
745
+ }
746
+ }
159
747
  /**
160
748
  * The `Content-Type` Discord's token endpoint requires.
161
749
  *
162
750
  * Discord rejects a JSON body outright, unlike Cal.com, which requires one.
163
751
  *
164
- * `@dereekb/util/oidc`'s `postTokenEndpoint` is the in-workspace precedent for this form-encoded
165
- * shape and is deliberately NOT reused: its `exchangeAuthorizationCode` requires a PKCE
166
- * `code_verifier`, it authenticates with `client_secret_post` rather than Basic, and it is
167
- * discovery-driven. Discord is not an OIDC provider — there is no discovery document and no
168
- * `id_token`.
752
+ * Discord IS an OIDC provider, contrary to what this file previously claimed. Verified directly:
753
+ * `https://discord.com/.well-known/openid-configuration` returns 200 with every required discovery
754
+ * field, `https://discord.com/api/oauth2/keys` serves a JWKS, and the `openid` scope yields an
755
+ * `id_token` on the authorization-code grant. The relying-party calls below therefore delegate to
756
+ * `@dereekb/util/oidc`, passing `clientAuth: 'client_secret_basic'` (Discord's discovery document
757
+ * omits `token_endpoint_auth_methods_supported`, whose OIDC Discovery default is Basic — which is
758
+ * what Discord in fact requires).
759
+ *
760
+ * Discovery itself is not performed: the endpoints are stable, and skipping the extra round trip
761
+ * keeps the per-request cost the same as before.
169
762
  */ var DISCORD_OAUTH_TOKEN_CONTENT_TYPE = 'application/x-www-form-urlencoded';
170
763
  /**
171
764
  * Builds the HTTP Basic `Authorization` header value that authenticates the OAuth client.
172
765
  *
173
- * Discord accepts the client credentials as Basic auth rather than in the request body, which is why
766
+ * Discord requires the client credentials as Basic auth rather than in the request body, which is why
174
767
  * `client_id` / `client_secret` are absent from the exchange body below.
175
768
  *
176
- * Uses `btoa()` rather than `Buffer`, so this package stays usable outside Node — the same choice
177
- * `@dereekb/util`'s PKCE helpers make.
769
+ * A thin alias of the generic {@link oidcClientSecretBasicAuthorizationHeader}, kept because the
770
+ * configured fetch bakes the header into its `baseRequest` and so needs it as a value, not as a
771
+ * per-request auth mode.
178
772
  *
179
773
  * @param config - The client credentials to encode.
180
774
  * @returns The `Authorization` header value, including the `Basic ` prefix.
181
775
  *
182
776
  * @__NO_SIDE_EFFECTS__
183
777
  */ function discordOAuthBasicAuthorizationHeader(config) {
184
- var credentials = "".concat(config.clientId, ":").concat(config.clientSecret);
185
- return "Basic ".concat(btoa(credentials));
778
+ return oidcClientSecretBasicAuthorizationHeader(config);
186
779
  }
187
780
  /**
188
781
  * Exchanges an OAuth authorization code for access and refresh tokens.
189
782
  *
190
- * Discord requires `application/x-www-form-urlencoded` — a JSON body is rejected — and authenticates
191
- * the client with HTTP Basic rather than credentials in the body. Both differ from Cal.com, which
192
- * posts JSON with the credentials inline. The Basic header rides on the context's configured fetch.
783
+ * Delegates to `@dereekb/util/oidc`'s relying-party `exchangeAuthorizationCode` with
784
+ * `clientAuth: 'client_secret_basic'`, which produces exactly the form-encoded, Basic-authenticated
785
+ * request Discord requires. The context's configured fetch supplies the base URL and surfaces
786
+ * Discord's RFC-6749 error bodies as typed {@link DiscordOAuthError}s.
193
787
  *
194
788
  * @param context - The Discord OAuth context providing the authenticated fetch.
195
789
  * @returns Exchanges an authorization code for access and refresh tokens.
@@ -205,16 +799,33 @@ function _object_spread_props(target, source) {
205
799
  * ```
206
800
  */ function exchangeAuthorizationCode(context) {
207
801
  return function(input) {
208
- var body = new URLSearchParams({
209
- grant_type: 'authorization_code',
210
- code: input.code,
211
- redirect_uri: input.redirectUri
212
- });
213
- var fetchJsonInput = {
214
- method: 'POST',
215
- body: body.toString()
216
- };
217
- return context.fetchJson(DISCORD_OAUTH_TOKEN_PATH, fetchJsonInput);
802
+ return _async_to_generator$1(function() {
803
+ var response;
804
+ return _ts_generator$1(this, function(_state) {
805
+ switch(_state.label){
806
+ case 0:
807
+ return [
808
+ 4,
809
+ exchangeAuthorizationCode$1({
810
+ fetch: context.fetch,
811
+ tokenEndpoint: DISCORD_OAUTH_TOKEN_PATH,
812
+ clientId: context.config.clientId,
813
+ clientSecret: context.config.clientSecret,
814
+ clientAuth: DISCORD_OAUTH_CLIENT_AUTH_METHOD,
815
+ redirectUri: input.redirectUri,
816
+ code: input.code,
817
+ codeVerifier: input.codeVerifier
818
+ })
819
+ ];
820
+ case 1:
821
+ response = _state.sent();
822
+ return [
823
+ 2,
824
+ response
825
+ ];
826
+ }
827
+ });
828
+ })();
218
829
  };
219
830
  }
220
831
  /**
@@ -233,15 +844,55 @@ function _object_spread_props(target, source) {
233
844
  * @see https://docs.discord.com/developers/topics/oauth2
234
845
  */ function refreshAccessToken(context) {
235
846
  return function(input) {
236
- var body = new URLSearchParams({
237
- grant_type: 'refresh_token',
238
- refresh_token: input.refreshToken
847
+ return _async_to_generator$1(function() {
848
+ var response;
849
+ return _ts_generator$1(this, function(_state) {
850
+ switch(_state.label){
851
+ case 0:
852
+ return [
853
+ 4,
854
+ refreshAccessToken$1({
855
+ fetch: context.fetch,
856
+ tokenEndpoint: DISCORD_OAUTH_TOKEN_PATH,
857
+ clientId: context.config.clientId,
858
+ clientSecret: context.config.clientSecret,
859
+ clientAuth: DISCORD_OAUTH_CLIENT_AUTH_METHOD,
860
+ refreshToken: input.refreshToken
861
+ })
862
+ ];
863
+ case 1:
864
+ response = _state.sent();
865
+ return [
866
+ 2,
867
+ response
868
+ ];
869
+ }
870
+ });
871
+ })();
872
+ };
873
+ }
874
+ /**
875
+ * Revokes an access or refresh token, ending Discord's side of the authorization.
876
+ *
877
+ * Called when a user disconnects their Discord account: deleting the stored credentials alone leaves
878
+ * the grant live on Discord, so the token stays usable by anyone who captured it.
879
+ *
880
+ * @param context - The Discord OAuth context providing the authenticated fetch.
881
+ * @returns Revokes the given token.
882
+ *
883
+ * @see https://docs.discord.com/developers/topics/oauth2
884
+ */ function revokeToken(context) {
885
+ return function(input) {
886
+ var _input_tokenTypeHint;
887
+ return revokeToken$1({
888
+ fetch: context.fetch,
889
+ revocationEndpoint: DISCORD_OAUTH_REVOKE_PATH,
890
+ clientId: context.config.clientId,
891
+ clientSecret: context.config.clientSecret,
892
+ clientAuth: DISCORD_OAUTH_CLIENT_AUTH_METHOD,
893
+ token: input.token,
894
+ tokenTypeHint: (_input_tokenTypeHint = input.tokenTypeHint) !== null && _input_tokenTypeHint !== void 0 ? _input_tokenTypeHint : undefined
239
895
  });
240
- var fetchJsonInput = {
241
- method: 'POST',
242
- body: body.toString()
243
- };
244
- return context.fetchJson(DISCORD_OAUTH_TOKEN_PATH, fetchJsonInput);
245
896
  };
246
897
  }
247
898
  /**
@@ -274,11 +925,16 @@ function _object_spread_props(target, source) {
274
925
  * A runtime list rather than a bare type union, so a configured scope can be validated instead of
275
926
  * being passed through to the consent screen and refused there.
276
927
  *
277
- * Deliberately NOT Discord's full ~40-scope surface: only what a per-user account connect can
278
- * legitimately ask for. Add a scope here when code actually uses it.
928
+ * Deliberately NOT Discord's full ~40-scope surface: only what a per-user account connect or sign-in
929
+ * can legitimately ask for. Add a scope here when code actually uses it.
930
+ *
931
+ * `openid` is included because Discord is an OIDC provider (its discovery document and JWKS are
932
+ * live) and requesting it yields an `id_token`. Nothing in this workspace consumes that token —
933
+ * identity is read server-side from `/users/@me` — but the scope is legal to request.
279
934
  *
280
935
  * @see https://docs.discord.com/developers/topics/oauth2
281
936
  */ var ALL_DISCORD_OAUTH_SCOPES = [
937
+ 'openid',
282
938
  'identify',
283
939
  'email',
284
940
  'guilds',
@@ -301,6 +957,12 @@ function _object_spread_props(target, source) {
301
957
  /**
302
958
  * The `response_type` used by the authorization-code flow.
303
959
  */ var DISCORD_OAUTH_AUTHORIZE_RESPONSE_TYPE = 'code';
960
+ /**
961
+ * The only PKCE challenge method this package emits.
962
+ *
963
+ * `plain` is not offered: it provides no protection against an attacker who can read the
964
+ * authorization request, which is the threat PKCE exists to address.
965
+ */ var DISCORD_OAUTH_AUTHORIZE_CODE_CHALLENGE_METHOD = 'S256';
304
966
  /**
305
967
  * Creates a {@link DiscordOAuthAuthorizeUrlFactory} that composes the Discord authorize URL a user's
306
968
  * browser is redirected to in order to begin the authorization-code flow.
@@ -321,7 +983,7 @@ function _object_spread_props(target, source) {
321
983
  * scopes: ['identify']
322
984
  * });
323
985
  *
324
- * const url = authorizeUrlFactory({ state: 'signed-state' });
986
+ * const url = authorizeUrlFactory({ state: 'signed-state', codeChallenge: 's256-challenge' });
325
987
  * ```
326
988
  *
327
989
  * @__NO_SIDE_EFFECTS__
@@ -339,6 +1001,10 @@ function _object_spread_props(target, source) {
339
1001
  if (state != null) {
340
1002
  url.searchParams.set('state', state);
341
1003
  }
1004
+ if ((params === null || params === void 0 ? void 0 : params.codeChallenge) != null) {
1005
+ url.searchParams.set('code_challenge', params.codeChallenge);
1006
+ url.searchParams.set('code_challenge_method', DISCORD_OAUTH_AUTHORIZE_CODE_CHALLENGE_METHOD);
1007
+ }
342
1008
  return url.toString();
343
1009
  };
344
1010
  }
@@ -770,4 +1436,4 @@ var logDiscordOAuthErrorToConsole = logDiscordOAuthErrorFunction('DiscordOAuth')
770
1436
  };
771
1437
  }
772
1438
 
773
- export { ALL_DISCORD_OAUTH_SCOPES, DEFAULT_DISCORD_MESSAGES_PER_PAGE, DISCORD_API_URL, DISCORD_OAUTH_AUTHORIZE_RESPONSE_TYPE, DISCORD_OAUTH_AUTHORIZE_URL, DISCORD_OAUTH_CURRENT_USER_PATH, DISCORD_OAUTH_INVALID_GRANT_ERROR_CODE, DISCORD_OAUTH_INVALID_SCOPE_ERROR_CODE, DISCORD_OAUTH_REVOKE_PATH, DISCORD_OAUTH_SCOPE_DELIMITER, DISCORD_OAUTH_TOKEN_CONTENT_TYPE, DISCORD_OAUTH_TOKEN_PATH, DiscordOAuthError, DiscordOAuthFetchResponseError, discordAccessTokenFromTokenResponse, discordFetchMessagePageFactory, discordOAuthAuthorizeUrlFactory, discordOAuthBasicAuthorizationHeader, discordOAuthFactory, exchangeAuthorizationCode, handleDiscordOAuthErrorFetch, isDiscordOAuthScope, logDiscordOAuthErrorFunction, logDiscordOAuthErrorToConsole, parseDiscordOAuthError, readCurrentUser, refreshAccessToken };
1439
+ export { ALL_DISCORD_OAUTH_SCOPES, DEFAULT_DISCORD_MESSAGES_PER_PAGE, DISCORD_API_URL, DISCORD_EPOCH_MS, DISCORD_OAUTH_AUTHORIZE_CODE_CHALLENGE_METHOD, DISCORD_OAUTH_AUTHORIZE_RESPONSE_TYPE, DISCORD_OAUTH_AUTHORIZE_URL, DISCORD_OAUTH_CLIENT_AUTH_METHOD, DISCORD_OAUTH_CURRENT_USER_PATH, DISCORD_OAUTH_INVALID_GRANT_ERROR_CODE, DISCORD_OAUTH_INVALID_SCOPE_ERROR_CODE, DISCORD_OAUTH_REVOKE_PATH, DISCORD_OAUTH_SCOPE_DELIMITER, DISCORD_OAUTH_TOKEN_CONTENT_TYPE, DISCORD_OAUTH_TOKEN_PATH, DISCORD_OIDC_ISSUER, DISCORD_SNOWFLAKE_TIMESTAMP_SHIFT, DiscordOAuthError, DiscordOAuthFetchResponseError, compareDiscordSnowflakes, discordAccessTokenFromTokenResponse, discordFetchMessagePageFactory, discordOAuthAuthorizeUrlFactory, discordOAuthBasicAuthorizationHeader, discordOAuthFactory, discordScanMessagesFactory, discordSnowflakeForDate, discordSnowflakeIsAfter, discordSnowflakeToDate, exchangeAuthorizationCode, handleDiscordOAuthErrorFetch, isDiscordOAuthScope, logDiscordOAuthErrorFunction, logDiscordOAuthErrorToConsole, parseDiscordOAuthError, readCurrentUser, refreshAccessToken, revokeToken };