@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.
- package/CHANGELOG.md +25 -0
- package/command-root-catalogue.json +452 -0
- package/dist/cached-message-deletion-authority.d.ts +27 -0
- package/dist/cached-message-deletion-authority.js +288 -0
- package/dist/channel-audit-configuration.d.ts +11 -1
- package/dist/channel-audit-configuration.js +28 -6
- package/dist/channel-audit-control.js +42 -2
- package/dist/channel-audit-permissions.js +76 -3
- package/dist/cloud-command-policy-sync.d.ts +26 -0
- package/dist/cloud-command-policy-sync.js +198 -0
- package/dist/cloud-connector.d.ts +6 -1
- package/dist/cloud-connector.js +224 -53
- package/dist/cloud-control-dispatcher.js +18 -0
- package/dist/command-policy-runtime.d.ts +23 -0
- package/dist/command-policy-runtime.js +53 -0
- package/dist/command-root-catalogue.d.ts +5 -0
- package/dist/command-root-catalogue.js +16 -0
- package/dist/control-error-mapping.d.ts +8 -0
- package/dist/control-error-mapping.js +60 -0
- package/dist/control-errors.d.ts +3 -1
- package/dist/control-errors.js +4 -1
- package/dist/control-health-routes.d.ts +10 -0
- package/dist/control-health-routes.js +28 -0
- package/dist/control-server-schemas.d.ts +7 -0
- package/dist/control-server-schemas.js +9 -0
- package/dist/control-server.d.ts +27 -6
- package/dist/control-server.js +103 -122
- package/dist/creator-invite-key.d.ts +6 -0
- package/dist/creator-invite-key.js +16 -0
- package/dist/creator-invite-service.d.ts +3 -1
- package/dist/creator-invite-service.js +8 -7
- package/dist/creator-onboarding-platform.d.ts +1 -0
- package/dist/creator-onboarding-platform.js +5 -2
- package/dist/dashboard-feature-control.d.ts +2 -1
- package/dist/dashboard-feature-control.js +1 -0
- package/dist/dashboard-logging.d.ts +20 -0
- package/dist/dashboard-logging.js +39 -0
- package/dist/dashboard-module-projection.d.ts +1 -1
- package/dist/dashboard-module-projection.js +5 -2
- package/dist/dashboard-preview-import-routes.js +8 -0
- package/dist/database-bootstrap.d.ts +1 -1
- package/dist/database-bootstrap.js +4 -3
- package/dist/discord-channel-audit.js +1 -0
- package/dist/discord-user-identities.d.ts +14 -0
- package/dist/discord-user-identities.js +64 -0
- package/dist/emergency-deletion-dispatcher.d.ts +68 -0
- package/dist/emergency-deletion-dispatcher.js +502 -0
- package/dist/emergency-deletion-rest-transport.d.ts +24 -0
- package/dist/emergency-deletion-rest-transport.js +253 -0
- package/dist/events.d.ts +5 -0
- package/dist/events.js +53 -10
- package/dist/guarded-discord-dispatcher.d.ts +29 -0
- package/dist/guarded-discord-dispatcher.js +204 -0
- package/dist/guild-command-discord-client.d.ts +2 -0
- package/dist/guild-command-discord-client.js +7 -0
- package/dist/guild-command-projection.d.ts +4 -1
- package/dist/guild-command-projection.js +25 -0
- package/dist/guild-command-read-service.d.ts +10 -1
- package/dist/guild-command-read-service.js +37 -0
- package/dist/guild-command-worker.d.ts +19 -4
- package/dist/guild-command-worker.js +356 -26
- package/dist/guild-message-protection-author-roles.d.ts +7 -0
- package/dist/guild-message-protection-author-roles.js +93 -0
- package/dist/guild-message-protection-deletion.d.ts +21 -0
- package/dist/guild-message-protection-deletion.js +138 -0
- package/dist/guild-message-protection-delivery.d.ts +41 -0
- package/dist/guild-message-protection-delivery.js +223 -0
- package/dist/guild-message-update-metadata.d.ts +32 -0
- package/dist/guild-message-update-metadata.js +255 -0
- package/dist/guilds.d.ts +2 -1
- package/dist/guilds.js +11 -1
- package/dist/hosted-server-moderation.d.ts +2 -2
- package/dist/index.js +68 -4
- package/dist/interactions.d.ts +3 -1
- package/dist/interactions.js +77 -12
- package/dist/invite-rollup-maintenance.d.ts +9 -0
- package/dist/invite-rollup-maintenance.js +12 -0
- package/dist/managed-resource-authorization.d.ts +15 -0
- package/dist/managed-resource-authorization.js +37 -0
- package/dist/managed-resource-create-defaults.d.ts +21 -0
- package/dist/managed-resource-create-defaults.js +83 -0
- package/dist/managed-resource-query.d.ts +6 -0
- package/dist/managed-resource-query.js +15 -0
- package/dist/member-role-ids.d.ts +17 -0
- package/dist/member-role-ids.js +66 -0
- package/dist/message-protection-admission.d.ts +60 -0
- package/dist/message-protection-admission.js +279 -0
- package/dist/message-protection-cold-data.d.ts +42 -0
- package/dist/message-protection-cold-data.js +180 -0
- package/dist/message-protection-coordinator.d.ts +73 -0
- package/dist/message-protection-coordinator.js +622 -0
- package/dist/message-protection-settings.d.ts +40 -0
- package/dist/message-protection-settings.js +275 -0
- package/dist/message-protection-summary.d.ts +8 -0
- package/dist/message-protection-summary.js +98 -0
- package/dist/module-availability-control.js +20 -12
- package/dist/module-logging.d.ts +3 -2
- package/dist/module-logging.js +6 -1
- package/dist/modules.js +56 -1
- package/dist/mysql-runtime-persistence.d.ts +1 -0
- package/dist/mysql-runtime-persistence.js +4 -1
- package/dist/nexus-command-recovery-control-routes.d.ts +5 -0
- package/dist/nexus-command-recovery-control-routes.js +48 -0
- package/dist/nexus-command-references.d.ts +6 -0
- package/dist/nexus-command-references.js +33 -0
- package/dist/scheduled-task-runner.js +23 -4
- package/dist/shard-command-startup.d.ts +13 -0
- package/dist/shard-command-startup.js +27 -0
- package/dist/shard-contracts.d.ts +84 -0
- package/dist/shard-contracts.js +33 -0
- package/dist/shard-core-readiness.d.ts +5 -0
- package/dist/shard-core-readiness.js +19 -0
- package/dist/shard-router.d.ts +3 -0
- package/dist/shard-router.js +10 -1
- package/dist/shard-supervisor.d.ts +1 -1
- package/dist/shard-supervisor.js +131 -43
- package/dist/shard-worker.d.ts +1 -0
- package/dist/shard-worker.js +37 -15
- package/documentation/README.md +1 -0
- package/documentation/guides/self-hosting.md +54 -0
- package/package.json +18 -17
package/dist/shard-supervisor.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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 (
|
|
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
|
-
|
|
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)))
|
package/dist/shard-worker.d.ts
CHANGED
|
@@ -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;
|
package/dist/shard-worker.js
CHANGED
|
@@ -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,
|
|
389
|
-
...(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
}
|
package/documentation/README.md
CHANGED
|
@@ -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.
|
|
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.
|
|
54
|
-
"@helyx/control-protocol": ">=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.
|
|
57
|
-
"@helyx/database-contracts": ">=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.
|
|
60
|
+
"@helyx/module-manifest": ">=0.14.0 <0.15.0",
|
|
60
61
|
"@helyx/platform": ">=1.3.0 <2.0.0",
|
|
61
|
-
"@helyx/sdk": ">=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.
|
|
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.
|
|
67
|
-
"redis": "6.
|
|
68
|
-
"undici": "6.29.
|
|
69
|
-
"ws": "8.
|
|
70
|
-
"zod": "4.
|
|
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.
|
|
74
|
+
"@helyx/database-mysql": ">=0.3.0 <0.4.0"
|
|
74
75
|
},
|
|
75
76
|
"peerDependencies": {
|
|
76
|
-
"@helyx/module-automod": ">=1.1.
|
|
77
|
-
"@helyx/module-moderation": ">=1.1.
|
|
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.
|
|
89
|
+
"@types/ws": "8.18.2"
|
|
89
90
|
}
|
|
90
91
|
}
|