@seedcord/kit 0.0.1

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.
@@ -0,0 +1,695 @@
1
+ let discord_js = require("discord.js");
2
+ let _seedcord_errors = require("@seedcord/errors");
3
+ let _seedcord_errors_internal = require("@seedcord/errors/internal");
4
+
5
+ //#region src/botColorHolder.ts
6
+ const DEFAULT_COLOR = "Default";
7
+ let current = DEFAULT_COLOR;
8
+ /** @internal */
9
+ function setBotColor(color) {
10
+ current = color ?? DEFAULT_COLOR;
11
+ }
12
+ /** @internal */
13
+ function getBotColor() {
14
+ return current;
15
+ }
16
+
17
+ //#endregion
18
+ //#region src/components/builderTypes.ts
19
+ /**
20
+ * Available Discord.js builder classes for use with BuilderComponent for commands, embeds, modals, etc.
21
+ *
22
+ * @internal
23
+ */
24
+ const BuilderTypes = {
25
+ command: discord_js.SlashCommandBuilder,
26
+ context_menu: discord_js.ContextMenuCommandBuilder,
27
+ subcommand: discord_js.SlashCommandSubcommandBuilder,
28
+ group: discord_js.SlashCommandSubcommandGroupBuilder,
29
+ embed: discord_js.EmbedBuilder,
30
+ modal: discord_js.ModalBuilder,
31
+ label: discord_js.LabelBuilder,
32
+ text_input: discord_js.TextInputBuilder,
33
+ file_upload: discord_js.FileUploadBuilder,
34
+ checkbox: discord_js.CheckboxBuilder,
35
+ checkbox_group: discord_js.CheckboxGroupBuilder,
36
+ checkbox_group_option: discord_js.CheckboxGroupOptionBuilder,
37
+ radio_group: discord_js.RadioGroupBuilder,
38
+ radio_group_option: discord_js.RadioGroupOptionBuilder,
39
+ button: discord_js.ButtonBuilder,
40
+ menu_string: discord_js.StringSelectMenuBuilder,
41
+ menu_option_string: discord_js.StringSelectMenuOptionBuilder,
42
+ menu_user: discord_js.UserSelectMenuBuilder,
43
+ menu_channel: discord_js.ChannelSelectMenuBuilder,
44
+ menu_mentionable: discord_js.MentionableSelectMenuBuilder,
45
+ menu_role: discord_js.RoleSelectMenuBuilder,
46
+ container: discord_js.ContainerBuilder,
47
+ text_display: discord_js.TextDisplayBuilder,
48
+ file: discord_js.FileBuilder,
49
+ media: discord_js.MediaGalleryBuilder,
50
+ section: discord_js.SectionBuilder,
51
+ separator: discord_js.SeparatorBuilder
52
+ };
53
+ /**
54
+ * Available Discord.js action row classes for use with RowComponent for Select Menus and Buttons
55
+ *
56
+ * @internal
57
+ */
58
+ const RowTypes = {
59
+ button: discord_js.ActionRowBuilder,
60
+ menu_string: discord_js.ActionRowBuilder,
61
+ menu_user: discord_js.ActionRowBuilder,
62
+ menu_channel: discord_js.ActionRowBuilder,
63
+ menu_mentionable: discord_js.ActionRowBuilder,
64
+ menu_role: discord_js.ActionRowBuilder
65
+ };
66
+
67
+ //#endregion
68
+ //#region src/components/Component.ts
69
+ /**
70
+ * Base class for Discord component wrappers.
71
+ *
72
+ * @typeParam TComponent - The Discord.js component type being wrapped
73
+ *
74
+ * @internal
75
+ */
76
+ var BaseComponent = class {
77
+ _component;
78
+ constructor(ComponentClass) {
79
+ this._component = new ComponentClass();
80
+ }
81
+ /**
82
+ * The wrapped builder, for calling Discord.js methods like setTitle() and setDescription() inside a subclass.
83
+ *
84
+ * @example this.instance.setTitle('My Modal')
85
+ */
86
+ get instance() {
87
+ return this._component;
88
+ }
89
+ };
90
+ /**
91
+ * Base class for Discord.js builder components
92
+ *
93
+ * Wraps Discord.js builders (SlashCommandBuilder, EmbedBuilder, etc.) with
94
+ * Seedcord-specific defaults and helper methods.
95
+ *
96
+ * @typeParam BuilderKey - The type of Discord.js builder being wrapped
97
+ */
98
+ var BuilderComponent = class extends BaseComponent {
99
+ type;
100
+ colorApplied = false;
101
+ constructor(type) {
102
+ const ComponentClass = BuilderTypes[type];
103
+ super(ComponentClass);
104
+ this.type = type;
105
+ if (this.instance instanceof discord_js.SlashCommandBuilder || this.instance instanceof discord_js.ContextMenuCommandBuilder) this.instance.setContexts(discord_js.InteractionContextType.Guild);
106
+ }
107
+ get component() {
108
+ this.applyBotColor();
109
+ return this.instance;
110
+ }
111
+ applyBotColor() {
112
+ if (this.colorApplied) return;
113
+ this.colorApplied = true;
114
+ const color = getBotColor();
115
+ if (this.instance instanceof discord_js.EmbedBuilder) {
116
+ if (this.instance.data.color === void 0) this.instance.setColor(color);
117
+ } else if (this.instance instanceof discord_js.ContainerBuilder) {
118
+ const accent = this.instance.data.accent_color;
119
+ if (accent === null || accent === void 0) this.instance.setAccentColor(color === "Default" ? void 0 : (0, discord_js.resolveColor)(color));
120
+ }
121
+ }
122
+ };
123
+ /**
124
+ * Base class for Discord action row components
125
+ *
126
+ * Wraps Discord.js action row builder with Seedcord-specific defaults and helper methods.
127
+ *
128
+ * @typeParam RowKey - The Discord.js action row type being wrapped
129
+ */
130
+ var RowComponent = class extends BaseComponent {
131
+ type;
132
+ constructor(type) {
133
+ const ComponentClass = RowTypes[type];
134
+ super(ComponentClass);
135
+ this.type = type;
136
+ }
137
+ get component() {
138
+ return this.instance;
139
+ }
140
+ };
141
+
142
+ //#endregion
143
+ //#region src/stops/Notice.ts
144
+ /**
145
+ * Base class for a user-facing refusal or a reported fault.
146
+ *
147
+ * Throw a `Notice` to stop a handler and reply to the user. The framework catches it at the controller
148
+ * boundary and renders {@link Notice.render}, which always decides what the user sees. With `report`
149
+ * false that render is all that happens. With `report` true the framework also logs the fault and
150
+ * publishes it to the `handledException` bus. A raw, non-Notice throw shows the generic message.
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * import { Notice, BuilderComponent, type RenderContext, type ReplyResponse } from 'seedcord';
155
+ * import { TextDisplayBuilder } from 'discord.js';
156
+ *
157
+ * // reading `.component` applies the configured bot color to the container accent
158
+ * class TooPoorCard extends BuilderComponent<'container'> {
159
+ * constructor(balance: number) {
160
+ * super('container');
161
+ * this.instance.addTextDisplayComponents(
162
+ * new TextDisplayBuilder().setContent(`### Insufficient balance\nYou need more than ${balance} coins.`)
163
+ * );
164
+ * }
165
+ * }
166
+ *
167
+ * class TooPoor extends Notice {
168
+ * constructor(private readonly balance: number) {
169
+ * super(`balance ${balance} is below the cost`);
170
+ * }
171
+ *
172
+ * render(_ctx: RenderContext): ReplyResponse {
173
+ * return { components: [new TooPoorCard(this.balance).component] };
174
+ * }
175
+ * }
176
+ *
177
+ * // in a handler, throwing stops the handler and replies with render(ctx)
178
+ * if (wallet.balance < cost) throw new TooPoor(wallet.balance);
179
+ * ```
180
+ */
181
+ var Notice = class extends Error {
182
+ /**
183
+ * Whether this denial is a reported fault. True also logs it and publishes it to the `handledException`
184
+ * bus. The user always sees {@link Notice.render} either way.
185
+ *
186
+ * @defaultValue `false`
187
+ */
188
+ report = false;
189
+ /**
190
+ * Whether the reply is ephemeral, so only the invoking user sees it. Set it false for a refusal the
191
+ * whole channel should see.
192
+ *
193
+ * @defaultValue `true`
194
+ */
195
+ ephemeral = true;
196
+ /**
197
+ * A short one-line reason. When every arm of an `or` gate refuses and each refusal sets this, `or`
198
+ * lists them instead of showing a neutral message.
199
+ */
200
+ summary;
201
+ constructor(message, options) {
202
+ super(message, options);
203
+ this.name = new.target.name;
204
+ Error.captureStackTrace(this, this.constructor);
205
+ }
206
+ };
207
+
208
+ //#endregion
209
+ //#region src/stops/NoticeCard.ts
210
+ /**
211
+ * Built fresh inside a {@link Notice}'s `render` to back its ComponentsV2 reply. The title renders as
212
+ * an h3 line with the description on the next line.
213
+ */
214
+ var NoticeCard = class extends BuilderComponent {
215
+ constructor(description, title = "Cannot Proceed") {
216
+ super("container");
217
+ this.instance.addTextDisplayComponents(new discord_js.TextDisplayBuilder().setContent(`### ${title}\n${description}`));
218
+ }
219
+ };
220
+
221
+ //#endregion
222
+ //#region src/customId/Errors.ts
223
+ /**
224
+ * Thrown when a customId was minted by an older version of its shape.
225
+ *
226
+ * This is normal after the shape changes. The reply tells the user to run the command again.
227
+ */
228
+ var StaleCustomId = class extends Notice {
229
+ constructor(prefix) {
230
+ super(`Stale customId for "${prefix}".`);
231
+ }
232
+ render() {
233
+ return { components: [new NoticeCard("This button or menu is from an older version. Please run the command again.", "Outdated").component] };
234
+ }
235
+ };
236
+ /**
237
+ * Thrown when a customId wire is corrupt or tampered with and cannot be trusted.
238
+ *
239
+ * This should not happen in normal use, so it reports.
240
+ */
241
+ var InvalidCustomId = class extends Notice {
242
+ constructor(detail) {
243
+ super(`Invalid customId. ${detail}`);
244
+ this.report = true;
245
+ }
246
+ render() {
247
+ return { components: [new NoticeCard("Something went wrong. Please try again.").component] };
248
+ }
249
+ };
250
+
251
+ //#endregion
252
+ //#region src/customId/codec.ts
253
+ const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
254
+ const BASE = 64n;
255
+ const CHAR_TO_VALUE = new Map([...ALPHABET].map((char, index) => [char, index]));
256
+ const DELIMITER = "";
257
+ const ESCAPE = "\x1B";
258
+ /** @internal */
259
+ const HASH_LENGTH = 3;
260
+ const SAFE_MAX = BigInt(Number.MAX_SAFE_INTEGER);
261
+ const SAFE_MIN = BigInt(Number.MIN_SAFE_INTEGER);
262
+ function bigintToBase64(value) {
263
+ if (value === 0n) return ALPHABET.charAt(0);
264
+ let text = "";
265
+ for (let remaining = value; remaining > 0n; remaining /= BASE) text = ALPHABET.charAt(Number(remaining % BASE)) + text;
266
+ return text;
267
+ }
268
+ function base64ToBigint(text) {
269
+ let value = 0n;
270
+ for (const char of text) {
271
+ const digit = CHAR_TO_VALUE.get(char);
272
+ if (digit === void 0) throw new InvalidCustomId(`bad character ${JSON.stringify(char)}`);
273
+ value = value * BASE + BigInt(digit);
274
+ }
275
+ return value;
276
+ }
277
+ function zigzagEncode(value) {
278
+ const big = BigInt(value);
279
+ return big >= 0n ? big << 1n : (-big << 1n) - 1n;
280
+ }
281
+ function zigzagDecode(encoded) {
282
+ return (encoded & 1n) === 1n ? -(encoded + 1n >> 1n) : encoded >> 1n;
283
+ }
284
+ function escapeToken(text) {
285
+ return text.replace(/[\x1b\x1f]/g, (char) => ESCAPE + char);
286
+ }
287
+ function unescapeToken(text) {
288
+ let out = "";
289
+ for (let i = 0; i < text.length; i++) {
290
+ if (text.charAt(i) !== ESCAPE) {
291
+ out += text.charAt(i);
292
+ continue;
293
+ }
294
+ const next = text.charAt(i + 1);
295
+ if (next === "") throw new InvalidCustomId("dangling escape at end of token");
296
+ out += next;
297
+ i++;
298
+ }
299
+ return out;
300
+ }
301
+ function splitTokens(body) {
302
+ const pieces = [];
303
+ let current = "";
304
+ for (let i = 0; i < body.length; i++) {
305
+ const char = body.charAt(i);
306
+ if (char === ESCAPE) {
307
+ current += char + body.charAt(i + 1);
308
+ i++;
309
+ } else if (char === DELIMITER) {
310
+ pieces.push(current);
311
+ current = "";
312
+ } else current += char;
313
+ }
314
+ pieces.push(current);
315
+ return pieces;
316
+ }
317
+ function isBounded(field) {
318
+ if (field.kind === "int") return field.min !== void 0 && field.max !== void 0;
319
+ return field.kind === "snowflake" || field.kind === "uuid" || field.kind === "bool" || field.kind === "oneOf";
320
+ }
321
+ function radixOf(field) {
322
+ switch (field.kind) {
323
+ case "snowflake": return 1n << 64n;
324
+ case "uuid": return 1n << 128n;
325
+ case "bool": return 2n;
326
+ case "oneOf":
327
+ if (!field.choices?.length) throw new InvalidCustomId("oneOf field has no choices");
328
+ return BigInt(field.choices.length);
329
+ case "int":
330
+ if (field.min === void 0 || field.max === void 0) throw new InvalidCustomId("bounded int field is missing a bound");
331
+ return BigInt(field.max) - BigInt(field.min) + 1n;
332
+ default: throw new InvalidCustomId(`field kind ${field.kind} has no radix`);
333
+ }
334
+ }
335
+ function boundedToBigint(field, name, value) {
336
+ const slot = boundedSlot(field, name, value);
337
+ if (slot < 0n || slot >= radixOf(field)) outOfRange(name, value);
338
+ return slot;
339
+ }
340
+ function boundedSlot(field, name, value) {
341
+ switch (field.kind) {
342
+ case "snowflake":
343
+ if (typeof value !== "string" || !/^\d+$/.test(value)) return outOfRange(name, value);
344
+ return BigInt(value);
345
+ case "uuid": {
346
+ if (typeof value !== "string") return outOfRange(name, value);
347
+ const hex = value.replace(/-/g, "");
348
+ if (!/^[0-9a-fA-F]{32}$/.test(hex)) return outOfRange(name, value);
349
+ return BigInt(`0x${hex}`);
350
+ }
351
+ case "bool": return value ? 1n : 0n;
352
+ case "oneOf": {
353
+ const index = (field.choices ?? []).indexOf(value);
354
+ return index < 0 ? outOfRange(name, value) : BigInt(index);
355
+ }
356
+ case "int":
357
+ if (!Number.isInteger(value)) return outOfRange(name, value);
358
+ return BigInt(value - (field.min ?? 0));
359
+ default: return outOfRange(name, value);
360
+ }
361
+ }
362
+ function outOfRange(name, value) {
363
+ throw new _seedcord_errors_internal.SeedcordRangeError(_seedcord_errors.SeedcordErrorCode.CustomIdValueOutOfRange, [name, String(value)]);
364
+ }
365
+ function bigintToBoundedValue(field, slot) {
366
+ switch (field.kind) {
367
+ case "snowflake": return slot.toString();
368
+ case "uuid": return bigintToUuid(slot);
369
+ case "bool": return slot === 1n;
370
+ case "oneOf": return (field.choices ?? [])[Number(slot)];
371
+ case "int": return Number(slot) + (field.min ?? 0);
372
+ default: throw new InvalidCustomId(`field kind ${field.kind} is not bounded`);
373
+ }
374
+ }
375
+ function bigintToUuid(value) {
376
+ const hex = value.toString(16).padStart(32, "0");
377
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
378
+ }
379
+ function encodeUnboundedToken(field, name, value) {
380
+ if (field.kind === "int") {
381
+ if (!Number.isSafeInteger(value)) outOfRange(name, value);
382
+ return bigintToBase64(zigzagEncode(value));
383
+ }
384
+ return escapeToken(value);
385
+ }
386
+ function decodeUnboundedToken(field, piece) {
387
+ if (field.kind !== "int") return unescapeToken(piece);
388
+ if (piece === "") throw new InvalidCustomId("empty integer token");
389
+ const decoded = zigzagDecode(base64ToBigint(piece));
390
+ if (decoded > SAFE_MAX || decoded < SAFE_MIN) throw new InvalidCustomId("integer out of safe range");
391
+ return Number(decoded);
392
+ }
393
+ /**
394
+ * A short fingerprint of the shape. Change the shape and the hash changes, so an old customId no
395
+ * longer matches the current routeKey and decode catches it as stale.
396
+ *
397
+ * @internal
398
+ */
399
+ function computeLayoutHash(shape) {
400
+ const signature = JSON.stringify(Object.entries(shape).map(([name, field]) => [
401
+ name,
402
+ field.kind,
403
+ isBounded(field),
404
+ field.kind === "oneOf" ? field.choices ?? [] : null,
405
+ field.kind === "int" ? [field.min ?? null, field.max ?? null] : null
406
+ ]));
407
+ const modulus = BASE ** BigInt(3);
408
+ let hash = 0n;
409
+ for (const char of signature) hash = (hash * 131n + BigInt(char.charCodeAt(0))) % modulus;
410
+ let text = "";
411
+ for (let i = 0; i < 3; i++) {
412
+ text = ALPHABET.charAt(Number(hash % BASE)) + text;
413
+ hash /= BASE;
414
+ }
415
+ return text;
416
+ }
417
+ /**
418
+ * Pack values into a body. Bounded fields fold into one integer, unbounded fields trail after it.
419
+ *
420
+ * @internal
421
+ */
422
+ function encodeBody(shape, values) {
423
+ const fields = Object.entries(shape);
424
+ const pieces = [];
425
+ const bounded = fields.filter(([, field]) => isBounded(field));
426
+ if (bounded.length > 0) {
427
+ let packed = 0n;
428
+ for (const [name, field] of bounded) packed = packed * radixOf(field) + boundedToBigint(field, name, values[name]);
429
+ pieces.push(bigintToBase64(packed));
430
+ }
431
+ for (const [name, field] of fields) if (!isBounded(field)) pieces.push(encodeUnboundedToken(field, name, values[name]));
432
+ return pieces.join(DELIMITER);
433
+ }
434
+ function unpackBounded(bounded, blob, result) {
435
+ if (blob === void 0 || blob === "") throw new InvalidCustomId("empty packed block");
436
+ let packed = base64ToBigint(blob);
437
+ for (const [name, field] of [...bounded].reverse()) {
438
+ const radix = radixOf(field);
439
+ result[name] = bigintToBoundedValue(field, packed % radix);
440
+ packed /= radix;
441
+ }
442
+ if (packed !== 0n) throw new InvalidCustomId("leftover bits after unpacking");
443
+ }
444
+ /**
445
+ * Reverse of encodeBody. Rejects any malformed or truncated body.
446
+ *
447
+ * @internal
448
+ */
449
+ function decodeBody(shape, body) {
450
+ const fields = Object.entries(shape);
451
+ const bounded = fields.filter(([, field]) => isBounded(field));
452
+ const unbounded = fields.filter(([, field]) => !isBounded(field));
453
+ const expected = (bounded.length > 0 ? 1 : 0) + unbounded.length;
454
+ if (expected === 0) {
455
+ if (body !== "") throw new InvalidCustomId(`expected an empty body, got ${JSON.stringify(body)}`);
456
+ return {};
457
+ }
458
+ const pieces = splitTokens(body);
459
+ if (pieces.length !== expected) throw new InvalidCustomId(`expected ${expected} piece(s), got ${pieces.length}`);
460
+ const result = {};
461
+ let cursor = 0;
462
+ if (bounded.length > 0) {
463
+ unpackBounded(bounded, pieces[cursor], result);
464
+ cursor++;
465
+ }
466
+ for (const [name, field] of unbounded) {
467
+ const piece = pieces[cursor];
468
+ cursor++;
469
+ if (piece === void 0) throw new InvalidCustomId("missing trailing piece");
470
+ result[name] = decodeUnboundedToken(field, piece);
471
+ }
472
+ return result;
473
+ }
474
+
475
+ //#endregion
476
+ //#region src/customId/CustomId.ts
477
+ const MAX_WIRE_LENGTH = 100;
478
+ function routeKeyOf(wire) {
479
+ const colon = wire.indexOf(":");
480
+ return colon < 0 ? "" : wire.slice(0, colon);
481
+ }
482
+ /** Strip the layout hash off the routeKey to recover the stable prefix the controller routes by. @internal */
483
+ function prefixOf(wire) {
484
+ const key = routeKeyOf(wire);
485
+ return key.length <= 3 ? "" : key.slice(0, key.length - 3);
486
+ }
487
+ /**
488
+ * A typed customId. The single source of truth shared by the component that mints it and the handler
489
+ * that reads it. This gives you typed reads on the `.customId` field in components. Values are packed into a compact wire string rather than plain stringified tokens, so the 100-char Discord limit goes further. More string per string, basically.
490
+ *
491
+ * @typeParam Prefix - The stable route prefix, e.g. 'approve'.
492
+ * @typeParam Shape - The accumulated fields, filled in by the chain.
493
+ *
494
+ * @example
495
+ * ```ts
496
+ * const ApproveId = new CustomId('approve')
497
+ * .snowflake('userId')
498
+ * .oneOf('action', ['approve', 'deny']);
499
+ *
500
+ * // Set the custom id on a button when creating it.
501
+ * new ButtonBuilder().setCustomId(ApproveId.encode({ userId: '123', action: 'approve' }));
502
+ *
503
+ * // reading in the handler: userId comes back a string
504
+ * const { userId, action } = this.params; // userId: string, action: 'approve' | 'deny'
505
+ * await this.event.guild?.members.fetch(userId);
506
+ * ```
507
+ */
508
+ var CustomId = class CustomId {
509
+ prefix;
510
+ shape;
511
+ /** The prefix plus a short hash of the shape, the part of the wire before the colon. */
512
+ routeKey;
513
+ constructor(prefix, shape = {}) {
514
+ if (!prefix || /[:\x1b\x1f]/.test(prefix)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CustomIdInvalidPrefix, [prefix]);
515
+ this.prefix = prefix;
516
+ this.shape = shape;
517
+ this.routeKey = prefix + computeLayoutHash(shape);
518
+ }
519
+ add(name, field) {
520
+ if (/^(?:0|[1-9]\d*)$/.test(name)) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CustomIdReservedFieldName, [name]);
521
+ if (name in this.shape) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CustomIdDuplicateFieldName, [name]);
522
+ const shape = {
523
+ ...this.shape,
524
+ [name]: field
525
+ };
526
+ return new CustomId(this.prefix, shape);
527
+ }
528
+ /**
529
+ * Add a Discord ID field, decoded as a string (the discord.js `Snowflake` type).
530
+ *
531
+ * @example
532
+ * ```ts
533
+ * new CustomId('ban').snowflake('userId');
534
+ * ```
535
+ */
536
+ snowflake(name) {
537
+ return this.add(name, { kind: "snowflake" });
538
+ }
539
+ /**
540
+ * Add a UUID field, decoded as a lowercase uuid string.
541
+ *
542
+ * @example
543
+ * ```ts
544
+ * new CustomId('ticket').uuid('ticketId');
545
+ * ```
546
+ */
547
+ uuid(name) {
548
+ return this.add(name, { kind: "uuid" });
549
+ }
550
+ int(name, min, max) {
551
+ if (min !== void 0 && max !== void 0 && min > max) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CustomIdInvalidBounds, [
552
+ name,
553
+ min,
554
+ max
555
+ ]);
556
+ const field = min === void 0 || max === void 0 ? { kind: "int" } : {
557
+ kind: "int",
558
+ min,
559
+ max
560
+ };
561
+ return this.add(name, field);
562
+ }
563
+ /**
564
+ * Add a boolean flag.
565
+ *
566
+ * @example
567
+ * ```ts
568
+ * new CustomId('settings').bool('silent');
569
+ * ```
570
+ */
571
+ bool(name) {
572
+ return this.add(name, { kind: "bool" });
573
+ }
574
+ /**
575
+ * Add a field that is one value from a fixed list, decoded as the literal union. No `as const` needed.
576
+ *
577
+ * @example
578
+ * ```ts
579
+ * new CustomId('poll').oneOf('choice', ['yes', 'no', 'abstain']);
580
+ * ```
581
+ */
582
+ oneOf(name, choices) {
583
+ if (choices.length === 0) throw new _seedcord_errors_internal.SeedcordError(_seedcord_errors.SeedcordErrorCode.CustomIdEmptyChoices, [name]);
584
+ return this.add(name, {
585
+ kind: "oneOf",
586
+ choices
587
+ });
588
+ }
589
+ /**
590
+ * Add a free short text field. Avoid it where possible, it cannot be packed so it costs the most wire space.
591
+ *
592
+ * @example
593
+ * ```ts
594
+ * new CustomId('note').str('message');
595
+ * ```
596
+ */
597
+ str(name) {
598
+ return this.add(name, { kind: "string" });
599
+ }
600
+ /**
601
+ * Mint a wire string from values. Throws if a value is out of its field's range or the wire is over 100 chars.
602
+ *
603
+ * @param values - One value per field, typed by the chain.
604
+ * @returns The wire string to put on the component's customId.
605
+ */
606
+ encode(values) {
607
+ const wire = `${this.routeKey}:${encodeBody(this.shape, values)}`;
608
+ if (wire.length > MAX_WIRE_LENGTH) throw new _seedcord_errors_internal.SeedcordRangeError(_seedcord_errors.SeedcordErrorCode.CustomIdWireTooLong, [wire.length]);
609
+ return wire;
610
+ }
611
+ /**
612
+ * Read a wire string back into values.
613
+ *
614
+ * @param wire - The customId string from the interaction.
615
+ * @returns The decoded values, typed by the chain.
616
+ * @throws A {@link StaleCustomId} when the shape changed since the wire was minted.
617
+ * @throws An {@link InvalidCustomId} on a corrupt or foreign wire.
618
+ */
619
+ decode(wire) {
620
+ const key = routeKeyOf(wire);
621
+ if (key !== this.routeKey) {
622
+ if (prefixOf(wire) === this.prefix) throw new StaleCustomId(this.prefix);
623
+ throw new InvalidCustomId(`routeKey ${JSON.stringify(key)} is not ${JSON.stringify(this.routeKey)}`);
624
+ }
625
+ return decodeBody(this.shape, wire.slice(key.length + 1));
626
+ }
627
+ /** True if this wire was minted from this customId's prefix, ignoring the shape hash. */
628
+ owns(wire) {
629
+ return prefixOf(wire) === this.prefix;
630
+ }
631
+ };
632
+ /**
633
+ * Find the customId whose prefix owns this wire, decode against it, and report which one matched.
634
+ *
635
+ * @internal
636
+ */
637
+ function decodeFor(defs, wire) {
638
+ const match = defs.find((def) => def.owns(wire));
639
+ if (!match) throw new InvalidCustomId(`no customId owns ${JSON.stringify(routeKeyOf(wire))}`);
640
+ return {
641
+ prefix: match.prefix,
642
+ params: match.decode(wire)
643
+ };
644
+ }
645
+
646
+ //#endregion
647
+ Object.defineProperty(exports, 'BuilderComponent', {
648
+ enumerable: true,
649
+ get: function () {
650
+ return BuilderComponent;
651
+ }
652
+ });
653
+ Object.defineProperty(exports, 'CustomId', {
654
+ enumerable: true,
655
+ get: function () {
656
+ return CustomId;
657
+ }
658
+ });
659
+ Object.defineProperty(exports, 'Notice', {
660
+ enumerable: true,
661
+ get: function () {
662
+ return Notice;
663
+ }
664
+ });
665
+ Object.defineProperty(exports, 'NoticeCard', {
666
+ enumerable: true,
667
+ get: function () {
668
+ return NoticeCard;
669
+ }
670
+ });
671
+ Object.defineProperty(exports, 'RowComponent', {
672
+ enumerable: true,
673
+ get: function () {
674
+ return RowComponent;
675
+ }
676
+ });
677
+ Object.defineProperty(exports, 'decodeFor', {
678
+ enumerable: true,
679
+ get: function () {
680
+ return decodeFor;
681
+ }
682
+ });
683
+ Object.defineProperty(exports, 'prefixOf', {
684
+ enumerable: true,
685
+ get: function () {
686
+ return prefixOf;
687
+ }
688
+ });
689
+ Object.defineProperty(exports, 'setBotColor', {
690
+ enumerable: true,
691
+ get: function () {
692
+ return setBotColor;
693
+ }
694
+ });
695
+ //# sourceMappingURL=CustomId-BuIoGHXw.cjs.map