@helyx/bot 1.5.1 → 1.5.3

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 (121) hide show
  1. package/CHANGELOG.md +25 -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/creator-invite-key.d.ts +6 -0
  29. package/dist/creator-invite-key.js +16 -0
  30. package/dist/creator-invite-service.d.ts +3 -1
  31. package/dist/creator-invite-service.js +8 -7
  32. package/dist/creator-onboarding-platform.d.ts +1 -0
  33. package/dist/creator-onboarding-platform.js +5 -2
  34. package/dist/dashboard-feature-control.d.ts +2 -1
  35. package/dist/dashboard-feature-control.js +1 -0
  36. package/dist/dashboard-logging.d.ts +20 -0
  37. package/dist/dashboard-logging.js +39 -0
  38. package/dist/dashboard-module-projection.d.ts +1 -1
  39. package/dist/dashboard-module-projection.js +5 -2
  40. package/dist/dashboard-preview-import-routes.js +8 -0
  41. package/dist/database-bootstrap.d.ts +1 -1
  42. package/dist/database-bootstrap.js +4 -3
  43. package/dist/discord-channel-audit.js +1 -0
  44. package/dist/discord-user-identities.d.ts +14 -0
  45. package/dist/discord-user-identities.js +64 -0
  46. package/dist/emergency-deletion-dispatcher.d.ts +68 -0
  47. package/dist/emergency-deletion-dispatcher.js +502 -0
  48. package/dist/emergency-deletion-rest-transport.d.ts +24 -0
  49. package/dist/emergency-deletion-rest-transport.js +253 -0
  50. package/dist/events.d.ts +5 -0
  51. package/dist/events.js +53 -10
  52. package/dist/guarded-discord-dispatcher.d.ts +29 -0
  53. package/dist/guarded-discord-dispatcher.js +204 -0
  54. package/dist/guild-command-discord-client.d.ts +2 -0
  55. package/dist/guild-command-discord-client.js +7 -0
  56. package/dist/guild-command-projection.d.ts +4 -1
  57. package/dist/guild-command-projection.js +25 -0
  58. package/dist/guild-command-read-service.d.ts +10 -1
  59. package/dist/guild-command-read-service.js +37 -0
  60. package/dist/guild-command-worker.d.ts +19 -4
  61. package/dist/guild-command-worker.js +356 -26
  62. package/dist/guild-message-protection-author-roles.d.ts +7 -0
  63. package/dist/guild-message-protection-author-roles.js +93 -0
  64. package/dist/guild-message-protection-deletion.d.ts +21 -0
  65. package/dist/guild-message-protection-deletion.js +138 -0
  66. package/dist/guild-message-protection-delivery.d.ts +41 -0
  67. package/dist/guild-message-protection-delivery.js +223 -0
  68. package/dist/guild-message-update-metadata.d.ts +32 -0
  69. package/dist/guild-message-update-metadata.js +255 -0
  70. package/dist/guilds.d.ts +2 -1
  71. package/dist/guilds.js +11 -1
  72. package/dist/hosted-server-moderation.d.ts +2 -2
  73. package/dist/index.js +68 -4
  74. package/dist/interactions.d.ts +3 -1
  75. package/dist/interactions.js +77 -12
  76. package/dist/invite-rollup-maintenance.d.ts +9 -0
  77. package/dist/invite-rollup-maintenance.js +12 -0
  78. package/dist/managed-resource-authorization.d.ts +15 -0
  79. package/dist/managed-resource-authorization.js +37 -0
  80. package/dist/managed-resource-create-defaults.d.ts +21 -0
  81. package/dist/managed-resource-create-defaults.js +83 -0
  82. package/dist/managed-resource-query.d.ts +6 -0
  83. package/dist/managed-resource-query.js +15 -0
  84. package/dist/member-role-ids.d.ts +17 -0
  85. package/dist/member-role-ids.js +66 -0
  86. package/dist/message-protection-admission.d.ts +60 -0
  87. package/dist/message-protection-admission.js +279 -0
  88. package/dist/message-protection-cold-data.d.ts +42 -0
  89. package/dist/message-protection-cold-data.js +180 -0
  90. package/dist/message-protection-coordinator.d.ts +73 -0
  91. package/dist/message-protection-coordinator.js +622 -0
  92. package/dist/message-protection-settings.d.ts +40 -0
  93. package/dist/message-protection-settings.js +275 -0
  94. package/dist/message-protection-summary.d.ts +8 -0
  95. package/dist/message-protection-summary.js +98 -0
  96. package/dist/module-availability-control.js +20 -12
  97. package/dist/module-logging.d.ts +3 -2
  98. package/dist/module-logging.js +6 -1
  99. package/dist/modules.js +56 -1
  100. package/dist/mysql-runtime-persistence.d.ts +1 -0
  101. package/dist/mysql-runtime-persistence.js +4 -1
  102. package/dist/nexus-command-recovery-control-routes.d.ts +5 -0
  103. package/dist/nexus-command-recovery-control-routes.js +48 -0
  104. package/dist/nexus-command-references.d.ts +6 -0
  105. package/dist/nexus-command-references.js +33 -0
  106. package/dist/scheduled-task-runner.js +23 -4
  107. package/dist/shard-command-startup.d.ts +13 -0
  108. package/dist/shard-command-startup.js +27 -0
  109. package/dist/shard-contracts.d.ts +84 -0
  110. package/dist/shard-contracts.js +33 -0
  111. package/dist/shard-core-readiness.d.ts +5 -0
  112. package/dist/shard-core-readiness.js +19 -0
  113. package/dist/shard-router.d.ts +3 -0
  114. package/dist/shard-router.js +10 -1
  115. package/dist/shard-supervisor.d.ts +1 -1
  116. package/dist/shard-supervisor.js +131 -43
  117. package/dist/shard-worker.d.ts +1 -0
  118. package/dist/shard-worker.js +37 -15
  119. package/documentation/README.md +1 -0
  120. package/documentation/guides/self-hosting.md +54 -0
  121. package/package.json +18 -17
@@ -1,18 +1,23 @@
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";
20
+ import { creatorInviteWorkerKeyName, deriveCreatorInviteKey, } from "./creator-invite-key.js";
16
21
  import { createDiagnosticModule } from "./diagnostic-command.js";
17
22
  import { buildApplicationCommandDefinitions } from "./interactions.js";
18
23
  import { resolveModulePackageSpecifiers } from "./module-configuration.js";
@@ -130,6 +135,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
130
135
  let numericRegistrations = [];
131
136
  let leaderLease;
132
137
  let cloudConnector;
138
+ let commandPolicyRuntime;
133
139
  let cloudStarted = false;
134
140
  let maintainingLeases = false;
135
141
  let rebalancingShards = false;
@@ -163,6 +169,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
163
169
  let guildCommandReleasePromise;
164
170
  let scheduledModules = [];
165
171
  let commandModules = [];
172
+ let commandCatalogue;
166
173
  let workersRoutable = false;
167
174
  let promoteToLeader = () => Promise.resolve(false);
168
175
  try {
@@ -172,6 +179,15 @@ export async function startShardSupervisor(environment, processEnvironment, work
172
179
  const modulePackages = await loadConfiguredModulePackages(moduleSpecifiers);
173
180
  const featureModules = modulePackages.map(({ definition }) => definition);
174
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;
175
191
  const gateway = await fetchGatewayBotInformation(environment.DISCORD_TOKEN);
176
192
  shardCount =
177
193
  environment.HELYX_SHARD_MODE === "fixed"
@@ -300,11 +316,57 @@ export async function startShardSupervisor(environment, processEnvironment, work
300
316
  listAvailabilityModules: () => availabilityCatalogue(availabilityModules, statusModules, runtimeAvailability),
301
317
  };
302
318
  const initialiseLeader = async () => {
303
- 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
+ });
304
361
  assertStartupLeaseHealthy();
305
362
  privacyDatabase = await createPrivacyDatabase(environment, database);
306
363
  assertStartupLeaseHealthy();
307
- 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
+ }));
308
370
  runtimeAvailability = services.get(HELYX_SERVICE_NAMES.moduleAvailability);
309
371
  leaderScannerRegistry = new BotAutoModerationScannerRegistry(publicModules, new ServiceRegistry(services), "hosted", undefined, environment.HELYX_DEPLOYMENT_ENVIRONMENT);
310
372
  if (numericWriteBehind)
@@ -349,9 +411,9 @@ export async function startShardSupervisor(environment, processEnvironment, work
349
411
  environment: environment.NODE_ENV,
350
412
  generation,
351
413
  shardCount,
352
- 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),
353
415
  };
354
- deploymentOrder = await nextShardDeploymentOrder(database);
416
+ deploymentOrder = await prepareShardDeploymentOrder(database, environment.HELYX_DATABASE_STARTUP_MODE);
355
417
  if (environment.HELYX_SHARD_SCOPE === "coordinated") {
356
418
  coordinator = new RedisShardCoordinator(environment.HELYX_SHARD_REDIS_URL, environment.HELYX_SHARD_REDIS_NAMESPACE, {
357
419
  onError: (error) => logger.error({ event: "shard.redis_error", error: safeError(error) }, "Shard coordination failed"),
@@ -416,7 +478,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
416
478
  await initialiseLeader();
417
479
  }
418
480
  else {
419
- await waitForLeaderStartup(database, generation);
481
+ await waitForLeaderStartup(database, generation, catalogueIdentity);
420
482
  assertStartupLeaseHealthy();
421
483
  }
422
484
  const router = new ShardControlRouter({
@@ -483,6 +545,15 @@ export async function startShardSupervisor(environment, processEnvironment, work
483
545
  : undefined;
484
546
  controlServer = await createBotControlServer({
485
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
+ : {}),
486
557
  secret: environment.HELYX_BOT_CONTROL_SECRET,
487
558
  ...shardHostedControlCallbacks(router),
488
559
  logger: { level: environment.LOG_LEVEL },
@@ -1217,6 +1288,15 @@ export async function startShardSupervisor(environment, processEnvironment, work
1217
1288
  }
1218
1289
  if (!guildCommandWorker) {
1219
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
+ : {}),
1220
1300
  database,
1221
1301
  modules: commandModules,
1222
1302
  logger,
@@ -1539,7 +1619,10 @@ export function splitNumericCommittedBatch(batch, index) {
1539
1619
  results: [result],
1540
1620
  };
1541
1621
  }
1542
- 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();
1543
1626
  const moduleMigrationSources = modulePackages.map(({ definition, packageRoot }) => ({
1544
1627
  moduleId: definition.manifest.id,
1545
1628
  moduleVersion: definition.manifest.version,
@@ -1565,23 +1648,36 @@ async function runLeaderStartup(database, environment, modulePackages, modules,
1565
1648
  await runCoreMigrations(database);
1566
1649
  await runConfiguredModuleMigrations(database, moduleMigrationSources);
1567
1650
  }
1651
+ await commandStartup.prepareCommandPolicy();
1568
1652
  await enforcePremiumCatalogue(database, logger);
1569
1653
  const repository = new RuntimeStateRepository(database);
1570
- await synchroniseApplicationCommands({
1571
- applicationId: environment.DISCORD_CLIENT_ID,
1572
- token: environment.DISCORD_TOKEN,
1573
- modules,
1574
- globalCommandMode: environment.HELYX_GLOBAL_COMMAND_MODE,
1575
- ...(environment.DISCORD_INTERNAL_GUILD_ID
1576
- ? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
1577
- : {}),
1578
- // Worker-owned guild collections stay untouched while command writes are off.
1579
- synchroniseInternalGuild: false,
1580
- state: new RuntimeCommandHashStore(repository),
1581
- });
1582
- await setRuntimeState(repository, `shards:${environment.HELYX_SHARD_GENERATION ?? "local"}:startup`, {
1583
- ready: true,
1584
- 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
+ }),
1585
1681
  });
1586
1682
  logger.info({ event: "shard.singleton_startup_complete" }, "Shard singleton startup complete");
1587
1683
  }
@@ -1618,15 +1714,12 @@ async function setRuntimeState(repository, key, value) {
1618
1714
  if (!updated)
1619
1715
  throw new Error("Runtime state changed concurrently");
1620
1716
  }
1621
- async function waitForLeaderStartup(database, generation) {
1717
+ async function waitForLeaderStartup(database, generation, catalogue) {
1622
1718
  const deadline = Date.now() + STARTUP_WAIT_MS;
1623
1719
  while (Date.now() < deadline) {
1624
1720
  try {
1625
1721
  const state = await new RuntimeStateRepository(database).get(`shards:${generation}:startup`);
1626
- if (typeof state?.value === "object" &&
1627
- state.value !== null &&
1628
- !Array.isArray(state.value) &&
1629
- state.value.ready === true)
1722
+ if (shardStartupCatalogueReady(state?.value, catalogue))
1630
1723
  return;
1631
1724
  }
1632
1725
  catch {
@@ -1716,6 +1809,9 @@ export function buildShardWorkerEnvironment(source, identity, restartCount) {
1716
1809
  const selectionKey = source.HELYX_DATA_ENCRYPTION_KEY
1717
1810
  ? Buffer.from(deriveSecureRandomSelectionKey(source.HELYX_DATA_ENCRYPTION_KEY)).toString("base64")
1718
1811
  : undefined;
1812
+ const creatorInviteKey = source.HELYX_DATA_ENCRYPTION_KEY
1813
+ ? deriveCreatorInviteKey(source.HELYX_DATA_ENCRYPTION_KEY).toString("base64")
1814
+ : undefined;
1719
1815
  const hostedModerationCredential = source.HELYX_HOSTED_MODERATION_ENABLED === "true" &&
1720
1816
  source.HELYX_BOT_CONTROL_SECRET
1721
1817
  ? { HELYX_BOT_CONTROL_SECRET: source.HELYX_BOT_CONTROL_SECRET }
@@ -1726,6 +1822,9 @@ export function buildShardWorkerEnvironment(source, identity, restartCount) {
1726
1822
  ...(selectionKey
1727
1823
  ? { [secureRandomSelectionWorkerKeyName]: selectionKey }
1728
1824
  : {}),
1825
+ ...(creatorInviteKey
1826
+ ? { [creatorInviteWorkerKeyName]: creatorInviteKey }
1827
+ : {}),
1729
1828
  SHARDING_MANAGER: "true",
1730
1829
  SHARDING_MANAGER_MODE: "process",
1731
1830
  SHARDS: String(identity.shardId),
@@ -1734,11 +1833,12 @@ export function buildShardWorkerEnvironment(source, identity, restartCount) {
1734
1833
  HELYX_SHARD_RESTART_COUNT: String(restartCount),
1735
1834
  };
1736
1835
  }
1737
- export function shardRuntimeCompositionHash(modules, internalGuildId, globalCommandMode = "legacy") {
1836
+ export function shardRuntimeCompositionHash(modules, internalGuildId, globalCommandMode = "legacy", commandCatalogueFingerprint) {
1738
1837
  const commands = buildApplicationCommandDefinitions(modules, internalGuildId, globalCommandMode);
1739
1838
  return createHash("sha256")
1740
1839
  .update(JSON.stringify({
1741
1840
  botVersion: botVersion(),
1841
+ ...(commandCatalogueFingerprint ? { commandCatalogueFingerprint } : {}),
1742
1842
  modules: modules
1743
1843
  .map(({ manifest }) => ({
1744
1844
  id: manifest.id,
@@ -1983,19 +2083,7 @@ function sameShardLease(left, right) {
1983
2083
  left.fencingToken === right.fencingToken);
1984
2084
  }
1985
2085
  export async function nextShardDeploymentOrder(database) {
1986
- let value;
1987
- try {
1988
- value = await new RuntimeStateRepository(database).nextShardDeploymentOrder();
1989
- }
1990
- catch (error) {
1991
- if (error.code !== "42P01")
1992
- throw error;
1993
- return 1;
1994
- }
1995
- if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
1996
- throw new Error("Shard deployment order exceeds the safe runtime range");
1997
- }
1998
- return Number(value);
2086
+ return allocateShardDeploymentOrder(database);
1999
2087
  }
2000
2088
  export async function renewStartupOwnership(coordinator, descriptor, deploymentOrder, leaderLease) {
2001
2089
  if (!(await coordinator.refreshGeneration(descriptor, deploymentOrder)))
@@ -23,6 +23,7 @@ export declare function startShardWorker(processEnvironment?: NodeJS.ProcessEnv,
23
23
  export declare function executeShardScheduledTaskRequest(identity: ShardIdentity, request: ShardScheduledTaskRequest, route: ScheduledTaskRoute, now?: number): Promise<ShardScheduledTaskResponse>;
24
24
  export declare function parseShardWorkerLaunchContract(value: string | undefined): ShardIdentity;
25
25
  export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.ProcessEnv, identity: ShardIdentity): {
26
+ HELYX_INTERNAL_CREATOR_INVITE_KEY?: string;
26
27
  HELYX_INTERNAL_SECURE_RANDOM_SELECTION_KEY?: string;
27
28
  HELYX_DATA_ENCRYPTION_KEY: undefined;
28
29
  HELYX_SHARD_WORKERS_PER_REPLICA: number;
@@ -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";
@@ -15,6 +16,7 @@ import { HttpVerificationService } from "./verification-service.js";
15
16
  import { DiscordMessageDeliveryService, DiscordMemberRoleService, DiscordVerificationMemberService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
16
17
  import { DiscordModerationMemberEnforcementAdapter } from "./discord-member-enforcement-service.js";
17
18
  import { installCreatorInvitePlatform } from "./creator-onboarding-platform.js";
19
+ import { creatorInviteWorkerKeyName, decodeCreatorInviteKey, } from "./creator-invite-key.js";
18
20
  import { installGuildTracking, } from "./guilds.js";
19
21
  import { installInteractions, } from "./interactions.js";
20
22
  import { createManagementModule } from "./management-module.js";
@@ -263,6 +265,11 @@ export async function startShardWorker(processEnvironment = process.env, identit
263
265
  const serviceOptions = platformServiceOptions(environment, publicModules);
264
266
  for (const [name, service] of createPlatformServices(database, publicModules, {
265
267
  ...serviceOptions,
268
+ ...(environment.HELYX_RUNTIME_PROFILE === "self-hosted"
269
+ ? {
270
+ commandPolicyDeliveryScope: shardCommandPolicyScope(identity.generation, identity.deploymentOrder),
271
+ }
272
+ : {}),
266
273
  recordRecoveryOwnership: {
267
274
  mode: "shard",
268
275
  shardId: identity.shardId,
@@ -318,6 +325,11 @@ export async function startShardWorker(processEnvironment = process.env, identit
318
325
  services: serviceAccess,
319
326
  modules: publicModules,
320
327
  encryptionKey: environment.HELYX_DATA_ENCRYPTION_KEY,
328
+ ...(environment.HELYX_INTERNAL_CREATOR_INVITE_KEY
329
+ ? {
330
+ inviteEncryptionKey: environment.HELYX_INTERNAL_CREATOR_INVITE_KEY,
331
+ }
332
+ : {}),
321
333
  ...(environment.HELYX_CLOUD_API_URL &&
322
334
  environment.HELYX_OAUTH_INTERNAL_SECRET
323
335
  ? {
@@ -385,21 +397,24 @@ export async function startShardWorker(processEnvironment = process.env, identit
385
397
  shardCount: identity.shardCount,
386
398
  })
387
399
  : undefined;
388
- const commandsReady = installInteractions(client, modules, shardWorkerCommandRegistration({
389
- ...(environment.DISCORD_INTERNAL_GUILD_ID
390
- ? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
391
- : {}),
392
- prepare: prepareGuilds,
393
- ...(operationsModule
394
- ? { operationsModuleId: operationsModule.manifest.id }
395
- : {}),
396
- supportModuleId: supportModule.manifest.id,
397
- diagnosticModuleId: diagnosticModule.manifest.id,
398
- ...(premiumActivationModule
399
- ? { premiumActivationModuleId: premiumActivationModule.manifest.id }
400
- : {}),
401
- ...(oauthModule ? { oauthModuleId: oauthModule.manifest.id } : {}),
402
- }), 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);
403
418
  moduleEvents = installModuleEvents(client, publicModules, logger, serviceAccess, {
404
419
  runtimeProfile: environment.HELYX_RUNTIME_PROFILE,
405
420
  evidenceStore: moderationPlatformServices.evidenceStore,
@@ -582,12 +597,18 @@ export function parseShardWorkerEnvironment(processEnvironment, identity) {
582
597
  environment.HELYX_SHARD_SCOPE !== identity.scope)
583
598
  throw new Error("Shard worker launch contract conflicts with configuration");
584
599
  const internalSelectionKey = processEnvironment[secureRandomSelectionWorkerKeyName];
600
+ const internalCreatorInviteKey = processEnvironment[creatorInviteWorkerKeyName];
601
+ if (internalCreatorInviteKey !== undefined)
602
+ decodeCreatorInviteKey(internalCreatorInviteKey);
585
603
  return {
586
604
  ...environment,
587
605
  HELYX_DATA_ENCRYPTION_KEY: undefined,
588
606
  ...(internalSelectionKey
589
607
  ? { HELYX_INTERNAL_SECURE_RANDOM_SELECTION_KEY: internalSelectionKey }
590
608
  : {}),
609
+ ...(internalCreatorInviteKey
610
+ ? { HELYX_INTERNAL_CREATOR_INVITE_KEY: internalCreatorInviteKey }
611
+ : {}),
591
612
  };
592
613
  }
593
614
  export function shardWorkerCommandRegistration(input) {
@@ -657,6 +678,7 @@ async function handleControl(identity, service, hostedOwnerObserver, client, hos
657
678
  ok: false,
658
679
  status: mapped?.status ?? 500,
659
680
  message: mapped?.message ?? "The owning shard rejected the request",
681
+ ...(mapped?.fieldError ? { fieldError: mapped.fieldError } : {}),
660
682
  };
661
683
  }
662
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)
@@ -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.1",
3
+ "version": "1.5.3",
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
  }