@helyx/bot 0.4.37 → 0.4.39

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +22 -2
  3. package/dist/cloud-management-module.js +275 -206
  4. package/dist/command-registration-policy.d.ts +8 -0
  5. package/dist/command-registration-policy.js +39 -0
  6. package/dist/command-sync.d.ts +5 -1
  7. package/dist/command-sync.js +67 -3
  8. package/dist/control-server.d.ts +15 -3
  9. package/dist/control-server.js +34 -48
  10. package/dist/dashboard-commands.js +5 -1
  11. package/dist/developer-operations-module.d.ts +2 -0
  12. package/dist/developer-operations-module.js +65 -4
  13. package/dist/discord-rest.d.ts +5 -0
  14. package/dist/discord-rest.js +9 -0
  15. package/dist/events.js +4 -2
  16. package/dist/first-time-command-setup.d.ts +25 -0
  17. package/dist/first-time-command-setup.js +22 -0
  18. package/dist/guild-command-control-routes.d.ts +5 -0
  19. package/dist/guild-command-control-routes.js +191 -0
  20. package/dist/guild-command-discord-client.d.ts +21 -0
  21. package/dist/guild-command-discord-client.js +50 -0
  22. package/dist/guild-command-projection.d.ts +46 -0
  23. package/dist/guild-command-projection.js +254 -0
  24. package/dist/guild-command-read-service.d.ts +201 -0
  25. package/dist/guild-command-read-service.js +272 -0
  26. package/dist/guild-command-release.d.ts +19 -0
  27. package/dist/guild-command-release.js +47 -0
  28. package/dist/guild-command-worker.d.ts +49 -0
  29. package/dist/guild-command-worker.js +443 -0
  30. package/dist/guilds.d.ts +1 -1
  31. package/dist/guilds.js +41 -26
  32. package/dist/index.js +75 -4
  33. package/dist/interactions.d.ts +15 -4
  34. package/dist/interactions.js +285 -70
  35. package/dist/management-module.js +117 -41
  36. package/dist/module-logging.d.ts +1 -0
  37. package/dist/module-logging.js +5 -5
  38. package/dist/module-status-control.d.ts +19 -0
  39. package/dist/module-status-control.js +69 -0
  40. package/dist/modules.d.ts +2 -0
  41. package/dist/modules.js +21 -2
  42. package/dist/nexus-commands.d.ts +1 -12
  43. package/dist/nexus-commands.js +1 -99
  44. package/dist/oauth-module.js +134 -88
  45. package/dist/permissions-module.js +112 -39
  46. package/dist/privacy-module.d.ts +2 -0
  47. package/dist/privacy-module.js +60 -0
  48. package/dist/scheduled-task-runner.js +36 -1
  49. package/dist/shard-contracts.d.ts +24 -0
  50. package/dist/shard-contracts.js +20 -0
  51. package/dist/shard-router.d.ts +1 -0
  52. package/dist/shard-router.js +7 -0
  53. package/dist/shard-supervisor.d.ts +2 -1
  54. package/dist/shard-supervisor.js +94 -16
  55. package/dist/shard-worker.d.ts +5 -0
  56. package/dist/shard-worker.js +9 -1
  57. package/dist/shared-commands.d.ts +13 -0
  58. package/dist/shared-commands.js +163 -0
  59. package/dist/support-module.js +51 -51
  60. package/dist/trivia-data-provider.d.ts +26 -0
  61. package/dist/trivia-data-provider.js +51 -0
  62. package/documentation/README.md +32 -0
  63. package/documentation/examples/.env.example +117 -0
  64. package/documentation/guides/getting-started.md +51 -0
  65. package/documentation/guides/self-hosting.md +40 -0
  66. package/documentation/modules/building-a-module.md +37 -0
  67. package/documentation/modules/publishing-a-module.md +27 -0
  68. package/documentation/modules/testing-a-module.md +46 -0
  69. package/documentation/reference/environment.md +42 -0
  70. package/documentation/runbooks/backup-and-recovery.md +41 -0
  71. package/documentation/runbooks/installation-and-upgrades.md +48 -0
  72. package/package.json +7 -4
  73. package/dist/oauth-module.test.d.ts +0 -2
  74. package/dist/oauth-module.test.js +0 -172
@@ -208,6 +208,14 @@ export declare const shardControlOperationSchema: z.ZodDiscriminatedUnion<[z.Zod
208
208
  guildId: z.ZodString;
209
209
  moduleId: z.ZodString;
210
210
  enabled: z.ZodBoolean;
211
+ }, z.core.$strict>, z.ZodObject<{
212
+ actorUserId: z.ZodString;
213
+ correlationId: z.ZodString;
214
+ kind: z.ZodLiteral<"guild.commands.first-time-setup">;
215
+ guildId: z.ZodString;
216
+ moduleIds: z.ZodArray<z.ZodString>;
217
+ expectedRevision: z.ZodString;
218
+ idempotencyKey: z.ZodString;
211
219
  }, z.core.$strict>, z.ZodObject<{
212
220
  actorUserId: z.ZodString;
213
221
  correlationId: z.ZodString;
@@ -561,6 +569,14 @@ export declare const shardControlRequestSchema: z.ZodObject<{
561
569
  guildId: z.ZodString;
562
570
  moduleId: z.ZodString;
563
571
  enabled: z.ZodBoolean;
572
+ }, z.core.$strict>, z.ZodObject<{
573
+ actorUserId: z.ZodString;
574
+ correlationId: z.ZodString;
575
+ kind: z.ZodLiteral<"guild.commands.first-time-setup">;
576
+ guildId: z.ZodString;
577
+ moduleIds: z.ZodArray<z.ZodString>;
578
+ expectedRevision: z.ZodString;
579
+ idempotencyKey: z.ZodString;
564
580
  }, z.core.$strict>, z.ZodObject<{
565
581
  actorUserId: z.ZodString;
566
582
  correlationId: z.ZodString;
@@ -1419,6 +1435,14 @@ export declare const supervisorMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObje
1419
1435
  guildId: z.ZodString;
1420
1436
  moduleId: z.ZodString;
1421
1437
  enabled: z.ZodBoolean;
1438
+ }, z.core.$strict>, z.ZodObject<{
1439
+ actorUserId: z.ZodString;
1440
+ correlationId: z.ZodString;
1441
+ kind: z.ZodLiteral<"guild.commands.first-time-setup">;
1442
+ guildId: z.ZodString;
1443
+ moduleIds: z.ZodArray<z.ZodString>;
1444
+ expectedRevision: z.ZodString;
1445
+ idempotencyKey: z.ZodString;
1422
1446
  }, z.core.$strict>, z.ZodObject<{
1423
1447
  actorUserId: z.ZodString;
1424
1448
  correlationId: z.ZodString;
@@ -280,6 +280,16 @@ export const shardControlOperationSchema = z.discriminatedUnion("kind", [
280
280
  ...mutationContext.shape,
281
281
  })
282
282
  .strict(),
283
+ z
284
+ .object({
285
+ kind: z.literal("guild.commands.first-time-setup"),
286
+ guildId: snowflake,
287
+ moduleIds: z.array(moduleId).min(1).max(100),
288
+ expectedRevision: z.string().regex(/^\d+$/u),
289
+ idempotencyKey: z.string().min(1).max(160),
290
+ ...mutationContext.shape,
291
+ })
292
+ .strict(),
283
293
  z
284
294
  .object({
285
295
  kind: z.literal("guild.module.configuration.update"),
@@ -992,6 +1002,16 @@ export async function executeShardControlOperation(handler, operation) {
992
1002
  correlationId: operation.correlationId,
993
1003
  });
994
1004
  }
1005
+ case "guild.commands.first-time-setup": {
1006
+ return handler.completeFirstTimeCommandSetup({
1007
+ guildId: operation.guildId,
1008
+ moduleIds: operation.moduleIds,
1009
+ expectedRevision: BigInt(operation.expectedRevision),
1010
+ idempotencyKey: operation.idempotencyKey,
1011
+ actorUserId: operation.actorUserId,
1012
+ correlationId: operation.correlationId,
1013
+ });
1014
+ }
995
1015
  case "guild.module.configuration.update": {
996
1016
  return handler.updateConfiguration({
997
1017
  guildId: operation.guildId,
@@ -26,6 +26,7 @@ export declare class ShardControlRouter implements BotControlHandler {
26
26
  canAccessManagedResource(input: Parameters<BotControlHandler["canAccessManagedResource"]>[0]): ReturnType<BotControlHandler["canAccessManagedResource"]>;
27
27
  listGuildPresence(input: Parameters<BotControlHandler["listGuildPresence"]>[0]): ReturnType<BotControlHandler["listGuildPresence"]>;
28
28
  setModuleStatus(input: Parameters<BotControlHandler["setModuleStatus"]>[0]): ReturnType<BotControlHandler["setModuleStatus"]>;
29
+ completeFirstTimeCommandSetup(input: Parameters<BotControlHandler["completeFirstTimeCommandSetup"]>[0]): ReturnType<BotControlHandler["completeFirstTimeCommandSetup"]>;
29
30
  updateConfiguration(input: Parameters<BotControlHandler["updateConfiguration"]>[0]): ReturnType<BotControlHandler["updateConfiguration"]>;
30
31
  updatePermissions(input: Parameters<BotControlHandler["updatePermissions"]>[0]): ReturnType<BotControlHandler["updatePermissions"]>;
31
32
  updateLoggingPolicy(input: Parameters<BotControlHandler["updateLoggingPolicy"]>[0]): ReturnType<BotControlHandler["updateLoggingPolicy"]>;
@@ -84,6 +84,13 @@ export class ShardControlRouter {
84
84
  setModuleStatus(input) {
85
85
  return this.#route({ kind: "guild.module.status.set", ...input });
86
86
  }
87
+ completeFirstTimeCommandSetup(input) {
88
+ return this.#route({
89
+ kind: "guild.commands.first-time-setup",
90
+ ...input,
91
+ expectedRevision: input.expectedRevision.toString(),
92
+ });
93
+ }
87
94
  updateConfiguration(input) {
88
95
  return this.#route(shardControlOperationSchema.parse({
89
96
  kind: "guild.module.configuration.update",
@@ -1,6 +1,7 @@
1
1
  import type { BotEnvironment } from "@helyx/config";
2
2
  import { type DatabasePool } from "@helyx/database";
3
3
  import type { ModuleDefinition, NumericWriteBehindCommittedBatch, ScheduledTaskExecutionResult } from "@helyx/sdk";
4
+ import { buildApplicationCommandDefinitions } from "./interactions.js";
4
5
  import { type ShardCloudRequest, type ShardIdentity, type ShardProcessHealth } from "./shard-contracts.js";
5
6
  import { type ShardCoordinator, type ShardGenerationDescriptor, type ShardLeaderLease, type ShardLease } from "./shard-coordinator.js";
6
7
  import type { BotHandle } from "./index.js";
@@ -18,7 +19,7 @@ export declare function deferScheduledRoutingFailures(route: () => Promise<Sched
18
19
  export declare function startShardSupervisor(environment: BotEnvironment, processEnvironment: NodeJS.ProcessEnv, workerEntry: string): Promise<BotHandle>;
19
20
  export declare function splitNumericCommittedBatch(batch: NumericWriteBehindCommittedBatch, index: number): NumericWriteBehindCommittedBatch;
20
21
  export declare function buildShardWorkerEnvironment(source: NodeJS.ProcessEnv, identity: ShardIdentity, restartCount: number): NodeJS.ProcessEnv;
21
- export declare function shardRuntimeCompositionHash(modules: readonly ModuleDefinition[], internalGuildId?: string): string;
22
+ export declare function shardRuntimeCompositionHash(modules: readonly ModuleDefinition[], internalGuildId?: string, globalCommandMode?: Parameters<typeof buildApplicationCommandDefinitions>[2]): string;
22
23
  export declare function boundedRespawnDelay(attempt: number): number;
23
24
  export interface SteadyStateOwnershipRenewal {
24
25
  generationRenewed: boolean;
@@ -1,5 +1,5 @@
1
1
  import { safeError, ServiceRegistry } from "@helyx/core";
2
- import { checkDatabaseConnection, createDatabasePool, AuditRepository, InteractionSessionRepository, RateLimitRepository, runConfiguredModuleMigrations, runCoreMigrations, RuntimeStateRepository, } from "@helyx/database";
2
+ import { checkDatabaseConnection, createDatabasePool, AuditRepository, GuildCommandRepository, InteractionSessionRepository, RateLimitRepository, runConfiguredModuleMigrations, runCoreMigrations, RuntimeStateRepository, } from "@helyx/database";
3
3
  import { createPlatformServices, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
4
4
  import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
5
5
  import { Client, ShardingManager } from "discord.js";
@@ -23,11 +23,18 @@ 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 { createPrivacyModule } from "./privacy-module.js";
27
+ import { discordRestOptions } from "./discord-rest.js";
26
28
  import { availabilityCatalogue, availabilityCatalogueDefinitions, } from "./module-availability-control.js";
27
29
  import { createScheduledTaskRunner, schedulerOperationalHealth, } from "./scheduled-task-runner.js";
28
30
  import { numericWriteBehindRegistrations } from "./numeric-write-behind.js";
31
+ import { PostgresTriviaDataProvider } from "./trivia-data-provider.js";
29
32
  import { executeRedisNumericOperation, RedisNumericWriteBehindService, } from "./numeric-write-behind-redis.js";
30
33
  import { encodeNumericIpcValue } from "./numeric-write-behind-proxy.js";
34
+ import { GuildCommandWorker } from "./guild-command-worker.js";
35
+ import { GuildCommandDiscordClient } from "./guild-command-discord-client.js";
36
+ import { GuildCommandReadService } from "./guild-command-read-service.js";
37
+ import { scheduleGuildCommandRelease } from "./guild-command-release.js";
31
38
  const LEASE_RENEW_INTERVAL_MS = 10_000;
32
39
  const STARTUP_LEASE_RENEW_INTERVAL_MS = 5_000;
33
40
  const LEASE_VALIDATION_TIMEOUT_MS = 1_000;
@@ -137,7 +144,10 @@ export async function startShardSupervisor(environment, processEnvironment, work
137
144
  let privacyService;
138
145
  let manager;
139
146
  let scheduledTasks;
147
+ let guildCommandWorker;
148
+ let guildCommandReleasePromise;
140
149
  let scheduledModules = [];
150
+ let commandModules = [];
141
151
  let workersRoutable = false;
142
152
  let promoteToLeader = () => Promise.resolve(false);
143
153
  try {
@@ -171,7 +181,10 @@ export async function startShardSupervisor(environment, processEnvironment, work
171
181
  coreVersion: botVersion(),
172
182
  logger,
173
183
  });
174
- const commandClient = new Client({ intents: [] });
184
+ const commandClient = new Client({
185
+ intents: [],
186
+ rest: discordRestOptions,
187
+ });
175
188
  const unavailableResources = {
176
189
  listGuildPresence: (guildIds) => Promise.resolve(guildIds.map((guildId) => ({
177
190
  guildId,
@@ -197,6 +210,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
197
210
  })
198
211
  : undefined;
199
212
  const supportModule = createSupportModule(cloudConnector);
213
+ const privacyModule = createPrivacyModule();
200
214
  const oauthModule = environment.HELYX_OAUTH_ENABLED
201
215
  ? createOAuthModule()
202
216
  : undefined;
@@ -204,6 +218,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
204
218
  managementModule,
205
219
  permissionsModule,
206
220
  supportModule,
221
+ privacyModule,
207
222
  ...(oauthModule ? [oauthModule] : []),
208
223
  ...(cloudManagementModule ? [cloudManagementModule] : []),
209
224
  ...featureModules,
@@ -224,11 +239,13 @@ export async function startShardSupervisor(environment, processEnvironment, work
224
239
  managementModule,
225
240
  permissionsModule,
226
241
  supportModule,
242
+ privacyModule,
227
243
  ...(oauthModule ? [oauthModule] : []),
228
244
  ...(cloudManagementModule ? [cloudManagementModule] : []),
229
245
  ].map(({ manifest }) => manifest),
230
246
  modules: featureModules.map(({ manifest }) => manifest),
231
247
  processLabel: "shard supervisor registration",
248
+ triviaData: new PostgresTriviaDataProvider(database),
232
249
  health: {
233
250
  getHealth: async () => boundedOperationalHealth(await buildHealth()),
234
251
  },
@@ -238,6 +255,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
238
255
  ...publicModules,
239
256
  ...(operationsModule ? [operationsModule] : []),
240
257
  ];
258
+ commandModules = modules;
241
259
  const availabilityModules = availabilityCatalogueDefinitions(modules);
242
260
  const availabilityService = {
243
261
  listAvailabilityModules: () => availabilityCatalogue(availabilityModules),
@@ -289,7 +307,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
289
307
  environment: environment.NODE_ENV,
290
308
  generation,
291
309
  shardCount,
292
- catalogueHash: shardRuntimeCompositionHash(modules, environment.DISCORD_INTERNAL_GUILD_ID),
310
+ catalogueHash: shardRuntimeCompositionHash(modules, environment.DISCORD_INTERNAL_GUILD_ID, environment.HELYX_GLOBAL_COMMAND_MODE),
293
311
  };
294
312
  deploymentOrder = await nextShardDeploymentOrder(database);
295
313
  if (environment.HELYX_SHARD_SCOPE === "coordinated") {
@@ -402,6 +420,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
402
420
  secret: environment.HELYX_BOT_CONTROL_SECRET,
403
421
  logger: { level: environment.LOG_LEVEL },
404
422
  shardHealth: buildHealth,
423
+ guildCommands: new GuildCommandReadService(database, environment.DISCORD_CLIENT_ID, () => guildCommandWorker?.getHealth(), environment.DISCORD_TOKEN, environment.HELYX_GUILD_COMMAND_WRITES_ENABLED, environment.HELYX_GUILD_COMMAND_WRITE_GUILD_IDS, environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all", commandModules),
405
424
  });
406
425
  await controlServer.listen({
407
426
  host: "::",
@@ -1087,16 +1106,62 @@ export async function startShardSupervisor(environment, processEnvironment, work
1087
1106
  startScheduledTasks();
1088
1107
  }
1089
1108
  function startScheduledTasks() {
1090
- if (scheduledTasks || !leaderLease || !workersRoutable)
1109
+ if (!leaderLease || !workersRoutable)
1091
1110
  return;
1092
- scheduledTasks = createScheduledTaskRunner({
1093
- database,
1094
- modules: scheduledModules,
1095
- route: routeScheduledTask,
1096
- logger,
1097
- workerId: `shard-leader:${leaderLease.generation}:${String(leaderLease.fencingToken)}`,
1098
- });
1099
- scheduledTasks.start();
1111
+ if (!scheduledTasks) {
1112
+ scheduledTasks = createScheduledTaskRunner({
1113
+ database,
1114
+ modules: scheduledModules,
1115
+ route: routeScheduledTask,
1116
+ logger,
1117
+ workerId: `shard-leader:${leaderLease.generation}:${String(leaderLease.fencingToken)}`,
1118
+ });
1119
+ scheduledTasks.start();
1120
+ }
1121
+ if (!guildCommandWorker) {
1122
+ guildCommandWorker = new GuildCommandWorker({
1123
+ database,
1124
+ modules: commandModules,
1125
+ logger,
1126
+ workerId: `shard-leader-command:${leaderLease.generation}:${String(leaderLease.fencingToken)}`,
1127
+ fencingToken: BigInt(leaderLease.fencingToken),
1128
+ ...(environment.DISCORD_INTERNAL_GUILD_ID
1129
+ ? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
1130
+ : {}),
1131
+ ...(environment.HELYX_GUILD_COMMAND_WRITES_ENABLED
1132
+ ? {
1133
+ discord: new GuildCommandDiscordClient({
1134
+ applicationId: environment.DISCORD_CLIENT_ID,
1135
+ token: environment.DISCORD_TOKEN,
1136
+ writeGuildIds: environment.HELYX_GUILD_COMMAND_WRITE_GUILD_IDS,
1137
+ writeAllGuilds: environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all",
1138
+ }),
1139
+ }
1140
+ : {}),
1141
+ });
1142
+ guildCommandWorker.start();
1143
+ }
1144
+ if (environment.HELYX_GUILD_COMMAND_WRITES_ENABLED &&
1145
+ environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all" &&
1146
+ !guildCommandReleasePromise) {
1147
+ const releasePromise = scheduleGuildCommandRelease({
1148
+ database,
1149
+ applicationId: environment.DISCORD_CLIENT_ID,
1150
+ releaseId: environment.HELYX_GUILD_COMMAND_RELEASE_ID,
1151
+ })
1152
+ .then((release) => {
1153
+ logger.info({ event: "guild_commands.release_scheduled", ...release }, "Guild command release rollout prepared");
1154
+ })
1155
+ .catch((error) => {
1156
+ if (guildCommandReleasePromise === releasePromise)
1157
+ guildCommandReleasePromise = undefined;
1158
+ logger.error({
1159
+ event: "guild_commands.release_schedule_failed",
1160
+ error: safeError(error),
1161
+ }, "Guild command release rollout could not be prepared");
1162
+ });
1163
+ guildCommandReleasePromise = releasePromise;
1164
+ }
1100
1165
  }
1101
1166
  async function routeScheduledTask(task, signal) {
1102
1167
  if (!task.guildId)
@@ -1296,6 +1361,8 @@ export async function startShardSupervisor(environment, processEnvironment, work
1296
1361
  workersRoutable = false;
1297
1362
  await scheduledTasks?.stop().catch(() => undefined);
1298
1363
  scheduledTasks = undefined;
1364
+ await guildCommandWorker?.stop().catch(() => undefined);
1365
+ guildCommandWorker = undefined;
1299
1366
  for (const [requestId, pending] of pendingScheduled) {
1300
1367
  clearTimeout(pending.timer);
1301
1368
  pending.resolve(unavailableScheduled(requestId, "supervisor_stopping"));
@@ -1366,9 +1433,11 @@ async function runLeaderStartup(database, environment, modulePackages, modules,
1366
1433
  applicationId: environment.DISCORD_CLIENT_ID,
1367
1434
  token: environment.DISCORD_TOKEN,
1368
1435
  modules,
1436
+ globalCommandMode: environment.HELYX_GLOBAL_COMMAND_MODE,
1369
1437
  ...(environment.DISCORD_INTERNAL_GUILD_ID
1370
1438
  ? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
1371
1439
  : {}),
1440
+ synchroniseInternalGuild: !environment.HELYX_GUILD_COMMAND_WRITES_ENABLED,
1372
1441
  state: new RuntimeCommandHashStore(repository),
1373
1442
  });
1374
1443
  await setRuntimeState(repository, `shards:${environment.HELYX_SHARD_GENERATION ?? "local"}:startup`, {
@@ -1514,8 +1583,8 @@ export function buildShardWorkerEnvironment(source, identity, restartCount) {
1514
1583
  HELYX_SHARD_RESTART_COUNT: String(restartCount),
1515
1584
  };
1516
1585
  }
1517
- export function shardRuntimeCompositionHash(modules, internalGuildId) {
1518
- const commands = buildApplicationCommandDefinitions(modules, internalGuildId);
1586
+ export function shardRuntimeCompositionHash(modules, internalGuildId, globalCommandMode = "legacy") {
1587
+ const commands = buildApplicationCommandDefinitions(modules, internalGuildId, globalCommandMode);
1519
1588
  return createHash("sha256")
1520
1589
  .update(JSON.stringify({
1521
1590
  botVersion: botVersion(),
@@ -1581,17 +1650,26 @@ function failedCloud(requestId, message) {
1581
1650
  }
1582
1651
  async function cleanupExpiredCoreRecords(database, logger) {
1583
1652
  try {
1584
- const [sessions, rateLimits, verificationAudit] = await Promise.all([
1653
+ const [sessions, rateLimits, verificationAudit, commandRecords] = await Promise.all([
1585
1654
  new InteractionSessionRepository(database).deleteExpired(),
1586
1655
  new RateLimitRepository(database).deleteExpired(),
1587
1656
  new AuditRepository(database).deleteModuleEventsOlderThanMonths("helyx.verification", 12),
1657
+ new GuildCommandRepository(database).deleteExpired(12),
1588
1658
  ]);
1589
- if (sessions > 0 || rateLimits > 0 || verificationAudit > 0)
1659
+ const commandRecordCount = commandRecords.history +
1660
+ commandRecords.idempotencyKeys +
1661
+ commandRecords.creationLedger +
1662
+ commandRecords.requests;
1663
+ if (sessions > 0 ||
1664
+ rateLimits > 0 ||
1665
+ verificationAudit > 0 ||
1666
+ commandRecordCount > 0)
1590
1667
  logger.info({
1591
1668
  event: "database.retention_cleaned",
1592
1669
  interactionSessions: sessions,
1593
1670
  rateLimitWindows: rateLimits,
1594
1671
  verificationAuditEvents: verificationAudit,
1672
+ commandRecords: commandRecordCount,
1595
1673
  }, "Expired core records removed");
1596
1674
  }
1597
1675
  catch (error) {
@@ -40,6 +40,10 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
40
40
  HELYX_VERIFICATION_SIGNING_PUBLIC_KEYS: Record<string, string>;
41
41
  HELYX_BOT_CONTROL_PORT: number;
42
42
  HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS: number;
43
+ HELYX_GUILD_COMMAND_WRITES_ENABLED: boolean;
44
+ HELYX_GUILD_COMMAND_WRITE_SCOPE: "allowlist" | "all";
45
+ HELYX_GLOBAL_COMMAND_MODE: "legacy" | "platform-only";
46
+ HELYX_GUILD_COMMAND_WRITE_GUILD_IDS: string[];
43
47
  HELYX_PRIVACY_ENABLED: boolean;
44
48
  HELYX_MESSAGE_CONTENT_ENABLED: boolean;
45
49
  HELYX_GUILD_MEMBERS_ENABLED: boolean;
@@ -64,6 +68,7 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
64
68
  HELYX_VERIFICATION_INSTALLATION_ID?: string | undefined;
65
69
  HELYX_VERIFICATION_URL?: string | undefined;
66
70
  HELYX_VERIFICATION_INTERNAL_SECRET?: string | undefined;
71
+ HELYX_GUILD_COMMAND_RELEASE_ID?: string | undefined;
67
72
  HELYX_PRIVACY_EXECUTOR_DATABASE_URL?: string | undefined;
68
73
  HELYX_PRIVACY_EXECUTOR_DATABASE_SSL?: "disable" | "require" | "verify-full" | undefined;
69
74
  HELYX_OAUTH_INTERNAL_SECRET?: string | undefined;
@@ -22,15 +22,18 @@ import { WorkerCloudConnectorProxy } from "./shard-cloud-proxy.js";
22
22
  import { DiscordOwnedMessageDeletionService } from "./discord-owned-message-service.js";
23
23
  import { DiscordGuildHealthInspection } from "./discord-guild-health.js";
24
24
  import { CoreResourceAuthorizationService } from "./resource-authorization.js";
25
+ import { PostgresTriviaDataProvider } from "./trivia-data-provider.js";
25
26
  import { createDeveloperOperationsModule, WorkerShardHealthProvider, } from "./developer-operations-module.js";
26
27
  import { assertShardMessageSize, controlOperationGuildId, executeShardControlOperation, guildIdToShardId, SHARD_LEASE_HEARTBEAT_TIMEOUT_MS, shardIdentitySchema, supervisorMessageSchema, } from "./shard-contracts.js";
27
28
  import { gatewayIntentsForModules, readBotVersion, } from "./index.js";
28
29
  import { createSupportModule } from "./support-module.js";
29
30
  import { createOAuthModule } from "./oauth-module.js";
31
+ import { createPrivacyModule } from "./privacy-module.js";
30
32
  import { HttpOAuthService } from "./oauth-service.js";
31
33
  import { createLocalScheduledTaskRoute, } from "./scheduled-task-runner.js";
32
34
  import { createLocalNumericWriteBehind } from "./numeric-write-behind.js";
33
35
  import { WorkerNumericWriteBehindProxy } from "./numeric-write-behind-proxy.js";
36
+ import { discordRestOptions } from "./discord-rest.js";
34
37
  const HEALTH_INTERVAL_MS = 5_000;
35
38
  export const WORKER_ORPHAN_TERMINATION_BOUND_MS = SHARD_LEASE_HEARTBEAT_TIMEOUT_MS + 5_000;
36
39
  export function createLeaseHeartbeatWatchdog(onExpired, timeoutMs = SHARD_LEASE_HEARTBEAT_TIMEOUT_MS) {
@@ -114,6 +117,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
114
117
  const modulePackages = await loadConfiguredModulePackages(moduleSpecifiers);
115
118
  const featureModules = modulePackages.map(({ definition }) => definition);
116
119
  client = new Client({
120
+ rest: discordRestOptions,
117
121
  intents: gatewayIntentsForModules(featureModules, environment.HELYX_MESSAGE_CONTENT_ENABLED, environment.HELYX_GUILD_MEMBERS_ENABLED),
118
122
  partials: [Partials.Message, Partials.Channel, Partials.Reaction],
119
123
  allowedMentions: { parse: [] },
@@ -133,6 +137,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
133
137
  })
134
138
  : undefined;
135
139
  const supportModule = createSupportModule(cloudProxy);
140
+ const privacyModule = createPrivacyModule();
136
141
  const oauthModule = environment.HELYX_OAUTH_ENABLED
137
142
  ? createOAuthModule()
138
143
  : undefined;
@@ -140,6 +145,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
140
145
  managementModule,
141
146
  permissionsModule,
142
147
  supportModule,
148
+ privacyModule,
143
149
  ...(oauthModule ? [oauthModule] : []),
144
150
  ...(cloudManagementModule ? [cloudManagementModule] : []),
145
151
  ...featureModules,
@@ -159,11 +165,13 @@ export async function startShardWorker(processEnvironment = process.env, identit
159
165
  managementModule,
160
166
  permissionsModule,
161
167
  supportModule,
168
+ privacyModule,
162
169
  ...(oauthModule ? [oauthModule] : []),
163
170
  ...(cloudManagementModule ? [cloudManagementModule] : []),
164
171
  ].map(({ manifest }) => manifest),
165
172
  modules: featureModules.map(({ manifest }) => manifest),
166
173
  processLabel: `shard worker ${identity.shardId}`,
174
+ triviaData: new PostgresTriviaDataProvider(database),
167
175
  health: shardHealthProvider,
168
176
  })
169
177
  : undefined;
@@ -231,7 +239,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
231
239
  shardId: identity.shardId,
232
240
  fencingToken: BigInt(identity.fencingToken),
233
241
  };
234
- const prepareGuilds = installGuildTracking(client, database, publicModules, logger, enabledCoreModuleIds, shardOwnership, environment.HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS * 1_000, identity.shardCount);
242
+ const prepareGuilds = installGuildTracking(client, database, publicModules, logger, enabledCoreModuleIds, shardOwnership, environment.HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS * 1_000, identity.shardCount, environment.DISCORD_CLIENT_ID);
235
243
  guildTracking = prepareGuilds;
236
244
  const commandsReady = installInteractions(client, modules, shardWorkerCommandRegistration({
237
245
  ...(environment.DISCORD_INTERNAL_GUILD_ID
@@ -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=shared-commands.d.ts.map
@@ -0,0 +1,163 @@
1
+ import { NEXUS_COMMAND_SPECS, NEXUS_ROOT_MODULE_ID, SHARED_COMMAND_FAMILY_SPECS, } from "@helyx/sdk";
2
+ const nexusFamilySpec = {
3
+ name: "nexus",
4
+ description: "View and manage Nexus",
5
+ registrationPolicy: "enabled-guild",
6
+ contexts: ["guild"],
7
+ integrationTypes: ["guild_install"],
8
+ paths: NEXUS_COMMAND_SPECS,
9
+ };
10
+ const familySpecs = [
11
+ ...SHARED_COMMAND_FAMILY_SPECS,
12
+ nexusFamilySpec,
13
+ ];
14
+ export function collectCommandRegistrations(modules) {
15
+ const registrations = modules.flatMap((module) => (module.interactions?.commands ?? []).map((contribution) => ({
16
+ moduleId: module.manifest.id,
17
+ moduleName: module.manifest.name,
18
+ contribution,
19
+ })));
20
+ const familyContributions = modules.flatMap((module) => [
21
+ ...(module.sharedCommands ?? []).map((contribution) => ({
22
+ moduleId: module.manifest.id,
23
+ moduleName: module.manifest.name,
24
+ contribution,
25
+ })),
26
+ ...(module.nexusCommands ?? []).map((contribution) => ({
27
+ moduleId: module.manifest.id,
28
+ moduleName: module.manifest.name,
29
+ contribution: fromNexusContribution(contribution),
30
+ })),
31
+ ]);
32
+ for (const family of familySpecs) {
33
+ const contributions = familyContributions.filter(({ contribution }) => contribution.family === family.name);
34
+ if (contributions.length === 0)
35
+ continue;
36
+ const roots = registrations.filter(({ contribution }) => contribution.name === family.name);
37
+ if (roots.length > 1)
38
+ throw new Error(`Shared command /${family.name} has multiple root owners`);
39
+ const explicitRoot = roots[0];
40
+ if (explicitRoot) {
41
+ validateExplicitRoot(family, explicitRoot);
42
+ registrations.splice(registrations.indexOf(explicitRoot), 1);
43
+ }
44
+ registrations.push(composeSharedCommand(family, explicitRoot ?? syntheticRoot(family, contributions), contributions));
45
+ }
46
+ const unknownFamily = familyContributions.find(({ contribution }) => !familySpecs.some(({ name }) => name === contribution.family));
47
+ if (unknownFamily)
48
+ throw new Error(`Unknown shared command family: ${unknownFamily.contribution.family}`);
49
+ return uniqueRegistrations(registrations);
50
+ }
51
+ function syntheticRoot(family, contributions) {
52
+ const owner = contributions[0];
53
+ return {
54
+ moduleId: owner.moduleId,
55
+ moduleName: owner.moduleName,
56
+ contribution: {
57
+ name: family.name,
58
+ description: family.description,
59
+ registrationPolicy: family.registrationPolicy,
60
+ contexts: family.contexts,
61
+ integrationTypes: family.integrationTypes,
62
+ subcommands: [],
63
+ },
64
+ };
65
+ }
66
+ function validateExplicitRoot(family, root) {
67
+ if (family.name !== "nexus" || root.moduleId !== NEXUS_ROOT_MODULE_ID)
68
+ throw new Error(`Shared command /${family.name} must use its composed root`);
69
+ if (root.contribution.execute ||
70
+ root.contribution.options?.length ||
71
+ root.contribution.subcommands?.length ||
72
+ root.contribution.subcommandGroups?.length) {
73
+ throw new Error(`Shared command /${family.name} root must be empty`);
74
+ }
75
+ }
76
+ function composeSharedCommand(family, root, contributions) {
77
+ const byPath = new Map();
78
+ for (const item of contributions) {
79
+ const path = family.paths.find(({ path: candidate }) => candidate === item.contribution.path);
80
+ if (!path || path.ownerModuleId !== item.moduleId) {
81
+ throw new Error(`Module ${item.moduleId} cannot contribute ${family.name} command ${item.contribution.path}`);
82
+ }
83
+ if (byPath.has(item.contribution.path))
84
+ throw new Error(`Duplicate shared command path: ${family.name}.${item.contribution.path}`);
85
+ byPath.set(item.contribution.path, item);
86
+ }
87
+ const subcommands = [];
88
+ const groups = new Map();
89
+ const routeOwners = new Map();
90
+ const contexts = new Set(family.contexts);
91
+ for (const pathSpec of family.paths) {
92
+ const item = byPath.get(pathSpec.path);
93
+ if (!item)
94
+ continue;
95
+ for (const context of item.contribution.contexts ?? [])
96
+ contexts.add(context);
97
+ routeOwners.set(pathSpec.path, {
98
+ moduleId: item.moduleId,
99
+ moduleName: item.moduleName,
100
+ });
101
+ const [first, second, ...rest] = pathSpec.path.split(".");
102
+ if (rest.length > 0)
103
+ throw new Error(`Shared command path is deeper than one group: ${family.name}.${pathSpec.path}`);
104
+ const command = toSubcommand(item.contribution, second ?? first, `${family.name}.${pathSpec.path}`);
105
+ if (!second) {
106
+ subcommands.push(command);
107
+ continue;
108
+ }
109
+ const current = groups.get(first);
110
+ const group = current ?? {
111
+ name: first,
112
+ description: pathSpec.groupDescription ?? `Manage ${family.name} ${first}`,
113
+ subcommands: [],
114
+ };
115
+ if (current &&
116
+ pathSpec.groupDescription &&
117
+ current.description !== pathSpec.groupDescription)
118
+ throw new Error(`Shared command group description mismatch: ${family.name}.${first}`);
119
+ groups.set(first, {
120
+ ...group,
121
+ subcommands: [...group.subcommands, command],
122
+ });
123
+ }
124
+ return {
125
+ ...root,
126
+ contribution: {
127
+ ...root.contribution,
128
+ contexts: [...contexts],
129
+ subcommands,
130
+ subcommandGroups: [...groups.values()],
131
+ },
132
+ routeOwners,
133
+ };
134
+ }
135
+ function toSubcommand(contribution, name, defaultPermissionId) {
136
+ const subcommand = {
137
+ ...contribution,
138
+ name,
139
+ permissionId: contribution.permissionId ?? defaultPermissionId,
140
+ };
141
+ delete subcommand.family;
142
+ delete subcommand.path;
143
+ delete subcommand.contexts;
144
+ return subcommand;
145
+ }
146
+ function fromNexusContribution(contribution) {
147
+ return {
148
+ ...contribution,
149
+ family: "nexus",
150
+ permissionId: `nexus.${contribution.path}`,
151
+ };
152
+ }
153
+ function uniqueRegistrations(items) {
154
+ const result = new Map();
155
+ for (const item of items) {
156
+ const key = item.contribution.name;
157
+ if (result.has(key))
158
+ throw new Error(`Duplicate command name: ${key}`);
159
+ result.set(key, item);
160
+ }
161
+ return result;
162
+ }
163
+ //# sourceMappingURL=shared-commands.js.map