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 CHANGED
@@ -1,339 +1,235 @@
1
- # shardwire
1
+ # Shardwire
2
2
 
3
- > Lightweight TypeScript library for building a Discord-hosted WebSocket command and event bridge.
3
+ [![npm version](https://img.shields.io/npm/v/shardwire)](https://www.npmjs.com/package/shardwire)
4
+ [![npm downloads](https://img.shields.io/npm/dm/shardwire)](https://www.npmjs.com/package/shardwire)
5
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18.18-339933)](https://nodejs.org/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
4
7
 
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)
8
+ Discord-first bridge for streaming bot events to external apps and executing bot actions over one WebSocket connection.
10
9
 
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.
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
- [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
+ ## Why Shardwire
14
13
 
15
- ## Table of Contents
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
- - [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)
20
+ ## Requirements
31
21
 
32
- ## Why shardwire
33
-
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
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
- pnpm add shardwire
30
+ npm install shardwire
58
31
  ```
59
32
 
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
- ```
33
+ ## Quick Start
74
34
 
75
- ## Host Setup
35
+ ### 1) Start the bot bridge process
76
36
 
77
37
  ```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
- };
38
+ import { createBotBridge } from "shardwire";
86
39
 
87
- type Events = {
88
- "member-joined": { userId: string; guildId: string };
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
- wire.emitEvent("member-joined", { userId: "123", guildId: "456" });
49
+ await bridge.ready();
50
+ console.log("Bot bridge listening on ws://127.0.0.1:3001/shardwire");
107
51
  ```
108
52
 
109
- ## Consumer Setup
53
+ ### 2) Connect from your app process
110
54
 
111
55
  ```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
- };
56
+ import { connectBotBridge } from "shardwire";
120
57
 
121
- type Events = {
122
- "member-joined": { userId: string; guildId: string };
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
- secretId: "s0",
129
- clientName: "dashboard-api",
61
+ appName: "dashboard",
130
62
  });
131
63
 
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);
64
+ app.on("ready", ({ user }) => {
65
+ console.log("Bot ready as", user.username);
142
66
  });
143
67
 
144
- wire.onReconnecting(({ attempt, delayMs }) => {
145
- console.warn(`reconnecting attempt ${attempt} in ${delayMs}ms`);
68
+ app.on("messageCreate", ({ message }) => {
69
+ console.log(message.channelId, message.content);
146
70
  });
147
71
 
148
- await wire.ready();
72
+ await app.ready();
149
73
  ```
150
74
 
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.
75
+ ### 3) Call bot actions from your app
154
76
 
155
77
  ```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",
162
- },
78
+ const result = await app.actions.sendMessage({
79
+ channelId: "123456789012345678",
80
+ content: "Hello from app side",
163
81
  });
164
- ```
165
82
 
166
- > [!IMPORTANT]
167
- > Keep `DISCORD_BOT_TOKEN` and `SHARDWIRE_SECRET` in environment variables. Never commit them.
168
-
169
- ## Reconnect and Timeout Hardening
83
+ if (!result.ok) {
84
+ console.error(result.error.code, result.error.message);
85
+ }
86
+ ```
170
87
 
171
- For unstable networks, tune reconnect behavior and request timeout explicitly:
88
+ ### 4) Filter subscriptions when needed
172
89
 
173
90
  ```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,
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
- ## Schema Validation (Zod)
100
+ ## Built-In Events
189
101
 
190
- Use runtime schemas to validate command request/response payloads and emitted event payloads.
102
+ Apps subscribe to events with `app.on(...)`. The bridge forwards only what each app subscribes to.
191
103
 
192
- ```ts
193
- import { z } from "zod";
194
- import { createShardwire, fromZodSchema } from "shardwire";
104
+ - `ready`
105
+ - `interactionCreate`
106
+ - `messageCreate`
107
+ - `messageUpdate`
108
+ - `messageDelete`
109
+ - `messageReactionAdd`
110
+ - `messageReactionRemove`
111
+ - `guildMemberAdd`
112
+ - `guildMemberRemove`
195
113
 
196
- type Commands = {
197
- "ban-user": {
198
- request: { userId: string };
199
- response: { banned: true; userId: string };
200
- };
201
- };
114
+ Supported filters:
202
115
 
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
- });
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
- ### Consumer API
121
+ ### Intent Notes
230
122
 
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.
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
- ## Configuration
128
+ ## Built-In Actions
243
129
 
244
- ### Host options
130
+ `app.actions.*` includes:
245
131
 
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).
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
- ### Consumer options
145
+ All actions return:
259
146
 
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.
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
- > [!NOTE]
270
- > Invalid configuration, empty command/event names, or non-serializable payloads throw synchronously with clear errors.
153
+ ## Secret Scopes
271
154
 
272
- ## Error Model
155
+ Use a plain string secret for full event/action access:
273
156
 
274
- `send()` resolves to:
157
+ ```ts
158
+ server: {
159
+ port: 3001,
160
+ secrets: ["full-access-secret"],
161
+ }
162
+ ```
275
163
 
276
- - success: `{ ok: true, requestId, ts, data }`
277
- - failure: `{ ok: false, requestId, ts, error }`
164
+ Use a scoped secret to limit what an app can do:
278
165
 
279
- When `error.code === "VALIDATION_ERROR"`, `error.details` includes:
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
- - `name`: command/event name
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
- Failure codes:
184
+ ```ts
185
+ const capabilities = app.capabilities();
186
+ console.log(capabilities.events, capabilities.actions);
187
+ ```
286
188
 
287
- - `UNAUTHORIZED`
288
- - `TIMEOUT`
289
- - `DISCONNECTED`
290
- - `COMMAND_NOT_FOUND`
291
- - `VALIDATION_ERROR`
292
- - `INTERNAL_ERROR`
189
+ ## Run the Included Examples
293
190
 
294
- ## Recipes and Troubleshooting
191
+ In two terminals:
295
192
 
296
- Run local examples:
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
- pnpm run example:host
300
- pnpm run example:consumer
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
- Practical guides:
203
+ Examples:
306
204
 
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)
205
+ - Bot bridge: `examples/bot-basic.ts`
206
+ - App client: `examples/app-basic.ts`
312
207
 
313
- Common symptoms:
208
+ ## Public API
314
209
 
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.
210
+ ```ts
211
+ import { createBotBridge, connectBotBridge } from "shardwire";
212
+ ```
318
213
 
319
- ## Compatibility
214
+ Main exports include:
320
215
 
321
- - Node.js `>=18.18`
322
- - TypeScript-first API
323
- - `discord.js` `^14` as optional peer dependency
324
- - ESM + CJS package exports
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
- ## Security Notes
229
+ ## Contributing
327
230
 
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.
231
+ Issues and pull requests are welcome: [github.com/unloopedmido/shardwire](https://github.com/unloopedmido/shardwire).
333
232
 
334
- ## Roadmap Constraints (v1)
233
+ ## License
335
234
 
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.
235
+ MIT - see [`LICENSE`](./LICENSE).