@helyx/bot 0.4.32 → 0.4.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/modules.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { orderModuleManifests, parseModuleManifest, } from "@helyx/module-manifest";
2
+ import { NEXUS_COMMAND_SPECS, NEXUS_ROOT_MODULE_ID } from "@helyx/sdk";
2
3
  import { createHmac } from "node:crypto";
3
4
  import { readFile } from "node:fs/promises";
4
5
  import { createRequire } from "node:module";
@@ -102,6 +103,28 @@ export function validateContributionParity(definition) {
102
103
  throw new Error("Module requests an unsupported or duplicate Discord capability");
103
104
  }
104
105
  const executableCommands = (definition.interactions?.commands ?? []).map(({ name }) => name);
106
+ const nexusCommandPaths = new Set();
107
+ for (const command of definition.nexusCommands ?? []) {
108
+ const spec = NEXUS_COMMAND_SPECS.find(({ path }) => path === command.path);
109
+ if (!spec || spec.ownerModuleId !== definition.manifest.id) {
110
+ throw new Error(`Module ${definition.manifest.id} cannot contribute Nexus command ${command.path}`);
111
+ }
112
+ if (nexusCommandPaths.has(command.path)) {
113
+ throw new Error(`Duplicate Nexus command path for ${definition.manifest.id}: ${command.path}`);
114
+ }
115
+ nexusCommandPaths.add(command.path);
116
+ }
117
+ if (definition.manifest.id !== NEXUS_ROOT_MODULE_ID) {
118
+ executableCommands.push(...[...nexusCommandPaths].map((path) => `nexus.${path}`));
119
+ if (nexusCommandPaths.size > 0 &&
120
+ !definition.manifest.dependencies.some(({ id }) => id === NEXUS_ROOT_MODULE_ID)) {
121
+ throw new Error(`Module ${definition.manifest.id} contributes Nexus commands without depending on Nexus Core`);
122
+ }
123
+ }
124
+ if (definition.manifest.id !== NEXUS_ROOT_MODULE_ID &&
125
+ definition.interactions?.commands?.some(({ name }) => name === "nexus")) {
126
+ throw new Error("Nexus Core must be the sole owner of /nexus");
127
+ }
105
128
  const executableEvents = [
106
129
  ...(definition.events?.privilegedMessageCreate ?? []),
107
130
  ...(definition.events?.guildMessageCreate ?? []),
@@ -135,7 +158,7 @@ export function validateContributionParity(definition) {
135
158
  function validatePrivacyParity(definition) {
136
159
  if (definition.manifest.manifestVersion === "0.1") {
137
160
  if (definition.privacy !== undefined) {
138
- throw new Error("Module executable privacy provider requires manifest version 0.2");
161
+ throw new Error("Module executable privacy provider requires manifest version 0.2 or later");
139
162
  }
140
163
  return;
141
164
  }
@@ -0,0 +1,13 @@
1
+ import { type CommandContribution, type ModuleDefinition } from "@helyx/sdk";
2
+ export interface CommandRouteOwner {
3
+ moduleId: string;
4
+ moduleName: string;
5
+ }
6
+ export interface RegisteredCommand {
7
+ moduleId: string;
8
+ moduleName: string;
9
+ contribution: CommandContribution;
10
+ routeOwners?: ReadonlyMap<string, CommandRouteOwner>;
11
+ }
12
+ export declare function collectCommandRegistrations(modules: readonly ModuleDefinition[]): Map<string, RegisteredCommand>;
13
+ //# sourceMappingURL=nexus-commands.d.ts.map
@@ -0,0 +1,100 @@
1
+ import { NEXUS_COMMAND_SPECS, NEXUS_ROOT_MODULE_ID, } from "@helyx/sdk";
2
+ export function collectCommandRegistrations(modules) {
3
+ const registrations = modules.flatMap((module) => (module.interactions?.commands ?? []).map((contribution) => ({
4
+ moduleId: module.manifest.id,
5
+ moduleName: module.manifest.name,
6
+ contribution,
7
+ })));
8
+ const nexusContributions = modules.flatMap((module) => (module.nexusCommands ?? []).map((contribution) => ({
9
+ moduleId: module.manifest.id,
10
+ moduleName: module.manifest.name,
11
+ contribution,
12
+ })));
13
+ if (nexusContributions.length > 0) {
14
+ const roots = registrations.filter(({ contribution }) => contribution.name === "nexus");
15
+ if (roots.length !== 1 || roots[0].moduleId !== NEXUS_ROOT_MODULE_ID) {
16
+ throw new Error("Nexus Core must be the sole owner of /nexus");
17
+ }
18
+ const root = roots[0];
19
+ if (root.contribution.execute ||
20
+ root.contribution.options?.length ||
21
+ root.contribution.subcommands?.length ||
22
+ root.contribution.subcommandGroups?.length) {
23
+ throw new Error("Nexus Core /nexus must be composed from Nexus commands");
24
+ }
25
+ const composed = composeNexusCommand(root, nexusContributions);
26
+ registrations.splice(registrations.indexOf(root), 1, composed);
27
+ }
28
+ return uniqueRegistrations(registrations);
29
+ }
30
+ function composeNexusCommand(root, contributions) {
31
+ const byPath = new Map(contributions.map((item) => {
32
+ const spec = NEXUS_COMMAND_SPECS.find(({ path }) => path === item.contribution.path);
33
+ if (!spec || spec.ownerModuleId !== item.moduleId) {
34
+ throw new Error(`Module ${item.moduleId} cannot contribute Nexus command ${item.contribution.path}`);
35
+ }
36
+ return [item.contribution.path, item];
37
+ }));
38
+ if (byPath.size !== contributions.length) {
39
+ throw new Error("Duplicate Nexus command path");
40
+ }
41
+ const subcommands = [];
42
+ const groups = new Map();
43
+ const routeOwners = new Map();
44
+ for (const spec of NEXUS_COMMAND_SPECS) {
45
+ const item = byPath.get(spec.path);
46
+ if (!item)
47
+ continue;
48
+ routeOwners.set(spec.path, {
49
+ moduleId: item.moduleId,
50
+ moduleName: item.moduleName,
51
+ });
52
+ const [first, second] = spec.path.split(".");
53
+ const command = toSubcommand(item.contribution, second ?? first);
54
+ if (!second) {
55
+ subcommands.push(command);
56
+ continue;
57
+ }
58
+ const current = groups.get(first);
59
+ const group = current ?? {
60
+ name: first,
61
+ description: "groupDescription" in spec
62
+ ? spec.groupDescription
63
+ : `Manage Nexus ${first}`,
64
+ subcommands: [],
65
+ };
66
+ groups.set(first, {
67
+ ...group,
68
+ subcommands: [...group.subcommands, command],
69
+ });
70
+ }
71
+ return {
72
+ ...root,
73
+ contribution: {
74
+ ...root.contribution,
75
+ subcommands,
76
+ subcommandGroups: [...groups.values()],
77
+ },
78
+ routeOwners,
79
+ };
80
+ }
81
+ function toSubcommand(contribution, name) {
82
+ const subcommand = {
83
+ ...contribution,
84
+ name,
85
+ permissionId: `nexus.${contribution.path}`,
86
+ };
87
+ delete subcommand.path;
88
+ return subcommand;
89
+ }
90
+ function uniqueRegistrations(items) {
91
+ const result = new Map();
92
+ for (const item of items) {
93
+ const key = item.contribution.name;
94
+ if (result.has(key))
95
+ throw new Error(`Duplicate command name: ${key}`);
96
+ result.set(key, item);
97
+ }
98
+ return result;
99
+ }
100
+ //# sourceMappingURL=nexus-commands.js.map
@@ -43,6 +43,7 @@ export function createOAuthModule() {
43
43
  {
44
44
  name: "revoke",
45
45
  description: "Revoke a service and delete the OAuth details Helyx stores for you.",
46
+ acknowledgement: "immediate-modal",
46
47
  rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
47
48
  execute: async (context) => {
48
49
  const oauth = context.services.get(HELYX_SERVICE_NAMES.oauth);
@@ -155,9 +156,13 @@ export function createOAuthModule() {
155
156
  componentsV2: {
156
157
  text: [
157
158
  {
158
- content: result.providerRevocation === "completed"
159
- ? `${providerName(result.provider)} access has been removed from Helyx. Local credentials were deleted.`
160
- : `${providerName(result.provider)} access has been removed from Helyx and local credentials were deleted. Helyx could not confirm provider-side revocation, so you may also remove Helyx from your ${providerName(result.provider)} connected-app settings.`,
159
+ content: result.outcome === "processing"
160
+ ? `${providerName(result.provider)} access removal is already in progress. Helyx will finish it automatically.`
161
+ : result.outcome === "cleanup_pending"
162
+ ? `${providerName(result.provider)} credentials were deleted. Helyx is still removing related roles and connection records.`
163
+ : result.providerRevocation === "completed"
164
+ ? `${providerName(result.provider)} access has been removed from Helyx. Local credentials were deleted.`
165
+ : `${providerName(result.provider)} access has been removed from Helyx and local credentials were deleted. Helyx could not confirm provider-side revocation, so you may also remove Helyx from your ${providerName(result.provider)} connected-app settings.`,
161
166
  },
162
167
  ],
163
168
  },
@@ -30,7 +30,7 @@ export class HttpOAuthService {
30
30
  "content-type": "application/json",
31
31
  },
32
32
  body: JSON.stringify(body),
33
- signal: AbortSignal.timeout(6_000),
33
+ signal: AbortSignal.timeout(15_000),
34
34
  });
35
35
  if (!response.ok)
36
36
  throw new Error(`oauth_service_http_${response.status}`);
@@ -1,4 +1,5 @@
1
1
  import { defineModule, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
2
+ import { isCustomerSelectableModule } from "@helyx/module-manifest";
2
3
  import { PermissionFlagsBits } from "discord.js";
3
4
  import { canMutatePermissionPolicy, permissionMutationDenialMessage, } from "@helyx/core";
4
5
  const MODULE_SELECT_ID = "helyx.permissions.module";
@@ -17,7 +18,9 @@ const MODULE_PAGE_SIZE = 25;
17
18
  const TARGET_PAGE_SIZE = 24;
18
19
  const VIOLET = 0x7c3aed;
19
20
  export function createPermissionsModule(configurableModules) {
20
- const modules = new Map(configurableModules.map((module) => [module.manifest.id, module]));
21
+ const modules = new Map(configurableModules
22
+ .filter((module) => isCustomerSelectableModule(module.manifest))
23
+ .map((module) => [module.manifest.id, module]));
21
24
  const commandModules = [...modules.values()]
22
25
  .filter((module) => commandTargets(module).length > 0)
23
26
  .sort((left, right) => left.manifest.name.localeCompare(right.manifest.name, "en-GB"));
@@ -46,6 +49,7 @@ export function createPermissionsModule(configurableModules) {
46
49
  {
47
50
  name: "permissions",
48
51
  description: "Configure access to Helyx modules and commands",
52
+ acknowledgement: "deferred-ephemeral",
49
53
  registration: "global",
50
54
  contexts: ["guild"],
51
55
  integrationTypes: ["guild_install"],
@@ -359,9 +363,17 @@ function restrictedPolicy() {
359
363
  };
360
364
  }
361
365
  function commandTargets(module) {
362
- return (module.interactions?.commands ?? []).flatMap((command) => command.subcommands?.length
363
- ? command.subcommands.map((subcommand) => `${command.name}.${subcommand.name}`)
364
- : [command.name]);
366
+ const targets = (module.interactions?.commands ?? []).flatMap((command) => {
367
+ const nested = [
368
+ ...(command.subcommands ?? []).map((subcommand) => `${command.name}.${subcommand.name}`),
369
+ ...(command.subcommandGroups ?? []).flatMap((group) => group.subcommands.map((subcommand) => `${command.name}.${group.name}.${subcommand.name}`)),
370
+ ];
371
+ if (nested.length > 0)
372
+ return nested;
373
+ return [command.name];
374
+ });
375
+ targets.push(...(module.nexusCommands ?? []).map((command) => `nexus.${command.path}`));
376
+ return [...new Set(targets)];
365
377
  }
366
378
  function modulePickerResponse(modules, requestedPage) {
367
379
  const pages = pageCount(modules.length, MODULE_PAGE_SIZE);
@@ -321,6 +321,13 @@ export function createLocalScheduledTaskRoute(input) {
321
321
  const module = modules.get(task.moduleId);
322
322
  if (!module)
323
323
  return deferred("module_unavailable", DEFAULT_POLL_INTERVAL_MS * 30);
324
+ if (typeof input.services.has === "function" &&
325
+ input.services.has(HELYX_SERVICE_NAMES.moduleAvailability)) {
326
+ const availability = input.services.get(HELYX_SERVICE_NAMES.moduleAvailability);
327
+ if (typeof availability?.isAvailable === "function" &&
328
+ !(await availability.isAvailable(task.moduleId)))
329
+ return deferred("module_globally_disabled", DEFAULT_POLL_INTERVAL_MS * 30);
330
+ }
324
331
  if (task.guildId) {
325
332
  const enabled = await input.services
326
333
  .get(HELYX_SERVICE_NAMES.installations)
@@ -359,6 +359,20 @@ export declare const shardControlOperationSchema: z.ZodDiscriminatedUnion<[z.Zod
359
359
  expectedRevision: z.ZodNumber;
360
360
  value: z.ZodRecord<z.ZodString, z.ZodCustom<ManagedResourceValue, ManagedResourceValue>>;
361
361
  actorAuthenticatedAt: z.ZodOptional<z.ZodISODateTime>;
362
+ }, z.core.$strict>, z.ZodObject<{
363
+ kind: z.ZodLiteral<"oauth.consumer.cleanup">;
364
+ consumerBindingId: z.ZodUUID;
365
+ moduleId: z.ZodString;
366
+ provider: z.ZodEnum<{
367
+ youtube: "youtube";
368
+ twitch: "twitch";
369
+ }>;
370
+ guildId: z.ZodString;
371
+ resourceType: z.ZodString;
372
+ resourceId: z.ZodString;
373
+ subjectUserId: z.ZodString;
374
+ contractVersion: z.ZodNumber;
375
+ correlationId: z.ZodString;
362
376
  }, z.core.$strict>, z.ZodObject<{
363
377
  kind: z.ZodLiteral<"support.notification.send">;
364
378
  guildId: z.ZodString;
@@ -698,6 +712,20 @@ export declare const shardControlRequestSchema: z.ZodObject<{
698
712
  expectedRevision: z.ZodNumber;
699
713
  value: z.ZodRecord<z.ZodString, z.ZodCustom<ManagedResourceValue, ManagedResourceValue>>;
700
714
  actorAuthenticatedAt: z.ZodOptional<z.ZodISODateTime>;
715
+ }, z.core.$strict>, z.ZodObject<{
716
+ kind: z.ZodLiteral<"oauth.consumer.cleanup">;
717
+ consumerBindingId: z.ZodUUID;
718
+ moduleId: z.ZodString;
719
+ provider: z.ZodEnum<{
720
+ youtube: "youtube";
721
+ twitch: "twitch";
722
+ }>;
723
+ guildId: z.ZodString;
724
+ resourceType: z.ZodString;
725
+ resourceId: z.ZodString;
726
+ subjectUserId: z.ZodString;
727
+ contractVersion: z.ZodNumber;
728
+ correlationId: z.ZodString;
701
729
  }, z.core.$strict>, z.ZodObject<{
702
730
  kind: z.ZodLiteral<"support.notification.send">;
703
731
  guildId: z.ZodString;
@@ -1542,6 +1570,20 @@ export declare const supervisorMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObje
1542
1570
  expectedRevision: z.ZodNumber;
1543
1571
  value: z.ZodRecord<z.ZodString, z.ZodCustom<ManagedResourceValue, ManagedResourceValue>>;
1544
1572
  actorAuthenticatedAt: z.ZodOptional<z.ZodISODateTime>;
1573
+ }, z.core.$strict>, z.ZodObject<{
1574
+ kind: z.ZodLiteral<"oauth.consumer.cleanup">;
1575
+ consumerBindingId: z.ZodUUID;
1576
+ moduleId: z.ZodString;
1577
+ provider: z.ZodEnum<{
1578
+ youtube: "youtube";
1579
+ twitch: "twitch";
1580
+ }>;
1581
+ guildId: z.ZodString;
1582
+ resourceType: z.ZodString;
1583
+ resourceId: z.ZodString;
1584
+ subjectUserId: z.ZodString;
1585
+ contractVersion: z.ZodNumber;
1586
+ correlationId: z.ZodString;
1545
1587
  }, z.core.$strict>, z.ZodObject<{
1546
1588
  kind: z.ZodLiteral<"support.notification.send">;
1547
1589
  guildId: z.ZodString;
@@ -425,6 +425,20 @@ export const shardControlOperationSchema = z.discriminatedUnion("kind", [
425
425
  ...mutationContext.shape,
426
426
  })
427
427
  .strict(),
428
+ z
429
+ .object({
430
+ kind: z.literal("oauth.consumer.cleanup"),
431
+ consumerBindingId: z.uuid(),
432
+ moduleId,
433
+ provider: z.enum(["youtube", "twitch"]),
434
+ guildId: snowflake,
435
+ resourceType,
436
+ resourceId,
437
+ subjectUserId: snowflake,
438
+ contractVersion: z.number().int().min(1).max(32_767),
439
+ correlationId: z.string().trim().min(1).max(200),
440
+ })
441
+ .strict(),
428
442
  z
429
443
  .object({
430
444
  kind: z.literal("support.notification.send"),
@@ -1111,6 +1125,18 @@ export async function executeShardControlOperation(handler, operation) {
1111
1125
  ? { actorAuthenticatedAt: operation.actorAuthenticatedAt }
1112
1126
  : {}),
1113
1127
  });
1128
+ case "oauth.consumer.cleanup":
1129
+ return handler.cleanupOAuthConsumer({
1130
+ consumerBindingId: operation.consumerBindingId,
1131
+ moduleId: operation.moduleId,
1132
+ provider: operation.provider,
1133
+ guildId: operation.guildId,
1134
+ resourceType: operation.resourceType,
1135
+ resourceId: operation.resourceId,
1136
+ subjectUserId: operation.subjectUserId,
1137
+ contractVersion: operation.contractVersion,
1138
+ correlationId: operation.correlationId,
1139
+ });
1114
1140
  case "support.notification.send": {
1115
1141
  return handler.sendSupportNotification({
1116
1142
  notificationId: operation.notificationId,
@@ -11,10 +11,13 @@ export declare class ShardControlRouter implements BotControlHandler {
11
11
  localLeases: ReadonlyMap<number, ShardLease>;
12
12
  dispatchLocal: LocalDispatcher;
13
13
  privacy?: Pick<BotControlHandler, "listPrivacyProviders" | "capturePrivacy" | "erasePrivacy">;
14
+ availability?: Pick<BotControlHandler, "listAvailabilityModules">;
14
15
  localLeaderLease?: ShardLeaderLease;
15
16
  coordinator?: ShardCoordinator;
16
17
  notificationGuildId?: string;
17
18
  });
19
+ cleanupOAuthConsumer(input: Parameters<BotControlHandler["cleanupOAuthConsumer"]>[0]): ReturnType<BotControlHandler["cleanupOAuthConsumer"]>;
20
+ listAvailabilityModules(): ReturnType<BotControlHandler["listAvailabilityModules"]>;
18
21
  getGuildState(guildId: string): ReturnType<BotControlHandler["getGuildState"]>;
19
22
  canAccessDashboard(input: Parameters<BotControlHandler["canAccessDashboard"]>[0]): ReturnType<BotControlHandler["canAccessDashboard"]>;
20
23
  getGuildAccessPolicy(input: Parameters<BotControlHandler["getGuildAccessPolicy"]>[0]): ReturnType<BotControlHandler["getGuildAccessPolicy"]>;
@@ -12,6 +12,14 @@ export class ShardControlRouter {
12
12
  constructor(input) {
13
13
  this.input = input;
14
14
  }
15
+ cleanupOAuthConsumer(input) {
16
+ return this.#route({ kind: "oauth.consumer.cleanup", ...input });
17
+ }
18
+ listAvailabilityModules() {
19
+ if (!this.input.availability)
20
+ return Promise.reject(new BotControlTransportError(503, "Module availability catalogue is unavailable"));
21
+ return this.input.availability.listAvailabilityModules();
22
+ }
15
23
  getGuildState(guildId) {
16
24
  return this.#route({ kind: "guild.state.get", guildId });
17
25
  }
@@ -23,6 +23,7 @@ import { createDeveloperOperationsModule } from "./developer-operations-module.j
23
23
  import { SupervisorCloudDiscordDirectory } from "./supervisor-cloud-directory.js";
24
24
  import { createSupportModule } from "./support-module.js";
25
25
  import { createOAuthModule } from "./oauth-module.js";
26
+ import { availabilityCatalogue, availabilityCatalogueDefinitions, } from "./module-availability-control.js";
26
27
  import { createScheduledTaskRunner, schedulerOperationalHealth, } from "./scheduled-task-runner.js";
27
28
  import { numericWriteBehindRegistrations } from "./numeric-write-behind.js";
28
29
  import { executeRedisNumericOperation, RedisNumericWriteBehindService, } from "./numeric-write-behind-redis.js";
@@ -229,6 +230,10 @@ export async function startShardSupervisor(environment, processEnvironment, work
229
230
  ...publicModules,
230
231
  ...(operationsModule ? [operationsModule] : []),
231
232
  ];
233
+ const availabilityModules = availabilityCatalogueDefinitions(modules);
234
+ const availabilityService = {
235
+ listAvailabilityModules: () => availabilityCatalogue(availabilityModules),
236
+ };
232
237
  const initialiseLeader = async () => {
233
238
  await runLeaderStartup(database, environment, modulePackages, modules, logger);
234
239
  assertStartupLeaseHealthy();
@@ -258,6 +263,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
258
263
  localLeases,
259
264
  dispatchLocal,
260
265
  privacy: privacyService,
266
+ availability: availabilityService,
261
267
  localLeaderLease: leaderLease,
262
268
  ...(coordinator ? { coordinator } : {}),
263
269
  ...(environment.DISCORD_INTERNAL_GUILD_ID
@@ -351,6 +357,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
351
357
  localLeases,
352
358
  dispatchLocal,
353
359
  ...(privacyService ? { privacy: privacyService } : {}),
360
+ availability: availabilityService,
354
361
  ...(leaderLease ? { localLeaderLease: leaderLease } : {}),
355
362
  ...(coordinator ? { coordinator } : {}),
356
363
  ...(environment.DISCORD_INTERNAL_GUILD_ID
@@ -651,7 +658,22 @@ export async function startShardSupervisor(environment, processEnvironment, work
651
658
  }
652
659
  }
653
660
  assertShardMessageSize(response);
654
- await workers.get(shardId)?.send(response);
661
+ await sendWorkerResponse(shardId, response);
662
+ }
663
+ async function sendWorkerResponse(shardId, response) {
664
+ if (shuttingDown)
665
+ return;
666
+ const worker = workers.get(shardId);
667
+ if (!worker?.process)
668
+ return;
669
+ try {
670
+ await worker.send(response);
671
+ }
672
+ catch (error) {
673
+ if (shuttingDown || !worker.process)
674
+ return;
675
+ throw error;
676
+ }
655
677
  }
656
678
  async function handleWorkerMessage(shardId, raw) {
657
679
  if (serializedBytes(raw) > SHARD_CONTROL_MAX_BYTES)
@@ -709,7 +731,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
709
731
  const response = leaderLease
710
732
  ? await executeLeaderCloud(message.data)
711
733
  : await routeCloudToLeader(message.data);
712
- await workers.get(shardId)?.send(response);
734
+ await sendWorkerResponse(shardId, response);
713
735
  return;
714
736
  }
715
737
  if (message.data.type === "helyx.operations.health.request") {
@@ -720,7 +742,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
720
742
  health: boundedOperationalHealth(await buildHealth()),
721
743
  };
722
744
  assertShardMessageSize(response);
723
- await workers.get(shardId)?.send(response);
745
+ await sendWorkerResponse(shardId, response);
724
746
  }
725
747
  }
726
748
  function dispatchLocal(shardId, request) {
@@ -1392,9 +1414,14 @@ export function buildShardWorkerEnvironment(source, identity, restartCount) {
1392
1414
  "HELYX_SUPPORT_STAFF_CHANNEL_ID",
1393
1415
  "HELYX_MODULE_PACKAGES",
1394
1416
  "HELYX_MESSAGE_CONTENT_ENABLED",
1417
+ "HELYX_GUILD_MEMBERS_ENABLED",
1418
+ "HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS",
1395
1419
  "HELYX_MODULE_CATALOGUE_PATH",
1396
1420
  "HELYX_CLOUD_CONNECT_ENABLED",
1421
+ "HELYX_CLOUD_API_URL",
1397
1422
  "HELYX_OWNER_USER_IDS",
1423
+ "HELYX_OAUTH_ENABLED",
1424
+ "HELYX_OAUTH_INTERNAL_SECRET",
1398
1425
  "HELYX_VERIFICATION_ENABLED",
1399
1426
  "HELYX_VERIFICATION_INSTALLATION_ID",
1400
1427
  "HELYX_VERIFICATION_URL",
@@ -9,7 +9,7 @@ export declare function createLeaseHeartbeatWatchdog(onExpired: () => void, time
9
9
  };
10
10
  export declare function startShardWorker(processEnvironment?: NodeJS.ProcessEnv, identity?: {
11
11
  version: 1;
12
- mode: "auto" | "fixed";
12
+ mode: "fixed" | "auto";
13
13
  scope: "local" | "coordinated";
14
14
  generation: string;
15
15
  supervisorId: string;
@@ -50,12 +50,15 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
50
50
  HELYX_OAUTH_ENABLED: boolean;
51
51
  HELYX_CLOUD_ENDPOINT: string;
52
52
  HELYX_OWNER_USER_IDS: string[];
53
- HELYX_SHARD_MODE: "off" | "auto" | "fixed";
53
+ HELYX_SHARD_MODE: "fixed" | "off" | "auto";
54
54
  HELYX_SHARD_SCOPE: "local" | "coordinated";
55
55
  HELYX_CLOUD_AUDIT_RETENTION_DAYS: number;
56
56
  HELYX_CLOUD_CREDENTIAL_AUDIT_RETENTION_DAYS: number;
57
57
  LOG_LEVEL: "error" | "fatal" | "warn" | "info" | "debug" | "trace" | "silent";
58
58
  NODE_ENV: "development" | "test" | "production";
59
+ HELYX_STATUS_ENABLED: boolean;
60
+ HELYX_STATUS_URL: string;
61
+ HELYX_STATUS_HEARTBEAT_INTERVAL_SECONDS: number;
59
62
  HELYX_BOT_CONTROL_SECRET?: string | undefined;
60
63
  HELYX_SUPPORT_SERVICE_SECRET?: string | undefined;
61
64
  HELYX_VERIFICATION_INSTALLATION_ID?: string | undefined;
@@ -69,6 +72,9 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
69
72
  HELYX_SHARD_REDIS_NAMESPACE?: string | undefined;
70
73
  HELYX_SHARD_GENERATION?: string | undefined;
71
74
  HELYX_SHARD_DATABASE_CONNECTION_BUDGET?: number | undefined;
75
+ HELYX_STATUS_HEARTBEAT_SECRET?: string | undefined;
76
+ CF_ACCESS_CLIENT_ID?: string | undefined;
77
+ CF_ACCESS_CLIENT_SECRET?: string | undefined;
72
78
  };
73
79
  export declare function shardWorkerCommandRegistration(input: {
74
80
  internalGuildId?: string;
@@ -3,7 +3,7 @@ import { safeError, ServiceRegistry } from "@helyx/core";
3
3
  import { checkDatabaseConnection, createDatabasePool } from "@helyx/database";
4
4
  import { createRuntime } from "@helyx/framework";
5
5
  import { createPlatformServices } from "@helyx/platform";
6
- import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
6
+ import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
7
7
  import { Client, Events, Partials } from "discord.js";
8
8
  import { monitorEventLoopDelay } from "node:perf_hooks";
9
9
  import pino from "pino";
@@ -179,6 +179,9 @@ export async function startShardWorker(processEnvironment = process.env, identit
179
179
  })) {
180
180
  platformServices.set(name, service);
181
181
  }
182
+ await serviceAccess
183
+ .get(HELYX_SERVICE_NAMES.moduleAvailability)
184
+ .refresh();
182
185
  if (environment.HELYX_VERIFICATION_ENABLED &&
183
186
  environment.HELYX_VERIFICATION_URL &&
184
187
  environment.HELYX_VERIFICATION_INTERNAL_SECRET) {
@@ -28,6 +28,7 @@ export function createSupportModule(transport) {
28
28
  {
29
29
  name: "bug",
30
30
  description: "Report a Helyx bug to the support team",
31
+ acknowledgement: "immediate-modal",
31
32
  registration: "global",
32
33
  contexts: ["guild"],
33
34
  integrationTypes: ["guild_install"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/bot",
3
- "version": "0.4.32",
3
+ "version": "0.4.35",
4
4
  "description": "Production Discord runtime for the modular Helyx platform.",
5
5
  "keywords": [
6
6
  "helyx",
@@ -53,9 +53,10 @@
53
53
  "@helyx/core": ">=0.1.0 <0.2.0",
54
54
  "@helyx/database": ">=0.8.0 <0.9.0",
55
55
  "@helyx/framework": ">=0.1.0 <0.2.0",
56
- "@helyx/module-manifest": ">=0.6.0 <0.7.0",
57
- "@helyx/platform": ">=0.5.0 <0.6.0",
58
- "@helyx/sdk": ">=0.7.0 <0.8.0",
56
+ "@helyx/module-manifest": ">=0.7.0 <0.8.0",
57
+ "@helyx/platform": ">=0.6.0 <0.7.0",
58
+ "@helyx/sdk": ">=0.8.0 <0.9.0",
59
+ "@helyx/status-client": ">=0.1.0 <0.2.0",
59
60
  "@helyx/verification-contracts": ">=0.3.0 <0.4.0",
60
61
  "discord.js": "14.27.0",
61
62
  "fastify": "5.10.0",