@seedcord/core 0.9.4 → 0.11.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +28 -10
  2. package/dist/CoordinatedShutdown-H5YjZn8A.d.mts +78 -0
  3. package/dist/CoordinatedStartup-CGnJrqcl.mjs +168 -0
  4. package/dist/CoordinatedStartup-CGnJrqcl.mjs.map +1 -0
  5. package/dist/{HmrModuleHandler-DOAtah3j.mjs → HmrModuleHandler-8ZC-UuZh.mjs} +9 -22
  6. package/dist/HmrModuleHandler-8ZC-UuZh.mjs.map +1 -0
  7. package/dist/PluginHost-B8kxLOeb.mjs +245 -0
  8. package/dist/PluginHost-B8kxLOeb.mjs.map +1 -0
  9. package/dist/{Plugin-Dhx_ivqj.d.mts → PluginHost-DB4j8unu.d.mts} +110 -17
  10. package/dist/PluginLifecycle-AhQ46NIr.mjs +158 -0
  11. package/dist/PluginLifecycle-AhQ46NIr.mjs.map +1 -0
  12. package/dist/ServerHost-CvGQr7oN.mjs +191 -0
  13. package/dist/ServerHost-CvGQr7oN.mjs.map +1 -0
  14. package/dist/ServerHost-DYeh0KTk.d.mts +40 -0
  15. package/dist/hmr.index.d.mts +0 -1
  16. package/dist/hmr.index.mjs +1 -1
  17. package/dist/{cursor-BTU5bGoo.d.mts → index-Du71Xx69.d.mts} +253 -6
  18. package/dist/index.d.mts +19 -84
  19. package/dist/index.mjs +17 -37
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/internal.index.d.mts +185 -22
  22. package/dist/internal.index.mjs +713 -19
  23. package/dist/internal.index.mjs.map +1 -1
  24. package/dist/node-internal.index.d.mts +25 -70
  25. package/dist/node-internal.index.mjs +84 -298
  26. package/dist/node-internal.index.mjs.map +1 -1
  27. package/dist/node.index.d.mts +3 -2
  28. package/dist/node.index.mjs +53 -200
  29. package/dist/node.index.mjs.map +1 -1
  30. package/dist/{phases-D_zQBXga.d.mts → phases-FTM69J3a.d.mts} +4 -4
  31. package/dist/plugin.index.d.mts +2 -2
  32. package/dist/plugin.index.mjs +2 -2
  33. package/dist/publishDefault-D3dAyzZ9.mjs +7 -0
  34. package/dist/publishDefault-D3dAyzZ9.mjs.map +1 -0
  35. package/dist/subscribers-Ce3SWy23.mjs +1913 -0
  36. package/dist/subscribers-Ce3SWy23.mjs.map +1 -0
  37. package/package.json +17 -16
  38. package/dist/Bus-BxPa-Uvp.mjs +0 -866
  39. package/dist/Bus-BxPa-Uvp.mjs.map +0 -1
  40. package/dist/HmrModuleHandler-DOAtah3j.mjs.map +0 -1
  41. package/dist/Pluggable-CXyUZIvT.mjs +0 -310
  42. package/dist/Pluggable-CXyUZIvT.mjs.map +0 -1
  43. package/dist/Pluggable-CwKbLui3.d.mts +0 -162
  44. package/dist/Plugin-CmNW5n6a.mjs +0 -122
  45. package/dist/Plugin-CmNW5n6a.mjs.map +0 -1
  46. package/dist/contextMenuLeaves-COzDYa6c.d.mts +0 -17
  47. package/dist/cursor-CgNBnJfu.mjs +0 -802
  48. package/dist/cursor-CgNBnJfu.mjs.map +0 -1
  49. package/dist/devChannel-CQU7hq7Q.mjs +0 -14
  50. package/dist/devChannel-CQU7hq7Q.mjs.map +0 -1
  51. package/dist/index-B_-EjyvS.d.mts +0 -135
  52. package/dist/publishDefault-BuSZPbjT.mjs +0 -14
  53. package/dist/publishDefault-BuSZPbjT.mjs.map +0 -1
  54. package/dist/slashRouteLeaves-k1hJnXDO.mjs +0 -31
  55. package/dist/slashRouteLeaves-k1hJnXDO.mjs.map +0 -1
  56. package/dist/subscribers-xGy7a7xR.mjs +0 -167
  57. package/dist/subscribers-xGy7a7xR.mjs.map +0 -1
  58. package/dist/types-BG0BimVr.d.mts +0 -13
@@ -1,802 +0,0 @@
1
- import { A as BuilderComponent, C as InteractionRouteKeys, S as InteractionMiddlewareMetadataKey, b as InteractionKind, x as InteractionMetadataKey } from "./Bus-BxPa-Uvp.mjs";
2
- import { n as asError, t as PublishDefault } from "./publishDefault-BuSZPbjT.mjs";
3
- import "reflect-metadata";
4
- import { CustomId, decodeFor } from "@seedcord/custom-id";
5
- import { TextDisplayBuilder } from "@discordjs/builders";
6
- import { ApplicationCommandType, PermissionFlagsBits } from "discord-api-types/v10";
7
- import { SeedcordErrorCode, paint } from "@seedcord/errors";
8
- import { SeedcordError, SeedcordTypeError } from "@seedcord/errors/internal";
9
- import { Logger } from "@seedcord/logger";
10
- import { prettify, toEpochSeconds } from "@seedcord/utils";
11
-
12
- //#region src/stops/Notice.ts
13
- /**
14
- * Base class for a user-facing refusal or a reported fault.
15
- *
16
- * Throw a `Notice` to stop a handler and reply to the user. The framework catches it at the controller
17
- * boundary and renders {@link Notice.render}, which always decides what the user sees. With `report`
18
- * false that render is all that happens. With `report` true the framework also logs the fault and
19
- * publishes it to the `handledException` bus. A raw, non-Notice throw shows the generic message.
20
- *
21
- * @example
22
- * ```ts
23
- * import { Notice, BuilderComponent, type RenderContext, type ReplyResponse } from '@seedcord/gateway';
24
- * import { TextDisplayBuilder } from '@discordjs/builders';
25
- *
26
- * // reading `.component` applies the configured bot color to the container accent
27
- * class TooPoorCard extends BuilderComponent<'container'> {
28
- * constructor(balance: number) {
29
- * super('container');
30
- * this.instance.addTextDisplayComponents(
31
- * new TextDisplayBuilder().setContent(`### Insufficient balance\nYou need more than ${balance} coins.`)
32
- * );
33
- * }
34
- * }
35
- *
36
- * class TooPoor extends Notice {
37
- * constructor(private readonly balance: number) {
38
- * super(`balance ${balance} is below the cost`);
39
- * }
40
- *
41
- * render(_ctx: RenderContext): ReplyResponse {
42
- * return { components: [new TooPoorCard(this.balance).component] };
43
- * }
44
- * }
45
- *
46
- * // in a handler, throwing stops the handler and replies with render(ctx)
47
- * if (wallet.balance < cost) throw new TooPoor(wallet.balance);
48
- * ```
49
- */
50
- var Notice = class extends Error {
51
- /**
52
- * Whether this denial is a reported fault. True also logs it and publishes it to the `handledException`
53
- * bus. The user always sees {@link Notice.render} either way.
54
- *
55
- * @defaultValue `false`
56
- */
57
- report = false;
58
- /**
59
- * Whether the reply is ephemeral, seen only by the invoking user. Set it false for a refusal the
60
- * whole channel should see.
61
- *
62
- * @defaultValue `true`
63
- */
64
- ephemeral = true;
65
- /**
66
- * A short one-line reason. When every arm of an {@link or} gate refuses and each one sets this, `or`
67
- * throws a refusal listing them under `You need to meet at least one of these:`. Write a verb phrase that
68
- * finishes that line, for example, "be the bot owner".
69
- */
70
- summary;
71
- constructor(message, options) {
72
- super(message, options);
73
- this.name = new.target.name;
74
- Error.captureStackTrace(this, this.constructor);
75
- }
76
- };
77
-
78
- //#endregion
79
- //#region src/stops/NoticeCard.ts
80
- var NoticeCard = class extends BuilderComponent {
81
- constructor(description, title = "Cannot Proceed") {
82
- super("container");
83
- this.instance.addTextDisplayComponents(new TextDisplayBuilder().setContent(`### ${title}\n${description}`));
84
- }
85
- };
86
-
87
- //#endregion
88
- //#region src/customId/routing.ts
89
- /**
90
- * route decorators store a handler's customId defs here, so the component base can decode
91
- * against them at runtime.
92
- *
93
- * @internal
94
- */
95
- const ComponentDefsKey = Symbol.for("seedcord:custom-id:component-defs");
96
- /**
97
- * throws `CustomIdHandlerRouteMissing` when the class has no stored defs. a drifted or corrupt wire
98
- * throws `StaleCustomId` or `InvalidCustomId`.
99
- *
100
- * @internal
101
- */
102
- function decodeComponentRoute(handlerClass, wire) {
103
- const defs = Reflect.getMetadata(ComponentDefsKey, handlerClass);
104
- if (!defs) throw new SeedcordError(SeedcordErrorCode.CustomIdHandlerRouteMissing, [handlerClass.name]);
105
- return decodeFor(defs, wire);
106
- }
107
-
108
- //#endregion
109
- //#region src/decorators/interactionRoutes.ts
110
- function areRoutes(routes) {
111
- return Array.isArray(routes) && routes.every((route) => typeof route === "string");
112
- }
113
- function interactionRoutesOf(constructor) {
114
- const pairs = [];
115
- for (const route of Object.values(InteractionKind)) {
116
- const meta = Reflect.getMetadata(InteractionRouteKeys[route], constructor);
117
- if (areRoutes(meta)) pairs.push([route, meta]);
118
- }
119
- return pairs;
120
- }
121
- function storeInteractionRoute(route, routes, constructor) {
122
- const key = InteractionRouteKeys[route];
123
- const saved = Reflect.getMetadata(key, constructor);
124
- const existing = areRoutes(saved) ? saved : [];
125
- const toStore = typeof routes === "string" ? [routes] : routes;
126
- Reflect.defineMetadata(key, [...existing, ...toStore], constructor);
127
- Reflect.defineMetadata(InteractionMetadataKey, true, constructor);
128
- }
129
- function storeComponentRoute(route, defs, constructor) {
130
- storeInteractionRoute(route, defs.map((def) => def.prefix), constructor);
131
- Reflect.defineMetadata(ComponentDefsKey, defs, constructor);
132
- }
133
- function contextMenuRouteOf(kind) {
134
- return kind === ApplicationCommandType.User ? "userContextMenu" : "messageContextMenu";
135
- }
136
-
137
- //#endregion
138
- //#region src/decorators/middleware.ts
139
- /**
140
- * Registers an interaction middleware. It runs after the dispatcher builds the handler and before its
141
- * gates, over that handler's reply surface.
142
- *
143
- * @param options - The kinds this middleware runs on and its ordering.
144
- * @decorator
145
- *
146
- * @example
147
- * ```ts
148
- * \@RegisterInteractionMiddleware({ kinds: [InteractionKind.Button], priority: 10 })
149
- * class Audit extends InteractionMiddleware<InteractionKind.Button> {
150
- * async execute() {
151
- * this.logger.info(this.dispatch.routeId);
152
- * }
153
- * }
154
- * ```
155
- *
156
- * @throws A **SeedcordTypeError** If `priority` is not a finite number.
157
- */
158
- function RegisterInteractionMiddleware(options = {}) {
159
- return function(ctor) {
160
- const priority = Number(options.priority ?? 0);
161
- if (!Number.isFinite(priority)) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorInvalidMiddlewarePriority);
162
- if (options.kinds?.length === 0) throw new SeedcordTypeError(SeedcordErrorCode.DecoratorEmptyMiddlewareFilter, ["kinds"]);
163
- const metadata = {
164
- priority,
165
- ...options.kinds && { kinds: options.kinds }
166
- };
167
- Reflect.defineMetadata(InteractionMiddlewareMetadataKey, metadata, ctor);
168
- };
169
- }
170
- /** @internal */
171
- function interactionMiddlewareMetaOf(constructor) {
172
- return Reflect.getMetadata(InteractionMiddlewareMetadataKey, constructor);
173
- }
174
-
175
- //#endregion
176
- //#region src/reply/ackLegality.ts
177
- const showModalReason = {
178
- reason: "this interaction was already acknowledged",
179
- alternative: "showModal() must be the initial response."
180
- };
181
- function deferReason(what) {
182
- return {
183
- reason: `this interaction was already acknowledged with ${what}`,
184
- alternative: "A deferral can only be the initial response."
185
- };
186
- }
187
- const ILLEGAL = {
188
- reply: {
189
- "deferred-reply": {
190
- reason: "this interaction was already deferred",
191
- alternative: "Use edit() to fill the deferred reply or followUp() for a new message."
192
- },
193
- "deferred-update": {
194
- reason: "this interaction was already acknowledged with a deferred update",
195
- alternative: "Use followUp() for a new message."
196
- },
197
- replied: {
198
- reason: "this interaction was already replied to",
199
- alternative: "Use followUp() for a new message or edit() to rewrite the reply."
200
- }
201
- },
202
- defer: {
203
- "deferred-reply": deferReason("a deferred reply"),
204
- "deferred-update": deferReason("a deferred update"),
205
- replied: deferReason("a reply")
206
- },
207
- deferUpdate: {
208
- "deferred-reply": deferReason("a deferred reply"),
209
- "deferred-update": deferReason("a deferred update"),
210
- replied: deferReason("a reply")
211
- },
212
- update: {
213
- "deferred-reply": {
214
- reason: "this interaction was deferred as a reply",
215
- alternative: "The placeholder is yours via edit(). update() rewrites a source message."
216
- },
217
- replied: {
218
- reason: "this interaction was already replied to",
219
- alternative: "update() rewrites a source message. Use edit() or followUp() instead."
220
- }
221
- },
222
- followUp: { unacked: {
223
- reason: "this interaction has not been acknowledged yet",
224
- alternative: "Ack first with reply() or defer()."
225
- } },
226
- edit: { unacked: {
227
- reason: "nothing has been sent yet",
228
- alternative: "Send with reply() or defer() first, then edit() rewrites it."
229
- } },
230
- delete: { unacked: {
231
- reason: "nothing has been sent yet",
232
- alternative: "Acknowledge first with reply() or defer(), then delete() removes it."
233
- } },
234
- showModal: {
235
- "deferred-reply": showModalReason,
236
- "deferred-update": showModalReason,
237
- replied: showModalReason
238
- }
239
- };
240
- const SEND_TARGET = {
241
- unacked: "reply",
242
- "deferred-reply": "edit",
243
- "deferred-update": "followUp",
244
- replied: "followUp"
245
- };
246
- function checkAckLegality(method, state, routeId, ackedBy) {
247
- if (method === "send") return;
248
- const illegal = ILLEGAL[method][state];
249
- if (!illegal) return;
250
- throw new SeedcordError(SeedcordErrorCode.ReplyIllegalAckState, [
251
- method,
252
- illegal.reason,
253
- illegal.alternative,
254
- routeId
255
- ], { cause: ackedBy });
256
- }
257
- function sendTarget(state) {
258
- return SEND_TARGET[state];
259
- }
260
-
261
- //#endregion
262
- //#region src/reply/AckTrace.ts
263
- var AckTrace = class extends Error {
264
- constructor(method) {
265
- super(`${method}() acknowledged this interaction`);
266
- this.name = "AckTrace";
267
- }
268
- };
269
-
270
- //#endregion
271
- //#region src/reply/responseReport.ts
272
- let replyLogger;
273
- function logger() {
274
- replyLogger ??= new Logger("Reply", { channel: "interactions" });
275
- return replyLogger;
276
- }
277
- /** @internal */
278
- function publishResponse(telemetry, report) {
279
- const durationMs = performance.now() - report.startedAt;
280
- const { routeId } = telemetry.dispatch;
281
- const write = {
282
- dispatchId: telemetry.dispatch.id,
283
- routeId,
284
- interactionId: telemetry.interactionId,
285
- method: report.method,
286
- durationMs
287
- };
288
- telemetry.bus[PublishDefault]("responseAttempted", report.outcome === "failed" ? {
289
- ...write,
290
- outcome: "failed",
291
- messageId: null,
292
- error: report.error
293
- } : {
294
- ...write,
295
- outcome: "sent",
296
- messageId: report.messageId
297
- });
298
- logger().trace(`${paint.sky.bold(routeId)} ${report.method} ${report.outcome} ${paint.mute("in")} ${Math.round(durationMs)}ms`);
299
- }
300
- async function reportedWrite(telemetry, method, write) {
301
- const startedAt = performance.now();
302
- const result = await attemptWrite(telemetry, method, startedAt, write);
303
- publishResponse(telemetry, {
304
- method,
305
- startedAt,
306
- outcome: "sent",
307
- messageId: null
308
- });
309
- return result;
310
- }
311
- async function attemptWrite(telemetry, method, startedAt, write) {
312
- try {
313
- return await write();
314
- } catch (caught) {
315
- publishResponse(telemetry, {
316
- method,
317
- startedAt,
318
- outcome: "failed",
319
- error: asError(caught)
320
- });
321
- throw caught;
322
- }
323
- }
324
-
325
- //#endregion
326
- //#region src/reply/translateSerialization.ts
327
- const MAX_DEPTH = 8;
328
- const OPERATOR_WORD = {
329
- "<=": "at most",
330
- ">=": "at least",
331
- "<": "fewer than",
332
- ">": "more than"
333
- };
334
- const KIND_WORD = {
335
- string: "a string",
336
- number: "a number",
337
- boolean: "a boolean",
338
- bigint: "a bigint",
339
- array: "a list",
340
- tuple: "a list",
341
- date: "a date",
342
- object: "an object"
343
- };
344
- function asRecord(value) {
345
- return typeof value === "object" && value !== null ? value : null;
346
- }
347
- function firstLine(message) {
348
- if (typeof message !== "string") return "unknown validation failure";
349
- return message.split("\n")[0] ?? message;
350
- }
351
- const GIVEN_PREVIEW_LIMIT = 40;
352
- function gotPhrase(given) {
353
- if (given === void 0) return "nothing";
354
- if (given === null) return "null";
355
- if (typeof given === "string") {
356
- if (given.length === 0) return "an empty string";
357
- if (given.length > GIVEN_PREVIEW_LIMIT) return `${given.length} characters`;
358
- return JSON.stringify(given);
359
- }
360
- if (Array.isArray(given)) return given.length === 0 ? "an empty list" : `${given.length} items`;
361
- if (typeof given === "number" || typeof given === "bigint" || typeof given === "boolean") return String(given);
362
- if (typeof given === "object") return given.constructor?.name ?? "an object";
363
- return typeof given;
364
- }
365
- function needPhrase(expected, given) {
366
- const bounds = [...expected.matchAll(/expected(\.length)?\s*(<=|>=|<|>)\s*(\d+)/g)];
367
- if (bounds.length === 0) return null;
368
- return bounds.map(([, lengthPart, operator, limit]) => {
369
- const word = OPERATOR_WORD[operator ?? ""] ?? operator ?? "";
370
- if (!lengthPart) return `${word} ${limit}`;
371
- const unit = typeof given === "string" ? "character" : "item";
372
- return `${word} ${limit} ${Number(limit) === 1 ? unit : `${unit}s`}`;
373
- }).join(" and ");
374
- }
375
- function instanceName(expected) {
376
- if (typeof expected === "function" && expected.name) return expected.name;
377
- return "specific class instance";
378
- }
379
- function describeLeaf(record) {
380
- if (typeof record.constraint === "string") {
381
- const need = typeof record.expected === "string" ? needPhrase(record.expected, record.given) : null;
382
- if (need) return `need ${need}, got ${gotPhrase(record.given)}`;
383
- return `${firstLine(record.message)}${"given" in record ? ` (got ${gotPhrase(record.given)})` : ""}`;
384
- }
385
- if (typeof record.expected === "function") return `need a ${instanceName(record.expected)}, got ${gotPhrase(record.given)}`;
386
- if (typeof record.validator === "string") {
387
- const kind = KIND_WORD[record.validator.replace(/^s\./, "").replace(/\(.*$/, "")];
388
- if (kind) return `need ${kind}, got ${gotPhrase(record.given)}`;
389
- return `${firstLine(record.message)}, got ${gotPhrase(record.given)}`;
390
- }
391
- return firstLine(record.message);
392
- }
393
- function describeUnion(branches, depth) {
394
- const records = [];
395
- for (const branch of branches) {
396
- const record = asRecord(branch);
397
- if (record) records.push(record);
398
- }
399
- const bounded = records.find((record) => typeof record.constraint === "string");
400
- if (bounded) return describeLeaf(bounded);
401
- const names = [];
402
- let allInstances = records.length > 0;
403
- for (const record of records) if (typeof record.expected === "function") names.push(instanceName(record.expected));
404
- else allInstances = false;
405
- if (allInstances) return `need a ${[...new Set(names)].join(" or ")}, got ${gotPhrase(records[0]?.given)}`;
406
- return detailOf(branches[0], depth + 1);
407
- }
408
- function detailOf(value, depth) {
409
- const record = asRecord(value);
410
- if (!record || depth >= MAX_DEPTH) return firstLine(record?.message);
411
- if (Array.isArray(record.errors) && record.errors.length > 0) {
412
- const errors = record.errors;
413
- const [first] = errors;
414
- if (Array.isArray(first)) {
415
- const [key, inner] = first;
416
- return `${String(key)}: ${detailOf(inner, depth + 1)}`;
417
- }
418
- return describeUnion(errors, depth);
419
- }
420
- return describeLeaf(record);
421
- }
422
- function translateSerializationError(error, componentClass, index, routeId) {
423
- const detail = detailOf(error, 0);
424
- return new SeedcordError(SeedcordErrorCode.ReplyComponentSerialization, [
425
- componentClass,
426
- index,
427
- detail,
428
- routeId
429
- ], { cause: error });
430
- }
431
-
432
- //#endregion
433
- //#region src/reply/serializeReply.ts
434
- function serializeReply(response, routeId) {
435
- if (typeof response === "string") try {
436
- return { components: [new TextDisplayBuilder().setContent(response).toJSON()] };
437
- } catch (error) {
438
- throw translateSerializationError(error, "TextDisplayBuilder", 0, routeId);
439
- }
440
- return {
441
- components: response.components.map((component, index) => {
442
- try {
443
- return component.toJSON();
444
- } catch (error) {
445
- const name = component.constructor?.name;
446
- throw translateSerializationError(error, !name || name === "Object" ? "component" : name, index, routeId);
447
- }
448
- }),
449
- ...response.allowedMentions && { allowedMentions: response.allowedMentions },
450
- ...response.files && { files: response.files }
451
- };
452
- }
453
-
454
- //#endregion
455
- //#region src/gates/Gate.ts
456
- /**
457
- * Builds a {@link Gate} from a check. The check refuses by throwing a Notice or a Silence and passes
458
- * by returning. The required context is inferred from `ctx`, so annotate it as narrowly as the fields
459
- * the check reads. No annotation defaults to {@link GateContextBase}, an agnostic gate that fits every
460
- * handler.
461
- *
462
- * @typeParam Name - The gate's name, captured as a literal for mismatch errors.
463
- * @typeParam Ctx - The context the check reads, inferred from the `ctx` annotation.
464
- *
465
- * @param name - The gate's name, used in mismatch errors.
466
- * @param fn - The check, which refuses by throwing a Notice or a Silence and passes by returning.
467
- *
468
- * @see {@link defineEffectGate}
469
- * @see the `@Gated` decorator from your transport package
470
- *
471
- * @example
472
- * ```ts
473
- * // a gate factory that takes its own option and closes over it
474
- * function MinRoles(min: number): Gate<GateContextBase, 'MinRoles'> {
475
- * return defineGate('MinRoles', (ctx) => {
476
- * if (ctx.memberRoleIds.length < min) throw new NotEnoughRoles(min);
477
- * });
478
- * }
479
- * ```
480
- *
481
- * @example
482
- * ```ts
483
- * // agnostic gate (no ctx annotation): fits any handler
484
- * const Owner = defineGate('owner', (ctx) => {
485
- * if (!ctx.userId) throw new NotOwner();
486
- * });
487
- * ```
488
- */
489
- function defineGate(name, fn) {
490
- return {
491
- name,
492
- check: async (ctx) => fn(ctx)
493
- };
494
- }
495
- /**
496
- * Builds an {@link EffectGate} from a `check` and a `commit`. `check` peeks and refuses by throwing, and
497
- * `commit` applies the side effect, running only once the whole gate set passes so a later refusal never
498
- * commits. In an `or`, a refusing arm's queued commit is rolled back. This is how {@link Cooldown} peeks
499
- * in `check` and charges the slot in `commit`.
500
- *
501
- * @typeParam Name - The gate's literal name, used to name a mismatch error.
502
- * @typeParam Ctx - The context both `check` and `commit` read, inferred from the `ctx` annotation.
503
- *
504
- * @param name - The gate's name, used in mismatch errors.
505
- * @param check - Peeks and refuses by throwing, without applying the side effect.
506
- * @param commit - Applies the side effect, running only after the whole gate set passes.
507
- *
508
- * @see {@link defineGate}
509
- * @see the `@Gated` decorator from your transport package
510
- * @see {@link Cooldown}
511
- *
512
- * @example
513
- * ```ts
514
- * // an effect-gate factory that takes its own limit and closes over it
515
- * function UsesPerDay(max: number): EffectGate<GateContextBase, 'UsesPerDay'> {
516
- * return defineEffectGate(
517
- * 'UsesPerDay',
518
- * (ctx) => {
519
- * // peek and refuse by throwing. do not mutate yet
520
- * if (usedToday(ctx.userId) >= max) throw new OutOfUsesNotice(max);
521
- * },
522
- * (ctx) => {
523
- * // runs only after the whole gate set passes
524
- * recordUse(ctx.userId);
525
- * }
526
- * );
527
- * }
528
- * ```
529
- */
530
- function defineEffectGate(name, check, commit) {
531
- return {
532
- name,
533
- check: async (ctx) => check(ctx),
534
- commit: async (ctx) => commit(ctx)
535
- };
536
- }
537
-
538
- //#endregion
539
- //#region src/notices/index.ts
540
- var GateNotice = class extends Notice {
541
- render() {
542
- return { components: [new NoticeCard(this.message).component] };
543
- }
544
- };
545
- var NotOwner = class extends GateNotice {
546
- constructor(message = "Only the bot owner can use this.") {
547
- super(message);
548
- this.summary = "be the bot owner";
549
- }
550
- };
551
- var NotInGuild = class extends GateNotice {
552
- constructor(message = "This can only be used in a server.") {
553
- super(message);
554
- this.summary = "be in a server";
555
- }
556
- };
557
- var NotInDm = class extends GateNotice {
558
- constructor(message = "This can only be used in a direct message.") {
559
- super(message);
560
- this.summary = "be in a direct message";
561
- }
562
- };
563
- function retryAt(resetAt) {
564
- return `<t:${toEpochSeconds(resetAt)}:R>`;
565
- }
566
- var OnCooldown = class extends GateNotice {
567
- resetAt;
568
- constructor(resetAt, message) {
569
- super(message ?? `You are doing that too fast. Try again ${retryAt(resetAt)}.`);
570
- this.resetAt = resetAt;
571
- this.summary = `try again ${retryAt(resetAt)}`;
572
- }
573
- };
574
- var NotAllowed = class extends Notice {
575
- constructor() {
576
- super("not allowed");
577
- }
578
- render() {
579
- return { components: [new NoticeCard("You are not allowed to use this command.").component] };
580
- }
581
- };
582
- var NeedsAny = class extends Notice {
583
- summaries;
584
- constructor(summaries) {
585
- super("not allowed");
586
- this.summaries = [...new Set(summaries)];
587
- }
588
- render() {
589
- const bullets = this.summaries.map((summary) => `• ${summary}`).join("\n");
590
- return { components: [new NoticeCard(`You need to meet at least one of these:\n${bullets}`).component] };
591
- }
592
- };
593
- var MissingRole = class extends GateNotice {
594
- constructor(message, roleId) {
595
- super(message ?? (roleId ? `You need the <@&${roleId}> role to use this.` : "You do not have the required role."));
596
- this.summary = roleId ? `hold the <@&${roleId}> role` : "hold the required role";
597
- }
598
- };
599
- var MissingPermissions = class extends Notice {
600
- subject;
601
- missingPerms;
602
- customLead;
603
- constructor(message, subject, missingPerms) {
604
- super(message ?? "A required permission is missing.");
605
- this.subject = subject;
606
- this.missingPerms = missingPerms;
607
- this.customLead = message;
608
- const names = missingPerms.join(", ");
609
- this.summary = subject === null ? `hold ${names}` : `${subject} needs ${names}`;
610
- }
611
- render() {
612
- const bullets = this.missingPerms.map((perm) => `• ${perm}`).join("\n");
613
- const lead = this.customLead ?? `${this.subject === null ? "You are" : `${this.subject} is`} missing the following permission entries:`;
614
- return { components: [new NoticeCard(`${lead}\n\n${bullets}`).component] };
615
- }
616
- };
617
- var HasDangerousPermissions = class extends Notice {
618
- subject;
619
- dangerousPerms;
620
- customLead;
621
- constructor(message, subject, dangerousPerms) {
622
- super(message ?? "A dangerous permission is enabled.");
623
- this.subject = subject;
624
- this.dangerousPerms = dangerousPerms;
625
- this.customLead = message;
626
- this.summary = `${subject} must not hold ${dangerousPerms.join(", ")}`;
627
- }
628
- render() {
629
- const bullets = this.dangerousPerms.map((perm) => `• ${perm}`).join("\n");
630
- const lead = this.customLead ?? `${this.subject} has the following permission entries that must not be enabled:`;
631
- return { components: [new NoticeCard(`${lead}\n\n${bullets}`).component] };
632
- }
633
- };
634
-
635
- //#endregion
636
- //#region src/gates/effects.ts
637
- const commitQueues = /* @__PURE__ */ new WeakMap();
638
- function isEffectGate(gate) {
639
- return typeof gate.commit === "function";
640
- }
641
- async function runCheck(gate, ctx) {
642
- await gate.check(ctx);
643
- if (!isEffectGate(gate)) return;
644
- let queue = commitQueues.get(ctx);
645
- if (!queue) {
646
- queue = [];
647
- commitQueues.set(ctx, queue);
648
- }
649
- queue.push(() => gate.commit(ctx));
650
- }
651
- async function runCommits(ctx) {
652
- const queue = commitQueues.get(ctx);
653
- if (!queue) return;
654
- for (const commit of queue) await commit();
655
- }
656
- function discardCommits(ctx) {
657
- commitQueues.delete(ctx);
658
- }
659
- function markCommits(ctx) {
660
- return commitQueues.get(ctx)?.length ?? 0;
661
- }
662
- function rollbackCommits(ctx, mark) {
663
- const queue = commitQueues.get(ctx);
664
- if (queue && queue.length > mark) queue.length = mark;
665
- }
666
-
667
- //#endregion
668
- //#region src/gates/catalog/options.ts
669
- function pickNotice(options, makeDefault) {
670
- return options?.notice ?? makeDefault(options?.message);
671
- }
672
-
673
- //#endregion
674
- //#region src/permissions/bits.ts
675
- const PermissionNames = new Map(Object.entries(PermissionFlagsBits).map(([key, bit]) => [bit, prettify(key)]));
676
- const ADMINISTRATOR = PermissionFlagsBits.Administrator;
677
- function nameOf(bit) {
678
- return PermissionNames.get(bit) ?? String(bit);
679
- }
680
- function holds(held, bit) {
681
- return (held & ADMINISTRATOR) === ADMINISTRATOR || (held & bit) === bit;
682
- }
683
- /** @internal */
684
- function hasAll(held, scope) {
685
- return scope.every((bit) => holds(held, bit));
686
- }
687
- /** @internal only reached once hasAll failed, so the administrator bit is absent */
688
- function missingNames(held, scope) {
689
- return scope.reduce((names, bit) => {
690
- if ((held & bit) !== bit) names.push(nameOf(bit));
691
- return names;
692
- }, []);
693
- }
694
- /** @internal */
695
- function presentNames(held, scope) {
696
- return scope.reduce((names, bit) => {
697
- if (holds(held, bit)) names.push(nameOf(bit));
698
- return names;
699
- }, []);
700
- }
701
-
702
- //#endregion
703
- //#region src/gates/catalog/permissions.ts
704
- const MEMBER_CHECK = {
705
- unresolved: "Your server member data could not be resolved. Try again.",
706
- subject: null
707
- };
708
- const BOT_CHECK = {
709
- unresolved: "The bot's permissions could not be resolved. Try again.",
710
- subject: "The bot"
711
- };
712
- function refusePermissions(held, scope, options, guildId, kind) {
713
- if (held === null) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message ?? (guildId ? kind.unresolved : void 0)));
714
- if (hasAll(held, scope)) return;
715
- throw pickNotice(options?.missing, (message) => new MissingPermissions(message, kind.subject, missingNames(held, scope)));
716
- }
717
- function RequirePermissions(scope, options) {
718
- if (options?.in === "guild") return defineGate("RequirePermissions", (ctx) => {
719
- refusePermissions(ctx.memberGuildPermissions, scope, options, ctx.guildId, MEMBER_CHECK);
720
- });
721
- return defineGate("RequirePermissions", (ctx) => {
722
- refusePermissions(ctx.memberPermissions, scope, options, ctx.guildId, MEMBER_CHECK);
723
- });
724
- }
725
- function RequireBotPermissions(scope, options) {
726
- if (options?.in === "guild") return defineGate("RequireBotPermissions", (ctx) => {
727
- refusePermissions(ctx.appGuildPermissions, scope, options, ctx.guildId, BOT_CHECK);
728
- });
729
- return defineGate("RequireBotPermissions", (ctx) => {
730
- refusePermissions(ctx.appPermissions, scope, options, ctx.guildId, BOT_CHECK);
731
- });
732
- }
733
- /**
734
- * Requires the caller to hold `roleId`, read from the member's role ids. Refuses outside a guild first,
735
- * then when the role is missing. Roles are guild-global, so there is no scope dimension.
736
- *
737
- * @param roleId - The role snowflake the caller must hold.
738
- * @param options - Override each refusal, the outside-guild one with `notInGuild` and the missing-role one with `missingRole`.
739
- *
740
- * @see the `@Gated` decorator from your transport package
741
- *
742
- * @example
743
- * ```ts
744
- * \@Gated(RequireRole('123456789012345678'))
745
- * \@SlashRoute('vip')
746
- * class VipHandler extends SlashHandler<'vip'> {
747
- * async execute() {
748
- * // ...
749
- * }
750
- * }
751
- * ```
752
- */
753
- function RequireRole(roleId, options) {
754
- return defineGate("RequireRole", (ctx) => {
755
- if (!ctx.guildId) throw pickNotice(options?.notInGuild, (message) => new NotInGuild(message));
756
- if (ctx.memberRoleIds.includes(roleId)) return;
757
- throw pickNotice(options?.missingRole, (message) => new MissingRole(message, roleId));
758
- });
759
- }
760
-
761
- //#endregion
762
- //#region src/stops/Silence.ts
763
- /**
764
- * Throw to stop a handler with no reply and no report.
765
- *
766
- * The framework boundary catches `Silence` before {@link Notice}, makes zero Discord calls, and stops.
767
- * Ideally you'd only throw this in `EventHandlers` (or `Gates` for those), because it doesn't make sense to
768
- * leave the user with no reply for an interaction.
769
- *
770
- * @example
771
- * ```ts
772
- * import { Silence } from '@seedcord/gateway';
773
- *
774
- * // a helper under a guildMemberAdd handler, stopping when this guild configured nothing to send
775
- * const template = await settings.welcome(member.guild.id);
776
- * if (!template) throw new Silence('no welcome message set');
777
- * ```
778
- */
779
- var Silence = class extends Error {
780
- reason;
781
- /**
782
- * @param reason - Optional note written only to a debug log, never shown to the user or reported.
783
- */
784
- constructor(reason) {
785
- super(reason ?? "Silence");
786
- this.reason = reason;
787
- Error.captureStackTrace(this, this.constructor);
788
- }
789
- };
790
-
791
- //#endregion
792
- //#region src/pagination/cursor.ts
793
- const PAGE_MAX = 1e6;
794
- const SLOT_MAX = 4;
795
- /** Build the page cursor for a paginator. */
796
- function pageCursor(prefix) {
797
- return new CustomId(prefix).int("page", 0, PAGE_MAX).int("slot", 0, SLOT_MAX);
798
- }
799
-
800
- //#endregion
801
- export { translateSerializationError as A, contextMenuRouteOf as B, NotInDm as C, defineEffectGate as D, OnCooldown as E, checkAckLegality as F, decodeComponentRoute as G, storeComponentRoute as H, sendTarget as I, NoticeCard as K, RegisterInteractionMiddleware as L, publishResponse as M, reportedWrite as N, defineGate as O, AckTrace as P, interactionMiddlewareMetaOf as R, NotAllowed as S, NotOwner as T, storeInteractionRoute as U, interactionRoutesOf as V, ComponentDefsKey as W, GateNotice as _, RequirePermissions as a, MissingRole as b, hasAll as c, pickNotice as d, discardCommits as f, runCommits as g, runCheck as h, RequireBotPermissions as i, attemptWrite as j, serializeReply as k, missingNames as l, rollbackCommits as m, pageCursor as n, RequireRole as o, markCommits as p, Notice as q, Silence as r, PermissionNames as s, PAGE_MAX as t, presentNames as u, HasDangerousPermissions as v, NotInGuild as w, NeedsAny as x, MissingPermissions as y, areRoutes as z };
802
- //# sourceMappingURL=cursor-CgNBnJfu.mjs.map