@seedcord/core 0.11.0 → 0.12.0-next.2

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 (54) hide show
  1. package/README.md +3 -3
  2. package/dist/Bus-VJw-QpoY.mjs +986 -0
  3. package/dist/Bus-VJw-QpoY.mjs.map +1 -0
  4. package/dist/{CoordinatedShutdown-H5YjZn8A.d.mts → CoordinatedShutdown-fPKLWjc5.d.mts} +13 -4
  5. package/dist/{CoordinatedStartup-CGnJrqcl.mjs → CoordinatedStartup-Zw_Lk7Zk.mjs} +22 -20
  6. package/dist/CoordinatedStartup-Zw_Lk7Zk.mjs.map +1 -0
  7. package/dist/Generated-ByC53_A2.d.mts +26 -0
  8. package/dist/HmrModuleHandler-Btgu8r96.mjs +165 -0
  9. package/dist/HmrModuleHandler-Btgu8r96.mjs.map +1 -0
  10. package/dist/{PluginHost-DB4j8unu.d.mts → PluginHost-DFnkIldj.d.mts} +43 -15
  11. package/dist/PluginHost-DTtxWRiL.mjs +552 -0
  12. package/dist/PluginHost-DTtxWRiL.mjs.map +1 -0
  13. package/dist/{PluginLifecycle-AhQ46NIr.mjs → PluginLifecycle-hx5zUc-P.mjs} +4 -4
  14. package/dist/PluginLifecycle-hx5zUc-P.mjs.map +1 -0
  15. package/dist/{ServerHost-DYeh0KTk.d.mts → ServerHost-B5o3ScCF.d.mts} +7 -6
  16. package/dist/{ServerHost-CtmAhEXn.mjs → ServerHost-CDaEDR4C.mjs} +17 -6
  17. package/dist/{ServerHost-CtmAhEXn.mjs.map → ServerHost-CDaEDR4C.mjs.map} +1 -1
  18. package/dist/bindViteEnv-NFzvdqlT.mjs +32 -0
  19. package/dist/{bindViteEnv-DWq5rftJ.mjs.map → bindViteEnv-NFzvdqlT.mjs.map} +1 -1
  20. package/dist/env-internal.index.mjs +1 -1
  21. package/dist/hmr.index.d.mts +20 -20
  22. package/dist/hmr.index.mjs +1 -1
  23. package/dist/{index-Du71Xx69.d.mts → index-DGHng1IN.d.mts} +115 -201
  24. package/dist/index.d.mts +321 -74
  25. package/dist/index.mjs +323 -102
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/internal.index.d.mts +223 -83
  28. package/dist/internal.index.mjs +573 -74
  29. package/dist/internal.index.mjs.map +1 -1
  30. package/dist/node-internal.index.d.mts +8 -4
  31. package/dist/node-internal.index.mjs +19 -6
  32. package/dist/node-internal.index.mjs.map +1 -1
  33. package/dist/node.index.d.mts +3 -3
  34. package/dist/node.index.mjs +22 -28
  35. package/dist/node.index.mjs.map +1 -1
  36. package/dist/{phases-FTM69J3a.d.mts → phases-C2QCSqRu.d.mts} +164 -55
  37. package/dist/plugin.index.d.mts +3 -2
  38. package/dist/plugin.index.mjs +1 -1
  39. package/dist/subscribers-CGuVjbrg.mjs +982 -0
  40. package/dist/subscribers-CGuVjbrg.mjs.map +1 -0
  41. package/dist/{bindViteEnv-DWq5rftJ.mjs → usingCtx-Cf2cZi96.mjs} +1 -31
  42. package/package.json +14 -11
  43. package/dist/CoordinatedStartup-CGnJrqcl.mjs.map +0 -1
  44. package/dist/HmrModuleHandler-8ZC-UuZh.mjs +0 -162
  45. package/dist/HmrModuleHandler-8ZC-UuZh.mjs.map +0 -1
  46. package/dist/PluginHost-B8kxLOeb.mjs +0 -245
  47. package/dist/PluginHost-B8kxLOeb.mjs.map +0 -1
  48. package/dist/PluginLifecycle-AhQ46NIr.mjs.map +0 -1
  49. package/dist/phases-BauKJOTT.mjs +0 -31
  50. package/dist/phases-BauKJOTT.mjs.map +0 -1
  51. package/dist/publishDefault-D3dAyzZ9.mjs +0 -7
  52. package/dist/publishDefault-D3dAyzZ9.mjs.map +0 -1
  53. package/dist/subscribers-Ce3SWy23.mjs +0 -1913
  54. package/dist/subscribers-Ce3SWy23.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,13 +1,13 @@
1
- import { $ as RegisterInteractionMiddleware, A as rollbackCommits, B as NotInGuild, D as pickNotice, E as presentNames, F as MissingPermissions, G as serializeReply, H as OnCooldown, Ht as Notice, J as publishResponse, K as translateSerializationError, L as NeedsAny, Lt as NoticeCard, P as HasDangerousPermissions, Q as sendTarget, R as NotAllowed, Rt as BuilderComponent, S as RequireRole, T as missingNames, U as defineEffectGate, V as NotOwner, W as defineGate, X as AckTrace, Z as checkAckLegality, _ as pageCursor, b as RequireBotPermissions, bt as Commands, ct as RegisterCommand, d as WebhookLog, f as Subscriber, g as PAGE_MAX, it as storeInteractionRoute, j as runCheck, k as markCommits, l as WebhookUrl, pt as InteractionKind, q as attemptWrite, rt as storeComponentRoute, t as Bus, tt as contextMenuRouteOf, u as Subscribe, v as ResolvedEmoji, w as hasAll, x as RequirePermissions, xt as ContextMenus, y as Silence, z as NotInDm, zt as RowComponent } from "./subscribers-Ce3SWy23.mjs";
2
- import { n as StartupPhase, t as ShutdownPhase } from "./phases-BauKJOTT.mjs";
1
+ import { D as InteractionKind, N as BuilderComponent, P as RowComponent, b as StartupPhase, d as WebhookLog, f as Subscriber, l as WebhookUrl, t as Bus, u as Subscribe, v as MemoryDriver, y as ShutdownPhase } from "./Bus-VJw-QpoY.mjs";
2
+ import { A as RegisterInteractionMiddleware, B as RegisterCommand, C as attemptWrite, D as AckTrace, F as storeComponentRoute, H as Commands, I as storeInteractionRoute, M as contextMenuRouteOf, O as checkAckLegality, S as translateSerializationError, U as ContextMenus, _ as NotInGuild, at as Notice, b as PermissionsUnresolved, c as runCheck, d as HasPresentPermissions, f as MissingPermissions, g as NotInDm, h as NotAllowed, i as Silence, k as sendTarget, m as NeedsAny, n as pageCursor, o as markCommits, ot as NoticeCard, p as MissingRole, r as ResolvedEmoji, s as rollbackCommits, t as PAGE_MAX, u as Fault, v as NotOwner, w as publishResponse, x as serializeReply, y as OnCooldown } from "./subscribers-CGuVjbrg.mjs";
3
3
  import "reflect-metadata";
4
4
  import { CustomId, decodeFor, prefixOf, setCustomIdErrors, setCustomIdErrors as setCustomIdErrors$1 } from "@seedcord/custom-id";
5
5
  import { ActionRowBuilder, ButtonBuilder, TextDisplayBuilder } from "@discordjs/builders";
6
- import { ApplicationCommandType, ButtonStyle } from "discord-api-types/v10";
6
+ import { ApplicationCommandType, ButtonStyle, PermissionFlagsBits } from "discord-api-types/v10";
7
7
  import { SeedcordErrorCode } from "@seedcord/errors";
8
8
  import { SeedcordError, SeedcordRangeError, SeedcordTypeError } from "@seedcord/errors/internal";
9
9
  import { Logger } from "@seedcord/logger";
10
- import { parseDuration } from "@seedcord/utils";
10
+ import { parseDuration, prettify } from "@seedcord/utils";
11
11
 
12
12
  //#region src/customId/Errors.ts
13
13
  /**
@@ -47,8 +47,8 @@ setCustomIdErrors$1({
47
47
  /**
48
48
  * Routes one or more slash commands to a `SlashHandler`.
49
49
  *
50
- * Pass the same route string(s) as the handler's generic. A single command reads `this.options`, several
51
- * commands branch with `this.match`. The decorator routes and the generic must match exactly. Listing
50
+ * Pass the same route string(s) as the handler's generic. On a handler for one command, read
51
+ * `this.options`. On a handler for several, branch with `this.match`. The decorator routes and the generic must match exactly. Listing
52
52
  * fewer or more than the handler declares is a compile error. Subcommands use a slash path.
53
53
  *
54
54
  * @param routes - The route string(s) this handler serves, e.g. `'ban'`, `'ban', 'kick'`, or `'demo/setup'`.
@@ -72,7 +72,7 @@ function SlashRoute(...routes) {
72
72
  * Routes one or more commands' autocomplete to an `AutocompleteHandler`.
73
73
  *
74
74
  * Pass the same command route string(s) as the handler's generic. One command branches with `this.match`
75
- * over its autocompletable fields, several commands share one handler whose arms span every command's
75
+ * over its autocompletable fields, several commands share one handler whose cases span every command's
76
76
  * fields. Listing fewer or more commands than the handler declares is a compile error, since the
77
77
  * decorator routes and the generic must match exactly.
78
78
  *
@@ -174,7 +174,7 @@ function ModalRoute(...defs) {
174
174
  * Routes string select menu interactions to a `StringMenuHandler`.
175
175
  *
176
176
  * Pass the {@link CustomId} definition(s) this handler decodes and list the same ones in the handler's
177
- * generic. The handler reads the chosen option values from `this.values`.
177
+ * generic. Read the chosen option values from `this.values`.
178
178
  *
179
179
  * @param defs - The customId definition(s) this handler decodes, one per route.
180
180
  * @decorator
@@ -289,38 +289,6 @@ function MentionableMenuRoute(...defs) {
289
289
  };
290
290
  }
291
291
 
292
- //#endregion
293
- //#region src/dispatch/DispatchContext.ts
294
- /**
295
- * A typed bag allocated once per interaction dispatch and handed to the handler. The middleware and i18n
296
- * work reads and writes it through {@link DispatchState}. Today the bag is empty, so it only carries the
297
- * dispatched `routeId`.
298
- */
299
- var DispatchContext = class {
300
- routeId;
301
- state = Object.create(null);
302
- id = crypto.randomUUID();
303
- constructor(routeId) {
304
- this.routeId = routeId;
305
- }
306
- set(key, value) {
307
- this.state[key] = value;
308
- }
309
- get(key) {
310
- return this.state[key];
311
- }
312
- /**
313
- * Read a key that must be present, with `undefined` stripped from the result.
314
- *
315
- * @throws A **SeedcordError** when nothing set the key on this dispatch.
316
- */
317
- require(key) {
318
- const value = this.state[key];
319
- if (value === void 0) throw new SeedcordError(SeedcordErrorCode.DispatchStateMissing, [String(key)]);
320
- return value;
321
- }
322
- };
323
-
324
292
  //#endregion
325
293
  //#region src/handlers/BaseHandler.ts
326
294
  /**
@@ -336,11 +304,14 @@ var BaseHandler = class {
336
304
  logger;
337
305
  /** The bag for this dispatch. Each interaction gets its own. Every handler for one event shares a single bag. */
338
306
  dispatch;
307
+ /** The route that started this dispatch. Each handler base narrows it to its own routes. */
308
+ route;
339
309
  constructor(event, core, dispatch, channel) {
340
310
  this.core = core;
341
311
  this.event = event;
342
312
  this.logger = new Logger(this.constructor.name, { channel });
343
313
  this.dispatch = dispatch;
314
+ this.route = dispatch.route;
344
315
  }
345
316
  };
346
317
 
@@ -348,7 +319,7 @@ var BaseHandler = class {
348
319
  //#region src/handlers/RepliableHandler.ts
349
320
  /**
350
321
  * Shared base the repliable handlers on both transports extend. Defines the reply members over one sender.
351
- * A handler builds its own through `buildSender`. A middleware takes the handler's.
322
+ * The dispatch run builds that sender and passes it to the handler and each middleware.
352
323
  *
353
324
  * @typeParam Event - The repliable interaction type this handler processes
354
325
  * @typeParam TCore - The transport's Core
@@ -359,11 +330,9 @@ var BaseHandler = class {
359
330
  var RepliableHandler = class extends BaseHandler {
360
331
  /** The reply surface for this interaction. Pass the handler to reply from outside its class. */
361
332
  sender;
362
- routeId;
363
333
  constructor(event, core, dispatch, sender) {
364
334
  super(event, core, dispatch, "interactions");
365
- this.routeId = dispatch.routeId;
366
- this.sender = sender ?? this.buildSender(event, core, dispatch);
335
+ this.sender = sender;
367
336
  }
368
337
  /** Send the initial response, exactly once. */
369
338
  reply(response, opts) {
@@ -559,44 +528,88 @@ var BaseReplySender = class {
559
528
  };
560
529
 
561
530
  //#endregion
562
- //#region src/stops/Fault.ts
531
+ //#region src/gates/Gate.ts
563
532
  /**
564
- * A generic fault you throw after catching an error you do not have a specific message for.
533
+ * Builds a {@link Gate} from a check. The check refuses by throwing a Notice or a Silence and passes
534
+ * by returning. The required context is inferred from `ctx`, so annotate it as narrowly as the fields
535
+ * the check reads. No annotation defaults to {@link GateContextBase}, an agnostic gate that fits every
536
+ * handler.
565
537
  *
566
- * The user sees a fixed generic reply with the tracking uuid, never the cause. `report` defaults to
567
- * `true`, so the framework logs it and publishes it to the `handledException` bus. Pass `report: false`
568
- * to show the generic reply without the bus publish. The original error is stored as the standard
569
- * `cause`, so the real stack reaches the webhook. When the user should see a real message for a fault,
570
- * subclass {@link Notice} and write your own `render` instead.
538
+ * @typeParam Name - The gate's name, captured as a literal for mismatch errors.
539
+ * @typeParam Ctx - The context the check reads, inferred from the `ctx` annotation.
571
540
  *
572
- * The framework also renders this for an unhandled throw, where it points the user at
573
- * `ctx.developerUsername`.
541
+ * @param name - The gate's name, used in mismatch errors.
542
+ * @param fn - The check, which refuses by throwing a Notice or a Silence and passes by returning.
543
+ *
544
+ * @see {@link defineEffectGate}
545
+ * @see the `@Gated` decorator from your transport package
574
546
  *
575
547
  * @example
576
548
  * ```ts
577
- * import { Fault } from '@seedcord/gateway';
549
+ * // a gate factory that takes its own option and closes over it
550
+ * function MinRoles(min: number): Gate<GateContextBase, 'MinRoles'> {
551
+ * return defineGate('MinRoles', (ctx) => {
552
+ * if (ctx.memberRoleIds.length < min) throw new NotEnoughRoles(min);
553
+ * });
554
+ * }
555
+ * ```
578
556
  *
579
- * try {
580
- * await db.write(record);
581
- * } catch (cause) {
582
- * // user sees the generic reply with the uuid, and the real error rides along as cause for the webhook
583
- * throw new Fault({ cause });
557
+ * @example
558
+ * ```ts
559
+ * // agnostic gate (no ctx annotation): fits any handler
560
+ * const Owner = defineGate('owner', (ctx) => {
561
+ * if (!ctx.userId) throw new NotOwner();
562
+ * });
563
+ * ```
564
+ */
565
+ function defineGate(name, fn) {
566
+ return {
567
+ name,
568
+ check: async (ctx) => fn(ctx)
569
+ };
570
+ }
571
+ /**
572
+ * Builds an {@link EffectGate} from a `check` and a `commit`. `check` peeks and refuses by throwing, and
573
+ * `commit` applies the side effect, running only once the whole gate set passes so a later refusal never
574
+ * commits. In an `or`, seedcord rolls back the commit a refusing gate queued. This is how {@link Cooldown} peeks
575
+ * in `check` and charges the slot in `commit`.
576
+ *
577
+ * @typeParam Name - The gate's literal name, used to name a mismatch error.
578
+ * @typeParam Ctx - The context both `check` and `commit` read, inferred from the `ctx` annotation.
579
+ *
580
+ * @param name - The gate's name, used in mismatch errors.
581
+ * @param check - Peeks and refuses by throwing, without applying the side effect.
582
+ * @param commit - Applies the side effect, running only after the whole gate set passes.
584
583
  *
585
- * // pass report: false to show the same reply without publishing to the handledException bus
586
- * // throw new Fault({ cause, report: false });
584
+ * @see {@link defineGate}
585
+ * @see the `@Gated` decorator from your transport package
586
+ * @see {@link Cooldown}
587
+ *
588
+ * @example
589
+ * ```ts
590
+ * // an effect-gate factory that takes its own limit and closes over it
591
+ * function UsesPerDay(max: number): EffectGate<GateContextBase, 'UsesPerDay'> {
592
+ * return defineEffectGate(
593
+ * 'UsesPerDay',
594
+ * (ctx) => {
595
+ * // peek and refuse by throwing. do not mutate yet
596
+ * if (usedToday(ctx.userId) >= max) throw new OutOfUsesNotice(max);
597
+ * },
598
+ * (ctx) => {
599
+ * // runs only after the whole gate set passes
600
+ * recordUse(ctx.userId);
601
+ * }
602
+ * );
587
603
  * }
588
604
  * ```
589
605
  */
590
- var Fault = class extends Notice {
591
- constructor(options) {
592
- super("A fault occurred", options?.cause === void 0 ? void 0 : { cause: options.cause });
593
- this.report = options?.report ?? true;
594
- }
595
- render(ctx) {
596
- const contact = ctx.developerUsername ?? "the developer";
597
- return { components: [new NoticeCard(`Something went wrong. Please reach out to ${contact} with a way to reproduce the error and the following:\n### UUID: \`${ctx.uuid}\``, "Error").component] };
598
- }
599
- };
606
+ function defineEffectGate(name, check, commit) {
607
+ return {
608
+ name,
609
+ check: async (ctx) => check(ctx),
610
+ commit: async (ctx) => commit(ctx)
611
+ };
612
+ }
600
613
 
601
614
  //#endregion
602
615
  //#region src/gates/combinators.ts
@@ -645,9 +658,9 @@ function or(...args) {
645
658
  /**
646
659
  * Passes only for a user id listed in `config.ownerIds`, else refuses.
647
660
  *
648
- * Agnostic, so it attaches to interaction and event handlers alike. With no `ownerIds` configured it refuses every caller. Pass {@link GateNoticeOptions} to reword or replace the refusal.
661
+ * Agnostic, so it attaches to interaction and event handlers alike. With no `ownerIds` configured it refuses every caller. Pass a Notice class as `notice` to replace the refusal.
649
662
  *
650
- * @param options - Reword the default refusal with `message`, or replace it with `notice`.
663
+ * @param options - Replace the refusal with a Notice class as `notice`.
651
664
  *
652
665
  * @see the `@Gated` decorator from your transport package
653
666
  *
@@ -664,15 +677,15 @@ function or(...args) {
664
677
  *
665
678
  * @example
666
679
  * ```ts
667
- * // reword the refusal, keeping the embed styling
668
- * OwnerOnly({ message: 'Owners only.' });
680
+ * // OwnersOnly is your own Notice class
681
+ * OwnerOnly({ notice: OwnersOnly });
669
682
  * ```
670
683
  */
671
684
  function OwnerOnly(options) {
672
685
  return defineGate("OwnerOnly", (ctx) => {
673
686
  const owners = ctx.core.config.ownerIds ?? [];
674
687
  if (ctx.userId !== null && owners.includes(ctx.userId)) return;
675
- throw pickNotice(options, (message) => new NotOwner(message));
688
+ throw new ((options?.notice) ?? NotOwner)();
676
689
  });
677
690
  }
678
691
  /**
@@ -680,7 +693,7 @@ function OwnerOnly(options) {
680
693
  *
681
694
  * Agnostic. Attaches to any handler kind. Often paired with `RequirePermissions` in an {@link and}.
682
695
  *
683
- * @param options - Reword the default refusal with `message`, or replace it with `notice`.
696
+ * @param options - Replace the refusal with a Notice class as `notice`.
684
697
  *
685
698
  * @see the `@Gated` decorator from your transport package
686
699
  *
@@ -700,7 +713,7 @@ function OwnerOnly(options) {
700
713
  function GuildOnly(options) {
701
714
  return defineGate("GuildOnly", (ctx) => {
702
715
  if (ctx.guildId) return;
703
- throw pickNotice(options, (message) => new NotInGuild(message));
716
+ throw new ((options?.notice) ?? NotInGuild)();
704
717
  });
705
718
  }
706
719
  /**
@@ -708,7 +721,7 @@ function GuildOnly(options) {
708
721
  *
709
722
  * Agnostic, fitting any handler kind. The inverse of {@link GuildOnly}, so combining the two in an {@link and} can never pass.
710
723
  *
711
- * @param options - Reword the default refusal with `message`, or replace it with `notice`.
724
+ * @param options - Replace the refusal with a Notice class as `notice`.
712
725
  *
713
726
  * @see the `@Gated` decorator from your transport package
714
727
  *
@@ -726,7 +739,7 @@ function GuildOnly(options) {
726
739
  function DmOnly(options) {
727
740
  return defineGate("DmOnly", (ctx) => {
728
741
  if (!ctx.guildId) return;
729
- throw pickNotice(options, (message) => new NotInDm(message));
742
+ throw new ((options?.notice) ?? NotInDm)();
730
743
  });
731
744
  }
732
745
 
@@ -744,10 +757,10 @@ function scopeValue(ctx, per) {
744
757
  * so a later refusal never burns the cooldown.
745
758
  * That split also means two requests racing the same key can both pass before either charges the slot.
746
759
  * The key combines the handler's route with the window settings, keeping two different handlers on separate
747
- * windows. Reword the refusal with {@link CooldownOptions.message} or replace it with {@link CooldownOptions.notice}.
760
+ * windows. Replace the refusal with {@link CooldownOptions.notice}.
748
761
  *
749
762
  * @param duration - A number is seconds, and a string is a duration like `30m` or `24h`.
750
- * @param options - Sets the scope with `per`, the uses per window with `limit`, and the refusal text with `message` or `notice`.
763
+ * @param options - Sets the scope with `per`, the uses per window with `limit`, and the refusal with `notice`.
751
764
  * @throws A **SeedcordTypeError** when `duration` is a string that doesn't parse.
752
765
  * @throws A **SeedcordRangeError** when `duration` is zero or negative.
753
766
  *
@@ -799,22 +812,46 @@ function Cooldown(duration, options) {
799
812
  limit: options.limit
800
813
  };
801
814
  const anonId = anonSeq++;
802
- const keyOf = (ctx) => `cooldown:${ctx.declaredRoute ?? `anon${anonId}`}:${per}:w${windowMs}:l${options?.limit ?? 1}:${scopeValue(ctx, per)}`;
815
+ const keyOf = (ctx) => `cooldown:${ctx.dispatch.declaredRoute ?? `anon${anonId}`}:${per}:w${windowMs}:l${options?.limit ?? 1}:${scopeValue(ctx, per)}`;
803
816
  return defineEffectGate("Cooldown", async (ctx) => {
804
817
  const result = await ctx.core.rateLimiter.peek(keyOf(ctx), window);
805
818
  if (!result.limited) return;
806
- if (options?.notice) throw options.notice(result.resetAt);
807
- throw new OnCooldown(result.resetAt, options?.message?.(result.resetAt));
819
+ throw new ((options?.notice) ?? OnCooldown)(result.resetAt);
808
820
  }, async (ctx) => {
809
821
  await ctx.core.rateLimiter.charge(keyOf(ctx), window);
810
822
  });
811
823
  }
812
824
 
825
+ //#endregion
826
+ //#region src/permissions/bits.ts
827
+ const PermissionNames = new Map(Object.entries(PermissionFlagsBits).map(([key, bit]) => [bit, prettify(key)]));
828
+ const ADMINISTRATOR = PermissionFlagsBits.Administrator;
829
+ function nameOf(bit) {
830
+ return PermissionNames.get(bit) ?? String(bit);
831
+ }
832
+ function holds(held, bit) {
833
+ return (held & ADMINISTRATOR) === ADMINISTRATOR || (held & bit) === bit;
834
+ }
835
+ function namesWhere(held, scope, isHeld) {
836
+ return scope.reduce((names, bit) => {
837
+ if (holds(held, bit) === isHeld) names.push(nameOf(bit));
838
+ return names;
839
+ }, []);
840
+ }
841
+ /** @internal */
842
+ function missingNames(held, scope) {
843
+ return namesWhere(held, scope, false);
844
+ }
845
+ /** @internal */
846
+ function presentNames(held, scope) {
847
+ return namesWhere(held, scope, true);
848
+ }
849
+
813
850
  //#endregion
814
851
  //#region src/permissions/assert.ts
815
852
  /**
816
853
  * Refuses when the subject is missing a permission in `scope`, or with `inverse` when it holds one.
817
- * The Administrator bit passes any scope, and on the inverse arm it reports every bit in `scope`.
854
+ * The Administrator bit passes any scope, and with `inverse` it reports every bit in `scope`.
818
855
  *
819
856
  * The caller computes `permissions`, gateway off a discord.js role, member, or channel, and HTTP off the
820
857
  * bitfield the interaction payload carries.
@@ -859,14 +896,12 @@ function Cooldown(duration, options) {
859
896
  * ```ts
860
897
  * import { Notice } from '@seedcord/core';
861
898
  *
862
- * // a replacement notice takes the same three arguments the default one does
863
899
  * class CannotBan extends Notice {
864
900
  * public constructor(
865
- * message: string | undefined,
866
- * private readonly subject: string,
901
+ * private readonly subject: string | null,
867
902
  * private readonly missing: readonly string[]
868
903
  * ) {
869
- * super(message ?? 'You cannot ban here.');
904
+ * super('You cannot ban here.');
870
905
  * }
871
906
  *
872
907
  * public render(): ReplyResponse {
@@ -878,19 +913,97 @@ function Cooldown(duration, options) {
878
913
  * subject: `<@${member.id}>`,
879
914
  * permissions: member.permissions.bitfield,
880
915
  * scope: [PermissionFlagsBits.BanMembers],
881
- * missingNotice: CannotBan
916
+ * notice: CannotBan
882
917
  * });
883
918
  * ```
884
919
  */
885
- function assertPermissions(check) {
886
- const { subject, permissions, scope, inverse = false, missingNotice, dangerousNotice } = check;
887
- if (inverse) {
888
- const present = presentNames(permissions, scope);
889
- if (present.length === 0) return;
890
- throw new (dangerousNotice ?? HasDangerousPermissions)(void 0, subject, present);
891
- }
892
- if (hasAll(permissions, scope)) return;
893
- throw new (missingNotice ?? MissingPermissions)(void 0, subject, missingNames(permissions, scope));
920
+ function assertPermissions({ subject, permissions, scope, inverse = false, notice }) {
921
+ const [refused, Default] = inverse ? [presentNames(permissions, scope), HasPresentPermissions] : [missingNames(permissions, scope), MissingPermissions];
922
+ if (refused.length === 0) return;
923
+ throw new (notice ?? Default)(subject, refused);
924
+ }
925
+
926
+ //#endregion
927
+ //#region src/gates/catalog/permissions.ts
928
+ const BOT_SUBJECT = "The bot";
929
+ function requireHeld({ guildId, subject, readOrThrow }, scope, options) {
930
+ if (!guildId) throw new ((options?.notInGuild) ?? NotInGuild)();
931
+ const permissions = readOrThrow(guildId);
932
+ assertPermissions({
933
+ subject,
934
+ permissions,
935
+ scope,
936
+ notice: options?.missing
937
+ });
938
+ }
939
+ function botMemberMissing(guildId) {
940
+ throw new SeedcordError(SeedcordErrorCode.CoreBotMemberMissing, [guildId]);
941
+ }
942
+ function RequirePermissions(scope, options) {
943
+ const resolved = (held) => {
944
+ if (held === null) throw new ((options?.unresolved) ?? PermissionsUnresolved)();
945
+ return held;
946
+ };
947
+ if (options?.in === "guild") return defineGate("RequirePermissions", (ctx) => {
948
+ const readOrThrow = () => resolved(ctx.memberGuildPermissions);
949
+ requireHeld({
950
+ guildId: ctx.guildId,
951
+ subject: null,
952
+ readOrThrow
953
+ }, scope, options);
954
+ });
955
+ return defineGate("RequirePermissions", (ctx) => {
956
+ const readOrThrow = () => resolved(ctx.memberPermissions);
957
+ requireHeld({
958
+ guildId: ctx.guildId,
959
+ subject: null,
960
+ readOrThrow
961
+ }, scope, options);
962
+ });
963
+ }
964
+ function RequireBotPermissions(scope, options) {
965
+ if (options?.in === "guild") return defineGate("RequireBotPermissions", (ctx) => {
966
+ const readOrThrow = (guildId) => ctx.appGuildPermissions ?? botMemberMissing(guildId);
967
+ requireHeld({
968
+ guildId: ctx.guildId,
969
+ subject: BOT_SUBJECT,
970
+ readOrThrow
971
+ }, scope, options);
972
+ });
973
+ return defineGate("RequireBotPermissions", (ctx) => {
974
+ requireHeld({
975
+ guildId: ctx.guildId,
976
+ subject: BOT_SUBJECT,
977
+ readOrThrow: () => ctx.appPermissions
978
+ }, scope, options);
979
+ });
980
+ }
981
+ /**
982
+ * Requires the caller to hold `roleId`, read from the member's role ids. Refuses outside a guild first,
983
+ * then when the role is missing. Roles are guild-global, so there is no scope dimension.
984
+ *
985
+ * @param roleId - The role snowflake the caller must hold.
986
+ * @param options - Replace a refusal with a Notice class as `notInGuild` or `missingRole`.
987
+ *
988
+ * @see the `@Gated` decorator from your transport package
989
+ *
990
+ * @example
991
+ * ```ts
992
+ * \@Gated(RequireRole('123456789012345678'))
993
+ * \@SlashRoute('vip')
994
+ * class VipHandler extends SlashHandler<'vip'> {
995
+ * async execute() {
996
+ * // ...
997
+ * }
998
+ * }
999
+ * ```
1000
+ */
1001
+ function RequireRole(roleId, options) {
1002
+ return defineGate("RequireRole", (ctx) => {
1003
+ if (!ctx.guildId) throw new ((options?.notInGuild) ?? NotInGuild)();
1004
+ if (ctx.memberRoleIds.includes(roleId)) return;
1005
+ throw new ((options?.missingRole) ?? MissingRole)(roleId);
1006
+ });
894
1007
  }
895
1008
 
896
1009
  //#endregion
@@ -1146,11 +1259,119 @@ var CursorSourceBase = class {
1146
1259
  }
1147
1260
  };
1148
1261
 
1262
+ //#endregion
1263
+ //#region src/kv/KeyvDriver.ts
1264
+ /**
1265
+ * Wraps a Keyv instance for `config.kv`.
1266
+ *
1267
+ * @example
1268
+ * ```ts
1269
+ * new Seedcord({ ...config, kv: new KeyvDriver(new Keyv(new KeyvSqlite('sqlite://./kv.sqlite'))) });
1270
+ * ```
1271
+ */
1272
+ var KeyvDriver = class {
1273
+ keyv;
1274
+ constructor(keyv) {
1275
+ this.keyv = keyv;
1276
+ }
1277
+ async get(key) {
1278
+ const value = await this.keyv.get(key);
1279
+ return typeof value === "string" ? value : void 0;
1280
+ }
1281
+ async set(key, value, ttlMs) {
1282
+ if (!await this.keyv.set(key, value, ttlMs)) throw new SeedcordError(SeedcordErrorCode.CoreKvDriverFailed, ["write", key]);
1283
+ }
1284
+ async delete(key) {
1285
+ await this.keyv.delete(key);
1286
+ }
1287
+ /** @throws A **SeedcordError** when the Keyv store can't list its keys, like `@keyv/memcache`. */
1288
+ async *entries(prefix) {
1289
+ if (!this.#canListKeys()) throw new SeedcordError(SeedcordErrorCode.CoreKvKeysUnsupported);
1290
+ for await (const [key, value] of this.keyv.iterator()) if (key.startsWith(prefix) && typeof value === "string") yield [key, value];
1291
+ }
1292
+ async [Symbol.asyncDispose]() {
1293
+ await this.keyv.disconnect();
1294
+ }
1295
+ #canListKeys() {
1296
+ const capabilities = this.keyv.store.capabilities;
1297
+ return capabilities?.store === "mapLike" || capabilities?.methods.iterator.exists === true;
1298
+ }
1299
+ };
1300
+
1301
+ //#endregion
1302
+ //#region src/kv/UnstorageDriver.ts
1303
+ const MIN_TTL_SECONDS = 60;
1304
+ const MS_PER_SECOND = 1e3;
1305
+ function storageKey(key) {
1306
+ return `_${encodeURIComponent(key).replaceAll(".", "%2E")}`;
1307
+ }
1308
+ function originalKey(stored) {
1309
+ return decodeURIComponent(stored.slice(1));
1310
+ }
1311
+ function text(raw) {
1312
+ if (raw === null) return void 0;
1313
+ return typeof raw === "string" ? raw : new TextDecoder().decode(raw);
1314
+ }
1315
+ /**
1316
+ * Wraps an unstorage `Storage` for `config.kv`.
1317
+ *
1318
+ * @example
1319
+ * ```ts
1320
+ * new Seedcord({ ...config, kv: new UnstorageDriver(createStorage({ driver: fsDriver({ base: './.kv' }) })) });
1321
+ * ```
1322
+ */
1323
+ var UnstorageDriver = class {
1324
+ storage;
1325
+ constructor(storage) {
1326
+ this.storage = storage;
1327
+ }
1328
+ async get(key) {
1329
+ return text(await this.storage.getItemRaw(storageKey(key)));
1330
+ }
1331
+ async set(key, value, ttlMs) {
1332
+ const ttl = ttlMs === void 0 ? {} : { ttl: Math.max(MIN_TTL_SECONDS, Math.ceil(ttlMs / MS_PER_SECOND)) };
1333
+ await this.storage.setItemRaw(storageKey(key), value, ttl);
1334
+ }
1335
+ async delete(key) {
1336
+ await this.storage.removeItem(storageKey(key));
1337
+ }
1338
+ async *entries(prefix) {
1339
+ const storedPrefix = storageKey(prefix);
1340
+ for (const key of await this.storage.getKeys()) {
1341
+ if (!key.startsWith(storedPrefix)) continue;
1342
+ const value = text(await this.storage.getItemRaw(key));
1343
+ if (value !== void 0) yield [originalKey(key), value];
1344
+ }
1345
+ }
1346
+ async [Symbol.asyncDispose]() {
1347
+ await this.storage.dispose();
1348
+ }
1349
+ };
1350
+
1351
+ //#endregion
1352
+ //#region src/rateLimiter/buildKey.ts
1353
+ /**
1354
+ * Builds a rate-limit key from a prefix and scope parts, joined with `:`. A null or undefined part
1355
+ * becomes `'global'`. A key built from a DM's guild id reads `<prefix>:global` for every DM caller.
1356
+ *
1357
+ * @param prefix - The bucket family, e.g. `'cooldown'`.
1358
+ * @param parts - Scope values, e.g. a user or guild id.
1359
+ *
1360
+ * @example
1361
+ * ```ts
1362
+ * buildKey('report', interaction.guildId); // 'report:1234...' or 'report:global' in a DM
1363
+ * ```
1364
+ */
1365
+ function buildKey(prefix, ...parts) {
1366
+ if (parts.length === 0) return prefix;
1367
+ return [prefix, ...parts.map((part) => part ?? "global")].join(":");
1368
+ }
1369
+
1149
1370
  //#endregion
1150
1371
  //#region src/index.ts
1151
1372
  /** Package version */
1152
- const version = "0.11.0";
1373
+ const version = "0.12.0-next.2";
1153
1374
 
1154
1375
  //#endregion
1155
- export { ArraySourceBase, AutocompleteRoute, BaseHandler, BaseReplySender, BuilderComponent, Bus, ButtonRoute, ChannelMenuRoute, Commands, ContextMenus, Cooldown, CursorSourceBase, CustomId, DispatchContext, DmOnly, Fault, GuildOnly, InteractionKind, MentionableMenuRoute, MessageContextMenuRoute, ModalRoute, Notice, OwnerOnly, PaginatorBase, RegisterCommand, RegisterInteractionMiddleware, RepliableHandler, RequireBotPermissions, RequirePermissions, RequireRole, ResolvedEmoji, RoleMenuRoute, RowComponent, ShutdownPhase, Silence, SlashRoute, StartupPhase, StringMenuRoute, Subscribe, Subscriber, UserContextMenuRoute, UserMenuRoute, WebhookLog, WebhookUrl, and, assertPermissions, decodeFor, defineEffectGate, defineGate, mergeRoles, or, paginate, prefixOf, setCustomIdErrors, version };
1376
+ export { ArraySourceBase, AutocompleteRoute, BaseHandler, BaseReplySender, BuilderComponent, Bus, ButtonRoute, ChannelMenuRoute, Commands, ContextMenus, Cooldown, CursorSourceBase, CustomId, DmOnly, Fault, GuildOnly, InteractionKind, KeyvDriver, MemoryDriver, MentionableMenuRoute, MessageContextMenuRoute, ModalRoute, Notice, OwnerOnly, PaginatorBase, RegisterCommand, RegisterInteractionMiddleware, RepliableHandler, RequireBotPermissions, RequirePermissions, RequireRole, ResolvedEmoji, RoleMenuRoute, RowComponent, ShutdownPhase, Silence, SlashRoute, StartupPhase, StringMenuRoute, Subscribe, Subscriber, UnstorageDriver, UserContextMenuRoute, UserMenuRoute, WebhookLog, WebhookUrl, and, assertPermissions, buildKey, decodeFor, defineEffectGate, defineGate, mergeRoles, or, paginate, prefixOf, setCustomIdErrors, version };
1156
1377
  //# sourceMappingURL=index.mjs.map