shardwire 1.1.0 → 1.3.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.
- package/README.md +133 -32
- package/dist/index.d.mts +160 -13
- package/dist/index.d.ts +160 -13
- package/dist/index.js +590 -44
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +590 -44
- package/dist/index.mjs.map +1 -1
- package/package.json +82 -73
package/dist/index.js
CHANGED
|
@@ -36,8 +36,14 @@ var BOT_EVENT_NAMES = [
|
|
|
36
36
|
"messageDelete",
|
|
37
37
|
"messageReactionAdd",
|
|
38
38
|
"messageReactionRemove",
|
|
39
|
+
"guildCreate",
|
|
40
|
+
"guildDelete",
|
|
39
41
|
"guildMemberAdd",
|
|
40
|
-
"guildMemberRemove"
|
|
42
|
+
"guildMemberRemove",
|
|
43
|
+
"guildMemberUpdate",
|
|
44
|
+
"threadCreate",
|
|
45
|
+
"threadUpdate",
|
|
46
|
+
"threadDelete"
|
|
41
47
|
];
|
|
42
48
|
var BOT_ACTION_NAMES = [
|
|
43
49
|
"sendMessage",
|
|
@@ -45,7 +51,14 @@ var BOT_ACTION_NAMES = [
|
|
|
45
51
|
"deleteMessage",
|
|
46
52
|
"replyToInteraction",
|
|
47
53
|
"deferInteraction",
|
|
54
|
+
"deferUpdateInteraction",
|
|
48
55
|
"followUpInteraction",
|
|
56
|
+
"editInteractionReply",
|
|
57
|
+
"deleteInteractionReply",
|
|
58
|
+
"updateInteraction",
|
|
59
|
+
"showModal",
|
|
60
|
+
"fetchMessage",
|
|
61
|
+
"fetchMember",
|
|
49
62
|
"banMember",
|
|
50
63
|
"kickMember",
|
|
51
64
|
"addMemberRole",
|
|
@@ -67,12 +80,20 @@ var EVENT_REQUIRED_INTENTS = {
|
|
|
67
80
|
messageDelete: ["GuildMessages"],
|
|
68
81
|
messageReactionAdd: ["GuildMessageReactions"],
|
|
69
82
|
messageReactionRemove: ["GuildMessageReactions"],
|
|
83
|
+
guildCreate: ["Guilds"],
|
|
84
|
+
guildDelete: ["Guilds"],
|
|
70
85
|
guildMemberAdd: ["GuildMembers"],
|
|
71
|
-
guildMemberRemove: ["GuildMembers"]
|
|
86
|
+
guildMemberRemove: ["GuildMembers"],
|
|
87
|
+
guildMemberUpdate: ["GuildMembers"],
|
|
88
|
+
threadCreate: ["Guilds"],
|
|
89
|
+
threadUpdate: ["Guilds"],
|
|
90
|
+
threadDelete: ["Guilds"]
|
|
72
91
|
};
|
|
73
92
|
function getAvailableEvents(intents) {
|
|
74
93
|
const enabled = new Set(intents);
|
|
75
|
-
return BOT_EVENT_NAMES.filter(
|
|
94
|
+
return BOT_EVENT_NAMES.filter(
|
|
95
|
+
(eventName) => EVENT_REQUIRED_INTENTS[eventName].every((intent) => enabled.has(intent))
|
|
96
|
+
);
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
// src/discord/runtime/adapter.ts
|
|
@@ -158,6 +179,7 @@ function serializeMessage(message) {
|
|
|
158
179
|
...message.reference.channelId ? { channelId: message.reference.channelId } : {},
|
|
159
180
|
...message.reference.guildId ? { guildId: message.reference.guildId } : {}
|
|
160
181
|
} : void 0;
|
|
182
|
+
const components = "components" in message && message.components && message.components.length > 0 ? message.components.map((row) => row.toJSON()) : void 0;
|
|
161
183
|
return {
|
|
162
184
|
id: message.id,
|
|
163
185
|
channelId: message.channelId,
|
|
@@ -175,9 +197,29 @@ function serializeMessage(message) {
|
|
|
175
197
|
size: attachment.size
|
|
176
198
|
})) : [],
|
|
177
199
|
embeds: serializeEmbeds(message),
|
|
200
|
+
...components ? { components } : {},
|
|
178
201
|
...reference ? { reference } : {}
|
|
179
202
|
};
|
|
180
203
|
}
|
|
204
|
+
function serializeGuild(guild) {
|
|
205
|
+
return {
|
|
206
|
+
id: guild.id,
|
|
207
|
+
name: guild.name,
|
|
208
|
+
...guild.icon !== null && guild.icon !== void 0 ? { icon: guild.icon } : { icon: null },
|
|
209
|
+
ownerId: guild.ownerId
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function serializeThread(thread) {
|
|
213
|
+
return {
|
|
214
|
+
id: thread.id,
|
|
215
|
+
guildId: thread.guildId,
|
|
216
|
+
parentId: thread.parentId,
|
|
217
|
+
name: thread.name,
|
|
218
|
+
type: thread.type,
|
|
219
|
+
...typeof thread.archived === "boolean" ? { archived: thread.archived } : {},
|
|
220
|
+
...typeof thread.locked === "boolean" ? { locked: thread.locked } : {}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
181
223
|
function serializeDeletedMessage(message) {
|
|
182
224
|
return {
|
|
183
225
|
id: message.id,
|
|
@@ -347,7 +389,10 @@ function toSendOptions(input) {
|
|
|
347
389
|
return {
|
|
348
390
|
...input.content !== void 0 ? { content: input.content } : {},
|
|
349
391
|
...input.embeds !== void 0 ? { embeds: input.embeds } : {},
|
|
350
|
-
...input.allowedMentions !== void 0 ? { allowedMentions: input.allowedMentions } : {}
|
|
392
|
+
...input.allowedMentions !== void 0 ? { allowedMentions: input.allowedMentions } : {},
|
|
393
|
+
...input.components !== void 0 ? { components: input.components } : {},
|
|
394
|
+
...input.flags !== void 0 ? { flags: input.flags } : {},
|
|
395
|
+
...input.stickerIds !== void 0 ? { stickers: input.stickerIds } : {}
|
|
351
396
|
};
|
|
352
397
|
}
|
|
353
398
|
function extractDiscordErrorDetails(error) {
|
|
@@ -371,14 +416,34 @@ function extractDiscordErrorDetails(error) {
|
|
|
371
416
|
function mapDiscordErrorToActionExecutionError(error) {
|
|
372
417
|
const details = extractDiscordErrorDetails(error);
|
|
373
418
|
const message = details.message ?? (error instanceof Error ? error.message : "Discord action failed.");
|
|
419
|
+
const detailPayload = {
|
|
420
|
+
...typeof details.status === "number" ? { discordStatus: details.status } : {},
|
|
421
|
+
...typeof details.code === "number" ? { discordCode: details.code } : {}
|
|
422
|
+
};
|
|
374
423
|
if (details.status === 403 || details.code !== void 0 && DISCORD_FORBIDDEN_CODES.has(details.code)) {
|
|
375
|
-
return new ActionExecutionError("FORBIDDEN", message);
|
|
424
|
+
return new ActionExecutionError("FORBIDDEN", message, detailPayload);
|
|
376
425
|
}
|
|
377
426
|
if (details.status === 404 || details.code !== void 0 && DISCORD_NOT_FOUND_CODES.has(details.code)) {
|
|
378
|
-
return new ActionExecutionError("NOT_FOUND", message);
|
|
427
|
+
return new ActionExecutionError("NOT_FOUND", message, detailPayload);
|
|
379
428
|
}
|
|
380
429
|
if (details.status === 400 || details.code !== void 0 && DISCORD_INVALID_REQUEST_CODES.has(details.code)) {
|
|
381
|
-
return new ActionExecutionError("INVALID_REQUEST", message);
|
|
430
|
+
return new ActionExecutionError("INVALID_REQUEST", message, detailPayload);
|
|
431
|
+
}
|
|
432
|
+
if (details.status === 429) {
|
|
433
|
+
let retryAfterMs;
|
|
434
|
+
if (error instanceof import_discord2.DiscordAPIError) {
|
|
435
|
+
const raw = error.rawError;
|
|
436
|
+
const retryAfter = raw?.retry_after;
|
|
437
|
+
if (typeof retryAfter === "number" && Number.isFinite(retryAfter)) {
|
|
438
|
+
retryAfterMs = Math.max(0, Math.ceil(retryAfter * 1e3));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return new ActionExecutionError("SERVICE_UNAVAILABLE", message, {
|
|
442
|
+
discordStatus: 429,
|
|
443
|
+
retryable: true,
|
|
444
|
+
...details.code !== void 0 ? { discordCode: details.code } : {},
|
|
445
|
+
...retryAfterMs !== void 0 ? { retryAfterMs } : {}
|
|
446
|
+
});
|
|
382
447
|
}
|
|
383
448
|
return null;
|
|
384
449
|
}
|
|
@@ -399,7 +464,14 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
399
464
|
deleteMessage: (payload) => this.deleteMessage(payload),
|
|
400
465
|
replyToInteraction: (payload) => this.replyToInteraction(payload),
|
|
401
466
|
deferInteraction: (payload) => this.deferInteraction(payload),
|
|
467
|
+
deferUpdateInteraction: (payload) => this.deferUpdateInteraction(payload),
|
|
402
468
|
followUpInteraction: (payload) => this.followUpInteraction(payload),
|
|
469
|
+
editInteractionReply: (payload) => this.editInteractionReply(payload),
|
|
470
|
+
deleteInteractionReply: (payload) => this.deleteInteractionReply(payload),
|
|
471
|
+
updateInteraction: (payload) => this.updateInteraction(payload),
|
|
472
|
+
showModal: (payload) => this.showModal(payload),
|
|
473
|
+
fetchMessage: (payload) => this.fetchMessage(payload),
|
|
474
|
+
fetchMember: (payload) => this.fetchMember(payload),
|
|
403
475
|
banMember: (payload) => this.banMember(payload),
|
|
404
476
|
kickMember: (payload) => this.kickMember(payload),
|
|
405
477
|
addMemberRole: (payload) => this.addMemberRole(payload),
|
|
@@ -415,6 +487,10 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
415
487
|
actionHandlers;
|
|
416
488
|
readyPromise = null;
|
|
417
489
|
hasReady = false;
|
|
490
|
+
shardEnvelope() {
|
|
491
|
+
const shardId = this.client.shard?.ids?.[0];
|
|
492
|
+
return shardId !== void 0 ? { shardId } : {};
|
|
493
|
+
}
|
|
418
494
|
isReady() {
|
|
419
495
|
return this.hasReady && this.client.isReady();
|
|
420
496
|
}
|
|
@@ -463,6 +539,7 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
463
539
|
}
|
|
464
540
|
handler({
|
|
465
541
|
receivedAt: Date.now(),
|
|
542
|
+
...this.shardEnvelope(),
|
|
466
543
|
user: serializeUser(this.client.user)
|
|
467
544
|
});
|
|
468
545
|
};
|
|
@@ -476,11 +553,10 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
476
553
|
}
|
|
477
554
|
case "interactionCreate": {
|
|
478
555
|
const listener = (interaction) => {
|
|
479
|
-
|
|
480
|
-
this.interactionCache.set(interaction.id, interaction);
|
|
481
|
-
}
|
|
556
|
+
this.interactionCache.set(interaction.id, interaction);
|
|
482
557
|
handler({
|
|
483
558
|
receivedAt: Date.now(),
|
|
559
|
+
...this.shardEnvelope(),
|
|
484
560
|
interaction: serializeInteraction(interaction)
|
|
485
561
|
});
|
|
486
562
|
};
|
|
@@ -493,6 +569,7 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
493
569
|
const listener = (message) => {
|
|
494
570
|
handler({
|
|
495
571
|
receivedAt: Date.now(),
|
|
572
|
+
...this.shardEnvelope(),
|
|
496
573
|
message: serializeMessage(message)
|
|
497
574
|
});
|
|
498
575
|
};
|
|
@@ -505,6 +582,7 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
505
582
|
const listener = (oldMessage, newMessage) => {
|
|
506
583
|
handler({
|
|
507
584
|
receivedAt: Date.now(),
|
|
585
|
+
...this.shardEnvelope(),
|
|
508
586
|
oldMessage: serializeMessage(oldMessage),
|
|
509
587
|
message: serializeMessage(newMessage)
|
|
510
588
|
});
|
|
@@ -518,6 +596,7 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
518
596
|
const listener = (message) => {
|
|
519
597
|
handler({
|
|
520
598
|
receivedAt: Date.now(),
|
|
599
|
+
...this.shardEnvelope(),
|
|
521
600
|
message: serializeDeletedMessage(message)
|
|
522
601
|
});
|
|
523
602
|
};
|
|
@@ -530,6 +609,7 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
530
609
|
const listener = (reaction, user) => {
|
|
531
610
|
handler({
|
|
532
611
|
receivedAt: Date.now(),
|
|
612
|
+
...this.shardEnvelope(),
|
|
533
613
|
reaction: serializeMessageReaction(reaction, user)
|
|
534
614
|
});
|
|
535
615
|
};
|
|
@@ -542,6 +622,7 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
542
622
|
const listener = (reaction, user) => {
|
|
543
623
|
handler({
|
|
544
624
|
receivedAt: Date.now(),
|
|
625
|
+
...this.shardEnvelope(),
|
|
545
626
|
reaction: serializeMessageReaction(reaction, user)
|
|
546
627
|
});
|
|
547
628
|
};
|
|
@@ -554,6 +635,7 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
554
635
|
const listener = (member) => {
|
|
555
636
|
handler({
|
|
556
637
|
receivedAt: Date.now(),
|
|
638
|
+
...this.shardEnvelope(),
|
|
557
639
|
member: serializeGuildMember(member)
|
|
558
640
|
});
|
|
559
641
|
};
|
|
@@ -566,6 +648,7 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
566
648
|
const listener = (member) => {
|
|
567
649
|
handler({
|
|
568
650
|
receivedAt: Date.now(),
|
|
651
|
+
...this.shardEnvelope(),
|
|
569
652
|
member: serializeGuildMember(member)
|
|
570
653
|
});
|
|
571
654
|
};
|
|
@@ -574,6 +657,91 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
574
657
|
this.client.off(import_discord2.Events.GuildMemberRemove, listener);
|
|
575
658
|
};
|
|
576
659
|
}
|
|
660
|
+
case "guildMemberUpdate": {
|
|
661
|
+
const listener = (oldMember, newMember) => {
|
|
662
|
+
handler({
|
|
663
|
+
receivedAt: Date.now(),
|
|
664
|
+
...this.shardEnvelope(),
|
|
665
|
+
oldMember: serializeGuildMember(oldMember),
|
|
666
|
+
member: serializeGuildMember(newMember)
|
|
667
|
+
});
|
|
668
|
+
};
|
|
669
|
+
this.client.on(import_discord2.Events.GuildMemberUpdate, listener);
|
|
670
|
+
return () => {
|
|
671
|
+
this.client.off(import_discord2.Events.GuildMemberUpdate, listener);
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
case "guildCreate": {
|
|
675
|
+
const listener = (guild) => {
|
|
676
|
+
handler({
|
|
677
|
+
receivedAt: Date.now(),
|
|
678
|
+
...this.shardEnvelope(),
|
|
679
|
+
guild: serializeGuild(guild)
|
|
680
|
+
});
|
|
681
|
+
};
|
|
682
|
+
this.client.on(import_discord2.Events.GuildCreate, listener);
|
|
683
|
+
return () => {
|
|
684
|
+
this.client.off(import_discord2.Events.GuildCreate, listener);
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
case "guildDelete": {
|
|
688
|
+
const listener = (guild) => {
|
|
689
|
+
handler({
|
|
690
|
+
receivedAt: Date.now(),
|
|
691
|
+
...this.shardEnvelope(),
|
|
692
|
+
guild: {
|
|
693
|
+
id: guild.id,
|
|
694
|
+
name: guild.name ?? "Unknown",
|
|
695
|
+
icon: guild.icon,
|
|
696
|
+
...guild.ownerId ? { ownerId: guild.ownerId } : {}
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
};
|
|
700
|
+
this.client.on(import_discord2.Events.GuildDelete, listener);
|
|
701
|
+
return () => {
|
|
702
|
+
this.client.off(import_discord2.Events.GuildDelete, listener);
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
case "threadCreate": {
|
|
706
|
+
const listener = (thread) => {
|
|
707
|
+
handler({
|
|
708
|
+
receivedAt: Date.now(),
|
|
709
|
+
...this.shardEnvelope(),
|
|
710
|
+
thread: serializeThread(thread)
|
|
711
|
+
});
|
|
712
|
+
};
|
|
713
|
+
this.client.on(import_discord2.Events.ThreadCreate, listener);
|
|
714
|
+
return () => {
|
|
715
|
+
this.client.off(import_discord2.Events.ThreadCreate, listener);
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
case "threadUpdate": {
|
|
719
|
+
const listener = (oldThread, newThread) => {
|
|
720
|
+
handler({
|
|
721
|
+
receivedAt: Date.now(),
|
|
722
|
+
...this.shardEnvelope(),
|
|
723
|
+
oldThread: serializeThread(oldThread),
|
|
724
|
+
thread: serializeThread(newThread)
|
|
725
|
+
});
|
|
726
|
+
};
|
|
727
|
+
this.client.on(import_discord2.Events.ThreadUpdate, listener);
|
|
728
|
+
return () => {
|
|
729
|
+
this.client.off(import_discord2.Events.ThreadUpdate, listener);
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
case "threadDelete": {
|
|
733
|
+
const listener = (thread) => {
|
|
734
|
+
handler({
|
|
735
|
+
receivedAt: Date.now(),
|
|
736
|
+
...this.shardEnvelope(),
|
|
737
|
+
thread: serializeThread(thread)
|
|
738
|
+
});
|
|
739
|
+
};
|
|
740
|
+
this.client.on(import_discord2.Events.ThreadDelete, listener);
|
|
741
|
+
return () => {
|
|
742
|
+
this.client.off(import_discord2.Events.ThreadDelete, listener);
|
|
743
|
+
};
|
|
744
|
+
}
|
|
577
745
|
default:
|
|
578
746
|
return () => void 0;
|
|
579
747
|
}
|
|
@@ -592,7 +760,10 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
592
760
|
throw mappedError;
|
|
593
761
|
}
|
|
594
762
|
this.logger.error("Discord action execution failed.", { action: name, error: String(error) });
|
|
595
|
-
throw new ActionExecutionError(
|
|
763
|
+
throw new ActionExecutionError(
|
|
764
|
+
"INTERNAL_ERROR",
|
|
765
|
+
error instanceof Error ? error.message : "Discord action failed."
|
|
766
|
+
);
|
|
596
767
|
}
|
|
597
768
|
}
|
|
598
769
|
async fetchSendableChannel(channelId) {
|
|
@@ -616,6 +787,26 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
616
787
|
}
|
|
617
788
|
return interaction;
|
|
618
789
|
}
|
|
790
|
+
getReplyCapableInteraction(interactionId) {
|
|
791
|
+
const interaction = this.getInteraction(interactionId);
|
|
792
|
+
if (!isReplyCapableInteraction(interaction)) {
|
|
793
|
+
throw new ActionExecutionError(
|
|
794
|
+
"INVALID_REQUEST",
|
|
795
|
+
`Interaction "${interactionId}" does not support reply-style acknowledgements.`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
return interaction;
|
|
799
|
+
}
|
|
800
|
+
getMessageComponentInteraction(interactionId) {
|
|
801
|
+
const interaction = this.getInteraction(interactionId);
|
|
802
|
+
if (!interaction.isMessageComponent()) {
|
|
803
|
+
throw new ActionExecutionError(
|
|
804
|
+
"INVALID_REQUEST",
|
|
805
|
+
`Interaction "${interactionId}" is not a message component interaction.`
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
return interaction;
|
|
809
|
+
}
|
|
619
810
|
async sendMessage(payload) {
|
|
620
811
|
const channel = await this.fetchSendableChannel(payload.channelId);
|
|
621
812
|
const message = await channel.send(toSendOptions(payload));
|
|
@@ -638,9 +829,12 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
638
829
|
};
|
|
639
830
|
}
|
|
640
831
|
async replyToInteraction(payload) {
|
|
641
|
-
const interaction = this.
|
|
832
|
+
const interaction = this.getReplyCapableInteraction(payload.interactionId);
|
|
642
833
|
if (interaction.replied || interaction.deferred) {
|
|
643
|
-
throw new ActionExecutionError(
|
|
834
|
+
throw new ActionExecutionError(
|
|
835
|
+
"INVALID_REQUEST",
|
|
836
|
+
`Interaction "${payload.interactionId}" has already been acknowledged.`
|
|
837
|
+
);
|
|
644
838
|
}
|
|
645
839
|
const reply = await interaction.reply({
|
|
646
840
|
...toSendOptions(payload),
|
|
@@ -650,9 +844,12 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
650
844
|
return serializeMessage(reply);
|
|
651
845
|
}
|
|
652
846
|
async deferInteraction(payload) {
|
|
653
|
-
const interaction = this.
|
|
847
|
+
const interaction = this.getReplyCapableInteraction(payload.interactionId);
|
|
654
848
|
if (interaction.replied) {
|
|
655
|
-
throw new ActionExecutionError(
|
|
849
|
+
throw new ActionExecutionError(
|
|
850
|
+
"INVALID_REQUEST",
|
|
851
|
+
`Interaction "${payload.interactionId}" has already been replied to.`
|
|
852
|
+
);
|
|
656
853
|
}
|
|
657
854
|
if (!interaction.deferred) {
|
|
658
855
|
await interaction.deferReply({
|
|
@@ -664,10 +861,21 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
664
861
|
interactionId: payload.interactionId
|
|
665
862
|
};
|
|
666
863
|
}
|
|
864
|
+
async deferUpdateInteraction(payload) {
|
|
865
|
+
const interaction = this.getMessageComponentInteraction(payload.interactionId);
|
|
866
|
+
await interaction.deferUpdate();
|
|
867
|
+
return {
|
|
868
|
+
deferred: true,
|
|
869
|
+
interactionId: payload.interactionId
|
|
870
|
+
};
|
|
871
|
+
}
|
|
667
872
|
async followUpInteraction(payload) {
|
|
668
|
-
const interaction = this.
|
|
873
|
+
const interaction = this.getReplyCapableInteraction(payload.interactionId);
|
|
669
874
|
if (!interaction.replied && !interaction.deferred) {
|
|
670
|
-
throw new ActionExecutionError(
|
|
875
|
+
throw new ActionExecutionError(
|
|
876
|
+
"INVALID_REQUEST",
|
|
877
|
+
`Interaction "${payload.interactionId}" has not been acknowledged yet.`
|
|
878
|
+
);
|
|
671
879
|
}
|
|
672
880
|
const followUp = await interaction.followUp({
|
|
673
881
|
...toSendOptions(payload),
|
|
@@ -675,6 +883,52 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
675
883
|
});
|
|
676
884
|
return serializeMessage(followUp);
|
|
677
885
|
}
|
|
886
|
+
async editInteractionReply(payload) {
|
|
887
|
+
const interaction = this.getReplyCapableInteraction(payload.interactionId);
|
|
888
|
+
const updated = await interaction.editReply({
|
|
889
|
+
...toSendOptions(payload),
|
|
890
|
+
fetchReply: true
|
|
891
|
+
});
|
|
892
|
+
return serializeMessage(updated);
|
|
893
|
+
}
|
|
894
|
+
async deleteInteractionReply(payload) {
|
|
895
|
+
const interaction = this.getReplyCapableInteraction(payload.interactionId);
|
|
896
|
+
await interaction.deleteReply();
|
|
897
|
+
return {
|
|
898
|
+
deleted: true,
|
|
899
|
+
interactionId: payload.interactionId
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
async updateInteraction(payload) {
|
|
903
|
+
const interaction = this.getMessageComponentInteraction(payload.interactionId);
|
|
904
|
+
const updated = await interaction.update({
|
|
905
|
+
...toSendOptions(payload),
|
|
906
|
+
fetchReply: true
|
|
907
|
+
});
|
|
908
|
+
return serializeMessage(updated);
|
|
909
|
+
}
|
|
910
|
+
async showModal(payload) {
|
|
911
|
+
const interaction = this.getMessageComponentInteraction(payload.interactionId);
|
|
912
|
+
await interaction.showModal({
|
|
913
|
+
title: payload.title,
|
|
914
|
+
customId: payload.customId,
|
|
915
|
+
components: payload.components
|
|
916
|
+
});
|
|
917
|
+
return {
|
|
918
|
+
shown: true,
|
|
919
|
+
interactionId: payload.interactionId
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
async fetchMessage(payload) {
|
|
923
|
+
const channel = await this.fetchMessageChannel(payload.channelId);
|
|
924
|
+
const message = await channel.messages.fetch(payload.messageId);
|
|
925
|
+
return serializeMessage(message);
|
|
926
|
+
}
|
|
927
|
+
async fetchMember(payload) {
|
|
928
|
+
const guild = await this.client.guilds.fetch(payload.guildId);
|
|
929
|
+
const member = await guild.members.fetch(payload.userId);
|
|
930
|
+
return serializeGuildMember(member);
|
|
931
|
+
}
|
|
678
932
|
async banMember(payload) {
|
|
679
933
|
const guild = await this.client.guilds.fetch(payload.guildId);
|
|
680
934
|
await guild.members.ban(payload.userId, {
|
|
@@ -730,7 +984,10 @@ var DiscordJsRuntimeAdapter = class {
|
|
|
730
984
|
return candidate.emoji.identifier === payload.emoji || candidate.emoji.name === payload.emoji || candidate.emoji.toString() === payload.emoji;
|
|
731
985
|
});
|
|
732
986
|
if (!reaction) {
|
|
733
|
-
throw new ActionExecutionError(
|
|
987
|
+
throw new ActionExecutionError(
|
|
988
|
+
"NOT_FOUND",
|
|
989
|
+
`Reaction "${payload.emoji}" was not found on message "${payload.messageId}".`
|
|
990
|
+
);
|
|
734
991
|
}
|
|
735
992
|
await reaction.users.remove(ownUserId);
|
|
736
993
|
return {
|
|
@@ -744,7 +1001,7 @@ function createDiscordJsRuntimeAdapter(options) {
|
|
|
744
1001
|
return new DiscordJsRuntimeAdapter(options);
|
|
745
1002
|
}
|
|
746
1003
|
function isReplyCapableInteraction(interaction) {
|
|
747
|
-
return interaction.isRepliable() && typeof interaction.reply === "function" && typeof interaction.deferReply === "function" && typeof interaction.followUp === "function";
|
|
1004
|
+
return interaction.isRepliable() && typeof interaction.reply === "function" && typeof interaction.deferReply === "function" && typeof interaction.followUp === "function" && typeof interaction.editReply === "function" && typeof interaction.deleteReply === "function";
|
|
748
1005
|
}
|
|
749
1006
|
|
|
750
1007
|
// src/bridge/transport/server.ts
|
|
@@ -768,6 +1025,16 @@ function normalizeStringList(value) {
|
|
|
768
1025
|
const normalized = [...new Set(rawValues.filter((entry) => typeof entry === "string" && entry.length > 0))].sort();
|
|
769
1026
|
return normalized.length > 0 ? normalized : void 0;
|
|
770
1027
|
}
|
|
1028
|
+
function normalizeKindList(value) {
|
|
1029
|
+
if (value === void 0) {
|
|
1030
|
+
return void 0;
|
|
1031
|
+
}
|
|
1032
|
+
const rawValues = Array.isArray(value) ? value : [value];
|
|
1033
|
+
const normalized = [
|
|
1034
|
+
...new Set(rawValues.filter((entry) => typeof entry === "string"))
|
|
1035
|
+
].sort();
|
|
1036
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
1037
|
+
}
|
|
771
1038
|
function normalizeEventSubscriptionFilter(filter) {
|
|
772
1039
|
if (!filter) {
|
|
773
1040
|
return void 0;
|
|
@@ -777,6 +1044,8 @@ function normalizeEventSubscriptionFilter(filter) {
|
|
|
777
1044
|
const channelIds = normalizeStringList(filter.channelId);
|
|
778
1045
|
const userIds = normalizeStringList(filter.userId);
|
|
779
1046
|
const commandNames = normalizeStringList(filter.commandName);
|
|
1047
|
+
const customIds = normalizeStringList(filter.customId);
|
|
1048
|
+
const interactionKinds = normalizeKindList(filter.interactionKind);
|
|
780
1049
|
if (guildIds) {
|
|
781
1050
|
normalized.guildId = guildIds;
|
|
782
1051
|
}
|
|
@@ -789,6 +1058,12 @@ function normalizeEventSubscriptionFilter(filter) {
|
|
|
789
1058
|
if (commandNames) {
|
|
790
1059
|
normalized.commandName = commandNames;
|
|
791
1060
|
}
|
|
1061
|
+
if (customIds) {
|
|
1062
|
+
normalized.customId = customIds;
|
|
1063
|
+
}
|
|
1064
|
+
if (interactionKinds) {
|
|
1065
|
+
normalized.interactionKind = interactionKinds;
|
|
1066
|
+
}
|
|
792
1067
|
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
793
1068
|
}
|
|
794
1069
|
function normalizeEventSubscription(subscription) {
|
|
@@ -810,17 +1085,29 @@ function matchesField(value, allowed) {
|
|
|
810
1085
|
}
|
|
811
1086
|
return allowed.includes(value);
|
|
812
1087
|
}
|
|
1088
|
+
function matchesKind(value, allowed) {
|
|
1089
|
+
if (!allowed) {
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
1092
|
+
if (!value) {
|
|
1093
|
+
return false;
|
|
1094
|
+
}
|
|
1095
|
+
return allowed.includes(value);
|
|
1096
|
+
}
|
|
813
1097
|
function eventMetadata(name, payload) {
|
|
814
1098
|
switch (name) {
|
|
815
1099
|
case "ready":
|
|
816
1100
|
return {};
|
|
817
1101
|
case "interactionCreate": {
|
|
818
1102
|
const interactionPayload = payload;
|
|
1103
|
+
const ix = interactionPayload.interaction;
|
|
819
1104
|
return {
|
|
820
|
-
...
|
|
821
|
-
...
|
|
822
|
-
userId:
|
|
823
|
-
...
|
|
1105
|
+
...ix.guildId ? { guildId: ix.guildId } : {},
|
|
1106
|
+
...ix.channelId ? { channelId: ix.channelId } : {},
|
|
1107
|
+
userId: ix.user.id,
|
|
1108
|
+
...ix.commandName ? { commandName: ix.commandName } : {},
|
|
1109
|
+
...ix.customId ? { customId: ix.customId } : {},
|
|
1110
|
+
interactionKind: ix.kind
|
|
824
1111
|
};
|
|
825
1112
|
}
|
|
826
1113
|
case "messageCreate": {
|
|
@@ -876,6 +1163,29 @@ function eventMetadata(name, payload) {
|
|
|
876
1163
|
userId: memberPayload.member.id
|
|
877
1164
|
};
|
|
878
1165
|
}
|
|
1166
|
+
case "guildMemberUpdate": {
|
|
1167
|
+
const memberPayload = payload;
|
|
1168
|
+
return {
|
|
1169
|
+
guildId: memberPayload.member.guildId,
|
|
1170
|
+
userId: memberPayload.member.id
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
case "guildCreate":
|
|
1174
|
+
case "guildDelete": {
|
|
1175
|
+
const guildPayload = payload;
|
|
1176
|
+
return {
|
|
1177
|
+
guildId: guildPayload.guild.id
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
case "threadCreate":
|
|
1181
|
+
case "threadUpdate":
|
|
1182
|
+
case "threadDelete": {
|
|
1183
|
+
const threadPayload = payload;
|
|
1184
|
+
return {
|
|
1185
|
+
guildId: threadPayload.thread.guildId,
|
|
1186
|
+
...threadPayload.thread.parentId ? { channelId: threadPayload.thread.parentId } : { channelId: threadPayload.thread.id }
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
879
1189
|
default:
|
|
880
1190
|
return {};
|
|
881
1191
|
}
|
|
@@ -886,7 +1196,7 @@ function matchesEventSubscription(subscription, payload) {
|
|
|
886
1196
|
return true;
|
|
887
1197
|
}
|
|
888
1198
|
const metadata = eventMetadata(normalized.name, payload);
|
|
889
|
-
return matchesField(metadata.guildId, normalized.filter.guildId) && matchesField(metadata.channelId, normalized.filter.channelId) && matchesField(metadata.userId, normalized.filter.userId) && matchesField(metadata.commandName, normalized.filter.commandName);
|
|
1199
|
+
return matchesField(metadata.guildId, normalized.filter.guildId) && matchesField(metadata.channelId, normalized.filter.channelId) && matchesField(metadata.userId, normalized.filter.userId) && matchesField(metadata.commandName, normalized.filter.commandName) && matchesField(metadata.customId, normalized.filter.customId) && matchesKind(metadata.interactionKind, normalized.filter.interactionKind);
|
|
890
1200
|
}
|
|
891
1201
|
|
|
892
1202
|
// src/bridge/transport/security.ts
|
|
@@ -922,22 +1232,81 @@ function stringifyEnvelope(envelope) {
|
|
|
922
1232
|
return JSON.stringify(envelope);
|
|
923
1233
|
}
|
|
924
1234
|
|
|
1235
|
+
// src/utils/semaphore.ts
|
|
1236
|
+
var AsyncSemaphore = class {
|
|
1237
|
+
constructor(max, acquireTimeoutMs) {
|
|
1238
|
+
this.max = max;
|
|
1239
|
+
this.acquireTimeoutMs = acquireTimeoutMs;
|
|
1240
|
+
}
|
|
1241
|
+
max;
|
|
1242
|
+
acquireTimeoutMs;
|
|
1243
|
+
active = 0;
|
|
1244
|
+
waiters = [];
|
|
1245
|
+
async run(fn) {
|
|
1246
|
+
const release = await this.acquire();
|
|
1247
|
+
try {
|
|
1248
|
+
return await fn();
|
|
1249
|
+
} finally {
|
|
1250
|
+
release();
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
acquire() {
|
|
1254
|
+
if (this.active < this.max) {
|
|
1255
|
+
this.active += 1;
|
|
1256
|
+
return Promise.resolve(() => this.release());
|
|
1257
|
+
}
|
|
1258
|
+
return new Promise((resolve, reject) => {
|
|
1259
|
+
const timeout = setTimeout(() => {
|
|
1260
|
+
const index = this.waiters.findIndex((entry) => entry.timeout === timeout);
|
|
1261
|
+
if (index >= 0) {
|
|
1262
|
+
this.waiters.splice(index, 1);
|
|
1263
|
+
}
|
|
1264
|
+
reject(new Error("ACTION_QUEUE_TIMEOUT"));
|
|
1265
|
+
}, this.acquireTimeoutMs);
|
|
1266
|
+
this.waiters.push({
|
|
1267
|
+
resolve: (release) => {
|
|
1268
|
+
clearTimeout(timeout);
|
|
1269
|
+
resolve(release);
|
|
1270
|
+
},
|
|
1271
|
+
reject,
|
|
1272
|
+
timeout
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
release() {
|
|
1277
|
+
this.active -= 1;
|
|
1278
|
+
const next = this.waiters.shift();
|
|
1279
|
+
if (next) {
|
|
1280
|
+
clearTimeout(next.timeout);
|
|
1281
|
+
this.active += 1;
|
|
1282
|
+
next.resolve(() => this.release());
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
|
|
925
1287
|
// src/bridge/transport/server.ts
|
|
926
1288
|
var CLOSE_AUTH_REQUIRED = 4001;
|
|
927
1289
|
var CLOSE_AUTH_FAILED = 4003;
|
|
928
1290
|
var CLOSE_INVALID_PAYLOAD = 4004;
|
|
1291
|
+
var CLOSE_SERVER_FULL = 4029;
|
|
1292
|
+
var CLOSE_AUTH_RATE_LIMITED = 4031;
|
|
929
1293
|
var BridgeTransportServer = class {
|
|
930
1294
|
constructor(config) {
|
|
931
1295
|
this.config = config;
|
|
932
1296
|
this.logger = withLogger(config.logger);
|
|
933
1297
|
this.heartbeatMs = config.options.server.heartbeatMs ?? 3e4;
|
|
1298
|
+
const maxConcurrent = config.options.server.maxConcurrentActions ?? 32;
|
|
1299
|
+
const queueTimeout = config.options.server.actionQueueTimeoutMs ?? 5e3;
|
|
1300
|
+
this.actionSemaphore = new AsyncSemaphore(maxConcurrent, queueTimeout);
|
|
1301
|
+
this.idempotencyTtlMs = config.options.server.idempotencyTtlMs ?? 12e4;
|
|
1302
|
+
this.idempotencyScope = config.options.server.idempotencyScope ?? "connection";
|
|
934
1303
|
this.wss = new import_ws.WebSocketServer({
|
|
935
1304
|
host: config.options.server.host,
|
|
936
1305
|
port: config.options.server.port,
|
|
937
1306
|
path: config.options.server.path ?? "/shardwire",
|
|
938
1307
|
maxPayload: config.options.server.maxPayloadBytes ?? 65536
|
|
939
1308
|
});
|
|
940
|
-
this.wss.on("connection", (socket) => this.handleConnection(socket));
|
|
1309
|
+
this.wss.on("connection", (socket, request) => this.handleConnection(socket, request));
|
|
941
1310
|
this.wss.on("error", (error) => this.logger.error("Bridge transport server error.", { error: String(error) }));
|
|
942
1311
|
this.interval = setInterval(() => {
|
|
943
1312
|
this.checkHeartbeats();
|
|
@@ -951,6 +1320,11 @@ var BridgeTransportServer = class {
|
|
|
951
1320
|
interval;
|
|
952
1321
|
connections = /* @__PURE__ */ new Map();
|
|
953
1322
|
stickyEvents = /* @__PURE__ */ new Map();
|
|
1323
|
+
actionSemaphore;
|
|
1324
|
+
idempotencyCache = /* @__PURE__ */ new Map();
|
|
1325
|
+
idempotencyTtlMs;
|
|
1326
|
+
idempotencyScope;
|
|
1327
|
+
authBuckets = /* @__PURE__ */ new Map();
|
|
954
1328
|
connectionCount() {
|
|
955
1329
|
let count = 0;
|
|
956
1330
|
for (const state of this.connections.values()) {
|
|
@@ -994,12 +1368,14 @@ var BridgeTransportServer = class {
|
|
|
994
1368
|
});
|
|
995
1369
|
});
|
|
996
1370
|
}
|
|
997
|
-
handleConnection(socket) {
|
|
1371
|
+
handleConnection(socket, request) {
|
|
1372
|
+
const remoteAddress = request?.socket?.remoteAddress ?? "unknown";
|
|
998
1373
|
const state = {
|
|
999
1374
|
id: createConnectionId(),
|
|
1000
1375
|
socket,
|
|
1001
1376
|
authenticated: false,
|
|
1002
1377
|
lastHeartbeatAt: Date.now(),
|
|
1378
|
+
remoteAddress,
|
|
1003
1379
|
subscriptions: /* @__PURE__ */ new Map()
|
|
1004
1380
|
};
|
|
1005
1381
|
this.connections.set(socket, state);
|
|
@@ -1050,6 +1426,21 @@ var BridgeTransportServer = class {
|
|
|
1050
1426
|
state.socket.close(CLOSE_AUTH_REQUIRED, "Authentication required.");
|
|
1051
1427
|
return;
|
|
1052
1428
|
}
|
|
1429
|
+
if (this.isAuthRateLimited(state.remoteAddress ?? "unknown")) {
|
|
1430
|
+
this.logger.warn("Bridge auth rate limited.", { connectionId: state.id, remoteAddress: state.remoteAddress });
|
|
1431
|
+
this.safeSend(
|
|
1432
|
+
state.socket,
|
|
1433
|
+
stringifyEnvelope(
|
|
1434
|
+
makeEnvelope("auth.error", {
|
|
1435
|
+
code: "UNAUTHORIZED",
|
|
1436
|
+
reason: "invalid_secret",
|
|
1437
|
+
message: "Too many authentication attempts. Try again later."
|
|
1438
|
+
})
|
|
1439
|
+
)
|
|
1440
|
+
);
|
|
1441
|
+
state.socket.close(CLOSE_AUTH_RATE_LIMITED, "Rate limited.");
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1053
1444
|
const authResult = this.config.authenticate(envelope.payload);
|
|
1054
1445
|
if (!authResult.ok) {
|
|
1055
1446
|
const message = authResult.reason === "ambiguous_secret" ? "Authentication failed: secret matches multiple configured scopes. Supply secretId or use unique secret values." : "Authentication failed.";
|
|
@@ -1066,6 +1457,25 @@ var BridgeTransportServer = class {
|
|
|
1066
1457
|
state.socket.close(CLOSE_AUTH_FAILED, "Invalid secret.");
|
|
1067
1458
|
return;
|
|
1068
1459
|
}
|
|
1460
|
+
const maxConnections = this.config.options.server.maxConnections;
|
|
1461
|
+
if (maxConnections !== void 0 && this.connectionCount() >= maxConnections) {
|
|
1462
|
+
this.logger.warn("Bridge connection rejected: server full.", {
|
|
1463
|
+
connectionId: state.id,
|
|
1464
|
+
maxConnections
|
|
1465
|
+
});
|
|
1466
|
+
this.safeSend(
|
|
1467
|
+
state.socket,
|
|
1468
|
+
stringifyEnvelope(
|
|
1469
|
+
makeEnvelope("auth.error", {
|
|
1470
|
+
code: "UNAUTHORIZED",
|
|
1471
|
+
reason: "invalid_secret",
|
|
1472
|
+
message: "Server is at maximum connection capacity."
|
|
1473
|
+
})
|
|
1474
|
+
)
|
|
1475
|
+
);
|
|
1476
|
+
state.socket.close(CLOSE_SERVER_FULL, "Server full.");
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1069
1479
|
state.authenticated = true;
|
|
1070
1480
|
state.lastHeartbeatAt = Date.now();
|
|
1071
1481
|
state.secret = authResult.secret;
|
|
@@ -1135,17 +1545,75 @@ var BridgeTransportServer = class {
|
|
|
1135
1545
|
if (!requestId || !payload || typeof payload.name !== "string") {
|
|
1136
1546
|
return;
|
|
1137
1547
|
}
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1548
|
+
const activeSecret = state.secret;
|
|
1549
|
+
const activeCapabilities = state.capabilities;
|
|
1550
|
+
if (!activeSecret || !activeCapabilities) {
|
|
1551
|
+
state.socket.close(CLOSE_AUTH_FAILED, "Invalid state.");
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
const idempotencyRaw = payload.idempotencyKey;
|
|
1555
|
+
const idempotencyKey = typeof idempotencyRaw === "string" && idempotencyRaw.length > 0 && idempotencyRaw.length <= 256 ? idempotencyRaw : void 0;
|
|
1556
|
+
const idempotencyCacheKey = idempotencyKey ? this.idempotencyScope === "secret" && activeSecret ? `secret:${activeSecret.id}:${idempotencyKey}` : `conn:${state.id}:${idempotencyKey}` : void 0;
|
|
1557
|
+
if (idempotencyCacheKey) {
|
|
1558
|
+
this.pruneIdempotencyCache(Date.now());
|
|
1559
|
+
const cached = this.idempotencyCache.get(idempotencyCacheKey);
|
|
1560
|
+
if (cached && cached.expires > Date.now()) {
|
|
1561
|
+
const replay = cached.result.ok ? { ok: true, requestId, ts: Date.now(), data: cached.result.data } : { ok: false, requestId, ts: Date.now(), error: cached.result.error };
|
|
1562
|
+
this.logger.info("Bridge action idempotent replay.", {
|
|
1563
|
+
connectionId: state.id,
|
|
1564
|
+
requestId,
|
|
1565
|
+
action: payload.name
|
|
1566
|
+
});
|
|
1567
|
+
this.safeSend(
|
|
1568
|
+
state.socket,
|
|
1569
|
+
stringifyEnvelope(makeEnvelope(replay.ok ? "action.result" : "action.error", replay, { requestId }))
|
|
1570
|
+
);
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
const started = Date.now();
|
|
1575
|
+
let result;
|
|
1576
|
+
try {
|
|
1577
|
+
result = await this.actionSemaphore.run(
|
|
1578
|
+
() => this.config.onActionRequest(
|
|
1579
|
+
{
|
|
1580
|
+
id: state.id,
|
|
1581
|
+
...state.appName ? { appName: state.appName } : {},
|
|
1582
|
+
secret: activeSecret,
|
|
1583
|
+
capabilities: activeCapabilities
|
|
1584
|
+
},
|
|
1585
|
+
payload.name,
|
|
1586
|
+
payload.data,
|
|
1587
|
+
requestId
|
|
1588
|
+
)
|
|
1589
|
+
);
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
const message = error instanceof Error ? error.message : "Action queue saturated.";
|
|
1592
|
+
result = {
|
|
1593
|
+
ok: false,
|
|
1594
|
+
requestId,
|
|
1595
|
+
ts: Date.now(),
|
|
1596
|
+
error: {
|
|
1597
|
+
code: "SERVICE_UNAVAILABLE",
|
|
1598
|
+
message,
|
|
1599
|
+
details: { retryable: true, reason: "action_queue" }
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
if (idempotencyCacheKey) {
|
|
1604
|
+
this.idempotencyCache.set(idempotencyCacheKey, {
|
|
1605
|
+
result,
|
|
1606
|
+
expires: Date.now() + this.idempotencyTtlMs
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
this.logger.info("Bridge action completed.", {
|
|
1610
|
+
connectionId: state.id,
|
|
1611
|
+
requestId,
|
|
1612
|
+
action: payload.name,
|
|
1613
|
+
durationMs: Date.now() - started,
|
|
1614
|
+
ok: result.ok,
|
|
1615
|
+
appName: state.appName
|
|
1616
|
+
});
|
|
1149
1617
|
this.safeSend(
|
|
1150
1618
|
state.socket,
|
|
1151
1619
|
stringifyEnvelope(makeEnvelope(result.ok ? "action.result" : "action.error", result, { requestId }))
|
|
@@ -1176,6 +1644,28 @@ var BridgeTransportServer = class {
|
|
|
1176
1644
|
socket.send(payload);
|
|
1177
1645
|
}
|
|
1178
1646
|
}
|
|
1647
|
+
isAuthRateLimited(remoteAddress) {
|
|
1648
|
+
const windowMs = 6e4;
|
|
1649
|
+
const limit = 40;
|
|
1650
|
+
const now = Date.now();
|
|
1651
|
+
let bucket = this.authBuckets.get(remoteAddress);
|
|
1652
|
+
if (!bucket || now > bucket.resetAt) {
|
|
1653
|
+
bucket = { count: 0, resetAt: now + windowMs };
|
|
1654
|
+
this.authBuckets.set(remoteAddress, bucket);
|
|
1655
|
+
}
|
|
1656
|
+
bucket.count += 1;
|
|
1657
|
+
return bucket.count > limit;
|
|
1658
|
+
}
|
|
1659
|
+
pruneIdempotencyCache(now) {
|
|
1660
|
+
if (this.idempotencyCache.size < 200) {
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
for (const [key, entry] of this.idempotencyCache.entries()) {
|
|
1664
|
+
if (entry.expires <= now) {
|
|
1665
|
+
this.idempotencyCache.delete(key);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1179
1669
|
};
|
|
1180
1670
|
function authenticateSecret(payload, secrets, resolver) {
|
|
1181
1671
|
if (!payload.secret) {
|
|
@@ -1267,11 +1757,7 @@ function normalizeSecretEntry(secret, index) {
|
|
|
1267
1757
|
value: scoped.value,
|
|
1268
1758
|
scope: {
|
|
1269
1759
|
events: normalizeScopeList(scoped.allow?.events, BOT_EVENT_NAMES, `server.secrets[${index}].allow.events`),
|
|
1270
|
-
actions: normalizeScopeList(
|
|
1271
|
-
scoped.allow?.actions,
|
|
1272
|
-
BOT_ACTION_NAMES,
|
|
1273
|
-
`server.secrets[${index}].allow.actions`
|
|
1274
|
-
)
|
|
1760
|
+
actions: normalizeScopeList(scoped.allow?.actions, BOT_ACTION_NAMES, `server.secrets[${index}].allow.actions`)
|
|
1275
1761
|
}
|
|
1276
1762
|
};
|
|
1277
1763
|
}
|
|
@@ -1289,6 +1775,23 @@ function assertBotBridgeOptions(options) {
|
|
|
1289
1775
|
if (options.server.maxPayloadBytes !== void 0) {
|
|
1290
1776
|
assertPositiveNumber("server.maxPayloadBytes", options.server.maxPayloadBytes);
|
|
1291
1777
|
}
|
|
1778
|
+
if (options.server.maxConnections !== void 0) {
|
|
1779
|
+
assertPositiveNumber("server.maxConnections", options.server.maxConnections);
|
|
1780
|
+
}
|
|
1781
|
+
if (options.server.maxConcurrentActions !== void 0) {
|
|
1782
|
+
assertPositiveNumber("server.maxConcurrentActions", options.server.maxConcurrentActions);
|
|
1783
|
+
}
|
|
1784
|
+
if (options.server.actionQueueTimeoutMs !== void 0) {
|
|
1785
|
+
assertPositiveNumber("server.actionQueueTimeoutMs", options.server.actionQueueTimeoutMs);
|
|
1786
|
+
}
|
|
1787
|
+
if (options.server.idempotencyScope !== void 0) {
|
|
1788
|
+
if (options.server.idempotencyScope !== "connection" && options.server.idempotencyScope !== "secret") {
|
|
1789
|
+
throw new Error('server.idempotencyScope must be "connection" or "secret".');
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
if (options.server.idempotencyTtlMs !== void 0) {
|
|
1793
|
+
assertPositiveNumber("server.idempotencyTtlMs", options.server.idempotencyTtlMs);
|
|
1794
|
+
}
|
|
1292
1795
|
if (!Array.isArray(options.server.secrets) || options.server.secrets.length === 0) {
|
|
1293
1796
|
throw new Error("server.secrets must contain at least one secret.");
|
|
1294
1797
|
}
|
|
@@ -1488,9 +1991,28 @@ var AppRequestError = class extends Error {
|
|
|
1488
1991
|
code;
|
|
1489
1992
|
};
|
|
1490
1993
|
var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
|
|
1994
|
+
function metricsExtrasFromActionError(error) {
|
|
1995
|
+
const details = error.details;
|
|
1996
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
|
1997
|
+
return {};
|
|
1998
|
+
}
|
|
1999
|
+
const obj = details;
|
|
2000
|
+
const extras = {};
|
|
2001
|
+
if (typeof obj.retryAfterMs === "number" && Number.isFinite(obj.retryAfterMs)) {
|
|
2002
|
+
extras.retryAfterMs = obj.retryAfterMs;
|
|
2003
|
+
}
|
|
2004
|
+
if (typeof obj.discordStatus === "number" && Number.isFinite(obj.discordStatus)) {
|
|
2005
|
+
extras.discordStatus = obj.discordStatus;
|
|
2006
|
+
}
|
|
2007
|
+
if (typeof obj.discordCode === "number" && Number.isFinite(obj.discordCode)) {
|
|
2008
|
+
extras.discordCode = obj.discordCode;
|
|
2009
|
+
}
|
|
2010
|
+
return extras;
|
|
2011
|
+
}
|
|
1491
2012
|
function connectBotBridge(options) {
|
|
1492
2013
|
assertAppBridgeOptions(options);
|
|
1493
2014
|
const logger = withLogger(options.logger);
|
|
2015
|
+
const metrics = options.metrics;
|
|
1494
2016
|
const reconnectEnabled = options.reconnect?.enabled ?? true;
|
|
1495
2017
|
const initialDelayMs = options.reconnect?.initialDelayMs ?? 500;
|
|
1496
2018
|
const maxDelayMs = options.reconnect?.maxDelayMs ?? 1e4;
|
|
@@ -1763,6 +2285,7 @@ function connectBotBridge(options) {
|
|
|
1763
2285
|
}
|
|
1764
2286
|
const requestId = sendOptions?.requestId ?? createRequestId();
|
|
1765
2287
|
const timeoutMs = sendOptions?.timeoutMs ?? requestTimeoutMs;
|
|
2288
|
+
const started = Date.now();
|
|
1766
2289
|
const promise = new Promise((resolve, reject) => {
|
|
1767
2290
|
const timer = setTimeout(() => {
|
|
1768
2291
|
pendingRequests.delete(requestId);
|
|
@@ -1780,16 +2303,32 @@ function connectBotBridge(options) {
|
|
|
1780
2303
|
"action.request",
|
|
1781
2304
|
{
|
|
1782
2305
|
name,
|
|
1783
|
-
data: payload
|
|
2306
|
+
data: payload,
|
|
2307
|
+
...sendOptions?.idempotencyKey ? { idempotencyKey: sendOptions.idempotencyKey } : {}
|
|
1784
2308
|
},
|
|
1785
2309
|
{ requestId }
|
|
1786
2310
|
)
|
|
1787
2311
|
)
|
|
1788
2312
|
);
|
|
1789
2313
|
try {
|
|
1790
|
-
|
|
2314
|
+
const result = await promise;
|
|
2315
|
+
metrics?.onActionComplete?.({
|
|
2316
|
+
name,
|
|
2317
|
+
requestId,
|
|
2318
|
+
durationMs: Date.now() - started,
|
|
2319
|
+
ok: result.ok,
|
|
2320
|
+
...!result.ok ? { errorCode: result.error.code, ...metricsExtrasFromActionError(result.error) } : {}
|
|
2321
|
+
});
|
|
2322
|
+
return result;
|
|
1791
2323
|
} catch (error) {
|
|
1792
2324
|
const code = error instanceof AppRequestError ? error.code : !socket || socket.readyState !== 1 ? "DISCONNECTED" : "TIMEOUT";
|
|
2325
|
+
metrics?.onActionComplete?.({
|
|
2326
|
+
name,
|
|
2327
|
+
requestId,
|
|
2328
|
+
durationMs: Date.now() - started,
|
|
2329
|
+
ok: false,
|
|
2330
|
+
errorCode: code
|
|
2331
|
+
});
|
|
1793
2332
|
return {
|
|
1794
2333
|
ok: false,
|
|
1795
2334
|
requestId,
|
|
@@ -1807,7 +2346,14 @@ function connectBotBridge(options) {
|
|
|
1807
2346
|
deleteMessage: (payload, sendOptions) => invokeAction("deleteMessage", payload, sendOptions),
|
|
1808
2347
|
replyToInteraction: (payload, sendOptions) => invokeAction("replyToInteraction", payload, sendOptions),
|
|
1809
2348
|
deferInteraction: (payload, sendOptions) => invokeAction("deferInteraction", payload, sendOptions),
|
|
2349
|
+
deferUpdateInteraction: (payload, sendOptions) => invokeAction("deferUpdateInteraction", payload, sendOptions),
|
|
1810
2350
|
followUpInteraction: (payload, sendOptions) => invokeAction("followUpInteraction", payload, sendOptions),
|
|
2351
|
+
editInteractionReply: (payload, sendOptions) => invokeAction("editInteractionReply", payload, sendOptions),
|
|
2352
|
+
deleteInteractionReply: (payload, sendOptions) => invokeAction("deleteInteractionReply", payload, sendOptions),
|
|
2353
|
+
updateInteraction: (payload, sendOptions) => invokeAction("updateInteraction", payload, sendOptions),
|
|
2354
|
+
showModal: (payload, sendOptions) => invokeAction("showModal", payload, sendOptions),
|
|
2355
|
+
fetchMessage: (payload, sendOptions) => invokeAction("fetchMessage", payload, sendOptions),
|
|
2356
|
+
fetchMember: (payload, sendOptions) => invokeAction("fetchMember", payload, sendOptions),
|
|
1811
2357
|
banMember: (payload, sendOptions) => invokeAction("banMember", payload, sendOptions),
|
|
1812
2358
|
kickMember: (payload, sendOptions) => invokeAction("kickMember", payload, sendOptions),
|
|
1813
2359
|
addMemberRole: (payload, sendOptions) => invokeAction("addMemberRole", payload, sendOptions),
|