@seedcord/core 0.12.0-next.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 (41) hide show
  1. package/README.md +3 -3
  2. package/dist/{Bus-W1K-uMeO.mjs → Bus-VJw-QpoY.mjs} +110 -54
  3. package/dist/Bus-VJw-QpoY.mjs.map +1 -0
  4. package/dist/{CoordinatedShutdown-BVtNNSsF.d.mts → CoordinatedShutdown-fPKLWjc5.d.mts} +2 -2
  5. package/dist/{CoordinatedStartup-DA3gW8op.mjs → CoordinatedStartup-Zw_Lk7Zk.mjs} +3 -3
  6. package/dist/{CoordinatedStartup-DA3gW8op.mjs.map → CoordinatedStartup-Zw_Lk7Zk.mjs.map} +1 -1
  7. package/dist/HmrModuleHandler-Btgu8r96.mjs +165 -0
  8. package/dist/HmrModuleHandler-Btgu8r96.mjs.map +1 -0
  9. package/dist/{PluginHost-DWBgBS6Q.d.mts → PluginHost-DFnkIldj.d.mts} +10 -9
  10. package/dist/{PluginHost-CYe-UVpp.mjs → PluginHost-DTtxWRiL.mjs} +4 -4
  11. package/dist/PluginHost-DTtxWRiL.mjs.map +1 -0
  12. package/dist/{PluginLifecycle-DdwRzcDj.mjs → PluginLifecycle-hx5zUc-P.mjs} +4 -4
  13. package/dist/PluginLifecycle-hx5zUc-P.mjs.map +1 -0
  14. package/dist/{ServerHost-whpI0h4d.d.mts → ServerHost-B5o3ScCF.d.mts} +3 -3
  15. package/dist/{ServerHost-DHg-Y2_q.mjs → ServerHost-CDaEDR4C.mjs} +4 -4
  16. package/dist/{ServerHost-DHg-Y2_q.mjs.map → ServerHost-CDaEDR4C.mjs.map} +1 -1
  17. package/dist/hmr.index.d.mts +20 -20
  18. package/dist/hmr.index.mjs +1 -1
  19. package/dist/{index-Boo33hh3.d.mts → index-DGHng1IN.d.mts} +108 -193
  20. package/dist/index.d.mts +216 -71
  21. package/dist/index.mjs +215 -100
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/internal.index.d.mts +204 -82
  24. package/dist/internal.index.mjs +572 -73
  25. package/dist/internal.index.mjs.map +1 -1
  26. package/dist/node-internal.index.d.mts +4 -3
  27. package/dist/node-internal.index.mjs +3 -3
  28. package/dist/node.index.d.mts +2 -2
  29. package/dist/node.index.mjs +4 -4
  30. package/dist/{phases-Dojnr-ng.d.mts → phases-C2QCSqRu.d.mts} +60 -50
  31. package/dist/plugin.index.d.mts +1 -1
  32. package/dist/plugin.index.mjs +1 -1
  33. package/dist/{subscribers-D4_HZcgw.mjs → subscribers-CGuVjbrg.mjs} +139 -253
  34. package/dist/subscribers-CGuVjbrg.mjs.map +1 -0
  35. package/package.json +10 -9
  36. package/dist/Bus-W1K-uMeO.mjs.map +0 -1
  37. package/dist/HmrModuleHandler-BMD7dg8N.mjs +0 -162
  38. package/dist/HmrModuleHandler-BMD7dg8N.mjs.map +0 -1
  39. package/dist/PluginHost-CYe-UVpp.mjs.map +0 -1
  40. package/dist/PluginLifecycle-DdwRzcDj.mjs.map +0 -1
  41. package/dist/subscribers-D4_HZcgw.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,13 +1,13 @@
1
- import { A as serializeReply, B as contextMenuRouteOf, C as NotAllowed, D as OnCooldown, E as NotOwner, F as AckTrace, H as storeComponentRoute, I as checkAckLegality, L as sendTarget, M as attemptWrite, N as publishResponse, O as defineEffectGate, R as RegisterInteractionMiddleware, S as NeedsAny, T as NotInGuild, U as storeInteractionRoute, X as ContextMenus, Y as Commands, a as RequireBotPermissions, b as MissingPermissions, d as presentNames, f as pickNotice, ft as NoticeCard, g as runCheck, h as rollbackCommits, i as Silence, j as translateSerializationError, k as defineGate, l as hasAll, m as markCommits, n as pageCursor, o as RequirePermissions, pt as Notice, q as RegisterCommand, r as ResolvedEmoji, s as RequireRole, t as PAGE_MAX, u as missingNames, w as NotInDm, y as HasDangerousPermissions } from "./subscribers-D4_HZcgw.mjs";
2
- import { M as RowComponent, T as InteractionKind, _ as MemoryDriver, d as WebhookLog, f as Subscriber, j as BuilderComponent, l as WebhookUrl, t as Bus, u as Subscribe, v as ShutdownPhase, y as StartupPhase } from "./Bus-W1K-uMeO.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
- /** State shared by middleware and handlers during one dispatch. */
295
- var DispatchContext = class {
296
- state = Object.create(null);
297
- id = crypto.randomUUID();
298
- /**
299
- * The dispatch identifier, such as `slash:search/docs`, `button:approve`, or `event:messageCreate`.
300
- * Component identifiers contain the custom-ID prefix without encoded params.
301
- */
302
- routeId;
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
@@ -361,7 +332,7 @@ var RepliableHandler = class extends BaseHandler {
361
332
  sender;
362
333
  constructor(event, core, dispatch, sender) {
363
334
  super(event, core, dispatch, "interactions");
364
- this.sender = sender ?? this.buildSender(event, core, dispatch);
335
+ this.sender = sender;
365
336
  }
366
337
  /** Send the initial response, exactly once. */
367
338
  reply(response, opts) {
@@ -557,44 +528,88 @@ var BaseReplySender = class {
557
528
  };
558
529
 
559
530
  //#endregion
560
- //#region src/stops/Fault.ts
531
+ //#region src/gates/Gate.ts
561
532
  /**
562
- * 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.
537
+ *
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.
563
540
  *
564
- * The user sees a fixed generic reply with the tracking uuid, never the cause. `report` defaults to
565
- * `true`, so the framework logs it and publishes it to the `handledException` bus. Pass `report: false`
566
- * to show the generic reply without the bus publish. The original error is stored as the standard
567
- * `cause`, so the real stack reaches the webhook. When the user should see a real message for a fault,
568
- * subclass {@link Notice} and write your own `render` instead.
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
569
546
  *
570
- * The framework also renders this for an unhandled throw, where it points the user at
571
- * `ctx.developerUsername`.
547
+ * @example
548
+ * ```ts
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
+ * ```
572
556
  *
573
557
  * @example
574
558
  * ```ts
575
- * import { Fault } from '@seedcord/gateway';
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.
576
583
  *
577
- * try {
578
- * await db.write(record);
579
- * } catch (cause) {
580
- * // user sees the generic reply with the uuid, and the real error rides along as cause for the webhook
581
- * throw new Fault({ cause });
584
+ * @see {@link defineGate}
585
+ * @see the `@Gated` decorator from your transport package
586
+ * @see {@link Cooldown}
582
587
  *
583
- * // pass report: false to show the same reply without publishing to the handledException bus
584
- * // throw new Fault({ cause, report: false });
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
+ * );
585
603
  * }
586
604
  * ```
587
605
  */
588
- var Fault = class extends Notice {
589
- constructor(options) {
590
- super("A fault occurred", options?.cause === void 0 ? void 0 : { cause: options.cause });
591
- this.report = options?.report ?? true;
592
- }
593
- render(ctx) {
594
- const contact = ctx.developerUsername ?? "the developer";
595
- 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] };
596
- }
597
- };
606
+ function defineEffectGate(name, check, commit) {
607
+ return {
608
+ name,
609
+ check: async (ctx) => check(ctx),
610
+ commit: async (ctx) => commit(ctx)
611
+ };
612
+ }
598
613
 
599
614
  //#endregion
600
615
  //#region src/gates/combinators.ts
@@ -643,9 +658,9 @@ function or(...args) {
643
658
  /**
644
659
  * Passes only for a user id listed in `config.ownerIds`, else refuses.
645
660
  *
646
- * 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.
647
662
  *
648
- * @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`.
649
664
  *
650
665
  * @see the `@Gated` decorator from your transport package
651
666
  *
@@ -662,15 +677,15 @@ function or(...args) {
662
677
  *
663
678
  * @example
664
679
  * ```ts
665
- * // reword the refusal, keeping the embed styling
666
- * OwnerOnly({ message: 'Owners only.' });
680
+ * // OwnersOnly is your own Notice class
681
+ * OwnerOnly({ notice: OwnersOnly });
667
682
  * ```
668
683
  */
669
684
  function OwnerOnly(options) {
670
685
  return defineGate("OwnerOnly", (ctx) => {
671
686
  const owners = ctx.core.config.ownerIds ?? [];
672
687
  if (ctx.userId !== null && owners.includes(ctx.userId)) return;
673
- throw pickNotice(options, (message) => new NotOwner(message));
688
+ throw new ((options?.notice) ?? NotOwner)();
674
689
  });
675
690
  }
676
691
  /**
@@ -678,7 +693,7 @@ function OwnerOnly(options) {
678
693
  *
679
694
  * Agnostic. Attaches to any handler kind. Often paired with `RequirePermissions` in an {@link and}.
680
695
  *
681
- * @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`.
682
697
  *
683
698
  * @see the `@Gated` decorator from your transport package
684
699
  *
@@ -698,7 +713,7 @@ function OwnerOnly(options) {
698
713
  function GuildOnly(options) {
699
714
  return defineGate("GuildOnly", (ctx) => {
700
715
  if (ctx.guildId) return;
701
- throw pickNotice(options, (message) => new NotInGuild(message));
716
+ throw new ((options?.notice) ?? NotInGuild)();
702
717
  });
703
718
  }
704
719
  /**
@@ -706,7 +721,7 @@ function GuildOnly(options) {
706
721
  *
707
722
  * Agnostic, fitting any handler kind. The inverse of {@link GuildOnly}, so combining the two in an {@link and} can never pass.
708
723
  *
709
- * @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`.
710
725
  *
711
726
  * @see the `@Gated` decorator from your transport package
712
727
  *
@@ -724,7 +739,7 @@ function GuildOnly(options) {
724
739
  function DmOnly(options) {
725
740
  return defineGate("DmOnly", (ctx) => {
726
741
  if (!ctx.guildId) return;
727
- throw pickNotice(options, (message) => new NotInDm(message));
742
+ throw new ((options?.notice) ?? NotInDm)();
728
743
  });
729
744
  }
730
745
 
@@ -742,10 +757,10 @@ function scopeValue(ctx, per) {
742
757
  * so a later refusal never burns the cooldown.
743
758
  * That split also means two requests racing the same key can both pass before either charges the slot.
744
759
  * The key combines the handler's route with the window settings, keeping two different handlers on separate
745
- * windows. Reword the refusal with {@link CooldownOptions.message} or replace it with {@link CooldownOptions.notice}.
760
+ * windows. Replace the refusal with {@link CooldownOptions.notice}.
746
761
  *
747
762
  * @param duration - A number is seconds, and a string is a duration like `30m` or `24h`.
748
- * @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`.
749
764
  * @throws A **SeedcordTypeError** when `duration` is a string that doesn't parse.
750
765
  * @throws A **SeedcordRangeError** when `duration` is zero or negative.
751
766
  *
@@ -797,22 +812,46 @@ function Cooldown(duration, options) {
797
812
  limit: options.limit
798
813
  };
799
814
  const anonId = anonSeq++;
800
- 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)}`;
801
816
  return defineEffectGate("Cooldown", async (ctx) => {
802
817
  const result = await ctx.core.rateLimiter.peek(keyOf(ctx), window);
803
818
  if (!result.limited) return;
804
- if (options?.notice) throw options.notice(result.resetAt);
805
- throw new OnCooldown(result.resetAt, options?.message?.(result.resetAt));
819
+ throw new ((options?.notice) ?? OnCooldown)(result.resetAt);
806
820
  }, async (ctx) => {
807
821
  await ctx.core.rateLimiter.charge(keyOf(ctx), window);
808
822
  });
809
823
  }
810
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
+
811
850
  //#endregion
812
851
  //#region src/permissions/assert.ts
813
852
  /**
814
853
  * Refuses when the subject is missing a permission in `scope`, or with `inverse` when it holds one.
815
- * 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`.
816
855
  *
817
856
  * The caller computes `permissions`, gateway off a discord.js role, member, or channel, and HTTP off the
818
857
  * bitfield the interaction payload carries.
@@ -857,14 +896,12 @@ function Cooldown(duration, options) {
857
896
  * ```ts
858
897
  * import { Notice } from '@seedcord/core';
859
898
  *
860
- * // a replacement notice takes the same three arguments the default one does
861
899
  * class CannotBan extends Notice {
862
900
  * public constructor(
863
- * message: string | undefined,
864
- * private readonly subject: string,
901
+ * private readonly subject: string | null,
865
902
  * private readonly missing: readonly string[]
866
903
  * ) {
867
- * super(message ?? 'You cannot ban here.');
904
+ * super('You cannot ban here.');
868
905
  * }
869
906
  *
870
907
  * public render(): ReplyResponse {
@@ -876,19 +913,97 @@ function Cooldown(duration, options) {
876
913
  * subject: `<@${member.id}>`,
877
914
  * permissions: member.permissions.bitfield,
878
915
  * scope: [PermissionFlagsBits.BanMembers],
879
- * missingNotice: CannotBan
916
+ * notice: CannotBan
880
917
  * });
881
918
  * ```
882
919
  */
883
- function assertPermissions(check) {
884
- const { subject, permissions, scope, inverse = false, missingNotice, dangerousNotice } = check;
885
- if (inverse) {
886
- const present = presentNames(permissions, scope);
887
- if (present.length === 0) return;
888
- throw new (dangerousNotice ?? HasDangerousPermissions)(void 0, subject, present);
889
- }
890
- if (hasAll(permissions, scope)) return;
891
- 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
+ });
892
1007
  }
893
1008
 
894
1009
  //#endregion
@@ -1255,8 +1370,8 @@ function buildKey(prefix, ...parts) {
1255
1370
  //#endregion
1256
1371
  //#region src/index.ts
1257
1372
  /** Package version */
1258
- const version = "0.12.0-next.0";
1373
+ const version = "0.12.0-next.2";
1259
1374
 
1260
1375
  //#endregion
1261
- export { ArraySourceBase, AutocompleteRoute, BaseHandler, BaseReplySender, BuilderComponent, Bus, ButtonRoute, ChannelMenuRoute, Commands, ContextMenus, Cooldown, CursorSourceBase, CustomId, DispatchContext, 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 };
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 };
1262
1377
  //# sourceMappingURL=index.mjs.map