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 CHANGED
@@ -1,339 +1,200 @@
1
- # shardwire
1
+ # Shardwire
2
2
 
3
- > Lightweight TypeScript library for building a Discord-hosted WebSocket command and event bridge.
3
+ Discord events and bot actions, streamed to your app over a single WebSocket bridge.
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/shardwire?style=flat-square)](https://www.npmjs.com/package/shardwire)
6
- [![npm downloads](https://img.shields.io/npm/dm/shardwire?style=flat-square)](https://www.npmjs.com/package/shardwire)
7
- [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18.18-3c873a?style=flat-square)](https://nodejs.org/)
8
- [![TypeScript](https://img.shields.io/badge/TypeScript-Strict-3178c6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
9
- [![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](./LICENSE)
5
+ Shardwire runs your bot connection, listens to Discord, and exposes a clean app-facing API for:
10
6
 
11
- `shardwire` helps you expose strongly-typed bot capabilities over WebSocket so dashboards, admin tools, and backend services can send commands to your Discord bot host and subscribe to events in real time.
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
- [Quick Start](#quick-start) [Why shardwire](#why-shardwire) [Host Setup](#host-setup) • [Consumer Setup](#consumer-setup) • [API Overview](#api-overview) • [Configuration](#configuration) • [Security Notes](#security-notes)
12
+ It is designed for one common setup:
14
13
 
15
- ## Table of Contents
14
+ > your Discord bot lives in one process, while your web app, backend, or worker lives somewhere else
16
15
 
17
- - [Why shardwire](#why-shardwire)
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
- ## Why shardwire
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
- Running bot logic inside your Discord host process while orchestrating it from external services is a common pattern, but wiring this safely and ergonomically takes time. `shardwire` gives you a focused transport layer with a typed contract so you can:
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
- pnpm add shardwire
27
+ npm install shardwire
58
28
  ```
59
29
 
60
- Define shared message contracts:
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
- ## Host Setup
32
+ ### 1. Start the bot bridge
76
33
 
77
34
  ```ts
78
- import { createShardwire } from "shardwire";
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 wire = createShardwire<Commands, Events>({
92
- client: discordClient,
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
- wire.onCommand("ban-user", async ({ userId }) => {
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
- ## Consumer Setup
49
+ ### 2. Connect from your app
110
50
 
111
51
  ```ts
112
- import { createShardwire } from "shardwire";
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 wire = createShardwire<Commands, Events>({
126
- url: "ws://localhost:3001/shardwire",
54
+ const app = connectBotBridge({
55
+ url: "ws://127.0.0.1:3001/shardwire",
127
56
  secret: process.env.SHARDWIRE_SECRET!,
128
- secretId: "s0",
129
- clientName: "dashboard-api",
57
+ appName: "dashboard",
130
58
  });
131
59
 
132
- const result = await wire.send("ban-user", { userId: "123" });
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
- wire.onReconnecting(({ attempt, delayMs }) => {
145
- console.warn(`reconnecting attempt ${attempt} in ${delayMs}ms`);
64
+ app.on("messageCreate", ({ message }) => {
65
+ console.log(message.channelId, message.content);
146
66
  });
147
67
 
148
- await wire.ready();
68
+ await app.ready();
149
69
  ```
150
70
 
151
- ## Token-Only Host (No Existing discord.js Client)
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
- const wire = createShardwire<Commands, Events>({
157
- token: process.env.DISCORD_BOT_TOKEN!,
158
- server: {
159
- port: 3001,
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
- > [!IMPORTANT]
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 wire = createShardwire({
175
- url: "ws://bot-host:3001/shardwire",
176
- secret: process.env.SHARDWIRE_SECRET!,
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
- ```ts
193
- import { z } from "zod";
194
- import { createShardwire, fromZodSchema } from "shardwire";
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
- ## 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.
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
- ## Configuration
98
+ Shardwire currently exposes these bot-side events:
243
99
 
244
- ### Host options
100
+ - `ready`
101
+ - `interactionCreate`
102
+ - `messageCreate`
103
+ - `messageUpdate`
104
+ - `messageDelete`
105
+ - `guildMemberAdd`
106
+ - `guildMemberRemove`
245
107
 
246
- - `server.port` required port.
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
- ### Consumer options
110
+ Optional filters can narrow delivery by:
259
111
 
260
- - `url` host endpoint (for example `ws://localhost:3001/shardwire`).
261
- - `secret` shared secret matching host.
262
- - `secretId` optional secret id (for example `"s0"`) used during handshake.
263
- - `clientName` optional identity sent during auth handshake for host-side telemetry.
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
- > [!NOTE]
270
- > Invalid configuration, empty command/event names, or non-serializable payloads throw synchronously with clear errors.
117
+ ## Built-In Actions
271
118
 
272
- ## Error Model
119
+ `app.actions.*` currently includes:
273
120
 
274
- `send()` resolves to:
121
+ - `sendMessage`
122
+ - `editMessage`
123
+ - `deleteMessage`
124
+ - `replyToInteraction`
125
+ - `deferInteraction`
126
+ - `followUpInteraction`
127
+ - `banMember`
128
+ - `kickMember`
129
+ - `addMemberRole`
130
+ - `removeMemberRole`
275
131
 
276
- - success: `{ ok: true, requestId, ts, data }`
277
- - failure: `{ ok: false, requestId, ts, error }`
132
+ Every action returns an `ActionResult<T>`:
278
133
 
279
- When `error.code === "VALIDATION_ERROR"`, `error.details` includes:
280
-
281
- - `name`: command/event name
282
- - `stage`: `"command.request" | "command.response" | "event.emit"`
283
- - `issues`: optional normalized issue list (`path`, `message`)
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
- Failure codes:
140
+ ## Secret Scopes
286
141
 
287
- - `UNAUTHORIZED`
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
- ## Recipes and Troubleshooting
144
+ ```ts
145
+ server: {
146
+ port: 3001,
147
+ secrets: ["full-access-secret"],
148
+ }
149
+ ```
295
150
 
296
- Run local examples:
151
+ Or scope a secret to specific events and actions:
297
152
 
298
- ```bash
299
- pnpm run example:host
300
- pnpm run example:consumer
301
- pnpm run example:host:schema
302
- pnpm run example:consumer:schema
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
- Practical guides:
169
+ On the app side, you can inspect what the connection is allowed to do:
306
170
 
307
- - [Integration reference](./.agents/skills/shardwire/references.md)
308
- - [Add command + event flow](./.agents/skills/shardwire/examples/command-event-change.md)
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)
171
+ ```ts
172
+ const capabilities = app.capabilities();
173
+ console.log(capabilities.events, capabilities.actions);
174
+ ```
312
175
 
313
- Common symptoms:
176
+ ## Public API
314
177
 
315
- - `UNAUTHORIZED`: verify `secret`, `secretId`, and host `server.secrets` ordering.
316
- - `DISCONNECTED`: host unavailable or connection dropped before response completed.
317
- - Frequent `TIMEOUT`: increase `requestTimeoutMs` and inspect host command handler duration.
178
+ ```ts
179
+ import { createBotBridge, connectBotBridge } from "shardwire";
180
+ ```
318
181
 
319
- ## Compatibility
182
+ Main exports:
320
183
 
321
- - Node.js `>=18.18`
322
- - TypeScript-first API
323
- - `discord.js` `^14` as optional peer dependency
324
- - ESM + CJS package exports
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
- ## Security Notes
191
+ ## Notes
327
192
 
328
- - Use strong, rotated secrets via environment variables.
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.
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
- ## Roadmap Constraints (v1)
197
+ ## Examples
335
198
 
336
- - Single npm package, no external infrastructure requirement.
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.
199
+ - Bot: [examples/bot-basic.ts](./examples/bot-basic.ts)
200
+ - App: [examples/app-basic.ts](./examples/app-basic.ts)