shardwire 0.2.0 → 1.0.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 +127 -266
- package/dist/index.d.mts +262 -129
- package/dist/index.d.ts +262 -129
- package/dist/index.js +1615 -942
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1616 -930
- package/dist/index.mjs.map +1 -1
- package/package.json +22 -34
package/README.md
CHANGED
|
@@ -1,339 +1,200 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Shardwire
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Discord events and bot actions, streamed to your app over a single WebSocket bridge.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
[](https://www.npmjs.com/package/shardwire)
|
|
7
|
-
[](https://nodejs.org/)
|
|
8
|
-
[](https://www.typescriptlang.org/)
|
|
9
|
-
[](./LICENSE)
|
|
5
|
+
Shardwire runs your bot connection, listens to Discord, and exposes a clean app-facing API for:
|
|
10
6
|
|
|
11
|
-
|
|
7
|
+
- subscribing to Discord events
|
|
8
|
+
- replying to interactions
|
|
9
|
+
- sending and editing messages
|
|
10
|
+
- moderation actions like ban, kick, and role changes
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
It is designed for one common setup:
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
> your Discord bot lives in one process, while your web app, backend, or worker lives somewhere else
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
- [Features](#features)
|
|
19
|
-
- [Quick Start](#quick-start)
|
|
20
|
-
- [Host Setup](#host-setup)
|
|
21
|
-
- [Consumer Setup](#consumer-setup)
|
|
22
|
-
- [Token-Only Host (No Existing discord.js Client)](#token-only-host-no-existing-discordjs-client)
|
|
23
|
-
- [Reconnect and Timeout Hardening](#reconnect-and-timeout-hardening)
|
|
24
|
-
- [API Overview](#api-overview)
|
|
25
|
-
- [Configuration](#configuration)
|
|
26
|
-
- [Error Model](#error-model)
|
|
27
|
-
- [Recipes and Troubleshooting](#recipes-and-troubleshooting)
|
|
28
|
-
- [Compatibility](#compatibility)
|
|
29
|
-
- [Security Notes](#security-notes)
|
|
30
|
-
- [Roadmap Constraints (v1)](#roadmap-constraints-v1)
|
|
16
|
+
## Why Shardwire
|
|
31
17
|
|
|
32
|
-
|
|
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
|
|
33
23
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- call bot-hosted commands from apps and services,
|
|
37
|
-
- stream real-time events back to consumers,
|
|
38
|
-
- keep payloads typed end-to-end with TypeScript,
|
|
39
|
-
- start quickly without deploying extra infrastructure.
|
|
40
|
-
|
|
41
|
-
## Features
|
|
42
|
-
|
|
43
|
-
- **Typed command RPC** from consumers to host (`send` -> `CommandResult`).
|
|
44
|
-
- **Real-time pub/sub events** from host to all authenticated consumers.
|
|
45
|
-
- **Single factory API** (`createShardwire`) with host and consumer overloads.
|
|
46
|
-
- **Built-in reliability controls** with reconnect backoff, jitter, and timeouts.
|
|
47
|
-
- **Runtime input validation** for config, names, and JSON-serializable payloads.
|
|
48
|
-
- **Optional schema validation** for command and event payloads.
|
|
49
|
-
- **Optional token-only Discord mode** where shardwire can own client lifecycle.
|
|
50
|
-
- **Dual package output** for ESM and CJS consumers.
|
|
51
|
-
|
|
52
|
-
## Quick Start
|
|
53
|
-
|
|
54
|
-
Install:
|
|
24
|
+
## Install
|
|
55
25
|
|
|
56
26
|
```bash
|
|
57
|
-
|
|
27
|
+
npm install shardwire
|
|
58
28
|
```
|
|
59
29
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
type Commands = {
|
|
64
|
-
"ban-user": {
|
|
65
|
-
request: { userId: string };
|
|
66
|
-
response: { banned: true; userId: string };
|
|
67
|
-
};
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
type Events = {
|
|
71
|
-
"member-joined": { userId: string; guildId: string };
|
|
72
|
-
};
|
|
73
|
-
```
|
|
30
|
+
## Quick Start
|
|
74
31
|
|
|
75
|
-
|
|
32
|
+
### 1. Start the bot bridge
|
|
76
33
|
|
|
77
34
|
```ts
|
|
78
|
-
import {
|
|
79
|
-
|
|
80
|
-
type Commands = {
|
|
81
|
-
"ban-user": {
|
|
82
|
-
request: { userId: string };
|
|
83
|
-
response: { banned: true; userId: string };
|
|
84
|
-
};
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
type Events = {
|
|
88
|
-
"member-joined": { userId: string; guildId: string };
|
|
89
|
-
};
|
|
35
|
+
import { createBotBridge } from "shardwire";
|
|
90
36
|
|
|
91
|
-
const
|
|
92
|
-
|
|
37
|
+
const bridge = createBotBridge({
|
|
38
|
+
token: process.env.DISCORD_TOKEN!,
|
|
39
|
+
intents: ["Guilds", "GuildMessages", "MessageContent", "GuildMembers"],
|
|
93
40
|
server: {
|
|
94
41
|
port: 3001,
|
|
95
42
|
secrets: [process.env.SHARDWIRE_SECRET!],
|
|
96
|
-
primarySecretId: "s0",
|
|
97
43
|
},
|
|
98
|
-
name: "bot-host",
|
|
99
44
|
});
|
|
100
45
|
|
|
101
|
-
|
|
102
|
-
await guild.members.ban(userId);
|
|
103
|
-
return { banned: true, userId };
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
wire.emitEvent("member-joined", { userId: "123", guildId: "456" });
|
|
46
|
+
await bridge.ready();
|
|
107
47
|
```
|
|
108
48
|
|
|
109
|
-
|
|
49
|
+
### 2. Connect from your app
|
|
110
50
|
|
|
111
51
|
```ts
|
|
112
|
-
import {
|
|
113
|
-
|
|
114
|
-
type Commands = {
|
|
115
|
-
"ban-user": {
|
|
116
|
-
request: { userId: string };
|
|
117
|
-
response: { banned: true; userId: string };
|
|
118
|
-
};
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
type Events = {
|
|
122
|
-
"member-joined": { userId: string; guildId: string };
|
|
123
|
-
};
|
|
52
|
+
import { connectBotBridge } from "shardwire";
|
|
124
53
|
|
|
125
|
-
const
|
|
126
|
-
url: "ws://
|
|
54
|
+
const app = connectBotBridge({
|
|
55
|
+
url: "ws://127.0.0.1:3001/shardwire",
|
|
127
56
|
secret: process.env.SHARDWIRE_SECRET!,
|
|
128
|
-
|
|
129
|
-
clientName: "dashboard-api",
|
|
57
|
+
appName: "dashboard",
|
|
130
58
|
});
|
|
131
59
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (result.ok) {
|
|
135
|
-
console.log("Command succeeded:", result.data);
|
|
136
|
-
} else {
|
|
137
|
-
console.error("Command failed:", result.error.code, result.error.message);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
wire.on("member-joined", (payload, meta) => {
|
|
141
|
-
console.log("event", payload, meta.ts);
|
|
60
|
+
app.on("ready", ({ user }) => {
|
|
61
|
+
console.log("Bot ready as", user.username);
|
|
142
62
|
});
|
|
143
63
|
|
|
144
|
-
|
|
145
|
-
console.
|
|
64
|
+
app.on("messageCreate", ({ message }) => {
|
|
65
|
+
console.log(message.channelId, message.content);
|
|
146
66
|
});
|
|
147
67
|
|
|
148
|
-
await
|
|
68
|
+
await app.ready();
|
|
149
69
|
```
|
|
150
70
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
If you do not already manage a `discord.js` client, provide a bot token and shardwire can initialize and own the client lifecycle.
|
|
71
|
+
### 2.5 Filter subscriptions when you need less noise
|
|
154
72
|
|
|
155
73
|
```ts
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
secrets: [process.env.SHARDWIRE_SECRET!],
|
|
161
|
-
primarySecretId: "s0",
|
|
74
|
+
app.on(
|
|
75
|
+
"messageCreate",
|
|
76
|
+
({ message }) => {
|
|
77
|
+
console.log("Only channel 123:", message.content);
|
|
162
78
|
},
|
|
163
|
-
}
|
|
79
|
+
{ channelId: "123456789012345678" },
|
|
80
|
+
);
|
|
164
81
|
```
|
|
165
82
|
|
|
166
|
-
|
|
167
|
-
> Keep `DISCORD_BOT_TOKEN` and `SHARDWIRE_SECRET` in environment variables. Never commit them.
|
|
168
|
-
|
|
169
|
-
## Reconnect and Timeout Hardening
|
|
170
|
-
|
|
171
|
-
For unstable networks, tune reconnect behavior and request timeout explicitly:
|
|
83
|
+
### 3. Call built-in bot actions
|
|
172
84
|
|
|
173
85
|
```ts
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
secretId: "s0",
|
|
178
|
-
requestTimeoutMs: 10_000,
|
|
179
|
-
reconnect: {
|
|
180
|
-
enabled: true,
|
|
181
|
-
initialDelayMs: 500,
|
|
182
|
-
maxDelayMs: 10_000,
|
|
183
|
-
jitter: true,
|
|
184
|
-
},
|
|
86
|
+
const result = await app.actions.sendMessage({
|
|
87
|
+
channelId: "123456789012345678",
|
|
88
|
+
content: "Hello from the app side",
|
|
185
89
|
});
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
## Schema Validation (Zod)
|
|
189
|
-
|
|
190
|
-
Use runtime schemas to validate command request/response payloads and emitted event payloads.
|
|
191
90
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
type Commands = {
|
|
197
|
-
"ban-user": {
|
|
198
|
-
request: { userId: string };
|
|
199
|
-
response: { banned: true; userId: string };
|
|
200
|
-
};
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
const wire = createShardwire<Commands, {}>({
|
|
204
|
-
server: {
|
|
205
|
-
port: 3001,
|
|
206
|
-
secrets: [process.env.SHARDWIRE_SECRET!],
|
|
207
|
-
primarySecretId: "s0",
|
|
208
|
-
},
|
|
209
|
-
validation: {
|
|
210
|
-
commands: {
|
|
211
|
-
"ban-user": {
|
|
212
|
-
request: fromZodSchema(z.object({ userId: z.string().min(3) })),
|
|
213
|
-
response: fromZodSchema(z.object({ banned: z.literal(true), userId: z.string() })),
|
|
214
|
-
},
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
});
|
|
91
|
+
if (!result.ok) {
|
|
92
|
+
console.error(result.error.code, result.error.message);
|
|
93
|
+
}
|
|
218
94
|
```
|
|
219
95
|
|
|
220
|
-
##
|
|
221
|
-
|
|
222
|
-
### Host API
|
|
223
|
-
|
|
224
|
-
- `wire.onCommand(name, handler)` register a command handler.
|
|
225
|
-
- `wire.emitEvent(name, payload)` emit an event to all connected consumers.
|
|
226
|
-
- `wire.broadcast(name, payload)` alias of `emitEvent`.
|
|
227
|
-
- `wire.close()` close the server and active sockets.
|
|
228
|
-
|
|
229
|
-
### Consumer API
|
|
230
|
-
|
|
231
|
-
- `wire.send(name, payload, options?)` send command and await typed result.
|
|
232
|
-
- `wire.on(name, handler)` subscribe to events.
|
|
233
|
-
- `wire.off(name, handler)` unsubscribe a specific handler.
|
|
234
|
-
- `wire.ready()` wait for authenticated connection.
|
|
235
|
-
- `wire.onConnected(handler)` subscribe to authenticated connection events.
|
|
236
|
-
- `wire.onDisconnected(handler)` subscribe to disconnect events.
|
|
237
|
-
- `wire.onReconnecting(handler)` subscribe to reconnect scheduling events.
|
|
238
|
-
- `wire.connected()` check authenticated connection state.
|
|
239
|
-
- `wire.connectionId()` get current authenticated connection id (or `null`).
|
|
240
|
-
- `wire.close()` close socket and stop reconnect attempts.
|
|
96
|
+
## Built-In Events
|
|
241
97
|
|
|
242
|
-
|
|
98
|
+
Shardwire currently exposes these bot-side events:
|
|
243
99
|
|
|
244
|
-
|
|
100
|
+
- `ready`
|
|
101
|
+
- `interactionCreate`
|
|
102
|
+
- `messageCreate`
|
|
103
|
+
- `messageUpdate`
|
|
104
|
+
- `messageDelete`
|
|
105
|
+
- `guildMemberAdd`
|
|
106
|
+
- `guildMemberRemove`
|
|
245
107
|
|
|
246
|
-
- `
|
|
247
|
-
- `server.secrets` required shared secret list for authentication.
|
|
248
|
-
- `server.primarySecretId` optional preferred secret id (for example `"s0"`).
|
|
249
|
-
- `server.path` optional WebSocket path (default `/shardwire`).
|
|
250
|
-
- `server.host` optional bind host.
|
|
251
|
-
- `server.heartbeatMs` heartbeat interval.
|
|
252
|
-
- `server.commandTimeoutMs` command execution timeout.
|
|
253
|
-
- `server.maxPayloadBytes` WebSocket payload size limit.
|
|
254
|
-
- `server.corsOrigins` CORS allowlist for browser clients.
|
|
255
|
-
- `client` existing `discord.js` client (optional).
|
|
256
|
-
- `token` Discord bot token (optional, enables token-only mode).
|
|
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.
|
|
257
109
|
|
|
258
|
-
|
|
110
|
+
Optional filters can narrow delivery by:
|
|
259
111
|
|
|
260
|
-
- `
|
|
261
|
-
- `
|
|
262
|
-
- `
|
|
263
|
-
- `
|
|
264
|
-
- `allowInsecureWs` optional escape hatch to allow `ws://` for non-loopback hosts (defaults to `false`).
|
|
265
|
-
- `requestTimeoutMs` default timeout for `send`.
|
|
266
|
-
- `reconnect` reconnect policy (`enabled`, delays, jitter).
|
|
267
|
-
- `webSocketFactory` optional custom client implementation.
|
|
112
|
+
- `guildId`
|
|
113
|
+
- `channelId`
|
|
114
|
+
- `userId`
|
|
115
|
+
- `commandName` for `interactionCreate`
|
|
268
116
|
|
|
269
|
-
|
|
270
|
-
> Invalid configuration, empty command/event names, or non-serializable payloads throw synchronously with clear errors.
|
|
117
|
+
## Built-In Actions
|
|
271
118
|
|
|
272
|
-
|
|
119
|
+
`app.actions.*` currently includes:
|
|
273
120
|
|
|
274
|
-
`
|
|
121
|
+
- `sendMessage`
|
|
122
|
+
- `editMessage`
|
|
123
|
+
- `deleteMessage`
|
|
124
|
+
- `replyToInteraction`
|
|
125
|
+
- `deferInteraction`
|
|
126
|
+
- `followUpInteraction`
|
|
127
|
+
- `banMember`
|
|
128
|
+
- `kickMember`
|
|
129
|
+
- `addMemberRole`
|
|
130
|
+
- `removeMemberRole`
|
|
275
131
|
|
|
276
|
-
|
|
277
|
-
- failure: `{ ok: false, requestId, ts, error }`
|
|
132
|
+
Every action returns an `ActionResult<T>`:
|
|
278
133
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
134
|
+
```ts
|
|
135
|
+
type ActionResult<T> =
|
|
136
|
+
| { ok: true; requestId: string; ts: number; data: T }
|
|
137
|
+
| { ok: false; requestId: string; ts: number; error: { code: string; message: string; details?: unknown } };
|
|
138
|
+
```
|
|
284
139
|
|
|
285
|
-
|
|
140
|
+
## Secret Scopes
|
|
286
141
|
|
|
287
|
-
|
|
288
|
-
- `TIMEOUT`
|
|
289
|
-
- `DISCONNECTED`
|
|
290
|
-
- `COMMAND_NOT_FOUND`
|
|
291
|
-
- `VALIDATION_ERROR`
|
|
292
|
-
- `INTERNAL_ERROR`
|
|
142
|
+
Use a plain string secret for full access:
|
|
293
143
|
|
|
294
|
-
|
|
144
|
+
```ts
|
|
145
|
+
server: {
|
|
146
|
+
port: 3001,
|
|
147
|
+
secrets: ["full-access-secret"],
|
|
148
|
+
}
|
|
149
|
+
```
|
|
295
150
|
|
|
296
|
-
|
|
151
|
+
Or scope a secret to specific events and actions:
|
|
297
152
|
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
153
|
+
```ts
|
|
154
|
+
server: {
|
|
155
|
+
port: 3001,
|
|
156
|
+
secrets: [
|
|
157
|
+
{
|
|
158
|
+
id: "dashboard",
|
|
159
|
+
value: process.env.SHARDWIRE_SECRET!,
|
|
160
|
+
allow: {
|
|
161
|
+
events: ["ready", "messageCreate"],
|
|
162
|
+
actions: ["sendMessage", "replyToInteraction"],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
}
|
|
303
167
|
```
|
|
304
168
|
|
|
305
|
-
|
|
169
|
+
On the app side, you can inspect what the connection is allowed to do:
|
|
306
170
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
- [Troubleshooting flow](./.agents/skills/shardwire/examples/troubleshooting-flow.md)
|
|
171
|
+
```ts
|
|
172
|
+
const capabilities = app.capabilities();
|
|
173
|
+
console.log(capabilities.events, capabilities.actions);
|
|
174
|
+
```
|
|
312
175
|
|
|
313
|
-
|
|
176
|
+
## Public API
|
|
314
177
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
178
|
+
```ts
|
|
179
|
+
import { createBotBridge, connectBotBridge } from "shardwire";
|
|
180
|
+
```
|
|
318
181
|
|
|
319
|
-
|
|
182
|
+
Main exports:
|
|
320
183
|
|
|
321
|
-
-
|
|
322
|
-
-
|
|
323
|
-
- `
|
|
324
|
-
-
|
|
184
|
+
- `createBotBridge(options)`
|
|
185
|
+
- `connectBotBridge(options)`
|
|
186
|
+
- `BridgeCapabilityError`
|
|
187
|
+
- bot/app option types
|
|
188
|
+
- normalized event payload types like `BridgeMessage`, `BridgeInteraction`, and `BridgeGuildMember`
|
|
189
|
+
- action payload/result types
|
|
325
190
|
|
|
326
|
-
##
|
|
191
|
+
## Notes
|
|
327
192
|
|
|
328
|
-
-
|
|
329
|
-
-
|
|
330
|
-
-
|
|
331
|
-
- Set payload and timeout limits appropriate for your workload.
|
|
332
|
-
- Configure `server.corsOrigins` when exposing browser consumers.
|
|
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
|
|
333
196
|
|
|
334
|
-
##
|
|
197
|
+
## Examples
|
|
335
198
|
|
|
336
|
-
-
|
|
337
|
-
-
|
|
338
|
-
- Single-host process model (no cross-host sharding in v1).
|
|
339
|
-
- Shared-secret handshake with in-memory dedupe and pending-request tracking.
|
|
199
|
+
- Bot: [examples/bot-basic.ts](./examples/bot-basic.ts)
|
|
200
|
+
- App: [examples/app-basic.ts](./examples/app-basic.ts)
|