shardwire 1.1.0 → 1.2.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/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,8 +80,14 @@ 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);
@@ -158,6 +177,7 @@ function serializeMessage(message) {
158
177
  ...message.reference.channelId ? { channelId: message.reference.channelId } : {},
159
178
  ...message.reference.guildId ? { guildId: message.reference.guildId } : {}
160
179
  } : void 0;
180
+ const components = "components" in message && message.components && message.components.length > 0 ? message.components.map((row) => row.toJSON()) : void 0;
161
181
  return {
162
182
  id: message.id,
163
183
  channelId: message.channelId,
@@ -175,9 +195,29 @@ function serializeMessage(message) {
175
195
  size: attachment.size
176
196
  })) : [],
177
197
  embeds: serializeEmbeds(message),
198
+ ...components ? { components } : {},
178
199
  ...reference ? { reference } : {}
179
200
  };
180
201
  }
202
+ function serializeGuild(guild) {
203
+ return {
204
+ id: guild.id,
205
+ name: guild.name,
206
+ ...guild.icon !== null && guild.icon !== void 0 ? { icon: guild.icon } : { icon: null },
207
+ ownerId: guild.ownerId
208
+ };
209
+ }
210
+ function serializeThread(thread) {
211
+ return {
212
+ id: thread.id,
213
+ guildId: thread.guildId,
214
+ parentId: thread.parentId,
215
+ name: thread.name,
216
+ type: thread.type,
217
+ ...typeof thread.archived === "boolean" ? { archived: thread.archived } : {},
218
+ ...typeof thread.locked === "boolean" ? { locked: thread.locked } : {}
219
+ };
220
+ }
181
221
  function serializeDeletedMessage(message) {
182
222
  return {
183
223
  id: message.id,
@@ -347,7 +387,10 @@ function toSendOptions(input) {
347
387
  return {
348
388
  ...input.content !== void 0 ? { content: input.content } : {},
349
389
  ...input.embeds !== void 0 ? { embeds: input.embeds } : {},
350
- ...input.allowedMentions !== void 0 ? { allowedMentions: input.allowedMentions } : {}
390
+ ...input.allowedMentions !== void 0 ? { allowedMentions: input.allowedMentions } : {},
391
+ ...input.components !== void 0 ? { components: input.components } : {},
392
+ ...input.flags !== void 0 ? { flags: input.flags } : {},
393
+ ...input.stickerIds !== void 0 ? { stickers: input.stickerIds } : {}
351
394
  };
352
395
  }
353
396
  function extractDiscordErrorDetails(error) {
@@ -371,14 +414,25 @@ function extractDiscordErrorDetails(error) {
371
414
  function mapDiscordErrorToActionExecutionError(error) {
372
415
  const details = extractDiscordErrorDetails(error);
373
416
  const message = details.message ?? (error instanceof Error ? error.message : "Discord action failed.");
417
+ const detailPayload = {
418
+ ...typeof details.status === "number" ? { discordStatus: details.status } : {},
419
+ ...typeof details.code === "number" ? { discordCode: details.code } : {}
420
+ };
374
421
  if (details.status === 403 || details.code !== void 0 && DISCORD_FORBIDDEN_CODES.has(details.code)) {
375
- return new ActionExecutionError("FORBIDDEN", message);
422
+ return new ActionExecutionError("FORBIDDEN", message, detailPayload);
376
423
  }
377
424
  if (details.status === 404 || details.code !== void 0 && DISCORD_NOT_FOUND_CODES.has(details.code)) {
378
- return new ActionExecutionError("NOT_FOUND", message);
425
+ return new ActionExecutionError("NOT_FOUND", message, detailPayload);
379
426
  }
380
427
  if (details.status === 400 || details.code !== void 0 && DISCORD_INVALID_REQUEST_CODES.has(details.code)) {
381
- return new ActionExecutionError("INVALID_REQUEST", message);
428
+ return new ActionExecutionError("INVALID_REQUEST", message, detailPayload);
429
+ }
430
+ if (details.status === 429) {
431
+ return new ActionExecutionError("SERVICE_UNAVAILABLE", message, {
432
+ discordStatus: 429,
433
+ retryable: true,
434
+ ...details.code !== void 0 ? { discordCode: details.code } : {}
435
+ });
382
436
  }
383
437
  return null;
384
438
  }
@@ -399,7 +453,14 @@ var DiscordJsRuntimeAdapter = class {
399
453
  deleteMessage: (payload) => this.deleteMessage(payload),
400
454
  replyToInteraction: (payload) => this.replyToInteraction(payload),
401
455
  deferInteraction: (payload) => this.deferInteraction(payload),
456
+ deferUpdateInteraction: (payload) => this.deferUpdateInteraction(payload),
402
457
  followUpInteraction: (payload) => this.followUpInteraction(payload),
458
+ editInteractionReply: (payload) => this.editInteractionReply(payload),
459
+ deleteInteractionReply: (payload) => this.deleteInteractionReply(payload),
460
+ updateInteraction: (payload) => this.updateInteraction(payload),
461
+ showModal: (payload) => this.showModal(payload),
462
+ fetchMessage: (payload) => this.fetchMessage(payload),
463
+ fetchMember: (payload) => this.fetchMember(payload),
403
464
  banMember: (payload) => this.banMember(payload),
404
465
  kickMember: (payload) => this.kickMember(payload),
405
466
  addMemberRole: (payload) => this.addMemberRole(payload),
@@ -415,6 +476,10 @@ var DiscordJsRuntimeAdapter = class {
415
476
  actionHandlers;
416
477
  readyPromise = null;
417
478
  hasReady = false;
479
+ shardEnvelope() {
480
+ const shardId = this.client.shard?.ids?.[0];
481
+ return shardId !== void 0 ? { shardId } : {};
482
+ }
418
483
  isReady() {
419
484
  return this.hasReady && this.client.isReady();
420
485
  }
@@ -463,6 +528,7 @@ var DiscordJsRuntimeAdapter = class {
463
528
  }
464
529
  handler({
465
530
  receivedAt: Date.now(),
531
+ ...this.shardEnvelope(),
466
532
  user: serializeUser(this.client.user)
467
533
  });
468
534
  };
@@ -476,11 +542,10 @@ var DiscordJsRuntimeAdapter = class {
476
542
  }
477
543
  case "interactionCreate": {
478
544
  const listener = (interaction) => {
479
- if (isReplyCapableInteraction(interaction)) {
480
- this.interactionCache.set(interaction.id, interaction);
481
- }
545
+ this.interactionCache.set(interaction.id, interaction);
482
546
  handler({
483
547
  receivedAt: Date.now(),
548
+ ...this.shardEnvelope(),
484
549
  interaction: serializeInteraction(interaction)
485
550
  });
486
551
  };
@@ -493,6 +558,7 @@ var DiscordJsRuntimeAdapter = class {
493
558
  const listener = (message) => {
494
559
  handler({
495
560
  receivedAt: Date.now(),
561
+ ...this.shardEnvelope(),
496
562
  message: serializeMessage(message)
497
563
  });
498
564
  };
@@ -505,6 +571,7 @@ var DiscordJsRuntimeAdapter = class {
505
571
  const listener = (oldMessage, newMessage) => {
506
572
  handler({
507
573
  receivedAt: Date.now(),
574
+ ...this.shardEnvelope(),
508
575
  oldMessage: serializeMessage(oldMessage),
509
576
  message: serializeMessage(newMessage)
510
577
  });
@@ -518,6 +585,7 @@ var DiscordJsRuntimeAdapter = class {
518
585
  const listener = (message) => {
519
586
  handler({
520
587
  receivedAt: Date.now(),
588
+ ...this.shardEnvelope(),
521
589
  message: serializeDeletedMessage(message)
522
590
  });
523
591
  };
@@ -530,6 +598,7 @@ var DiscordJsRuntimeAdapter = class {
530
598
  const listener = (reaction, user) => {
531
599
  handler({
532
600
  receivedAt: Date.now(),
601
+ ...this.shardEnvelope(),
533
602
  reaction: serializeMessageReaction(reaction, user)
534
603
  });
535
604
  };
@@ -542,6 +611,7 @@ var DiscordJsRuntimeAdapter = class {
542
611
  const listener = (reaction, user) => {
543
612
  handler({
544
613
  receivedAt: Date.now(),
614
+ ...this.shardEnvelope(),
545
615
  reaction: serializeMessageReaction(reaction, user)
546
616
  });
547
617
  };
@@ -554,6 +624,7 @@ var DiscordJsRuntimeAdapter = class {
554
624
  const listener = (member) => {
555
625
  handler({
556
626
  receivedAt: Date.now(),
627
+ ...this.shardEnvelope(),
557
628
  member: serializeGuildMember(member)
558
629
  });
559
630
  };
@@ -566,6 +637,7 @@ var DiscordJsRuntimeAdapter = class {
566
637
  const listener = (member) => {
567
638
  handler({
568
639
  receivedAt: Date.now(),
640
+ ...this.shardEnvelope(),
569
641
  member: serializeGuildMember(member)
570
642
  });
571
643
  };
@@ -574,6 +646,91 @@ var DiscordJsRuntimeAdapter = class {
574
646
  this.client.off(import_discord2.Events.GuildMemberRemove, listener);
575
647
  };
576
648
  }
649
+ case "guildMemberUpdate": {
650
+ const listener = (oldMember, newMember) => {
651
+ handler({
652
+ receivedAt: Date.now(),
653
+ ...this.shardEnvelope(),
654
+ oldMember: serializeGuildMember(oldMember),
655
+ member: serializeGuildMember(newMember)
656
+ });
657
+ };
658
+ this.client.on(import_discord2.Events.GuildMemberUpdate, listener);
659
+ return () => {
660
+ this.client.off(import_discord2.Events.GuildMemberUpdate, listener);
661
+ };
662
+ }
663
+ case "guildCreate": {
664
+ const listener = (guild) => {
665
+ handler({
666
+ receivedAt: Date.now(),
667
+ ...this.shardEnvelope(),
668
+ guild: serializeGuild(guild)
669
+ });
670
+ };
671
+ this.client.on(import_discord2.Events.GuildCreate, listener);
672
+ return () => {
673
+ this.client.off(import_discord2.Events.GuildCreate, listener);
674
+ };
675
+ }
676
+ case "guildDelete": {
677
+ const listener = (guild) => {
678
+ handler({
679
+ receivedAt: Date.now(),
680
+ ...this.shardEnvelope(),
681
+ guild: {
682
+ id: guild.id,
683
+ name: guild.name ?? "Unknown",
684
+ icon: guild.icon,
685
+ ...guild.ownerId ? { ownerId: guild.ownerId } : {}
686
+ }
687
+ });
688
+ };
689
+ this.client.on(import_discord2.Events.GuildDelete, listener);
690
+ return () => {
691
+ this.client.off(import_discord2.Events.GuildDelete, listener);
692
+ };
693
+ }
694
+ case "threadCreate": {
695
+ const listener = (thread) => {
696
+ handler({
697
+ receivedAt: Date.now(),
698
+ ...this.shardEnvelope(),
699
+ thread: serializeThread(thread)
700
+ });
701
+ };
702
+ this.client.on(import_discord2.Events.ThreadCreate, listener);
703
+ return () => {
704
+ this.client.off(import_discord2.Events.ThreadCreate, listener);
705
+ };
706
+ }
707
+ case "threadUpdate": {
708
+ const listener = (oldThread, newThread) => {
709
+ handler({
710
+ receivedAt: Date.now(),
711
+ ...this.shardEnvelope(),
712
+ oldThread: serializeThread(oldThread),
713
+ thread: serializeThread(newThread)
714
+ });
715
+ };
716
+ this.client.on(import_discord2.Events.ThreadUpdate, listener);
717
+ return () => {
718
+ this.client.off(import_discord2.Events.ThreadUpdate, listener);
719
+ };
720
+ }
721
+ case "threadDelete": {
722
+ const listener = (thread) => {
723
+ handler({
724
+ receivedAt: Date.now(),
725
+ ...this.shardEnvelope(),
726
+ thread: serializeThread(thread)
727
+ });
728
+ };
729
+ this.client.on(import_discord2.Events.ThreadDelete, listener);
730
+ return () => {
731
+ this.client.off(import_discord2.Events.ThreadDelete, listener);
732
+ };
733
+ }
577
734
  default:
578
735
  return () => void 0;
579
736
  }
@@ -616,6 +773,26 @@ var DiscordJsRuntimeAdapter = class {
616
773
  }
617
774
  return interaction;
618
775
  }
776
+ getReplyCapableInteraction(interactionId) {
777
+ const interaction = this.getInteraction(interactionId);
778
+ if (!isReplyCapableInteraction(interaction)) {
779
+ throw new ActionExecutionError(
780
+ "INVALID_REQUEST",
781
+ `Interaction "${interactionId}" does not support reply-style acknowledgements.`
782
+ );
783
+ }
784
+ return interaction;
785
+ }
786
+ getMessageComponentInteraction(interactionId) {
787
+ const interaction = this.getInteraction(interactionId);
788
+ if (!interaction.isMessageComponent()) {
789
+ throw new ActionExecutionError(
790
+ "INVALID_REQUEST",
791
+ `Interaction "${interactionId}" is not a message component interaction.`
792
+ );
793
+ }
794
+ return interaction;
795
+ }
619
796
  async sendMessage(payload) {
620
797
  const channel = await this.fetchSendableChannel(payload.channelId);
621
798
  const message = await channel.send(toSendOptions(payload));
@@ -638,7 +815,7 @@ var DiscordJsRuntimeAdapter = class {
638
815
  };
639
816
  }
640
817
  async replyToInteraction(payload) {
641
- const interaction = this.getInteraction(payload.interactionId);
818
+ const interaction = this.getReplyCapableInteraction(payload.interactionId);
642
819
  if (interaction.replied || interaction.deferred) {
643
820
  throw new ActionExecutionError("INVALID_REQUEST", `Interaction "${payload.interactionId}" has already been acknowledged.`);
644
821
  }
@@ -650,7 +827,7 @@ var DiscordJsRuntimeAdapter = class {
650
827
  return serializeMessage(reply);
651
828
  }
652
829
  async deferInteraction(payload) {
653
- const interaction = this.getInteraction(payload.interactionId);
830
+ const interaction = this.getReplyCapableInteraction(payload.interactionId);
654
831
  if (interaction.replied) {
655
832
  throw new ActionExecutionError("INVALID_REQUEST", `Interaction "${payload.interactionId}" has already been replied to.`);
656
833
  }
@@ -664,8 +841,16 @@ var DiscordJsRuntimeAdapter = class {
664
841
  interactionId: payload.interactionId
665
842
  };
666
843
  }
844
+ async deferUpdateInteraction(payload) {
845
+ const interaction = this.getMessageComponentInteraction(payload.interactionId);
846
+ await interaction.deferUpdate();
847
+ return {
848
+ deferred: true,
849
+ interactionId: payload.interactionId
850
+ };
851
+ }
667
852
  async followUpInteraction(payload) {
668
- const interaction = this.getInteraction(payload.interactionId);
853
+ const interaction = this.getReplyCapableInteraction(payload.interactionId);
669
854
  if (!interaction.replied && !interaction.deferred) {
670
855
  throw new ActionExecutionError("INVALID_REQUEST", `Interaction "${payload.interactionId}" has not been acknowledged yet.`);
671
856
  }
@@ -675,6 +860,52 @@ var DiscordJsRuntimeAdapter = class {
675
860
  });
676
861
  return serializeMessage(followUp);
677
862
  }
863
+ async editInteractionReply(payload) {
864
+ const interaction = this.getReplyCapableInteraction(payload.interactionId);
865
+ const updated = await interaction.editReply({
866
+ ...toSendOptions(payload),
867
+ fetchReply: true
868
+ });
869
+ return serializeMessage(updated);
870
+ }
871
+ async deleteInteractionReply(payload) {
872
+ const interaction = this.getReplyCapableInteraction(payload.interactionId);
873
+ await interaction.deleteReply();
874
+ return {
875
+ deleted: true,
876
+ interactionId: payload.interactionId
877
+ };
878
+ }
879
+ async updateInteraction(payload) {
880
+ const interaction = this.getMessageComponentInteraction(payload.interactionId);
881
+ const updated = await interaction.update({
882
+ ...toSendOptions(payload),
883
+ fetchReply: true
884
+ });
885
+ return serializeMessage(updated);
886
+ }
887
+ async showModal(payload) {
888
+ const interaction = this.getMessageComponentInteraction(payload.interactionId);
889
+ await interaction.showModal({
890
+ title: payload.title,
891
+ customId: payload.customId,
892
+ components: payload.components
893
+ });
894
+ return {
895
+ shown: true,
896
+ interactionId: payload.interactionId
897
+ };
898
+ }
899
+ async fetchMessage(payload) {
900
+ const channel = await this.fetchMessageChannel(payload.channelId);
901
+ const message = await channel.messages.fetch(payload.messageId);
902
+ return serializeMessage(message);
903
+ }
904
+ async fetchMember(payload) {
905
+ const guild = await this.client.guilds.fetch(payload.guildId);
906
+ const member = await guild.members.fetch(payload.userId);
907
+ return serializeGuildMember(member);
908
+ }
678
909
  async banMember(payload) {
679
910
  const guild = await this.client.guilds.fetch(payload.guildId);
680
911
  await guild.members.ban(payload.userId, {
@@ -744,7 +975,7 @@ function createDiscordJsRuntimeAdapter(options) {
744
975
  return new DiscordJsRuntimeAdapter(options);
745
976
  }
746
977
  function isReplyCapableInteraction(interaction) {
747
- return interaction.isRepliable() && typeof interaction.reply === "function" && typeof interaction.deferReply === "function" && typeof interaction.followUp === "function";
978
+ return interaction.isRepliable() && typeof interaction.reply === "function" && typeof interaction.deferReply === "function" && typeof interaction.followUp === "function" && typeof interaction.editReply === "function" && typeof interaction.deleteReply === "function";
748
979
  }
749
980
 
750
981
  // src/bridge/transport/server.ts
@@ -768,6 +999,14 @@ function normalizeStringList(value) {
768
999
  const normalized = [...new Set(rawValues.filter((entry) => typeof entry === "string" && entry.length > 0))].sort();
769
1000
  return normalized.length > 0 ? normalized : void 0;
770
1001
  }
1002
+ function normalizeKindList(value) {
1003
+ if (value === void 0) {
1004
+ return void 0;
1005
+ }
1006
+ const rawValues = Array.isArray(value) ? value : [value];
1007
+ const normalized = [...new Set(rawValues.filter((entry) => typeof entry === "string"))].sort();
1008
+ return normalized.length > 0 ? normalized : void 0;
1009
+ }
771
1010
  function normalizeEventSubscriptionFilter(filter) {
772
1011
  if (!filter) {
773
1012
  return void 0;
@@ -777,6 +1016,8 @@ function normalizeEventSubscriptionFilter(filter) {
777
1016
  const channelIds = normalizeStringList(filter.channelId);
778
1017
  const userIds = normalizeStringList(filter.userId);
779
1018
  const commandNames = normalizeStringList(filter.commandName);
1019
+ const customIds = normalizeStringList(filter.customId);
1020
+ const interactionKinds = normalizeKindList(filter.interactionKind);
780
1021
  if (guildIds) {
781
1022
  normalized.guildId = guildIds;
782
1023
  }
@@ -789,6 +1030,12 @@ function normalizeEventSubscriptionFilter(filter) {
789
1030
  if (commandNames) {
790
1031
  normalized.commandName = commandNames;
791
1032
  }
1033
+ if (customIds) {
1034
+ normalized.customId = customIds;
1035
+ }
1036
+ if (interactionKinds) {
1037
+ normalized.interactionKind = interactionKinds;
1038
+ }
792
1039
  return Object.keys(normalized).length > 0 ? normalized : void 0;
793
1040
  }
794
1041
  function normalizeEventSubscription(subscription) {
@@ -810,17 +1057,29 @@ function matchesField(value, allowed) {
810
1057
  }
811
1058
  return allowed.includes(value);
812
1059
  }
1060
+ function matchesKind(value, allowed) {
1061
+ if (!allowed) {
1062
+ return true;
1063
+ }
1064
+ if (!value) {
1065
+ return false;
1066
+ }
1067
+ return allowed.includes(value);
1068
+ }
813
1069
  function eventMetadata(name, payload) {
814
1070
  switch (name) {
815
1071
  case "ready":
816
1072
  return {};
817
1073
  case "interactionCreate": {
818
1074
  const interactionPayload = payload;
1075
+ const ix = interactionPayload.interaction;
819
1076
  return {
820
- ...interactionPayload.interaction.guildId ? { guildId: interactionPayload.interaction.guildId } : {},
821
- ...interactionPayload.interaction.channelId ? { channelId: interactionPayload.interaction.channelId } : {},
822
- userId: interactionPayload.interaction.user.id,
823
- ...interactionPayload.interaction.commandName ? { commandName: interactionPayload.interaction.commandName } : {}
1077
+ ...ix.guildId ? { guildId: ix.guildId } : {},
1078
+ ...ix.channelId ? { channelId: ix.channelId } : {},
1079
+ userId: ix.user.id,
1080
+ ...ix.commandName ? { commandName: ix.commandName } : {},
1081
+ ...ix.customId ? { customId: ix.customId } : {},
1082
+ interactionKind: ix.kind
824
1083
  };
825
1084
  }
826
1085
  case "messageCreate": {
@@ -876,6 +1135,29 @@ function eventMetadata(name, payload) {
876
1135
  userId: memberPayload.member.id
877
1136
  };
878
1137
  }
1138
+ case "guildMemberUpdate": {
1139
+ const memberPayload = payload;
1140
+ return {
1141
+ guildId: memberPayload.member.guildId,
1142
+ userId: memberPayload.member.id
1143
+ };
1144
+ }
1145
+ case "guildCreate":
1146
+ case "guildDelete": {
1147
+ const guildPayload = payload;
1148
+ return {
1149
+ guildId: guildPayload.guild.id
1150
+ };
1151
+ }
1152
+ case "threadCreate":
1153
+ case "threadUpdate":
1154
+ case "threadDelete": {
1155
+ const threadPayload = payload;
1156
+ return {
1157
+ guildId: threadPayload.thread.guildId,
1158
+ ...threadPayload.thread.parentId ? { channelId: threadPayload.thread.parentId } : { channelId: threadPayload.thread.id }
1159
+ };
1160
+ }
879
1161
  default:
880
1162
  return {};
881
1163
  }
@@ -886,7 +1168,7 @@ function matchesEventSubscription(subscription, payload) {
886
1168
  return true;
887
1169
  }
888
1170
  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);
1171
+ 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
1172
  }
891
1173
 
892
1174
  // src/bridge/transport/security.ts
@@ -922,22 +1204,79 @@ function stringifyEnvelope(envelope) {
922
1204
  return JSON.stringify(envelope);
923
1205
  }
924
1206
 
1207
+ // src/utils/semaphore.ts
1208
+ var AsyncSemaphore = class {
1209
+ constructor(max, acquireTimeoutMs) {
1210
+ this.max = max;
1211
+ this.acquireTimeoutMs = acquireTimeoutMs;
1212
+ }
1213
+ max;
1214
+ acquireTimeoutMs;
1215
+ active = 0;
1216
+ waiters = [];
1217
+ async run(fn) {
1218
+ const release = await this.acquire();
1219
+ try {
1220
+ return await fn();
1221
+ } finally {
1222
+ release();
1223
+ }
1224
+ }
1225
+ acquire() {
1226
+ if (this.active < this.max) {
1227
+ this.active += 1;
1228
+ return Promise.resolve(() => this.release());
1229
+ }
1230
+ return new Promise((resolve, reject) => {
1231
+ const timeout = setTimeout(() => {
1232
+ const index = this.waiters.findIndex((entry) => entry.timeout === timeout);
1233
+ if (index >= 0) {
1234
+ this.waiters.splice(index, 1);
1235
+ }
1236
+ reject(new Error("ACTION_QUEUE_TIMEOUT"));
1237
+ }, this.acquireTimeoutMs);
1238
+ this.waiters.push({
1239
+ resolve: (release) => {
1240
+ clearTimeout(timeout);
1241
+ resolve(release);
1242
+ },
1243
+ reject,
1244
+ timeout
1245
+ });
1246
+ });
1247
+ }
1248
+ release() {
1249
+ this.active -= 1;
1250
+ const next = this.waiters.shift();
1251
+ if (next) {
1252
+ clearTimeout(next.timeout);
1253
+ this.active += 1;
1254
+ next.resolve(() => this.release());
1255
+ }
1256
+ }
1257
+ };
1258
+
925
1259
  // src/bridge/transport/server.ts
926
1260
  var CLOSE_AUTH_REQUIRED = 4001;
927
1261
  var CLOSE_AUTH_FAILED = 4003;
928
1262
  var CLOSE_INVALID_PAYLOAD = 4004;
1263
+ var CLOSE_SERVER_FULL = 4029;
1264
+ var CLOSE_AUTH_RATE_LIMITED = 4031;
929
1265
  var BridgeTransportServer = class {
930
1266
  constructor(config) {
931
1267
  this.config = config;
932
1268
  this.logger = withLogger(config.logger);
933
1269
  this.heartbeatMs = config.options.server.heartbeatMs ?? 3e4;
1270
+ const maxConcurrent = config.options.server.maxConcurrentActions ?? 32;
1271
+ const queueTimeout = config.options.server.actionQueueTimeoutMs ?? 5e3;
1272
+ this.actionSemaphore = new AsyncSemaphore(maxConcurrent, queueTimeout);
934
1273
  this.wss = new import_ws.WebSocketServer({
935
1274
  host: config.options.server.host,
936
1275
  port: config.options.server.port,
937
1276
  path: config.options.server.path ?? "/shardwire",
938
1277
  maxPayload: config.options.server.maxPayloadBytes ?? 65536
939
1278
  });
940
- this.wss.on("connection", (socket) => this.handleConnection(socket));
1279
+ this.wss.on("connection", (socket, request) => this.handleConnection(socket, request));
941
1280
  this.wss.on("error", (error) => this.logger.error("Bridge transport server error.", { error: String(error) }));
942
1281
  this.interval = setInterval(() => {
943
1282
  this.checkHeartbeats();
@@ -951,6 +1290,10 @@ var BridgeTransportServer = class {
951
1290
  interval;
952
1291
  connections = /* @__PURE__ */ new Map();
953
1292
  stickyEvents = /* @__PURE__ */ new Map();
1293
+ actionSemaphore;
1294
+ idempotencyCache = /* @__PURE__ */ new Map();
1295
+ idempotencyTtlMs = 12e4;
1296
+ authBuckets = /* @__PURE__ */ new Map();
954
1297
  connectionCount() {
955
1298
  let count = 0;
956
1299
  for (const state of this.connections.values()) {
@@ -994,12 +1337,14 @@ var BridgeTransportServer = class {
994
1337
  });
995
1338
  });
996
1339
  }
997
- handleConnection(socket) {
1340
+ handleConnection(socket, request) {
1341
+ const remoteAddress = request?.socket?.remoteAddress ?? "unknown";
998
1342
  const state = {
999
1343
  id: createConnectionId(),
1000
1344
  socket,
1001
1345
  authenticated: false,
1002
1346
  lastHeartbeatAt: Date.now(),
1347
+ remoteAddress,
1003
1348
  subscriptions: /* @__PURE__ */ new Map()
1004
1349
  };
1005
1350
  this.connections.set(socket, state);
@@ -1050,6 +1395,21 @@ var BridgeTransportServer = class {
1050
1395
  state.socket.close(CLOSE_AUTH_REQUIRED, "Authentication required.");
1051
1396
  return;
1052
1397
  }
1398
+ if (this.isAuthRateLimited(state.remoteAddress ?? "unknown")) {
1399
+ this.logger.warn("Bridge auth rate limited.", { connectionId: state.id, remoteAddress: state.remoteAddress });
1400
+ this.safeSend(
1401
+ state.socket,
1402
+ stringifyEnvelope(
1403
+ makeEnvelope("auth.error", {
1404
+ code: "UNAUTHORIZED",
1405
+ reason: "invalid_secret",
1406
+ message: "Too many authentication attempts. Try again later."
1407
+ })
1408
+ )
1409
+ );
1410
+ state.socket.close(CLOSE_AUTH_RATE_LIMITED, "Rate limited.");
1411
+ return;
1412
+ }
1053
1413
  const authResult = this.config.authenticate(envelope.payload);
1054
1414
  if (!authResult.ok) {
1055
1415
  const message = authResult.reason === "ambiguous_secret" ? "Authentication failed: secret matches multiple configured scopes. Supply secretId or use unique secret values." : "Authentication failed.";
@@ -1066,6 +1426,25 @@ var BridgeTransportServer = class {
1066
1426
  state.socket.close(CLOSE_AUTH_FAILED, "Invalid secret.");
1067
1427
  return;
1068
1428
  }
1429
+ const maxConnections = this.config.options.server.maxConnections;
1430
+ if (maxConnections !== void 0 && this.connectionCount() >= maxConnections) {
1431
+ this.logger.warn("Bridge connection rejected: server full.", {
1432
+ connectionId: state.id,
1433
+ maxConnections
1434
+ });
1435
+ this.safeSend(
1436
+ state.socket,
1437
+ stringifyEnvelope(
1438
+ makeEnvelope("auth.error", {
1439
+ code: "UNAUTHORIZED",
1440
+ reason: "invalid_secret",
1441
+ message: "Server is at maximum connection capacity."
1442
+ })
1443
+ )
1444
+ );
1445
+ state.socket.close(CLOSE_SERVER_FULL, "Server full.");
1446
+ return;
1447
+ }
1069
1448
  state.authenticated = true;
1070
1449
  state.lastHeartbeatAt = Date.now();
1071
1450
  state.secret = authResult.secret;
@@ -1135,17 +1514,77 @@ var BridgeTransportServer = class {
1135
1514
  if (!requestId || !payload || typeof payload.name !== "string") {
1136
1515
  return;
1137
1516
  }
1138
- const result = await this.config.onActionRequest(
1139
- {
1140
- id: state.id,
1141
- ...state.appName ? { appName: state.appName } : {},
1142
- secret: state.secret,
1143
- capabilities: state.capabilities
1144
- },
1145
- payload.name,
1146
- payload.data,
1147
- requestId
1148
- );
1517
+ const activeSecret = state.secret;
1518
+ const activeCapabilities = state.capabilities;
1519
+ if (!activeSecret || !activeCapabilities) {
1520
+ state.socket.close(CLOSE_AUTH_FAILED, "Invalid state.");
1521
+ return;
1522
+ }
1523
+ const idempotencyRaw = payload.idempotencyKey;
1524
+ const idempotencyKey = typeof idempotencyRaw === "string" && idempotencyRaw.length > 0 && idempotencyRaw.length <= 256 ? idempotencyRaw : void 0;
1525
+ const idempotencyCacheKey = idempotencyKey ? `${state.id}:${idempotencyKey}` : void 0;
1526
+ if (idempotencyCacheKey) {
1527
+ this.pruneIdempotencyCache(Date.now());
1528
+ const cached = this.idempotencyCache.get(idempotencyCacheKey);
1529
+ if (cached && cached.expires > Date.now()) {
1530
+ 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 };
1531
+ this.logger.info("Bridge action idempotent replay.", {
1532
+ connectionId: state.id,
1533
+ requestId,
1534
+ action: payload.name
1535
+ });
1536
+ this.safeSend(
1537
+ state.socket,
1538
+ stringifyEnvelope(
1539
+ makeEnvelope(replay.ok ? "action.result" : "action.error", replay, { requestId })
1540
+ )
1541
+ );
1542
+ return;
1543
+ }
1544
+ }
1545
+ const started = Date.now();
1546
+ let result;
1547
+ try {
1548
+ result = await this.actionSemaphore.run(
1549
+ () => this.config.onActionRequest(
1550
+ {
1551
+ id: state.id,
1552
+ ...state.appName ? { appName: state.appName } : {},
1553
+ secret: activeSecret,
1554
+ capabilities: activeCapabilities
1555
+ },
1556
+ payload.name,
1557
+ payload.data,
1558
+ requestId
1559
+ )
1560
+ );
1561
+ } catch (error) {
1562
+ const message = error instanceof Error ? error.message : "Action queue saturated.";
1563
+ result = {
1564
+ ok: false,
1565
+ requestId,
1566
+ ts: Date.now(),
1567
+ error: {
1568
+ code: "SERVICE_UNAVAILABLE",
1569
+ message,
1570
+ details: { retryable: true, reason: "action_queue" }
1571
+ }
1572
+ };
1573
+ }
1574
+ if (idempotencyCacheKey) {
1575
+ this.idempotencyCache.set(idempotencyCacheKey, {
1576
+ result,
1577
+ expires: Date.now() + this.idempotencyTtlMs
1578
+ });
1579
+ }
1580
+ this.logger.info("Bridge action completed.", {
1581
+ connectionId: state.id,
1582
+ requestId,
1583
+ action: payload.name,
1584
+ durationMs: Date.now() - started,
1585
+ ok: result.ok,
1586
+ appName: state.appName
1587
+ });
1149
1588
  this.safeSend(
1150
1589
  state.socket,
1151
1590
  stringifyEnvelope(makeEnvelope(result.ok ? "action.result" : "action.error", result, { requestId }))
@@ -1176,6 +1615,28 @@ var BridgeTransportServer = class {
1176
1615
  socket.send(payload);
1177
1616
  }
1178
1617
  }
1618
+ isAuthRateLimited(remoteAddress) {
1619
+ const windowMs = 6e4;
1620
+ const limit = 40;
1621
+ const now = Date.now();
1622
+ let bucket = this.authBuckets.get(remoteAddress);
1623
+ if (!bucket || now > bucket.resetAt) {
1624
+ bucket = { count: 0, resetAt: now + windowMs };
1625
+ this.authBuckets.set(remoteAddress, bucket);
1626
+ }
1627
+ bucket.count += 1;
1628
+ return bucket.count > limit;
1629
+ }
1630
+ pruneIdempotencyCache(now) {
1631
+ if (this.idempotencyCache.size < 200) {
1632
+ return;
1633
+ }
1634
+ for (const [key, entry] of this.idempotencyCache.entries()) {
1635
+ if (entry.expires <= now) {
1636
+ this.idempotencyCache.delete(key);
1637
+ }
1638
+ }
1639
+ }
1179
1640
  };
1180
1641
  function authenticateSecret(payload, secrets, resolver) {
1181
1642
  if (!payload.secret) {
@@ -1289,6 +1750,15 @@ function assertBotBridgeOptions(options) {
1289
1750
  if (options.server.maxPayloadBytes !== void 0) {
1290
1751
  assertPositiveNumber("server.maxPayloadBytes", options.server.maxPayloadBytes);
1291
1752
  }
1753
+ if (options.server.maxConnections !== void 0) {
1754
+ assertPositiveNumber("server.maxConnections", options.server.maxConnections);
1755
+ }
1756
+ if (options.server.maxConcurrentActions !== void 0) {
1757
+ assertPositiveNumber("server.maxConcurrentActions", options.server.maxConcurrentActions);
1758
+ }
1759
+ if (options.server.actionQueueTimeoutMs !== void 0) {
1760
+ assertPositiveNumber("server.actionQueueTimeoutMs", options.server.actionQueueTimeoutMs);
1761
+ }
1292
1762
  if (!Array.isArray(options.server.secrets) || options.server.secrets.length === 0) {
1293
1763
  throw new Error("server.secrets must contain at least one secret.");
1294
1764
  }
@@ -1491,6 +1961,7 @@ var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
1491
1961
  function connectBotBridge(options) {
1492
1962
  assertAppBridgeOptions(options);
1493
1963
  const logger = withLogger(options.logger);
1964
+ const metrics = options.metrics;
1494
1965
  const reconnectEnabled = options.reconnect?.enabled ?? true;
1495
1966
  const initialDelayMs = options.reconnect?.initialDelayMs ?? 500;
1496
1967
  const maxDelayMs = options.reconnect?.maxDelayMs ?? 1e4;
@@ -1763,6 +2234,7 @@ function connectBotBridge(options) {
1763
2234
  }
1764
2235
  const requestId = sendOptions?.requestId ?? createRequestId();
1765
2236
  const timeoutMs = sendOptions?.timeoutMs ?? requestTimeoutMs;
2237
+ const started = Date.now();
1766
2238
  const promise = new Promise((resolve, reject) => {
1767
2239
  const timer = setTimeout(() => {
1768
2240
  pendingRequests.delete(requestId);
@@ -1780,16 +2252,32 @@ function connectBotBridge(options) {
1780
2252
  "action.request",
1781
2253
  {
1782
2254
  name,
1783
- data: payload
2255
+ data: payload,
2256
+ ...sendOptions?.idempotencyKey ? { idempotencyKey: sendOptions.idempotencyKey } : {}
1784
2257
  },
1785
2258
  { requestId }
1786
2259
  )
1787
2260
  )
1788
2261
  );
1789
2262
  try {
1790
- return await promise;
2263
+ const result = await promise;
2264
+ metrics?.onActionComplete?.({
2265
+ name,
2266
+ requestId,
2267
+ durationMs: Date.now() - started,
2268
+ ok: result.ok,
2269
+ ...!result.ok ? { errorCode: result.error.code } : {}
2270
+ });
2271
+ return result;
1791
2272
  } catch (error) {
1792
2273
  const code = error instanceof AppRequestError ? error.code : !socket || socket.readyState !== 1 ? "DISCONNECTED" : "TIMEOUT";
2274
+ metrics?.onActionComplete?.({
2275
+ name,
2276
+ requestId,
2277
+ durationMs: Date.now() - started,
2278
+ ok: false,
2279
+ errorCode: code
2280
+ });
1793
2281
  return {
1794
2282
  ok: false,
1795
2283
  requestId,
@@ -1807,7 +2295,14 @@ function connectBotBridge(options) {
1807
2295
  deleteMessage: (payload, sendOptions) => invokeAction("deleteMessage", payload, sendOptions),
1808
2296
  replyToInteraction: (payload, sendOptions) => invokeAction("replyToInteraction", payload, sendOptions),
1809
2297
  deferInteraction: (payload, sendOptions) => invokeAction("deferInteraction", payload, sendOptions),
2298
+ deferUpdateInteraction: (payload, sendOptions) => invokeAction("deferUpdateInteraction", payload, sendOptions),
1810
2299
  followUpInteraction: (payload, sendOptions) => invokeAction("followUpInteraction", payload, sendOptions),
2300
+ editInteractionReply: (payload, sendOptions) => invokeAction("editInteractionReply", payload, sendOptions),
2301
+ deleteInteractionReply: (payload, sendOptions) => invokeAction("deleteInteractionReply", payload, sendOptions),
2302
+ updateInteraction: (payload, sendOptions) => invokeAction("updateInteraction", payload, sendOptions),
2303
+ showModal: (payload, sendOptions) => invokeAction("showModal", payload, sendOptions),
2304
+ fetchMessage: (payload, sendOptions) => invokeAction("fetchMessage", payload, sendOptions),
2305
+ fetchMember: (payload, sendOptions) => invokeAction("fetchMember", payload, sendOptions),
1811
2306
  banMember: (payload, sendOptions) => invokeAction("banMember", payload, sendOptions),
1812
2307
  kickMember: (payload, sendOptions) => invokeAction("kickMember", payload, sendOptions),
1813
2308
  addMemberRole: (payload, sendOptions) => invokeAction("addMemberRole", payload, sendOptions),