@helyx/bot 1.3.1 → 1.4.0
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 +77 -28
- package/README.md +65 -11
- package/dist/cloud-connector.d.ts +11 -4
- package/dist/cloud-connector.js +52 -15
- package/dist/cloud-control-dispatcher.d.ts +8 -3
- package/dist/cloud-control-dispatcher.js +62 -4
- package/dist/control-errors.d.ts +4 -0
- package/dist/control-errors.js +8 -0
- package/dist/control-server-schemas.d.ts +84 -0
- package/dist/control-server-schemas.js +101 -0
- package/dist/control-server.d.ts +21 -7
- package/dist/control-server.js +88 -132
- package/dist/core-retention.d.ts +26 -0
- package/dist/core-retention.js +49 -0
- package/dist/creator-guild-join-service.d.ts +25 -0
- package/dist/creator-guild-join-service.js +218 -0
- package/dist/creator-invite-service.d.ts +37 -0
- package/dist/creator-invite-service.js +686 -0
- package/dist/creator-onboarding-control.d.ts +12 -0
- package/dist/creator-onboarding-control.js +65 -0
- package/dist/creator-onboarding-platform.d.ts +20 -0
- package/dist/creator-onboarding-platform.js +58 -0
- package/dist/dashboard-action-control.d.ts +2 -2
- package/dist/dashboard-action-control.js +2 -3
- package/dist/database-bootstrap.d.ts +36 -0
- package/dist/database-bootstrap.js +101 -0
- package/dist/developer-operations-module.d.ts +1 -0
- package/dist/developer-operations-module.js +34 -16
- package/dist/discord-owned-message-service.d.ts +1 -0
- package/dist/discord-owned-message-service.js +3 -0
- package/dist/guild-command-read-service.d.ts +8 -3
- package/dist/guild-command-read-service.js +14 -7
- package/dist/guild-command-release.d.ts +15 -8
- package/dist/guild-command-release.js +10 -3
- package/dist/guild-command-worker.d.ts +17 -4
- package/dist/guild-command-worker.js +17 -6
- package/dist/guilds.d.ts +24 -3
- package/dist/guilds.js +185 -64
- package/dist/hosted-server-moderation.d.ts +2 -2
- package/dist/hosted-server-moderation.js +10 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +200 -82
- package/dist/interactions.js +16 -3
- package/dist/module-configuration.d.ts +1 -1
- package/dist/module-configuration.js +5 -2
- package/dist/module-logging.d.ts +8 -3
- package/dist/module-logging.js +13 -7
- package/dist/modules.d.ts +1 -0
- package/dist/modules.js +8 -1
- package/dist/mysql-runtime-persistence.d.ts +46 -0
- package/dist/mysql-runtime-persistence.js +91 -0
- package/dist/oauth-module.js +2 -2
- package/dist/oauth-service.d.ts +7 -0
- package/dist/oauth-service.js +3 -0
- package/dist/scheduled-task-runner.d.ts +9 -85
- package/dist/scheduled-task-runner.js +1 -1
- package/dist/shard-contracts.d.ts +135 -0
- package/dist/shard-contracts.js +13 -1
- package/dist/shard-router.d.ts +2 -1
- package/dist/shard-router.js +7 -0
- package/dist/shard-supervisor.js +43 -9
- package/dist/shard-worker.d.ts +5 -2
- package/dist/shard-worker.js +41 -5
- package/dist/supervisor-cloud-directory.d.ts +9 -3
- package/dist/supervisor-cloud-directory.js +8 -12
- package/dist/trivia-data-provider.d.ts +11 -0
- package/dist/trivia-data-provider.js +46 -26
- package/dist/verification-service.d.ts +2 -2
- package/dist/verification-service.js +6 -3
- package/documentation/README.md +5 -3
- package/documentation/examples/.env.example +15 -11
- package/documentation/guides/getting-started.md +29 -12
- package/documentation/guides/privacy-and-support.md +6 -5
- package/documentation/guides/self-hosting.md +16 -11
- package/documentation/modules/auto-moderation.md +7 -5
- package/documentation/modules/building-a-module.md +48 -2
- package/documentation/modules/moderation.md +5 -5
- package/documentation/modules/publishing-a-module.md +6 -4
- package/documentation/modules/testing-a-module.md +39 -4
- package/documentation/reference/environment.md +43 -9
- package/documentation/runbooks/backup-and-recovery.md +26 -7
- package/documentation/runbooks/installation-and-upgrades.md +97 -27
- package/package.json +20 -16
package/dist/shard-supervisor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { safeError, ServiceRegistry } from "@helyx/core";
|
|
2
|
-
import { checkDatabaseConnection, createDatabasePool, AuditRepository, GuildCommandRepository, HostedServerSafetyRepository, InteractionSessionRepository, EckoOperationsRepository, PremiumAllocationRepository, RateLimitRepository, runConfiguredModuleMigrations, runCoreMigrations, RuntimeStateRepository, } from "@helyx/database";
|
|
2
|
+
import { checkDatabaseConnection, createDatabasePool, AuditRepository, GuildCommandRepository, HostedServerSafetyRepository, InstallationRepository, inspectConfiguredModuleMigrations, inspectCoreMigrations, inspectCreatorOnboardingMigrations, runCreatorOnboardingMigrations, InteractionSessionRepository, EckoOperationsRepository, PremiumAllocationRepository, RateLimitRepository, runConfiguredModuleMigrations, runCoreMigrations, RuntimeStateRepository, } from "@helyx/database";
|
|
3
|
+
import { assertRepositoryEngineSupported } from "@helyx/database/adapters";
|
|
3
4
|
import { createPlatformServices, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
|
|
4
5
|
import { AUTOMOD_ENHANCED_CONTRACT_VERSION, AUTOMOD_ENHANCED_PROVIDER_ID, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
5
6
|
import { Client, ShardingManager } from "discord.js";
|
|
@@ -102,6 +103,7 @@ function sameNumericIdentity(left, right) {
|
|
|
102
103
|
left.fencingToken === right.fencingToken);
|
|
103
104
|
}
|
|
104
105
|
export async function startShardSupervisor(environment, processEnvironment, workerEntry) {
|
|
106
|
+
assertRepositoryEngineSupported(environment.DATABASE_ENGINE, ["postgresql"], "Bot shard supervisor");
|
|
105
107
|
if (environment.HELYX_SHARD_MODE === "off")
|
|
106
108
|
throw new Error("Shard supervisor requires sharding to be enabled");
|
|
107
109
|
const shardMode = environment.HELYX_SHARD_MODE;
|
|
@@ -185,7 +187,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
185
187
|
maxConcurrency: gateway.session_start_limit.max_concurrency,
|
|
186
188
|
observedAt: new Date().toISOString(),
|
|
187
189
|
};
|
|
188
|
-
const supervisorDirectory = new SupervisorCloudDiscordDirectory(environment.DISCORD_TOKEN, database, environment.HELYX_OWNER_USER_IDS);
|
|
190
|
+
const supervisorDirectory = new SupervisorCloudDiscordDirectory(environment.DISCORD_TOKEN, new InstallationRepository(database), environment.HELYX_OWNER_USER_IDS);
|
|
189
191
|
const eckoOperations = new EckoOperationsRepository(database);
|
|
190
192
|
cloudConnector = new CloudConnectorController({
|
|
191
193
|
environment,
|
|
@@ -255,6 +257,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
255
257
|
internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID,
|
|
256
258
|
approvedRoleIds: environment.APPROVED_ROLES,
|
|
257
259
|
botVersion: botVersion(),
|
|
260
|
+
databaseEngine: environment.DATABASE_ENGINE,
|
|
258
261
|
...(processEnvironment.RAILWAY_GIT_COMMIT_SHA
|
|
259
262
|
? {
|
|
260
263
|
deploymentReference: processEnvironment.RAILWAY_GIT_COMMIT_SHA.slice(0, 12),
|
|
@@ -282,6 +285,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
282
285
|
...(operationsModule ? [operationsModule] : []),
|
|
283
286
|
];
|
|
284
287
|
commandModules = modules;
|
|
288
|
+
const guildCommandReads = new GuildCommandReadService(database, environment.DISCORD_CLIENT_ID, () => guildCommandWorker?.getHealth(), environment.DISCORD_TOKEN, environment.HELYX_GUILD_COMMAND_WRITES_ENABLED, environment.HELYX_GUILD_COMMAND_WRITE_GUILD_IDS, environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all", commandModules);
|
|
285
289
|
const availabilityModules = availabilityCatalogueDefinitions(modules);
|
|
286
290
|
const statusModules = statusCatalogueDefinitions(featureModules);
|
|
287
291
|
let runtimeAvailability;
|
|
@@ -328,7 +332,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
328
332
|
notificationGuildId: environment.DISCORD_INTERNAL_GUILD_ID,
|
|
329
333
|
}
|
|
330
334
|
: {}),
|
|
331
|
-
}));
|
|
335
|
+
}), guildCommandReads);
|
|
332
336
|
await startLeaderSingletons();
|
|
333
337
|
assertStartupLeaseHealthy();
|
|
334
338
|
};
|
|
@@ -425,7 +429,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
425
429
|
: {}),
|
|
426
430
|
});
|
|
427
431
|
if (!leaderLease)
|
|
428
|
-
cloudConnector.setControlHandler(router);
|
|
432
|
+
cloudConnector.setControlHandler(router, guildCommandReads);
|
|
429
433
|
const claimedIds = await claimInitialShards();
|
|
430
434
|
manager = new ShardingManager(workerEntry, {
|
|
431
435
|
totalShards: shardCount,
|
|
@@ -480,7 +484,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
480
484
|
},
|
|
481
485
|
shardHealth: buildHealth,
|
|
482
486
|
readiness: buildDeploymentReadiness,
|
|
483
|
-
guildCommands:
|
|
487
|
+
guildCommands: guildCommandReads,
|
|
484
488
|
...(ecko ? { ecko } : {}),
|
|
485
489
|
});
|
|
486
490
|
await controlServer.listen({
|
|
@@ -1529,12 +1533,31 @@ export function splitNumericCommittedBatch(batch, index) {
|
|
|
1529
1533
|
};
|
|
1530
1534
|
}
|
|
1531
1535
|
async function runLeaderStartup(database, environment, modulePackages, modules, logger) {
|
|
1532
|
-
|
|
1533
|
-
await runConfiguredModuleMigrations(database, modulePackages.map(({ definition, packageRoot }) => ({
|
|
1536
|
+
const moduleMigrationSources = modulePackages.map(({ definition, packageRoot }) => ({
|
|
1534
1537
|
moduleId: definition.manifest.id,
|
|
1535
1538
|
moduleVersion: definition.manifest.version,
|
|
1536
1539
|
migrationsDirectory: join(packageRoot, "migrations"),
|
|
1537
|
-
}))
|
|
1540
|
+
}));
|
|
1541
|
+
if (environment.HELYX_RUNTIME_PROFILE === "hosted" &&
|
|
1542
|
+
modulePackages.some(({ definition }) => definition.manifest.id === "helyx.creator-connections")) {
|
|
1543
|
+
if (environment.HELYX_DATABASE_STARTUP_MODE === "inspect") {
|
|
1544
|
+
const inspection = await inspectCreatorOnboardingMigrations(database);
|
|
1545
|
+
assertNoPendingLeaderMigrations("hosted creator onboarding", inspection.pending);
|
|
1546
|
+
}
|
|
1547
|
+
else
|
|
1548
|
+
await runCreatorOnboardingMigrations(database);
|
|
1549
|
+
}
|
|
1550
|
+
if (environment.HELYX_DATABASE_STARTUP_MODE === "inspect") {
|
|
1551
|
+
const coreInspection = await inspectCoreMigrations(database);
|
|
1552
|
+
assertNoPendingLeaderMigrations("core", coreInspection.pending);
|
|
1553
|
+
const moduleInspections = await inspectConfiguredModuleMigrations(database, moduleMigrationSources);
|
|
1554
|
+
for (const inspection of moduleInspections)
|
|
1555
|
+
assertNoPendingLeaderMigrations(`module ${inspection.moduleId}`, inspection.pending);
|
|
1556
|
+
}
|
|
1557
|
+
else {
|
|
1558
|
+
await runCoreMigrations(database);
|
|
1559
|
+
await runConfiguredModuleMigrations(database, moduleMigrationSources);
|
|
1560
|
+
}
|
|
1538
1561
|
await enforcePremiumCatalogue(database, logger);
|
|
1539
1562
|
const repository = new RuntimeStateRepository(database);
|
|
1540
1563
|
await synchroniseApplicationCommands({
|
|
@@ -1545,7 +1568,8 @@ async function runLeaderStartup(database, environment, modulePackages, modules,
|
|
|
1545
1568
|
...(environment.DISCORD_INTERNAL_GUILD_ID
|
|
1546
1569
|
? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
|
|
1547
1570
|
: {}),
|
|
1548
|
-
|
|
1571
|
+
// Worker-owned guild collections stay untouched while command writes are off.
|
|
1572
|
+
synchroniseInternalGuild: false,
|
|
1549
1573
|
state: new RuntimeCommandHashStore(repository),
|
|
1550
1574
|
});
|
|
1551
1575
|
await setRuntimeState(repository, `shards:${environment.HELYX_SHARD_GENERATION ?? "local"}:startup`, {
|
|
@@ -1554,6 +1578,10 @@ async function runLeaderStartup(database, environment, modulePackages, modules,
|
|
|
1554
1578
|
});
|
|
1555
1579
|
logger.info({ event: "shard.singleton_startup_complete" }, "Shard singleton startup complete");
|
|
1556
1580
|
}
|
|
1581
|
+
function assertNoPendingLeaderMigrations(scope, pending) {
|
|
1582
|
+
if (pending.length > 0)
|
|
1583
|
+
throw new Error(`Hosted ${scope} migrations must be applied by the dedicated primary migration identity before bot startup: ${pending.join(", ")}`);
|
|
1584
|
+
}
|
|
1557
1585
|
class RuntimeCommandHashStore {
|
|
1558
1586
|
repository;
|
|
1559
1587
|
constructor(repository) {
|
|
@@ -1635,6 +1663,7 @@ async function fetchGatewayBotInformation(token) {
|
|
|
1635
1663
|
}
|
|
1636
1664
|
export function buildShardWorkerEnvironment(source, identity, restartCount) {
|
|
1637
1665
|
const allowed = new Set([
|
|
1666
|
+
"DATABASE_ENGINE",
|
|
1638
1667
|
"DATABASE_URL",
|
|
1639
1668
|
"DATABASE_SSL",
|
|
1640
1669
|
"DISCORD_TOKEN",
|
|
@@ -1680,8 +1709,13 @@ export function buildShardWorkerEnvironment(source, identity, restartCount) {
|
|
|
1680
1709
|
const selectionKey = source.HELYX_DATA_ENCRYPTION_KEY
|
|
1681
1710
|
? Buffer.from(deriveSecureRandomSelectionKey(source.HELYX_DATA_ENCRYPTION_KEY)).toString("base64")
|
|
1682
1711
|
: undefined;
|
|
1712
|
+
const hostedModerationCredential = source.HELYX_HOSTED_MODERATION_ENABLED === "true" &&
|
|
1713
|
+
source.HELYX_BOT_CONTROL_SECRET
|
|
1714
|
+
? { HELYX_BOT_CONTROL_SECRET: source.HELYX_BOT_CONTROL_SECRET }
|
|
1715
|
+
: {};
|
|
1683
1716
|
return {
|
|
1684
1717
|
...environment,
|
|
1718
|
+
...hostedModerationCredential,
|
|
1685
1719
|
...(selectionKey
|
|
1686
1720
|
? { [secureRandomSelectionWorkerKeyName]: selectionKey }
|
|
1687
1721
|
: {}),
|
package/dist/shard-worker.d.ts
CHANGED
|
@@ -28,8 +28,6 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
28
28
|
HELYX_SHARD_WORKERS_PER_REPLICA: number;
|
|
29
29
|
HELYX_SHARD_SUPERVISOR_DATABASE_POOL_MAX: number | undefined;
|
|
30
30
|
HELYX_SHARD_WORKER_DATABASE_POOL_MAX: number | undefined;
|
|
31
|
-
DATABASE_URL: string;
|
|
32
|
-
DATABASE_SSL: "disable" | "require" | "verify-full";
|
|
33
31
|
HELYX_RUNTIME_PROFILE: "hosted" | "self-hosted";
|
|
34
32
|
DISCORD_TOKEN: string;
|
|
35
33
|
DISCORD_CLIENT_ID: string;
|
|
@@ -70,6 +68,10 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
70
68
|
HELYX_CLOUD_CREDENTIAL_AUDIT_RETENTION_DAYS: number;
|
|
71
69
|
LOG_LEVEL: "error" | "fatal" | "warn" | "info" | "debug" | "trace" | "silent";
|
|
72
70
|
NODE_ENV: "development" | "test" | "production";
|
|
71
|
+
DATABASE_ENGINE: "postgresql" | "mysql";
|
|
72
|
+
DATABASE_URL: string;
|
|
73
|
+
DATABASE_SSL: "disable" | "require" | "verify-full";
|
|
74
|
+
HELYX_DATABASE_STARTUP_MODE: "apply" | "inspect";
|
|
73
75
|
HELYX_STATUS_ENABLED: boolean;
|
|
74
76
|
HELYX_STATUS_URL: string;
|
|
75
77
|
HELYX_STATUS_HEARTBEAT_INTERVAL_SECONDS: number;
|
|
@@ -80,6 +82,7 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
80
82
|
HELYX_SUPPORT_SERVICE_SECRET?: string | undefined;
|
|
81
83
|
HELYX_VERIFICATION_INSTALLATION_ID?: string | undefined;
|
|
82
84
|
HELYX_VERIFICATION_URL?: string | undefined;
|
|
85
|
+
HELYX_VERIFICATION_PUBLIC_URL?: string | undefined;
|
|
83
86
|
HELYX_VERIFICATION_INTERNAL_SECRET?: string | undefined;
|
|
84
87
|
HELYX_GUILD_COMMAND_RELEASE_ID?: string | undefined;
|
|
85
88
|
HELYX_PRIVACY_EXECUTOR_DATABASE_URL?: string | undefined;
|
package/dist/shard-worker.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseBotEnvironment } from "@helyx/config";
|
|
2
2
|
import { safeError, ServiceRegistry } from "@helyx/core";
|
|
3
3
|
import { checkDatabaseConnection, createDatabasePool, PremiumAllocationRepository, } from "@helyx/database";
|
|
4
|
+
import { assertRepositoryEngineSupported } from "@helyx/database/adapters";
|
|
4
5
|
import { REVIEWED_PREMIUM_MODULE_IDS } from "@helyx/module-manifest";
|
|
5
6
|
import { createRuntime } from "@helyx/framework";
|
|
6
7
|
import { createPlatformServices, PlatformMemberRoleGrantOwnershipService, PostgresMemberRoleGrantOwnershipStore, } from "@helyx/platform";
|
|
@@ -13,6 +14,7 @@ import { BotControlService, BotControlTransportError, DiscordGuildResourceDirect
|
|
|
13
14
|
import { HttpVerificationService } from "./verification-service.js";
|
|
14
15
|
import { DiscordMessageDeliveryService, DiscordMemberRoleService, DiscordVerificationMemberService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
|
|
15
16
|
import { DiscordModerationMemberEnforcementAdapter } from "./discord-member-enforcement-service.js";
|
|
17
|
+
import { installCreatorInvitePlatform } from "./creator-onboarding-platform.js";
|
|
16
18
|
import { installGuildTracking, } from "./guilds.js";
|
|
17
19
|
import { installInteractions, } from "./interactions.js";
|
|
18
20
|
import { createManagementModule } from "./management-module.js";
|
|
@@ -64,6 +66,7 @@ export function createLeaseHeartbeatWatchdog(onExpired, timeoutMs = SHARD_LEASE_
|
|
|
64
66
|
}
|
|
65
67
|
export async function startShardWorker(processEnvironment = process.env, identity = parseShardWorkerLaunchContract(processEnvironment.HELYX_SHARD_WORKER_CONTRACT)) {
|
|
66
68
|
const environment = parseShardWorkerEnvironment(processEnvironment, identity);
|
|
69
|
+
assertRepositoryEngineSupported(environment.DATABASE_ENGINE, ["postgresql"], "Bot shard worker");
|
|
67
70
|
const logger = pino({
|
|
68
71
|
level: environment.LOG_LEVEL,
|
|
69
72
|
base: {
|
|
@@ -109,14 +112,16 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
109
112
|
let hostedLeaveWorker;
|
|
110
113
|
let leaseHeartbeatWatchdog;
|
|
111
114
|
let forcedLeaseExit;
|
|
115
|
+
let creatorInvitePlatform;
|
|
112
116
|
let numericWriteBehind;
|
|
113
117
|
async function shutdown(reason = "programmatic") {
|
|
114
118
|
if (shuttingDown)
|
|
115
119
|
return;
|
|
116
120
|
shuttingDown = true;
|
|
117
121
|
status = "stopped";
|
|
118
|
-
guildTracking?.stop();
|
|
122
|
+
await guildTracking?.stop();
|
|
119
123
|
await hostedLeaveWorker?.stop();
|
|
124
|
+
await creatorInvitePlatform?.stop();
|
|
120
125
|
if (healthTimer)
|
|
121
126
|
clearInterval(healthTimer);
|
|
122
127
|
leaseHeartbeatWatchdog?.stop();
|
|
@@ -193,6 +198,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
193
198
|
internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID,
|
|
194
199
|
approvedRoleIds: environment.APPROVED_ROLES,
|
|
195
200
|
botVersion: readBotVersion(),
|
|
201
|
+
databaseEngine: environment.DATABASE_ENGINE,
|
|
196
202
|
...(processEnvironment.RAILWAY_GIT_COMMIT_SHA
|
|
197
203
|
? {
|
|
198
204
|
deploymentReference: processEnvironment.RAILWAY_GIT_COMMIT_SHA.slice(0, 12),
|
|
@@ -232,6 +238,15 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
232
238
|
})) {
|
|
233
239
|
platformServices.set(name, service);
|
|
234
240
|
}
|
|
241
|
+
const applicationUrls = serviceAccess.get(HELYX_SERVICE_NAMES.applicationUrls);
|
|
242
|
+
platformServices.set(HELYX_SERVICE_NAMES.applicationUrls, {
|
|
243
|
+
dashboardBaseUrl: applicationUrls.dashboardBaseUrl,
|
|
244
|
+
get dashboardInstallationId() {
|
|
245
|
+
return environment.HELYX_RUNTIME_PROFILE === "self-hosted"
|
|
246
|
+
? cloudProxy?.status().installationId
|
|
247
|
+
: undefined;
|
|
248
|
+
},
|
|
249
|
+
});
|
|
235
250
|
await serviceAccess
|
|
236
251
|
.get(HELYX_SERVICE_NAMES.moduleAvailability)
|
|
237
252
|
.refresh();
|
|
@@ -262,6 +277,25 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
262
277
|
platformServices.set(HELYX_SERVICE_NAMES.memberRoles, memberRoles);
|
|
263
278
|
platformServices.set(HELYX_SERVICE_NAMES.memberRoleGrants, new PlatformMemberRoleGrantOwnershipService(new PostgresMemberRoleGrantOwnershipStore(database), memberRoles));
|
|
264
279
|
platformServices.set(HELYX_SERVICE_NAMES.discordResources, new DiscordResourceInspection(client));
|
|
280
|
+
if (environment.HELYX_RUNTIME_PROFILE === "hosted")
|
|
281
|
+
creatorInvitePlatform = await installCreatorInvitePlatform({
|
|
282
|
+
client,
|
|
283
|
+
database,
|
|
284
|
+
services: serviceAccess,
|
|
285
|
+
modules: publicModules,
|
|
286
|
+
encryptionKey: environment.HELYX_DATA_ENCRYPTION_KEY,
|
|
287
|
+
...(environment.HELYX_CLOUD_API_URL &&
|
|
288
|
+
environment.HELYX_OAUTH_INTERNAL_SECRET
|
|
289
|
+
? {
|
|
290
|
+
oauthBroker: {
|
|
291
|
+
baseUrl: environment.HELYX_CLOUD_API_URL,
|
|
292
|
+
secret: environment.HELYX_OAUTH_INTERNAL_SECRET,
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
: {}),
|
|
296
|
+
register: (name, service) => platformServices.set(name, service),
|
|
297
|
+
warn: (event) => logger.warn({ event }, "Creator invite cleanup will retry"),
|
|
298
|
+
});
|
|
265
299
|
const moderationPlatformServices = await installModerationPlatformServices({
|
|
266
300
|
services: platformServices,
|
|
267
301
|
serviceAccess,
|
|
@@ -310,10 +344,12 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
310
344
|
};
|
|
311
345
|
const prepareGuilds = installGuildTracking(client, database, publicModules, logger, enabledCoreModuleIds, shardOwnership, environment.HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS * 1_000, identity.shardCount, environment.DISCORD_CLIENT_ID, hostedSafety, hostedInviteGate, hostedOwnerObserver);
|
|
312
346
|
guildTracking = prepareGuilds;
|
|
313
|
-
hostedLeaveWorker =
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
347
|
+
hostedLeaveWorker = environment.HELYX_HOSTED_MODERATION_ENABLED
|
|
348
|
+
? new HostedServerLeaveWorker(database, client, logger, {
|
|
349
|
+
shardId: identity.shardId,
|
|
350
|
+
shardCount: identity.shardCount,
|
|
351
|
+
})
|
|
352
|
+
: undefined;
|
|
317
353
|
const commandsReady = installInteractions(client, modules, shardWorkerCommandRegistration({
|
|
318
354
|
...(environment.DISCORD_INTERNAL_GUILD_ID
|
|
319
355
|
? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import type { DatabasePool } from "@helyx/database";
|
|
2
1
|
import type { CloudDiscordDirectory, CloudDiscordSnapshot } from "./cloud-connector.js";
|
|
3
2
|
export declare const CLOUD_HELLO_GUILD_LIMIT = 1000;
|
|
3
|
+
export interface SupervisorInstallationDirectory {
|
|
4
|
+
listActiveGuilds(): Promise<readonly {
|
|
5
|
+
guildId: string;
|
|
6
|
+
name: string | null;
|
|
7
|
+
ownerUserId: string | null;
|
|
8
|
+
}[]>;
|
|
9
|
+
}
|
|
4
10
|
export declare class SupervisorCloudDiscordDirectory implements CloudDiscordDirectory {
|
|
5
11
|
#private;
|
|
6
|
-
private readonly
|
|
12
|
+
private readonly installations;
|
|
7
13
|
private readonly configuredOwnerUserIds;
|
|
8
|
-
constructor(token: string,
|
|
14
|
+
constructor(token: string, installations: SupervisorInstallationDirectory, configuredOwnerUserIds: readonly string[]);
|
|
9
15
|
snapshot(): Promise<CloudDiscordSnapshot>;
|
|
10
16
|
}
|
|
11
17
|
//# sourceMappingURL=supervisor-cloud-directory.d.ts.map
|
|
@@ -25,23 +25,19 @@ const userSchema = z
|
|
|
25
25
|
.object({ id: snowflake, username: z.string().min(1).max(100) })
|
|
26
26
|
.passthrough();
|
|
27
27
|
export class SupervisorCloudDiscordDirectory {
|
|
28
|
-
|
|
28
|
+
installations;
|
|
29
29
|
configuredOwnerUserIds;
|
|
30
30
|
#rest;
|
|
31
|
-
constructor(token,
|
|
32
|
-
this.
|
|
31
|
+
constructor(token, installations, configuredOwnerUserIds) {
|
|
32
|
+
this.installations = installations;
|
|
33
33
|
this.configuredOwnerUserIds = configuredOwnerUserIds;
|
|
34
34
|
this.#rest = new REST({ version: "10" }).setToken(token);
|
|
35
35
|
}
|
|
36
36
|
async snapshot() {
|
|
37
|
-
const [applicationValue, botValue,
|
|
37
|
+
const [applicationValue, botValue, activeGuilds] = await Promise.all([
|
|
38
38
|
this.#rest.get(Routes.oauth2CurrentApplication()),
|
|
39
39
|
this.#rest.get(Routes.user("@me")),
|
|
40
|
-
this.
|
|
41
|
-
FROM discord_guilds
|
|
42
|
-
WHERE active = true
|
|
43
|
-
ORDER BY guild_id
|
|
44
|
-
LIMIT ${String(CLOUD_HELLO_GUILD_LIMIT)}`),
|
|
40
|
+
this.installations.listActiveGuilds(),
|
|
45
41
|
]);
|
|
46
42
|
const application = applicationSchema.parse(applicationValue);
|
|
47
43
|
const bot = userSchema.parse(botValue);
|
|
@@ -55,10 +51,10 @@ export class SupervisorCloudDiscordDirectory {
|
|
|
55
51
|
ownerUserIds: [
|
|
56
52
|
...new Set([...this.configuredOwnerUserIds, ...applicationOwnerIds]),
|
|
57
53
|
],
|
|
58
|
-
guilds:
|
|
59
|
-
guildId: guild.
|
|
54
|
+
guilds: activeGuilds.slice(0, CLOUD_HELLO_GUILD_LIMIT).map((guild) => ({
|
|
55
|
+
guildId: guild.guildId,
|
|
60
56
|
name: guild.name ?? "Unknown server",
|
|
61
|
-
ownerUserId: guild.
|
|
57
|
+
ownerUserId: guild.ownerUserId,
|
|
62
58
|
})),
|
|
63
59
|
};
|
|
64
60
|
}
|
|
@@ -18,9 +18,20 @@ export interface TriviaDataSummary {
|
|
|
18
18
|
export interface TriviaDataProvider {
|
|
19
19
|
getSummary(): Promise<TriviaDataSummary>;
|
|
20
20
|
}
|
|
21
|
+
interface MysqlTriviaExecutor {
|
|
22
|
+
query<Row extends Record<string, unknown>>(sql: string): Promise<{
|
|
23
|
+
rows: readonly Row[];
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
21
26
|
export declare class PostgresTriviaDataProvider implements TriviaDataProvider {
|
|
22
27
|
#private;
|
|
23
28
|
constructor(database: DatabasePool);
|
|
24
29
|
getSummary(): Promise<TriviaDataSummary>;
|
|
25
30
|
}
|
|
31
|
+
export declare class MysqlTriviaDataProvider implements TriviaDataProvider {
|
|
32
|
+
#private;
|
|
33
|
+
constructor(database: MysqlTriviaExecutor);
|
|
34
|
+
getSummary(): Promise<TriviaDataSummary>;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
26
37
|
//# sourceMappingURL=trivia-data-provider.d.ts.map
|
|
@@ -14,34 +14,54 @@ export class PostgresTriviaDataProvider {
|
|
|
14
14
|
GROUP BY category, difficulty, type
|
|
15
15
|
ORDER BY category, difficulty, type`),
|
|
16
16
|
]);
|
|
17
|
-
|
|
18
|
-
for (const row of countResult.rows) {
|
|
19
|
-
if (!TRIVIA_CATEGORIES.includes(row.category) ||
|
|
20
|
-
!difficulties.includes(row.difficulty) ||
|
|
21
|
-
!["text_choice", "text_input", "true_false"].includes(row.type))
|
|
22
|
-
throw new Error("Trivia data contains an unsupported group");
|
|
23
|
-
counts.set(`${row.category}:${row.difficulty}`, (counts.get(`${row.category}:${row.difficulty}`) ?? 0) +
|
|
24
|
-
nonNegativeInteger(row.questions, "Trivia question count"));
|
|
25
|
-
}
|
|
26
|
-
const typeCount = (type) => countResult.rows
|
|
27
|
-
.filter((row) => row.type === type)
|
|
28
|
-
.reduce((total, row) => total + nonNegativeInteger(row.questions, "Trivia type count"), 0);
|
|
29
|
-
return {
|
|
30
|
-
uniqueQuestions: nonNegativeInteger(uniqueResult.rows[0]?.unique_questions, "Unique trivia question count"),
|
|
31
|
-
types: {
|
|
32
|
-
multipleChoice: typeCount("text_choice"),
|
|
33
|
-
guess: typeCount("text_input"),
|
|
34
|
-
trueFalse: typeCount("true_false"),
|
|
35
|
-
},
|
|
36
|
-
categories: TRIVIA_CATEGORIES.map((category) => ({
|
|
37
|
-
category,
|
|
38
|
-
easy: counts.get(`${category}:easy`) ?? 0,
|
|
39
|
-
medium: counts.get(`${category}:medium`) ?? 0,
|
|
40
|
-
hard: counts.get(`${category}:hard`) ?? 0,
|
|
41
|
-
})),
|
|
42
|
-
};
|
|
17
|
+
return summarizeTriviaData(uniqueResult.rows[0]?.unique_questions, countResult.rows);
|
|
43
18
|
}
|
|
44
19
|
}
|
|
20
|
+
export class MysqlTriviaDataProvider {
|
|
21
|
+
#database;
|
|
22
|
+
constructor(database) {
|
|
23
|
+
this.#database = database;
|
|
24
|
+
}
|
|
25
|
+
async getSummary() {
|
|
26
|
+
const [uniqueResult, countResult] = await Promise.all([
|
|
27
|
+
this.#database.query(`SELECT count(DISTINCT lower(trim(JSON_UNQUOTE(JSON_EXTRACT(question, '$.text'))))) AS unique_questions
|
|
28
|
+
FROM trivia_questions`),
|
|
29
|
+
this.#database.query(`SELECT category, difficulty, type, count(*) AS questions
|
|
30
|
+
FROM trivia_questions
|
|
31
|
+
GROUP BY category, difficulty, type
|
|
32
|
+
ORDER BY category, difficulty, type`),
|
|
33
|
+
]);
|
|
34
|
+
return summarizeTriviaData(uniqueResult.rows[0]?.unique_questions, countResult.rows);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function summarizeTriviaData(uniqueQuestions, groupedRows) {
|
|
38
|
+
const counts = new Map();
|
|
39
|
+
for (const row of groupedRows) {
|
|
40
|
+
if (!TRIVIA_CATEGORIES.includes(row.category) ||
|
|
41
|
+
!difficulties.includes(row.difficulty) ||
|
|
42
|
+
!["text_choice", "text_input", "true_false"].includes(row.type))
|
|
43
|
+
throw new Error("Trivia data contains an unsupported group");
|
|
44
|
+
counts.set(`${row.category}:${row.difficulty}`, (counts.get(`${row.category}:${row.difficulty}`) ?? 0) +
|
|
45
|
+
nonNegativeInteger(row.questions, "Trivia question count"));
|
|
46
|
+
}
|
|
47
|
+
const typeCount = (type) => groupedRows
|
|
48
|
+
.filter((row) => row.type === type)
|
|
49
|
+
.reduce((total, row) => total + nonNegativeInteger(row.questions, "Trivia type count"), 0);
|
|
50
|
+
return {
|
|
51
|
+
uniqueQuestions: nonNegativeInteger(uniqueQuestions, "Unique trivia question count"),
|
|
52
|
+
types: {
|
|
53
|
+
multipleChoice: typeCount("text_choice"),
|
|
54
|
+
guess: typeCount("text_input"),
|
|
55
|
+
trueFalse: typeCount("true_false"),
|
|
56
|
+
},
|
|
57
|
+
categories: TRIVIA_CATEGORIES.map((category) => ({
|
|
58
|
+
category,
|
|
59
|
+
easy: counts.get(`${category}:easy`) ?? 0,
|
|
60
|
+
medium: counts.get(`${category}:medium`) ?? 0,
|
|
61
|
+
hard: counts.get(`${category}:hard`) ?? 0,
|
|
62
|
+
})),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
45
65
|
function nonNegativeInteger(value, label) {
|
|
46
66
|
const parsed = typeof value === "string" ? Number(value) : value;
|
|
47
67
|
if (typeof parsed !== "number" || !Number.isSafeInteger(parsed) || parsed < 0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type ActionReceipt, type ChallengeCreateRequest, type ChallengeCreateResponse, type DashboardAttemptPage, type DashboardAttemptQuery, type DashboardSafeAttemptDetail, type SignedAssessmentEnvelope, type VerificationInstallationClaims } from "@helyx/verification-contracts";
|
|
1
|
+
import { type ActionReceipt, type ChallengeCreateRequest, type ChallengeCreateResponse, type DashboardAttemptPage, type DashboardAttemptQuery, type DashboardSafeAttemptDetail, type SignedAssessmentEnvelope, type VerificationInstallationClaims, type VerificationResponseUrlOptions } from "@helyx/verification-contracts";
|
|
2
2
|
import type { VerificationResultContext, VerificationService } from "@helyx/sdk";
|
|
3
|
-
export interface HttpVerificationServiceOptions {
|
|
3
|
+
export interface HttpVerificationServiceOptions extends VerificationResponseUrlOptions {
|
|
4
4
|
baseUrl: string;
|
|
5
5
|
internalSecret: string;
|
|
6
6
|
publicSigningKeys: Readonly<Record<string, string>>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { actionReceiptSchema, challengeCreateRequestSchema,
|
|
1
|
+
import { actionReceiptSchema, challengeCreateRequestSchema, createChallengeCreateResponseSchema, dashboardAttemptPageSchema, dashboardAttemptQuerySchema, dashboardSafeAttemptDetailSchema, signedAssessmentEnvelopeSchema, } from "@helyx/verification-contracts";
|
|
2
2
|
import { VerificationChallengeActiveError } from "@helyx/sdk";
|
|
3
3
|
import { createPublicKey, verify } from "node:crypto";
|
|
4
4
|
import { z } from "zod";
|
|
@@ -21,6 +21,7 @@ export class HttpVerificationService {
|
|
|
21
21
|
#publicSigningKeys;
|
|
22
22
|
#fetch;
|
|
23
23
|
#installationId;
|
|
24
|
+
#challengeResponseSchema;
|
|
24
25
|
#challengeCache = new Map();
|
|
25
26
|
#handler;
|
|
26
27
|
constructor(options) {
|
|
@@ -29,6 +30,8 @@ export class HttpVerificationService {
|
|
|
29
30
|
this.#publicSigningKeys = { ...options.publicSigningKeys };
|
|
30
31
|
this.#installationId = options.installationId;
|
|
31
32
|
this.#fetch = options.fetchImplementation ?? fetch;
|
|
33
|
+
this.#challengeResponseSchema =
|
|
34
|
+
createChallengeCreateResponseSchema(options);
|
|
32
35
|
}
|
|
33
36
|
getInstallationContext() {
|
|
34
37
|
return Promise.resolve({
|
|
@@ -54,9 +57,9 @@ export class HttpVerificationService {
|
|
|
54
57
|
if (input.replaceActive !== true &&
|
|
55
58
|
cached &&
|
|
56
59
|
Date.parse(cached.expiresAt) > Date.now()) {
|
|
57
|
-
return
|
|
60
|
+
return this.#challengeResponseSchema.parse({ ...cached, reused: true });
|
|
58
61
|
}
|
|
59
|
-
const created =
|
|
62
|
+
const created = this.#challengeResponseSchema.parse(await this.#request("/internal/v1/challenges", {
|
|
60
63
|
method: "POST",
|
|
61
64
|
body: challengeCreateRequestSchema.parse(input),
|
|
62
65
|
}));
|
package/documentation/README.md
CHANGED
|
@@ -23,9 +23,11 @@ and module guides.
|
|
|
23
23
|
- [Operate the Moderation module](modules/moderation.md)
|
|
24
24
|
- [Operate the Auto Moderation module](modules/auto-moderation.md)
|
|
25
25
|
|
|
26
|
-
The
|
|
27
|
-
`@helyx/module-example`.
|
|
28
|
-
|
|
26
|
+
The standalone authoring reference is published separately as
|
|
27
|
+
`@helyx/module-example`. Adapt its compatibility manifest and compose the
|
|
28
|
+
required privacy contribution before bot installation, as described in
|
|
29
|
+
[the author guide](modules/building-a-module.md). Module-specific commands,
|
|
30
|
+
settings, tests and lifecycle behaviour remain inside that module.
|
|
29
31
|
|
|
30
32
|
## Choose the right path
|
|
31
33
|
|
|
@@ -10,7 +10,8 @@ DISCORD_CLIENT_ID=12345678901234567
|
|
|
10
10
|
# requires message content and it is enabled in the Discord developer portal.
|
|
11
11
|
HELYX_MESSAGE_CONTENT_ENABLED=false
|
|
12
12
|
# Enhanced Auto Moderation stays inside this bot process. One worker is the
|
|
13
|
-
# recommended default; increase only after measuring
|
|
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.
|
|
14
15
|
HELYX_AUTOMOD_EMBEDDED_WORKERS=1
|
|
15
16
|
# Optional conservative queue overrides (hard ceilings: 5000 items, 64 MiB,
|
|
16
17
|
# 100 items per server). Defaults are used when these remain commented.
|
|
@@ -18,13 +19,19 @@ HELYX_AUTOMOD_EMBEDDED_WORKERS=1
|
|
|
18
19
|
# HELYX_AUTOMOD_EMBEDDED_QUEUE_BYTES=67108864
|
|
19
20
|
# HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS_PER_GUILD=100
|
|
20
21
|
# HELYX_AUTOMOD_EMBEDDED_REFRESH_SECONDS=300
|
|
21
|
-
# Enable
|
|
22
|
-
# Members Intent
|
|
22
|
+
# Enable for loaded member workflows such as Welcome, Access Control or Levels,
|
|
23
|
+
# with Server Members Intent also enabled in the Discord developer portal.
|
|
23
24
|
HELYX_GUILD_MEMBERS_ENABLED=false
|
|
24
25
|
# Database
|
|
25
26
|
# Docker: keep the URL. Direct Node.js/managed hosting: use your provider URL.
|
|
27
|
+
DATABASE_ENGINE=postgresql
|
|
26
28
|
DATABASE_URL=postgresql://database:5432/helyx
|
|
27
29
|
DATABASE_SSL=disable
|
|
30
|
+
# Self-host startup applies pending migrations automatically (the default).
|
|
31
|
+
# Hosted-only inspect mode is not a self-host setting; leave this commented.
|
|
32
|
+
# HELYX_DATABASE_STARTUP_MODE=apply
|
|
33
|
+
# Match your provider: disable=no TLS; require=TLS without certificate
|
|
34
|
+
# verification; verify-full=TLS with certificate verification.
|
|
28
35
|
|
|
29
36
|
# Included Docker database — both password values must match.
|
|
30
37
|
PGUSER=helyx
|
|
@@ -51,8 +58,8 @@ LOG_LEVEL=info
|
|
|
51
58
|
# SHARDING: OFF BY DEFAULT
|
|
52
59
|
|
|
53
60
|
# `off` is the compatible self-host default. Keep shard-only values commented
|
|
54
|
-
# in this mode; an empty value is still rejected as configured.
|
|
55
|
-
# self-hosting may use `auto`
|
|
61
|
+
# in this mode; an empty value is still rejected as configured. MySQL must stay
|
|
62
|
+
# off. Advanced PostgreSQL self-hosting may use `auto` under one
|
|
56
63
|
# supervisor, or `fixed` with a deliberate total count. Local mode never needs
|
|
57
64
|
# Redis. Coordinated multi-host self-hosting is outside the first release.
|
|
58
65
|
HELYX_SHARD_MODE=off
|
|
@@ -80,18 +87,15 @@ HELYX_SHARD_MODE=off
|
|
|
80
87
|
HELYX_HOSTED_MODERATION_ENABLED=false
|
|
81
88
|
|
|
82
89
|
# Guild command writes remain off until explicitly enabled for known guild IDs.
|
|
90
|
+
# For enabled-module server commands, set writes=true, retain allowlist scope,
|
|
91
|
+
# add comma-separated Discord server IDs below, and use global mode platform-only.
|
|
92
|
+
# See README.md before changing these defaults. Legacy mode still allows globals.
|
|
83
93
|
HELYX_GUILD_COMMAND_WRITES_ENABLED=false
|
|
84
94
|
HELYX_GUILD_COMMAND_WRITE_SCOPE=allowlist
|
|
85
95
|
# HELYX_GUILD_COMMAND_RELEASE_ID=
|
|
86
96
|
HELYX_GLOBAL_COMMAND_MODE=legacy
|
|
87
97
|
# HELYX_GUILD_COMMAND_WRITE_GUILD_IDS=
|
|
88
98
|
|
|
89
|
-
# Privacy tooling ships with self-hosted Helyx but stays disabled until the
|
|
90
|
-
# dedicated least-privilege executor connection has been configured.
|
|
91
|
-
HELYX_PRIVACY_ENABLED=false
|
|
92
|
-
# HELYX_PRIVACY_EXECUTOR_DATABASE_URL=<dedicated-privacy-executor-postgresql-url>
|
|
93
|
-
# HELYX_PRIVACY_EXECUTOR_DATABASE_SSL=disable
|
|
94
|
-
|
|
95
99
|
# Alternative Cloud Connect enrolment — the Discord command is preferred
|
|
96
100
|
# HELYX_CLOUD_ENROLMENT_TOKEN=replace-with-a-short-lived-enrolment-code
|
|
97
101
|
# HELYX_CLOUD_ENROLMENT_FILE=./helyx.cloud-enrolment.json
|