@seedcord/core 0.1.0-next.1 → 0.1.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.
- package/dist/{CustomId-XgZ2KDgT.mjs → CustomId-Bq6WagiA.mjs} +196 -88
- package/dist/CustomId-Bq6WagiA.mjs.map +1 -0
- package/dist/{CustomId-CqiuEllp.cjs → CustomId-DQ6ZH4s-.cjs} +321 -87
- package/dist/CustomId-DQ6ZH4s-.cjs.map +1 -0
- package/dist/CustomId-Ddwg48VY.d.mts +654 -0
- package/dist/index.cjs +323 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +201 -41
- package/dist/index.mjs +316 -4
- package/dist/index.mjs.map +1 -1
- package/dist/internal.index.cjs +39 -1
- package/dist/internal.index.cjs.map +1 -1
- package/dist/internal.index.d.mts +60 -41
- package/dist/internal.index.mjs +22 -2
- package/dist/internal.index.mjs.map +1 -1
- package/package.json +7 -3
- package/dist/CustomId-BJF6mbJs.d.mts +0 -317
- package/dist/CustomId-CqiuEllp.cjs.map +0 -1
- package/dist/CustomId-XgZ2KDgT.mjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,114 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_CustomId = require('./CustomId-
|
|
2
|
+
const require_CustomId = require('./CustomId-DQ6ZH4s-.cjs');
|
|
3
|
+
require("reflect-metadata");
|
|
3
4
|
let _seedcord_errors = require("@seedcord/errors");
|
|
4
5
|
let _seedcord_errors_internal = require("@seedcord/errors/internal");
|
|
6
|
+
let _seedcord_utils = require("@seedcord/utils");
|
|
5
7
|
|
|
8
|
+
//#region src/decorators/Command.ts
|
|
9
|
+
function RegisterCommand(scope, guilds = []) {
|
|
10
|
+
return (ctor) => {
|
|
11
|
+
const existingMeta = Reflect.getOwnMetadata(require_CustomId.CommandMetadataKey, ctor);
|
|
12
|
+
if (existingMeta) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.DecoratorCommandAlreadyRegistered, [
|
|
13
|
+
ctor.name,
|
|
14
|
+
existingMeta.scope,
|
|
15
|
+
scope
|
|
16
|
+
]);
|
|
17
|
+
if (scope === "global" && guilds.length > 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.DecoratorCommandGlobalWithGuilds);
|
|
18
|
+
if (scope === "guild" && (!Array.isArray(guilds) || guilds.length === 0)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.DecoratorCommandGuildWithoutGuilds);
|
|
19
|
+
const meta = scope === "global" ? { scope } : {
|
|
20
|
+
scope,
|
|
21
|
+
guilds
|
|
22
|
+
};
|
|
23
|
+
Reflect.defineMetadata(require_CustomId.CommandMetadataKey, meta, ctor);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/gates/Gate.ts
|
|
29
|
+
/**
|
|
30
|
+
* Builds a {@link Gate} from a check. The check refuses by throwing a Notice or a Silence and passes
|
|
31
|
+
* by returning. The required context is inferred from `ctx`, so annotate it as narrowly as the fields
|
|
32
|
+
* the check reads. No annotation defaults to {@link GateContextBase}, an agnostic gate that fits every
|
|
33
|
+
* handler.
|
|
34
|
+
*
|
|
35
|
+
* @typeParam Name - The gate's name captured as a literal, so a mismatch error can name the gate.
|
|
36
|
+
* @typeParam Ctx - The context the check reads, inferred from the `ctx` annotation.
|
|
37
|
+
*
|
|
38
|
+
* @param name - The gate's name, used in mismatch errors.
|
|
39
|
+
* @param fn - The check, which refuses by throwing a Notice or a Silence and passes by returning.
|
|
40
|
+
*
|
|
41
|
+
* @see {@link defineEffectGate}
|
|
42
|
+
* @see the `@Gated` decorator from `seedcord`
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* // a gate factory that takes its own option and closes over it
|
|
47
|
+
* function MinRoles(min: number): Gate<GateContextBase, 'MinRoles'> {
|
|
48
|
+
* return defineGate('MinRoles', (ctx) => {
|
|
49
|
+
* if (ctx.memberRoleIds.length < min) throw new NotEnoughRoles(min);
|
|
50
|
+
* });
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* // agnostic gate (no ctx annotation): fits any handler
|
|
57
|
+
* const Owner = defineGate('owner', (ctx) => {
|
|
58
|
+
* if (!ctx.userId) throw new NotOwner();
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
function defineGate(name, fn) {
|
|
63
|
+
return {
|
|
64
|
+
name,
|
|
65
|
+
check: async (ctx) => fn(ctx)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Builds an {@link EffectGate} from a `check` and a `commit`. `check` peeks and refuses by throwing, and
|
|
70
|
+
* `commit` applies the side effect, running only once the whole gate set passes so a later refusal never
|
|
71
|
+
* commits. In an `or`, a refusing arm's queued commit is rolled back. This is how {@link Cooldown} peeks
|
|
72
|
+
* in `check` and charges the slot in `commit`.
|
|
73
|
+
*
|
|
74
|
+
* @typeParam Name - The gate's name captured as a literal, so a mismatch error can name the gate.
|
|
75
|
+
* @typeParam Ctx - The context both `check` and `commit` read, inferred from the `ctx` annotation.
|
|
76
|
+
*
|
|
77
|
+
* @param name - The gate's name, used in mismatch errors.
|
|
78
|
+
* @param check - Peeks and refuses by throwing, without applying the side effect.
|
|
79
|
+
* @param commit - Applies the side effect, running only after the whole gate set passes.
|
|
80
|
+
*
|
|
81
|
+
* @see {@link defineGate}
|
|
82
|
+
* @see the `@Gated` decorator from `seedcord`
|
|
83
|
+
* @see {@link Cooldown}
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* // an effect-gate factory that takes its own limit and closes over it
|
|
88
|
+
* function UsesPerDay(max: number): EffectGate<GateContextBase, 'UsesPerDay'> {
|
|
89
|
+
* return defineEffectGate(
|
|
90
|
+
* 'UsesPerDay',
|
|
91
|
+
* (ctx) => {
|
|
92
|
+
* // peek and refuse by throwing, do not mutate yet
|
|
93
|
+
* if (usedToday(ctx.userId) >= max) throw new OutOfUsesNotice(max);
|
|
94
|
+
* },
|
|
95
|
+
* (ctx) => {
|
|
96
|
+
* // runs only after the whole gate set passes
|
|
97
|
+
* recordUse(ctx.userId);
|
|
98
|
+
* }
|
|
99
|
+
* );
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
function defineEffectGate(name, check, commit) {
|
|
104
|
+
return {
|
|
105
|
+
name,
|
|
106
|
+
check: async (ctx) => check(ctx),
|
|
107
|
+
commit: async (ctx) => commit(ctx)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
//#endregion
|
|
6
112
|
//#region src/stops/Fault.ts
|
|
7
113
|
/**
|
|
8
114
|
* A generic fault you throw after catching an error you do not have a specific message for.
|
|
@@ -41,6 +147,212 @@ var Fault = class extends require_CustomId.Notice {
|
|
|
41
147
|
}
|
|
42
148
|
};
|
|
43
149
|
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/gates/combinators.ts
|
|
152
|
+
function and(...gates) {
|
|
153
|
+
return defineGate(gates.map((gate) => gate.name).join(" & "), async (ctx) => {
|
|
154
|
+
for (const gate of gates) await require_CustomId.runCheck(gate, ctx);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function isOrOptions(arg) {
|
|
158
|
+
return !("check" in arg);
|
|
159
|
+
}
|
|
160
|
+
function or(...args) {
|
|
161
|
+
const last = args.at(-1);
|
|
162
|
+
const options = last !== void 0 && isOrOptions(last) ? last : void 0;
|
|
163
|
+
const gates = options ? args.slice(0, -1) : args;
|
|
164
|
+
return defineGate(gates.map((gate) => gate.name).join(" | "), async (ctx) => {
|
|
165
|
+
const summaries = [];
|
|
166
|
+
let everyArmHasSummary = true;
|
|
167
|
+
for (const gate of gates) {
|
|
168
|
+
const mark = require_CustomId.markCommits(ctx);
|
|
169
|
+
try {
|
|
170
|
+
await require_CustomId.runCheck(gate, ctx);
|
|
171
|
+
return;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (error instanceof require_CustomId.Notice && !(error instanceof Fault) && !error.report) {
|
|
174
|
+
require_CustomId.rollbackCommits(ctx, mark);
|
|
175
|
+
if (error.summary === void 0) everyArmHasSummary = false;
|
|
176
|
+
else summaries.push(error.summary);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (options) throw typeof options.fail === "function" ? options.fail(ctx) : options.fail;
|
|
183
|
+
if (everyArmHasSummary && summaries.length > 0) throw new require_CustomId.NeedsAny(summaries);
|
|
184
|
+
throw new require_CustomId.NotAllowed();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/gates/catalog/access.ts
|
|
190
|
+
/**
|
|
191
|
+
* Passes only for a user id listed in `config.ownerIds`, else refuses.
|
|
192
|
+
*
|
|
193
|
+
* 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.
|
|
194
|
+
*
|
|
195
|
+
* @param options - Reword the default refusal with `message`, or replace it with `notice`.
|
|
196
|
+
*
|
|
197
|
+
* @see the `@Gated` decorator from `seedcord`
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```ts
|
|
201
|
+
* \@Gated(OwnerOnly())
|
|
202
|
+
* \@SlashRoute('shutdown')
|
|
203
|
+
* class ShutdownHandler extends SlashHandler<'shutdown'> {
|
|
204
|
+
* async execute() {
|
|
205
|
+
* // ...
|
|
206
|
+
* }
|
|
207
|
+
* }
|
|
208
|
+
* ```
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```ts
|
|
212
|
+
* // reword the refusal, keeping the embed styling
|
|
213
|
+
* OwnerOnly({ message: 'Owners only.' });
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
function OwnerOnly(options) {
|
|
217
|
+
return defineGate("OwnerOnly", (ctx) => {
|
|
218
|
+
const owners = ctx.core.config.ownerIds ?? [];
|
|
219
|
+
if (ctx.userId !== null && owners.includes(ctx.userId)) return;
|
|
220
|
+
throw require_CustomId.pickNotice(options, (message) => new require_CustomId.NotOwner(message));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Passes inside a guild, else refuses.
|
|
225
|
+
*
|
|
226
|
+
* Agnostic, so it attaches to any handler kind. Often paired with `RequirePermissions` in an {@link and}.
|
|
227
|
+
*
|
|
228
|
+
* @param options - Reword the default refusal with `message`, or replace it with `notice`.
|
|
229
|
+
*
|
|
230
|
+
* @see the `@Gated` decorator from `seedcord`
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```ts
|
|
234
|
+
* import { PermissionFlagsBits } from 'discord.js';
|
|
235
|
+
*
|
|
236
|
+
* \@Gated(GuildOnly(), RequirePermissions([PermissionFlagsBits.Administrator]))
|
|
237
|
+
* \@SlashRoute('maintenance')
|
|
238
|
+
* class Maintenance extends SlashHandler<'maintenance'> {
|
|
239
|
+
* async execute() {
|
|
240
|
+
* // ...
|
|
241
|
+
* }
|
|
242
|
+
* }
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
function GuildOnly(options) {
|
|
246
|
+
return defineGate("GuildOnly", (ctx) => {
|
|
247
|
+
if (ctx.guildId) return;
|
|
248
|
+
throw require_CustomId.pickNotice(options, (message) => new require_CustomId.NotInGuild(message));
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Passes in a direct message, else refuses.
|
|
253
|
+
*
|
|
254
|
+
* Agnostic, so it attaches to any handler kind. The inverse of {@link GuildOnly}, so combining the two in an {@link and} can never pass.
|
|
255
|
+
*
|
|
256
|
+
* @param options - Reword the default refusal with `message`, or replace it with `notice`.
|
|
257
|
+
*
|
|
258
|
+
* @see the `@Gated` decorator from `seedcord`
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```ts
|
|
262
|
+
* \@Gated(DmOnly())
|
|
263
|
+
* \@SlashRoute('verify')
|
|
264
|
+
* class VerifyHandler extends SlashHandler<'verify'> {
|
|
265
|
+
* async execute() {
|
|
266
|
+
* // ...
|
|
267
|
+
* }
|
|
268
|
+
* }
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
271
|
+
function DmOnly(options) {
|
|
272
|
+
return defineGate("DmOnly", (ctx) => {
|
|
273
|
+
if (!ctx.guildId) return;
|
|
274
|
+
throw require_CustomId.pickNotice(options, (message) => new require_CustomId.NotInDm(message));
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/gates/catalog/Cooldown.ts
|
|
280
|
+
let bucketSeq = 0;
|
|
281
|
+
function scopeValue(ctx, per) {
|
|
282
|
+
if (per === "guild") return ctx.guildId ?? "global";
|
|
283
|
+
if (per === "channel") return ctx.channelId ?? "global";
|
|
284
|
+
return ctx.userId ?? "global";
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Allows `limit` uses per window, scoped by `per`. A number `duration` is **seconds**, a string is
|
|
288
|
+
* a duration like `30m` or `24h`. An unparseable string throws a **SeedcordTypeError** at construction. The
|
|
289
|
+
* slot is charged in commit, only after the whole gate set passes, so a later refusal never burns the cooldown.
|
|
290
|
+
* That split also means two requests racing the same key can both pass before either charges the slot.
|
|
291
|
+
* Each call gets its own bucket, so two handlers never share a window. Reword the refusal with {@link CooldownOptions.message}
|
|
292
|
+
* or replace it with {@link CooldownOptions.notice}.
|
|
293
|
+
*
|
|
294
|
+
* @param duration - A number is seconds, a string is a duration like `30m` or `24h`. An unparseable string throws a **SeedcordTypeError**.
|
|
295
|
+
* @param options - Sets the scope with `per`, the uses per window with `limit`, and the refusal text with `message` or `notice`.
|
|
296
|
+
*
|
|
297
|
+
* @see the `@Gated` decorator from `seedcord`
|
|
298
|
+
* @see {@link IRateLimiter}
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```ts
|
|
302
|
+
* // a number argument is SECONDS
|
|
303
|
+
* \@Gated(Cooldown(5))
|
|
304
|
+
* \@SlashRoute('daily')
|
|
305
|
+
* class DailyHandler extends SlashHandler<'daily'> {
|
|
306
|
+
* async execute() {
|
|
307
|
+
* // ...
|
|
308
|
+
* }
|
|
309
|
+
* }
|
|
310
|
+
* ```
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* ```ts
|
|
314
|
+
* // a string is a duration, here scoped per guild (the default is per user)
|
|
315
|
+
* Cooldown('30m', { per: 'guild' });
|
|
316
|
+
* ```
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* ```ts
|
|
320
|
+
* // a burst, 3 uses per 10 minutes per user, then it refuses until a slot frees
|
|
321
|
+
* \@Gated(Cooldown('10m', { limit: 3 }))
|
|
322
|
+
* \@SlashRoute('report')
|
|
323
|
+
* class ReportHandler extends SlashHandler<'report'> {
|
|
324
|
+
* async execute() {
|
|
325
|
+
* // ...
|
|
326
|
+
* }
|
|
327
|
+
* }
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
function Cooldown(duration, options) {
|
|
331
|
+
let windowMs;
|
|
332
|
+
if (typeof duration === "number") windowMs = duration * 1e3;
|
|
333
|
+
else {
|
|
334
|
+
const parsed = (0, _seedcord_utils.parseDuration)(duration);
|
|
335
|
+
if (parsed === null) throw new _seedcord_errors_internal.SeedcordTypeError(_seedcord_errors.SeedcordErrorCode.GateInvalidCooldownDuration, [duration]);
|
|
336
|
+
windowMs = parsed;
|
|
337
|
+
}
|
|
338
|
+
if (windowMs <= 0) throw new _seedcord_errors_internal.SeedcordTypeError(_seedcord_errors.SeedcordErrorCode.GateInvalidCooldownDuration, [String(duration)]);
|
|
339
|
+
const bucket = `cooldown:${bucketSeq++}`;
|
|
340
|
+
const per = options?.per ?? "user";
|
|
341
|
+
const window = options?.limit === void 0 ? { windowMs } : {
|
|
342
|
+
windowMs,
|
|
343
|
+
limit: options.limit
|
|
344
|
+
};
|
|
345
|
+
const keyOf = (ctx) => `${bucket}:${scopeValue(ctx, per)}`;
|
|
346
|
+
return defineEffectGate("Cooldown", async (ctx) => {
|
|
347
|
+
const result = await ctx.core.rateLimiter.peek(keyOf(ctx), window);
|
|
348
|
+
if (!result.limited) return;
|
|
349
|
+
if (options?.notice) throw options.notice(result.resetAt);
|
|
350
|
+
throw new require_CustomId.OnCooldown(result.resetAt, options?.message?.(result.resetAt));
|
|
351
|
+
}, async (ctx) => {
|
|
352
|
+
await ctx.core.rateLimiter.charge(keyOf(ctx), window);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
44
356
|
//#endregion
|
|
45
357
|
//#region src/stops/Silence.ts
|
|
46
358
|
/**
|
|
@@ -99,15 +411,24 @@ function paginate(items, page, perPage) {
|
|
|
99
411
|
//#endregion
|
|
100
412
|
//#region src/index.ts
|
|
101
413
|
/** Package version */
|
|
102
|
-
const version = "0.1.0-next.
|
|
414
|
+
const version = "0.1.0-next.2";
|
|
103
415
|
|
|
104
416
|
//#endregion
|
|
105
417
|
exports.BuilderComponent = require_CustomId.BuilderComponent;
|
|
418
|
+
exports.Cooldown = Cooldown;
|
|
106
419
|
exports.CustomId = require_CustomId.CustomId;
|
|
420
|
+
exports.DmOnly = DmOnly;
|
|
107
421
|
exports.Fault = Fault;
|
|
422
|
+
exports.GuildOnly = GuildOnly;
|
|
108
423
|
exports.Notice = require_CustomId.Notice;
|
|
424
|
+
exports.OwnerOnly = OwnerOnly;
|
|
425
|
+
exports.RegisterCommand = RegisterCommand;
|
|
109
426
|
exports.RowComponent = require_CustomId.RowComponent;
|
|
110
427
|
exports.Silence = Silence;
|
|
428
|
+
exports.and = and;
|
|
429
|
+
exports.defineEffectGate = defineEffectGate;
|
|
430
|
+
exports.defineGate = defineGate;
|
|
431
|
+
exports.or = or;
|
|
111
432
|
exports.paginate = paginate;
|
|
112
433
|
exports.version = version;
|
|
113
434
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["Notice","NoticeCard","SeedcordRangeError","SeedcordErrorCode"],"sources":["../src/stops/Fault.ts","../src/stops/Silence.ts","../src/pagination/paginate.ts","../src/index.ts"],"sourcesContent":["import { Notice } from './Notice';\nimport { NoticeCard } from './NoticeCard';\n\nimport type { RenderContext, ReplyResponse } from '@seedcord/types';\n\n/**\n * A generic fault you throw after catching an error you do not have a specific message for.\n *\n * The user sees a fixed generic reply with the tracking uuid, never the cause. `report` defaults to\n * `true`, so the framework logs it and publishes it to the `handledException` bus. Pass `report: false`\n * to show the generic reply without the bus publish. The original error is stored as the standard\n * `cause`, so the real stack reaches the webhook. For a fault the user should see a real message,\n * subclass {@link Notice} and write your own `render` instead.\n *\n * The framework also renders this for an unhandled throw, where it points the user at\n * `ctx.developerUsername`.\n *\n * @example\n * ```ts\n * import { Fault } from 'seedcord';\n *\n * try {\n * await db.write(record);\n * } catch (cause) {\n * // user sees the generic reply with the uuid, the real error rides along as cause for the webhook\n * throw new Fault({ cause });\n *\n * // pass report: false to show the same reply without publishing to the handledException bus\n * // throw new Fault({ cause, report: false });\n * }\n * ```\n */\nexport class Fault extends Notice {\n public constructor(options?: { cause?: unknown; report?: boolean }) {\n super('A fault occurred', options?.cause === undefined ? undefined : { cause: options.cause });\n this.report = options?.report ?? true;\n }\n\n public render(ctx: RenderContext): ReplyResponse {\n const contact = ctx.developerUsername ?? 'the developer';\n const card = new NoticeCard(\n `Something went wrong. Please reach out to ${contact} with a way to reproduce the error and the following:\\n### UUID: \\`${ctx.uuid}\\``,\n 'Error'\n );\n return { components: [card.component] };\n }\n}\n","/**\n * Throw to stop a handler with no reply and no report.\n *\n * The framework boundary catches `Silence` before {@link Notice}, makes zero Discord calls, and stops.\n * Ideally you'd only throw this in `EventHandlers` (or `Gates` for those), because it doesn't make sense to\n * leave the user with no reply for an interaction.\n *\n * @example\n * ```ts\n * import { Silence } from 'seedcord';\n *\n * // before any reply or defer, drop the interaction with no reply and no report\n * if (await isBlacklisted(interaction.user.id)) throw new Silence('blacklisted user');\n * ```\n */\nexport class Silence extends Error {\n /**\n * @param reason - Optional note written only to a debug log, never shown to the user or reported.\n */\n public constructor(public readonly reason?: string) {\n super(reason ?? 'Silence');\n\n Error.captureStackTrace(this, this.constructor);\n }\n}\n","import { SeedcordErrorCode } from '@seedcord/errors';\nimport { SeedcordRangeError } from '@seedcord/errors/internal';\n\nimport type { PageView } from './PageView';\n\n/**\n * Pure page math, usable headless. Clamps `page` into range and slices the window.\n *\n * @param items - The full list to page over.\n * @param page - The requested zero-based page, clamped into `[0, totalPages - 1]`.\n * @param perPage - The page size. Must be a positive integer.\n * @returns The sliced {@link PageView}, with `totalPages` always known.\n * @throws SeedcordRangeError when `perPage` is not a positive integer.\n */\nexport function paginate<Item>(items: readonly Item[], page: number, perPage: number): PageView<Item> {\n if (!Number.isSafeInteger(perPage) || perPage <= 0) {\n throw new SeedcordRangeError(SeedcordErrorCode.PaginationInvalidPerPage, [perPage]);\n }\n\n const totalPages = Math.max(1, Math.ceil(items.length / perPage));\n // a headless caller can pass a non-integer or out-of-range page.\n const clamped = Math.min(Math.max(Math.trunc(page), 0), totalPages - 1);\n const start = clamped * perPage;\n\n return {\n items: items.slice(start, start + perPage),\n page: clamped,\n perPage,\n totalPages,\n hasPrev: clamped > 0,\n hasNext: clamped < totalPages - 1\n };\n}\n","export type * from '@registries/ContextMenuRegistry';\nexport type * from '@registries/SlashOptionRegistry';\n\nexport { BuilderComponent, RowComponent } from '@components/Component';\nexport { type RowType, type BuilderType } from '@components/builderTypes';\n\nexport { Notice } from '@stops/Notice';\nexport { Fault } from '@stops/Fault';\nexport { Silence } from '@stops/Silence';\n\nexport { CustomId } from '@customId/CustomId';\n\nexport { paginate } from '@pagination/paginate';\nexport { type PageView } from '@pagination/PageView';\n\n/** Package version */\nexport const version = process.env.PACKAGE_VERSION ?? '0.0.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,QAAb,cAA2BA,wBAAO;CAC9B,AAAO,YAAY,SAAiD;EAChE,MAAM,oBAAoB,SAAS,UAAU,SAAY,SAAY,EAAE,OAAO,QAAQ,MAAM,CAAC;EAC7F,KAAK,SAAS,SAAS,UAAU;CACrC;CAEA,AAAO,OAAO,KAAmC;EAM7C,OAAO,EAAE,YAAY,CAAC,IAJLC,4BACb,6CAFY,IAAI,qBAAqB,gBAEgB,qEAAqE,IAAI,KAAK,KACnI,OAEqB,CAAC,CAAC,SAAS,EAAE;CAC1C;AACJ;;;;;;;;;;;;;;;;;;;AC/BA,IAAa,UAAb,cAA6B,MAAM;CAII;;;;CAAnC,AAAO,YAAY,AAAgB,QAAiB;EAChD,MAAM,UAAU,SAAS;EADM;EAG/B,MAAM,kBAAkB,MAAM,KAAK,WAAW;CAClD;AACJ;;;;;;;;;;;;;ACVA,SAAgB,SAAe,OAAwB,MAAc,SAAiC;CAClG,IAAI,CAAC,OAAO,cAAc,OAAO,KAAK,WAAW,GAC7C,MAAM,IAAIC,6CAAmBC,mCAAkB,0BAA0B,CAAC,OAAO,CAAC;CAGtF,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,KAAK,MAAM,SAAS,OAAO,CAAC;CAEhE,MAAM,UAAU,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,IAAI,GAAG,CAAC,GAAG,aAAa,CAAC;CACtE,MAAM,QAAQ,UAAU;CAExB,OAAO;EACH,OAAO,MAAM,MAAM,OAAO,QAAQ,OAAO;EACzC,MAAM;EACN;EACA;EACA,SAAS,UAAU;EACnB,SAAS,UAAU,aAAa;CACpC;AACJ;;;;;AChBA,MAAa"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["CommandMetadataKey","SeedcordError","SeedcordErrorCode","Notice","NoticeCard","runCheck","markCommits","Notice","NeedsAny","NotAllowed","pickNotice","NotOwner","NotInGuild","NotInDm","SeedcordTypeError","SeedcordErrorCode","OnCooldown","SeedcordRangeError","SeedcordErrorCode"],"sources":["../src/decorators/Command.ts","../src/gates/Gate.ts","../src/stops/Fault.ts","../src/gates/combinators.ts","../src/gates/catalog/access.ts","../src/gates/catalog/Cooldown.ts","../src/stops/Silence.ts","../src/pagination/paginate.ts","../src/index.ts"],"sourcesContent":["import { SeedcordErrorCode } from '@seedcord/errors';\nimport { SeedcordError } from '@seedcord/errors/internal';\n\nimport { CommandMetadataKey } from '@src/metadataKeys';\n\nimport type { BuilderComponent } from '@components/Component';\nimport type { Constructor } from 'type-fest';\n\ntype CommandCtor = Constructor<BuilderComponent<'command' | 'context_menu'>>;\n\ninterface GlobalMeta {\n scope: 'global';\n}\n\ninterface GuildMeta {\n scope: 'guild';\n guilds: string[];\n}\n\nexport type CommandMeta = GlobalMeta | GuildMeta;\n\ntype CommandScope = CommandMeta['scope'];\n\n/**\n * Attaches deployment metadata so the framework registers this command globally or in specific guilds.\n *\n * @param scope - Must be 'global' for global registration\n * @decorator\n * @example\n * ```typescript\n * \\@RegisterCommand('global')\n * class PingCommand extends BuilderComponent<'command'> {} // or BuilderComponent<'context_menu'>\n * ```\n */\nexport function RegisterCommand(scope: 'global'): (ctor: CommandCtor) => void;\n\n/**\n * Registers a command for specific guild deployment.\n *\n * @param scope - Must be 'guild' for guild-specific registration\n * @param guilds - Array of guild IDs where the command should be registered\n * @decorator\n * @example\n * ```typescript\n * \\@RegisterCommand('guild', ['123456789'])\n * class AdminCommand extends BuilderComponent<'command'> {} // or BuilderComponent<'context_menu'>\n * ```\n */\nexport function RegisterCommand(scope: 'guild', guilds: string[]): (ctor: CommandCtor) => void;\n\nexport function RegisterCommand(scope: CommandScope, guilds: string[] = []) {\n return (ctor: CommandCtor): void => {\n const existingMeta = Reflect.getOwnMetadata(CommandMetadataKey, ctor) as CommandMeta | undefined;\n if (existingMeta) {\n throw new SeedcordError(SeedcordErrorCode.DecoratorCommandAlreadyRegistered, [\n ctor.name,\n existingMeta.scope,\n scope\n ]);\n }\n\n if (scope === 'global' && guilds.length > 0) {\n throw new SeedcordError(SeedcordErrorCode.DecoratorCommandGlobalWithGuilds);\n }\n\n if (scope === 'guild' && (!Array.isArray(guilds) || guilds.length === 0)) {\n throw new SeedcordError(SeedcordErrorCode.DecoratorCommandGuildWithoutGuilds);\n }\n\n const meta: GlobalMeta | GuildMeta = scope === 'global' ? { scope } : { scope, guilds };\n Reflect.defineMetadata(CommandMetadataKey, meta, ctor);\n };\n}\n","import type { CoreBase } from '@interfaces/CoreBase';\n\n/**\n * The transport-agnostic gate context, the scalar identity of the acting user resolvable from any\n * transport's payload. Every field except `core` is nullable, since an event or DM may carry no\n * guild or member. Each transport extends it with its own rich arm. For e.g., the gateway adds the djs\n * `user`/`guild`/`member` objects and the live interaction or event payload.\n *\n * @example\n * ```ts\n * // a gate reading only the base fields fits any handler on any transport\n * const NeedsGuild = defineGate('NeedsGuild', (ctx: GateContextBase) => {\n * if (!ctx.guildId) throw new NotInGuild();\n * });\n * ```\n */\nexport interface GateContextBase {\n /** Gates read the config and the rate limiter through it. */\n readonly core: CoreBase;\n /** The acting user's id, or null on an event that carries none. */\n readonly userId: string | null;\n /** The guild id, or null outside a guild. */\n readonly guildId: string | null;\n /** The channel id, or null when the source carries none. */\n readonly channelId: string | null;\n /**\n * The acting member's role ids without the everyone role, the shape a raw interaction payload\n * carries. Empty outside a guild or when the source carries none.\n */\n readonly memberRoleIds: readonly string[];\n /**\n * The acting member's permission bits, or null outside a guild. On an interaction these are the\n * channel-scoped permissions the payload carries, on a gateway event the member's guild-level ones.\n */\n readonly memberPermissions: bigint | null;\n}\n\n// phantom brand, so a bare check function or a plain object is rejected where a Gate is expected\ndeclare const GateBrand: unique symbol;\n\n/**\n * A precondition attached to a handler. It refuses by throwing a `Notice` (a reply) or a `Silence`\n * (a drop with no reply), and passes by returning. `Ctx` is the context it requires, so a gate that reads\n * a field absent on a handler's context is rejected at compile time. `Name` is captured as a literal so\n * a mismatch error can name the gate. Build one with {@link defineGate} or {@link defineEffectGate}.\n *\n * @typeParam Ctx - The context the gate requires, so reading a field absent on a handler's context is a compile error.\n * @typeParam Name - The gate's name captured as a literal, so a mismatch error can name the gate.\n *\n * @example\n * ```ts\n * // a factory returning a typed, agnostic gate\n * function MyGate(): Gate<GateContextBase, 'MyGate'> {\n * return defineGate('MyGate', (ctx) => {\n * if (shouldRefuse(ctx)) throw new MyNotice();\n * });\n * }\n * ```\n *\n * @see {@link defineGate}\n */\nexport interface Gate<Ctx extends GateContextBase = GateContextBase, Name extends string = string> {\n /** The gate's name, surfaced in a mismatch error and joined by the combinators. */\n readonly name: Name;\n /** Reads the context and refuses by throwing a Notice or a Silence, or passes by resolving. */\n check(ctx: Ctx): Promise<void>;\n\n /** @internal */\n readonly [GateBrand]: true;\n}\n\n/**\n * A gate that carries a side effect, split so the effect fires only once the gate is the one that\n * let the request through. `check` peeks and refuses, `commit` applies the effect after the whole\n * gate set passes. {@link Cooldown} is the catalog gate that returns one. Build one with\n * {@link defineEffectGate}.\n *\n * @typeParam Ctx - The context the gate requires, so reading a field absent on a handler's context is a compile error.\n * @typeParam Name - The gate's name captured as a literal, so a mismatch error can name the gate.\n *\n * @example\n * ```ts\n * // a factory returning a typed effect gate\n * function MyEffectGate(): EffectGate<GateContextBase, 'MyEffectGate'> {\n * return defineEffectGate(\n * 'MyEffectGate',\n * (ctx) => {\n * if (shouldRefuse(ctx)) throw new MyNotice();\n * },\n * (ctx) => {\n * applyMyEffect(ctx);\n * }\n * );\n * }\n * ```\n *\n * @see {@link defineEffectGate}\n */\nexport interface EffectGate<Ctx extends GateContextBase = GateContextBase, Name extends string = string> extends Gate<\n Ctx,\n Name\n> {\n /** Applies the side effect, run only after the whole gate set passes. */\n commit(ctx: Ctx): Promise<void>;\n}\n\n/**\n * Builds a {@link Gate} from a check. The check refuses by throwing a Notice or a Silence and passes\n * by returning. The required context is inferred from `ctx`, so annotate it as narrowly as the fields\n * the check reads. No annotation defaults to {@link GateContextBase}, an agnostic gate that fits every\n * handler.\n *\n * @typeParam Name - The gate's name captured as a literal, so a mismatch error can name the gate.\n * @typeParam Ctx - The context the check reads, inferred from the `ctx` annotation.\n *\n * @param name - The gate's name, used in mismatch errors.\n * @param fn - The check, which refuses by throwing a Notice or a Silence and passes by returning.\n *\n * @see {@link defineEffectGate}\n * @see the `@Gated` decorator from `seedcord`\n *\n * @example\n * ```ts\n * // a gate factory that takes its own option and closes over it\n * function MinRoles(min: number): Gate<GateContextBase, 'MinRoles'> {\n * return defineGate('MinRoles', (ctx) => {\n * if (ctx.memberRoleIds.length < min) throw new NotEnoughRoles(min);\n * });\n * }\n * ```\n *\n * @example\n * ```ts\n * // agnostic gate (no ctx annotation): fits any handler\n * const Owner = defineGate('owner', (ctx) => {\n * if (!ctx.userId) throw new NotOwner();\n * });\n * ```\n */\nexport function defineGate<const Name extends string, Ctx extends GateContextBase = GateContextBase>(\n name: Name,\n fn: (ctx: Ctx) => void | Promise<void>\n): Gate<Ctx, Name> {\n return { name, check: async (ctx) => fn(ctx) } as Gate<Ctx, Name>;\n}\n\n/**\n * Builds an {@link EffectGate} from a `check` and a `commit`. `check` peeks and refuses by throwing, and\n * `commit` applies the side effect, running only once the whole gate set passes so a later refusal never\n * commits. In an `or`, a refusing arm's queued commit is rolled back. This is how {@link Cooldown} peeks\n * in `check` and charges the slot in `commit`.\n *\n * @typeParam Name - The gate's name captured as a literal, so a mismatch error can name the gate.\n * @typeParam Ctx - The context both `check` and `commit` read, inferred from the `ctx` annotation.\n *\n * @param name - The gate's name, used in mismatch errors.\n * @param check - Peeks and refuses by throwing, without applying the side effect.\n * @param commit - Applies the side effect, running only after the whole gate set passes.\n *\n * @see {@link defineGate}\n * @see the `@Gated` decorator from `seedcord`\n * @see {@link Cooldown}\n *\n * @example\n * ```ts\n * // an effect-gate factory that takes its own limit and closes over it\n * function UsesPerDay(max: number): EffectGate<GateContextBase, 'UsesPerDay'> {\n * return defineEffectGate(\n * 'UsesPerDay',\n * (ctx) => {\n * // peek and refuse by throwing, do not mutate yet\n * if (usedToday(ctx.userId) >= max) throw new OutOfUsesNotice(max);\n * },\n * (ctx) => {\n * // runs only after the whole gate set passes\n * recordUse(ctx.userId);\n * }\n * );\n * }\n * ```\n */\nexport function defineEffectGate<const Name extends string, Ctx extends GateContextBase = GateContextBase>(\n name: Name,\n check: (ctx: Ctx) => void | Promise<void>,\n commit: (ctx: Ctx) => void | Promise<void>\n): EffectGate<Ctx, Name> {\n return {\n name,\n check: async (ctx) => check(ctx),\n commit: async (ctx) => commit(ctx)\n } as EffectGate<Ctx, Name>;\n}\n\n/**\n * The context a {@link Gate} requires.\n *\n * @typeParam TGate - The gate type to read the required context from.\n *\n * @example\n * ```ts\n * type OwnerCtx = RequiredOf<ReturnType<typeof OwnerOnly>>; // GateContextBase\n * ```\n */\nexport type RequiredOf<TGate> = TGate extends Gate<infer Ctx> ? Ctx : never;\n","import { Notice } from './Notice';\nimport { NoticeCard } from './NoticeCard';\n\nimport type { RenderContext, ReplyResponse } from '@seedcord/types';\n\n/**\n * A generic fault you throw after catching an error you do not have a specific message for.\n *\n * The user sees a fixed generic reply with the tracking uuid, never the cause. `report` defaults to\n * `true`, so the framework logs it and publishes it to the `handledException` bus. Pass `report: false`\n * to show the generic reply without the bus publish. The original error is stored as the standard\n * `cause`, so the real stack reaches the webhook. For a fault the user should see a real message,\n * subclass {@link Notice} and write your own `render` instead.\n *\n * The framework also renders this for an unhandled throw, where it points the user at\n * `ctx.developerUsername`.\n *\n * @example\n * ```ts\n * import { Fault } from 'seedcord';\n *\n * try {\n * await db.write(record);\n * } catch (cause) {\n * // user sees the generic reply with the uuid, the real error rides along as cause for the webhook\n * throw new Fault({ cause });\n *\n * // pass report: false to show the same reply without publishing to the handledException bus\n * // throw new Fault({ cause, report: false });\n * }\n * ```\n */\nexport class Fault extends Notice {\n public constructor(options?: { cause?: unknown; report?: boolean }) {\n super('A fault occurred', options?.cause === undefined ? undefined : { cause: options.cause });\n this.report = options?.report ?? true;\n }\n\n public render(ctx: RenderContext): ReplyResponse {\n const contact = ctx.developerUsername ?? 'the developer';\n const card = new NoticeCard(\n `Something went wrong. Please reach out to ${contact} with a way to reproduce the error and the following:\\n### UUID: \\`${ctx.uuid}\\``,\n 'Error'\n );\n return { components: [card.component] };\n }\n}\n","import { NeedsAny, NotAllowed } from '@notices/index';\nimport { Fault } from '@stops/Fault';\nimport { Notice } from '@stops/Notice';\n\nimport { markCommits, rollbackCommits, runCheck } from './effects';\nimport { defineGate } from './Gate';\n\nimport type { Gate, GateContextBase, RequiredOf } from './Gate';\nimport type { IntersectRequired, JoinNames, TwoOrMore } from './matching';\n\n/**\n * Runs each gate in order and refuses on the first refusal. Takes two or more arms. The required\n * context is the intersection of the arms, so an event-only and an interaction-only gate cannot be\n * combined.\n *\n * @typeParam Gates - The tuple of two or more gate arms, run left to right.\n * @param gates - The gate arms to run in order, all refused on the first refusal.\n *\n * @see {@link or}\n *\n * @example\n * ```ts\n * import { PermissionFlagsBits } from 'discord.js';\n *\n * \\@Gated(and(GuildOnly(), RequirePermissions([PermissionFlagsBits.BanMembers])))\n * \\@SlashRoute('ban')\n * class BanHandler extends SlashHandler<'ban'> {\n * async execute() {\n * // ...\n * }\n * }\n * ```\n */\nexport function and<Gates extends TwoOrMore<Gate<GateContextBase>>>(\n ...gates: Gates\n): Gate<IntersectRequired<Gates> & GateContextBase, JoinNames<Gates, ' & '>>;\nexport function and(...gates: readonly Gate<GateContextBase>[]): Gate<GateContextBase> {\n return defineGate(gates.map((gate) => gate.name).join(' & '), async (ctx) => {\n for (const gate of gates) {\n await runCheck(gate, ctx);\n }\n });\n}\n\n// the caller-supplied refusal when every or arm refuses, replacing the default list or neutral refusal\ninterface OrOptions {\n fail: Notice | ((ctx: GateContextBase) => Notice);\n}\n\n// a gate has a check method, the options object does not\nfunction isOrOptions(arg: Gate<GateContextBase> | OrOptions): arg is OrOptions {\n return !('check' in arg);\n}\n\n/**\n * Runs each gate in order and passes on the first arm that passes. Takes two or more arms. The\n * required context is the union of the arms, so a handler that matches any one arm fits. When every\n * arm refuses it throws the trailing {@link OrOptions} `fail` if given, else an auto list of the\n * arms derived from the summary field, else a default refusal. The trailing options object does not\n * count as an arm.\n *\n * @typeParam Gates - The tuple of two or more gate arms, tried left to right.\n * @param gates - The gate arms to try in order, with an optional trailing {@link OrOptions} object.\n *\n * @see {@link and}\n *\n * @example\n * ```ts\n * \\@Gated(or(OwnerOnly(), RequireRole('123456789012345678')))\n * \\@SlashRoute('admin')\n * class AdminHandler extends SlashHandler<'admin'> {\n * async execute() {\n * // ...\n * }\n * }\n * ```\n */\nexport function or<Gates extends TwoOrMore<Gate<GateContextBase>>>(\n ...gates: Gates\n): Gate<RequiredOf<Gates[number]>, JoinNames<Gates, ' | '>>;\n/**\n * @example\n * ```ts\n * // custom refusal when every arm refuses\n * or(OwnerOnly(), RequireRole('123456789012345678'), { fail: new NotAllowedNotice() });\n * ```\n */\nexport function or<Gates extends TwoOrMore<Gate<GateContextBase>>>(\n ...args: [...Gates, OrOptions]\n): Gate<RequiredOf<Gates[number]>, JoinNames<Gates, ' | '>>;\nexport function or(...args: readonly (Gate<GateContextBase> | OrOptions)[]): Gate<GateContextBase> {\n const last = args.at(-1);\n const options = last !== undefined && isOrOptions(last) ? last : undefined;\n const gates = (options ? args.slice(0, -1) : args) as readonly Gate<GateContextBase>[];\n\n return defineGate(gates.map((gate) => gate.name).join(' | '), async (ctx) => {\n const summaries: string[] = [];\n let everyArmHasSummary = true;\n for (const gate of gates) {\n const mark = markCommits(ctx);\n try {\n await runCheck(gate, ctx);\n return;\n } catch (error) {\n // only a refusal is an arm declining, a Fault (even report-false), a Silence, or a raw error stops everything\n if (error instanceof Notice && !(error instanceof Fault) && !error.report) {\n // the arm may have queued an effect's commit before refusing, drop it so the winner does not carry it\n rollbackCommits(ctx, mark);\n if (error.summary === undefined) everyArmHasSummary = false;\n else summaries.push(error.summary);\n continue;\n }\n throw error;\n }\n }\n if (options) throw typeof options.fail === 'function' ? options.fail(ctx) : options.fail;\n // the auto-list shows only when every refusal gave a summary, a partial list would mislead by omission\n if (everyArmHasSummary && summaries.length > 0) throw new NeedsAny(summaries);\n throw new NotAllowed();\n });\n}\n","import { defineGate } from '@gates/Gate';\nimport { NotInDm, NotInGuild, NotOwner } from '@notices/index';\n\nimport { pickNotice } from './options';\n\nimport type { GateNoticeOptions } from './options';\nimport type { Gate, GateContextBase } from '@gates/Gate';\n\n/**\n * Passes only for a user id listed in `config.ownerIds`, else refuses.\n *\n * 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.\n *\n * @param options - Reword the default refusal with `message`, or replace it with `notice`.\n *\n * @see the `@Gated` decorator from `seedcord`\n *\n * @example\n * ```ts\n * \\@Gated(OwnerOnly())\n * \\@SlashRoute('shutdown')\n * class ShutdownHandler extends SlashHandler<'shutdown'> {\n * async execute() {\n * // ...\n * }\n * }\n * ```\n *\n * @example\n * ```ts\n * // reword the refusal, keeping the embed styling\n * OwnerOnly({ message: 'Owners only.' });\n * ```\n */\nexport function OwnerOnly(options?: GateNoticeOptions): Gate<GateContextBase, 'OwnerOnly'> {\n return defineGate('OwnerOnly', (ctx) => {\n const owners = ctx.core.config.ownerIds ?? [];\n if (ctx.userId !== null && owners.includes(ctx.userId)) return;\n throw pickNotice(options, (message) => new NotOwner(message));\n });\n}\n\n/**\n * Passes inside a guild, else refuses.\n *\n * Agnostic, so it attaches to any handler kind. Often paired with `RequirePermissions` in an {@link and}.\n *\n * @param options - Reword the default refusal with `message`, or replace it with `notice`.\n *\n * @see the `@Gated` decorator from `seedcord`\n *\n * @example\n * ```ts\n * import { PermissionFlagsBits } from 'discord.js';\n *\n * \\@Gated(GuildOnly(), RequirePermissions([PermissionFlagsBits.Administrator]))\n * \\@SlashRoute('maintenance')\n * class Maintenance extends SlashHandler<'maintenance'> {\n * async execute() {\n * // ...\n * }\n * }\n * ```\n */\nexport function GuildOnly(options?: GateNoticeOptions): Gate<GateContextBase, 'GuildOnly'> {\n return defineGate('GuildOnly', (ctx) => {\n if (ctx.guildId) return;\n throw pickNotice(options, (message) => new NotInGuild(message));\n });\n}\n\n/**\n * Passes in a direct message, else refuses.\n *\n * Agnostic, so it attaches to any handler kind. The inverse of {@link GuildOnly}, so combining the two in an {@link and} can never pass.\n *\n * @param options - Reword the default refusal with `message`, or replace it with `notice`.\n *\n * @see the `@Gated` decorator from `seedcord`\n *\n * @example\n * ```ts\n * \\@Gated(DmOnly())\n * \\@SlashRoute('verify')\n * class VerifyHandler extends SlashHandler<'verify'> {\n * async execute() {\n * // ...\n * }\n * }\n * ```\n */\nexport function DmOnly(options?: GateNoticeOptions): Gate<GateContextBase, 'DmOnly'> {\n return defineGate('DmOnly', (ctx) => {\n if (!ctx.guildId) return;\n throw pickNotice(options, (message) => new NotInDm(message));\n });\n}\n","import { SeedcordErrorCode } from '@seedcord/errors';\nimport { SeedcordTypeError } from '@seedcord/errors/internal';\nimport { parseDuration, type ValidDuration } from '@seedcord/utils';\n\nimport { defineEffectGate } from '@gates/Gate';\nimport { OnCooldown } from '@notices/index';\n\nimport type { EffectGate, GateContextBase } from '@gates/Gate';\nimport type { EpochMs } from '@seedcord/types';\nimport type { Notice } from '@stops/Notice';\n\n/**\n * Options for {@link Cooldown}. `per` sets the bucket the window applies to and `limit` the uses allowed per\n * window. `message` rewords the refusal and `notice` replaces it, both receiving the epoch ms the key frees\n * up so the refusal can show the retry time.\n *\n * @example\n * ```ts\n * // one use per channel, rewording the refusal with the retry time\n * Cooldown('10s', {\n * per: 'channel',\n * message: (resetAt) => `Slow down. Try again <t:${Math.round(resetAt / 1000)}:R>.`\n * });\n * ```\n *\n * @example\n * ```ts\n * // five uses per minute, per user\n * Cooldown('1m', { limit: 5 });\n * ```\n */\nexport interface CooldownOptions {\n /**\n * The bucket the cooldown window applies to. 'user' scopes by user ID, falls back to global when the source carries no user id. 'guild' scopes by guild ID (falls back to global if no guild), and 'channel' scopes by channel ID (falls back to global if no channel).\n *\n * @defaultValue `'user'`\n */\n per?: 'user' | 'guild' | 'channel';\n /**\n * Uses allowed inside one window before the gate refuses.\n *\n * @defaultValue `1`\n */\n limit?: number;\n /**\n * Reword the refusal, keeping the standard notice card. Receives the epoch ms the key frees up, so the\n * text can include the retry time with `<t:${Math.round(resetAt / 1000)}:R>`.\n */\n message?: (resetAt: EpochMs) => string;\n /**\n * Replace the refusal Notice entirely, for full control or a translated copy. Receives the epoch ms the\n * key frees up.\n */\n notice?: (resetAt: EpochMs) => Notice;\n}\n\n// each Cooldown() gets its own bucket, so two handlers that both use a cooldown do not share one window\nlet bucketSeq = 0;\n\nfunction scopeValue(ctx: GateContextBase, per: 'user' | 'guild' | 'channel'): string {\n if (per === 'guild') return ctx.guildId ?? 'global';\n if (per === 'channel') return ctx.channelId ?? 'global';\n return ctx.userId ?? 'global';\n}\n\n/**\n * Allows `limit` uses per window, scoped by `per`. A number `duration` is **seconds**, a string is\n * a duration like `30m` or `24h`. An unparseable string throws a **SeedcordTypeError** at construction. The\n * slot is charged in commit, only after the whole gate set passes, so a later refusal never burns the cooldown.\n * That split also means two requests racing the same key can both pass before either charges the slot.\n * Each call gets its own bucket, so two handlers never share a window. Reword the refusal with {@link CooldownOptions.message}\n * or replace it with {@link CooldownOptions.notice}.\n *\n * @param duration - A number is seconds, a string is a duration like `30m` or `24h`. An unparseable string throws a **SeedcordTypeError**.\n * @param options - Sets the scope with `per`, the uses per window with `limit`, and the refusal text with `message` or `notice`.\n *\n * @see the `@Gated` decorator from `seedcord`\n * @see {@link IRateLimiter}\n *\n * @example\n * ```ts\n * // a number argument is SECONDS\n * \\@Gated(Cooldown(5))\n * \\@SlashRoute('daily')\n * class DailyHandler extends SlashHandler<'daily'> {\n * async execute() {\n * // ...\n * }\n * }\n * ```\n *\n * @example\n * ```ts\n * // a string is a duration, here scoped per guild (the default is per user)\n * Cooldown('30m', { per: 'guild' });\n * ```\n *\n * @example\n * ```ts\n * // a burst, 3 uses per 10 minutes per user, then it refuses until a slot frees\n * \\@Gated(Cooldown('10m', { limit: 3 }))\n * \\@SlashRoute('report')\n * class ReportHandler extends SlashHandler<'report'> {\n * async execute() {\n * // ...\n * }\n * }\n * ```\n */\nexport function Cooldown(\n duration: number | ValidDuration,\n options?: CooldownOptions\n): EffectGate<GateContextBase, 'Cooldown'> {\n let windowMs: number;\n if (typeof duration === 'number') {\n windowMs = duration * 1000;\n } else {\n const parsed = parseDuration(duration);\n if (parsed === null) throw new SeedcordTypeError(SeedcordErrorCode.GateInvalidCooldownDuration, [duration]);\n windowMs = parsed;\n }\n // a zero or negative window would never limit, refuse it the same way an unparseable string is refused\n if (windowMs <= 0) {\n throw new SeedcordTypeError(SeedcordErrorCode.GateInvalidCooldownDuration, [String(duration)]);\n }\n\n const bucket = `cooldown:${bucketSeq++}`;\n const per = options?.per ?? 'user';\n // omit limit when unset so the limiter applies its default of 1, exactOptionalPropertyTypes rejects an explicit undefined\n const window = options?.limit === undefined ? { windowMs } : { windowMs, limit: options.limit };\n const keyOf = (ctx: GateContextBase): string => `${bucket}:${scopeValue(ctx, per)}`;\n\n return defineEffectGate(\n 'Cooldown',\n async (ctx) => {\n const result = await ctx.core.rateLimiter.peek(keyOf(ctx), window);\n if (!result.limited) return;\n if (options?.notice) throw options.notice(result.resetAt);\n throw new OnCooldown(result.resetAt, options?.message?.(result.resetAt));\n },\n async (ctx) => {\n await ctx.core.rateLimiter.charge(keyOf(ctx), window);\n }\n );\n}\n","/**\n * Throw to stop a handler with no reply and no report.\n *\n * The framework boundary catches `Silence` before {@link Notice}, makes zero Discord calls, and stops.\n * Ideally you'd only throw this in `EventHandlers` (or `Gates` for those), because it doesn't make sense to\n * leave the user with no reply for an interaction.\n *\n * @example\n * ```ts\n * import { Silence } from 'seedcord';\n *\n * // before any reply or defer, drop the interaction with no reply and no report\n * if (await isBlacklisted(interaction.user.id)) throw new Silence('blacklisted user');\n * ```\n */\nexport class Silence extends Error {\n /**\n * @param reason - Optional note written only to a debug log, never shown to the user or reported.\n */\n public constructor(public readonly reason?: string) {\n super(reason ?? 'Silence');\n\n Error.captureStackTrace(this, this.constructor);\n }\n}\n","import { SeedcordErrorCode } from '@seedcord/errors';\nimport { SeedcordRangeError } from '@seedcord/errors/internal';\n\nimport type { PageView } from './PageView';\n\n/**\n * Pure page math, usable headless. Clamps `page` into range and slices the window.\n *\n * @param items - The full list to page over.\n * @param page - The requested zero-based page, clamped into `[0, totalPages - 1]`.\n * @param perPage - The page size. Must be a positive integer.\n * @returns The sliced {@link PageView}, with `totalPages` always known.\n * @throws SeedcordRangeError when `perPage` is not a positive integer.\n */\nexport function paginate<Item>(items: readonly Item[], page: number, perPage: number): PageView<Item> {\n if (!Number.isSafeInteger(perPage) || perPage <= 0) {\n throw new SeedcordRangeError(SeedcordErrorCode.PaginationInvalidPerPage, [perPage]);\n }\n\n const totalPages = Math.max(1, Math.ceil(items.length / perPage));\n // a headless caller can pass a non-integer or out-of-range page.\n const clamped = Math.min(Math.max(Math.trunc(page), 0), totalPages - 1);\n const start = clamped * perPage;\n\n return {\n items: items.slice(start, start + perPage),\n page: clamped,\n perPage,\n totalPages,\n hasPrev: clamped > 0,\n hasNext: clamped < totalPages - 1\n };\n}\n","import 'reflect-metadata';\n\nexport type * from '@registries/ContextMenuRegistry';\nexport type * from '@registries/SlashOptionRegistry';\n\nexport { RegisterCommand } from '@decorators/Command';\n\nexport type { CoreBase } from '@interfaces/CoreBase';\n\nexport { defineEffectGate, defineGate } from '@gates/Gate';\nexport type { EffectGate, Gate, GateContextBase, RequiredOf } from '@gates/Gate';\nexport { and, or } from '@gates/combinators';\nexport { DmOnly, GuildOnly, OwnerOnly } from '@gates/catalog/access';\nexport { Cooldown, type CooldownOptions } from '@gates/catalog/Cooldown';\nexport type { GateNoticeOptions } from '@gates/catalog/options';\n\nexport { BuilderComponent, RowComponent } from '@components/Component';\nexport { type RowType, type BuilderType } from '@components/builderTypes';\n\nexport { Notice } from '@stops/Notice';\nexport { Fault } from '@stops/Fault';\nexport { Silence } from '@stops/Silence';\n\nexport { CustomId } from '@customId/CustomId';\n\nexport { paginate } from '@pagination/paginate';\nexport { type PageView } from '@pagination/PageView';\n\n/** Package version */\nexport const version = process.env.PACKAGE_VERSION ?? '0.0.0';\n"],"mappings":";;;;;;;;AAkDA,SAAgB,gBAAgB,OAAqB,SAAmB,CAAC,GAAG;CACxE,QAAQ,SAA4B;EAChC,MAAM,eAAe,QAAQ,eAAeA,qCAAoB,IAAI;EACpE,IAAI,cACA,MAAM,IAAIC,wCAAcC,mCAAkB,mCAAmC;GACzE,KAAK;GACL,aAAa;GACb;EACJ,CAAC;EAGL,IAAI,UAAU,YAAY,OAAO,SAAS,GACtC,MAAM,IAAID,wCAAcC,mCAAkB,gCAAgC;EAG9E,IAAI,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM,KAAK,OAAO,WAAW,IAClE,MAAM,IAAID,wCAAcC,mCAAkB,kCAAkC;EAGhF,MAAM,OAA+B,UAAU,WAAW,EAAE,MAAM,IAAI;GAAE;GAAO;EAAO;EACtF,QAAQ,eAAeF,qCAAoB,MAAM,IAAI;CACzD;AACJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACmEA,SAAgB,WACZ,MACA,IACe;CACf,OAAO;EAAE;EAAM,OAAO,OAAO,QAAQ,GAAG,GAAG;CAAE;AACjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,SAAgB,iBACZ,MACA,OACA,QACqB;CACrB,OAAO;EACH;EACA,OAAO,OAAO,QAAQ,MAAM,GAAG;EAC/B,QAAQ,OAAO,QAAQ,OAAO,GAAG;CACrC;AACJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC/JA,IAAa,QAAb,cAA2BG,wBAAO;CAC9B,AAAO,YAAY,SAAiD;EAChE,MAAM,oBAAoB,SAAS,UAAU,SAAY,SAAY,EAAE,OAAO,QAAQ,MAAM,CAAC;EAC7F,KAAK,SAAS,SAAS,UAAU;CACrC;CAEA,AAAO,OAAO,KAAmC;EAM7C,OAAO,EAAE,YAAY,CAAC,IAJLC,4BACb,6CAFY,IAAI,qBAAqB,gBAEgB,qEAAqE,IAAI,KAAK,KACnI,OAEqB,CAAC,CAAC,SAAS,EAAE;CAC1C;AACJ;;;;ACVA,SAAgB,IAAI,GAAG,OAAgE;CACnF,OAAO,WAAW,MAAM,KAAK,SAAS,KAAK,IAAI,CAAC,CAAC,KAAK,KAAK,GAAG,OAAO,QAAQ;EACzE,KAAK,MAAM,QAAQ,OACf,MAAMC,0BAAS,MAAM,GAAG;CAEhC,CAAC;AACL;AAQA,SAAS,YAAY,KAA0D;CAC3E,OAAO,EAAE,WAAW;AACxB;AAsCA,SAAgB,GAAG,GAAG,MAA6E;CAC/F,MAAM,OAAO,KAAK,GAAG,EAAE;CACvB,MAAM,UAAU,SAAS,UAAa,YAAY,IAAI,IAAI,OAAO;CACjE,MAAM,QAAS,UAAU,KAAK,MAAM,GAAG,EAAE,IAAI;CAE7C,OAAO,WAAW,MAAM,KAAK,SAAS,KAAK,IAAI,CAAC,CAAC,KAAK,KAAK,GAAG,OAAO,QAAQ;EACzE,MAAM,YAAsB,CAAC;EAC7B,IAAI,qBAAqB;EACzB,KAAK,MAAM,QAAQ,OAAO;GACtB,MAAM,OAAOC,6BAAY,GAAG;GAC5B,IAAI;IACA,MAAMD,0BAAS,MAAM,GAAG;IACxB;GACJ,SAAS,OAAO;IAEZ,IAAI,iBAAiBE,2BAAU,EAAE,iBAAiB,UAAU,CAAC,MAAM,QAAQ;KAEvE,iCAAgB,KAAK,IAAI;KACzB,IAAI,MAAM,YAAY,QAAW,qBAAqB;UACjD,UAAU,KAAK,MAAM,OAAO;KACjC;IACJ;IACA,MAAM;GACV;EACJ;EACA,IAAI,SAAS,MAAM,OAAO,QAAQ,SAAS,aAAa,QAAQ,KAAK,GAAG,IAAI,QAAQ;EAEpF,IAAI,sBAAsB,UAAU,SAAS,GAAG,MAAM,IAAIC,0BAAS,SAAS;EAC5E,MAAM,IAAIC,4BAAW;CACzB,CAAC;AACL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACtFA,SAAgB,UAAU,SAAiE;CACvF,OAAO,WAAW,cAAc,QAAQ;EACpC,MAAM,SAAS,IAAI,KAAK,OAAO,YAAY,CAAC;EAC5C,IAAI,IAAI,WAAW,QAAQ,OAAO,SAAS,IAAI,MAAM,GAAG;EACxD,MAAMC,4BAAW,UAAU,YAAY,IAAIC,0BAAS,OAAO,CAAC;CAChE,CAAC;AACL;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,UAAU,SAAiE;CACvF,OAAO,WAAW,cAAc,QAAQ;EACpC,IAAI,IAAI,SAAS;EACjB,MAAMD,4BAAW,UAAU,YAAY,IAAIE,4BAAW,OAAO,CAAC;CAClE,CAAC;AACL;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,OAAO,SAA8D;CACjF,OAAO,WAAW,WAAW,QAAQ;EACjC,IAAI,CAAC,IAAI,SAAS;EAClB,MAAMF,4BAAW,UAAU,YAAY,IAAIG,yBAAQ,OAAO,CAAC;CAC/D,CAAC;AACL;;;;ACvCA,IAAI,YAAY;AAEhB,SAAS,WAAW,KAAsB,KAA2C;CACjF,IAAI,QAAQ,SAAS,OAAO,IAAI,WAAW;CAC3C,IAAI,QAAQ,WAAW,OAAO,IAAI,aAAa;CAC/C,OAAO,IAAI,UAAU;AACzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,SAAgB,SACZ,UACA,SACuC;CACvC,IAAI;CACJ,IAAI,OAAO,aAAa,UACpB,WAAW,WAAW;MACnB;EACH,MAAM,4CAAuB,QAAQ;EACrC,IAAI,WAAW,MAAM,MAAM,IAAIC,4CAAkBC,mCAAkB,6BAA6B,CAAC,QAAQ,CAAC;EAC1G,WAAW;CACf;CAEA,IAAI,YAAY,GACZ,MAAM,IAAID,4CAAkBC,mCAAkB,6BAA6B,CAAC,OAAO,QAAQ,CAAC,CAAC;CAGjG,MAAM,SAAS,YAAY;CAC3B,MAAM,MAAM,SAAS,OAAO;CAE5B,MAAM,SAAS,SAAS,UAAU,SAAY,EAAE,SAAS,IAAI;EAAE;EAAU,OAAO,QAAQ;CAAM;CAC9F,MAAM,SAAS,QAAiC,GAAG,OAAO,GAAG,WAAW,KAAK,GAAG;CAEhF,OAAO,iBACH,YACA,OAAO,QAAQ;EACX,MAAM,SAAS,MAAM,IAAI,KAAK,YAAY,KAAK,MAAM,GAAG,GAAG,MAAM;EACjE,IAAI,CAAC,OAAO,SAAS;EACrB,IAAI,SAAS,QAAQ,MAAM,QAAQ,OAAO,OAAO,OAAO;EACxD,MAAM,IAAIC,4BAAW,OAAO,SAAS,SAAS,UAAU,OAAO,OAAO,CAAC;CAC3E,GACA,OAAO,QAAQ;EACX,MAAM,IAAI,KAAK,YAAY,OAAO,MAAM,GAAG,GAAG,MAAM;CACxD,CACJ;AACJ;;;;;;;;;;;;;;;;;;;ACjIA,IAAa,UAAb,cAA6B,MAAM;CAII;;;;CAAnC,AAAO,YAAY,AAAgB,QAAiB;EAChD,MAAM,UAAU,SAAS;EADM;EAG/B,MAAM,kBAAkB,MAAM,KAAK,WAAW;CAClD;AACJ;;;;;;;;;;;;;ACVA,SAAgB,SAAe,OAAwB,MAAc,SAAiC;CAClG,IAAI,CAAC,OAAO,cAAc,OAAO,KAAK,WAAW,GAC7C,MAAM,IAAIC,6CAAmBC,mCAAkB,0BAA0B,CAAC,OAAO,CAAC;CAGtF,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,KAAK,MAAM,SAAS,OAAO,CAAC;CAEhE,MAAM,UAAU,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,IAAI,GAAG,CAAC,GAAG,aAAa,CAAC;CACtE,MAAM,QAAQ,UAAU;CAExB,OAAO;EACH,OAAO,MAAM,MAAM,OAAO,QAAQ,OAAO;EACzC,MAAM;EACN;EACA;EACA,SAAS,UAAU;EACnB,SAAS,UAAU,aAAa;CACpC;AACJ;;;;;ACHA,MAAa"}
|