shardwire 1.0.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/README.md CHANGED
@@ -1,25 +1,28 @@
1
1
  # Shardwire
2
2
 
3
- Discord events and bot actions, streamed to your app over a single WebSocket bridge.
3
+ [![npm version](https://img.shields.io/npm/v/shardwire)](https://www.npmjs.com/package/shardwire)
4
+ [![npm downloads](https://img.shields.io/npm/dm/shardwire)](https://www.npmjs.com/package/shardwire)
5
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18.18-339933)](https://nodejs.org/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
4
7
 
5
- Shardwire runs your bot connection, listens to Discord, and exposes a clean app-facing API for:
8
+ Discord-first bridge for streaming bot events to external apps and executing bot actions over one WebSocket connection.
6
9
 
7
- - subscribing to Discord events
8
- - replying to interactions
9
- - sending and editing messages
10
- - moderation actions like ban, kick, and role changes
10
+ Shardwire is built for a common architecture: your Discord bot runs in one process, while your web app, backend API, worker, or dashboard runs in another.
11
11
 
12
- It is designed for one common setup:
12
+ ## Why Shardwire
13
13
 
14
- > your Discord bot lives in one process, while your web app, backend, or worker lives somewhere else
14
+ - **Discord-first API**: no generic command bus wiring required.
15
+ - **App-friendly payloads**: receive normalized JSON payloads instead of live `discord.js` objects.
16
+ - **Built-in actions**: send messages, reply to interactions, moderate members, and more from your app process.
17
+ - **Scoped permissions**: restrict each app secret to specific events and actions.
18
+ - **Capability-aware runtime**: apps can inspect what they are allowed to subscribe to and invoke.
15
19
 
16
- ## Why Shardwire
20
+ ## Requirements
17
21
 
18
- - **Discord-first**: no generic command bus or event map setup
19
- - **Token-first**: start the bot bridge with `token`, `intents`, and `server`
20
- - **App-friendly payloads**: normalized JSON, not live `discord.js` objects
21
- - **Built-in actions**: call `app.actions.sendMessage(...)`, `app.actions.replyToInteraction(...)`, and more
22
- - **Scoped secrets**: optionally limit which apps can subscribe to which events or invoke which actions
22
+ - Node.js `>=18.18`
23
+ - A Discord bot token (`DISCORD_TOKEN`)
24
+ - At least one shared bridge secret (`SHARDWIRE_SECRET`)
25
+ - Discord gateway intents that match the events you want
23
26
 
24
27
  ## Install
25
28
 
@@ -29,14 +32,14 @@ npm install shardwire
29
32
 
30
33
  ## Quick Start
31
34
 
32
- ### 1. Start the bot bridge
35
+ ### 1) Start the bot bridge process
33
36
 
34
37
  ```ts
35
38
  import { createBotBridge } from "shardwire";
36
39
 
37
40
  const bridge = createBotBridge({
38
41
  token: process.env.DISCORD_TOKEN!,
39
- intents: ["Guilds", "GuildMessages", "MessageContent", "GuildMembers"],
42
+ intents: ["Guilds", "GuildMessages", "GuildMessageReactions", "MessageContent", "GuildMembers"],
40
43
  server: {
41
44
  port: 3001,
42
45
  secrets: [process.env.SHARDWIRE_SECRET!],
@@ -44,9 +47,10 @@ const bridge = createBotBridge({
44
47
  });
45
48
 
46
49
  await bridge.ready();
50
+ console.log("Bot bridge listening on ws://127.0.0.1:3001/shardwire");
47
51
  ```
48
52
 
49
- ### 2. Connect from your app
53
+ ### 2) Connect from your app process
50
54
 
51
55
  ```ts
52
56
  import { connectBotBridge } from "shardwire";
@@ -68,24 +72,12 @@ app.on("messageCreate", ({ message }) => {
68
72
  await app.ready();
69
73
  ```
70
74
 
71
- ### 2.5 Filter subscriptions when you need less noise
72
-
73
- ```ts
74
- app.on(
75
- "messageCreate",
76
- ({ message }) => {
77
- console.log("Only channel 123:", message.content);
78
- },
79
- { channelId: "123456789012345678" },
80
- );
81
- ```
82
-
83
- ### 3. Call built-in bot actions
75
+ ### 3) Call bot actions from your app
84
76
 
85
77
  ```ts
86
78
  const result = await app.actions.sendMessage({
87
79
  channelId: "123456789012345678",
88
- content: "Hello from the app side",
80
+ content: "Hello from app side",
89
81
  });
90
82
 
91
83
  if (!result.ok) {
@@ -93,43 +85,80 @@ if (!result.ok) {
93
85
  }
94
86
  ```
95
87
 
88
+ ### 4) Filter subscriptions when needed
89
+
90
+ ```ts
91
+ app.on(
92
+ "messageCreate",
93
+ ({ message }) => {
94
+ console.log("Only this channel:", message.content);
95
+ },
96
+ { channelId: "123456789012345678" },
97
+ );
98
+ ```
99
+
96
100
  ## Built-In Events
97
101
 
98
- Shardwire currently exposes these bot-side events:
102
+ Apps subscribe to events with `app.on(...)`. The bridge forwards only what each app subscribes to.
99
103
 
100
104
  - `ready`
101
105
  - `interactionCreate`
102
106
  - `messageCreate`
103
107
  - `messageUpdate`
104
108
  - `messageDelete`
109
+ - `messageReactionAdd`
110
+ - `messageReactionRemove`
111
+ - `guildCreate`
112
+ - `guildDelete`
105
113
  - `guildMemberAdd`
106
114
  - `guildMemberRemove`
115
+ - `guildMemberUpdate`
116
+ - `threadCreate`
117
+ - `threadUpdate`
118
+ - `threadDelete`
107
119
 
108
- Subscriptions are app-driven. The bot bridge does not need per-event setup. Your app subscribes by calling `app.on(...)`, and the host only forwards the events that app is listening to.
109
-
110
- Optional filters can narrow delivery by:
120
+ Supported filters:
111
121
 
112
122
  - `guildId`
113
123
  - `channelId`
114
124
  - `userId`
115
- - `commandName` for `interactionCreate`
125
+ - `commandName` (for `interactionCreate`)
126
+ - `customId` (for `interactionCreate`)
127
+ - `interactionKind` (for `interactionCreate`)
128
+
129
+ ### Intent Notes
130
+
131
+ - `ready` and `interactionCreate`: no specific event intent requirement
132
+ - `messageCreate`, `messageUpdate`, `messageDelete`: `GuildMessages`
133
+ - `messageReactionAdd`, `messageReactionRemove`: `GuildMessageReactions`
134
+ - `guildCreate`, `guildDelete`, `threadCreate`, `threadUpdate`, `threadDelete`: `Guilds`
135
+ - `guildMemberAdd`, `guildMemberRemove`, `guildMemberUpdate`: `GuildMembers`
116
136
 
117
137
  ## Built-In Actions
118
138
 
119
- `app.actions.*` currently includes:
139
+ `app.actions.*` includes:
120
140
 
121
141
  - `sendMessage`
122
142
  - `editMessage`
123
143
  - `deleteMessage`
124
144
  - `replyToInteraction`
125
145
  - `deferInteraction`
146
+ - `deferUpdateInteraction`
126
147
  - `followUpInteraction`
148
+ - `editInteractionReply`
149
+ - `deleteInteractionReply`
150
+ - `updateInteraction`
151
+ - `showModal`
152
+ - `fetchMessage`
153
+ - `fetchMember`
127
154
  - `banMember`
128
155
  - `kickMember`
129
156
  - `addMemberRole`
130
157
  - `removeMemberRole`
158
+ - `addMessageReaction`
159
+ - `removeOwnMessageReaction`
131
160
 
132
- Every action returns an `ActionResult<T>`:
161
+ All actions return:
133
162
 
134
163
  ```ts
135
164
  type ActionResult<T> =
@@ -137,9 +166,34 @@ type ActionResult<T> =
137
166
  | { ok: false; requestId: string; ts: number; error: { code: string; message: string; details?: unknown } };
138
167
  ```
139
168
 
169
+ ### Idempotency for safe retries
170
+
171
+ You can provide an `idempotencyKey` in action options to dedupe repeated requests on the same connection:
172
+
173
+ ```ts
174
+ await app.actions.sendMessage(
175
+ { channelId: "123456789012345678", content: "Hello once" },
176
+ { idempotencyKey: "notify:order:123" },
177
+ );
178
+ ```
179
+
180
+ ### App-side action metrics
181
+
182
+ ```ts
183
+ const app = connectBotBridge({
184
+ url: "ws://127.0.0.1:3001/shardwire",
185
+ secret: process.env.SHARDWIRE_SECRET!,
186
+ metrics: {
187
+ onActionComplete(meta) {
188
+ console.log(meta.name, meta.durationMs, meta.ok, meta.errorCode);
189
+ },
190
+ },
191
+ });
192
+ ```
193
+
140
194
  ## Secret Scopes
141
195
 
142
- Use a plain string secret for full access:
196
+ Use a plain string secret for full event/action access:
143
197
 
144
198
  ```ts
145
199
  server: {
@@ -148,7 +202,7 @@ server: {
148
202
  }
149
203
  ```
150
204
 
151
- Or scope a secret to specific events and actions:
205
+ Use a scoped secret to limit what an app can do:
152
206
 
153
207
  ```ts
154
208
  server: {
@@ -166,35 +220,58 @@ server: {
166
220
  }
167
221
  ```
168
222
 
169
- On the app side, you can inspect what the connection is allowed to do:
223
+ Inspect negotiated capabilities in the app:
170
224
 
171
225
  ```ts
172
226
  const capabilities = app.capabilities();
173
227
  console.log(capabilities.events, capabilities.actions);
174
228
  ```
175
229
 
230
+ ## Run the Included Examples
231
+
232
+ In two terminals:
233
+
234
+ ```bash
235
+ # terminal 1
236
+ DISCORD_TOKEN=your-token SHARDWIRE_SECRET=dev-secret npm run example:bot
237
+ ```
238
+
239
+ ```bash
240
+ # terminal 2
241
+ SHARDWIRE_SECRET=dev-secret npm run example:app
242
+ ```
243
+
244
+ Examples:
245
+
246
+ - Bot bridge: `examples/bot-basic.ts`
247
+ - App client: `examples/app-basic.ts`
248
+
176
249
  ## Public API
177
250
 
178
251
  ```ts
179
252
  import { createBotBridge, connectBotBridge } from "shardwire";
180
253
  ```
181
254
 
182
- Main exports:
255
+ Main exports include:
183
256
 
184
257
  - `createBotBridge(options)`
185
258
  - `connectBotBridge(options)`
186
259
  - `BridgeCapabilityError`
187
260
  - bot/app option types
188
- - normalized event payload types like `BridgeMessage`, `BridgeInteraction`, and `BridgeGuildMember`
189
- - action payload/result types
261
+ - normalized event payload types (for example `BridgeMessage`, `BridgeInteraction`, `BridgeGuildMember`)
262
+ - action payload and result types
263
+
264
+ ## Security and Transport Notes
265
+
266
+ - Use `wss://` for non-loopback deployments.
267
+ - `ws://` is only accepted for loopback hosts (`127.0.0.1`, `localhost`, `::1`).
268
+ - Event availability depends on enabled intents and secret scope.
269
+ - For vulnerability reporting and security policy, see [`SECURITY.md`](./SECURITY.md).
190
270
 
191
- ## Notes
271
+ ## Contributing
192
272
 
193
- - Non-loopback app connections should use `wss://`
194
- - `discord.js` is used internally by the default runtime, but apps interact with Shardwire through Shardwire's own JSON payloads
195
- - Event availability depends on the intents you enable for the bot bridge
273
+ Issues and pull requests are welcome: [github.com/unloopedmido/shardwire](https://github.com/unloopedmido/shardwire).
196
274
 
197
- ## Examples
275
+ ## License
198
276
 
199
- - Bot: [examples/bot-basic.ts](./examples/bot-basic.ts)
200
- - App: [examples/app-basic.ts](./examples/app-basic.ts)
277
+ MIT - see [`LICENSE`](./LICENSE).
package/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- import { Snowflake, APIEmbed, APIAllowedMentions } from 'discord-api-types/v10';
1
+ import { Snowflake, APIEmbed, APIAllowedMentions, APIActionRowComponent, APIComponentInMessageActionRow, APITextInputComponent } from 'discord-api-types/v10';
2
+ import { GatewayIntentBits } from 'discord.js';
2
3
 
3
4
  type Unsubscribe = () => void;
4
5
  interface ShardwireLogger {
@@ -7,7 +8,7 @@ interface ShardwireLogger {
7
8
  warn?: (message: string, meta?: Record<string, unknown>) => void;
8
9
  error?: (message: string, meta?: Record<string, unknown>) => void;
9
10
  }
10
- type BotIntentName = "Guilds" | "GuildMembers" | "GuildMessages" | "MessageContent";
11
+ type BotIntentName = keyof typeof GatewayIntentBits;
11
12
  interface BridgeUser {
12
13
  id: Snowflake;
13
14
  username: string;
@@ -41,6 +42,23 @@ interface BridgeGuildMember {
41
42
  pending?: boolean;
42
43
  communicationDisabledUntil?: string | null;
43
44
  }
45
+ /** Normalized guild snapshot for `guildCreate` / `guildDelete` events. */
46
+ interface BridgeGuild {
47
+ id: Snowflake;
48
+ name: string;
49
+ icon?: string | null;
50
+ ownerId?: Snowflake;
51
+ }
52
+ /** Normalized thread channel snapshot for thread lifecycle events. */
53
+ interface BridgeThread {
54
+ id: Snowflake;
55
+ guildId: Snowflake;
56
+ parentId: Snowflake | null;
57
+ name: string;
58
+ type: number;
59
+ archived?: boolean;
60
+ locked?: boolean;
61
+ }
44
62
  interface BridgeMessage {
45
63
  id: Snowflake;
46
64
  channelId: Snowflake;
@@ -52,6 +70,8 @@ interface BridgeMessage {
52
70
  editedAt?: string | null;
53
71
  attachments: BridgeAttachment[];
54
72
  embeds: APIEmbed[];
73
+ /** Message component rows (JSON-serializable API shape). */
74
+ components?: APIActionRowComponent<APIComponentInMessageActionRow>[];
55
75
  reference?: BridgeMessageReference;
56
76
  }
57
77
  interface BridgeDeletedMessage {
@@ -60,6 +80,18 @@ interface BridgeDeletedMessage {
60
80
  guildId?: Snowflake;
61
81
  deletedAt: string;
62
82
  }
83
+ interface BridgeReactionEmoji {
84
+ id?: Snowflake;
85
+ name?: string | null;
86
+ animated?: boolean;
87
+ }
88
+ interface BridgeMessageReaction {
89
+ messageId: Snowflake;
90
+ channelId: Snowflake;
91
+ guildId?: Snowflake;
92
+ user?: BridgeUser;
93
+ emoji: BridgeReactionEmoji;
94
+ }
63
95
  type BridgeInteractionKind = "chatInput" | "contextMenu" | "button" | "stringSelect" | "userSelect" | "roleSelect" | "mentionableSelect" | "channelSelect" | "modalSubmit" | "unknown";
64
96
  interface BridgeInteraction {
65
97
  id: Snowflake;
@@ -78,6 +110,7 @@ interface BridgeInteraction {
78
110
  }
79
111
  interface EventEnvelopeBase {
80
112
  receivedAt: number;
113
+ /** Populated when the bot runs under `ShardingManager` (multi-shard). */
81
114
  shardId?: number;
82
115
  }
83
116
  interface ReadyEventPayload extends EventEnvelopeBase {
@@ -102,20 +135,58 @@ interface GuildMemberAddEventPayload extends EventEnvelopeBase {
102
135
  interface GuildMemberRemoveEventPayload extends EventEnvelopeBase {
103
136
  member: BridgeGuildMember;
104
137
  }
138
+ interface GuildMemberUpdateEventPayload extends EventEnvelopeBase {
139
+ oldMember?: BridgeGuildMember;
140
+ member: BridgeGuildMember;
141
+ }
142
+ interface GuildCreateEventPayload extends EventEnvelopeBase {
143
+ guild: BridgeGuild;
144
+ }
145
+ interface GuildDeleteEventPayload extends EventEnvelopeBase {
146
+ guild: BridgeGuild;
147
+ }
148
+ interface ThreadCreateEventPayload extends EventEnvelopeBase {
149
+ thread: BridgeThread;
150
+ }
151
+ interface ThreadUpdateEventPayload extends EventEnvelopeBase {
152
+ oldThread?: BridgeThread;
153
+ thread: BridgeThread;
154
+ }
155
+ interface ThreadDeleteEventPayload extends EventEnvelopeBase {
156
+ thread: BridgeThread;
157
+ }
158
+ interface MessageReactionAddEventPayload extends EventEnvelopeBase {
159
+ reaction: BridgeMessageReaction;
160
+ }
161
+ interface MessageReactionRemoveEventPayload extends EventEnvelopeBase {
162
+ reaction: BridgeMessageReaction;
163
+ }
105
164
  interface BotEventPayloadMap {
106
165
  ready: ReadyEventPayload;
107
166
  interactionCreate: InteractionCreateEventPayload;
108
167
  messageCreate: MessageCreateEventPayload;
109
168
  messageUpdate: MessageUpdateEventPayload;
110
169
  messageDelete: MessageDeleteEventPayload;
170
+ messageReactionAdd: MessageReactionAddEventPayload;
171
+ messageReactionRemove: MessageReactionRemoveEventPayload;
172
+ guildCreate: GuildCreateEventPayload;
173
+ guildDelete: GuildDeleteEventPayload;
111
174
  guildMemberAdd: GuildMemberAddEventPayload;
112
175
  guildMemberRemove: GuildMemberRemoveEventPayload;
176
+ guildMemberUpdate: GuildMemberUpdateEventPayload;
177
+ threadCreate: ThreadCreateEventPayload;
178
+ threadUpdate: ThreadUpdateEventPayload;
179
+ threadDelete: ThreadDeleteEventPayload;
113
180
  }
114
181
  type BotEventName = keyof BotEventPayloadMap;
115
182
  interface BridgeMessageInput {
116
183
  content?: string;
117
184
  embeds?: APIEmbed[];
118
185
  allowedMentions?: APIAllowedMentions;
186
+ components?: APIActionRowComponent<APIComponentInMessageActionRow>[];
187
+ /** Bitfield compatible with `MessageFlags` from discord.js / Discord API. */
188
+ flags?: number;
189
+ stickerIds?: Snowflake[];
119
190
  }
120
191
  interface SendMessageActionPayload extends BridgeMessageInput {
121
192
  channelId: Snowflake;
@@ -140,6 +211,32 @@ interface FollowUpInteractionActionPayload extends BridgeMessageInput {
140
211
  interactionId: Snowflake;
141
212
  ephemeral?: boolean;
142
213
  }
214
+ interface DeferUpdateInteractionActionPayload {
215
+ interactionId: Snowflake;
216
+ }
217
+ interface EditInteractionReplyActionPayload extends BridgeMessageInput {
218
+ interactionId: Snowflake;
219
+ }
220
+ interface DeleteInteractionReplyActionPayload {
221
+ interactionId: Snowflake;
222
+ }
223
+ interface UpdateInteractionActionPayload extends BridgeMessageInput {
224
+ interactionId: Snowflake;
225
+ }
226
+ interface ShowModalActionPayload {
227
+ interactionId: Snowflake;
228
+ title: string;
229
+ customId: string;
230
+ components: APIActionRowComponent<APITextInputComponent>[];
231
+ }
232
+ interface FetchMessageActionPayload {
233
+ channelId: Snowflake;
234
+ messageId: Snowflake;
235
+ }
236
+ interface FetchMemberActionPayload {
237
+ guildId: Snowflake;
238
+ userId: Snowflake;
239
+ }
143
240
  interface BanMemberActionPayload {
144
241
  guildId: Snowflake;
145
242
  userId: Snowflake;
@@ -163,17 +260,36 @@ interface RemoveMemberRoleActionPayload {
163
260
  roleId: Snowflake;
164
261
  reason?: string;
165
262
  }
263
+ interface AddMessageReactionActionPayload {
264
+ channelId: Snowflake;
265
+ messageId: Snowflake;
266
+ emoji: string;
267
+ }
268
+ interface RemoveOwnMessageReactionActionPayload {
269
+ channelId: Snowflake;
270
+ messageId: Snowflake;
271
+ emoji: string;
272
+ }
166
273
  interface BotActionPayloadMap {
167
274
  sendMessage: SendMessageActionPayload;
168
275
  editMessage: EditMessageActionPayload;
169
276
  deleteMessage: DeleteMessageActionPayload;
170
277
  replyToInteraction: ReplyToInteractionActionPayload;
171
278
  deferInteraction: DeferInteractionActionPayload;
279
+ deferUpdateInteraction: DeferUpdateInteractionActionPayload;
172
280
  followUpInteraction: FollowUpInteractionActionPayload;
281
+ editInteractionReply: EditInteractionReplyActionPayload;
282
+ deleteInteractionReply: DeleteInteractionReplyActionPayload;
283
+ updateInteraction: UpdateInteractionActionPayload;
284
+ showModal: ShowModalActionPayload;
285
+ fetchMessage: FetchMessageActionPayload;
286
+ fetchMember: FetchMemberActionPayload;
173
287
  banMember: BanMemberActionPayload;
174
288
  kickMember: KickMemberActionPayload;
175
289
  addMemberRole: AddMemberRoleActionPayload;
176
290
  removeMemberRole: RemoveMemberRoleActionPayload;
291
+ addMessageReaction: AddMessageReactionActionPayload;
292
+ removeOwnMessageReaction: RemoveOwnMessageReactionActionPayload;
177
293
  }
178
294
  interface DeleteMessageActionResult {
179
295
  deleted: true;
@@ -184,21 +300,47 @@ interface DeferInteractionActionResult {
184
300
  deferred: true;
185
301
  interactionId: Snowflake;
186
302
  }
303
+ interface DeferUpdateInteractionActionResult {
304
+ deferred: true;
305
+ interactionId: Snowflake;
306
+ }
307
+ interface DeleteInteractionReplyActionResult {
308
+ deleted: true;
309
+ interactionId: Snowflake;
310
+ }
311
+ interface ShowModalActionResult {
312
+ shown: true;
313
+ interactionId: Snowflake;
314
+ }
187
315
  interface MemberModerationActionResult {
188
316
  guildId: Snowflake;
189
317
  userId: Snowflake;
190
318
  }
319
+ interface MessageReactionActionResult {
320
+ messageId: Snowflake;
321
+ channelId: Snowflake;
322
+ emoji: string;
323
+ }
191
324
  interface BotActionResultDataMap {
192
325
  sendMessage: BridgeMessage;
193
326
  editMessage: BridgeMessage;
194
327
  deleteMessage: DeleteMessageActionResult;
195
328
  replyToInteraction: BridgeMessage;
196
329
  deferInteraction: DeferInteractionActionResult;
330
+ deferUpdateInteraction: DeferUpdateInteractionActionResult;
197
331
  followUpInteraction: BridgeMessage;
332
+ editInteractionReply: BridgeMessage;
333
+ deleteInteractionReply: DeleteInteractionReplyActionResult;
334
+ updateInteraction: BridgeMessage;
335
+ showModal: ShowModalActionResult;
336
+ fetchMessage: BridgeMessage;
337
+ fetchMember: BridgeGuildMember;
198
338
  banMember: MemberModerationActionResult;
199
339
  kickMember: MemberModerationActionResult;
200
340
  addMemberRole: BridgeGuildMember;
201
341
  removeMemberRole: BridgeGuildMember;
342
+ addMessageReaction: MessageReactionActionResult;
343
+ removeOwnMessageReaction: MessageReactionActionResult;
202
344
  }
203
345
  type BotActionName = keyof BotActionPayloadMap;
204
346
  interface BridgeCapabilities {
@@ -210,6 +352,10 @@ interface EventSubscriptionFilter {
210
352
  channelId?: Snowflake | readonly Snowflake[];
211
353
  userId?: Snowflake | readonly Snowflake[];
212
354
  commandName?: string | readonly string[];
355
+ /** Matches `BridgeInteraction.customId` when present (components, modals). */
356
+ customId?: string | readonly string[];
357
+ /** Matches `BridgeInteraction.kind`. */
358
+ interactionKind?: BridgeInteractionKind | readonly BridgeInteractionKind[];
213
359
  }
214
360
  interface EventSubscription<K extends BotEventName = BotEventName> {
215
361
  name: K;
@@ -225,6 +371,14 @@ interface ScopedSecretConfig {
225
371
  allow?: SecretPermissions;
226
372
  }
227
373
  type BotBridgeSecret = string | ScopedSecretConfig;
374
+ /** Structured Discord / transport context for failed actions (machine-readable). */
375
+ interface ActionErrorDetails {
376
+ discordStatus?: number;
377
+ discordCode?: number;
378
+ /** When true, callers may retry with backoff (e.g. rate limits). */
379
+ retryable?: boolean;
380
+ [key: string]: unknown;
381
+ }
228
382
  interface BotBridgeOptions {
229
383
  token: string;
230
384
  intents: readonly BotIntentName[];
@@ -235,9 +389,24 @@ interface BotBridgeOptions {
235
389
  heartbeatMs?: number;
236
390
  maxPayloadBytes?: number;
237
391
  secrets: readonly BotBridgeSecret[];
392
+ /** Reject new TCP connections when authenticated client count reaches this cap (default: unlimited). */
393
+ maxConnections?: number;
394
+ /** Max concurrent action executions per bot process (default: 32). */
395
+ maxConcurrentActions?: number;
396
+ /** When the queue is full, fail fast with `SERVICE_UNAVAILABLE` (default: 5000). */
397
+ actionQueueTimeoutMs?: number;
238
398
  };
239
399
  logger?: ShardwireLogger;
240
400
  }
401
+ interface AppBridgeMetricsHooks {
402
+ onActionComplete?: (meta: {
403
+ name: BotActionName;
404
+ requestId: string;
405
+ durationMs: number;
406
+ ok: boolean;
407
+ errorCode?: string;
408
+ }) => void;
409
+ }
241
410
  interface AppBridgeOptions {
242
411
  url: string;
243
412
  secret: string;
@@ -251,11 +420,12 @@ interface AppBridgeOptions {
251
420
  };
252
421
  requestTimeoutMs?: number;
253
422
  logger?: ShardwireLogger;
423
+ metrics?: AppBridgeMetricsHooks;
254
424
  }
255
425
  interface ActionError {
256
- code: "UNAUTHORIZED" | "TIMEOUT" | "DISCONNECTED" | "FORBIDDEN" | "NOT_FOUND" | "INVALID_REQUEST" | "INTERNAL_ERROR";
426
+ code: "UNAUTHORIZED" | "TIMEOUT" | "DISCONNECTED" | "FORBIDDEN" | "NOT_FOUND" | "INVALID_REQUEST" | "INTERNAL_ERROR" | "SERVICE_UNAVAILABLE";
257
427
  message: string;
258
- details?: unknown;
428
+ details?: ActionErrorDetails | unknown;
259
429
  }
260
430
  interface ActionSuccess<T> {
261
431
  ok: true;
@@ -276,11 +446,14 @@ declare class BridgeCapabilityError extends Error {
276
446
  constructor(kind: "event" | "action", name: string, message?: string);
277
447
  }
278
448
  type EventHandler<K extends BotEventName> = (payload: BotEventPayloadMap[K]) => void;
449
+ type AppBridgeActionInvokeOptions = {
450
+ timeoutMs?: number;
451
+ requestId?: string;
452
+ /** When set, duplicate keys within TTL return the first result (best-effort idempotency). */
453
+ idempotencyKey?: string;
454
+ };
279
455
  type AppBridgeActions = {
280
- [K in BotActionName]: (payload: BotActionPayloadMap[K], options?: {
281
- timeoutMs?: number;
282
- requestId?: string;
283
- }) => Promise<ActionResult<BotActionResultDataMap[K]>>;
456
+ [K in BotActionName]: (payload: BotActionPayloadMap[K], options?: AppBridgeActionInvokeOptions) => Promise<ActionResult<BotActionResultDataMap[K]>>;
284
457
  };
285
458
  interface BotBridge {
286
459
  ready(): Promise<void>;
@@ -305,4 +478,4 @@ declare function createBotBridge(options: BotBridgeOptions): BotBridge;
305
478
 
306
479
  declare function connectBotBridge(options: AppBridgeOptions): AppBridge;
307
480
 
308
- export { type ActionError, type ActionFailure, type ActionResult, type ActionSuccess, type AddMemberRoleActionPayload, type AppBridge, type AppBridgeActions, type AppBridgeOptions, type BanMemberActionPayload, type BotActionName, type BotActionPayloadMap, type BotActionResultDataMap, type BotBridge, type BotBridgeOptions, type BotBridgeSecret, type BotEventName, type BotEventPayloadMap, type BotIntentName, type BridgeAttachment, type BridgeCapabilities, BridgeCapabilityError, type BridgeDeletedMessage, type BridgeGuildMember, type BridgeInteraction, type BridgeInteractionKind, type BridgeMessage, type BridgeMessageInput, type BridgeMessageReference, type BridgeUser, type DeferInteractionActionPayload, type DeleteMessageActionPayload, type EditMessageActionPayload, type EventEnvelopeBase, type EventHandler, type EventSubscription, type EventSubscriptionFilter, type FollowUpInteractionActionPayload, type GuildMemberAddEventPayload, type GuildMemberRemoveEventPayload, type InteractionCreateEventPayload, type KickMemberActionPayload, type MessageCreateEventPayload, type MessageDeleteEventPayload, type MessageUpdateEventPayload, type ReadyEventPayload, type RemoveMemberRoleActionPayload, type ReplyToInteractionActionPayload, type ScopedSecretConfig, type SecretPermissions, type SendMessageActionPayload, type ShardwireLogger, type Unsubscribe, connectBotBridge, createBotBridge };
481
+ export { type ActionError, type ActionErrorDetails, type ActionFailure, type ActionResult, type ActionSuccess, type AddMemberRoleActionPayload, type AddMessageReactionActionPayload, type AppBridge, type AppBridgeActionInvokeOptions, type AppBridgeActions, type AppBridgeMetricsHooks, type AppBridgeOptions, type BanMemberActionPayload, type BotActionName, type BotActionPayloadMap, type BotActionResultDataMap, type BotBridge, type BotBridgeOptions, type BotBridgeSecret, type BotEventName, type BotEventPayloadMap, type BotIntentName, type BridgeAttachment, type BridgeCapabilities, BridgeCapabilityError, type BridgeDeletedMessage, type BridgeGuild, type BridgeGuildMember, type BridgeInteraction, type BridgeInteractionKind, type BridgeMessage, type BridgeMessageInput, type BridgeMessageReaction, type BridgeMessageReference, type BridgeReactionEmoji, type BridgeThread, type BridgeUser, type DeferInteractionActionPayload, type DeferInteractionActionResult, type DeferUpdateInteractionActionPayload, type DeferUpdateInteractionActionResult, type DeleteInteractionReplyActionPayload, type DeleteInteractionReplyActionResult, type DeleteMessageActionPayload, type DeleteMessageActionResult, type EditInteractionReplyActionPayload, type EditMessageActionPayload, type EventEnvelopeBase, type EventHandler, type EventSubscription, type EventSubscriptionFilter, type FetchMemberActionPayload, type FetchMessageActionPayload, type FollowUpInteractionActionPayload, type GuildCreateEventPayload, type GuildDeleteEventPayload, type GuildMemberAddEventPayload, type GuildMemberRemoveEventPayload, type GuildMemberUpdateEventPayload, type InteractionCreateEventPayload, type KickMemberActionPayload, type MemberModerationActionResult, type MessageCreateEventPayload, type MessageDeleteEventPayload, type MessageReactionActionResult, type MessageReactionAddEventPayload, type MessageReactionRemoveEventPayload, type MessageUpdateEventPayload, type ReadyEventPayload, type RemoveMemberRoleActionPayload, type RemoveOwnMessageReactionActionPayload, type ReplyToInteractionActionPayload, type ScopedSecretConfig, type SecretPermissions, type SendMessageActionPayload, type ShardwireLogger, type ShowModalActionPayload, type ShowModalActionResult, type ThreadCreateEventPayload, type ThreadDeleteEventPayload, type ThreadUpdateEventPayload, type Unsubscribe, type UpdateInteractionActionPayload, connectBotBridge, createBotBridge };