@helyx/bot 1.5.2 → 1.5.4

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 (115) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/command-root-catalogue.json +452 -0
  3. package/dist/cached-message-deletion-authority.d.ts +27 -0
  4. package/dist/cached-message-deletion-authority.js +288 -0
  5. package/dist/channel-audit-configuration.d.ts +11 -1
  6. package/dist/channel-audit-configuration.js +28 -6
  7. package/dist/channel-audit-control.js +42 -2
  8. package/dist/channel-audit-permissions.js +76 -3
  9. package/dist/cloud-command-policy-sync.d.ts +26 -0
  10. package/dist/cloud-command-policy-sync.js +198 -0
  11. package/dist/cloud-connector.d.ts +6 -1
  12. package/dist/cloud-connector.js +224 -53
  13. package/dist/cloud-control-dispatcher.js +18 -0
  14. package/dist/command-policy-runtime.d.ts +23 -0
  15. package/dist/command-policy-runtime.js +53 -0
  16. package/dist/command-root-catalogue.d.ts +5 -0
  17. package/dist/command-root-catalogue.js +16 -0
  18. package/dist/control-error-mapping.d.ts +8 -0
  19. package/dist/control-error-mapping.js +60 -0
  20. package/dist/control-errors.d.ts +3 -1
  21. package/dist/control-errors.js +4 -1
  22. package/dist/control-health-routes.d.ts +10 -0
  23. package/dist/control-health-routes.js +28 -0
  24. package/dist/control-server-schemas.d.ts +7 -0
  25. package/dist/control-server-schemas.js +9 -0
  26. package/dist/control-server.d.ts +27 -6
  27. package/dist/control-server.js +103 -122
  28. package/dist/dashboard-feature-control.d.ts +2 -1
  29. package/dist/dashboard-feature-control.js +1 -0
  30. package/dist/dashboard-logging.d.ts +20 -0
  31. package/dist/dashboard-logging.js +39 -0
  32. package/dist/dashboard-module-projection.d.ts +1 -1
  33. package/dist/dashboard-module-projection.js +5 -2
  34. package/dist/dashboard-preview-import-routes.js +8 -0
  35. package/dist/database-bootstrap.d.ts +1 -1
  36. package/dist/database-bootstrap.js +4 -3
  37. package/dist/discord-channel-audit.js +1 -0
  38. package/dist/discord-user-identities.d.ts +14 -0
  39. package/dist/discord-user-identities.js +64 -0
  40. package/dist/emergency-deletion-dispatcher.d.ts +68 -0
  41. package/dist/emergency-deletion-dispatcher.js +502 -0
  42. package/dist/emergency-deletion-rest-transport.d.ts +24 -0
  43. package/dist/emergency-deletion-rest-transport.js +253 -0
  44. package/dist/events.d.ts +5 -0
  45. package/dist/events.js +53 -10
  46. package/dist/guarded-discord-dispatcher.d.ts +29 -0
  47. package/dist/guarded-discord-dispatcher.js +204 -0
  48. package/dist/guild-command-discord-client.d.ts +2 -0
  49. package/dist/guild-command-discord-client.js +7 -0
  50. package/dist/guild-command-projection.d.ts +4 -1
  51. package/dist/guild-command-projection.js +25 -0
  52. package/dist/guild-command-read-service.d.ts +10 -1
  53. package/dist/guild-command-read-service.js +37 -0
  54. package/dist/guild-command-worker.d.ts +19 -4
  55. package/dist/guild-command-worker.js +356 -26
  56. package/dist/guild-message-protection-author-roles.d.ts +7 -0
  57. package/dist/guild-message-protection-author-roles.js +93 -0
  58. package/dist/guild-message-protection-deletion.d.ts +21 -0
  59. package/dist/guild-message-protection-deletion.js +138 -0
  60. package/dist/guild-message-protection-delivery.d.ts +41 -0
  61. package/dist/guild-message-protection-delivery.js +223 -0
  62. package/dist/guild-message-update-metadata.d.ts +32 -0
  63. package/dist/guild-message-update-metadata.js +255 -0
  64. package/dist/guilds.d.ts +2 -1
  65. package/dist/guilds.js +11 -1
  66. package/dist/hosted-server-moderation.d.ts +2 -2
  67. package/dist/index.js +68 -4
  68. package/dist/interactions.d.ts +3 -1
  69. package/dist/interactions.js +77 -12
  70. package/dist/invite-rollup-maintenance.d.ts +9 -0
  71. package/dist/invite-rollup-maintenance.js +12 -0
  72. package/dist/managed-resource-authorization.d.ts +15 -0
  73. package/dist/managed-resource-authorization.js +37 -0
  74. package/dist/managed-resource-create-defaults.d.ts +21 -0
  75. package/dist/managed-resource-create-defaults.js +83 -0
  76. package/dist/managed-resource-query.d.ts +6 -0
  77. package/dist/managed-resource-query.js +15 -0
  78. package/dist/member-role-ids.d.ts +17 -0
  79. package/dist/member-role-ids.js +66 -0
  80. package/dist/message-protection-admission.d.ts +60 -0
  81. package/dist/message-protection-admission.js +279 -0
  82. package/dist/message-protection-cold-data.d.ts +42 -0
  83. package/dist/message-protection-cold-data.js +180 -0
  84. package/dist/message-protection-coordinator.d.ts +73 -0
  85. package/dist/message-protection-coordinator.js +622 -0
  86. package/dist/message-protection-settings.d.ts +40 -0
  87. package/dist/message-protection-settings.js +275 -0
  88. package/dist/message-protection-summary.d.ts +8 -0
  89. package/dist/message-protection-summary.js +98 -0
  90. package/dist/module-availability-control.js +20 -12
  91. package/dist/module-logging.d.ts +3 -2
  92. package/dist/module-logging.js +6 -1
  93. package/dist/modules.js +56 -1
  94. package/dist/mysql-runtime-persistence.d.ts +1 -0
  95. package/dist/mysql-runtime-persistence.js +4 -1
  96. package/dist/nexus-command-recovery-control-routes.d.ts +5 -0
  97. package/dist/nexus-command-recovery-control-routes.js +48 -0
  98. package/dist/nexus-command-references.d.ts +6 -0
  99. package/dist/nexus-command-references.js +33 -0
  100. package/dist/scheduled-task-runner.js +23 -4
  101. package/dist/shard-command-startup.d.ts +13 -0
  102. package/dist/shard-command-startup.js +27 -0
  103. package/dist/shard-contracts.d.ts +84 -0
  104. package/dist/shard-contracts.js +33 -0
  105. package/dist/shard-core-readiness.d.ts +5 -0
  106. package/dist/shard-core-readiness.js +19 -0
  107. package/dist/shard-router.d.ts +3 -0
  108. package/dist/shard-router.js +10 -1
  109. package/dist/shard-supervisor.d.ts +1 -1
  110. package/dist/shard-supervisor.js +124 -43
  111. package/dist/shard-worker.js +25 -15
  112. package/documentation/README.md +1 -0
  113. package/documentation/examples/.env.example +0 -45
  114. package/documentation/guides/self-hosting.md +54 -0
  115. package/package.json +18 -17
@@ -1,15 +1,19 @@
1
1
  import { safeError, ServiceRegistry } from "@helyx/core";
2
2
  import { checkDatabaseConnection, createDatabasePool, AuditRepository, GuildCommandRepository, HostedServerSafetyRepository, InstallationRepository, inspectConfiguredModuleMigrations, inspectCoreMigrations, inspectCreatorOnboardingMigrations, runCreatorOnboardingMigrations, InteractionSessionRepository, EckoOperationsRepository, PremiumAllocationRepository, RateLimitRepository, runConfiguredModuleMigrations, runCoreMigrations, RuntimeStateRepository, } from "@helyx/database";
3
3
  import { assertRepositoryEngineSupported } from "@helyx/database/adapters";
4
- import { createPlatformServices, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
5
- import { AUTOMOD_ENHANCED_CONTRACT_VERSION, AUTOMOD_ENHANCED_PROVIDER_ID, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
4
+ import { createPlatformServices, createPostgresCommandRestrictionRecovery, createPostgresNexusCommandRecovery, createPostgresCommandCatalogueRecovery, createPostgresCommandPolicyDelivery, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
5
+ import { AUTOMOD_ENHANCED_CONTRACT_VERSION, AUTOMOD_ENHANCED_PROVIDER_ID, HELYX_SERVICE_NAMES, collectReservedCommandNames, } from "@helyx/sdk";
6
6
  import { Client, ShardingManager } from "discord.js";
7
7
  import { createHash, randomUUID } from "node:crypto";
8
8
  import { createRequire } from "node:module";
9
+ import { loadBundledCommandCatalogue } from "./command-root-catalogue.js";
10
+ import { completeShardCommandStartup, shardStartupCatalogueReady, } from "./shard-command-startup.js";
9
11
  import { join } from "node:path";
10
12
  import pino, {} from "pino";
11
13
  import { synchroniseApplicationCommands, } from "./command-sync.js";
12
- import { CloudConnectorController, } from "./cloud-connector.js";
14
+ import { CloudConnectorController } from "./cloud-connector.js";
15
+ import { commandPolicyLeaderIsCurrent, captureShardedCommandPolicyAdmission, shardCommandPolicyScope, } from "./command-policy-runtime.js";
16
+ import { allocateShardDeploymentOrder, prepareShardDeploymentOrder, } from "./shard-core-readiness.js";
13
17
  import { createCloudManagementModule } from "./cloud-management-module.js";
14
18
  import { BotControlService, createBotControlServer, mapBotControlError, } from "./control-server.js";
15
19
  import { createManagementModule } from "./management-module.js";
@@ -131,6 +135,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
131
135
  let numericRegistrations = [];
132
136
  let leaderLease;
133
137
  let cloudConnector;
138
+ let commandPolicyRuntime;
134
139
  let cloudStarted = false;
135
140
  let maintainingLeases = false;
136
141
  let rebalancingShards = false;
@@ -164,6 +169,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
164
169
  let guildCommandReleasePromise;
165
170
  let scheduledModules = [];
166
171
  let commandModules = [];
172
+ let commandCatalogue;
167
173
  let workersRoutable = false;
168
174
  let promoteToLeader = () => Promise.resolve(false);
169
175
  try {
@@ -173,6 +179,15 @@ export async function startShardSupervisor(environment, processEnvironment, work
173
179
  const modulePackages = await loadConfiguredModulePackages(moduleSpecifiers);
174
180
  const featureModules = modulePackages.map(({ definition }) => definition);
175
181
  assertReviewedPremiumCatalogue(featureModules);
182
+ const bundledCommandCatalogue = featureModules.some(({ manifest }) => manifest.id === "helyx.nexus-core")
183
+ ? await loadBundledCommandCatalogue()
184
+ : null;
185
+ const catalogueIdentity = bundledCommandCatalogue
186
+ ? {
187
+ revision: bundledCommandCatalogue.revision,
188
+ fingerprint: bundledCommandCatalogue.fingerprint,
189
+ }
190
+ : null;
176
191
  const gateway = await fetchGatewayBotInformation(environment.DISCORD_TOKEN);
177
192
  shardCount =
178
193
  environment.HELYX_SHARD_MODE === "fixed"
@@ -301,11 +316,57 @@ export async function startShardSupervisor(environment, processEnvironment, work
301
316
  listAvailabilityModules: () => availabilityCatalogue(availabilityModules, statusModules, runtimeAvailability),
302
317
  };
303
318
  const initialiseLeader = async () => {
304
- await runLeaderStartup(database, environment, modulePackages, modules, logger);
319
+ let commandPolicyAdmission;
320
+ let policyLeader;
321
+ let policyOwnerCurrent;
322
+ await runLeaderStartup(database, environment, modulePackages, modules, logger, {
323
+ catalogue: catalogueIdentity,
324
+ assertOwnership: assertStartupLeaseHealthy,
325
+ captureCommandPolicy: async () => {
326
+ if (environment.HELYX_RUNTIME_PROFILE !== "self-hosted")
327
+ return;
328
+ const leader = leaderLease;
329
+ if (!leader)
330
+ throw new Error("Command policy requires the current shard leader");
331
+ const delivery = createPostgresCommandPolicyDelivery(database, {
332
+ applicationId: environment.DISCORD_CLIENT_ID,
333
+ scope: shardCommandPolicyScope(generation, deploymentOrder),
334
+ });
335
+ policyLeader = leader;
336
+ policyOwnerCurrent = coordinator
337
+ ? () => withTimeout(commandPolicyLeaderIsCurrent({
338
+ coordinator: coordinator,
339
+ descriptor: generationDescriptor,
340
+ deploymentOrder,
341
+ leader,
342
+ }), LEASE_VALIDATION_TIMEOUT_MS)
343
+ : () => Promise.resolve(true);
344
+ commandPolicyAdmission = await captureShardedCommandPolicyAdmission(delivery, leader.supervisorId);
345
+ },
346
+ prepareCommandPolicy: async () => {
347
+ if (environment.HELYX_RUNTIME_PROFILE !== "self-hosted")
348
+ return;
349
+ if (!commandPolicyAdmission || !policyLeader || !policyOwnerCurrent)
350
+ throw new Error("Command policy admission was not captured at database readiness");
351
+ commandPolicyRuntime = await commandPolicyAdmission.claim(String(policyLeader.fencingToken), policyOwnerCurrent);
352
+ await cloudConnector?.setCommandPolicyDelivery(commandPolicyRuntime);
353
+ },
354
+ publishCatalogue: async () => {
355
+ if (!bundledCommandCatalogue)
356
+ return;
357
+ commandCatalogue = createPostgresCommandCatalogueRecovery(database, environment.DISCORD_CLIENT_ID);
358
+ await commandCatalogue.publish(bundledCommandCatalogue);
359
+ },
360
+ });
305
361
  assertStartupLeaseHealthy();
306
362
  privacyDatabase = await createPrivacyDatabase(environment, database);
307
363
  assertStartupLeaseHealthy();
308
- const services = new Map(createPlatformServices(database, publicModules, platformServiceOptions(environment, publicModules)));
364
+ const services = new Map(createPlatformServices(database, publicModules, {
365
+ ...platformServiceOptions(environment, publicModules),
366
+ ...(commandPolicyRuntime
367
+ ? { commandPolicyDeliveryScope: commandPolicyRuntime.owner.scope }
368
+ : {}),
369
+ }));
309
370
  runtimeAvailability = services.get(HELYX_SERVICE_NAMES.moduleAvailability);
310
371
  leaderScannerRegistry = new BotAutoModerationScannerRegistry(publicModules, new ServiceRegistry(services), "hosted", undefined, environment.HELYX_DEPLOYMENT_ENVIRONMENT);
311
372
  if (numericWriteBehind)
@@ -350,9 +411,9 @@ export async function startShardSupervisor(environment, processEnvironment, work
350
411
  environment: environment.NODE_ENV,
351
412
  generation,
352
413
  shardCount,
353
- catalogueHash: shardRuntimeCompositionHash(modules, environment.DISCORD_INTERNAL_GUILD_ID, environment.HELYX_GLOBAL_COMMAND_MODE),
414
+ catalogueHash: shardRuntimeCompositionHash(modules, environment.DISCORD_INTERNAL_GUILD_ID, environment.HELYX_GLOBAL_COMMAND_MODE, bundledCommandCatalogue?.fingerprint),
354
415
  };
355
- deploymentOrder = await nextShardDeploymentOrder(database);
416
+ deploymentOrder = await prepareShardDeploymentOrder(database, environment.HELYX_DATABASE_STARTUP_MODE);
356
417
  if (environment.HELYX_SHARD_SCOPE === "coordinated") {
357
418
  coordinator = new RedisShardCoordinator(environment.HELYX_SHARD_REDIS_URL, environment.HELYX_SHARD_REDIS_NAMESPACE, {
358
419
  onError: (error) => logger.error({ event: "shard.redis_error", error: safeError(error) }, "Shard coordination failed"),
@@ -417,7 +478,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
417
478
  await initialiseLeader();
418
479
  }
419
480
  else {
420
- await waitForLeaderStartup(database, generation);
481
+ await waitForLeaderStartup(database, generation, catalogueIdentity);
421
482
  assertStartupLeaseHealthy();
422
483
  }
423
484
  const router = new ShardControlRouter({
@@ -484,6 +545,15 @@ export async function startShardSupervisor(environment, processEnvironment, work
484
545
  : undefined;
485
546
  controlServer = await createBotControlServer({
486
547
  service: router,
548
+ ...(commandModules.some(({ manifest }) => manifest.id === "helyx.nexus-core")
549
+ ? {
550
+ nexusCommandRecovery: createPostgresNexusCommandRecovery(database, {
551
+ applicationId: environment.DISCORD_CLIENT_ID,
552
+ persistedCommandCatalogue: true,
553
+ reservedCommandNames: collectReservedCommandNames(commandModules),
554
+ }),
555
+ }
556
+ : {}),
487
557
  secret: environment.HELYX_BOT_CONTROL_SECRET,
488
558
  ...shardHostedControlCallbacks(router),
489
559
  logger: { level: environment.LOG_LEVEL },
@@ -1218,6 +1288,15 @@ export async function startShardSupervisor(environment, processEnvironment, work
1218
1288
  }
1219
1289
  if (!guildCommandWorker) {
1220
1290
  guildCommandWorker = new GuildCommandWorker({
1291
+ ...(commandPolicyRuntime
1292
+ ? { commandPolicyDelivery: commandPolicyRuntime.delivery }
1293
+ : {}),
1294
+ ...(commandCatalogue ? { commandCatalogue } : {}),
1295
+ ...(commandModules.some(({ manifest }) => manifest.id === "helyx.nexus-core")
1296
+ ? {
1297
+ commandRestrictions: createPostgresCommandRestrictionRecovery(database, environment.DISCORD_CLIENT_ID),
1298
+ }
1299
+ : {}),
1221
1300
  database,
1222
1301
  modules: commandModules,
1223
1302
  logger,
@@ -1540,7 +1619,10 @@ export function splitNumericCommittedBatch(batch, index) {
1540
1619
  results: [result],
1541
1620
  };
1542
1621
  }
1543
- async function runLeaderStartup(database, environment, modulePackages, modules, logger) {
1622
+ async function runLeaderStartup(database, environment, modulePackages, modules, logger, commandStartup) {
1623
+ // Core readiness and durable order were established before election. Capture
1624
+ // admission before any further migration/owner startup can wait asynchronously.
1625
+ await commandStartup.captureCommandPolicy();
1544
1626
  const moduleMigrationSources = modulePackages.map(({ definition, packageRoot }) => ({
1545
1627
  moduleId: definition.manifest.id,
1546
1628
  moduleVersion: definition.manifest.version,
@@ -1566,23 +1648,36 @@ async function runLeaderStartup(database, environment, modulePackages, modules,
1566
1648
  await runCoreMigrations(database);
1567
1649
  await runConfiguredModuleMigrations(database, moduleMigrationSources);
1568
1650
  }
1651
+ await commandStartup.prepareCommandPolicy();
1569
1652
  await enforcePremiumCatalogue(database, logger);
1570
1653
  const repository = new RuntimeStateRepository(database);
1571
- await synchroniseApplicationCommands({
1572
- applicationId: environment.DISCORD_CLIENT_ID,
1573
- token: environment.DISCORD_TOKEN,
1574
- modules,
1575
- globalCommandMode: environment.HELYX_GLOBAL_COMMAND_MODE,
1576
- ...(environment.DISCORD_INTERNAL_GUILD_ID
1577
- ? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
1578
- : {}),
1579
- // Worker-owned guild collections stay untouched while command writes are off.
1580
- synchroniseInternalGuild: false,
1581
- state: new RuntimeCommandHashStore(repository),
1582
- });
1583
- await setRuntimeState(repository, `shards:${environment.HELYX_SHARD_GENERATION ?? "local"}:startup`, {
1584
- ready: true,
1585
- completedAt: new Date().toISOString(),
1654
+ await completeShardCommandStartup({
1655
+ assertOwnership: () => commandStartup.assertOwnership(),
1656
+ publishCatalogue: () => commandStartup.publishCatalogue(),
1657
+ synchronise: async () => {
1658
+ await synchroniseApplicationCommands({
1659
+ applicationId: environment.DISCORD_CLIENT_ID,
1660
+ token: environment.DISCORD_TOKEN,
1661
+ modules,
1662
+ globalCommandMode: environment.HELYX_GLOBAL_COMMAND_MODE,
1663
+ ...(environment.DISCORD_INTERNAL_GUILD_ID
1664
+ ? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
1665
+ : {}),
1666
+ // Worker-owned guild collections stay untouched while command writes are off.
1667
+ synchroniseInternalGuild: false,
1668
+ state: new RuntimeCommandHashStore(repository),
1669
+ });
1670
+ },
1671
+ markReady: () => setRuntimeState(repository, `shards:${environment.HELYX_SHARD_GENERATION ?? "local"}:startup`, {
1672
+ ready: true,
1673
+ completedAt: new Date().toISOString(),
1674
+ commandCatalogue: commandStartup.catalogue
1675
+ ? {
1676
+ revision: commandStartup.catalogue.revision,
1677
+ fingerprint: commandStartup.catalogue.fingerprint,
1678
+ }
1679
+ : null,
1680
+ }),
1586
1681
  });
1587
1682
  logger.info({ event: "shard.singleton_startup_complete" }, "Shard singleton startup complete");
1588
1683
  }
@@ -1619,15 +1714,12 @@ async function setRuntimeState(repository, key, value) {
1619
1714
  if (!updated)
1620
1715
  throw new Error("Runtime state changed concurrently");
1621
1716
  }
1622
- async function waitForLeaderStartup(database, generation) {
1717
+ async function waitForLeaderStartup(database, generation, catalogue) {
1623
1718
  const deadline = Date.now() + STARTUP_WAIT_MS;
1624
1719
  while (Date.now() < deadline) {
1625
1720
  try {
1626
1721
  const state = await new RuntimeStateRepository(database).get(`shards:${generation}:startup`);
1627
- if (typeof state?.value === "object" &&
1628
- state.value !== null &&
1629
- !Array.isArray(state.value) &&
1630
- state.value.ready === true)
1722
+ if (shardStartupCatalogueReady(state?.value, catalogue))
1631
1723
  return;
1632
1724
  }
1633
1725
  catch {
@@ -1741,11 +1833,12 @@ export function buildShardWorkerEnvironment(source, identity, restartCount) {
1741
1833
  HELYX_SHARD_RESTART_COUNT: String(restartCount),
1742
1834
  };
1743
1835
  }
1744
- export function shardRuntimeCompositionHash(modules, internalGuildId, globalCommandMode = "legacy") {
1836
+ export function shardRuntimeCompositionHash(modules, internalGuildId, globalCommandMode = "legacy", commandCatalogueFingerprint) {
1745
1837
  const commands = buildApplicationCommandDefinitions(modules, internalGuildId, globalCommandMode);
1746
1838
  return createHash("sha256")
1747
1839
  .update(JSON.stringify({
1748
1840
  botVersion: botVersion(),
1841
+ ...(commandCatalogueFingerprint ? { commandCatalogueFingerprint } : {}),
1749
1842
  modules: modules
1750
1843
  .map(({ manifest }) => ({
1751
1844
  id: manifest.id,
@@ -1990,19 +2083,7 @@ function sameShardLease(left, right) {
1990
2083
  left.fencingToken === right.fencingToken);
1991
2084
  }
1992
2085
  export async function nextShardDeploymentOrder(database) {
1993
- let value;
1994
- try {
1995
- value = await new RuntimeStateRepository(database).nextShardDeploymentOrder();
1996
- }
1997
- catch (error) {
1998
- if (error.code !== "42P01")
1999
- throw error;
2000
- return 1;
2001
- }
2002
- if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
2003
- throw new Error("Shard deployment order exceeds the safe runtime range");
2004
- }
2005
- return Number(value);
2086
+ return allocateShardDeploymentOrder(database);
2006
2087
  }
2007
2088
  export async function renewStartupOwnership(coordinator, descriptor, deploymentOrder, leaderLease) {
2008
2089
  if (!(await coordinator.refreshGeneration(descriptor, deploymentOrder)))
@@ -1,4 +1,5 @@
1
1
  import { parseBotEnvironment } from "@helyx/config";
2
+ import { shardCommandPolicyScope } from "./command-policy-runtime.js";
2
3
  import { safeError, ServiceRegistry } from "@helyx/core";
3
4
  import { checkDatabaseConnection, createDatabasePool, PremiumAllocationRepository, InstallationRepository, } from "@helyx/database";
4
5
  import { assertRepositoryEngineSupported } from "@helyx/database/adapters";
@@ -264,6 +265,11 @@ export async function startShardWorker(processEnvironment = process.env, identit
264
265
  const serviceOptions = platformServiceOptions(environment, publicModules);
265
266
  for (const [name, service] of createPlatformServices(database, publicModules, {
266
267
  ...serviceOptions,
268
+ ...(environment.HELYX_RUNTIME_PROFILE === "self-hosted"
269
+ ? {
270
+ commandPolicyDeliveryScope: shardCommandPolicyScope(identity.generation, identity.deploymentOrder),
271
+ }
272
+ : {}),
267
273
  recordRecoveryOwnership: {
268
274
  mode: "shard",
269
275
  shardId: identity.shardId,
@@ -391,21 +397,24 @@ export async function startShardWorker(processEnvironment = process.env, identit
391
397
  shardCount: identity.shardCount,
392
398
  })
393
399
  : undefined;
394
- const commandsReady = installInteractions(client, modules, shardWorkerCommandRegistration({
395
- ...(environment.DISCORD_INTERNAL_GUILD_ID
396
- ? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
397
- : {}),
398
- prepare: prepareGuilds,
399
- ...(operationsModule
400
- ? { operationsModuleId: operationsModule.manifest.id }
401
- : {}),
402
- supportModuleId: supportModule.manifest.id,
403
- diagnosticModuleId: diagnosticModule.manifest.id,
404
- ...(premiumActivationModule
405
- ? { premiumActivationModuleId: premiumActivationModule.manifest.id }
406
- : {}),
407
- ...(oauthModule ? { oauthModuleId: oauthModule.manifest.id } : {}),
408
- }), logger, serviceAccess);
400
+ const commandsReady = installInteractions(client, modules, {
401
+ ...shardWorkerCommandRegistration({
402
+ ...(environment.DISCORD_INTERNAL_GUILD_ID
403
+ ? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
404
+ : {}),
405
+ prepare: prepareGuilds,
406
+ ...(operationsModule
407
+ ? { operationsModuleId: operationsModule.manifest.id }
408
+ : {}),
409
+ supportModuleId: supportModule.manifest.id,
410
+ diagnosticModuleId: diagnosticModule.manifest.id,
411
+ ...(premiumActivationModule
412
+ ? { premiumActivationModuleId: premiumActivationModule.manifest.id }
413
+ : {}),
414
+ ...(oauthModule ? { oauthModuleId: oauthModule.manifest.id } : {}),
415
+ }),
416
+ resolveGuildCommandName: (input) => guildCommandReads.resolveInteractionCommand(input),
417
+ }, logger, serviceAccess);
409
418
  moduleEvents = installModuleEvents(client, publicModules, logger, serviceAccess, {
410
419
  runtimeProfile: environment.HELYX_RUNTIME_PROFILE,
411
420
  evidenceStore: moderationPlatformServices.evidenceStore,
@@ -669,6 +678,7 @@ async function handleControl(identity, service, hostedOwnerObserver, client, hos
669
678
  ok: false,
670
679
  status: mapped?.status ?? 500,
671
680
  message: mapped?.message ?? "The owning shard rejected the request",
681
+ ...(mapped?.fieldError ? { fieldError: mapped.fieldError } : {}),
672
682
  };
673
683
  }
674
684
  }
@@ -13,6 +13,7 @@ and module guides.
13
13
 
14
14
  - [Install a self-hosted Helyx instance](guides/getting-started.md)
15
15
  - [Understand the self-hosting boundary](guides/self-hosting.md)
16
+ - [Understand upcoming shared Nexus command names](guides/self-hosting.md#nexus-command-names-upcoming)
16
17
  - [Configure the environment](reference/environment.md)
17
18
  - [Operate installations and upgrades](runbooks/installation-and-upgrades.md)
18
19
  - [Plan backup and recovery](runbooks/backup-and-recovery.md)
@@ -9,16 +9,6 @@ DISCORD_CLIENT_ID=12345678901234567
9
9
  # Explicit privileged gateway opt-in. Keep false unless a reviewed loaded module
10
10
  # requires message content and it is enabled in the Discord developer portal.
11
11
  HELYX_MESSAGE_CONTENT_ENABLED=false
12
- # Enhanced Auto Moderation stays inside this bot process. One worker is the
13
- # recommended default (range 1-4); increase only after measuring CPU and memory.
14
- # Hosted scanning uses the separate Helyx Ecko service, not these settings.
15
- HELYX_AUTOMOD_EMBEDDED_WORKERS=1
16
- # Optional conservative queue overrides (hard ceilings: 5000 items, 64 MiB,
17
- # 100 items per server). Defaults are used when these remain commented.
18
- # HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS=5000
19
- # HELYX_AUTOMOD_EMBEDDED_QUEUE_BYTES=67108864
20
- # HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS_PER_GUILD=100
21
- # HELYX_AUTOMOD_EMBEDDED_REFRESH_SECONDS=300
22
12
  # Enable for loaded member workflows such as Welcome, Access Control or Levels,
23
13
  # with Server Members Intent also enabled in the Discord developer portal.
24
14
  HELYX_GUILD_MEMBERS_ENABLED=false
@@ -51,10 +41,6 @@ HELYX_CLOUD_CONNECT_ENABLED=true
51
41
  # trace, debug, info, warn, error, fatal or silent
52
42
  LOG_LEVEL=info
53
43
 
54
- # Guild-scoped development commands only
55
- # DISCORD_INTERNAL_GUILD_ID=12345678901234567
56
- # APPROVED_ROLES=12345678901234567,12345678901234568
57
-
58
44
  # SHARDING: OFF BY DEFAULT
59
45
 
60
46
  # `off` is the compatible self-host default. Keep shard-only values commented
@@ -80,12 +66,6 @@ HELYX_SHARD_MODE=off
80
66
  # Additional reviewed module packages
81
67
  # HELYX_MODULE_PACKAGES=
82
68
 
83
- # Private dashboard control endpoint — configure both together
84
- # HELYX_BOT_CONTROL_SECRET=replace-with-a-base64-encoded-32-byte-key
85
- # HELYX_BOT_CONTROL_PORT=3001
86
- # HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS=900
87
- HELYX_HOSTED_MODERATION_ENABLED=false
88
-
89
69
  # Guild command writes remain off until explicitly enabled for known guild IDs.
90
70
  # For enabled-module server commands, set writes=true, retain allowlist scope,
91
71
  # add comma-separated Discord server IDs below, and use global mode platform-only.
@@ -96,14 +76,6 @@ HELYX_GUILD_COMMAND_WRITE_SCOPE=allowlist
96
76
  HELYX_GLOBAL_COMMAND_MODE=legacy
97
77
  # HELYX_GUILD_COMMAND_WRITE_GUILD_IDS=
98
78
 
99
- # Alternative Cloud Connect enrolment — the Discord command is preferred
100
- # HELYX_CLOUD_ENROLMENT_TOKEN=replace-with-a-short-lived-enrolment-code
101
- # HELYX_CLOUD_ENROLMENT_FILE=./helyx.cloud-enrolment.json
102
-
103
- # Hosted OAuth bridge. Keep disabled unless the API and bot share this secret.
104
- HELYX_OAUTH_ENABLED=false
105
- # HELYX_OAUTH_INTERNAL_SECRET=replace-with-a-base64-encoded-32-byte-key
106
-
107
79
  # DANGER ZONE — HELYX-MANAGED VALUES, DO NOT EDIT
108
80
 
109
81
  # Generated module selection
@@ -113,22 +85,5 @@ HELYX_MODULE_CATALOGUE_PATH=./helyx.modules.json
113
85
  # and Invite Tracking; retain it unchanged across restarts and deployments.
114
86
  # HELYX_DATA_ENCRYPTION_KEY=replace-with-a-base64-encoded-32-byte-key
115
87
 
116
- # Official Cloud Connect endpoints
117
- # HELYX_CLOUD_API_URL=https://api.helyx.gg
118
- # HELYX_CLOUD_ENDPOINT=wss://connect.helyx.gg/v1/connect
119
-
120
- # Security audit retention
121
- # HELYX_CLOUD_AUDIT_RETENTION_DAYS=90
122
- # HELYX_CLOUD_CREDENTIAL_AUDIT_RETENTION_DAYS=365
123
-
124
- # Optional public status reporting. Keep disabled unless this installation has
125
- # its own approved Helyx status credentials.
126
- HELYX_STATUS_ENABLED=false
127
- # HELYX_STATUS_URL=https://status.helyx.gg
128
- # HELYX_STATUS_HEARTBEAT_SECRET=replace-with-a-base64-encoded-32-byte-key
129
- # HELYX_STATUS_HEARTBEAT_INTERVAL_SECONDS=60
130
- # CF_ACCESS_CLIENT_ID=replace-with-access-client-id
131
- # CF_ACCESS_CLIENT_SECRET=replace-with-access-client-secret
132
-
133
88
  # Runtime mode
134
89
  NODE_ENV=production
@@ -95,6 +95,60 @@ features continue if the hosted dashboard is temporarily unavailable.
95
95
  Set `HELYX_CLOUD_CONNECT_ENABLED=false` before startup when the installation
96
96
  must remain entirely local.
97
97
 
98
+ ## Nexus command names (upcoming)
99
+
100
+ Nexus Rewards and Nexus Economy keep `/nexus` as their default command in this
101
+ guide. Custom command naming is an upcoming feature, held until the current name
102
+ policy is configured and controlled acceptance is complete. Having a compatible
103
+ package or enabling Cloud Connect does not activate it.
104
+
105
+ The shared Nexus settings own one command name per Discord server. When offered
106
+ by an approved release, the same **Slash command name** will appear under either
107
+ **Modules > Nexus Rewards > Settings** or **Modules > Nexus Economy > Settings**.
108
+ Changing currency terminology does not change that command. A new root uses
109
+ 1-10 lowercase ASCII letters, digits, hyphens or underscores, excluding `/`, and
110
+ must pass the current restricted-word and full command-reservation checks.
111
+ Reservations include commands from disabled modules.
112
+
113
+ In a supporting Self-Hosted release, Cloud Connect must verify the current
114
+ centrally managed name policy before a new custom name can be accepted. An
115
+ unpaired, disconnected or unsupported connection holds custom-name changes;
116
+ reconnection checks the latest policy before making them available again. A
117
+ previously stored list alone does not establish current verification. Existing
118
+ confirmed commands and ordinary Nexus settings remain usable. **Restore nexus**
119
+ can stage the default name while custom-name editing is held, subject to the
120
+ same confirmation, cooldown and unresolved-update checks described below.
121
+
122
+ A changed-name save requires **Confirm** or **Cancel**. Cancel, Escape or closing
123
+ the dialog restores only the root draft without saving the other fields.
124
+ Confirmation warns about the rolling 24-hour restriction, including a return to
125
+ `nexus`, and Discord command permission overrides which must be reapplied after
126
+ the update. Invalid/rejected requests create no future rename queue. Ordinary
127
+ settings saves, unchanged names and delivery retries do not extend the timer.
128
+
129
+ An accepted setting and a confirmed Discord command are different states. While
130
+ an update is pending or failed, use the previously confirmed command in Discord;
131
+ another customer rename remains blocked until the update is resolved, even after
132
+ the timer expires. If no Nexus commands are enabled, a selected name can remain
133
+ saved without an active slash command. Once a custom name is confirmed, replace
134
+ only `/nexus` in examples: a confirmed `coins` root makes `/nexus balance` become
135
+ `/coins balance`, with the same subcommands and access rules.
136
+
137
+ A new reserved command or restricted-list update can select `nexus` again without
138
+ extending the existing expiry or changing unrelated settings. Check the reset's
139
+ pending/confirmed state and reapply Discord command permission overrides after
140
+ the update.
141
+
142
+ Private command-history inspection, timer reset and rename override by Helyx staff
143
+ are a Hosted-only support path. Remote Self-Hosted and Cloud-connected
144
+ installations do not have that recovery capability. Keep custom-name activation
145
+ held when the required policy or recovery path is unavailable; do not bypass it
146
+ by changing stored records or registering a competing command manually. Check
147
+ your release, `npm run doctor`, module health and the current Discord command
148
+ picker, then use the supported [privacy and support routes](privacy-and-support.md)
149
+ without sharing credentials. Hosted dashboard workflow details are in the
150
+ [shared Nexus Settings guide](https://docs.helyx.gg/modules/nexus-rewards/settings/).
151
+
98
152
  ## Before production use
99
153
 
100
154
  1. Prove `npm run doctor` from the production environment.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/bot",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "Production Discord runtime for the modular Helyx platform.",
5
5
  "keywords": [
6
6
  "helyx",
@@ -30,6 +30,7 @@
30
30
  "type": "module",
31
31
  "files": [
32
32
  "dist",
33
+ "command-root-catalogue.json",
33
34
  "!dist/*.tsbuildinfo",
34
35
  "!dist/**/*.map",
35
36
  "documentation",
@@ -50,31 +51,31 @@
50
51
  "start": "node --env-file-if-exists=../../.env dist/index.js"
51
52
  },
52
53
  "dependencies": {
53
- "@helyx/config": ">=1.4.0 <2.0.0",
54
- "@helyx/control-protocol": ">=0.8.0 <0.9.0",
54
+ "@helyx/config": ">=1.6.0 <2.0.0",
55
+ "@helyx/control-protocol": ">=0.9.0 <0.10.0",
55
56
  "@helyx/core": ">=0.2.0 <0.3.0",
56
- "@helyx/database": ">=2.4.1 <3.0.0",
57
- "@helyx/database-contracts": ">=0.2.0 <0.3.0",
57
+ "@helyx/database": ">=2.5.0 <3.0.0",
58
+ "@helyx/database-contracts": ">=0.3.0 <0.4.0",
58
59
  "@helyx/framework": ">=0.2.0 <0.3.0",
59
- "@helyx/module-manifest": ">=0.13.0 <0.14.0",
60
+ "@helyx/module-manifest": ">=0.14.0 <0.15.0",
60
61
  "@helyx/platform": ">=1.3.0 <2.0.0",
61
- "@helyx/sdk": ">=0.16.0 <0.17.0",
62
+ "@helyx/sdk": ">=0.17.0 <0.18.0",
62
63
  "@helyx/status-client": ">=0.3.0 <0.4.0",
63
- "@helyx/verification-contracts": ">=0.4.0 <0.5.0",
64
+ "@helyx/verification-contracts": ">=0.5.0 <0.6.0",
64
65
  "discord.js": "14.27.0",
65
66
  "fastify": "5.12.7",
66
- "pino": "10.3.1",
67
- "redis": "6.1.0",
68
- "undici": "6.29.0",
69
- "ws": "8.21.1",
70
- "zod": "4.4.3"
67
+ "pino": "10.4.0",
68
+ "redis": "6.3.0",
69
+ "undici": "6.29.1",
70
+ "ws": "8.22.0",
71
+ "zod": "4.6.7"
71
72
  },
72
73
  "optionalDependencies": {
73
- "@helyx/database-mysql": ">=0.2.1 <0.3.0"
74
+ "@helyx/database-mysql": ">=0.3.0 <0.4.0"
74
75
  },
75
76
  "peerDependencies": {
76
- "@helyx/module-automod": ">=1.1.1 <2.0.0",
77
- "@helyx/module-moderation": ">=1.1.1 <2.0.0"
77
+ "@helyx/module-automod": ">=1.1.2 <2.0.0",
78
+ "@helyx/module-moderation": ">=1.1.2 <2.0.0"
78
79
  },
79
80
  "peerDependenciesMeta": {
80
81
  "@helyx/module-automod": {
@@ -85,6 +86,6 @@
85
86
  }
86
87
  },
87
88
  "devDependencies": {
88
- "@types/ws": "8.18.1"
89
+ "@types/ws": "8.18.2"
89
90
  }
90
91
  }