shardwire 1.2.0 → 1.4.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
@@ -30,43 +30,49 @@ Shardwire is built for a common architecture: your Discord bot runs in one proce
30
30
  npm install shardwire
31
31
  ```
32
32
 
33
+ ## Documentation
34
+
35
+ - [Deployment (TLS, nginx, limits, shutdown)](./docs/deployment.md)
36
+ - [Troubleshooting (auth, capabilities, action errors)](./docs/troubleshooting.md)
37
+ - [Patterns (moderation, interactions, multi-secret)](./docs/patterns.md)
38
+
33
39
  ## Quick Start
34
40
 
35
41
  ### 1) Start the bot bridge process
36
42
 
37
43
  ```ts
38
- import { createBotBridge } from "shardwire";
44
+ import { createBotBridge } from 'shardwire';
39
45
 
40
46
  const bridge = createBotBridge({
41
- token: process.env.DISCORD_TOKEN!,
42
- intents: ["Guilds", "GuildMessages", "GuildMessageReactions", "MessageContent", "GuildMembers"],
43
- server: {
44
- port: 3001,
45
- secrets: [process.env.SHARDWIRE_SECRET!],
46
- },
47
+ token: process.env.DISCORD_TOKEN!,
48
+ intents: ['Guilds', 'GuildMessages', 'GuildMessageReactions', 'MessageContent', 'GuildMembers'],
49
+ server: {
50
+ port: 3001,
51
+ secrets: [process.env.SHARDWIRE_SECRET!],
52
+ },
47
53
  });
48
54
 
49
55
  await bridge.ready();
50
- console.log("Bot bridge listening on ws://127.0.0.1:3001/shardwire");
56
+ console.log('Bot bridge listening on ws://127.0.0.1:3001/shardwire');
51
57
  ```
52
58
 
53
59
  ### 2) Connect from your app process
54
60
 
55
61
  ```ts
56
- import { connectBotBridge } from "shardwire";
62
+ import { connectBotBridge } from 'shardwire';
57
63
 
58
64
  const app = connectBotBridge({
59
- url: "ws://127.0.0.1:3001/shardwire",
60
- secret: process.env.SHARDWIRE_SECRET!,
61
- appName: "dashboard",
65
+ url: 'ws://127.0.0.1:3001/shardwire',
66
+ secret: process.env.SHARDWIRE_SECRET!,
67
+ appName: 'dashboard',
62
68
  });
63
69
 
64
- app.on("ready", ({ user }) => {
65
- console.log("Bot ready as", user.username);
70
+ app.on('ready', ({ user }) => {
71
+ console.log('Bot ready as', user.username);
66
72
  });
67
73
 
68
- app.on("messageCreate", ({ message }) => {
69
- console.log(message.channelId, message.content);
74
+ app.on('messageCreate', ({ message }) => {
75
+ console.log(message.channelId, message.content);
70
76
  });
71
77
 
72
78
  await app.ready();
@@ -76,12 +82,12 @@ await app.ready();
76
82
 
77
83
  ```ts
78
84
  const result = await app.actions.sendMessage({
79
- channelId: "123456789012345678",
80
- content: "Hello from app side",
85
+ channelId: '123456789012345678',
86
+ content: 'Hello from app side',
81
87
  });
82
88
 
83
89
  if (!result.ok) {
84
- console.error(result.error.code, result.error.message);
90
+ console.error(result.error.code, result.error.message);
85
91
  }
86
92
  ```
87
93
 
@@ -89,14 +95,20 @@ if (!result.ok) {
89
95
 
90
96
  ```ts
91
97
  app.on(
92
- "messageCreate",
93
- ({ message }) => {
94
- console.log("Only this channel:", message.content);
95
- },
96
- { channelId: "123456789012345678" },
98
+ 'messageCreate',
99
+ ({ message }) => {
100
+ console.log('Only this channel:', message.content);
101
+ },
102
+ { channelId: '123456789012345678' },
97
103
  );
98
104
  ```
99
105
 
106
+ ## Startup lifecycle
107
+
108
+ - **`createBotBridge(...)`** starts the WebSocket server immediately; `await bridge.ready()` resolves when the Discord client has finished its initial `ready` handshake (same timing you would expect from a normal bot login).
109
+ - **Register `app.on(...)` handlers before `await app.ready()`** so subscriptions are known when the app authenticates. `ready()` connects, completes auth, negotiates capabilities from intents + secret scope, then throws `BridgeCapabilityError` if any registered handler targets an event the app is not allowed to receive.
110
+ - **Sticky `ready`**: if the bot was already ready before the app connected, the bridge replays the latest `ready` payload to matching subscriptions after auth.
111
+
100
112
  ## Built-In Events
101
113
 
102
114
  Apps subscribe to events with `app.on(...)`. The bridge forwards only what each app subscribes to.
@@ -106,6 +118,7 @@ Apps subscribe to events with `app.on(...)`. The bridge forwards only what each
106
118
  - `messageCreate`
107
119
  - `messageUpdate`
108
120
  - `messageDelete`
121
+ - `messageBulkDelete`
109
122
  - `messageReactionAdd`
110
123
  - `messageReactionRemove`
111
124
  - `guildCreate`
@@ -116,6 +129,9 @@ Apps subscribe to events with `app.on(...)`. The bridge forwards only what each
116
129
  - `threadCreate`
117
130
  - `threadUpdate`
118
131
  - `threadDelete`
132
+ - `channelCreate`
133
+ - `channelUpdate`
134
+ - `channelDelete`
119
135
 
120
136
  Supported filters:
121
137
 
@@ -125,13 +141,16 @@ Supported filters:
125
141
  - `commandName` (for `interactionCreate`)
126
142
  - `customId` (for `interactionCreate`)
127
143
  - `interactionKind` (for `interactionCreate`)
144
+ - `channelType` (Discord `ChannelType` when present on the payload, for example `messageCreate` / `messageBulkDelete`)
145
+ - `parentChannelId` (category parent, forum/text parent for threads, or thread parent when serialized)
146
+ - `threadId` (guild thread channels only: matches thread channel ids)
128
147
 
129
148
  ### Intent Notes
130
149
 
131
150
  - `ready` and `interactionCreate`: no specific event intent requirement
132
- - `messageCreate`, `messageUpdate`, `messageDelete`: `GuildMessages`
151
+ - `messageCreate`, `messageUpdate`, `messageDelete`, `messageBulkDelete`: `GuildMessages`
133
152
  - `messageReactionAdd`, `messageReactionRemove`: `GuildMessageReactions`
134
- - `guildCreate`, `guildDelete`, `threadCreate`, `threadUpdate`, `threadDelete`: `Guilds`
153
+ - `guildCreate`, `guildDelete`, `threadCreate`, `threadUpdate`, `threadDelete`, `channelCreate`, `channelUpdate`, `channelDelete`: `Guilds`
135
154
  - `guildMemberAdd`, `guildMemberRemove`, `guildMemberUpdate`: `GuildMembers`
136
155
 
137
156
  ## Built-In Actions
@@ -157,40 +176,64 @@ Supported filters:
157
176
  - `removeMemberRole`
158
177
  - `addMessageReaction`
159
178
  - `removeOwnMessageReaction`
179
+ - `timeoutMember`
180
+ - `removeMemberTimeout`
181
+ - `createChannel`
182
+ - `editChannel`
183
+ - `deleteChannel`
184
+ - `createThread`
185
+ - `archiveThread`
160
186
 
161
187
  All actions return:
162
188
 
163
189
  ```ts
164
190
  type ActionResult<T> =
165
- | { ok: true; requestId: string; ts: number; data: T }
166
- | { ok: false; requestId: string; ts: number; error: { code: string; message: string; details?: unknown } };
191
+ | { ok: true; requestId: string; ts: number; data: T }
192
+ | { ok: false; requestId: string; ts: number; error: { code: string; message: string; details?: unknown } };
167
193
  ```
168
194
 
169
195
  ### Idempotency for safe retries
170
196
 
171
- You can provide an `idempotencyKey` in action options to dedupe repeated requests on the same connection:
197
+ You can provide an `idempotencyKey` in action options so repeated calls return the first result while the server-side entry is still valid (default TTL **120s**).
198
+
199
+ - **Default scope (`connection`)**: dedupe is per WebSocket connection (a reconnect is a new connection and does not replay prior keys).
200
+ - **Optional scope (`secret`)**: set `server.idempotencyScope: "secret"` on the bot bridge to dedupe by configured secret id across connections (useful when the app reconnects and retries the same logical operation).
172
201
 
173
202
  ```ts
174
203
  await app.actions.sendMessage(
175
- { channelId: "123456789012345678", content: "Hello once" },
176
- { idempotencyKey: "notify:order:123" },
204
+ { channelId: '123456789012345678', content: 'Hello once' },
205
+ { idempotencyKey: 'notify:order:123' },
177
206
  );
178
207
  ```
179
208
 
209
+ Tune limits on the bot bridge when needed:
210
+
211
+ ```ts
212
+ server: {
213
+ port: 3001,
214
+ secrets: [process.env.SHARDWIRE_SECRET!],
215
+ maxPayloadBytes: 65536, // per-frame JSON limit (default 65536)
216
+ idempotencyScope: "secret",
217
+ idempotencyTtlMs: 120_000,
218
+ },
219
+ ```
220
+
180
221
  ### App-side action metrics
181
222
 
182
223
  ```ts
183
224
  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
- },
225
+ url: 'ws://127.0.0.1:3001/shardwire',
226
+ secret: process.env.SHARDWIRE_SECRET!,
227
+ metrics: {
228
+ onActionComplete(meta) {
229
+ console.log(meta.name, meta.durationMs, meta.ok, meta.errorCode, meta.discordStatus, meta.retryAfterMs);
230
+ },
231
+ },
191
232
  });
192
233
  ```
193
234
 
235
+ On Discord **429** responses, failed actions surface `SERVICE_UNAVAILABLE` with `details.retryAfterMs` (when Discord provides `retry_after`) and the metrics hook receives `retryAfterMs` / `discordStatus` for backoff and dashboards.
236
+
194
237
  ## Secret Scopes
195
238
 
196
239
  Use a plain string secret for full event/action access:
@@ -229,7 +272,7 @@ console.log(capabilities.events, capabilities.actions);
229
272
 
230
273
  ## Run the Included Examples
231
274
 
232
- In two terminals:
275
+ ### Minimal (single shared secret)
233
276
 
234
277
  ```bash
235
278
  # terminal 1
@@ -241,15 +284,42 @@ DISCORD_TOKEN=your-token SHARDWIRE_SECRET=dev-secret npm run example:bot
241
284
  SHARDWIRE_SECRET=dev-secret npm run example:app
242
285
  ```
243
286
 
244
- Examples:
287
+ - Bot bridge: [`examples/bot-basic.ts`](./examples/bot-basic.ts)
288
+ - App client: [`examples/app-basic.ts`](./examples/app-basic.ts)
245
289
 
246
- - Bot bridge: `examples/bot-basic.ts`
247
- - App client: `examples/app-basic.ts`
290
+ ### Production-style (scoped secrets + moderation + interactions)
291
+
292
+ Use **two different** secret strings (bridge rejects duplicate values across scoped entries).
293
+
294
+ ```bash
295
+ # terminal 1 — bot with dashboard + moderation scopes
296
+ DISCORD_TOKEN=your-token \
297
+ SHARDWIRE_SECRET_DASHBOARD=dashboard-secret \
298
+ SHARDWIRE_SECRET_MODERATION=moderation-secret \
299
+ npm run example:bot:prod
300
+ ```
301
+
302
+ ```bash
303
+ # terminal 2 — delete messages that contain the demo trigger (optional channel filter)
304
+ SHARDWIRE_SECRET_MODERATION=moderation-secret \
305
+ MOD_ALERT_CHANNEL_ID=123456789012345678 \
306
+ npm run example:app:moderation
307
+ ```
308
+
309
+ ```bash
310
+ # terminal 3 — defer + edit reply for `customId: shardwire.demo.btn` buttons
311
+ SHARDWIRE_SECRET_MODERATION=moderation-secret \
312
+ npm run example:app:interaction
313
+ ```
314
+
315
+ - [`examples/bot-production.ts`](./examples/bot-production.ts)
316
+ - [`examples/app-moderation.ts`](./examples/app-moderation.ts)
317
+ - [`examples/app-interaction.ts`](./examples/app-interaction.ts)
248
318
 
249
319
  ## Public API
250
320
 
251
321
  ```ts
252
- import { createBotBridge, connectBotBridge } from "shardwire";
322
+ import { createBotBridge, connectBotBridge } from 'shardwire';
253
323
  ```
254
324
 
255
325
  Main exports include:
@@ -266,7 +336,10 @@ Main exports include:
266
336
  - Use `wss://` for non-loopback deployments.
267
337
  - `ws://` is only accepted for loopback hosts (`127.0.0.1`, `localhost`, `::1`).
268
338
  - Event availability depends on enabled intents and secret scope.
269
- - For vulnerability reporting and security policy, see [`SECURITY.md`](./SECURITY.md).
339
+
340
+ **Reporting vulnerabilities:** email [cored.developments@gmail.com](mailto:cored.developments@gmail.com) with enough detail to reproduce the issue (versions, config shape, and steps). Do not open a public issue for undisclosed security problems.
341
+
342
+ **Rotating bridge secrets without downtime:** configure **two** entries in `server.secrets` (old + new values), roll the app env to the new secret, then remove the old entry on the next deploy. Scoped secrets should keep **distinct** `id` values so apps can pin `secretId` during rotation.
270
343
 
271
344
  ## Contributing
272
345
 
package/dist/index.d.mts CHANGED
@@ -59,6 +59,14 @@ interface BridgeThread {
59
59
  archived?: boolean;
60
60
  locked?: boolean;
61
61
  }
62
+ /** Normalized non-thread channel snapshot for channel lifecycle events. */
63
+ interface BridgeChannel {
64
+ id: Snowflake;
65
+ type: number;
66
+ name?: string | null;
67
+ guildId?: Snowflake;
68
+ parentId?: Snowflake | null;
69
+ }
62
70
  interface BridgeMessage {
63
71
  id: Snowflake;
64
72
  channelId: Snowflake;
@@ -73,12 +81,18 @@ interface BridgeMessage {
73
81
  /** Message component rows (JSON-serializable API shape). */
74
82
  components?: APIActionRowComponent<APIComponentInMessageActionRow>[];
75
83
  reference?: BridgeMessageReference;
84
+ /** Discord `ChannelType` when the runtime could resolve `message.channel`. */
85
+ channelType?: number;
86
+ /** Parent category (guild text channels) or parent text/forum (threads), when known. */
87
+ parentChannelId?: Snowflake;
76
88
  }
77
89
  interface BridgeDeletedMessage {
78
90
  id: Snowflake;
79
91
  channelId: Snowflake;
80
92
  guildId?: Snowflake;
81
93
  deletedAt: string;
94
+ channelType?: number;
95
+ parentChannelId?: Snowflake;
82
96
  }
83
97
  interface BridgeReactionEmoji {
84
98
  id?: Snowflake;
@@ -92,13 +106,17 @@ interface BridgeMessageReaction {
92
106
  user?: BridgeUser;
93
107
  emoji: BridgeReactionEmoji;
94
108
  }
95
- type BridgeInteractionKind = "chatInput" | "contextMenu" | "button" | "stringSelect" | "userSelect" | "roleSelect" | "mentionableSelect" | "channelSelect" | "modalSubmit" | "unknown";
109
+ type BridgeInteractionKind = 'chatInput' | 'contextMenu' | 'button' | 'stringSelect' | 'userSelect' | 'roleSelect' | 'mentionableSelect' | 'channelSelect' | 'modalSubmit' | 'unknown';
96
110
  interface BridgeInteraction {
97
111
  id: Snowflake;
98
112
  applicationId: Snowflake;
99
113
  kind: BridgeInteractionKind;
100
114
  guildId?: Snowflake;
101
115
  channelId?: Snowflake;
116
+ /** Discord `ChannelType` for `channelId` when the runtime resolved the channel. */
117
+ channelType?: number;
118
+ /** Parent category or parent text/forum for thread channels, when known. */
119
+ parentChannelId?: Snowflake;
102
120
  user: BridgeUser;
103
121
  member?: BridgeGuildMember;
104
122
  commandName?: string;
@@ -161,12 +179,32 @@ interface MessageReactionAddEventPayload extends EventEnvelopeBase {
161
179
  interface MessageReactionRemoveEventPayload extends EventEnvelopeBase {
162
180
  reaction: BridgeMessageReaction;
163
181
  }
182
+ interface ChannelCreateEventPayload extends EventEnvelopeBase {
183
+ channel: BridgeChannel;
184
+ }
185
+ interface ChannelUpdateEventPayload extends EventEnvelopeBase {
186
+ oldChannel?: BridgeChannel;
187
+ channel: BridgeChannel;
188
+ }
189
+ interface ChannelDeleteEventPayload extends EventEnvelopeBase {
190
+ channel: BridgeChannel;
191
+ }
192
+ interface MessageBulkDeleteEventPayload extends EventEnvelopeBase {
193
+ channelId: Snowflake;
194
+ guildId: Snowflake;
195
+ messageIds: Snowflake[];
196
+ /** Discord `ChannelType` for `channelId` when known. */
197
+ channelType?: number;
198
+ /** Parent category or forum/text parent when the channel reports one. */
199
+ parentChannelId?: Snowflake;
200
+ }
164
201
  interface BotEventPayloadMap {
165
202
  ready: ReadyEventPayload;
166
203
  interactionCreate: InteractionCreateEventPayload;
167
204
  messageCreate: MessageCreateEventPayload;
168
205
  messageUpdate: MessageUpdateEventPayload;
169
206
  messageDelete: MessageDeleteEventPayload;
207
+ messageBulkDelete: MessageBulkDeleteEventPayload;
170
208
  messageReactionAdd: MessageReactionAddEventPayload;
171
209
  messageReactionRemove: MessageReactionRemoveEventPayload;
172
210
  guildCreate: GuildCreateEventPayload;
@@ -177,6 +215,9 @@ interface BotEventPayloadMap {
177
215
  threadCreate: ThreadCreateEventPayload;
178
216
  threadUpdate: ThreadUpdateEventPayload;
179
217
  threadDelete: ThreadDeleteEventPayload;
218
+ channelCreate: ChannelCreateEventPayload;
219
+ channelUpdate: ChannelUpdateEventPayload;
220
+ channelDelete: ChannelDeleteEventPayload;
180
221
  }
181
222
  type BotEventName = keyof BotEventPayloadMap;
182
223
  interface BridgeMessageInput {
@@ -270,6 +311,53 @@ interface RemoveOwnMessageReactionActionPayload {
270
311
  messageId: Snowflake;
271
312
  emoji: string;
272
313
  }
314
+ interface TimeoutMemberActionPayload {
315
+ guildId: Snowflake;
316
+ userId: Snowflake;
317
+ /** Duration in milliseconds (Discord allows up to 28 days). */
318
+ durationMs: number;
319
+ reason?: string;
320
+ }
321
+ interface RemoveMemberTimeoutActionPayload {
322
+ guildId: Snowflake;
323
+ userId: Snowflake;
324
+ reason?: string;
325
+ }
326
+ interface CreateChannelActionPayload {
327
+ guildId: Snowflake;
328
+ name: string;
329
+ /** Discord `ChannelType` (default: `0` guild text). */
330
+ type?: number;
331
+ parentId?: Snowflake;
332
+ topic?: string;
333
+ reason?: string;
334
+ }
335
+ interface EditChannelActionPayload {
336
+ channelId: Snowflake;
337
+ name?: string | null;
338
+ parentId?: Snowflake | null;
339
+ topic?: string | null;
340
+ reason?: string;
341
+ }
342
+ interface DeleteChannelActionPayload {
343
+ channelId: Snowflake;
344
+ reason?: string;
345
+ }
346
+ /** `autoArchiveDuration` is in minutes (Discord-supported values). */
347
+ interface CreateThreadActionPayload {
348
+ parentChannelId: Snowflake;
349
+ name: string;
350
+ /** When set, starts a thread on this message (requires a text-based parent). */
351
+ messageId?: Snowflake;
352
+ type?: 'public' | 'private';
353
+ autoArchiveDuration?: 60 | 1440 | 4320 | 10080;
354
+ reason?: string;
355
+ }
356
+ interface ArchiveThreadActionPayload {
357
+ threadId: Snowflake;
358
+ archived?: boolean;
359
+ reason?: string;
360
+ }
273
361
  interface BotActionPayloadMap {
274
362
  sendMessage: SendMessageActionPayload;
275
363
  editMessage: EditMessageActionPayload;
@@ -290,6 +378,13 @@ interface BotActionPayloadMap {
290
378
  removeMemberRole: RemoveMemberRoleActionPayload;
291
379
  addMessageReaction: AddMessageReactionActionPayload;
292
380
  removeOwnMessageReaction: RemoveOwnMessageReactionActionPayload;
381
+ timeoutMember: TimeoutMemberActionPayload;
382
+ removeMemberTimeout: RemoveMemberTimeoutActionPayload;
383
+ createChannel: CreateChannelActionPayload;
384
+ editChannel: EditChannelActionPayload;
385
+ deleteChannel: DeleteChannelActionPayload;
386
+ createThread: CreateThreadActionPayload;
387
+ archiveThread: ArchiveThreadActionPayload;
293
388
  }
294
389
  interface DeleteMessageActionResult {
295
390
  deleted: true;
@@ -321,6 +416,10 @@ interface MessageReactionActionResult {
321
416
  channelId: Snowflake;
322
417
  emoji: string;
323
418
  }
419
+ interface DeleteChannelActionResult {
420
+ deleted: true;
421
+ channelId: Snowflake;
422
+ }
324
423
  interface BotActionResultDataMap {
325
424
  sendMessage: BridgeMessage;
326
425
  editMessage: BridgeMessage;
@@ -341,6 +440,13 @@ interface BotActionResultDataMap {
341
440
  removeMemberRole: BridgeGuildMember;
342
441
  addMessageReaction: MessageReactionActionResult;
343
442
  removeOwnMessageReaction: MessageReactionActionResult;
443
+ timeoutMember: MemberModerationActionResult;
444
+ removeMemberTimeout: BridgeGuildMember;
445
+ createChannel: BridgeChannel;
446
+ editChannel: BridgeChannel;
447
+ deleteChannel: DeleteChannelActionResult;
448
+ createThread: BridgeThread;
449
+ archiveThread: BridgeThread;
344
450
  }
345
451
  type BotActionName = keyof BotActionPayloadMap;
346
452
  interface BridgeCapabilities {
@@ -356,14 +462,20 @@ interface EventSubscriptionFilter {
356
462
  customId?: string | readonly string[];
357
463
  /** Matches `BridgeInteraction.kind`. */
358
464
  interactionKind?: BridgeInteractionKind | readonly BridgeInteractionKind[];
465
+ /** Matches Discord `ChannelType` when present on the payload (messages, interactions, bulk delete). */
466
+ channelType?: number | readonly number[];
467
+ /** Matches `BridgeMessage.parentChannelId` / thread parent / channel parent when present. */
468
+ parentChannelId?: Snowflake | readonly Snowflake[];
469
+ /** Matches guild thread channels only: same as message `channelId` when `channelType` is a guild thread. */
470
+ threadId?: Snowflake | readonly Snowflake[];
359
471
  }
360
472
  interface EventSubscription<K extends BotEventName = BotEventName> {
361
473
  name: K;
362
474
  filter?: EventSubscriptionFilter;
363
475
  }
364
476
  interface SecretPermissions {
365
- events?: "*" | readonly BotEventName[];
366
- actions?: "*" | readonly BotActionName[];
477
+ events?: '*' | readonly BotEventName[];
478
+ actions?: '*' | readonly BotActionName[];
367
479
  }
368
480
  interface ScopedSecretConfig {
369
481
  id?: string;
@@ -377,6 +489,8 @@ interface ActionErrorDetails {
377
489
  discordCode?: number;
378
490
  /** When true, callers may retry with backoff (e.g. rate limits). */
379
491
  retryable?: boolean;
492
+ /** Suggested wait derived from Discord `retry_after` (milliseconds), when available. */
493
+ retryAfterMs?: number;
380
494
  [key: string]: unknown;
381
495
  }
382
496
  interface BotBridgeOptions {
@@ -395,6 +509,14 @@ interface BotBridgeOptions {
395
509
  maxConcurrentActions?: number;
396
510
  /** When the queue is full, fail fast with `SERVICE_UNAVAILABLE` (default: 5000). */
397
511
  actionQueueTimeoutMs?: number;
512
+ /**
513
+ * Where `idempotencyKey` deduplication is scoped (default: `connection`).
514
+ * - `connection`: same WebSocket connection only (reconnect uses a new scope).
515
+ * - `secret`: same configured secret id across connections (useful for retries after reconnect).
516
+ */
517
+ idempotencyScope?: 'connection' | 'secret';
518
+ /** TTL for idempotency cache entries in ms (default: 120000). */
519
+ idempotencyTtlMs?: number;
398
520
  };
399
521
  logger?: ShardwireLogger;
400
522
  }
@@ -405,6 +527,10 @@ interface AppBridgeMetricsHooks {
405
527
  durationMs: number;
406
528
  ok: boolean;
407
529
  errorCode?: string;
530
+ /** Present when Discord returned HTTP 429 or similar retryable signals. */
531
+ retryAfterMs?: number;
532
+ discordStatus?: number;
533
+ discordCode?: number;
408
534
  }) => void;
409
535
  }
410
536
  interface AppBridgeOptions {
@@ -423,7 +549,7 @@ interface AppBridgeOptions {
423
549
  metrics?: AppBridgeMetricsHooks;
424
550
  }
425
551
  interface ActionError {
426
- code: "UNAUTHORIZED" | "TIMEOUT" | "DISCONNECTED" | "FORBIDDEN" | "NOT_FOUND" | "INVALID_REQUEST" | "INTERNAL_ERROR" | "SERVICE_UNAVAILABLE";
552
+ code: 'UNAUTHORIZED' | 'TIMEOUT' | 'DISCONNECTED' | 'FORBIDDEN' | 'NOT_FOUND' | 'INVALID_REQUEST' | 'INTERNAL_ERROR' | 'SERVICE_UNAVAILABLE';
427
553
  message: string;
428
554
  details?: ActionErrorDetails | unknown;
429
555
  }
@@ -441,9 +567,9 @@ interface ActionFailure {
441
567
  }
442
568
  type ActionResult<T> = ActionSuccess<T> | ActionFailure;
443
569
  declare class BridgeCapabilityError extends Error {
444
- readonly kind: "event" | "action";
570
+ readonly kind: 'event' | 'action';
445
571
  readonly name: string;
446
- constructor(kind: "event" | "action", name: string, message?: string);
572
+ constructor(kind: 'event' | 'action', name: string, message?: string);
447
573
  }
448
574
  type EventHandler<K extends BotEventName> = (payload: BotEventPayloadMap[K]) => void;
449
575
  type AppBridgeActionInvokeOptions = {
@@ -478,4 +604,4 @@ declare function createBotBridge(options: BotBridgeOptions): BotBridge;
478
604
 
479
605
  declare function connectBotBridge(options: AppBridgeOptions): AppBridge;
480
606
 
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 };
607
+ 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 ArchiveThreadActionPayload, 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 BridgeChannel, 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 ChannelCreateEventPayload, type ChannelDeleteEventPayload, type ChannelUpdateEventPayload, type CreateChannelActionPayload, type CreateThreadActionPayload, type DeferInteractionActionPayload, type DeferInteractionActionResult, type DeferUpdateInteractionActionPayload, type DeferUpdateInteractionActionResult, type DeleteChannelActionPayload, type DeleteChannelActionResult, type DeleteInteractionReplyActionPayload, type DeleteInteractionReplyActionResult, type DeleteMessageActionPayload, type DeleteMessageActionResult, type EditChannelActionPayload, 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 MessageBulkDeleteEventPayload, type MessageCreateEventPayload, type MessageDeleteEventPayload, type MessageReactionActionResult, type MessageReactionAddEventPayload, type MessageReactionRemoveEventPayload, type MessageUpdateEventPayload, type ReadyEventPayload, type RemoveMemberRoleActionPayload, type RemoveMemberTimeoutActionPayload, type RemoveOwnMessageReactionActionPayload, type ReplyToInteractionActionPayload, type ScopedSecretConfig, type SecretPermissions, type SendMessageActionPayload, type ShardwireLogger, type ShowModalActionPayload, type ShowModalActionResult, type ThreadCreateEventPayload, type ThreadDeleteEventPayload, type ThreadUpdateEventPayload, type TimeoutMemberActionPayload, type Unsubscribe, type UpdateInteractionActionPayload, connectBotBridge, createBotBridge };