@helyx/bot 0.3.1 → 0.4.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.
@@ -0,0 +1,62 @@
1
+ import { type CommandRegistration } from "./interactions.js";
2
+ import { type ShardIdentity } from "./shard-contracts.js";
3
+ import { type BotHandle } from "./index.js";
4
+ export declare const WORKER_ORPHAN_TERMINATION_BOUND_MS: number;
5
+ export declare function createLeaseHeartbeatWatchdog(onExpired: () => void, timeoutMs?: number): {
6
+ heartbeat(): void;
7
+ stop(): void;
8
+ };
9
+ export declare function startShardWorker(processEnvironment?: NodeJS.ProcessEnv, identity?: {
10
+ version: 1;
11
+ mode: "auto" | "fixed";
12
+ scope: "local" | "coordinated";
13
+ generation: string;
14
+ supervisorId: string;
15
+ shardId: number;
16
+ shardCount: number;
17
+ fencingToken: number;
18
+ deploymentOrder?: number | undefined;
19
+ }): Promise<BotHandle>;
20
+ export declare function parseShardWorkerLaunchContract(value: string | undefined): ShardIdentity;
21
+ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.ProcessEnv, identity: ShardIdentity): {
22
+ HELYX_SHARD_WORKERS_PER_REPLICA: number;
23
+ HELYX_SHARD_SUPERVISOR_DATABASE_POOL_MAX: number | undefined;
24
+ HELYX_SHARD_WORKER_DATABASE_POOL_MAX: number | undefined;
25
+ DATABASE_URL: string;
26
+ DATABASE_SSL: "disable" | "require" | "verify-full";
27
+ DISCORD_TOKEN: string;
28
+ DISCORD_CLIENT_ID: string;
29
+ DISCORD_INTERNAL_GUILD_ID: string | undefined;
30
+ APPROVED_ROLES: string[];
31
+ HELYX_MODULE_PACKAGES: string[];
32
+ HELYX_MODULE_CATALOGUE_PATH: string | undefined;
33
+ HELYX_BOT_CONTROL_PORT: number;
34
+ HELYX_PRIVACY_ENABLED: boolean;
35
+ HELYX_CLOUD_CONNECT_ENABLED: boolean;
36
+ HELYX_CLOUD_ENROLMENT_TOKEN: string | undefined;
37
+ HELYX_CLOUD_ENROLMENT_FILE: string;
38
+ HELYX_CLOUD_API_URL: string;
39
+ HELYX_CLOUD_ENDPOINT: string;
40
+ HELYX_OWNER_USER_IDS: string[];
41
+ HELYX_SHARD_MODE: "off" | "auto" | "fixed";
42
+ HELYX_SHARD_SCOPE: "local" | "coordinated";
43
+ HELYX_CLOUD_AUDIT_RETENTION_DAYS: number;
44
+ HELYX_CLOUD_CREDENTIAL_AUDIT_RETENTION_DAYS: number;
45
+ LOG_LEVEL: "error" | "fatal" | "warn" | "info" | "debug" | "trace" | "silent";
46
+ NODE_ENV: "development" | "test" | "production";
47
+ HELYX_BOT_CONTROL_SECRET?: string | undefined;
48
+ HELYX_PRIVACY_EXECUTOR_DATABASE_URL?: string | undefined;
49
+ HELYX_PRIVACY_EXECUTOR_DATABASE_SSL?: "disable" | "require" | "verify-full" | undefined;
50
+ HELYX_DATA_ENCRYPTION_KEY?: string | undefined;
51
+ HELYX_SHARD_COUNT?: number | undefined;
52
+ HELYX_SHARD_REDIS_URL?: string | undefined;
53
+ HELYX_SHARD_REDIS_NAMESPACE?: string | undefined;
54
+ HELYX_SHARD_GENERATION?: string | undefined;
55
+ HELYX_SHARD_DATABASE_CONNECTION_BUDGET?: number | undefined;
56
+ };
57
+ export declare function shardWorkerCommandRegistration(input: {
58
+ internalGuildId?: string;
59
+ prepare: NonNullable<CommandRegistration["prepare"]>;
60
+ operationsModuleId?: string;
61
+ }): CommandRegistration;
62
+ //# sourceMappingURL=shard-worker.d.ts.map
@@ -0,0 +1,380 @@
1
+ import { parseBotEnvironment } from "@helyx/config";
2
+ import { safeError, ServiceRegistry } from "@helyx/core";
3
+ import { checkDatabaseConnection, createDatabasePool } from "@helyx/database";
4
+ import { createRuntime } from "@helyx/framework";
5
+ import { createPlatformServices } from "@helyx/platform";
6
+ import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
7
+ import { Client, Events, Partials } from "discord.js";
8
+ import { monitorEventLoopDelay } from "node:perf_hooks";
9
+ import pino from "pino";
10
+ import { createCloudManagementModule } from "./cloud-management-module.js";
11
+ import { BotControlService, DiscordGuildResourceDirectory, mapBotControlError, } from "./control-server.js";
12
+ import { installModuleEvents } from "./events.js";
13
+ import { installGuildTracking } from "./guilds.js";
14
+ import { installInteractions, } from "./interactions.js";
15
+ import { createManagementModule } from "./management-module.js";
16
+ import { DiscordModuleLoggingService } from "./module-logging.js";
17
+ import { resolveModulePackageSpecifiers } from "./module-configuration.js";
18
+ import { loadConfiguredModulePackages } from "./modules.js";
19
+ import { createPermissionsModule } from "./permissions-module.js";
20
+ import { WorkerCloudConnectorProxy } from "./shard-cloud-proxy.js";
21
+ import { createDeveloperOperationsModule, WorkerShardHealthProvider, } from "./developer-operations-module.js";
22
+ import { assertShardMessageSize, controlOperationGuildId, executeShardControlOperation, guildIdToShardId, SHARD_LEASE_HEARTBEAT_TIMEOUT_MS, shardIdentitySchema, supervisorMessageSchema, } from "./shard-contracts.js";
23
+ import { gatewayIntentsForModules, readBotVersion, } from "./index.js";
24
+ const HEALTH_INTERVAL_MS = 5_000;
25
+ export const WORKER_ORPHAN_TERMINATION_BOUND_MS = SHARD_LEASE_HEARTBEAT_TIMEOUT_MS + 5_000;
26
+ export function createLeaseHeartbeatWatchdog(onExpired, timeoutMs = SHARD_LEASE_HEARTBEAT_TIMEOUT_MS) {
27
+ let timer;
28
+ const heartbeat = () => {
29
+ if (timer)
30
+ clearTimeout(timer);
31
+ timer = setTimeout(onExpired, timeoutMs);
32
+ timer.unref();
33
+ };
34
+ return {
35
+ heartbeat,
36
+ stop: () => {
37
+ if (timer)
38
+ clearTimeout(timer);
39
+ timer = undefined;
40
+ },
41
+ };
42
+ }
43
+ export async function startShardWorker(processEnvironment = process.env, identity = parseShardWorkerLaunchContract(processEnvironment.HELYX_SHARD_WORKER_CONTRACT)) {
44
+ const environment = parseShardWorkerEnvironment(processEnvironment, identity);
45
+ const logger = pino({
46
+ level: environment.LOG_LEVEL,
47
+ base: {
48
+ component: "bot-worker",
49
+ shardId: identity.shardId,
50
+ shardCount: identity.shardCount,
51
+ supervisorId: identity.supervisorId,
52
+ generation: identity.generation,
53
+ },
54
+ });
55
+ const database = createDatabasePool({
56
+ connectionString: environment.DATABASE_URL,
57
+ sslMode: environment.DATABASE_SSL,
58
+ applicationName: `helyx-bot-shard-${String(identity.shardId)}`,
59
+ ...(environment.HELYX_SHARD_WORKER_DATABASE_POOL_MAX === undefined
60
+ ? {}
61
+ : { maxConnections: environment.HELYX_SHARD_WORKER_DATABASE_POOL_MAX }),
62
+ });
63
+ const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
64
+ eventLoopDelay.enable();
65
+ let status = "starting";
66
+ let healthTimer;
67
+ let shuttingDown = false;
68
+ let runtimeStarted = false;
69
+ let moduleEvents;
70
+ let client;
71
+ let cloudProxy;
72
+ let shardHealthProvider;
73
+ let leaseHeartbeatWatchdog;
74
+ let forcedLeaseExit;
75
+ async function shutdown(reason = "programmatic") {
76
+ if (shuttingDown)
77
+ return;
78
+ shuttingDown = true;
79
+ status = "stopped";
80
+ if (healthTimer)
81
+ clearInterval(healthTimer);
82
+ leaseHeartbeatWatchdog?.stop();
83
+ if (forcedLeaseExit)
84
+ clearTimeout(forcedLeaseExit);
85
+ sendHealth(identity, health(identity, client, status, eventLoopDelay.mean, moduleEvents?.queueDepth() ?? 0));
86
+ await cloudProxy?.stop().catch(() => undefined);
87
+ shardHealthProvider?.stop();
88
+ await moduleEvents?.stop().catch(() => undefined);
89
+ if (runtimeStarted)
90
+ await runtime?.stop().catch(() => undefined);
91
+ await client?.destroy();
92
+ eventLoopDelay.disable();
93
+ await database.end();
94
+ logger.info({ event: "shard.stopped", reason }, "Shard worker stopped");
95
+ }
96
+ let runtime;
97
+ try {
98
+ await checkDatabaseConnection(database);
99
+ const moduleSpecifiers = await resolveModulePackageSpecifiers(environment.HELYX_MODULE_PACKAGES, undefined, undefined, environment.HELYX_MODULE_CATALOGUE_PATH);
100
+ const modulePackages = await loadConfiguredModulePackages(moduleSpecifiers);
101
+ const featureModules = modulePackages.map(({ definition }) => definition);
102
+ client = new Client({
103
+ intents: gatewayIntentsForModules(featureModules),
104
+ partials: [Partials.Message, Partials.Channel, Partials.Reaction],
105
+ allowedMentions: { parse: [] },
106
+ shards: [identity.shardId],
107
+ shardCount: identity.shardCount,
108
+ });
109
+ const guildResources = new DiscordGuildResourceDirectory(client);
110
+ const permissionsModule = createPermissionsModule(featureModules);
111
+ const managementModule = createManagementModule(featureModules, guildResources);
112
+ cloudProxy = environment.HELYX_CLOUD_CONNECT_ENABLED
113
+ ? new WorkerCloudConnectorProxy()
114
+ : undefined;
115
+ const cloudManagementModule = cloudProxy
116
+ ? createCloudManagementModule({
117
+ connector: cloudProxy,
118
+ client,
119
+ configuredOwnerUserIds: environment.HELYX_OWNER_USER_IDS,
120
+ periodicOwnerRefresh: false,
121
+ })
122
+ : undefined;
123
+ const publicModules = [
124
+ managementModule,
125
+ permissionsModule,
126
+ ...(cloudManagementModule ? [cloudManagementModule] : []),
127
+ ...featureModules,
128
+ ];
129
+ shardHealthProvider = new WorkerShardHealthProvider();
130
+ const operationsModule = environment.DISCORD_INTERNAL_GUILD_ID
131
+ ? createDeveloperOperationsModule({
132
+ internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID,
133
+ approvedRoleIds: environment.APPROVED_ROLES,
134
+ botVersion: readBotVersion(),
135
+ ...(processEnvironment.RAILWAY_GIT_COMMIT_SHA
136
+ ? {
137
+ deploymentReference: processEnvironment.RAILWAY_GIT_COMMIT_SHA.slice(0, 12),
138
+ }
139
+ : {}),
140
+ coreFunctions: [
141
+ managementModule,
142
+ permissionsModule,
143
+ ...(cloudManagementModule ? [cloudManagementModule] : []),
144
+ ].map(({ manifest }) => manifest),
145
+ modules: featureModules.map(({ manifest }) => manifest),
146
+ processLabel: `shard worker ${identity.shardId}`,
147
+ health: shardHealthProvider,
148
+ })
149
+ : undefined;
150
+ const modules = [
151
+ ...publicModules,
152
+ ...(operationsModule ? [operationsModule] : []),
153
+ ];
154
+ const platformServices = new Map(createPlatformServices(database, publicModules));
155
+ if (featureModules.some(({ manifest }) => manifest.id === "helyx.logging"))
156
+ platformServices.set(HELYX_SERVICE_NAMES.moduleLogging, new DiscordModuleLoggingService(client, database, publicModules, logger));
157
+ const serviceAccess = new ServiceRegistry(platformServices);
158
+ runtime = createRuntime({ modules, logger, services: platformServices });
159
+ const enabledCoreModuleIds = new Set([
160
+ managementModule.manifest.id,
161
+ permissionsModule.manifest.id,
162
+ ...(cloudManagementModule ? [cloudManagementModule.manifest.id] : []),
163
+ ...(operationsModule ? [operationsModule.manifest.id] : []),
164
+ ]);
165
+ const prepareGuilds = installGuildTracking(client, database, publicModules, logger, enabledCoreModuleIds, {
166
+ generation: identity.generation,
167
+ deploymentOrder: BigInt(identity.deploymentOrder ?? 1),
168
+ supervisorId: identity.supervisorId,
169
+ shardId: identity.shardId,
170
+ fencingToken: BigInt(identity.fencingToken),
171
+ });
172
+ const commandsReady = installInteractions(client, modules, shardWorkerCommandRegistration({
173
+ ...(environment.DISCORD_INTERNAL_GUILD_ID
174
+ ? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
175
+ : {}),
176
+ prepare: prepareGuilds,
177
+ ...(operationsModule
178
+ ? { operationsModuleId: operationsModule.manifest.id }
179
+ : {}),
180
+ }), logger, serviceAccess);
181
+ moduleEvents = installModuleEvents(client, publicModules, logger, serviceAccess);
182
+ const controlService = new BotControlService(database, publicModules, guildResources, serviceAccess);
183
+ process.on("message", (raw) => {
184
+ const parsed = supervisorMessageSchema.safeParse(raw);
185
+ if (!parsed.success)
186
+ return;
187
+ if (parsed.data.type === "helyx.control.request") {
188
+ void handleControl(identity, controlService, parsed.data);
189
+ }
190
+ else if (parsed.data.type === "helyx.lease.heartbeat") {
191
+ if (sameShardIdentity(parsed.data.identity, identity))
192
+ leaseHeartbeatWatchdog?.heartbeat();
193
+ }
194
+ else if (parsed.data.type === "helyx.cloud.response") {
195
+ cloudProxy?.accept(parsed.data);
196
+ }
197
+ else if (parsed.data.type === "helyx.health.request") {
198
+ sendHealth(identity, health(identity, client, status, eventLoopDelay.mean, moduleEvents?.queueDepth() ?? 0));
199
+ }
200
+ else if (parsed.data.type === "helyx.operations.health.response") {
201
+ shardHealthProvider?.accept(parsed.data);
202
+ }
203
+ else if (parsed.data.type === "helyx.shutdown") {
204
+ void shutdown(parsed.data.reason).finally(() => {
205
+ if (process.connected)
206
+ process.disconnect?.();
207
+ });
208
+ }
209
+ });
210
+ if (identity.scope === "coordinated") {
211
+ leaseHeartbeatWatchdog = createLeaseHeartbeatWatchdog(() => {
212
+ logger.error({ event: "shard.supervisor_heartbeat_lost" }, "Supervisor liveness heartbeat was lost; stopping shard worker");
213
+ void shutdown("supervisor-heartbeat-lost").finally(() => {
214
+ if (process.connected)
215
+ process.disconnect?.();
216
+ });
217
+ forcedLeaseExit = setTimeout(() => process.exit(1), 5_000);
218
+ forcedLeaseExit.unref();
219
+ });
220
+ leaseHeartbeatWatchdog.heartbeat();
221
+ }
222
+ client.on(Events.ShardReady, () => {
223
+ status = "ready";
224
+ sendHealth(identity, health(identity, client, status, eventLoopDelay.mean, moduleEvents?.queueDepth() ?? 0));
225
+ });
226
+ client.on(Events.ShardDisconnect, () => {
227
+ status = "disconnected";
228
+ sendHealth(identity, health(identity, client, status, eventLoopDelay.mean, moduleEvents?.queueDepth() ?? 0));
229
+ });
230
+ client.on(Events.ShardReconnecting, () => {
231
+ status = "reconnecting";
232
+ });
233
+ client.on(Events.ShardResume, () => {
234
+ status = "ready";
235
+ });
236
+ client.on(Events.Error, (error) => {
237
+ logger.error({ event: "discord.error", error: safeError(error) }, "Discord shard error");
238
+ });
239
+ await cloudProxy?.start();
240
+ await runtime.start();
241
+ runtimeStarted = true;
242
+ await client.login(environment.DISCORD_TOKEN);
243
+ await commandsReady;
244
+ healthTimer = setInterval(() => sendHealth(identity, health(identity, client, status, eventLoopDelay.mean, moduleEvents?.queueDepth() ?? 0)), HEALTH_INTERVAL_MS);
245
+ healthTimer.unref();
246
+ process.once("SIGTERM", () => void shutdown("SIGTERM"));
247
+ process.once("SIGINT", () => void shutdown("SIGINT"));
248
+ return { shutdown };
249
+ }
250
+ catch (error) {
251
+ logger.error({ event: "shard.start_failed", error: safeError(error) }, "Shard worker failed to start");
252
+ await shutdown("startup-failed");
253
+ throw error;
254
+ }
255
+ }
256
+ function sameShardIdentity(left, right) {
257
+ return (left.generation === right.generation &&
258
+ left.deploymentOrder === right.deploymentOrder &&
259
+ left.supervisorId === right.supervisorId &&
260
+ left.shardId === right.shardId &&
261
+ left.shardCount === right.shardCount &&
262
+ left.fencingToken === right.fencingToken);
263
+ }
264
+ export function parseShardWorkerLaunchContract(value) {
265
+ if (!value)
266
+ throw new Error("Shard worker launch contract is missing");
267
+ try {
268
+ return shardIdentitySchema.parse(JSON.parse(value));
269
+ }
270
+ catch (error) {
271
+ if (error instanceof SyntaxError)
272
+ throw new Error("Shard worker launch contract is invalid JSON", {
273
+ cause: error,
274
+ });
275
+ throw error;
276
+ }
277
+ }
278
+ export function parseShardWorkerEnvironment(processEnvironment, identity) {
279
+ const cloudEnabled = processEnvironment.HELYX_CLOUD_CONNECT_ENABLED === "true";
280
+ const environment = parseBotEnvironment({
281
+ ...processEnvironment,
282
+ ...(cloudEnabled
283
+ ? {
284
+ HELYX_BOT_CONTROL_SECRET: Buffer.alloc(32).toString("base64"),
285
+ HELYX_DATA_ENCRYPTION_KEY: Buffer.alloc(32).toString("base64"),
286
+ }
287
+ : {}),
288
+ ...(identity.scope === "coordinated"
289
+ ? { HELYX_SHARD_REDIS_URL: "redis://worker-validation.invalid" }
290
+ : {}),
291
+ });
292
+ if (environment.HELYX_SHARD_MODE === "off" ||
293
+ environment.HELYX_SHARD_MODE !== identity.mode ||
294
+ environment.HELYX_SHARD_SCOPE !== identity.scope)
295
+ throw new Error("Shard worker launch contract conflicts with configuration");
296
+ return environment;
297
+ }
298
+ export function shardWorkerCommandRegistration(input) {
299
+ return {
300
+ ...(input.internalGuildId
301
+ ? { internalGuildId: input.internalGuildId }
302
+ : {}),
303
+ prepare: input.prepare,
304
+ synchronise: false,
305
+ ...(input.operationsModuleId
306
+ ? {
307
+ selfAuthorizingGuildModuleIds: new Set([input.operationsModuleId]),
308
+ }
309
+ : {}),
310
+ };
311
+ }
312
+ async function handleControl(identity, service, request) {
313
+ let response;
314
+ const guildId = controlOperationGuildId(request.operation);
315
+ if (request.generation !== identity.generation ||
316
+ request.fencingToken !== identity.fencingToken ||
317
+ !guildId ||
318
+ guildIdToShardId(guildId, identity.shardCount) !== identity.shardId ||
319
+ new Date(request.deadlineAt).getTime() <= Date.now()) {
320
+ response = {
321
+ version: 1,
322
+ type: "helyx.control.response",
323
+ requestId: request.requestId,
324
+ ok: false,
325
+ status: 503,
326
+ message: "Shard ownership changed or the request expired",
327
+ };
328
+ }
329
+ else {
330
+ try {
331
+ response = {
332
+ version: 1,
333
+ type: "helyx.control.response",
334
+ requestId: request.requestId,
335
+ ok: true,
336
+ result: await executeShardControlOperation(service, request.operation),
337
+ };
338
+ }
339
+ catch (error) {
340
+ const mapped = mapBotControlError(error);
341
+ response = {
342
+ version: 1,
343
+ type: "helyx.control.response",
344
+ requestId: request.requestId,
345
+ ok: false,
346
+ status: mapped?.status ?? 500,
347
+ message: mapped?.message ?? "The owning shard rejected the request",
348
+ };
349
+ }
350
+ }
351
+ assertShardMessageSize(response);
352
+ process.send?.(response);
353
+ }
354
+ function health(identity, client, status, eventLoopDelayNanoseconds, queueDepth) {
355
+ return {
356
+ shardId: identity.shardId,
357
+ status,
358
+ guildCount: client?.guilds.cache.size ?? 0,
359
+ gatewayPingMs: client?.ws.ping ?? -1,
360
+ uptimeSeconds: process.uptime(),
361
+ restartCount: Number(process.env.HELYX_SHARD_RESTART_COUNT ?? "0"),
362
+ eventLoopDelayMs: Number.isFinite(eventLoopDelayNanoseconds)
363
+ ? eventLoopDelayNanoseconds / 1_000_000
364
+ : 0,
365
+ memoryRssBytes: process.memoryUsage().rss,
366
+ queueDepth,
367
+ observedAt: new Date().toISOString(),
368
+ };
369
+ }
370
+ function sendHealth(identity, workerHealth) {
371
+ const message = {
372
+ version: 1,
373
+ type: "helyx.health.update",
374
+ identity,
375
+ health: workerHealth,
376
+ };
377
+ assertShardMessageSize(message);
378
+ process.send?.(message);
379
+ }
380
+ //# sourceMappingURL=shard-worker.js.map
@@ -0,0 +1,11 @@
1
+ import type { DatabasePool } from "@helyx/database";
2
+ import type { CloudDiscordDirectory, CloudDiscordSnapshot } from "./cloud-connector.js";
3
+ export declare const CLOUD_HELLO_GUILD_LIMIT = 10000;
4
+ export declare class SupervisorCloudDiscordDirectory implements CloudDiscordDirectory {
5
+ #private;
6
+ private readonly database;
7
+ private readonly configuredOwnerUserIds;
8
+ constructor(token: string, database: DatabasePool, configuredOwnerUserIds: readonly string[]);
9
+ snapshot(): Promise<CloudDiscordSnapshot>;
10
+ }
11
+ //# sourceMappingURL=supervisor-cloud-directory.d.ts.map
@@ -0,0 +1,66 @@
1
+ import { CONNECTOR_HELLO_MAX_GUILDS } from "@helyx/control-protocol";
2
+ import { REST, Routes } from "discord.js";
3
+ import { z } from "zod";
4
+ const snowflake = z.string().regex(/^\d{17,20}$/u);
5
+ export const CLOUD_HELLO_GUILD_LIMIT = CONNECTOR_HELLO_MAX_GUILDS;
6
+ const applicationSchema = z
7
+ .object({
8
+ id: snowflake,
9
+ owner: z
10
+ .union([
11
+ z.object({ id: snowflake }).passthrough(),
12
+ z
13
+ .object({
14
+ members: z
15
+ .array(z.object({ user: z.object({ id: snowflake }) }))
16
+ .optional(),
17
+ })
18
+ .passthrough(),
19
+ ])
20
+ .nullable()
21
+ .optional(),
22
+ })
23
+ .passthrough();
24
+ const userSchema = z
25
+ .object({ id: snowflake, username: z.string().min(1).max(100) })
26
+ .passthrough();
27
+ export class SupervisorCloudDiscordDirectory {
28
+ database;
29
+ configuredOwnerUserIds;
30
+ #rest;
31
+ constructor(token, database, configuredOwnerUserIds) {
32
+ this.database = database;
33
+ this.configuredOwnerUserIds = configuredOwnerUserIds;
34
+ this.#rest = new REST({ version: "10" }).setToken(token);
35
+ }
36
+ async snapshot() {
37
+ const [applicationValue, botValue, guildResult] = await Promise.all([
38
+ this.#rest.get(Routes.oauth2CurrentApplication()),
39
+ this.#rest.get(Routes.user("@me")),
40
+ this.database.query(`SELECT guild_id, name, owner_user_id
41
+ FROM discord_guilds
42
+ WHERE active = true
43
+ ORDER BY guild_id
44
+ LIMIT ${String(CLOUD_HELLO_GUILD_LIMIT)}`),
45
+ ]);
46
+ const application = applicationSchema.parse(applicationValue);
47
+ const bot = userSchema.parse(botValue);
48
+ const applicationOwnerIds = application.owner && "id" in application.owner
49
+ ? [snowflake.parse(application.owner.id)]
50
+ : (application.owner?.members?.map((member) => snowflake.parse(member.user.id)) ?? []);
51
+ return {
52
+ applicationId: application.id,
53
+ botUserId: bot.id,
54
+ botUsername: bot.username,
55
+ ownerUserIds: [
56
+ ...new Set([...this.configuredOwnerUserIds, ...applicationOwnerIds]),
57
+ ],
58
+ guilds: guildResult.rows.map((guild) => ({
59
+ guildId: guild.guild_id,
60
+ name: guild.name ?? "Unknown server",
61
+ ownerUserId: guild.owner_user_id,
62
+ })),
63
+ };
64
+ }
65
+ }
66
+ //# sourceMappingURL=supervisor-cloud-directory.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/bot",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Production Discord runtime for the modular Helyx platform.",
5
5
  "keywords": [
6
6
  "helyx",
@@ -48,17 +48,18 @@
48
48
  "start": "node --env-file-if-exists=../../.env dist/index.js"
49
49
  },
50
50
  "dependencies": {
51
- "@helyx/config": "0.3.0",
52
- "@helyx/control-protocol": "0.1.1",
53
- "@helyx/core": "0.1.5",
54
- "@helyx/database": "0.3.1",
55
- "@helyx/framework": "0.1.5",
51
+ "@helyx/config": "0.4.1",
52
+ "@helyx/control-protocol": "0.1.2",
53
+ "@helyx/core": "0.1.7",
54
+ "@helyx/database": "0.4.0",
55
+ "@helyx/framework": "0.1.7",
56
56
  "@helyx/module-manifest": "0.4.0",
57
- "@helyx/platform": "0.2.1",
58
- "@helyx/sdk": "0.3.0",
57
+ "@helyx/platform": "0.2.3",
58
+ "@helyx/sdk": "0.3.2",
59
59
  "discord.js": "14.27.0",
60
60
  "fastify": "5.10.0",
61
61
  "pino": "10.3.1",
62
+ "redis": "6.1.0",
62
63
  "ws": "8.21.1",
63
64
  "zod": "4.4.3"
64
65
  },