@helyx/bot 0.1.3 → 0.2.1
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/CHANGELOG.md +34 -0
- package/README.md +2 -0
- package/dist/cloud-connector.d.ts +43 -0
- package/dist/cloud-connector.js +671 -0
- package/dist/cloud-control-dispatcher.d.ts +16 -0
- package/dist/cloud-control-dispatcher.js +250 -0
- package/dist/cloud-management-module.d.ts +9 -0
- package/dist/cloud-management-module.js +491 -0
- package/dist/control-server.d.ts +1 -0
- package/dist/control-server.js +1 -0
- package/dist/index.js +47 -3
- package/dist/management-module.js +1 -0
- package/dist/permissions-module.js +1 -0
- package/package.json +13 -8
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { defineModule, } from "@helyx/sdk";
|
|
2
|
+
import { CloudConnectorUserError, } from "./cloud-connector.js";
|
|
3
|
+
const CONNECT_MODAL_ID = "helyx.cloud.connect";
|
|
4
|
+
const CONNECT_CODE_FIELD_ID = "helyx.cloud.connect.code";
|
|
5
|
+
const DISCONNECT_MODAL_ID = "helyx.cloud.disconnect";
|
|
6
|
+
const DISCONNECT_CONFIRM_FIELD_ID = "helyx.cloud.disconnect.confirm";
|
|
7
|
+
const ROTATE_MODAL_ID = "helyx.cloud.rotate";
|
|
8
|
+
const ROTATE_CONFIRM_FIELD_ID = "helyx.cloud.rotate.confirm";
|
|
9
|
+
const SUPPORT_MODAL_ID = "helyx.cloud.support";
|
|
10
|
+
const SUPPORT_ENABLED_FIELD_ID = "helyx.cloud.support.enabled";
|
|
11
|
+
const SUPPORT_DATA_FIELD_ID = "helyx.cloud.support.data";
|
|
12
|
+
const DIAGNOSTICS_ENABLED_FIELD_ID = "helyx.cloud.support.diagnostics";
|
|
13
|
+
const VIOLET = 0x7c3aed;
|
|
14
|
+
const SUCCESS = 0x16a34a;
|
|
15
|
+
const DANGER = 0xdc2626;
|
|
16
|
+
export function createCloudManagementModule(input) {
|
|
17
|
+
const requireOwner = async (context) => {
|
|
18
|
+
if (await isDeploymentOwner(input.client, context.userId, input.configuredOwnerUserIds))
|
|
19
|
+
return true;
|
|
20
|
+
await context.reply({
|
|
21
|
+
ephemeral: true,
|
|
22
|
+
componentsV2: {
|
|
23
|
+
accentColor: DANGER,
|
|
24
|
+
text: [
|
|
25
|
+
{
|
|
26
|
+
content: "## Deployment owner required\nOnly the Discord application owner, an application team member, or a user listed in `HELYX_OWNER_USER_IDS` can change Cloud Connect.",
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
return false;
|
|
32
|
+
};
|
|
33
|
+
return defineModule({
|
|
34
|
+
manifest: {
|
|
35
|
+
manifestVersion: "0.1",
|
|
36
|
+
releaseStatus: "live",
|
|
37
|
+
id: "helyx.cloud-connect",
|
|
38
|
+
name: "Helyx Cloud Connect",
|
|
39
|
+
version: "0.1.0",
|
|
40
|
+
description: "Links this self-hosted Helyx deployment to the hosted dashboard.",
|
|
41
|
+
entry: "./dist/cloud-management-module.js",
|
|
42
|
+
framework: "^0.1.0",
|
|
43
|
+
dependencies: [],
|
|
44
|
+
permissions: [],
|
|
45
|
+
contributions: { commands: ["helyx-cloud"], events: [] },
|
|
46
|
+
},
|
|
47
|
+
defaultPermissions: {
|
|
48
|
+
module: restrictedPolicy(),
|
|
49
|
+
commands: {
|
|
50
|
+
"helyx-cloud.connect": restrictedPolicy(),
|
|
51
|
+
"helyx-cloud.status": restrictedPolicy(),
|
|
52
|
+
"helyx-cloud.disconnect": restrictedPolicy(),
|
|
53
|
+
"helyx-cloud.rotate": restrictedPolicy(),
|
|
54
|
+
"helyx-cloud.support": restrictedPolicy(),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
interactions: {
|
|
58
|
+
commands: [
|
|
59
|
+
{
|
|
60
|
+
name: "helyx-cloud",
|
|
61
|
+
description: "Manage this self-hosted bot's Helyx Cloud connection",
|
|
62
|
+
registration: "global",
|
|
63
|
+
contexts: ["guild", "bot_dm"],
|
|
64
|
+
integrationTypes: ["guild_install"],
|
|
65
|
+
subcommands: [
|
|
66
|
+
{
|
|
67
|
+
name: "connect",
|
|
68
|
+
description: "Connect this deployment using a one-use code",
|
|
69
|
+
rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
|
|
70
|
+
async execute(context) {
|
|
71
|
+
if (!(await requireOwner(context)))
|
|
72
|
+
return;
|
|
73
|
+
await context.showModal({
|
|
74
|
+
customId: CONNECT_MODAL_ID,
|
|
75
|
+
title: "Connect to Helyx Cloud",
|
|
76
|
+
text: [
|
|
77
|
+
{
|
|
78
|
+
content: "Create a short-lived connection code in `app.helyx.gg`, then paste it below. The code is used once and is not stored.",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
fields: [
|
|
82
|
+
{
|
|
83
|
+
customId: CONNECT_CODE_FIELD_ID,
|
|
84
|
+
label: "Cloud Connect code",
|
|
85
|
+
description: "Five groups of four letters and numbers.",
|
|
86
|
+
style: "short",
|
|
87
|
+
required: true,
|
|
88
|
+
minLength: 24,
|
|
89
|
+
maxLength: 24,
|
|
90
|
+
placeholder: "ABCD-EFGH-JKLM-NPQR-STUV",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "status",
|
|
98
|
+
description: "Show this deployment's Cloud Connect status",
|
|
99
|
+
rateLimit: { scope: "user", limit: 20, windowSeconds: 60 },
|
|
100
|
+
async execute(context) {
|
|
101
|
+
if (!(await requireOwner(context)))
|
|
102
|
+
return;
|
|
103
|
+
await context.reply(statusResponse(input.connector.status()));
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "disconnect",
|
|
108
|
+
description: "Revoke this deployment's Cloud Connect identity",
|
|
109
|
+
rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
|
|
110
|
+
async execute(context) {
|
|
111
|
+
if (!(await requireOwner(context)))
|
|
112
|
+
return;
|
|
113
|
+
await context.showModal({
|
|
114
|
+
customId: DISCONNECT_MODAL_ID,
|
|
115
|
+
title: "Disconnect Helyx Cloud",
|
|
116
|
+
text: [
|
|
117
|
+
{
|
|
118
|
+
content: "This revokes the saved connector identity. A new dashboard code will be required to reconnect.",
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
fields: [
|
|
122
|
+
{
|
|
123
|
+
type: "string-select",
|
|
124
|
+
customId: DISCONNECT_CONFIRM_FIELD_ID,
|
|
125
|
+
label: "Confirm disconnection",
|
|
126
|
+
required: true,
|
|
127
|
+
minValues: 1,
|
|
128
|
+
maxValues: 1,
|
|
129
|
+
options: [
|
|
130
|
+
{
|
|
131
|
+
label: "Keep Cloud Connect",
|
|
132
|
+
value: "keep",
|
|
133
|
+
default: true,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
label: "Disconnect and revoke",
|
|
137
|
+
value: "disconnect",
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "rotate",
|
|
147
|
+
description: "Replace the saved Cloud Connect credential",
|
|
148
|
+
rateLimit: { scope: "user", limit: 3, windowSeconds: 60 },
|
|
149
|
+
async execute(context) {
|
|
150
|
+
if (!(await requireOwner(context)))
|
|
151
|
+
return;
|
|
152
|
+
await context.showModal({
|
|
153
|
+
customId: ROTATE_MODAL_ID,
|
|
154
|
+
title: "Rotate Cloud credential",
|
|
155
|
+
text: [
|
|
156
|
+
{
|
|
157
|
+
content: "Rotation replaces the encrypted connector key and immediately reconnects the bot. Local Discord features keep running.",
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
fields: [
|
|
161
|
+
{
|
|
162
|
+
type: "string-select",
|
|
163
|
+
customId: ROTATE_CONFIRM_FIELD_ID,
|
|
164
|
+
label: "Confirm credential rotation",
|
|
165
|
+
required: true,
|
|
166
|
+
minValues: 1,
|
|
167
|
+
maxValues: 1,
|
|
168
|
+
options: [
|
|
169
|
+
{
|
|
170
|
+
label: "Cancel",
|
|
171
|
+
value: "cancel",
|
|
172
|
+
default: true,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
label: "Rotate credential",
|
|
176
|
+
value: "rotate",
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "support",
|
|
186
|
+
description: "Choose what Helyx support may inspect",
|
|
187
|
+
rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
|
|
188
|
+
async execute(context) {
|
|
189
|
+
if (!(await requireOwner(context)))
|
|
190
|
+
return;
|
|
191
|
+
const consent = await input.connector.getSupportConsent();
|
|
192
|
+
await context.showModal({
|
|
193
|
+
customId: SUPPORT_MODAL_ID,
|
|
194
|
+
title: "Cloud support access",
|
|
195
|
+
text: [
|
|
196
|
+
{
|
|
197
|
+
content: "Support inspection is off by default. Submitted message content, secrets and arbitrary logs are never included.",
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
fields: [
|
|
201
|
+
{
|
|
202
|
+
type: "string-select",
|
|
203
|
+
customId: SUPPORT_ENABLED_FIELD_ID,
|
|
204
|
+
label: "Allow bounded support inspection",
|
|
205
|
+
required: true,
|
|
206
|
+
minValues: 1,
|
|
207
|
+
maxValues: 1,
|
|
208
|
+
options: [
|
|
209
|
+
{
|
|
210
|
+
label: "No",
|
|
211
|
+
value: "no",
|
|
212
|
+
default: !consent.supportInspectionEnabled,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
label: "Yes",
|
|
216
|
+
value: "yes",
|
|
217
|
+
default: consent.supportInspectionEnabled,
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
type: "string-select",
|
|
223
|
+
customId: SUPPORT_DATA_FIELD_ID,
|
|
224
|
+
label: "Allowed support data",
|
|
225
|
+
description: "Select only the bounded metadata support may inspect.",
|
|
226
|
+
required: false,
|
|
227
|
+
minValues: 0,
|
|
228
|
+
maxValues: 5,
|
|
229
|
+
options: [
|
|
230
|
+
["Module inventory", "module_inventory"],
|
|
231
|
+
["Redacted configuration", "configuration"],
|
|
232
|
+
["Permission summaries", "permissions"],
|
|
233
|
+
["Logging policy", "logging"],
|
|
234
|
+
["Connector health", "health"],
|
|
235
|
+
].map(([label, value]) => ({
|
|
236
|
+
label,
|
|
237
|
+
value,
|
|
238
|
+
default: consent.supportDataClasses.includes(value),
|
|
239
|
+
})),
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
type: "string-select",
|
|
243
|
+
customId: DIAGNOSTICS_ENABLED_FIELD_ID,
|
|
244
|
+
label: "Allow bounded diagnostics",
|
|
245
|
+
required: true,
|
|
246
|
+
minValues: 1,
|
|
247
|
+
maxValues: 1,
|
|
248
|
+
options: [
|
|
249
|
+
{
|
|
250
|
+
label: "No",
|
|
251
|
+
value: "no",
|
|
252
|
+
default: !consent.diagnosticsEnabled,
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
label: "Yes",
|
|
256
|
+
value: "yes",
|
|
257
|
+
default: consent.diagnosticsEnabled,
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
modals: [
|
|
269
|
+
{
|
|
270
|
+
customId: CONNECT_MODAL_ID,
|
|
271
|
+
async execute(context) {
|
|
272
|
+
if (!(await requireOwner(context)))
|
|
273
|
+
return;
|
|
274
|
+
await context.deferReply?.(true);
|
|
275
|
+
try {
|
|
276
|
+
const status = await input.connector.enrol(context.getField(CONNECT_CODE_FIELD_ID), "discord");
|
|
277
|
+
await context.reply({
|
|
278
|
+
ephemeral: true,
|
|
279
|
+
componentsV2: {
|
|
280
|
+
accentColor: SUCCESS,
|
|
281
|
+
text: [
|
|
282
|
+
{
|
|
283
|
+
content: `## Cloud Connect saved\nInstallation \`${status.installationId ?? "pending"}\` has been enrolled. The bot is establishing its secure outbound connection now.`,
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
await context.reply(connectorErrorResponse(error));
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
customId: DISCONNECT_MODAL_ID,
|
|
296
|
+
async execute(context) {
|
|
297
|
+
if (!(await requireOwner(context)))
|
|
298
|
+
return;
|
|
299
|
+
const confirmation = context.getStringSelectValues(DISCONNECT_CONFIRM_FIELD_ID)[0];
|
|
300
|
+
if (confirmation !== "disconnect") {
|
|
301
|
+
await context.reply({
|
|
302
|
+
ephemeral: true,
|
|
303
|
+
componentsV2: {
|
|
304
|
+
accentColor: VIOLET,
|
|
305
|
+
text: [
|
|
306
|
+
{
|
|
307
|
+
content: "## Cloud Connect kept\nNo connection or credential changes were made.",
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
await context.deferReply?.(true);
|
|
315
|
+
try {
|
|
316
|
+
await input.connector.disconnect();
|
|
317
|
+
await context.reply({
|
|
318
|
+
ephemeral: true,
|
|
319
|
+
componentsV2: {
|
|
320
|
+
accentColor: SUCCESS,
|
|
321
|
+
text: [
|
|
322
|
+
{
|
|
323
|
+
content: "## Cloud Connect disconnected\nThe local connector identity has been revoked. This bot continues to work locally.",
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
await context.reply(connectorErrorResponse(error));
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
customId: ROTATE_MODAL_ID,
|
|
336
|
+
async execute(context) {
|
|
337
|
+
if (!(await requireOwner(context)))
|
|
338
|
+
return;
|
|
339
|
+
if (context.getStringSelectValues(ROTATE_CONFIRM_FIELD_ID)[0] !==
|
|
340
|
+
"rotate") {
|
|
341
|
+
await context.reply({
|
|
342
|
+
ephemeral: true,
|
|
343
|
+
componentsV2: {
|
|
344
|
+
accentColor: VIOLET,
|
|
345
|
+
text: [
|
|
346
|
+
{
|
|
347
|
+
content: "## Rotation cancelled\nThe connector credential was not changed.",
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
await context.deferReply?.(true);
|
|
355
|
+
try {
|
|
356
|
+
await input.connector.rotate();
|
|
357
|
+
await context.reply({
|
|
358
|
+
ephemeral: true,
|
|
359
|
+
componentsV2: {
|
|
360
|
+
accentColor: SUCCESS,
|
|
361
|
+
text: [
|
|
362
|
+
{
|
|
363
|
+
content: "## Credential rotated\nThe previous key was revoked and Cloud Connect is reconnecting with its new encrypted identity.",
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
await context.reply(connectorErrorResponse(error));
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
customId: SUPPORT_MODAL_ID,
|
|
376
|
+
async execute(context) {
|
|
377
|
+
if (!(await requireOwner(context)))
|
|
378
|
+
return;
|
|
379
|
+
await context.deferReply?.(true);
|
|
380
|
+
try {
|
|
381
|
+
const enabled = context.getStringSelectValues(SUPPORT_ENABLED_FIELD_ID)[0] ===
|
|
382
|
+
"yes";
|
|
383
|
+
const updated = await input.connector.updateSupportConsent({
|
|
384
|
+
supportInspectionEnabled: enabled,
|
|
385
|
+
supportDataClasses: [
|
|
386
|
+
...context.getStringSelectValues(SUPPORT_DATA_FIELD_ID),
|
|
387
|
+
],
|
|
388
|
+
diagnosticsEnabled: context.getStringSelectValues(DIAGNOSTICS_ENABLED_FIELD_ID)[0] === "yes",
|
|
389
|
+
});
|
|
390
|
+
await context.reply({
|
|
391
|
+
ephemeral: true,
|
|
392
|
+
componentsV2: {
|
|
393
|
+
accentColor: SUCCESS,
|
|
394
|
+
text: [
|
|
395
|
+
{
|
|
396
|
+
content: [
|
|
397
|
+
"## Support consent saved",
|
|
398
|
+
`**Inspection:** ${updated.supportInspectionEnabled ? "Allowed" : "Off"}`,
|
|
399
|
+
`**Allowed data:** ${updated.supportDataClasses.length ? updated.supportDataClasses.join(", ") : "None"}`,
|
|
400
|
+
`**Diagnostics:** ${updated.diagnosticsEnabled ? "Allowed" : "Off"}`,
|
|
401
|
+
].join("\n"),
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
await context.reply(connectorErrorResponse(error));
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
],
|
|
413
|
+
},
|
|
414
|
+
async start() { },
|
|
415
|
+
async stop() { },
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
async function isDeploymentOwner(client, userId, configuredOwnerUserIds) {
|
|
419
|
+
if (configuredOwnerUserIds.includes(userId))
|
|
420
|
+
return true;
|
|
421
|
+
const application = await client.application?.fetch();
|
|
422
|
+
const owner = application?.owner;
|
|
423
|
+
if (!owner)
|
|
424
|
+
return false;
|
|
425
|
+
if ("members" in owner)
|
|
426
|
+
return owner.members.has(userId);
|
|
427
|
+
return owner.id === userId;
|
|
428
|
+
}
|
|
429
|
+
function statusResponse(status) {
|
|
430
|
+
const labels = {
|
|
431
|
+
disabled: "Disabled by deployment configuration",
|
|
432
|
+
unpaired: "Ready for a connection code",
|
|
433
|
+
connecting: "Connecting securely",
|
|
434
|
+
online: "Connected",
|
|
435
|
+
offline: "Temporarily offline; local operation continues",
|
|
436
|
+
revoked: "Disconnected and revoked",
|
|
437
|
+
stopped: "Stopped",
|
|
438
|
+
};
|
|
439
|
+
return {
|
|
440
|
+
ephemeral: true,
|
|
441
|
+
componentsV2: {
|
|
442
|
+
accentColor: status.state === "online"
|
|
443
|
+
? SUCCESS
|
|
444
|
+
: status.state === "revoked"
|
|
445
|
+
? DANGER
|
|
446
|
+
: VIOLET,
|
|
447
|
+
text: [
|
|
448
|
+
{
|
|
449
|
+
content: [
|
|
450
|
+
"## Helyx Cloud Connect",
|
|
451
|
+
`**Status:** ${labels[status.state]}`,
|
|
452
|
+
status.installationId
|
|
453
|
+
? `**Installation:** \`${status.installationId}\``
|
|
454
|
+
: undefined,
|
|
455
|
+
status.lastConnectedAt
|
|
456
|
+
? `**Last connected:** <t:${Math.floor(status.lastConnectedAt.getTime() / 1_000)}:R>`
|
|
457
|
+
: undefined,
|
|
458
|
+
]
|
|
459
|
+
.filter(Boolean)
|
|
460
|
+
.join("\n"),
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function connectorErrorResponse(error) {
|
|
467
|
+
return {
|
|
468
|
+
ephemeral: true,
|
|
469
|
+
componentsV2: {
|
|
470
|
+
accentColor: DANGER,
|
|
471
|
+
text: [
|
|
472
|
+
{
|
|
473
|
+
content: `## Cloud Connect could not complete that request\n${error instanceof CloudConnectorUserError
|
|
474
|
+
? error.message
|
|
475
|
+
: "Cloud Connect is temporarily unavailable. The local bot continues to work normally."}`,
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function restrictedPolicy() {
|
|
482
|
+
return {
|
|
483
|
+
everyone: false,
|
|
484
|
+
requiredPermissions: ["MANAGE_GUILD"],
|
|
485
|
+
allowedRoleIds: [],
|
|
486
|
+
deniedRoleIds: [],
|
|
487
|
+
allowedUserIds: [],
|
|
488
|
+
deniedUserIds: [],
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
//# sourceMappingURL=cloud-management-module.js.map
|
package/dist/control-server.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ interface DashboardModuleDefinition {
|
|
|
34
34
|
category: string;
|
|
35
35
|
icon: string;
|
|
36
36
|
availability: "available";
|
|
37
|
+
releaseStatus: "planned" | "testing" | "beta" | "live";
|
|
37
38
|
requiredPermissions: string[];
|
|
38
39
|
settings: readonly unknown[];
|
|
39
40
|
commands: DashboardCommandDefinition[];
|
package/dist/control-server.js
CHANGED
|
@@ -575,6 +575,7 @@ function mapDashboardModule(module) {
|
|
|
575
575
|
category: dashboard.category[0].toUpperCase() + dashboard.category.slice(1),
|
|
576
576
|
icon: dashboard.icon,
|
|
577
577
|
availability: "available",
|
|
578
|
+
releaseStatus: module.manifest.releaseStatus,
|
|
578
579
|
requiredPermissions: [...module.manifest.permissions],
|
|
579
580
|
settings: dashboard.settings,
|
|
580
581
|
commands: dashboardCommands(module),
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,11 @@ import { createPlatformServices } from "@helyx/platform";
|
|
|
6
6
|
import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
|
|
7
7
|
import { Client, Events, GatewayIntentBits, Partials } from "discord.js";
|
|
8
8
|
import pino from "pino";
|
|
9
|
+
import { createRequire } from "node:module";
|
|
9
10
|
import { join } from "node:path";
|
|
10
11
|
import { pathToFileURL } from "node:url";
|
|
12
|
+
import { CloudConnectorController } from "./cloud-connector.js";
|
|
13
|
+
import { createCloudManagementModule } from "./cloud-management-module.js";
|
|
11
14
|
import { installInteractions } from "./interactions.js";
|
|
12
15
|
import { installGuildTracking } from "./guilds.js";
|
|
13
16
|
import { loadConfiguredModulePackages } from "./modules.js";
|
|
@@ -66,9 +69,29 @@ export async function startBot(processEnvironment = process.env) {
|
|
|
66
69
|
allowedMentions: { parse: [] },
|
|
67
70
|
});
|
|
68
71
|
const guildResources = new DiscordGuildResourceDirectory(client);
|
|
72
|
+
const cloudConnector = new CloudConnectorController({
|
|
73
|
+
environment,
|
|
74
|
+
database,
|
|
75
|
+
client,
|
|
76
|
+
modules: modulePackages,
|
|
77
|
+
coreVersion: readBotVersion(),
|
|
78
|
+
logger: bootstrapLogger,
|
|
79
|
+
});
|
|
69
80
|
const permissionsModule = createPermissionsModule(featureModules);
|
|
70
81
|
const managementModule = createManagementModule(featureModules, guildResources);
|
|
71
|
-
const
|
|
82
|
+
const cloudManagementModule = environment.HELYX_CLOUD_CONNECT_ENABLED
|
|
83
|
+
? createCloudManagementModule({
|
|
84
|
+
connector: cloudConnector,
|
|
85
|
+
client,
|
|
86
|
+
configuredOwnerUserIds: environment.HELYX_OWNER_USER_IDS,
|
|
87
|
+
})
|
|
88
|
+
: undefined;
|
|
89
|
+
const modules = [
|
|
90
|
+
managementModule,
|
|
91
|
+
permissionsModule,
|
|
92
|
+
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
93
|
+
...featureModules,
|
|
94
|
+
];
|
|
72
95
|
const platformServices = new Map(createPlatformServices(database, modules));
|
|
73
96
|
if (featureModules.some(({ manifest }) => manifest.id === "helyx.logging")) {
|
|
74
97
|
platformServices.set(HELYX_SERVICE_NAMES.moduleLogging, new DiscordModuleLoggingService(client, database, modules, bootstrapLogger));
|
|
@@ -79,7 +102,12 @@ export async function startBot(processEnvironment = process.env) {
|
|
|
79
102
|
logger: bootstrapLogger,
|
|
80
103
|
services: platformServices,
|
|
81
104
|
});
|
|
82
|
-
const
|
|
105
|
+
const enabledCoreModuleIds = new Set([
|
|
106
|
+
managementModule.manifest.id,
|
|
107
|
+
permissionsModule.manifest.id,
|
|
108
|
+
...(cloudManagementModule ? [cloudManagementModule.manifest.id] : []),
|
|
109
|
+
]);
|
|
110
|
+
const prepareGuilds = installGuildTracking(client, database, modules, bootstrapLogger, enabledCoreModuleIds);
|
|
83
111
|
const commandsReady = installInteractions(client, modules, {
|
|
84
112
|
...(environment.DISCORD_INTERNAL_GUILD_ID
|
|
85
113
|
? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
|
|
@@ -87,6 +115,8 @@ export async function startBot(processEnvironment = process.env) {
|
|
|
87
115
|
prepare: prepareGuilds,
|
|
88
116
|
}, bootstrapLogger, serviceAccess);
|
|
89
117
|
installModuleEvents(client, modules, bootstrapLogger, serviceAccess);
|
|
118
|
+
const controlService = new BotControlService(database, modules, guildResources, serviceAccess);
|
|
119
|
+
cloudConnector.setControlHandler(controlService);
|
|
90
120
|
let controlServer;
|
|
91
121
|
let shuttingDown = false;
|
|
92
122
|
async function shutdown(reason = "programmatic") {
|
|
@@ -96,6 +126,10 @@ export async function startBot(processEnvironment = process.env) {
|
|
|
96
126
|
process.off("SIGINT", onSigint);
|
|
97
127
|
process.off("SIGTERM", onSigterm);
|
|
98
128
|
bootstrapLogger.info({ event: "bot.shutdown", reason }, "Bot shutdown requested");
|
|
129
|
+
await cloudConnector.stop().catch((error) => {
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
bootstrapLogger.error({ event: "cloud.stop_failed", error: safeError(error) }, "Cloud Connect shutdown failed");
|
|
132
|
+
});
|
|
99
133
|
if (controlServer)
|
|
100
134
|
await controlServer.close().catch((error) => {
|
|
101
135
|
process.exitCode = 1;
|
|
@@ -122,9 +156,10 @@ export async function startBot(processEnvironment = process.env) {
|
|
|
122
156
|
await runtime.start();
|
|
123
157
|
await client.login(environment.DISCORD_TOKEN);
|
|
124
158
|
await commandsReady;
|
|
159
|
+
await cloudConnector.start();
|
|
125
160
|
if (environment.HELYX_BOT_CONTROL_SECRET) {
|
|
126
161
|
controlServer = await createBotControlServer({
|
|
127
|
-
service:
|
|
162
|
+
service: controlService,
|
|
128
163
|
secret: environment.HELYX_BOT_CONTROL_SECRET,
|
|
129
164
|
logger: { level: environment.LOG_LEVEL },
|
|
130
165
|
});
|
|
@@ -142,6 +177,7 @@ export async function startBot(processEnvironment = process.env) {
|
|
|
142
177
|
}
|
|
143
178
|
}
|
|
144
179
|
catch (error) {
|
|
180
|
+
await cloudConnector.stop().catch(() => undefined);
|
|
145
181
|
if (controlServer)
|
|
146
182
|
await controlServer.close().catch(() => undefined);
|
|
147
183
|
try {
|
|
@@ -162,6 +198,14 @@ export async function startBot(processEnvironment = process.env) {
|
|
|
162
198
|
throw error;
|
|
163
199
|
}
|
|
164
200
|
}
|
|
201
|
+
function readBotVersion() {
|
|
202
|
+
const metadata = createRequire(import.meta.url)("../package.json");
|
|
203
|
+
if (typeof metadata !== "object" ||
|
|
204
|
+
metadata === null ||
|
|
205
|
+
typeof metadata.version !== "string")
|
|
206
|
+
throw new Error("Bot package version is unavailable");
|
|
207
|
+
return metadata.version;
|
|
208
|
+
}
|
|
165
209
|
export function gatewayIntentsForModules(modules) {
|
|
166
210
|
const intents = [GatewayIntentBits.Guilds];
|
|
167
211
|
if (modules.some((module) => module.events?.messageReactionAdd?.length)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helyx/bot",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Production Discord runtime for the modular Helyx platform.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"helyx",
|
|
@@ -48,16 +48,21 @@
|
|
|
48
48
|
"start": "node --env-file-if-exists=../../.env dist/index.js"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@helyx/config": "0.1
|
|
52
|
-
"@helyx/
|
|
53
|
-
"@helyx/
|
|
54
|
-
"@helyx/
|
|
55
|
-
"@helyx/
|
|
56
|
-
"@helyx/
|
|
57
|
-
"@helyx/
|
|
51
|
+
"@helyx/config": "0.2.1",
|
|
52
|
+
"@helyx/control-protocol": "0.1.1",
|
|
53
|
+
"@helyx/core": "0.1.4",
|
|
54
|
+
"@helyx/database": "0.2.0",
|
|
55
|
+
"@helyx/framework": "0.1.4",
|
|
56
|
+
"@helyx/module-manifest": "0.3.0",
|
|
57
|
+
"@helyx/platform": "0.1.4",
|
|
58
|
+
"@helyx/sdk": "0.2.4",
|
|
58
59
|
"discord.js": "14.27.0",
|
|
59
60
|
"fastify": "5.10.0",
|
|
60
61
|
"pino": "10.3.1",
|
|
62
|
+
"ws": "8.21.1",
|
|
61
63
|
"zod": "4.4.3"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/ws": "8.18.1"
|
|
62
67
|
}
|
|
63
68
|
}
|