shardwire 0.1.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,337 +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
- - `requestTimeoutMs` default timeout for `send`.
265
- - `reconnect` reconnect policy (`enabled`, delays, jitter).
266
- - `webSocketFactory` optional custom client implementation.
112
+ - `guildId`
113
+ - `channelId`
114
+ - `userId`
115
+ - `commandName` for `interactionCreate`
267
116
 
268
- > [!NOTE]
269
- > Invalid configuration, empty command/event names, or non-serializable payloads throw synchronously with clear errors.
117
+ ## Built-In Actions
270
118
 
271
- ## Error Model
119
+ `app.actions.*` currently includes:
272
120
 
273
- `send()` resolves to:
121
+ - `sendMessage`
122
+ - `editMessage`
123
+ - `deleteMessage`
124
+ - `replyToInteraction`
125
+ - `deferInteraction`
126
+ - `followUpInteraction`
127
+ - `banMember`
128
+ - `kickMember`
129
+ - `addMemberRole`
130
+ - `removeMemberRole`
274
131
 
275
- - success: `{ ok: true, requestId, ts, data }`
276
- - failure: `{ ok: false, requestId, ts, error }`
132
+ Every action returns an `ActionResult<T>`:
277
133
 
278
- When `error.code === "VALIDATION_ERROR"`, `error.details` includes:
279
-
280
- - `name`: command/event name
281
- - `stage`: `"command.request" | "command.response" | "event.emit"`
282
- - `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
+ ```
283
139
 
284
- Failure codes:
140
+ ## Secret Scopes
285
141
 
286
- - `UNAUTHORIZED`
287
- - `TIMEOUT`
288
- - `DISCONNECTED`
289
- - `COMMAND_NOT_FOUND`
290
- - `VALIDATION_ERROR`
291
- - `INTERNAL_ERROR`
142
+ Use a plain string secret for full access:
292
143
 
293
- ## Recipes and Troubleshooting
144
+ ```ts
145
+ server: {
146
+ port: 3001,
147
+ secrets: ["full-access-secret"],
148
+ }
149
+ ```
294
150
 
295
- Run local examples:
151
+ Or scope a secret to specific events and actions:
296
152
 
297
- ```bash
298
- pnpm run example:host
299
- pnpm run example:consumer
300
- pnpm run example:host:schema
301
- 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
+ }
302
167
  ```
303
168
 
304
- Practical guides:
169
+ On the app side, you can inspect what the connection is allowed to do:
305
170
 
306
- - [Integration reference](./.agents/skills/shardwire/references.md)
307
- - [Add command + event flow](./.agents/skills/shardwire/examples/command-event-change.md)
308
- - [Reconnect hardening](./.agents/skills/shardwire/examples/reconnect-hardening.md)
309
- - [Token-only host setup](./.agents/skills/shardwire/examples/token-only-host.md)
310
- - [Troubleshooting flow](./.agents/skills/shardwire/examples/troubleshooting-flow.md)
171
+ ```ts
172
+ const capabilities = app.capabilities();
173
+ console.log(capabilities.events, capabilities.actions);
174
+ ```
311
175
 
312
- Common symptoms:
176
+ ## Public API
313
177
 
314
- - `UNAUTHORIZED`: verify `secret`, `secretId`, and host `server.secrets` ordering.
315
- - `DISCONNECTED`: host unavailable or connection dropped before response completed.
316
- - Frequent `TIMEOUT`: increase `requestTimeoutMs` and inspect host command handler duration.
178
+ ```ts
179
+ import { createBotBridge, connectBotBridge } from "shardwire";
180
+ ```
317
181
 
318
- ## Compatibility
182
+ Main exports:
319
183
 
320
- - Node.js `>=18.18`
321
- - TypeScript-first API
322
- - `discord.js` `^14` as optional peer dependency
323
- - 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
324
190
 
325
- ## Security Notes
191
+ ## Notes
326
192
 
327
- - Use strong, rotated secrets via environment variables.
328
- - Rotate with overlapping `server.secrets` entries and explicit `secretId` cutovers.
329
- - Set payload and timeout limits appropriate for your workload.
330
- - 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
331
196
 
332
- ## Roadmap Constraints (v1)
197
+ ## Examples
333
198
 
334
- - Single npm package, no external infrastructure requirement.
335
- - Host process embeds WebSocket server.
336
- - Single-host process model (no cross-host sharding in v1).
337
- - 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)