shardwire 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -266
- package/dist/index.d.mts +262 -129
- package/dist/index.d.ts +262 -129
- package/dist/index.js +1615 -942
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1616 -930
- package/dist/index.mjs.map +1 -1
- package/package.json +22 -34
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
23
|
+
BridgeCapabilityError: () => BridgeCapabilityError,
|
|
24
|
+
connectBotBridge: () => connectBotBridge,
|
|
25
|
+
createBotBridge: () => createBotBridge
|
|
36
26
|
});
|
|
37
27
|
module.exports = __toCommonJS(index_exports);
|
|
38
28
|
|
|
39
|
-
// src/
|
|
40
|
-
var
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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/
|
|
77
|
-
var
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/discord/runtime/serializers.ts
|
|
121
|
+
function serializeEmbeds(message) {
|
|
122
|
+
return message.embeds.map((embed) => embed.toJSON());
|
|
99
123
|
}
|
|
100
|
-
function
|
|
101
|
-
return
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
148
|
-
if (
|
|
149
|
-
|
|
213
|
+
function serializeInteractionMessage(interaction) {
|
|
214
|
+
if ("message" in interaction && interaction.message) {
|
|
215
|
+
return serializeMessage(interaction.message);
|
|
150
216
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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 (
|
|
158
|
-
|
|
237
|
+
if (interaction.isContextMenuCommand()) {
|
|
238
|
+
return {
|
|
239
|
+
...base,
|
|
240
|
+
kind: "contextMenu",
|
|
241
|
+
commandName: interaction.commandName
|
|
242
|
+
};
|
|
159
243
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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 (
|
|
167
|
-
|
|
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 (
|
|
170
|
-
|
|
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 (
|
|
173
|
-
|
|
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 (
|
|
176
|
-
|
|
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 (
|
|
179
|
-
|
|
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 (
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
206
|
-
if (
|
|
207
|
-
return
|
|
348
|
+
ready() {
|
|
349
|
+
if (this.readyPromise) {
|
|
350
|
+
return this.readyPromise;
|
|
208
351
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (typeof message !== "string") {
|
|
212
|
-
return null;
|
|
352
|
+
if (this.isReady()) {
|
|
353
|
+
return Promise.resolve();
|
|
213
354
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
if (
|
|
283
|
-
|
|
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
|
-
|
|
286
|
-
connectReject = null;
|
|
287
|
-
connectPromise = null;
|
|
524
|
+
return channel;
|
|
288
525
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
531
|
+
return channel;
|
|
294
532
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
340
|
-
|
|
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
|
-
|
|
343
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
435
|
-
|
|
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("
|
|
910
|
+
this.logger.warn("Transport socket error.", { connectionId: state.id, error: String(error) });
|
|
456
911
|
});
|
|
457
|
-
return connectPromise;
|
|
458
912
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
494
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
543
|
-
const
|
|
544
|
-
if (!
|
|
994
|
+
case "unsubscribe": {
|
|
995
|
+
const rawSubscriptions = envelope.payload.subscriptions;
|
|
996
|
+
if (!Array.isArray(rawSubscriptions)) {
|
|
545
997
|
return;
|
|
546
998
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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/
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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 (
|
|
624
|
-
|
|
1219
|
+
if (options.reconnect?.maxDelayMs !== void 0) {
|
|
1220
|
+
assertPositiveNumber("reconnect.maxDelayMs", options.reconnect.maxDelayMs);
|
|
625
1221
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
|
|
631
|
-
|
|
1231
|
+
return {
|
|
1232
|
+
events: [...events],
|
|
1233
|
+
actions: [...actions]
|
|
1234
|
+
};
|
|
632
1235
|
}
|
|
633
1236
|
|
|
634
|
-
// src/
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
}
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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/
|
|
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/
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
return
|
|
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/
|
|
689
|
-
var
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
751
|
-
|
|
752
|
-
if (
|
|
753
|
-
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
-
|
|
801
|
-
if (
|
|
802
|
-
|
|
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
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
|
|
1456
|
+
for (const entry of handlers) {
|
|
1457
|
+
desiredSubscriptions.set(entry.signature, entry.subscription);
|
|
1458
|
+
}
|
|
905
1459
|
}
|
|
1460
|
+
return desiredSubscriptions;
|
|
906
1461
|
}
|
|
907
|
-
|
|
908
|
-
if (socket.readyState
|
|
909
|
-
|
|
1462
|
+
function syncSubscriptions() {
|
|
1463
|
+
if (!isAuthed || !socket || socket.readyState !== 1) {
|
|
1464
|
+
return;
|
|
910
1465
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
952
|
-
|
|
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
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
|
|
1608
|
+
ok: false,
|
|
1609
|
+
requestId: sendOptions?.requestId ?? "unknown",
|
|
1610
|
+
ts: Date.now(),
|
|
1076
1611
|
error: {
|
|
1077
|
-
|
|
1078
|
-
|
|
1612
|
+
code: "UNAUTHORIZED",
|
|
1613
|
+
message: error instanceof Error ? error.message : "Failed to authenticate."
|
|
1079
1614
|
}
|
|
1080
1615
|
};
|
|
1081
1616
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
1105
|
-
|
|
1106
|
-
|
|
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
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1793
|
+
BridgeCapabilityError,
|
|
1794
|
+
connectBotBridge,
|
|
1795
|
+
createBotBridge
|
|
1123
1796
|
});
|
|
1124
1797
|
//# sourceMappingURL=index.js.map
|