shardwire 0.2.0 → 1.1.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 +155 -259
- package/dist/index.d.mts +302 -129
- package/dist/index.d.ts +302 -129
- package/dist/index.js +1635 -835
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1633 -818
- package/dist/index.mjs.map +1 -1
- package/package.json +22 -34
package/README.md
CHANGED
|
@@ -1,339 +1,235 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Shardwire
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/shardwire)
|
|
4
|
+
[](https://www.npmjs.com/package/shardwire)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](./LICENSE)
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
[](https://www.npmjs.com/package/shardwire)
|
|
7
|
-
[](https://nodejs.org/)
|
|
8
|
-
[](https://www.typescriptlang.org/)
|
|
9
|
-
[](./LICENSE)
|
|
8
|
+
Discord-first bridge for streaming bot events to external apps and executing bot actions over one WebSocket connection.
|
|
10
9
|
|
|
11
|
-
|
|
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.
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
## Why Shardwire
|
|
14
13
|
|
|
15
|
-
|
|
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.
|
|
16
19
|
|
|
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)
|
|
20
|
+
## Requirements
|
|
31
21
|
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
|
53
26
|
|
|
54
|
-
Install
|
|
27
|
+
## Install
|
|
55
28
|
|
|
56
29
|
```bash
|
|
57
|
-
|
|
30
|
+
npm install shardwire
|
|
58
31
|
```
|
|
59
32
|
|
|
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
|
-
```
|
|
33
|
+
## Quick Start
|
|
74
34
|
|
|
75
|
-
|
|
35
|
+
### 1) Start the bot bridge process
|
|
76
36
|
|
|
77
37
|
```ts
|
|
78
|
-
import {
|
|
79
|
-
|
|
80
|
-
type Commands = {
|
|
81
|
-
"ban-user": {
|
|
82
|
-
request: { userId: string };
|
|
83
|
-
response: { banned: true; userId: string };
|
|
84
|
-
};
|
|
85
|
-
};
|
|
38
|
+
import { createBotBridge } from "shardwire";
|
|
86
39
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const wire = createShardwire<Commands, Events>({
|
|
92
|
-
client: discordClient,
|
|
40
|
+
const bridge = createBotBridge({
|
|
41
|
+
token: process.env.DISCORD_TOKEN!,
|
|
42
|
+
intents: ["Guilds", "GuildMessages", "GuildMessageReactions", "MessageContent", "GuildMembers"],
|
|
93
43
|
server: {
|
|
94
44
|
port: 3001,
|
|
95
45
|
secrets: [process.env.SHARDWIRE_SECRET!],
|
|
96
|
-
primarySecretId: "s0",
|
|
97
46
|
},
|
|
98
|
-
name: "bot-host",
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
wire.onCommand("ban-user", async ({ userId }) => {
|
|
102
|
-
await guild.members.ban(userId);
|
|
103
|
-
return { banned: true, userId };
|
|
104
47
|
});
|
|
105
48
|
|
|
106
|
-
|
|
49
|
+
await bridge.ready();
|
|
50
|
+
console.log("Bot bridge listening on ws://127.0.0.1:3001/shardwire");
|
|
107
51
|
```
|
|
108
52
|
|
|
109
|
-
|
|
53
|
+
### 2) Connect from your app process
|
|
110
54
|
|
|
111
55
|
```ts
|
|
112
|
-
import {
|
|
113
|
-
|
|
114
|
-
type Commands = {
|
|
115
|
-
"ban-user": {
|
|
116
|
-
request: { userId: string };
|
|
117
|
-
response: { banned: true; userId: string };
|
|
118
|
-
};
|
|
119
|
-
};
|
|
56
|
+
import { connectBotBridge } from "shardwire";
|
|
120
57
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const wire = createShardwire<Commands, Events>({
|
|
126
|
-
url: "ws://localhost:3001/shardwire",
|
|
58
|
+
const app = connectBotBridge({
|
|
59
|
+
url: "ws://127.0.0.1:3001/shardwire",
|
|
127
60
|
secret: process.env.SHARDWIRE_SECRET!,
|
|
128
|
-
|
|
129
|
-
clientName: "dashboard-api",
|
|
61
|
+
appName: "dashboard",
|
|
130
62
|
});
|
|
131
63
|
|
|
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);
|
|
64
|
+
app.on("ready", ({ user }) => {
|
|
65
|
+
console.log("Bot ready as", user.username);
|
|
142
66
|
});
|
|
143
67
|
|
|
144
|
-
|
|
145
|
-
console.
|
|
68
|
+
app.on("messageCreate", ({ message }) => {
|
|
69
|
+
console.log(message.channelId, message.content);
|
|
146
70
|
});
|
|
147
71
|
|
|
148
|
-
await
|
|
72
|
+
await app.ready();
|
|
149
73
|
```
|
|
150
74
|
|
|
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.
|
|
75
|
+
### 3) Call bot actions from your app
|
|
154
76
|
|
|
155
77
|
```ts
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
port: 3001,
|
|
160
|
-
secrets: [process.env.SHARDWIRE_SECRET!],
|
|
161
|
-
primarySecretId: "s0",
|
|
162
|
-
},
|
|
78
|
+
const result = await app.actions.sendMessage({
|
|
79
|
+
channelId: "123456789012345678",
|
|
80
|
+
content: "Hello from app side",
|
|
163
81
|
});
|
|
164
|
-
```
|
|
165
82
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
console.error(result.error.code, result.error.message);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
170
87
|
|
|
171
|
-
|
|
88
|
+
### 4) Filter subscriptions when needed
|
|
172
89
|
|
|
173
90
|
```ts
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
requestTimeoutMs: 10_000,
|
|
179
|
-
reconnect: {
|
|
180
|
-
enabled: true,
|
|
181
|
-
initialDelayMs: 500,
|
|
182
|
-
maxDelayMs: 10_000,
|
|
183
|
-
jitter: true,
|
|
91
|
+
app.on(
|
|
92
|
+
"messageCreate",
|
|
93
|
+
({ message }) => {
|
|
94
|
+
console.log("Only this channel:", message.content);
|
|
184
95
|
},
|
|
185
|
-
}
|
|
96
|
+
{ channelId: "123456789012345678" },
|
|
97
|
+
);
|
|
186
98
|
```
|
|
187
99
|
|
|
188
|
-
##
|
|
100
|
+
## Built-In Events
|
|
189
101
|
|
|
190
|
-
|
|
102
|
+
Apps subscribe to events with `app.on(...)`. The bridge forwards only what each app subscribes to.
|
|
191
103
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
104
|
+
- `ready`
|
|
105
|
+
- `interactionCreate`
|
|
106
|
+
- `messageCreate`
|
|
107
|
+
- `messageUpdate`
|
|
108
|
+
- `messageDelete`
|
|
109
|
+
- `messageReactionAdd`
|
|
110
|
+
- `messageReactionRemove`
|
|
111
|
+
- `guildMemberAdd`
|
|
112
|
+
- `guildMemberRemove`
|
|
195
113
|
|
|
196
|
-
|
|
197
|
-
"ban-user": {
|
|
198
|
-
request: { userId: string };
|
|
199
|
-
response: { banned: true; userId: string };
|
|
200
|
-
};
|
|
201
|
-
};
|
|
114
|
+
Supported filters:
|
|
202
115
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
});
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
## API Overview
|
|
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.
|
|
116
|
+
- `guildId`
|
|
117
|
+
- `channelId`
|
|
118
|
+
- `userId`
|
|
119
|
+
- `commandName` (for `interactionCreate`)
|
|
228
120
|
|
|
229
|
-
###
|
|
121
|
+
### Intent Notes
|
|
230
122
|
|
|
231
|
-
- `
|
|
232
|
-
- `
|
|
233
|
-
- `
|
|
234
|
-
- `
|
|
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.
|
|
123
|
+
- `ready` and `interactionCreate`: no specific event intent requirement
|
|
124
|
+
- `messageCreate`, `messageUpdate`, `messageDelete`: `GuildMessages`
|
|
125
|
+
- `messageReactionAdd`, `messageReactionRemove`: `GuildMessageReactions`
|
|
126
|
+
- `guildMemberAdd`, `guildMemberRemove`: `GuildMembers`
|
|
241
127
|
|
|
242
|
-
##
|
|
128
|
+
## Built-In Actions
|
|
243
129
|
|
|
244
|
-
|
|
130
|
+
`app.actions.*` includes:
|
|
245
131
|
|
|
246
|
-
- `
|
|
247
|
-
- `
|
|
248
|
-
- `
|
|
249
|
-
- `
|
|
250
|
-
- `
|
|
251
|
-
- `
|
|
252
|
-
- `
|
|
253
|
-
- `
|
|
254
|
-
- `
|
|
255
|
-
- `
|
|
256
|
-
- `
|
|
132
|
+
- `sendMessage`
|
|
133
|
+
- `editMessage`
|
|
134
|
+
- `deleteMessage`
|
|
135
|
+
- `replyToInteraction`
|
|
136
|
+
- `deferInteraction`
|
|
137
|
+
- `followUpInteraction`
|
|
138
|
+
- `banMember`
|
|
139
|
+
- `kickMember`
|
|
140
|
+
- `addMemberRole`
|
|
141
|
+
- `removeMemberRole`
|
|
142
|
+
- `addMessageReaction`
|
|
143
|
+
- `removeOwnMessageReaction`
|
|
257
144
|
|
|
258
|
-
|
|
145
|
+
All actions return:
|
|
259
146
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
- `requestTimeoutMs` default timeout for `send`.
|
|
266
|
-
- `reconnect` reconnect policy (`enabled`, delays, jitter).
|
|
267
|
-
- `webSocketFactory` optional custom client implementation.
|
|
147
|
+
```ts
|
|
148
|
+
type ActionResult<T> =
|
|
149
|
+
| { ok: true; requestId: string; ts: number; data: T }
|
|
150
|
+
| { ok: false; requestId: string; ts: number; error: { code: string; message: string; details?: unknown } };
|
|
151
|
+
```
|
|
268
152
|
|
|
269
|
-
|
|
270
|
-
> Invalid configuration, empty command/event names, or non-serializable payloads throw synchronously with clear errors.
|
|
153
|
+
## Secret Scopes
|
|
271
154
|
|
|
272
|
-
|
|
155
|
+
Use a plain string secret for full event/action access:
|
|
273
156
|
|
|
274
|
-
|
|
157
|
+
```ts
|
|
158
|
+
server: {
|
|
159
|
+
port: 3001,
|
|
160
|
+
secrets: ["full-access-secret"],
|
|
161
|
+
}
|
|
162
|
+
```
|
|
275
163
|
|
|
276
|
-
|
|
277
|
-
- failure: `{ ok: false, requestId, ts, error }`
|
|
164
|
+
Use a scoped secret to limit what an app can do:
|
|
278
165
|
|
|
279
|
-
|
|
166
|
+
```ts
|
|
167
|
+
server: {
|
|
168
|
+
port: 3001,
|
|
169
|
+
secrets: [
|
|
170
|
+
{
|
|
171
|
+
id: "dashboard",
|
|
172
|
+
value: process.env.SHARDWIRE_SECRET!,
|
|
173
|
+
allow: {
|
|
174
|
+
events: ["ready", "messageCreate"],
|
|
175
|
+
actions: ["sendMessage", "replyToInteraction"],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
}
|
|
180
|
+
```
|
|
280
181
|
|
|
281
|
-
|
|
282
|
-
- `stage`: `"command.request" | "command.response" | "event.emit"`
|
|
283
|
-
- `issues`: optional normalized issue list (`path`, `message`)
|
|
182
|
+
Inspect negotiated capabilities in the app:
|
|
284
183
|
|
|
285
|
-
|
|
184
|
+
```ts
|
|
185
|
+
const capabilities = app.capabilities();
|
|
186
|
+
console.log(capabilities.events, capabilities.actions);
|
|
187
|
+
```
|
|
286
188
|
|
|
287
|
-
|
|
288
|
-
- `TIMEOUT`
|
|
289
|
-
- `DISCONNECTED`
|
|
290
|
-
- `COMMAND_NOT_FOUND`
|
|
291
|
-
- `VALIDATION_ERROR`
|
|
292
|
-
- `INTERNAL_ERROR`
|
|
189
|
+
## Run the Included Examples
|
|
293
190
|
|
|
294
|
-
|
|
191
|
+
In two terminals:
|
|
295
192
|
|
|
296
|
-
|
|
193
|
+
```bash
|
|
194
|
+
# terminal 1
|
|
195
|
+
DISCORD_TOKEN=your-token SHARDWIRE_SECRET=dev-secret npm run example:bot
|
|
196
|
+
```
|
|
297
197
|
|
|
298
198
|
```bash
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
pnpm run example:host:schema
|
|
302
|
-
pnpm run example:consumer:schema
|
|
199
|
+
# terminal 2
|
|
200
|
+
SHARDWIRE_SECRET=dev-secret npm run example:app
|
|
303
201
|
```
|
|
304
202
|
|
|
305
|
-
|
|
203
|
+
Examples:
|
|
306
204
|
|
|
307
|
-
-
|
|
308
|
-
-
|
|
309
|
-
- [Reconnect hardening](./.agents/skills/shardwire/examples/reconnect-hardening.md)
|
|
310
|
-
- [Token-only host setup](./.agents/skills/shardwire/examples/token-only-host.md)
|
|
311
|
-
- [Troubleshooting flow](./.agents/skills/shardwire/examples/troubleshooting-flow.md)
|
|
205
|
+
- Bot bridge: `examples/bot-basic.ts`
|
|
206
|
+
- App client: `examples/app-basic.ts`
|
|
312
207
|
|
|
313
|
-
|
|
208
|
+
## Public API
|
|
314
209
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
210
|
+
```ts
|
|
211
|
+
import { createBotBridge, connectBotBridge } from "shardwire";
|
|
212
|
+
```
|
|
318
213
|
|
|
319
|
-
|
|
214
|
+
Main exports include:
|
|
320
215
|
|
|
321
|
-
-
|
|
322
|
-
-
|
|
323
|
-
- `
|
|
324
|
-
-
|
|
216
|
+
- `createBotBridge(options)`
|
|
217
|
+
- `connectBotBridge(options)`
|
|
218
|
+
- `BridgeCapabilityError`
|
|
219
|
+
- bot/app option types
|
|
220
|
+
- normalized event payload types (for example `BridgeMessage`, `BridgeInteraction`, `BridgeGuildMember`)
|
|
221
|
+
- action payload and result types
|
|
222
|
+
|
|
223
|
+
## Security and Transport Notes
|
|
224
|
+
|
|
225
|
+
- Use `wss://` for non-loopback deployments.
|
|
226
|
+
- `ws://` is only accepted for loopback hosts (`127.0.0.1`, `localhost`, `::1`).
|
|
227
|
+
- Event availability depends on enabled intents and secret scope.
|
|
325
228
|
|
|
326
|
-
##
|
|
229
|
+
## Contributing
|
|
327
230
|
|
|
328
|
-
|
|
329
|
-
- Rotate with overlapping `server.secrets` entries and explicit `secretId` cutovers.
|
|
330
|
-
- Use `wss://` for non-localhost deployments. By default, consumer `ws://` is only accepted for loopback hosts.
|
|
331
|
-
- Set payload and timeout limits appropriate for your workload.
|
|
332
|
-
- Configure `server.corsOrigins` when exposing browser consumers.
|
|
231
|
+
Issues and pull requests are welcome: [github.com/unloopedmido/shardwire](https://github.com/unloopedmido/shardwire).
|
|
333
232
|
|
|
334
|
-
##
|
|
233
|
+
## License
|
|
335
234
|
|
|
336
|
-
-
|
|
337
|
-
- Host process embeds WebSocket server.
|
|
338
|
-
- Single-host process model (no cross-host sharding in v1).
|
|
339
|
-
- Shared-secret handshake with in-memory dedupe and pending-request tracking.
|
|
235
|
+
MIT - see [`LICENSE`](./LICENSE).
|