shardwire 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -32
- package/dist/index.d.mts +160 -13
- package/dist/index.d.ts +160 -13
- package/dist/index.js +590 -44
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +590 -44
- package/dist/index.mjs.map +1 -1
- package/package.json +82 -73
package/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
|
|
44
|
+
import { createBotBridge } from 'shardwire';
|
|
39
45
|
|
|
40
46
|
const bridge = createBotBridge({
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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(
|
|
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
|
|
62
|
+
import { connectBotBridge } from 'shardwire';
|
|
57
63
|
|
|
58
64
|
const app = connectBotBridge({
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
65
|
-
|
|
70
|
+
app.on('ready', ({ user }) => {
|
|
71
|
+
console.log('Bot ready as', user.username);
|
|
66
72
|
});
|
|
67
73
|
|
|
68
|
-
app.on(
|
|
69
|
-
|
|
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
|
-
|
|
80
|
-
|
|
85
|
+
channelId: '123456789012345678',
|
|
86
|
+
content: 'Hello from app side',
|
|
81
87
|
});
|
|
82
88
|
|
|
83
89
|
if (!result.ok) {
|
|
84
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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.
|
|
@@ -108,8 +120,14 @@ Apps subscribe to events with `app.on(...)`. The bridge forwards only what each
|
|
|
108
120
|
- `messageDelete`
|
|
109
121
|
- `messageReactionAdd`
|
|
110
122
|
- `messageReactionRemove`
|
|
123
|
+
- `guildCreate`
|
|
124
|
+
- `guildDelete`
|
|
111
125
|
- `guildMemberAdd`
|
|
112
126
|
- `guildMemberRemove`
|
|
127
|
+
- `guildMemberUpdate`
|
|
128
|
+
- `threadCreate`
|
|
129
|
+
- `threadUpdate`
|
|
130
|
+
- `threadDelete`
|
|
113
131
|
|
|
114
132
|
Supported filters:
|
|
115
133
|
|
|
@@ -117,13 +135,16 @@ Supported filters:
|
|
|
117
135
|
- `channelId`
|
|
118
136
|
- `userId`
|
|
119
137
|
- `commandName` (for `interactionCreate`)
|
|
138
|
+
- `customId` (for `interactionCreate`)
|
|
139
|
+
- `interactionKind` (for `interactionCreate`)
|
|
120
140
|
|
|
121
141
|
### Intent Notes
|
|
122
142
|
|
|
123
143
|
- `ready` and `interactionCreate`: no specific event intent requirement
|
|
124
144
|
- `messageCreate`, `messageUpdate`, `messageDelete`: `GuildMessages`
|
|
125
145
|
- `messageReactionAdd`, `messageReactionRemove`: `GuildMessageReactions`
|
|
126
|
-
- `
|
|
146
|
+
- `guildCreate`, `guildDelete`, `threadCreate`, `threadUpdate`, `threadDelete`: `Guilds`
|
|
147
|
+
- `guildMemberAdd`, `guildMemberRemove`, `guildMemberUpdate`: `GuildMembers`
|
|
127
148
|
|
|
128
149
|
## Built-In Actions
|
|
129
150
|
|
|
@@ -134,7 +155,14 @@ Supported filters:
|
|
|
134
155
|
- `deleteMessage`
|
|
135
156
|
- `replyToInteraction`
|
|
136
157
|
- `deferInteraction`
|
|
158
|
+
- `deferUpdateInteraction`
|
|
137
159
|
- `followUpInteraction`
|
|
160
|
+
- `editInteractionReply`
|
|
161
|
+
- `deleteInteractionReply`
|
|
162
|
+
- `updateInteraction`
|
|
163
|
+
- `showModal`
|
|
164
|
+
- `fetchMessage`
|
|
165
|
+
- `fetchMember`
|
|
138
166
|
- `banMember`
|
|
139
167
|
- `kickMember`
|
|
140
168
|
- `addMemberRole`
|
|
@@ -146,10 +174,52 @@ All actions return:
|
|
|
146
174
|
|
|
147
175
|
```ts
|
|
148
176
|
type ActionResult<T> =
|
|
149
|
-
|
|
150
|
-
|
|
177
|
+
| { ok: true; requestId: string; ts: number; data: T }
|
|
178
|
+
| { ok: false; requestId: string; ts: number; error: { code: string; message: string; details?: unknown } };
|
|
151
179
|
```
|
|
152
180
|
|
|
181
|
+
### Idempotency for safe retries
|
|
182
|
+
|
|
183
|
+
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**).
|
|
184
|
+
|
|
185
|
+
- **Default scope (`connection`)**: dedupe is per WebSocket connection (a reconnect is a new connection and does not replay prior keys).
|
|
186
|
+
- **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).
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
await app.actions.sendMessage(
|
|
190
|
+
{ channelId: '123456789012345678', content: 'Hello once' },
|
|
191
|
+
{ idempotencyKey: 'notify:order:123' },
|
|
192
|
+
);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Tune limits on the bot bridge when needed:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
server: {
|
|
199
|
+
port: 3001,
|
|
200
|
+
secrets: [process.env.SHARDWIRE_SECRET!],
|
|
201
|
+
maxPayloadBytes: 65536, // per-frame JSON limit (default 65536)
|
|
202
|
+
idempotencyScope: "secret",
|
|
203
|
+
idempotencyTtlMs: 120_000,
|
|
204
|
+
},
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### App-side action metrics
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
const app = connectBotBridge({
|
|
211
|
+
url: 'ws://127.0.0.1:3001/shardwire',
|
|
212
|
+
secret: process.env.SHARDWIRE_SECRET!,
|
|
213
|
+
metrics: {
|
|
214
|
+
onActionComplete(meta) {
|
|
215
|
+
console.log(meta.name, meta.durationMs, meta.ok, meta.errorCode, meta.discordStatus, meta.retryAfterMs);
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
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.
|
|
222
|
+
|
|
153
223
|
## Secret Scopes
|
|
154
224
|
|
|
155
225
|
Use a plain string secret for full event/action access:
|
|
@@ -188,7 +258,7 @@ console.log(capabilities.events, capabilities.actions);
|
|
|
188
258
|
|
|
189
259
|
## Run the Included Examples
|
|
190
260
|
|
|
191
|
-
|
|
261
|
+
### Minimal (single shared secret)
|
|
192
262
|
|
|
193
263
|
```bash
|
|
194
264
|
# terminal 1
|
|
@@ -200,15 +270,42 @@ DISCORD_TOKEN=your-token SHARDWIRE_SECRET=dev-secret npm run example:bot
|
|
|
200
270
|
SHARDWIRE_SECRET=dev-secret npm run example:app
|
|
201
271
|
```
|
|
202
272
|
|
|
203
|
-
|
|
273
|
+
- Bot bridge: [`examples/bot-basic.ts`](./examples/bot-basic.ts)
|
|
274
|
+
- App client: [`examples/app-basic.ts`](./examples/app-basic.ts)
|
|
275
|
+
|
|
276
|
+
### Production-style (scoped secrets + moderation + interactions)
|
|
277
|
+
|
|
278
|
+
Use **two different** secret strings (bridge rejects duplicate values across scoped entries).
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
# terminal 1 — bot with dashboard + moderation scopes
|
|
282
|
+
DISCORD_TOKEN=your-token \
|
|
283
|
+
SHARDWIRE_SECRET_DASHBOARD=dashboard-secret \
|
|
284
|
+
SHARDWIRE_SECRET_MODERATION=moderation-secret \
|
|
285
|
+
npm run example:bot:prod
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
# terminal 2 — delete messages that contain the demo trigger (optional channel filter)
|
|
290
|
+
SHARDWIRE_SECRET_MODERATION=moderation-secret \
|
|
291
|
+
MOD_ALERT_CHANNEL_ID=123456789012345678 \
|
|
292
|
+
npm run example:app:moderation
|
|
293
|
+
```
|
|
204
294
|
|
|
205
|
-
|
|
206
|
-
|
|
295
|
+
```bash
|
|
296
|
+
# terminal 3 — defer + edit reply for `customId: shardwire.demo.btn` buttons
|
|
297
|
+
SHARDWIRE_SECRET_MODERATION=moderation-secret \
|
|
298
|
+
npm run example:app:interaction
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
- [`examples/bot-production.ts`](./examples/bot-production.ts)
|
|
302
|
+
- [`examples/app-moderation.ts`](./examples/app-moderation.ts)
|
|
303
|
+
- [`examples/app-interaction.ts`](./examples/app-interaction.ts)
|
|
207
304
|
|
|
208
305
|
## Public API
|
|
209
306
|
|
|
210
307
|
```ts
|
|
211
|
-
import { createBotBridge, connectBotBridge } from
|
|
308
|
+
import { createBotBridge, connectBotBridge } from 'shardwire';
|
|
212
309
|
```
|
|
213
310
|
|
|
214
311
|
Main exports include:
|
|
@@ -226,6 +323,10 @@ Main exports include:
|
|
|
226
323
|
- `ws://` is only accepted for loopback hosts (`127.0.0.1`, `localhost`, `::1`).
|
|
227
324
|
- Event availability depends on enabled intents and secret scope.
|
|
228
325
|
|
|
326
|
+
**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.
|
|
327
|
+
|
|
328
|
+
**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.
|
|
329
|
+
|
|
229
330
|
## Contributing
|
|
230
331
|
|
|
231
332
|
Issues and pull requests are welcome: [github.com/unloopedmido/shardwire](https://github.com/unloopedmido/shardwire).
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Snowflake, APIEmbed, APIAllowedMentions } from 'discord-api-types/v10';
|
|
1
|
+
import { Snowflake, APIEmbed, APIAllowedMentions, APIActionRowComponent, APIComponentInMessageActionRow, APITextInputComponent } from 'discord-api-types/v10';
|
|
2
2
|
import { GatewayIntentBits } from 'discord.js';
|
|
3
3
|
|
|
4
4
|
type Unsubscribe = () => void;
|
|
@@ -42,6 +42,23 @@ interface BridgeGuildMember {
|
|
|
42
42
|
pending?: boolean;
|
|
43
43
|
communicationDisabledUntil?: string | null;
|
|
44
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
|
+
}
|
|
45
62
|
interface BridgeMessage {
|
|
46
63
|
id: Snowflake;
|
|
47
64
|
channelId: Snowflake;
|
|
@@ -53,6 +70,8 @@ interface BridgeMessage {
|
|
|
53
70
|
editedAt?: string | null;
|
|
54
71
|
attachments: BridgeAttachment[];
|
|
55
72
|
embeds: APIEmbed[];
|
|
73
|
+
/** Message component rows (JSON-serializable API shape). */
|
|
74
|
+
components?: APIActionRowComponent<APIComponentInMessageActionRow>[];
|
|
56
75
|
reference?: BridgeMessageReference;
|
|
57
76
|
}
|
|
58
77
|
interface BridgeDeletedMessage {
|
|
@@ -73,7 +92,7 @@ interface BridgeMessageReaction {
|
|
|
73
92
|
user?: BridgeUser;
|
|
74
93
|
emoji: BridgeReactionEmoji;
|
|
75
94
|
}
|
|
76
|
-
type BridgeInteractionKind =
|
|
95
|
+
type BridgeInteractionKind = 'chatInput' | 'contextMenu' | 'button' | 'stringSelect' | 'userSelect' | 'roleSelect' | 'mentionableSelect' | 'channelSelect' | 'modalSubmit' | 'unknown';
|
|
77
96
|
interface BridgeInteraction {
|
|
78
97
|
id: Snowflake;
|
|
79
98
|
applicationId: Snowflake;
|
|
@@ -91,6 +110,7 @@ interface BridgeInteraction {
|
|
|
91
110
|
}
|
|
92
111
|
interface EventEnvelopeBase {
|
|
93
112
|
receivedAt: number;
|
|
113
|
+
/** Populated when the bot runs under `ShardingManager` (multi-shard). */
|
|
94
114
|
shardId?: number;
|
|
95
115
|
}
|
|
96
116
|
interface ReadyEventPayload extends EventEnvelopeBase {
|
|
@@ -115,6 +135,26 @@ interface GuildMemberAddEventPayload extends EventEnvelopeBase {
|
|
|
115
135
|
interface GuildMemberRemoveEventPayload extends EventEnvelopeBase {
|
|
116
136
|
member: BridgeGuildMember;
|
|
117
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
|
+
}
|
|
118
158
|
interface MessageReactionAddEventPayload extends EventEnvelopeBase {
|
|
119
159
|
reaction: BridgeMessageReaction;
|
|
120
160
|
}
|
|
@@ -129,14 +169,24 @@ interface BotEventPayloadMap {
|
|
|
129
169
|
messageDelete: MessageDeleteEventPayload;
|
|
130
170
|
messageReactionAdd: MessageReactionAddEventPayload;
|
|
131
171
|
messageReactionRemove: MessageReactionRemoveEventPayload;
|
|
172
|
+
guildCreate: GuildCreateEventPayload;
|
|
173
|
+
guildDelete: GuildDeleteEventPayload;
|
|
132
174
|
guildMemberAdd: GuildMemberAddEventPayload;
|
|
133
175
|
guildMemberRemove: GuildMemberRemoveEventPayload;
|
|
176
|
+
guildMemberUpdate: GuildMemberUpdateEventPayload;
|
|
177
|
+
threadCreate: ThreadCreateEventPayload;
|
|
178
|
+
threadUpdate: ThreadUpdateEventPayload;
|
|
179
|
+
threadDelete: ThreadDeleteEventPayload;
|
|
134
180
|
}
|
|
135
181
|
type BotEventName = keyof BotEventPayloadMap;
|
|
136
182
|
interface BridgeMessageInput {
|
|
137
183
|
content?: string;
|
|
138
184
|
embeds?: APIEmbed[];
|
|
139
185
|
allowedMentions?: APIAllowedMentions;
|
|
186
|
+
components?: APIActionRowComponent<APIComponentInMessageActionRow>[];
|
|
187
|
+
/** Bitfield compatible with `MessageFlags` from discord.js / Discord API. */
|
|
188
|
+
flags?: number;
|
|
189
|
+
stickerIds?: Snowflake[];
|
|
140
190
|
}
|
|
141
191
|
interface SendMessageActionPayload extends BridgeMessageInput {
|
|
142
192
|
channelId: Snowflake;
|
|
@@ -161,6 +211,32 @@ interface FollowUpInteractionActionPayload extends BridgeMessageInput {
|
|
|
161
211
|
interactionId: Snowflake;
|
|
162
212
|
ephemeral?: boolean;
|
|
163
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
|
+
}
|
|
164
240
|
interface BanMemberActionPayload {
|
|
165
241
|
guildId: Snowflake;
|
|
166
242
|
userId: Snowflake;
|
|
@@ -200,7 +276,14 @@ interface BotActionPayloadMap {
|
|
|
200
276
|
deleteMessage: DeleteMessageActionPayload;
|
|
201
277
|
replyToInteraction: ReplyToInteractionActionPayload;
|
|
202
278
|
deferInteraction: DeferInteractionActionPayload;
|
|
279
|
+
deferUpdateInteraction: DeferUpdateInteractionActionPayload;
|
|
203
280
|
followUpInteraction: FollowUpInteractionActionPayload;
|
|
281
|
+
editInteractionReply: EditInteractionReplyActionPayload;
|
|
282
|
+
deleteInteractionReply: DeleteInteractionReplyActionPayload;
|
|
283
|
+
updateInteraction: UpdateInteractionActionPayload;
|
|
284
|
+
showModal: ShowModalActionPayload;
|
|
285
|
+
fetchMessage: FetchMessageActionPayload;
|
|
286
|
+
fetchMember: FetchMemberActionPayload;
|
|
204
287
|
banMember: BanMemberActionPayload;
|
|
205
288
|
kickMember: KickMemberActionPayload;
|
|
206
289
|
addMemberRole: AddMemberRoleActionPayload;
|
|
@@ -217,6 +300,18 @@ interface DeferInteractionActionResult {
|
|
|
217
300
|
deferred: true;
|
|
218
301
|
interactionId: Snowflake;
|
|
219
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
|
+
}
|
|
220
315
|
interface MemberModerationActionResult {
|
|
221
316
|
guildId: Snowflake;
|
|
222
317
|
userId: Snowflake;
|
|
@@ -232,7 +327,14 @@ interface BotActionResultDataMap {
|
|
|
232
327
|
deleteMessage: DeleteMessageActionResult;
|
|
233
328
|
replyToInteraction: BridgeMessage;
|
|
234
329
|
deferInteraction: DeferInteractionActionResult;
|
|
330
|
+
deferUpdateInteraction: DeferUpdateInteractionActionResult;
|
|
235
331
|
followUpInteraction: BridgeMessage;
|
|
332
|
+
editInteractionReply: BridgeMessage;
|
|
333
|
+
deleteInteractionReply: DeleteInteractionReplyActionResult;
|
|
334
|
+
updateInteraction: BridgeMessage;
|
|
335
|
+
showModal: ShowModalActionResult;
|
|
336
|
+
fetchMessage: BridgeMessage;
|
|
337
|
+
fetchMember: BridgeGuildMember;
|
|
236
338
|
banMember: MemberModerationActionResult;
|
|
237
339
|
kickMember: MemberModerationActionResult;
|
|
238
340
|
addMemberRole: BridgeGuildMember;
|
|
@@ -250,14 +352,18 @@ interface EventSubscriptionFilter {
|
|
|
250
352
|
channelId?: Snowflake | readonly Snowflake[];
|
|
251
353
|
userId?: Snowflake | readonly Snowflake[];
|
|
252
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[];
|
|
253
359
|
}
|
|
254
360
|
interface EventSubscription<K extends BotEventName = BotEventName> {
|
|
255
361
|
name: K;
|
|
256
362
|
filter?: EventSubscriptionFilter;
|
|
257
363
|
}
|
|
258
364
|
interface SecretPermissions {
|
|
259
|
-
events?:
|
|
260
|
-
actions?:
|
|
365
|
+
events?: '*' | readonly BotEventName[];
|
|
366
|
+
actions?: '*' | readonly BotActionName[];
|
|
261
367
|
}
|
|
262
368
|
interface ScopedSecretConfig {
|
|
263
369
|
id?: string;
|
|
@@ -265,6 +371,16 @@ interface ScopedSecretConfig {
|
|
|
265
371
|
allow?: SecretPermissions;
|
|
266
372
|
}
|
|
267
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
|
+
/** Suggested wait derived from Discord `retry_after` (milliseconds), when available. */
|
|
381
|
+
retryAfterMs?: number;
|
|
382
|
+
[key: string]: unknown;
|
|
383
|
+
}
|
|
268
384
|
interface BotBridgeOptions {
|
|
269
385
|
token: string;
|
|
270
386
|
intents: readonly BotIntentName[];
|
|
@@ -275,9 +391,36 @@ interface BotBridgeOptions {
|
|
|
275
391
|
heartbeatMs?: number;
|
|
276
392
|
maxPayloadBytes?: number;
|
|
277
393
|
secrets: readonly BotBridgeSecret[];
|
|
394
|
+
/** Reject new TCP connections when authenticated client count reaches this cap (default: unlimited). */
|
|
395
|
+
maxConnections?: number;
|
|
396
|
+
/** Max concurrent action executions per bot process (default: 32). */
|
|
397
|
+
maxConcurrentActions?: number;
|
|
398
|
+
/** When the queue is full, fail fast with `SERVICE_UNAVAILABLE` (default: 5000). */
|
|
399
|
+
actionQueueTimeoutMs?: number;
|
|
400
|
+
/**
|
|
401
|
+
* Where `idempotencyKey` deduplication is scoped (default: `connection`).
|
|
402
|
+
* - `connection`: same WebSocket connection only (reconnect uses a new scope).
|
|
403
|
+
* - `secret`: same configured secret id across connections (useful for retries after reconnect).
|
|
404
|
+
*/
|
|
405
|
+
idempotencyScope?: 'connection' | 'secret';
|
|
406
|
+
/** TTL for idempotency cache entries in ms (default: 120000). */
|
|
407
|
+
idempotencyTtlMs?: number;
|
|
278
408
|
};
|
|
279
409
|
logger?: ShardwireLogger;
|
|
280
410
|
}
|
|
411
|
+
interface AppBridgeMetricsHooks {
|
|
412
|
+
onActionComplete?: (meta: {
|
|
413
|
+
name: BotActionName;
|
|
414
|
+
requestId: string;
|
|
415
|
+
durationMs: number;
|
|
416
|
+
ok: boolean;
|
|
417
|
+
errorCode?: string;
|
|
418
|
+
/** Present when Discord returned HTTP 429 or similar retryable signals. */
|
|
419
|
+
retryAfterMs?: number;
|
|
420
|
+
discordStatus?: number;
|
|
421
|
+
discordCode?: number;
|
|
422
|
+
}) => void;
|
|
423
|
+
}
|
|
281
424
|
interface AppBridgeOptions {
|
|
282
425
|
url: string;
|
|
283
426
|
secret: string;
|
|
@@ -291,11 +434,12 @@ interface AppBridgeOptions {
|
|
|
291
434
|
};
|
|
292
435
|
requestTimeoutMs?: number;
|
|
293
436
|
logger?: ShardwireLogger;
|
|
437
|
+
metrics?: AppBridgeMetricsHooks;
|
|
294
438
|
}
|
|
295
439
|
interface ActionError {
|
|
296
|
-
code:
|
|
440
|
+
code: 'UNAUTHORIZED' | 'TIMEOUT' | 'DISCONNECTED' | 'FORBIDDEN' | 'NOT_FOUND' | 'INVALID_REQUEST' | 'INTERNAL_ERROR' | 'SERVICE_UNAVAILABLE';
|
|
297
441
|
message: string;
|
|
298
|
-
details?: unknown;
|
|
442
|
+
details?: ActionErrorDetails | unknown;
|
|
299
443
|
}
|
|
300
444
|
interface ActionSuccess<T> {
|
|
301
445
|
ok: true;
|
|
@@ -311,16 +455,19 @@ interface ActionFailure {
|
|
|
311
455
|
}
|
|
312
456
|
type ActionResult<T> = ActionSuccess<T> | ActionFailure;
|
|
313
457
|
declare class BridgeCapabilityError extends Error {
|
|
314
|
-
readonly kind:
|
|
458
|
+
readonly kind: 'event' | 'action';
|
|
315
459
|
readonly name: string;
|
|
316
|
-
constructor(kind:
|
|
460
|
+
constructor(kind: 'event' | 'action', name: string, message?: string);
|
|
317
461
|
}
|
|
318
462
|
type EventHandler<K extends BotEventName> = (payload: BotEventPayloadMap[K]) => void;
|
|
463
|
+
type AppBridgeActionInvokeOptions = {
|
|
464
|
+
timeoutMs?: number;
|
|
465
|
+
requestId?: string;
|
|
466
|
+
/** When set, duplicate keys within TTL return the first result (best-effort idempotency). */
|
|
467
|
+
idempotencyKey?: string;
|
|
468
|
+
};
|
|
319
469
|
type AppBridgeActions = {
|
|
320
|
-
[K in BotActionName]: (payload: BotActionPayloadMap[K], options?:
|
|
321
|
-
timeoutMs?: number;
|
|
322
|
-
requestId?: string;
|
|
323
|
-
}) => Promise<ActionResult<BotActionResultDataMap[K]>>;
|
|
470
|
+
[K in BotActionName]: (payload: BotActionPayloadMap[K], options?: AppBridgeActionInvokeOptions) => Promise<ActionResult<BotActionResultDataMap[K]>>;
|
|
324
471
|
};
|
|
325
472
|
interface BotBridge {
|
|
326
473
|
ready(): Promise<void>;
|
|
@@ -345,4 +492,4 @@ declare function createBotBridge(options: BotBridgeOptions): BotBridge;
|
|
|
345
492
|
|
|
346
493
|
declare function connectBotBridge(options: AppBridgeOptions): AppBridge;
|
|
347
494
|
|
|
348
|
-
export { type ActionError, type ActionFailure, type ActionResult, type ActionSuccess, type AddMemberRoleActionPayload, type AddMessageReactionActionPayload, 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 BridgeMessageReaction, type BridgeMessageReference, type BridgeReactionEmoji, 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 MessageReactionActionResult, type MessageReactionAddEventPayload, type MessageReactionRemoveEventPayload, type MessageUpdateEventPayload, type ReadyEventPayload, type RemoveMemberRoleActionPayload, type RemoveOwnMessageReactionActionPayload, type ReplyToInteractionActionPayload, type ScopedSecretConfig, type SecretPermissions, type SendMessageActionPayload, type ShardwireLogger, type Unsubscribe, connectBotBridge, createBotBridge };
|
|
495
|
+
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 };
|