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/dist/index.js CHANGED
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,51 +15,74 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
 
30
20
  // src/index.ts
31
21
  var index_exports = {};
32
22
  __export(index_exports, {
33
- createShardwire: () => createShardwire,
34
- fromSafeParseSchema: () => fromSafeParseSchema,
35
- fromZodSchema: () => fromZodSchema
23
+ BridgeCapabilityError: () => BridgeCapabilityError,
24
+ connectBotBridge: () => connectBotBridge,
25
+ createBotBridge: () => createBotBridge
36
26
  });
37
27
  module.exports = __toCommonJS(index_exports);
38
28
 
39
- // src/transport/ws/consumer-socket.ts
40
- var import_ws = require("ws");
41
- function createNodeWebSocket(url) {
42
- return new import_ws.WebSocket(url);
43
- }
44
-
45
- // src/utils/id.ts
46
- var import_node_crypto = require("crypto");
47
- function createRequestId() {
48
- return (0, import_node_crypto.randomUUID)();
49
- }
50
- function createConnectionId() {
51
- return (0, import_node_crypto.randomUUID)();
29
+ // src/discord/catalog.ts
30
+ var import_discord = require("discord.js");
31
+ var BOT_EVENT_NAMES = [
32
+ "ready",
33
+ "interactionCreate",
34
+ "messageCreate",
35
+ "messageUpdate",
36
+ "messageDelete",
37
+ "guildMemberAdd",
38
+ "guildMemberRemove"
39
+ ];
40
+ var BOT_ACTION_NAMES = [
41
+ "sendMessage",
42
+ "editMessage",
43
+ "deleteMessage",
44
+ "replyToInteraction",
45
+ "deferInteraction",
46
+ "followUpInteraction",
47
+ "banMember",
48
+ "kickMember",
49
+ "addMemberRole",
50
+ "removeMemberRole"
51
+ ];
52
+ var BOT_INTENT_BITS = {
53
+ Guilds: import_discord.GatewayIntentBits.Guilds,
54
+ GuildMembers: import_discord.GatewayIntentBits.GuildMembers,
55
+ GuildMessages: import_discord.GatewayIntentBits.GuildMessages,
56
+ MessageContent: import_discord.GatewayIntentBits.MessageContent
57
+ };
58
+ var EVENT_REQUIRED_INTENTS = {
59
+ ready: [],
60
+ interactionCreate: ["Guilds"],
61
+ messageCreate: ["GuildMessages"],
62
+ messageUpdate: ["GuildMessages"],
63
+ messageDelete: ["GuildMessages"],
64
+ guildMemberAdd: ["GuildMembers"],
65
+ guildMemberRemove: ["GuildMembers"]
66
+ };
67
+ function getAvailableEvents(intents) {
68
+ const enabled = new Set(intents);
69
+ return BOT_EVENT_NAMES.filter((eventName) => EVENT_REQUIRED_INTENTS[eventName].every((intent) => enabled.has(intent)));
52
70
  }
53
71
 
54
- // src/utils/backoff.ts
55
- function getBackoffDelay(attempt, config) {
56
- const base = Math.min(config.maxDelayMs, config.initialDelayMs * 2 ** attempt);
57
- if (!config.jitter) {
58
- return base;
72
+ // src/discord/runtime/adapter.ts
73
+ var ActionExecutionError = class extends Error {
74
+ constructor(code, message, details) {
75
+ super(message);
76
+ this.code = code;
77
+ this.details = details;
78
+ this.name = "ActionExecutionError";
59
79
  }
60
- const spread = Math.floor(base * 0.2);
61
- const min = Math.max(0, base - spread);
62
- const max = base + spread;
63
- return Math.floor(Math.random() * (max - min + 1)) + min;
64
- }
80
+ code;
81
+ details;
82
+ };
83
+
84
+ // src/discord/runtime/discordjs-adapter.ts
85
+ var import_discord2 = require("discord.js");
65
86
 
66
87
  // src/utils/logger.ts
67
88
  function withLogger(logger) {
@@ -73,1052 +94,1704 @@ function withLogger(logger) {
73
94
  };
74
95
  }
75
96
 
76
- // src/core/protocol.ts
77
- var PROTOCOL_VERSION = 1;
78
- function makeEnvelope(type, payload, extras) {
79
- const envelope = {
80
- v: PROTOCOL_VERSION,
81
- type,
82
- ts: Date.now(),
83
- payload
84
- };
85
- if (extras?.requestId) {
86
- envelope.requestId = extras.requestId;
97
+ // src/utils/cache.ts
98
+ var DedupeCache = class {
99
+ constructor(ttlMs) {
100
+ this.ttlMs = ttlMs;
87
101
  }
88
- if (extras?.source) {
89
- envelope.source = extras.source;
102
+ ttlMs;
103
+ cache = /* @__PURE__ */ new Map();
104
+ get(key) {
105
+ const entry = this.cache.get(key);
106
+ if (!entry) {
107
+ return void 0;
108
+ }
109
+ if (entry.expiresAt <= Date.now()) {
110
+ this.cache.delete(key);
111
+ return void 0;
112
+ }
113
+ return entry.value;
90
114
  }
91
- return envelope;
92
- }
93
- function parseEnvelope(raw) {
94
- const parsed = JSON.parse(raw);
95
- if (!parsed || parsed.v !== PROTOCOL_VERSION || typeof parsed.type !== "string") {
96
- throw new Error("Invalid wire envelope.");
115
+ set(key, value) {
116
+ this.cache.set(key, { value, expiresAt: Date.now() + this.ttlMs });
97
117
  }
98
- return parsed;
118
+ };
119
+
120
+ // src/discord/runtime/serializers.ts
121
+ function serializeEmbeds(message) {
122
+ return message.embeds.map((embed) => embed.toJSON());
99
123
  }
100
- function stringifyEnvelope(envelope) {
101
- return JSON.stringify(envelope);
124
+ function serializeUser(user) {
125
+ return {
126
+ id: user.id,
127
+ username: user.username,
128
+ discriminator: user.discriminator,
129
+ ...user.globalName !== void 0 ? { globalName: user.globalName } : {},
130
+ avatarUrl: user.displayAvatarURL() || null,
131
+ bot: user.bot,
132
+ system: user.system ?? false
133
+ };
102
134
  }
103
-
104
- // src/runtime/validation.ts
105
- var PayloadValidationError = class extends Error {
106
- constructor(message, details) {
107
- super(message);
108
- this.details = details;
109
- this.name = "PayloadValidationError";
110
- }
111
- details;
112
- };
113
- function isNonEmptyString(value) {
114
- return typeof value === "string" && value.trim().length > 0;
135
+ function serializeGuildMember(member) {
136
+ return {
137
+ id: member.id,
138
+ guildId: member.guild.id,
139
+ ..."user" in member && member.user ? { user: serializeUser(member.user) } : {},
140
+ ..."displayName" in member && typeof member.displayName === "string" ? { displayName: member.displayName } : {},
141
+ ..."nickname" in member ? { nickname: member.nickname ?? null } : {},
142
+ roles: "roles" in member && "cache" in member.roles ? [...member.roles.cache.keys()] : [],
143
+ ..."joinedAt" in member ? { joinedAt: member.joinedAt?.toISOString() ?? null } : {},
144
+ ..."premiumSince" in member ? { premiumSince: member.premiumSince?.toISOString() ?? null } : {},
145
+ ..."pending" in member && typeof member.pending === "boolean" ? { pending: member.pending } : {},
146
+ ..."communicationDisabledUntil" in member ? { communicationDisabledUntil: member.communicationDisabledUntil?.toISOString() ?? null } : {}
147
+ };
115
148
  }
116
- function assertPositiveNumber(name, value) {
117
- if (typeof value !== "number" || Number.isNaN(value) || value <= 0) {
118
- throw new Error(`${name} must be a positive number.`);
119
- }
149
+ function serializeMessage(message) {
150
+ const reference = message.reference ? {
151
+ ...message.reference.messageId ? { messageId: message.reference.messageId } : {},
152
+ ...message.reference.channelId ? { channelId: message.reference.channelId } : {},
153
+ ...message.reference.guildId ? { guildId: message.reference.guildId } : {}
154
+ } : void 0;
155
+ return {
156
+ id: message.id,
157
+ channelId: message.channelId,
158
+ ...message.guildId ? { guildId: message.guildId } : {},
159
+ ..."author" in message && message.author ? { author: serializeUser(message.author) } : {},
160
+ ..."member" in message && message.member ? { member: serializeGuildMember(message.member) } : {},
161
+ ..."content" in message && typeof message.content === "string" ? { content: message.content } : {},
162
+ ..."createdAt" in message ? { createdAt: message.createdAt.toISOString() } : {},
163
+ ..."editedAt" in message ? { editedAt: message.editedAt ? message.editedAt.toISOString() : null } : {},
164
+ attachments: "attachments" in message ? [...message.attachments.values()].map((attachment) => ({
165
+ id: attachment.id,
166
+ name: attachment.name,
167
+ url: attachment.url,
168
+ ...attachment.contentType !== void 0 ? { contentType: attachment.contentType } : {},
169
+ size: attachment.size
170
+ })) : [],
171
+ embeds: serializeEmbeds(message),
172
+ ...reference ? { reference } : {}
173
+ };
120
174
  }
121
- function assertHostOptions(options) {
122
- if (!options.server) {
123
- throw new Error("Host mode requires a server configuration.");
124
- }
125
- assertPositiveNumber("server.port", options.server.port);
126
- if (!Array.isArray(options.server.secrets) || options.server.secrets.length === 0) {
127
- throw new Error("server.secrets must contain at least one secret.");
128
- }
129
- for (const [index, secret] of options.server.secrets.entries()) {
130
- if (!isNonEmptyString(secret)) {
131
- throw new Error(`server.secrets[${index}] must be a non-empty string.`);
175
+ function serializeDeletedMessage(message) {
176
+ return {
177
+ id: message.id,
178
+ channelId: message.channelId,
179
+ ...message.guildId ? { guildId: message.guildId } : {},
180
+ deletedAt: (/* @__PURE__ */ new Date()).toISOString()
181
+ };
182
+ }
183
+ function serializeChatInputOptions(interaction) {
184
+ const options = {};
185
+ for (const option of interaction.options.data) {
186
+ if (option.options && option.options.length > 0) {
187
+ options[option.name] = option.options.map((child) => child.value);
188
+ continue;
132
189
  }
190
+ options[option.name] = option.value ?? null;
133
191
  }
134
- if (options.server.primarySecretId !== void 0 && !options.server.secrets.some((_, index) => options.server.primarySecretId === `s${index}`)) {
135
- throw new Error("server.primarySecretId must reference an existing secret id.");
136
- }
137
- if (options.server.heartbeatMs !== void 0) {
138
- assertPositiveNumber("server.heartbeatMs", options.server.heartbeatMs);
139
- }
140
- if (options.server.commandTimeoutMs !== void 0) {
141
- assertPositiveNumber("server.commandTimeoutMs", options.server.commandTimeoutMs);
142
- }
143
- if (options.server.maxPayloadBytes !== void 0) {
144
- assertPositiveNumber("server.maxPayloadBytes", options.server.maxPayloadBytes);
192
+ return options;
193
+ }
194
+ function serializeSelectMenu(interaction) {
195
+ return {
196
+ customId: interaction.customId,
197
+ values: [...interaction.values]
198
+ };
199
+ }
200
+ function serializeModalFields(interaction) {
201
+ const fields = {};
202
+ for (const field of interaction.fields.fields.values()) {
203
+ if ("value" in field && typeof field.value === "string") {
204
+ fields[field.customId] = field.value;
205
+ continue;
206
+ }
207
+ if ("values" in field && Array.isArray(field.values)) {
208
+ fields[field.customId] = field.values.join(",");
209
+ }
145
210
  }
211
+ return fields;
146
212
  }
147
- function assertConsumerOptions(options) {
148
- if (!isNonEmptyString(options.url)) {
149
- throw new Error("Consumer mode requires `url`.");
213
+ function serializeInteractionMessage(interaction) {
214
+ if ("message" in interaction && interaction.message) {
215
+ return serializeMessage(interaction.message);
150
216
  }
151
- let parsedUrl;
152
- try {
153
- parsedUrl = new URL(options.url);
154
- } catch {
155
- throw new Error("Consumer option `url` must be a valid URL.");
217
+ return void 0;
218
+ }
219
+ function serializeInteraction(interaction) {
220
+ const base = {
221
+ id: interaction.id,
222
+ applicationId: interaction.applicationId,
223
+ kind: "unknown",
224
+ ...interaction.guildId ? { guildId: interaction.guildId } : {},
225
+ ...interaction.channelId ? { channelId: interaction.channelId } : {},
226
+ user: serializeUser(interaction.user),
227
+ ...interaction.member && "guild" in interaction.member ? { member: serializeGuildMember(interaction.member) } : {}
228
+ };
229
+ if (interaction.isChatInputCommand()) {
230
+ return {
231
+ ...base,
232
+ kind: "chatInput",
233
+ commandName: interaction.commandName,
234
+ options: serializeChatInputOptions(interaction)
235
+ };
156
236
  }
157
- if (parsedUrl.protocol !== "ws:" && parsedUrl.protocol !== "wss:") {
158
- throw new Error("Consumer option `url` must use `ws://` or `wss://`.");
237
+ if (interaction.isContextMenuCommand()) {
238
+ return {
239
+ ...base,
240
+ kind: "contextMenu",
241
+ commandName: interaction.commandName
242
+ };
159
243
  }
160
- const isLoopbackHost = parsedUrl.hostname === "localhost" || parsedUrl.hostname === "127.0.0.1" || parsedUrl.hostname === "::1";
161
- if (parsedUrl.protocol === "ws:" && !isLoopbackHost && !options.allowInsecureWs) {
162
- throw new Error(
163
- "Insecure `ws://` is only allowed for loopback hosts by default. Use `wss://` or set `allowInsecureWs` to true."
164
- );
244
+ if (interaction.isButton()) {
245
+ const message = serializeInteractionMessage(interaction);
246
+ return {
247
+ ...base,
248
+ kind: "button",
249
+ customId: interaction.customId,
250
+ ...message ? { message } : {}
251
+ };
165
252
  }
166
- if (!isNonEmptyString(options.secret)) {
167
- throw new Error("Consumer mode requires `secret`.");
253
+ if (interaction.isStringSelectMenu()) {
254
+ const message = serializeInteractionMessage(interaction);
255
+ return {
256
+ ...base,
257
+ kind: "stringSelect",
258
+ ...serializeSelectMenu(interaction),
259
+ ...message ? { message } : {}
260
+ };
168
261
  }
169
- if (options.secretId !== void 0 && !isNonEmptyString(options.secretId)) {
170
- throw new Error("Consumer option `secretId` must be a non-empty string.");
262
+ if (interaction.isUserSelectMenu()) {
263
+ const message = serializeInteractionMessage(interaction);
264
+ return {
265
+ ...base,
266
+ kind: "userSelect",
267
+ ...serializeSelectMenu(interaction),
268
+ ...message ? { message } : {}
269
+ };
171
270
  }
172
- if (options.clientName !== void 0 && !isNonEmptyString(options.clientName)) {
173
- throw new Error("Consumer option `clientName` must be a non-empty string.");
271
+ if (interaction.isRoleSelectMenu()) {
272
+ const message = serializeInteractionMessage(interaction);
273
+ return {
274
+ ...base,
275
+ kind: "roleSelect",
276
+ ...serializeSelectMenu(interaction),
277
+ ...message ? { message } : {}
278
+ };
174
279
  }
175
- if (options.requestTimeoutMs !== void 0) {
176
- assertPositiveNumber("requestTimeoutMs", options.requestTimeoutMs);
280
+ if (interaction.isMentionableSelectMenu()) {
281
+ const message = serializeInteractionMessage(interaction);
282
+ return {
283
+ ...base,
284
+ kind: "mentionableSelect",
285
+ ...serializeSelectMenu(interaction),
286
+ ...message ? { message } : {}
287
+ };
177
288
  }
178
- if (options.reconnect?.initialDelayMs !== void 0) {
179
- assertPositiveNumber("reconnect.initialDelayMs", options.reconnect.initialDelayMs);
289
+ if (interaction.isChannelSelectMenu()) {
290
+ const message = serializeInteractionMessage(interaction);
291
+ return {
292
+ ...base,
293
+ kind: "channelSelect",
294
+ ...serializeSelectMenu(interaction),
295
+ ...message ? { message } : {}
296
+ };
180
297
  }
181
- if (options.reconnect?.maxDelayMs !== void 0) {
182
- assertPositiveNumber("reconnect.maxDelayMs", options.reconnect.maxDelayMs);
298
+ if (interaction.isModalSubmit()) {
299
+ return {
300
+ ...base,
301
+ kind: "modalSubmit",
302
+ customId: interaction.customId,
303
+ fields: serializeModalFields(interaction)
304
+ };
183
305
  }
306
+ return base;
184
307
  }
185
- function assertMessageName(kind, name) {
186
- if (!isNonEmptyString(name)) {
187
- throw new Error(`${kind} name must be a non-empty string.`);
188
- }
308
+
309
+ // src/discord/runtime/discordjs-adapter.ts
310
+ function isSendCapableChannel(channel) {
311
+ return Boolean(channel && typeof channel.send === "function");
189
312
  }
190
- function assertJsonPayload(kind, name, payload) {
191
- try {
192
- JSON.stringify(payload);
193
- } catch {
194
- throw new Error(`${kind} "${name}" payload must be JSON-serializable.`);
313
+ function isMessageManageableChannel(channel) {
314
+ if (!isSendCapableChannel(channel)) {
315
+ return false;
195
316
  }
317
+ const messages = channel.messages;
318
+ return Boolean(messages && typeof messages.fetch === "function");
196
319
  }
197
- function normalizeSchemaIssues(error) {
198
- if (!error || typeof error !== "object") {
199
- return void 0;
320
+ function toSendOptions(input) {
321
+ return {
322
+ ...input.content !== void 0 ? { content: input.content } : {},
323
+ ...input.embeds !== void 0 ? { embeds: input.embeds } : {},
324
+ ...input.allowedMentions !== void 0 ? { allowedMentions: input.allowedMentions } : {}
325
+ };
326
+ }
327
+ var DiscordJsRuntimeAdapter = class {
328
+ constructor(options) {
329
+ this.options = options;
330
+ const intentBits = options.intents.map((intent) => BOT_INTENT_BITS[intent]);
331
+ this.logger = withLogger(options.logger);
332
+ this.client = new import_discord2.Client({
333
+ intents: intentBits
334
+ });
335
+ this.client.once("ready", () => {
336
+ this.hasReady = true;
337
+ });
200
338
  }
201
- const issues = error.issues;
202
- if (!Array.isArray(issues)) {
203
- return void 0;
339
+ options;
340
+ client;
341
+ logger;
342
+ interactionCache = new DedupeCache(15 * 60 * 1e3);
343
+ readyPromise = null;
344
+ hasReady = false;
345
+ isReady() {
346
+ return this.hasReady && this.client.isReady();
204
347
  }
205
- const normalized = issues.map((issue) => {
206
- if (!issue || typeof issue !== "object") {
207
- return null;
348
+ ready() {
349
+ if (this.readyPromise) {
350
+ return this.readyPromise;
208
351
  }
209
- const message = issue.message;
210
- const rawPath = issue.path;
211
- if (typeof message !== "string") {
212
- return null;
352
+ if (this.isReady()) {
353
+ return Promise.resolve();
213
354
  }
214
- const path = Array.isArray(rawPath) && rawPath.length > 0 ? rawPath.map((segment) => typeof segment === "string" || typeof segment === "number" ? String(segment) : "").filter(Boolean).join(".") : "";
215
- return { path, message };
216
- }).filter((issue) => Boolean(issue));
217
- return normalized.length > 0 ? normalized : void 0;
218
- }
219
- function parsePayloadWithSchema(schema, payload, context) {
220
- if (!schema) {
221
- return payload;
222
- }
223
- try {
224
- return schema.parse(payload);
225
- } catch (error) {
226
- const message = error instanceof Error && error.message.trim().length > 0 ? error.message : `Payload validation failed for ${context.stage} "${context.name}".`;
227
- const issues = normalizeSchemaIssues(error);
228
- throw new PayloadValidationError(message, {
229
- ...context,
230
- ...issues ? { issues } : {}
355
+ this.readyPromise = new Promise((resolve, reject) => {
356
+ const onReady = () => {
357
+ cleanup();
358
+ this.hasReady = true;
359
+ resolve();
360
+ };
361
+ const onError = (error) => {
362
+ cleanup();
363
+ reject(error instanceof Error ? error : new Error(String(error)));
364
+ };
365
+ const cleanup = () => {
366
+ this.client.off("ready", onReady);
367
+ this.client.off("error", onError);
368
+ };
369
+ this.client.once("ready", onReady);
370
+ this.client.once("error", onError);
371
+ void this.client.login(this.options.token).catch((error) => {
372
+ cleanup();
373
+ reject(error instanceof Error ? error : new Error(String(error)));
374
+ });
231
375
  });
376
+ return this.readyPromise;
232
377
  }
233
- }
234
-
235
- // src/consumer/index.ts
236
- var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
237
- var CommandRequestError = class extends Error {
238
- constructor(code, message) {
239
- super(message);
240
- this.code = code;
241
- this.name = "CommandRequestError";
378
+ async close() {
379
+ this.client.removeAllListeners();
380
+ await this.client.destroy();
381
+ this.readyPromise = null;
382
+ this.hasReady = false;
242
383
  }
243
- code;
244
- };
245
- function createConsumerShardwire(options) {
246
- const logger = withLogger(options.logger);
247
- const reconnectEnabled = options.reconnect?.enabled ?? true;
248
- const initialDelayMs = options.reconnect?.initialDelayMs ?? 500;
249
- const maxDelayMs = options.reconnect?.maxDelayMs ?? 1e4;
250
- const jitter = options.reconnect?.jitter ?? true;
251
- const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
252
- let socket = null;
253
- let isClosed = false;
254
- let isAuthed = false;
255
- let reconnectAttempts = 0;
256
- let reconnectTimer = null;
257
- let connectPromise = null;
258
- let connectResolve = null;
259
- let connectReject = null;
260
- let authTimeoutTimer = null;
261
- let currentConnectionId = null;
262
- const pendingRequests = /* @__PURE__ */ new Map();
263
- const eventHandlers = /* @__PURE__ */ new Map();
264
- const connectedHandlers = /* @__PURE__ */ new Set();
265
- const disconnectedHandlers = /* @__PURE__ */ new Set();
266
- const reconnectingHandlers = /* @__PURE__ */ new Set();
267
- function clearAuthTimeout() {
268
- if (authTimeoutTimer) {
269
- clearTimeout(authTimeoutTimer);
270
- authTimeoutTimer = null;
384
+ on(name, handler) {
385
+ switch (name) {
386
+ case "ready": {
387
+ const listener = () => {
388
+ if (!this.client.user) {
389
+ return;
390
+ }
391
+ handler({
392
+ receivedAt: Date.now(),
393
+ user: serializeUser(this.client.user)
394
+ });
395
+ };
396
+ this.client.on("ready", listener);
397
+ if (this.client.isReady() && this.client.user) {
398
+ listener();
399
+ }
400
+ return () => {
401
+ this.client.off("ready", listener);
402
+ };
403
+ }
404
+ case "interactionCreate": {
405
+ const listener = (interaction) => {
406
+ if (isReplyCapableInteraction(interaction)) {
407
+ this.interactionCache.set(interaction.id, interaction);
408
+ }
409
+ handler({
410
+ receivedAt: Date.now(),
411
+ interaction: serializeInteraction(interaction)
412
+ });
413
+ };
414
+ this.client.on("interactionCreate", listener);
415
+ return () => {
416
+ this.client.off("interactionCreate", listener);
417
+ };
418
+ }
419
+ case "messageCreate": {
420
+ const listener = (message) => {
421
+ handler({
422
+ receivedAt: Date.now(),
423
+ message: serializeMessage(message)
424
+ });
425
+ };
426
+ this.client.on("messageCreate", listener);
427
+ return () => {
428
+ this.client.off("messageCreate", listener);
429
+ };
430
+ }
431
+ case "messageUpdate": {
432
+ const listener = (oldMessage, newMessage) => {
433
+ handler({
434
+ receivedAt: Date.now(),
435
+ oldMessage: serializeMessage(oldMessage),
436
+ message: serializeMessage(newMessage)
437
+ });
438
+ };
439
+ this.client.on("messageUpdate", listener);
440
+ return () => {
441
+ this.client.off("messageUpdate", listener);
442
+ };
443
+ }
444
+ case "messageDelete": {
445
+ const listener = (message) => {
446
+ handler({
447
+ receivedAt: Date.now(),
448
+ message: serializeDeletedMessage(message)
449
+ });
450
+ };
451
+ this.client.on("messageDelete", listener);
452
+ return () => {
453
+ this.client.off("messageDelete", listener);
454
+ };
455
+ }
456
+ case "guildMemberAdd": {
457
+ const listener = (member) => {
458
+ handler({
459
+ receivedAt: Date.now(),
460
+ member: serializeGuildMember(member)
461
+ });
462
+ };
463
+ this.client.on("guildMemberAdd", listener);
464
+ return () => {
465
+ this.client.off("guildMemberAdd", listener);
466
+ };
467
+ }
468
+ case "guildMemberRemove": {
469
+ const listener = (member) => {
470
+ handler({
471
+ receivedAt: Date.now(),
472
+ member: serializeGuildMember(member)
473
+ });
474
+ };
475
+ this.client.on("guildMemberRemove", listener);
476
+ return () => {
477
+ this.client.off("guildMemberRemove", listener);
478
+ };
479
+ }
480
+ default:
481
+ return () => void 0;
271
482
  }
272
483
  }
273
- function resolveConnect() {
274
- clearAuthTimeout();
275
- connectResolve?.();
276
- connectResolve = null;
277
- connectReject = null;
278
- connectPromise = null;
484
+ async executeAction(name, payload) {
485
+ await this.ready();
486
+ try {
487
+ switch (name) {
488
+ case "sendMessage":
489
+ return await this.sendMessage(payload);
490
+ case "editMessage":
491
+ return await this.editMessage(payload);
492
+ case "deleteMessage":
493
+ return await this.deleteMessage(payload);
494
+ case "replyToInteraction":
495
+ return await this.replyToInteraction(payload);
496
+ case "deferInteraction":
497
+ return await this.deferInteraction(payload);
498
+ case "followUpInteraction":
499
+ return await this.followUpInteraction(payload);
500
+ case "banMember":
501
+ return await this.banMember(payload);
502
+ case "kickMember":
503
+ return await this.kickMember(payload);
504
+ case "addMemberRole":
505
+ return await this.addMemberRole(payload);
506
+ case "removeMemberRole":
507
+ return await this.removeMemberRole(payload);
508
+ default:
509
+ throw new ActionExecutionError("INVALID_REQUEST", `Unsupported action "${String(name)}".`);
510
+ }
511
+ } catch (error) {
512
+ if (error instanceof ActionExecutionError) {
513
+ throw error;
514
+ }
515
+ this.logger.error("Discord action execution failed.", { action: name, error: String(error) });
516
+ throw new ActionExecutionError("INTERNAL_ERROR", error instanceof Error ? error.message : "Discord action failed.");
517
+ }
279
518
  }
280
- function rejectConnect(message) {
281
- clearAuthTimeout();
282
- if (connectReject) {
283
- connectReject(new Error(message));
519
+ async fetchSendableChannel(channelId) {
520
+ const channel = await this.client.channels.fetch(channelId);
521
+ if (!isSendCapableChannel(channel)) {
522
+ throw new ActionExecutionError("NOT_FOUND", `Channel "${channelId}" cannot send messages.`);
284
523
  }
285
- connectResolve = null;
286
- connectReject = null;
287
- connectPromise = null;
524
+ return channel;
288
525
  }
289
- function sendRaw(data) {
290
- if (!socket || socket.readyState !== 1) {
291
- throw new Error("Shardwire consumer is not connected.");
526
+ async fetchMessageChannel(channelId) {
527
+ const channel = await this.client.channels.fetch(channelId);
528
+ if (!isMessageManageableChannel(channel)) {
529
+ throw new ActionExecutionError("NOT_FOUND", `Channel "${channelId}" does not support message operations.`);
292
530
  }
293
- socket.send(data);
531
+ return channel;
294
532
  }
295
- function rejectAllPending(code, reason) {
296
- for (const [requestId, pending] of pendingRequests.entries()) {
297
- clearTimeout(pending.timer);
298
- pending.reject(new CommandRequestError(code, reason));
299
- pendingRequests.delete(requestId);
533
+ getInteraction(interactionId) {
534
+ const interaction = this.interactionCache.get(interactionId);
535
+ if (!interaction) {
536
+ throw new ActionExecutionError("NOT_FOUND", `Interaction "${interactionId}" is no longer available.`);
300
537
  }
538
+ return interaction;
301
539
  }
302
- function scheduleReconnect() {
303
- if (isClosed || !reconnectEnabled || reconnectTimer) {
304
- return;
540
+ async sendMessage(payload) {
541
+ const channel = await this.fetchSendableChannel(payload.channelId);
542
+ const message = await channel.send(toSendOptions(payload));
543
+ return serializeMessage(message);
544
+ }
545
+ async editMessage(payload) {
546
+ const channel = await this.fetchMessageChannel(payload.channelId);
547
+ const message = await channel.messages.fetch(payload.messageId);
548
+ const edited = await message.edit(toSendOptions(payload));
549
+ return serializeMessage(edited);
550
+ }
551
+ async deleteMessage(payload) {
552
+ const channel = await this.fetchMessageChannel(payload.channelId);
553
+ const message = await channel.messages.fetch(payload.messageId);
554
+ await message.delete();
555
+ return {
556
+ deleted: true,
557
+ channelId: payload.channelId,
558
+ messageId: payload.messageId
559
+ };
560
+ }
561
+ async replyToInteraction(payload) {
562
+ const interaction = this.getInteraction(payload.interactionId);
563
+ if (interaction.replied || interaction.deferred) {
564
+ throw new ActionExecutionError("INVALID_REQUEST", `Interaction "${payload.interactionId}" has already been acknowledged.`);
305
565
  }
306
- const delay = getBackoffDelay(reconnectAttempts, { initialDelayMs, maxDelayMs, jitter });
307
- reconnectAttempts += 1;
308
- for (const handler of reconnectingHandlers) {
309
- try {
310
- handler({ attempt: reconnectAttempts, delayMs: delay, at: Date.now() });
311
- } catch (error) {
312
- logger.warn("Reconnect handler threw an error.", { error: String(error) });
313
- }
566
+ const reply = await interaction.reply({
567
+ ...toSendOptions(payload),
568
+ fetchReply: true,
569
+ ...payload.ephemeral ? { flags: import_discord2.MessageFlags.Ephemeral } : {}
570
+ });
571
+ return serializeMessage(reply);
572
+ }
573
+ async deferInteraction(payload) {
574
+ const interaction = this.getInteraction(payload.interactionId);
575
+ if (interaction.replied) {
576
+ throw new ActionExecutionError("INVALID_REQUEST", `Interaction "${payload.interactionId}" has already been replied to.`);
314
577
  }
315
- reconnectTimer = setTimeout(() => {
316
- reconnectTimer = null;
317
- void connect().catch((error) => {
318
- logger.warn("Reconnect attempt failed.", { error: String(error) });
578
+ if (!interaction.deferred) {
579
+ await interaction.deferReply({
580
+ ...payload.ephemeral ? { flags: import_discord2.MessageFlags.Ephemeral } : {}
319
581
  });
320
- }, delay);
321
- }
322
- function handleEvent(name, payload, meta) {
323
- const handlers = eventHandlers.get(name);
324
- if (!handlers || handlers.size === 0) {
325
- return;
326
582
  }
327
- for (const handler of handlers) {
328
- try {
329
- handler(payload, meta);
330
- } catch (error) {
331
- logger.warn("Event handler threw an error.", { name, error: String(error) });
332
- }
583
+ return {
584
+ deferred: true,
585
+ interactionId: payload.interactionId
586
+ };
587
+ }
588
+ async followUpInteraction(payload) {
589
+ const interaction = this.getInteraction(payload.interactionId);
590
+ if (!interaction.replied && !interaction.deferred) {
591
+ throw new ActionExecutionError("INVALID_REQUEST", `Interaction "${payload.interactionId}" has not been acknowledged yet.`);
333
592
  }
593
+ const followUp = await interaction.followUp({
594
+ ...toSendOptions(payload),
595
+ ...payload.ephemeral ? { flags: import_discord2.MessageFlags.Ephemeral } : {}
596
+ });
597
+ return serializeMessage(followUp);
334
598
  }
335
- async function connect() {
336
- if (isClosed) {
337
- return;
599
+ async banMember(payload) {
600
+ const guild = await this.client.guilds.fetch(payload.guildId);
601
+ await guild.members.ban(payload.userId, {
602
+ ...payload.reason ? { reason: payload.reason } : {},
603
+ ...payload.deleteMessageSeconds !== void 0 ? { deleteMessageSeconds: payload.deleteMessageSeconds } : {}
604
+ });
605
+ return {
606
+ guildId: payload.guildId,
607
+ userId: payload.userId
608
+ };
609
+ }
610
+ async kickMember(payload) {
611
+ const guild = await this.client.guilds.fetch(payload.guildId);
612
+ const member = await guild.members.fetch(payload.userId);
613
+ await member.kick(payload.reason);
614
+ return {
615
+ guildId: payload.guildId,
616
+ userId: payload.userId
617
+ };
618
+ }
619
+ async addMemberRole(payload) {
620
+ const guild = await this.client.guilds.fetch(payload.guildId);
621
+ const member = await guild.members.fetch(payload.userId);
622
+ await member.roles.add(payload.roleId, payload.reason);
623
+ const refreshed = await member.fetch();
624
+ return serializeGuildMember(refreshed);
625
+ }
626
+ async removeMemberRole(payload) {
627
+ const guild = await this.client.guilds.fetch(payload.guildId);
628
+ const member = await guild.members.fetch(payload.userId);
629
+ await member.roles.remove(payload.roleId, payload.reason);
630
+ const refreshed = await member.fetch();
631
+ return serializeGuildMember(refreshed);
632
+ }
633
+ };
634
+ function createDiscordJsRuntimeAdapter(options) {
635
+ return new DiscordJsRuntimeAdapter(options);
636
+ }
637
+ function isReplyCapableInteraction(interaction) {
638
+ return interaction.isRepliable() && typeof interaction.reply === "function" && typeof interaction.deferReply === "function" && typeof interaction.followUp === "function";
639
+ }
640
+
641
+ // src/bridge/transport/server.ts
642
+ var import_ws = require("ws");
643
+
644
+ // src/utils/id.ts
645
+ var import_node_crypto = require("crypto");
646
+ function createRequestId() {
647
+ return (0, import_node_crypto.randomUUID)();
648
+ }
649
+ function createConnectionId() {
650
+ return (0, import_node_crypto.randomUUID)();
651
+ }
652
+
653
+ // src/bridge/subscriptions.ts
654
+ function normalizeStringList(value) {
655
+ if (value === void 0) {
656
+ return void 0;
657
+ }
658
+ const rawValues = Array.isArray(value) ? value : [value];
659
+ const normalized = [...new Set(rawValues.filter((entry) => typeof entry === "string" && entry.length > 0))].sort();
660
+ return normalized.length > 0 ? normalized : void 0;
661
+ }
662
+ function normalizeEventSubscriptionFilter(filter) {
663
+ if (!filter) {
664
+ return void 0;
665
+ }
666
+ const normalized = {};
667
+ const guildIds = normalizeStringList(filter.guildId);
668
+ const channelIds = normalizeStringList(filter.channelId);
669
+ const userIds = normalizeStringList(filter.userId);
670
+ const commandNames = normalizeStringList(filter.commandName);
671
+ if (guildIds) {
672
+ normalized.guildId = guildIds;
673
+ }
674
+ if (channelIds) {
675
+ normalized.channelId = channelIds;
676
+ }
677
+ if (userIds) {
678
+ normalized.userId = userIds;
679
+ }
680
+ if (commandNames) {
681
+ normalized.commandName = commandNames;
682
+ }
683
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
684
+ }
685
+ function normalizeEventSubscription(subscription) {
686
+ const normalizedFilter = normalizeEventSubscriptionFilter(subscription.filter);
687
+ return {
688
+ name: subscription.name,
689
+ ...normalizedFilter ? { filter: normalizedFilter } : {}
690
+ };
691
+ }
692
+ function serializeEventSubscription(subscription) {
693
+ return JSON.stringify(normalizeEventSubscription(subscription));
694
+ }
695
+ function matchesField(value, allowed) {
696
+ if (!allowed) {
697
+ return true;
698
+ }
699
+ if (!value) {
700
+ return false;
701
+ }
702
+ return allowed.includes(value);
703
+ }
704
+ function eventMetadata(name, payload) {
705
+ switch (name) {
706
+ case "ready":
707
+ return {};
708
+ case "interactionCreate": {
709
+ const interactionPayload = payload;
710
+ return {
711
+ ...interactionPayload.interaction.guildId ? { guildId: interactionPayload.interaction.guildId } : {},
712
+ ...interactionPayload.interaction.channelId ? { channelId: interactionPayload.interaction.channelId } : {},
713
+ userId: interactionPayload.interaction.user.id,
714
+ ...interactionPayload.interaction.commandName ? { commandName: interactionPayload.interaction.commandName } : {}
715
+ };
338
716
  }
339
- if (socket && socket.readyState === 1 && isAuthed) {
340
- return;
717
+ case "messageCreate": {
718
+ const messagePayload = payload;
719
+ return {
720
+ ...messagePayload.message.guildId ? { guildId: messagePayload.message.guildId } : {},
721
+ channelId: messagePayload.message.channelId,
722
+ ...messagePayload.message.author ? { userId: messagePayload.message.author.id } : {}
723
+ };
341
724
  }
342
- if (connectPromise) {
343
- return connectPromise;
725
+ case "messageUpdate": {
726
+ const messagePayload = payload;
727
+ return {
728
+ ...messagePayload.message.guildId ? { guildId: messagePayload.message.guildId } : {},
729
+ channelId: messagePayload.message.channelId,
730
+ ...messagePayload.message.author ? { userId: messagePayload.message.author.id } : {}
731
+ };
344
732
  }
345
- connectPromise = new Promise((resolve, reject) => {
346
- connectResolve = resolve;
347
- connectReject = reject;
733
+ case "messageDelete": {
734
+ const messagePayload = payload;
735
+ return {
736
+ ...messagePayload.message.guildId ? { guildId: messagePayload.message.guildId } : {},
737
+ channelId: messagePayload.message.channelId
738
+ };
739
+ }
740
+ case "guildMemberAdd": {
741
+ const memberPayload = payload;
742
+ return {
743
+ guildId: memberPayload.member.guildId,
744
+ userId: memberPayload.member.id
745
+ };
746
+ }
747
+ case "guildMemberRemove": {
748
+ const memberPayload = payload;
749
+ return {
750
+ guildId: memberPayload.member.guildId,
751
+ userId: memberPayload.member.id
752
+ };
753
+ }
754
+ default:
755
+ return {};
756
+ }
757
+ }
758
+ function matchesEventSubscription(subscription, payload) {
759
+ const normalized = normalizeEventSubscription(subscription);
760
+ if (!normalized.filter) {
761
+ return true;
762
+ }
763
+ const metadata = eventMetadata(normalized.name, payload);
764
+ return matchesField(metadata.guildId, normalized.filter.guildId) && matchesField(metadata.channelId, normalized.filter.channelId) && matchesField(metadata.userId, normalized.filter.userId) && matchesField(metadata.commandName, normalized.filter.commandName);
765
+ }
766
+
767
+ // src/bridge/transport/security.ts
768
+ var import_node_crypto2 = require("crypto");
769
+ function isSecretValid(provided, expected) {
770
+ const providedBuffer = Buffer.from(provided);
771
+ const expectedBuffer = Buffer.from(expected);
772
+ if (providedBuffer.length !== expectedBuffer.length) {
773
+ return false;
774
+ }
775
+ return (0, import_node_crypto2.timingSafeEqual)(providedBuffer, expectedBuffer);
776
+ }
777
+
778
+ // src/bridge/transport/protocol.ts
779
+ var PROTOCOL_VERSION = 2;
780
+ function makeEnvelope(type, payload, extras) {
781
+ return {
782
+ v: PROTOCOL_VERSION,
783
+ type,
784
+ ts: Date.now(),
785
+ ...extras?.requestId ? { requestId: extras.requestId } : {},
786
+ payload
787
+ };
788
+ }
789
+ function parseEnvelope(raw) {
790
+ const parsed = JSON.parse(raw);
791
+ if (!parsed || parsed.v !== PROTOCOL_VERSION || typeof parsed.type !== "string") {
792
+ throw new Error("Invalid bridge envelope.");
793
+ }
794
+ return parsed;
795
+ }
796
+ function stringifyEnvelope(envelope) {
797
+ return JSON.stringify(envelope);
798
+ }
799
+
800
+ // src/bridge/transport/server.ts
801
+ var CLOSE_AUTH_REQUIRED = 4001;
802
+ var CLOSE_AUTH_FAILED = 4003;
803
+ var CLOSE_INVALID_PAYLOAD = 4004;
804
+ var BridgeTransportServer = class {
805
+ constructor(config) {
806
+ this.config = config;
807
+ this.logger = withLogger(config.logger);
808
+ this.heartbeatMs = config.options.server.heartbeatMs ?? 3e4;
809
+ this.wss = new import_ws.WebSocketServer({
810
+ host: config.options.server.host,
811
+ port: config.options.server.port,
812
+ path: config.options.server.path ?? "/shardwire",
813
+ maxPayload: config.options.server.maxPayloadBytes ?? 65536
348
814
  });
349
- socket = options.webSocketFactory ? options.webSocketFactory(options.url) : createNodeWebSocket(options.url);
350
- socket.on("open", () => {
351
- reconnectAttempts = 0;
352
- isAuthed = false;
353
- currentConnectionId = null;
354
- const hello = makeEnvelope("auth.hello", {
355
- secret: options.secret,
356
- secretId: options.secretId,
357
- clientName: options.clientName
815
+ this.wss.on("connection", (socket) => this.handleConnection(socket));
816
+ this.wss.on("error", (error) => this.logger.error("Bridge transport server error.", { error: String(error) }));
817
+ this.interval = setInterval(() => {
818
+ this.checkHeartbeats();
819
+ }, this.heartbeatMs);
820
+ }
821
+ config;
822
+ wss;
823
+ logger;
824
+ heartbeatMs;
825
+ authTimeoutMs = 5e3;
826
+ interval;
827
+ connections = /* @__PURE__ */ new Map();
828
+ stickyEvents = /* @__PURE__ */ new Map();
829
+ connectionCount() {
830
+ let count = 0;
831
+ for (const state of this.connections.values()) {
832
+ if (state.authenticated) {
833
+ count += 1;
834
+ }
835
+ }
836
+ return count;
837
+ }
838
+ setStickyEvent(name, payload) {
839
+ this.stickyEvents.set(name, payload);
840
+ }
841
+ publishEvent(name, payload) {
842
+ const envelope = stringifyEnvelope(makeEnvelope("discord.event", { name, data: payload }));
843
+ for (const state of this.connections.values()) {
844
+ if (!state.authenticated) {
845
+ continue;
846
+ }
847
+ const shouldReceive = [...state.subscriptions.values()].some((subscription) => {
848
+ return subscription.name === name && matchesEventSubscription(subscription, payload);
358
849
  });
359
- socket?.send(stringifyEnvelope(hello));
360
- authTimeoutTimer = setTimeout(() => {
361
- if (!isAuthed) {
362
- rejectConnect("Shardwire auth timed out.");
363
- socket?.close();
850
+ if (!shouldReceive) {
851
+ continue;
852
+ }
853
+ this.safeSend(state.socket, envelope);
854
+ }
855
+ }
856
+ async close() {
857
+ clearInterval(this.interval);
858
+ for (const state of this.connections.values()) {
859
+ state.socket.close();
860
+ }
861
+ this.connections.clear();
862
+ await new Promise((resolve, reject) => {
863
+ this.wss.close((error) => {
864
+ if (error) {
865
+ reject(error);
866
+ return;
364
867
  }
365
- }, requestTimeoutMs);
868
+ resolve();
869
+ });
366
870
  });
871
+ }
872
+ handleConnection(socket) {
873
+ const state = {
874
+ id: createConnectionId(),
875
+ socket,
876
+ authenticated: false,
877
+ lastHeartbeatAt: Date.now(),
878
+ subscriptions: /* @__PURE__ */ new Map()
879
+ };
880
+ this.connections.set(socket, state);
881
+ const authTimer = setTimeout(() => {
882
+ if (!state.authenticated) {
883
+ socket.close(CLOSE_AUTH_REQUIRED, "Authentication required.");
884
+ }
885
+ }, this.authTimeoutMs);
367
886
  socket.on("message", (raw) => {
887
+ const serialized = typeof raw === "string" ? raw : Buffer.isBuffer(raw) ? raw.toString("utf8") : void 0;
888
+ if (!serialized) {
889
+ socket.close(CLOSE_INVALID_PAYLOAD, "Invalid payload.");
890
+ return;
891
+ }
892
+ let envelope;
368
893
  try {
369
- const serialized = typeof raw === "string" ? raw : String(raw);
370
- const envelope = parseEnvelope(serialized);
371
- switch (envelope.type) {
372
- case "auth.ok":
373
- isAuthed = true;
374
- if (envelope.payload && typeof envelope.payload === "object" && "connectionId" in envelope.payload && typeof envelope.payload.connectionId === "string") {
375
- currentConnectionId = envelope.payload.connectionId;
376
- }
377
- if (currentConnectionId) {
378
- for (const handler of connectedHandlers) {
379
- try {
380
- handler({ connectionId: currentConnectionId, connectedAt: Date.now() });
381
- } catch (error) {
382
- logger.warn("Connected handler threw an error.", { error: String(error) });
383
- }
384
- }
385
- }
386
- resolveConnect();
387
- break;
388
- case "auth.error": {
389
- const payload = envelope.payload;
390
- logger.error("Authentication failed for consumer.", {
391
- code: payload.code,
392
- message: payload.message
393
- });
394
- rejectConnect(payload.message);
395
- rejectAllPending("UNAUTHORIZED", "Shardwire authentication failed.");
396
- socket?.close();
397
- break;
398
- }
399
- case "command.result":
400
- case "command.error": {
401
- const requestId = envelope.requestId;
402
- if (!requestId) {
403
- return;
404
- }
405
- const pending = pendingRequests.get(requestId);
406
- if (!pending) {
407
- return;
408
- }
409
- clearTimeout(pending.timer);
410
- pending.resolve(envelope.payload);
411
- pendingRequests.delete(requestId);
412
- break;
413
- }
414
- case "event.emit": {
415
- const payload = envelope.payload;
416
- const meta = { ts: envelope.ts };
417
- if (envelope.source) {
418
- meta.source = envelope.source;
419
- }
420
- handleEvent(payload.name, payload.data, meta);
421
- break;
422
- }
423
- case "ping":
424
- sendRaw(stringifyEnvelope(makeEnvelope("pong", {})));
425
- break;
426
- default:
427
- break;
428
- }
894
+ envelope = parseEnvelope(serialized);
429
895
  } catch (error) {
430
- logger.warn("Failed to parse consumer message.", { error: String(error) });
896
+ this.logger.warn("Invalid transport envelope from app.", { error: String(error) });
897
+ socket.close(CLOSE_INVALID_PAYLOAD, "Invalid payload.");
898
+ return;
431
899
  }
900
+ void this.handleMessage(state, envelope).catch((error) => {
901
+ this.logger.warn("Failed to handle transport message.", { error: String(error) });
902
+ socket.close(CLOSE_INVALID_PAYLOAD, "Invalid payload.");
903
+ });
432
904
  });
433
905
  socket.on("close", () => {
434
- const willReconnect = !isClosed && reconnectEnabled;
435
- rejectConnect("Shardwire connection closed.");
436
- isAuthed = false;
437
- currentConnectionId = null;
438
- rejectAllPending("DISCONNECTED", "Shardwire connection closed before command completed.");
439
- for (const handler of disconnectedHandlers) {
440
- try {
441
- handler({
442
- reason: "Shardwire connection closed.",
443
- at: Date.now(),
444
- willReconnect
445
- });
446
- } catch (error) {
447
- logger.warn("Disconnected handler threw an error.", { error: String(error) });
448
- }
449
- }
450
- if (!isClosed) {
451
- scheduleReconnect();
452
- }
906
+ clearTimeout(authTimer);
907
+ this.connections.delete(socket);
453
908
  });
454
909
  socket.on("error", (error) => {
455
- logger.warn("Consumer socket error.", { error: String(error) });
910
+ this.logger.warn("Transport socket error.", { connectionId: state.id, error: String(error) });
456
911
  });
457
- return connectPromise;
458
912
  }
459
- void connect().catch((error) => {
460
- logger.warn("Initial connection attempt failed.", { error: String(error) });
461
- });
462
- return {
463
- mode: "consumer",
464
- async send(name, payload, sendOptions) {
465
- assertMessageName("command", name);
466
- assertJsonPayload("command", name, payload);
467
- if (!isAuthed) {
468
- try {
469
- await connect();
470
- } catch (error) {
471
- return {
472
- ok: false,
473
- requestId: sendOptions?.requestId ?? "unknown",
474
- ts: Date.now(),
475
- error: {
913
+ async handleMessage(state, envelope) {
914
+ if (envelope.type === "ping") {
915
+ state.lastHeartbeatAt = Date.now();
916
+ this.safeSend(state.socket, stringifyEnvelope(makeEnvelope("pong", {})));
917
+ return;
918
+ }
919
+ if (envelope.type === "pong") {
920
+ state.lastHeartbeatAt = Date.now();
921
+ return;
922
+ }
923
+ if (!state.authenticated) {
924
+ if (envelope.type !== "auth.hello") {
925
+ state.socket.close(CLOSE_AUTH_REQUIRED, "Authentication required.");
926
+ return;
927
+ }
928
+ const authResult = this.config.authenticate(envelope.payload);
929
+ if (!authResult.ok) {
930
+ const message = authResult.reason === "ambiguous_secret" ? "Authentication failed: secret matches multiple configured scopes. Supply secretId or use unique secret values." : "Authentication failed.";
931
+ this.safeSend(
932
+ state.socket,
933
+ stringifyEnvelope(
934
+ makeEnvelope("auth.error", {
476
935
  code: "UNAUTHORIZED",
477
- message: error instanceof Error ? error.message : "Failed to authenticate."
478
- }
479
- };
480
- }
936
+ reason: authResult.reason,
937
+ message
938
+ })
939
+ )
940
+ );
941
+ state.socket.close(CLOSE_AUTH_FAILED, "Invalid secret.");
942
+ return;
481
943
  }
482
- if (!socket || socket.readyState !== 1) {
483
- return {
484
- ok: false,
485
- requestId: sendOptions?.requestId ?? "unknown",
486
- ts: Date.now(),
487
- error: {
488
- code: "DISCONNECTED",
489
- message: "Not connected to Shardwire host."
490
- }
491
- };
944
+ state.authenticated = true;
945
+ state.lastHeartbeatAt = Date.now();
946
+ state.secret = authResult.secret;
947
+ state.capabilities = authResult.capabilities;
948
+ const payload = envelope.payload;
949
+ if (payload.appName) {
950
+ state.appName = payload.appName;
492
951
  }
493
- const requestId = sendOptions?.requestId ?? createRequestId();
494
- const timeoutMs = sendOptions?.timeoutMs ?? requestTimeoutMs;
495
- const promise = new Promise((resolve, reject) => {
496
- const timer = setTimeout(() => {
497
- pendingRequests.delete(requestId);
498
- reject(new CommandRequestError("TIMEOUT", `Command "${name}" timed out after ${timeoutMs}ms.`));
499
- }, timeoutMs);
500
- pendingRequests.set(requestId, {
501
- resolve,
502
- reject: (error) => reject(error),
503
- timer
504
- });
505
- });
506
- sendRaw(
952
+ this.safeSend(
953
+ state.socket,
507
954
  stringifyEnvelope(
508
- makeEnvelope(
509
- "command.request",
510
- {
511
- name,
512
- data: payload
513
- },
514
- { requestId }
515
- )
955
+ makeEnvelope("auth.ok", {
956
+ connectionId: state.id,
957
+ capabilities: authResult.capabilities
958
+ })
516
959
  )
517
960
  );
518
- try {
519
- return await promise;
520
- } catch (error) {
521
- const failureCode = error instanceof CommandRequestError ? error.code : !socket || socket.readyState !== 1 ? "DISCONNECTED" : "TIMEOUT";
522
- return {
523
- ok: false,
524
- requestId,
525
- ts: Date.now(),
526
- error: {
527
- code: failureCode,
528
- message: error instanceof Error ? error.message : "Command request failed."
961
+ return;
962
+ }
963
+ if (!state.secret || !state.capabilities) {
964
+ state.socket.close(CLOSE_AUTH_FAILED, "Invalid state.");
965
+ return;
966
+ }
967
+ switch (envelope.type) {
968
+ case "subscribe": {
969
+ const rawSubscriptions = envelope.payload.subscriptions;
970
+ if (!Array.isArray(rawSubscriptions)) {
971
+ return;
972
+ }
973
+ const allowedEvents = new Set(state.capabilities.events);
974
+ for (const rawSubscription of rawSubscriptions) {
975
+ if (!rawSubscription || typeof rawSubscription !== "object" || typeof rawSubscription.name !== "string") {
976
+ continue;
529
977
  }
530
- };
531
- }
532
- },
533
- on(name, handler) {
534
- assertMessageName("event", name);
535
- const casted = handler;
536
- const existing = eventHandlers.get(name);
537
- if (existing) {
538
- existing.add(casted);
539
- } else {
540
- eventHandlers.set(name, /* @__PURE__ */ new Set([casted]));
978
+ const typedEvent = rawSubscription.name;
979
+ if (!allowedEvents.has(typedEvent)) {
980
+ continue;
981
+ }
982
+ const signature = serializeEventSubscription(rawSubscription);
983
+ state.subscriptions.set(signature, rawSubscription);
984
+ const stickyPayload = this.stickyEvents.get(typedEvent);
985
+ if (stickyPayload && matchesEventSubscription(rawSubscription, stickyPayload)) {
986
+ this.safeSend(
987
+ state.socket,
988
+ stringifyEnvelope(makeEnvelope("discord.event", { name: typedEvent, data: stickyPayload }))
989
+ );
990
+ }
991
+ }
992
+ return;
541
993
  }
542
- return () => {
543
- const handlers = eventHandlers.get(name);
544
- if (!handlers) {
994
+ case "unsubscribe": {
995
+ const rawSubscriptions = envelope.payload.subscriptions;
996
+ if (!Array.isArray(rawSubscriptions)) {
545
997
  return;
546
998
  }
547
- handlers.delete(casted);
548
- if (handlers.size === 0) {
549
- eventHandlers.delete(name);
999
+ for (const rawSubscription of rawSubscriptions) {
1000
+ if (!rawSubscription || typeof rawSubscription !== "object" || typeof rawSubscription.name !== "string") {
1001
+ continue;
1002
+ }
1003
+ state.subscriptions.delete(serializeEventSubscription(rawSubscription));
550
1004
  }
551
- };
552
- },
553
- off(name, handler) {
554
- assertMessageName("event", name);
555
- const handlers = eventHandlers.get(name);
556
- if (!handlers) {
557
1005
  return;
558
1006
  }
559
- handlers.delete(handler);
560
- if (handlers.size === 0) {
561
- eventHandlers.delete(name);
562
- }
563
- },
564
- onConnected(handler) {
565
- connectedHandlers.add(handler);
566
- return () => {
567
- connectedHandlers.delete(handler);
568
- };
569
- },
570
- onDisconnected(handler) {
571
- disconnectedHandlers.add(handler);
572
- return () => {
573
- disconnectedHandlers.delete(handler);
574
- };
575
- },
576
- onReconnecting(handler) {
577
- reconnectingHandlers.add(handler);
578
- return () => {
579
- reconnectingHandlers.delete(handler);
580
- };
581
- },
582
- ready() {
583
- return connect();
584
- },
585
- connected() {
586
- return Boolean(socket && socket.readyState === 1 && isAuthed);
587
- },
588
- connectionId() {
589
- return currentConnectionId;
590
- },
591
- async close() {
592
- isClosed = true;
593
- isAuthed = false;
594
- currentConnectionId = null;
595
- rejectConnect("Shardwire consumer has been closed.");
596
- if (reconnectTimer) {
597
- clearTimeout(reconnectTimer);
598
- reconnectTimer = null;
1007
+ case "action.request": {
1008
+ const requestId = envelope.requestId;
1009
+ const payload = envelope.payload;
1010
+ if (!requestId || !payload || typeof payload.name !== "string") {
1011
+ return;
1012
+ }
1013
+ const result = await this.config.onActionRequest(
1014
+ {
1015
+ id: state.id,
1016
+ ...state.appName ? { appName: state.appName } : {},
1017
+ secret: state.secret,
1018
+ capabilities: state.capabilities
1019
+ },
1020
+ payload.name,
1021
+ payload.data,
1022
+ requestId
1023
+ );
1024
+ this.safeSend(
1025
+ state.socket,
1026
+ stringifyEnvelope(makeEnvelope(result.ok ? "action.result" : "action.error", result, { requestId }))
1027
+ );
1028
+ return;
599
1029
  }
600
- rejectAllPending("DISCONNECTED", "Shardwire consumer has been closed.");
601
- if (!socket) {
1030
+ default:
602
1031
  return;
1032
+ }
1033
+ }
1034
+ checkHeartbeats() {
1035
+ const now = Date.now();
1036
+ const threshold = this.heartbeatMs * 2;
1037
+ for (const state of this.connections.values()) {
1038
+ if (!state.authenticated) {
1039
+ continue;
603
1040
  }
604
- await new Promise((resolve) => {
605
- const current = socket;
606
- if (!current) {
607
- resolve();
608
- return;
609
- }
610
- current.once("close", () => resolve());
611
- current.close();
612
- });
613
- socket = null;
1041
+ if (now - state.lastHeartbeatAt > threshold) {
1042
+ state.socket.terminate();
1043
+ this.connections.delete(state.socket);
1044
+ continue;
1045
+ }
1046
+ this.safeSend(state.socket, stringifyEnvelope(makeEnvelope("ping", {})));
1047
+ }
1048
+ }
1049
+ safeSend(socket, payload) {
1050
+ if (socket.readyState === 1) {
1051
+ socket.send(payload);
1052
+ }
1053
+ }
1054
+ };
1055
+ function authenticateSecret(payload, secrets, resolver) {
1056
+ if (!payload.secret) {
1057
+ return { ok: false, reason: "invalid_secret" };
1058
+ }
1059
+ let matchedSecret;
1060
+ if (payload.secretId) {
1061
+ matchedSecret = secrets.find((secret) => secret.id === payload.secretId);
1062
+ if (!matchedSecret) {
1063
+ return { ok: false, reason: "unknown_secret_id" };
614
1064
  }
1065
+ if (!isSecretValid(payload.secret, matchedSecret.value)) {
1066
+ return { ok: false, reason: "invalid_secret" };
1067
+ }
1068
+ } else {
1069
+ const matches = secrets.filter((secret) => isSecretValid(payload.secret, secret.value));
1070
+ if (matches.length === 0) {
1071
+ return { ok: false, reason: "invalid_secret" };
1072
+ }
1073
+ if (matches.length > 1) {
1074
+ return { ok: false, reason: "ambiguous_secret" };
1075
+ }
1076
+ matchedSecret = matches[0];
1077
+ }
1078
+ if (!matchedSecret) {
1079
+ return { ok: false, reason: "invalid_secret" };
1080
+ }
1081
+ return {
1082
+ ok: true,
1083
+ secret: matchedSecret,
1084
+ capabilities: resolver(matchedSecret)
615
1085
  };
616
1086
  }
617
1087
 
618
- // src/discord/client.ts
619
- async function resolveDiscordClient(options) {
620
- if (options.client) {
621
- return { client: options.client, owned: false };
1088
+ // src/bridge/validation.ts
1089
+ function isNonEmptyString(value) {
1090
+ return typeof value === "string" && value.trim().length > 0;
1091
+ }
1092
+ function assertPositiveNumber(name, value) {
1093
+ if (typeof value !== "number" || Number.isNaN(value) || value <= 0) {
1094
+ throw new Error(`${name} must be a positive number.`);
1095
+ }
1096
+ }
1097
+ function normalizeScopeList(value, known, label) {
1098
+ if (value === void 0 || value === "*") {
1099
+ return "*";
1100
+ }
1101
+ if (!Array.isArray(value)) {
1102
+ throw new Error(`${label} must be "*" or an array.`);
1103
+ }
1104
+ const knownSet = new Set(known);
1105
+ const entries = /* @__PURE__ */ new Set();
1106
+ for (const rawItem of value) {
1107
+ if (typeof rawItem !== "string") {
1108
+ throw new Error(`${label} contains a non-string value.`);
1109
+ }
1110
+ const item = rawItem;
1111
+ if (!knownSet.has(item)) {
1112
+ throw new Error(`${label} contains unsupported value "${String(item)}".`);
1113
+ }
1114
+ entries.add(item);
1115
+ }
1116
+ return entries;
1117
+ }
1118
+ function normalizeSecretEntry(secret, index) {
1119
+ const defaultId = `s${index}`;
1120
+ if (typeof secret === "string") {
1121
+ if (!isNonEmptyString(secret)) {
1122
+ throw new Error(`server.secrets[${index}] must be a non-empty string.`);
1123
+ }
1124
+ return {
1125
+ id: defaultId,
1126
+ value: secret,
1127
+ scope: {
1128
+ events: "*",
1129
+ actions: "*"
1130
+ }
1131
+ };
1132
+ }
1133
+ const scoped = secret;
1134
+ if (!isNonEmptyString(scoped.value)) {
1135
+ throw new Error(`server.secrets[${index}].value must be a non-empty string.`);
1136
+ }
1137
+ if (scoped.id !== void 0 && !isNonEmptyString(scoped.id)) {
1138
+ throw new Error(`server.secrets[${index}].id must be a non-empty string when provided.`);
1139
+ }
1140
+ return {
1141
+ id: scoped.id ?? defaultId,
1142
+ value: scoped.value,
1143
+ scope: {
1144
+ events: normalizeScopeList(scoped.allow?.events, BOT_EVENT_NAMES, `server.secrets[${index}].allow.events`),
1145
+ actions: normalizeScopeList(
1146
+ scoped.allow?.actions,
1147
+ BOT_ACTION_NAMES,
1148
+ `server.secrets[${index}].allow.actions`
1149
+ )
1150
+ }
1151
+ };
1152
+ }
1153
+ function assertBotBridgeOptions(options) {
1154
+ if (!isNonEmptyString(options.token)) {
1155
+ throw new Error("Bot bridge requires `token`.");
1156
+ }
1157
+ if (!Array.isArray(options.intents) || options.intents.length === 0) {
1158
+ throw new Error("Bot bridge requires at least one intent.");
1159
+ }
1160
+ assertPositiveNumber("server.port", options.server.port);
1161
+ if (options.server.heartbeatMs !== void 0) {
1162
+ assertPositiveNumber("server.heartbeatMs", options.server.heartbeatMs);
1163
+ }
1164
+ if (options.server.maxPayloadBytes !== void 0) {
1165
+ assertPositiveNumber("server.maxPayloadBytes", options.server.maxPayloadBytes);
1166
+ }
1167
+ if (!Array.isArray(options.server.secrets) || options.server.secrets.length === 0) {
1168
+ throw new Error("server.secrets must contain at least one secret.");
1169
+ }
1170
+ const ids = /* @__PURE__ */ new Set();
1171
+ const values = /* @__PURE__ */ new Set();
1172
+ options.server.secrets.forEach((secret, index) => {
1173
+ const normalized = normalizeSecretEntry(secret, index);
1174
+ if (ids.has(normalized.id)) {
1175
+ throw new Error(`server.secrets contains duplicate secret id "${normalized.id}".`);
1176
+ }
1177
+ if (values.has(normalized.value)) {
1178
+ throw new Error(`server.secrets contains duplicate secret value at index ${index}.`);
1179
+ }
1180
+ ids.add(normalized.id);
1181
+ values.add(normalized.value);
1182
+ });
1183
+ }
1184
+ function normalizeSecrets(options) {
1185
+ return options.server.secrets.map((secret, index) => normalizeSecretEntry(secret, index));
1186
+ }
1187
+ function assertAppBridgeOptions(options) {
1188
+ if (!isNonEmptyString(options.url)) {
1189
+ throw new Error("App bridge requires `url`.");
1190
+ }
1191
+ let parsedUrl;
1192
+ try {
1193
+ parsedUrl = new URL(options.url);
1194
+ } catch {
1195
+ throw new Error("App bridge option `url` must be a valid URL.");
1196
+ }
1197
+ if (parsedUrl.protocol !== "ws:" && parsedUrl.protocol !== "wss:") {
1198
+ throw new Error("App bridge option `url` must use `ws://` or `wss://`.");
1199
+ }
1200
+ const isLoopbackHost = parsedUrl.hostname === "localhost" || parsedUrl.hostname === "127.0.0.1" || parsedUrl.hostname === "::1";
1201
+ if (parsedUrl.protocol === "ws:" && !isLoopbackHost) {
1202
+ throw new Error("Non-loopback app bridge URLs must use `wss://`.");
1203
+ }
1204
+ if (!isNonEmptyString(options.secret)) {
1205
+ throw new Error("App bridge requires `secret`.");
1206
+ }
1207
+ if (options.secretId !== void 0 && !isNonEmptyString(options.secretId)) {
1208
+ throw new Error("App bridge option `secretId` must be a non-empty string.");
1209
+ }
1210
+ if (options.appName !== void 0 && !isNonEmptyString(options.appName)) {
1211
+ throw new Error("App bridge option `appName` must be a non-empty string.");
1212
+ }
1213
+ if (options.requestTimeoutMs !== void 0) {
1214
+ assertPositiveNumber("requestTimeoutMs", options.requestTimeoutMs);
1215
+ }
1216
+ if (options.reconnect?.initialDelayMs !== void 0) {
1217
+ assertPositiveNumber("reconnect.initialDelayMs", options.reconnect.initialDelayMs);
622
1218
  }
623
- if (!options.token) {
624
- return { owned: false };
1219
+ if (options.reconnect?.maxDelayMs !== void 0) {
1220
+ assertPositiveNumber("reconnect.maxDelayMs", options.reconnect.maxDelayMs);
625
1221
  }
626
- const discordModule = await import("discord.js");
627
- const created = new discordModule.Client({
628
- intents: []
1222
+ }
1223
+ function resolveCapabilitiesForSecret(intents, secret) {
1224
+ const availableEvents = getAvailableEvents(intents);
1225
+ const events = secret.scope.events === "*" ? [...availableEvents] : availableEvents.filter((eventName) => {
1226
+ return secret.scope.events !== "*" && secret.scope.events.has(eventName);
1227
+ });
1228
+ const actions = secret.scope.actions === "*" ? [...BOT_ACTION_NAMES] : BOT_ACTION_NAMES.filter((action) => {
1229
+ return secret.scope.actions !== "*" && secret.scope.actions.has(action);
629
1230
  });
630
- await created.login(options.token);
631
- return { client: created, owned: true };
1231
+ return {
1232
+ events: [...events],
1233
+ actions: [...actions]
1234
+ };
632
1235
  }
633
1236
 
634
- // src/runtime/reliability.ts
635
- async function withTimeout(promise, timeoutMs, timeoutMessage = "Operation timed out.") {
636
- return new Promise((resolve, reject) => {
637
- const timeout = setTimeout(() => {
638
- reject(new Error(timeoutMessage));
639
- }, timeoutMs);
640
- promise.then((value) => {
641
- clearTimeout(timeout);
642
- resolve(value);
643
- }).catch((error) => {
644
- clearTimeout(timeout);
645
- reject(error instanceof Error ? error : new Error(String(error)));
646
- });
1237
+ // src/bot/index.ts
1238
+ function createBotBridge(options) {
1239
+ const runtime = createDiscordJsRuntimeAdapter({
1240
+ token: options.token,
1241
+ intents: options.intents,
1242
+ ...options.logger ? { logger: options.logger } : {}
647
1243
  });
1244
+ return createBotBridgeWithRuntime(options, runtime);
648
1245
  }
649
- var DedupeCache = class {
650
- constructor(ttlMs) {
651
- this.ttlMs = ttlMs;
652
- }
653
- ttlMs;
654
- cache = /* @__PURE__ */ new Map();
655
- get(key) {
656
- const entry = this.cache.get(key);
657
- if (!entry) {
658
- return void 0;
1246
+ function createBotBridgeWithRuntime(options, runtime) {
1247
+ assertBotBridgeOptions(options);
1248
+ const secrets = normalizeSecrets(options);
1249
+ const availableEvents = getAvailableEvents(options.intents);
1250
+ const server = new BridgeTransportServer({
1251
+ options,
1252
+ ...options.logger ? { logger: options.logger } : {},
1253
+ authenticate: (payload) => authenticateSecret(payload, secrets, (secret) => resolveCapabilitiesForSecret(options.intents, secret)),
1254
+ onActionRequest: async (connection, actionName, payload, requestId) => {
1255
+ if (!connection.capabilities.actions.includes(actionName)) {
1256
+ return {
1257
+ ok: false,
1258
+ requestId,
1259
+ ts: Date.now(),
1260
+ error: {
1261
+ code: "FORBIDDEN",
1262
+ message: `Action "${actionName}" is not allowed for this app.`
1263
+ }
1264
+ };
1265
+ }
1266
+ try {
1267
+ const result = await runtime.executeAction(actionName, payload);
1268
+ return {
1269
+ ok: true,
1270
+ requestId,
1271
+ ts: Date.now(),
1272
+ data: result
1273
+ };
1274
+ } catch (error) {
1275
+ if (error instanceof ActionExecutionError) {
1276
+ return {
1277
+ ok: false,
1278
+ requestId,
1279
+ ts: Date.now(),
1280
+ error: {
1281
+ code: error.code,
1282
+ message: error.message,
1283
+ ...error.details !== void 0 ? { details: error.details } : {}
1284
+ }
1285
+ };
1286
+ }
1287
+ return {
1288
+ ok: false,
1289
+ requestId,
1290
+ ts: Date.now(),
1291
+ error: {
1292
+ code: "INTERNAL_ERROR",
1293
+ message: error instanceof Error ? error.message : "Unknown action failure."
1294
+ }
1295
+ };
1296
+ }
659
1297
  }
660
- if (entry.expiresAt <= Date.now()) {
661
- this.cache.delete(key);
662
- return void 0;
1298
+ });
1299
+ const unsubscribers = availableEvents.map(
1300
+ (eventName) => runtime.on(eventName, (payload) => {
1301
+ if (eventName === "ready") {
1302
+ server.setStickyEvent("ready", payload);
1303
+ }
1304
+ server.publishEvent(eventName, payload);
1305
+ })
1306
+ );
1307
+ return {
1308
+ async ready() {
1309
+ await runtime.ready();
1310
+ },
1311
+ async close() {
1312
+ for (const unsubscribe of unsubscribers) {
1313
+ unsubscribe();
1314
+ }
1315
+ await Promise.all([server.close(), runtime.close()]);
1316
+ },
1317
+ status() {
1318
+ return {
1319
+ ready: runtime.isReady(),
1320
+ connectionCount: server.connectionCount()
1321
+ };
663
1322
  }
664
- return entry.value;
665
- }
666
- set(key, value) {
667
- this.cache.set(key, { value, expiresAt: Date.now() + this.ttlMs });
1323
+ };
1324
+ }
1325
+
1326
+ // src/discord/types.ts
1327
+ var BridgeCapabilityError = class extends Error {
1328
+ constructor(kind, name, message) {
1329
+ super(message ?? `Capability "${name}" is not available for ${kind}.`);
1330
+ this.kind = kind;
1331
+ this.name = name;
1332
+ this.name = "BridgeCapabilityError";
668
1333
  }
1334
+ kind;
1335
+ name;
669
1336
  };
670
1337
 
671
- // src/transport/ws/host-server.ts
1338
+ // src/bridge/transport/socket.ts
672
1339
  var import_ws2 = require("ws");
1340
+ function createNodeWebSocket(url) {
1341
+ return new import_ws2.WebSocket(url);
1342
+ }
673
1343
 
674
- // src/runtime/security.ts
675
- var import_node_crypto2 = require("crypto");
676
- function isSecretValid(provided, expected) {
677
- const providedBuffer = Buffer.from(provided);
678
- const expectedBuffer = Buffer.from(expected);
679
- if (providedBuffer.length !== expectedBuffer.length) {
680
- return false;
1344
+ // src/utils/backoff.ts
1345
+ function getBackoffDelay(attempt, config) {
1346
+ const base = Math.min(config.maxDelayMs, config.initialDelayMs * 2 ** attempt);
1347
+ if (!config.jitter) {
1348
+ return base;
681
1349
  }
682
- return (0, import_node_crypto2.timingSafeEqual)(providedBuffer, expectedBuffer);
683
- }
684
- function getSecretId(secretIndex) {
685
- return `s${secretIndex}`;
1350
+ const spread = Math.floor(base * 0.2);
1351
+ const min = Math.max(0, base - spread);
1352
+ const max = base + spread;
1353
+ return Math.floor(Math.random() * (max - min + 1)) + min;
686
1354
  }
687
1355
 
688
- // src/transport/ws/host-server.ts
689
- var CLOSE_AUTH_REQUIRED = 4001;
690
- var CLOSE_AUTH_FAILED = 4003;
691
- var CLOSE_INVALID_PAYLOAD = 4004;
692
- var HostWebSocketServer = class {
693
- constructor(config) {
694
- this.config = config;
695
- const serverConfig = config.options.server;
696
- this.heartbeatMs = serverConfig.heartbeatMs ?? 3e4;
697
- this.logger = withLogger(config.options.logger);
698
- this.wss = new import_ws2.WebSocketServer({
699
- host: serverConfig.host,
700
- port: serverConfig.port,
701
- path: serverConfig.path ?? "/shardwire",
702
- maxPayload: serverConfig.maxPayloadBytes ?? 65536
703
- });
704
- this.wss.on("connection", (socket, request) => this.handleConnection(socket, request));
705
- this.wss.on(
706
- "error",
707
- (error) => this.logger.error("Shardwire host server error.", { error: String(error) })
708
- );
709
- this.interval = setInterval(() => {
710
- this.checkHeartbeats();
711
- }, this.heartbeatMs);
1356
+ // src/app/index.ts
1357
+ var AppRequestError = class extends Error {
1358
+ constructor(code, message) {
1359
+ super(message);
1360
+ this.code = code;
1361
+ this.name = "AppRequestError";
712
1362
  }
713
- config;
714
- wss;
715
- connections = /* @__PURE__ */ new Map();
716
- logger;
717
- heartbeatMs;
718
- authTimeoutMs = 5e3;
719
- interval;
720
- emitEvent(name, data, source) {
721
- const envelope = makeEnvelope(
722
- "event.emit",
723
- { name, data },
724
- source ? { source } : void 0
725
- );
726
- const raw = stringifyEnvelope(envelope);
727
- for (const state of this.connections.values()) {
728
- if (!state.authenticated) {
729
- continue;
730
- }
731
- this.safeSend(state.socket, raw);
1363
+ code;
1364
+ };
1365
+ var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
1366
+ function connectBotBridge(options) {
1367
+ assertAppBridgeOptions(options);
1368
+ const logger = withLogger(options.logger);
1369
+ const reconnectEnabled = options.reconnect?.enabled ?? true;
1370
+ const initialDelayMs = options.reconnect?.initialDelayMs ?? 500;
1371
+ const maxDelayMs = options.reconnect?.maxDelayMs ?? 1e4;
1372
+ const jitter = options.reconnect?.jitter ?? true;
1373
+ const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
1374
+ let socket = null;
1375
+ let isClosed = false;
1376
+ let isAuthed = false;
1377
+ let currentConnectionId = null;
1378
+ let currentCapabilities = { events: [], actions: [] };
1379
+ let reconnectAttempts = 0;
1380
+ let reconnectTimer = null;
1381
+ let connectPromise = null;
1382
+ let connectResolve = null;
1383
+ let connectReject = null;
1384
+ let authTimeoutTimer = null;
1385
+ let capabilityError = null;
1386
+ const pendingRequests = /* @__PURE__ */ new Map();
1387
+ const eventHandlers = /* @__PURE__ */ new Map();
1388
+ const subscribedEntries = /* @__PURE__ */ new Map();
1389
+ function clearAuthTimeout() {
1390
+ if (authTimeoutTimer) {
1391
+ clearTimeout(authTimeoutTimer);
1392
+ authTimeoutTimer = null;
732
1393
  }
733
1394
  }
734
- async close() {
735
- clearInterval(this.interval);
736
- for (const connection of this.connections.values()) {
737
- connection.socket.close();
738
- }
739
- this.connections.clear();
740
- await new Promise((resolve, reject) => {
741
- this.wss.close((error) => {
742
- if (error) {
743
- reject(error);
744
- return;
745
- }
746
- resolve();
747
- });
748
- });
1395
+ function resolveConnect() {
1396
+ clearAuthTimeout();
1397
+ connectResolve?.();
1398
+ connectResolve = null;
1399
+ connectReject = null;
1400
+ connectPromise = null;
749
1401
  }
750
- handleConnection(socket, request) {
751
- const allowlist = this.config.options.server.corsOrigins;
752
- if (allowlist && allowlist.length > 0) {
753
- const origin = request.headers.origin;
754
- if (!origin || !allowlist.includes(origin)) {
755
- socket.close(CLOSE_AUTH_FAILED, "Origin not allowed.");
756
- return;
757
- }
1402
+ function rejectConnect(message) {
1403
+ clearAuthTimeout();
1404
+ if (connectReject) {
1405
+ connectReject(new Error(message));
758
1406
  }
759
- const state = {
760
- id: createConnectionId(),
761
- socket,
762
- authenticated: false,
763
- lastHeartbeatAt: Date.now()
764
- };
765
- this.connections.set(socket, state);
766
- const authTimer = setTimeout(() => {
767
- if (!state.authenticated) {
768
- socket.close(CLOSE_AUTH_REQUIRED, "Authentication required.");
769
- }
770
- }, this.authTimeoutMs);
771
- socket.on("message", (raw) => {
772
- const serialized = typeof raw === "string" ? raw : Buffer.isBuffer(raw) ? raw.toString("utf8") : void 0;
773
- if (!serialized) {
774
- this.logger.warn("Invalid message payload from client.", { error: "Unsupported payload type." });
775
- socket.close(CLOSE_INVALID_PAYLOAD, "Invalid payload.");
776
- return;
777
- }
778
- let parsed;
779
- try {
780
- parsed = parseEnvelope(serialized);
781
- } catch (error) {
782
- this.logger.warn("Invalid message payload from client.", { error: String(error) });
783
- socket.close(CLOSE_INVALID_PAYLOAD, "Invalid payload.");
784
- return;
785
- }
786
- void this.handleMessage(state, parsed).catch((error) => {
787
- this.logger.warn("Invalid message payload from client.", { error: String(error) });
788
- socket.close(CLOSE_INVALID_PAYLOAD, "Invalid payload.");
789
- });
790
- });
791
- socket.on("close", () => {
792
- clearTimeout(authTimer);
793
- this.connections.delete(socket);
794
- });
795
- socket.on(
796
- "error",
797
- (error) => this.logger.warn("Socket error.", { connectionId: state.id, error: String(error) })
798
- );
1407
+ connectResolve = null;
1408
+ connectReject = null;
1409
+ connectPromise = null;
799
1410
  }
800
- async handleMessage(state, envelope) {
801
- if (envelope.type === "ping") {
802
- state.lastHeartbeatAt = Date.now();
803
- this.safeSend(state.socket, stringifyEnvelope(makeEnvelope("pong", {})));
804
- return;
805
- }
806
- if (envelope.type === "pong") {
807
- state.lastHeartbeatAt = Date.now();
808
- return;
809
- }
810
- if (!state.authenticated) {
811
- if (envelope.type !== "auth.hello") {
812
- state.socket.close(CLOSE_AUTH_REQUIRED, "Authentication required.");
813
- return;
814
- }
815
- const payload = envelope.payload;
816
- const providedSecret = payload?.secret;
817
- const knownSecrets = this.config.options.server.secrets;
818
- const secretId = payload?.secretId;
819
- let authReason = null;
820
- if (!providedSecret) {
821
- authReason = "invalid_secret";
822
- } else if (secretId) {
823
- const secretIndex = knownSecrets.findIndex((_, index) => getSecretId(index) === secretId);
824
- if (secretIndex < 0) {
825
- authReason = "unknown_secret_id";
826
- } else {
827
- const expectedSecret = knownSecrets[secretIndex];
828
- if (!expectedSecret || !isSecretValid(providedSecret, expectedSecret)) {
829
- authReason = "invalid_secret";
830
- }
831
- }
832
- } else if (!knownSecrets.some((secret) => isSecretValid(providedSecret, secret))) {
833
- authReason = "invalid_secret";
834
- }
835
- if (authReason) {
836
- this.safeSend(
837
- state.socket,
838
- stringifyEnvelope(
839
- makeEnvelope("auth.error", {
840
- code: "UNAUTHORIZED",
841
- reason: authReason,
842
- message: "Authentication failed."
843
- })
844
- )
845
- );
846
- state.socket.close(CLOSE_AUTH_FAILED, "Invalid secret.");
847
- return;
848
- }
849
- state.authenticated = true;
850
- state.lastHeartbeatAt = Date.now();
851
- if (payload.clientName) {
852
- state.clientName = payload.clientName;
853
- }
854
- this.safeSend(
855
- state.socket,
856
- stringifyEnvelope(makeEnvelope("auth.ok", { connectionId: state.id }))
857
- );
858
- return;
1411
+ function sendRaw(data) {
1412
+ if (!socket || socket.readyState !== 1) {
1413
+ throw new Error("App bridge is not connected.");
859
1414
  }
860
- if (envelope.type === "command.request") {
861
- const payload = envelope.payload;
862
- if (!envelope.requestId || !payload?.name) {
863
- const invalid = {
864
- ok: false,
865
- requestId: envelope.requestId ?? "unknown",
866
- ts: Date.now(),
867
- error: {
868
- code: "VALIDATION_ERROR",
869
- message: "Invalid command request envelope."
870
- }
871
- };
872
- this.safeSend(
873
- state.socket,
874
- stringifyEnvelope(makeEnvelope("command.error", invalid, { requestId: invalid.requestId }))
875
- );
876
- return;
877
- }
878
- const response = await this.config.onCommandRequest(
879
- state,
880
- payload,
881
- envelope.requestId,
882
- envelope.source
883
- );
884
- const responseType = response.ok ? "command.result" : "command.error";
885
- this.safeSend(
886
- state.socket,
887
- stringifyEnvelope(makeEnvelope(responseType, response, { requestId: response.requestId }))
888
- );
1415
+ socket.send(data);
1416
+ }
1417
+ function rejectAllPending(code, reason) {
1418
+ for (const [requestId, pending] of pendingRequests.entries()) {
1419
+ clearTimeout(pending.timer);
1420
+ pending.reject(new AppRequestError(code, reason));
1421
+ pendingRequests.delete(requestId);
1422
+ }
1423
+ }
1424
+ function scheduleReconnect() {
1425
+ if (isClosed || !reconnectEnabled || reconnectTimer) {
889
1426
  return;
890
1427
  }
1428
+ const delay = getBackoffDelay(reconnectAttempts, { initialDelayMs, maxDelayMs, jitter });
1429
+ reconnectAttempts += 1;
1430
+ reconnectTimer = setTimeout(() => {
1431
+ reconnectTimer = null;
1432
+ void connect().catch((error) => {
1433
+ logger.warn("Reconnect attempt failed.", { error: String(error) });
1434
+ });
1435
+ }, delay);
891
1436
  }
892
- checkHeartbeats() {
893
- const now = Date.now();
894
- const threshold = this.heartbeatMs * 2;
895
- for (const state of this.connections.values()) {
896
- if (!state.authenticated) {
897
- continue;
1437
+ function evaluateSubscriptions() {
1438
+ const desiredSubscriptions = /* @__PURE__ */ new Map();
1439
+ if (currentCapabilities.events.length === 0) {
1440
+ capabilityError = null;
1441
+ for (const handlers of eventHandlers.values()) {
1442
+ for (const entry of handlers) {
1443
+ desiredSubscriptions.set(entry.signature, entry.subscription);
1444
+ }
898
1445
  }
899
- if (now - state.lastHeartbeatAt > threshold) {
900
- state.socket.terminate();
901
- this.connections.delete(state.socket);
1446
+ return desiredSubscriptions;
1447
+ }
1448
+ const allowedEvents = new Set(currentCapabilities.events);
1449
+ const invalidEvents = [...eventHandlers.keys()].filter((eventName) => !allowedEvents.has(eventName));
1450
+ const firstInvalid = invalidEvents[0];
1451
+ capabilityError = firstInvalid !== void 0 ? new BridgeCapabilityError("event", firstInvalid, `Event "${firstInvalid}" is not available for this app.`) : null;
1452
+ for (const [eventName, handlers] of eventHandlers.entries()) {
1453
+ if (!allowedEvents.has(eventName)) {
902
1454
  continue;
903
1455
  }
904
- this.safeSend(state.socket, stringifyEnvelope(makeEnvelope("ping", {})));
1456
+ for (const entry of handlers) {
1457
+ desiredSubscriptions.set(entry.signature, entry.subscription);
1458
+ }
905
1459
  }
1460
+ return desiredSubscriptions;
906
1461
  }
907
- safeSend(socket, payload) {
908
- if (socket.readyState === 1) {
909
- socket.send(payload);
1462
+ function syncSubscriptions() {
1463
+ if (!isAuthed || !socket || socket.readyState !== 1) {
1464
+ return;
910
1465
  }
911
- }
912
- };
913
-
914
- // src/host/index.ts
915
- var DEFAULT_COMMAND_TIMEOUT_MS = 1e4;
916
- function createHostShardwire(options, runtimeHooks) {
917
- const commandHandlers = /* @__PURE__ */ new Map();
918
- const commandTimeoutMs = options.server.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
919
- const dedupeCache = new DedupeCache(commandTimeoutMs * 2);
920
- const hostServer = new HostWebSocketServer({
921
- options,
922
- onCommandRequest: async (connection, payload, requestId, source) => {
923
- const cacheKey = `${connection.id}:${requestId}:${payload.name}`;
924
- const cached = dedupeCache.get(cacheKey);
925
- if (cached) {
926
- return cached;
927
- }
928
- const handler = commandHandlers.get(payload.name);
929
- if (!handler) {
930
- const failure = {
931
- ok: false,
932
- requestId,
933
- ts: Date.now(),
934
- error: {
935
- code: "COMMAND_NOT_FOUND",
936
- message: `No command handler registered for "${payload.name}".`
937
- }
938
- };
939
- dedupeCache.set(cacheKey, failure);
940
- return failure;
1466
+ const desiredSubscriptions = evaluateSubscriptions();
1467
+ const toSubscribe = [...desiredSubscriptions.entries()].filter(([signature]) => !subscribedEntries.has(signature)).map(([, subscription]) => subscription);
1468
+ if (toSubscribe.length > 0) {
1469
+ sendRaw(stringifyEnvelope(makeEnvelope("subscribe", { subscriptions: toSubscribe })));
1470
+ for (const subscription of toSubscribe) {
1471
+ subscribedEntries.set(serializeEventSubscription(subscription), subscription);
941
1472
  }
942
- const context = {
943
- requestId,
944
- connectionId: connection.id,
945
- receivedAt: Date.now()
946
- };
947
- if (source) {
948
- context.source = source;
1473
+ }
1474
+ const toUnsubscribe = [...subscribedEntries.entries()].filter(([signature]) => !desiredSubscriptions.has(signature)).map(([, subscription]) => subscription);
1475
+ if (toUnsubscribe.length > 0) {
1476
+ sendRaw(stringifyEnvelope(makeEnvelope("unsubscribe", { subscriptions: toUnsubscribe })));
1477
+ for (const subscription of toUnsubscribe) {
1478
+ subscribedEntries.delete(serializeEventSubscription(subscription));
949
1479
  }
1480
+ }
1481
+ }
1482
+ function handleEvent(name, payload) {
1483
+ const handlers = eventHandlers.get(name);
1484
+ if (!handlers || handlers.size === 0) {
1485
+ return;
1486
+ }
1487
+ for (const entry of handlers) {
950
1488
  try {
951
- const commandValidation = options.validation?.commands?.[payload.name];
952
- const validatedRequest = parsePayloadWithSchema(commandValidation?.request, payload.data, {
953
- name: payload.name,
954
- stage: "command.request"
955
- });
956
- const maybePromise = Promise.resolve(handler(validatedRequest, context));
957
- const value = await withTimeout(
958
- maybePromise,
959
- commandTimeoutMs,
960
- `Command "${payload.name}" timed out after ${commandTimeoutMs}ms.`
961
- );
962
- const validatedResponse = parsePayloadWithSchema(commandValidation?.response, value, {
963
- name: payload.name,
964
- stage: "command.response"
965
- });
966
- const success = {
967
- ok: true,
968
- requestId,
969
- ts: Date.now(),
970
- data: validatedResponse
971
- };
972
- dedupeCache.set(cacheKey, success);
973
- return success;
974
- } catch (error) {
975
- if (error instanceof PayloadValidationError) {
976
- const failure2 = {
977
- ok: false,
978
- requestId,
979
- ts: Date.now(),
980
- error: {
981
- code: "VALIDATION_ERROR",
982
- message: error.message,
983
- details: error.details
984
- }
985
- };
986
- dedupeCache.set(cacheKey, failure2);
987
- return failure2;
1489
+ if (!matchesEventSubscription(entry.subscription, payload)) {
1490
+ continue;
988
1491
  }
989
- const isTimeout = error instanceof Error && /timed out/i.test(error.message);
990
- const failure = {
991
- ok: false,
992
- requestId,
993
- ts: Date.now(),
994
- error: {
995
- code: isTimeout ? "TIMEOUT" : "INTERNAL_ERROR",
996
- message: error instanceof Error ? error.message : "Unknown command execution error."
997
- }
998
- };
999
- dedupeCache.set(cacheKey, failure);
1000
- return failure;
1492
+ entry.handler(payload);
1493
+ } catch (error) {
1494
+ logger.warn("App event handler threw an error.", { event: name, error: String(error) });
1001
1495
  }
1002
1496
  }
1003
- });
1004
- return {
1005
- mode: "host",
1006
- onCommand(name, handler) {
1007
- assertMessageName("command", name);
1008
- commandHandlers.set(name, handler);
1009
- return () => {
1010
- commandHandlers.delete(name);
1011
- };
1012
- },
1013
- emitEvent(name, payload) {
1014
- assertMessageName("event", name);
1015
- const validatedPayload = parsePayloadWithSchema(options.validation?.events?.[name], payload, {
1016
- name,
1017
- stage: "event.emit"
1018
- });
1019
- assertJsonPayload("event", name, validatedPayload);
1020
- hostServer.emitEvent(name, validatedPayload, options.name);
1021
- },
1022
- broadcast(name, payload) {
1023
- assertMessageName("event", name);
1024
- const validatedPayload = parsePayloadWithSchema(options.validation?.events?.[name], payload, {
1025
- name,
1026
- stage: "event.emit"
1027
- });
1028
- assertJsonPayload("event", name, validatedPayload);
1029
- hostServer.emitEvent(name, validatedPayload, options.name);
1030
- },
1031
- close() {
1032
- return hostServer.close().then(async () => {
1033
- await runtimeHooks?.onClose?.();
1034
- });
1497
+ }
1498
+ async function connect() {
1499
+ if (isClosed) {
1500
+ return;
1035
1501
  }
1036
- };
1037
- }
1038
-
1039
- // src/schema/index.ts
1040
- function fromSafeParseSchema(schema) {
1041
- return {
1042
- parse(value) {
1043
- const result = schema.safeParse(value);
1044
- if (result.success) {
1045
- return result.data;
1502
+ if (socket && socket.readyState === 1 && isAuthed) {
1503
+ return;
1504
+ }
1505
+ if (connectPromise) {
1506
+ return connectPromise;
1507
+ }
1508
+ connectPromise = new Promise((resolve, reject) => {
1509
+ connectResolve = resolve;
1510
+ connectReject = reject;
1511
+ });
1512
+ socket = createNodeWebSocket(options.url);
1513
+ socket.on("open", () => {
1514
+ reconnectAttempts = 0;
1515
+ isAuthed = false;
1516
+ subscribedEntries.clear();
1517
+ currentConnectionId = null;
1518
+ currentCapabilities = { events: [], actions: [] };
1519
+ capabilityError = null;
1520
+ sendRaw(
1521
+ stringifyEnvelope(
1522
+ makeEnvelope("auth.hello", {
1523
+ secret: options.secret,
1524
+ ...options.secretId ? { secretId: options.secretId } : {},
1525
+ ...options.appName ? { appName: options.appName } : {}
1526
+ })
1527
+ )
1528
+ );
1529
+ authTimeoutTimer = setTimeout(() => {
1530
+ if (!isAuthed) {
1531
+ rejectConnect("App bridge authentication timed out.");
1532
+ socket?.close();
1533
+ }
1534
+ }, requestTimeoutMs);
1535
+ });
1536
+ socket.on("message", (raw) => {
1537
+ try {
1538
+ const serialized = typeof raw === "string" ? raw : String(raw);
1539
+ const envelope = parseEnvelope(serialized);
1540
+ switch (envelope.type) {
1541
+ case "auth.ok": {
1542
+ const payload = envelope.payload;
1543
+ isAuthed = true;
1544
+ currentConnectionId = payload.connectionId;
1545
+ currentCapabilities = payload.capabilities;
1546
+ syncSubscriptions();
1547
+ resolveConnect();
1548
+ break;
1549
+ }
1550
+ case "auth.error": {
1551
+ const payload = envelope.payload;
1552
+ rejectConnect(payload.message);
1553
+ rejectAllPending("UNAUTHORIZED", payload.message);
1554
+ socket?.close();
1555
+ break;
1556
+ }
1557
+ case "action.result":
1558
+ case "action.error": {
1559
+ const requestId = envelope.requestId;
1560
+ if (!requestId) {
1561
+ return;
1562
+ }
1563
+ const pending = pendingRequests.get(requestId);
1564
+ if (!pending) {
1565
+ return;
1566
+ }
1567
+ clearTimeout(pending.timer);
1568
+ pending.resolve(envelope.payload);
1569
+ pendingRequests.delete(requestId);
1570
+ break;
1571
+ }
1572
+ case "discord.event": {
1573
+ const payload = envelope.payload;
1574
+ handleEvent(payload.name, payload.data);
1575
+ break;
1576
+ }
1577
+ case "ping":
1578
+ sendRaw(stringifyEnvelope(makeEnvelope("pong", {})));
1579
+ break;
1580
+ default:
1581
+ break;
1582
+ }
1583
+ } catch (error) {
1584
+ logger.warn("Failed to parse app bridge message.", { error: String(error) });
1046
1585
  }
1047
- const error = new Error(result.error.message);
1048
- if (result.error.issues) {
1049
- error.issues = result.error.issues;
1586
+ });
1587
+ socket.on("close", () => {
1588
+ rejectConnect("App bridge connection closed.");
1589
+ isAuthed = false;
1590
+ currentConnectionId = null;
1591
+ currentCapabilities = { events: [], actions: [] };
1592
+ subscribedEntries.clear();
1593
+ rejectAllPending("DISCONNECTED", "App bridge connection closed before action completed.");
1594
+ if (!isClosed) {
1595
+ scheduleReconnect();
1050
1596
  }
1051
- throw error;
1052
- }
1053
- };
1054
- }
1055
-
1056
- // src/schema/zod.ts
1057
- function normalizeZodPath(path) {
1058
- if (path.length === 0) {
1059
- return "";
1597
+ });
1598
+ socket.on("error", (error) => {
1599
+ logger.warn("App bridge socket error.", { error: String(error) });
1600
+ });
1601
+ return connectPromise;
1060
1602
  }
1061
- return path.filter((segment) => typeof segment === "string" || typeof segment === "number").map((segment) => String(segment)).join(".");
1062
- }
1063
- function fromZodSchema(schema) {
1064
- return fromSafeParseSchema({
1065
- safeParse(value) {
1066
- const result = schema.safeParse(value);
1067
- if (result.success) {
1068
- return { success: true, data: result.data };
1069
- }
1070
- const issues = result.error.issues.map((issue) => ({
1071
- path: normalizeZodPath(issue.path),
1072
- message: issue.message
1073
- }));
1603
+ async function invokeAction(name, payload, sendOptions) {
1604
+ try {
1605
+ await connect();
1606
+ } catch (error) {
1074
1607
  return {
1075
- success: false,
1608
+ ok: false,
1609
+ requestId: sendOptions?.requestId ?? "unknown",
1610
+ ts: Date.now(),
1076
1611
  error: {
1077
- message: "Schema validation failed.",
1078
- issues
1612
+ code: "UNAUTHORIZED",
1613
+ message: error instanceof Error ? error.message : "Failed to authenticate."
1079
1614
  }
1080
1615
  };
1081
1616
  }
1082
- });
1083
- }
1084
-
1085
- // src/index.ts
1086
- function isHostOptions(options) {
1087
- return "server" in options;
1088
- }
1089
- function createShardwire(options) {
1090
- if (!isHostOptions(options)) {
1091
- assertConsumerOptions(options);
1092
- return createConsumerShardwire(options);
1093
- }
1094
- assertHostOptions(options);
1095
- let ownedClientPromise;
1096
- if (!options.client && options.token) {
1097
- ownedClientPromise = resolveDiscordClient(options).then((state) => {
1098
- if (!state.owned || !state.client) {
1099
- return void 0;
1100
- }
1617
+ if (!currentCapabilities.actions.includes(name)) {
1618
+ return {
1619
+ ok: false,
1620
+ requestId: sendOptions?.requestId ?? "unknown",
1621
+ ts: Date.now(),
1622
+ error: {
1623
+ code: "FORBIDDEN",
1624
+ message: `Action "${name}" is not available for this app.`
1625
+ }
1626
+ };
1627
+ }
1628
+ if (!socket || socket.readyState !== 1 || !isAuthed) {
1101
1629
  return {
1102
- destroy: () => state.client?.destroy()
1630
+ ok: false,
1631
+ requestId: sendOptions?.requestId ?? "unknown",
1632
+ ts: Date.now(),
1633
+ error: {
1634
+ code: "DISCONNECTED",
1635
+ message: "Not connected to the bot bridge."
1636
+ }
1103
1637
  };
1104
- }).catch((error) => {
1105
- options.logger?.error?.("Failed to initialize discord.js client from token.", {
1106
- error: String(error)
1638
+ }
1639
+ const requestId = sendOptions?.requestId ?? createRequestId();
1640
+ const timeoutMs = sendOptions?.timeoutMs ?? requestTimeoutMs;
1641
+ const promise = new Promise((resolve, reject) => {
1642
+ const timer = setTimeout(() => {
1643
+ pendingRequests.delete(requestId);
1644
+ reject(new AppRequestError("TIMEOUT", `Action "${name}" timed out after ${timeoutMs}ms.`));
1645
+ }, timeoutMs);
1646
+ pendingRequests.set(requestId, {
1647
+ resolve,
1648
+ reject: (error) => reject(error),
1649
+ timer
1107
1650
  });
1108
- return void 0;
1109
1651
  });
1652
+ sendRaw(
1653
+ stringifyEnvelope(
1654
+ makeEnvelope(
1655
+ "action.request",
1656
+ {
1657
+ name,
1658
+ data: payload
1659
+ },
1660
+ { requestId }
1661
+ )
1662
+ )
1663
+ );
1664
+ try {
1665
+ return await promise;
1666
+ } catch (error) {
1667
+ const code = error instanceof AppRequestError ? error.code : !socket || socket.readyState !== 1 ? "DISCONNECTED" : "TIMEOUT";
1668
+ return {
1669
+ ok: false,
1670
+ requestId,
1671
+ ts: Date.now(),
1672
+ error: {
1673
+ code,
1674
+ message: error instanceof Error ? error.message : "Action request failed."
1675
+ }
1676
+ };
1677
+ }
1110
1678
  }
1111
- return createHostShardwire(options, {
1112
- onClose: async () => {
1113
- const owned = await ownedClientPromise;
1114
- owned?.destroy();
1679
+ const actions = {
1680
+ sendMessage: (payload, sendOptions) => invokeAction("sendMessage", payload, sendOptions),
1681
+ editMessage: (payload, sendOptions) => invokeAction("editMessage", payload, sendOptions),
1682
+ deleteMessage: (payload, sendOptions) => invokeAction("deleteMessage", payload, sendOptions),
1683
+ replyToInteraction: (payload, sendOptions) => invokeAction("replyToInteraction", payload, sendOptions),
1684
+ deferInteraction: (payload, sendOptions) => invokeAction("deferInteraction", payload, sendOptions),
1685
+ followUpInteraction: (payload, sendOptions) => invokeAction("followUpInteraction", payload, sendOptions),
1686
+ banMember: (payload, sendOptions) => invokeAction("banMember", payload, sendOptions),
1687
+ kickMember: (payload, sendOptions) => invokeAction("kickMember", payload, sendOptions),
1688
+ addMemberRole: (payload, sendOptions) => invokeAction("addMemberRole", payload, sendOptions),
1689
+ removeMemberRole: (payload, sendOptions) => invokeAction("removeMemberRole", payload, sendOptions)
1690
+ };
1691
+ return {
1692
+ actions,
1693
+ async ready() {
1694
+ await connect();
1695
+ if (capabilityError) {
1696
+ throw capabilityError;
1697
+ }
1698
+ },
1699
+ async close() {
1700
+ isClosed = true;
1701
+ isAuthed = false;
1702
+ currentConnectionId = null;
1703
+ currentCapabilities = { events: [], actions: [] };
1704
+ capabilityError = null;
1705
+ subscribedEntries.clear();
1706
+ rejectConnect("App bridge has been closed.");
1707
+ if (reconnectTimer) {
1708
+ clearTimeout(reconnectTimer);
1709
+ reconnectTimer = null;
1710
+ }
1711
+ rejectAllPending("DISCONNECTED", "App bridge has been closed.");
1712
+ if (!socket) {
1713
+ return;
1714
+ }
1715
+ await new Promise((resolve) => {
1716
+ const current = socket;
1717
+ if (!current) {
1718
+ resolve();
1719
+ return;
1720
+ }
1721
+ current.once("close", () => resolve());
1722
+ current.close();
1723
+ });
1724
+ socket = null;
1725
+ },
1726
+ connected() {
1727
+ return Boolean(socket && socket.readyState === 1 && isAuthed);
1728
+ },
1729
+ connectionId() {
1730
+ return currentConnectionId;
1731
+ },
1732
+ capabilities() {
1733
+ return {
1734
+ events: [...currentCapabilities.events],
1735
+ actions: [...currentCapabilities.actions]
1736
+ };
1737
+ },
1738
+ on(name, handler, filter) {
1739
+ if (currentCapabilities.events.length > 0 && !currentCapabilities.events.includes(name)) {
1740
+ throw new BridgeCapabilityError("event", name, `Event "${name}" is not available for this app.`);
1741
+ }
1742
+ const casted = handler;
1743
+ const subscription = filter ? { name, filter } : { name };
1744
+ const entry = {
1745
+ handler: casted,
1746
+ subscription,
1747
+ signature: serializeEventSubscription(subscription)
1748
+ };
1749
+ const existing = eventHandlers.get(name);
1750
+ if (existing) {
1751
+ existing.add(entry);
1752
+ } else {
1753
+ eventHandlers.set(name, /* @__PURE__ */ new Set([entry]));
1754
+ }
1755
+ if (this.connected()) {
1756
+ syncSubscriptions();
1757
+ }
1758
+ return () => {
1759
+ const handlers = eventHandlers.get(name);
1760
+ if (!handlers) {
1761
+ return;
1762
+ }
1763
+ handlers.delete(entry);
1764
+ if (handlers.size === 0) {
1765
+ eventHandlers.delete(name);
1766
+ }
1767
+ if (this.connected()) {
1768
+ syncSubscriptions();
1769
+ }
1770
+ };
1771
+ },
1772
+ off(name, handler) {
1773
+ const handlers = eventHandlers.get(name);
1774
+ if (!handlers) {
1775
+ return;
1776
+ }
1777
+ for (const entry of [...handlers]) {
1778
+ if (entry.handler === handler) {
1779
+ handlers.delete(entry);
1780
+ }
1781
+ }
1782
+ if (handlers.size === 0) {
1783
+ eventHandlers.delete(name);
1784
+ }
1785
+ if (this.connected()) {
1786
+ syncSubscriptions();
1787
+ }
1115
1788
  }
1116
- });
1789
+ };
1117
1790
  }
1118
1791
  // Annotate the CommonJS export names for ESM import in node:
1119
1792
  0 && (module.exports = {
1120
- createShardwire,
1121
- fromSafeParseSchema,
1122
- fromZodSchema
1793
+ BridgeCapabilityError,
1794
+ connectBotBridge,
1795
+ createBotBridge
1123
1796
  });
1124
1797
  //# sourceMappingURL=index.js.map