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/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((eventName) => EVENT_REQUIRED_INTENTS[eventName].every((intent) => enabled.has(intent)));
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
- if (isReplyCapableInteraction(interaction)) {
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("INTERNAL_ERROR", error instanceof Error ? error.message : "Discord action failed.");
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.getInteraction(payload.interactionId);
832
+ const interaction = this.getReplyCapableInteraction(payload.interactionId);
642
833
  if (interaction.replied || interaction.deferred) {
643
- throw new ActionExecutionError("INVALID_REQUEST", `Interaction "${payload.interactionId}" has already been acknowledged.`);
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.getInteraction(payload.interactionId);
847
+ const interaction = this.getReplyCapableInteraction(payload.interactionId);
654
848
  if (interaction.replied) {
655
- throw new ActionExecutionError("INVALID_REQUEST", `Interaction "${payload.interactionId}" has already been replied to.`);
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.getInteraction(payload.interactionId);
873
+ const interaction = this.getReplyCapableInteraction(payload.interactionId);
669
874
  if (!interaction.replied && !interaction.deferred) {
670
- throw new ActionExecutionError("INVALID_REQUEST", `Interaction "${payload.interactionId}" has not been acknowledged yet.`);
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("NOT_FOUND", `Reaction "${payload.emoji}" was not found on message "${payload.messageId}".`);
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
- ...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 } : {}
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 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
- );
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
- return await promise;
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),