@helyx/bot 1.1.0 → 1.2.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.
Files changed (85) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +16 -1
  3. package/dist/auto-moderation-scanner-registry.d.ts +27 -0
  4. package/dist/auto-moderation-scanner-registry.js +172 -0
  5. package/dist/cloud-connector.js +3 -0
  6. package/dist/cloud-control-dispatcher.js +32 -0
  7. package/dist/command-path-contract.js +1 -0
  8. package/dist/control-permissions.d.ts +2 -2
  9. package/dist/control-permissions.js +4 -2
  10. package/dist/control-server.d.ts +72 -16
  11. package/dist/control-server.js +150 -250
  12. package/dist/dashboard-action-control.d.ts +25 -0
  13. package/dist/dashboard-action-control.js +73 -0
  14. package/dist/dashboard-action-routes.d.ts +9 -0
  15. package/dist/dashboard-action-routes.js +36 -0
  16. package/dist/dashboard-feature-control.d.ts +37 -0
  17. package/dist/dashboard-feature-control.js +206 -0
  18. package/dist/dashboard-module-projection.d.ts +9 -0
  19. package/dist/dashboard-module-projection.js +15 -0
  20. package/dist/dashboard-preview-import-routes.d.ts +6 -0
  21. package/dist/dashboard-preview-import-routes.js +121 -0
  22. package/dist/discord-analytics.d.ts +3 -0
  23. package/dist/discord-analytics.js +49 -5
  24. package/dist/discord-auto-moderation-service.d.ts +21 -0
  25. package/dist/discord-auto-moderation-service.js +425 -0
  26. package/dist/discord-guild-health.d.ts +2 -2
  27. package/dist/discord-member-enforcement-service.d.ts +26 -0
  28. package/dist/discord-member-enforcement-service.js +590 -0
  29. package/dist/discord-message-enforcement-service.d.ts +9 -0
  30. package/dist/discord-message-enforcement-service.js +102 -0
  31. package/dist/discord-violation-thread-service.d.ts +33 -0
  32. package/dist/discord-violation-thread-service.js +789 -0
  33. package/dist/ecko-evidence-receiver.d.ts +32 -0
  34. package/dist/ecko-evidence-receiver.js +173 -0
  35. package/dist/ecko-transport-auth.d.ts +19 -0
  36. package/dist/ecko-transport-auth.js +80 -0
  37. package/dist/ecko-transport-routes.d.ts +19 -0
  38. package/dist/ecko-transport-routes.js +130 -0
  39. package/dist/embedded-auto-moderation-pool.d.ts +46 -0
  40. package/dist/embedded-auto-moderation-pool.js +324 -0
  41. package/dist/embedded-auto-moderation-scanner.d.ts +34 -0
  42. package/dist/embedded-auto-moderation-scanner.js +263 -0
  43. package/dist/embedded-auto-moderation-worker-runtime.d.ts +37 -0
  44. package/dist/embedded-auto-moderation-worker-runtime.js +37 -0
  45. package/dist/embedded-auto-moderation-worker.d.ts +2 -0
  46. package/dist/embedded-auto-moderation-worker.js +13 -0
  47. package/dist/events.d.ts +5 -1
  48. package/dist/events.js +243 -4
  49. package/dist/guilds.d.ts +2 -2
  50. package/dist/guilds.js +14 -8
  51. package/dist/hosted-server-moderation.d.ts +16 -5
  52. package/dist/hosted-server-moderation.js +32 -4
  53. package/dist/index.d.ts +1 -1
  54. package/dist/index.js +84 -14
  55. package/dist/moderation-action-registry.d.ts +11 -0
  56. package/dist/moderation-action-registry.js +375 -0
  57. package/dist/moderation-case-disassociation.d.ts +13 -0
  58. package/dist/moderation-case-disassociation.js +83 -0
  59. package/dist/moderation-platform-services.d.ts +27 -0
  60. package/dist/moderation-platform-services.js +47 -0
  61. package/dist/modules.d.ts +6 -0
  62. package/dist/modules.js +65 -3
  63. package/dist/privileged-message-metadata-cache.d.ts +35 -0
  64. package/dist/privileged-message-metadata-cache.js +70 -0
  65. package/dist/scheduled-task-runner.js +24 -1
  66. package/dist/shard-contracts.d.ts +151 -1
  67. package/dist/shard-contracts.js +142 -1
  68. package/dist/shard-coordinator.d.ts +1 -1
  69. package/dist/shard-ecko-scanner-registry.d.ts +38 -0
  70. package/dist/shard-ecko-scanner-registry.js +96 -0
  71. package/dist/shard-router.d.ts +23 -0
  72. package/dist/shard-router.js +69 -5
  73. package/dist/shard-supervisor.d.ts +11 -0
  74. package/dist/shard-supervisor.js +67 -4
  75. package/dist/shard-worker.d.ts +15 -1
  76. package/dist/shard-worker.js +95 -17
  77. package/dist/transient-violation-evidence.d.ts +39 -0
  78. package/dist/transient-violation-evidence.js +132 -0
  79. package/documentation/README.md +2 -0
  80. package/documentation/examples/.env.example +12 -0
  81. package/documentation/guides/self-hosting.md +17 -1
  82. package/documentation/modules/auto-moderation.md +51 -0
  83. package/documentation/modules/moderation.md +42 -0
  84. package/documentation/reference/environment.md +11 -0
  85. package/package.json +17 -5
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { AUTOMOD_ENHANCED_CONTRACT_VERSION, AUTOMOD_ENHANCED_PROVIDER_ID, AUTOMOD_SCANNER_LIMITS, } from "@helyx/sdk";
2
3
  import { BotControlTransportError, } from "./control-server.js";
3
4
  import { SHARD_CONTROL_TIMEOUT_MS, controlOperationGuildId, guildIdToShardId, shardControlOperationSchema, shardControlResponseSchema, } from "./shard-contracts.js";
4
5
  // Cross-region/public Redis connections used by local staging routinely take
@@ -7,6 +8,7 @@ import { SHARD_CONTROL_TIMEOUT_MS, controlOperationGuildId, guildIdToShardId, sh
7
8
  // deadline turned healthy leases into deterministic 503s. Keep the check
8
9
  // bounded while allowing normal network variance.
9
10
  const LEASE_VALIDATION_TIMEOUT_MS = 3_000;
11
+ const ECKO_RESULT_TIMEOUT_MS = AUTOMOD_SCANNER_LIMITS.actionDeadlineSeconds * 1_000;
10
12
  export class ShardControlRouter {
11
13
  input;
12
14
  constructor(input) {
@@ -15,11 +17,28 @@ export class ShardControlRouter {
15
17
  cleanupOAuthConsumer(input) {
16
18
  return this.#route({ kind: "oauth.consumer.cleanup", ...input });
17
19
  }
20
+ deliverHostedServerLeave(input) {
21
+ return this.#route({ kind: "hosted-server.leave.deliver", ...input });
22
+ }
23
+ refreshHostedServerOwner(guildId) {
24
+ return this.#route({ kind: "hosted-server.owner.refresh", guildId });
25
+ }
18
26
  listAvailabilityModules() {
19
27
  if (!this.input.availability)
20
28
  return Promise.reject(new BotControlTransportError(503, "Module availability catalogue is unavailable"));
21
29
  return this.input.availability.listAvailabilityModules();
22
30
  }
31
+ /** Routes an authenticated Ecko result to the current guild owner. */
32
+ acceptHostedEckoResult(input) {
33
+ return this.#route({ kind: "automod.enhanced.result.accept", ...input }, remainingEnhancedDeadline(input.result));
34
+ }
35
+ /** Reads the fenced leader's current Enhanced configuration snapshot. */
36
+ getHostedEckoConfigurationPage(input) {
37
+ return this.#routeLeader({
38
+ kind: "automod.enhanced.configuration.page",
39
+ ...input,
40
+ });
41
+ }
23
42
  getGuildState(guildId) {
24
43
  return this.#route({ kind: "guild.state.get", guildId });
25
44
  }
@@ -97,6 +116,12 @@ export class ShardControlRouter {
97
116
  ...input,
98
117
  }));
99
118
  }
119
+ previewConfiguration(input) {
120
+ return this.#route(shardControlOperationSchema.parse({
121
+ kind: "guild.module.configuration.preview",
122
+ ...input,
123
+ }));
124
+ }
100
125
  updatePermissions(input) {
101
126
  return this.#route({ kind: "guild.module.permissions.update", ...input });
102
127
  }
@@ -106,6 +131,9 @@ export class ShardControlRouter {
106
131
  executeDashboardAction(input) {
107
132
  return this.#route({ kind: "guild.module.action.execute", ...input });
108
133
  }
134
+ previewDashboardAction(input) {
135
+ return this.#route({ kind: "guild.module.action.preview", ...input });
136
+ }
109
137
  listManagedResources(input) {
110
138
  return this.#route(shardControlOperationSchema.parse({
111
139
  kind: "guild.module.resource.list",
@@ -130,6 +158,12 @@ export class ShardControlRouter {
130
158
  ...input,
131
159
  }));
132
160
  }
161
+ importManagedResource(input) {
162
+ return this.#route(shardControlOperationSchema.parse({
163
+ kind: "guild.module.resource.import",
164
+ ...input,
165
+ }));
166
+ }
133
167
  createManagedResource(input) {
134
168
  return this.#route(shardControlOperationSchema.parse({
135
169
  kind: "guild.module.resource.create",
@@ -188,7 +222,7 @@ export class ShardControlRouter {
188
222
  executeThrough: executeThrough.toISOString(),
189
223
  });
190
224
  }
191
- async #route(operation) {
225
+ async #route(operation, timeoutMs = SHARD_CONTROL_TIMEOUT_MS) {
192
226
  const guildId = controlOperationGuildId(operation);
193
227
  if (!guildId)
194
228
  throw new BotControlTransportError(502, "The control operation has no shard route");
@@ -212,13 +246,13 @@ export class ShardControlRouter {
212
246
  requestId: randomUUID(),
213
247
  generation: this.input.generation,
214
248
  fencingToken: lease.fencingToken,
215
- deadlineAt: new Date(Date.now() + SHARD_CONTROL_TIMEOUT_MS).toISOString(),
249
+ deadlineAt: new Date(Date.now() + timeoutMs).toISOString(),
216
250
  operation,
217
251
  };
218
252
  let response;
219
253
  try {
220
254
  response = localLease
221
- ? await withDeadline(this.input.dispatchLocal(shardId, request), SHARD_CONTROL_TIMEOUT_MS)
255
+ ? await withDeadline(this.input.dispatchLocal(shardId, request), timeoutMs)
222
256
  : await this.input.coordinator.routeRemote(lease, request);
223
257
  }
224
258
  catch (error) {
@@ -231,13 +265,13 @@ export class ShardControlRouter {
231
265
  return unwrapResponse(response);
232
266
  }
233
267
  async #routeLeader(operation) {
234
- if (this.input.localLeaderLease && this.input.privacy) {
268
+ if (this.input.localLeaderLease) {
235
269
  if (this.input.coordinator) {
236
270
  const current = await this.#currentLeaderLease();
237
271
  if (!current || !sameLeaderLease(this.input.localLeaderLease, current))
238
272
  throw new BotControlTransportError(503, "The shard leader is temporarily unavailable");
239
273
  }
240
- return executePrivacy(this.input.privacy, operation);
274
+ return executeLeaderOperation(this.input, operation);
241
275
  }
242
276
  const coordinator = this.input.coordinator;
243
277
  const lease = await this.#currentLeaderLease();
@@ -283,6 +317,36 @@ export class ShardControlRouter {
283
317
  }
284
318
  }
285
319
  }
320
+ async function executeLeaderOperation(input, operation) {
321
+ if (operation.kind === "automod.enhanced.configuration.page") {
322
+ if (!input.leaderScannerRegistry)
323
+ throw new BotControlTransportError(503, "The Enhanced configuration leader is unavailable");
324
+ return input.leaderScannerRegistry.getConfigurationPage({
325
+ adapter: "hosted_ecko",
326
+ providerId: AUTOMOD_ENHANCED_PROVIDER_ID,
327
+ contractVersion: AUTOMOD_ENHANCED_CONTRACT_VERSION,
328
+ limit: operation.limit,
329
+ ...(operation.cursor ? { cursor: operation.cursor } : {}),
330
+ ...(operation.knownFingerprint
331
+ ? { knownFingerprint: operation.knownFingerprint }
332
+ : {}),
333
+ });
334
+ }
335
+ if (!input.privacy)
336
+ throw new BotControlTransportError(503, "The shard leader is temporarily unavailable");
337
+ return executePrivacy(input.privacy, operation);
338
+ }
339
+ function remainingEnhancedDeadline(result) {
340
+ if (!result || typeof result !== "object")
341
+ return ECKO_RESULT_TIMEOUT_MS;
342
+ const deadlineAt = result["deadlineAt"];
343
+ if (typeof deadlineAt !== "string")
344
+ return ECKO_RESULT_TIMEOUT_MS;
345
+ const parsed = Date.parse(deadlineAt);
346
+ if (!Number.isFinite(parsed))
347
+ return ECKO_RESULT_TIMEOUT_MS;
348
+ return Math.max(1, Math.min(ECKO_RESULT_TIMEOUT_MS, parsed - Date.now()));
349
+ }
286
350
  async function executePrivacy(privacy, operation) {
287
351
  if (operation.kind === "privacy.providers.list")
288
352
  return (await privacy.listPrivacyProviders());
@@ -4,6 +4,7 @@ import type { ModuleDefinition, NumericWriteBehindCommittedBatch, ScheduledTaskE
4
4
  import { buildApplicationCommandDefinitions } from "./interactions.js";
5
5
  import { type ShardCloudRequest, type ShardHealthSnapshot, type ShardIdentity, type ShardProcessHealth } from "./shard-contracts.js";
6
6
  import { type ShardCoordinator, type ShardGenerationDescriptor, type ShardLeaderLease, type ShardLease } from "./shard-coordinator.js";
7
+ import { ShardControlRouter } from "./shard-router.js";
7
8
  import type { BotHandle } from "./index.js";
8
9
  export declare const STEADY_STATE_LEASE_LOSS_BOUND_MS: number;
9
10
  export declare function supervisorShutdownExitCode(reason: string): number | undefined;
@@ -17,6 +18,16 @@ export declare function relinquishRestartCircuitOwnership(input: {
17
18
  }): Promise<boolean>;
18
19
  export declare function deferScheduledRoutingFailures(route: () => Promise<ScheduledTaskExecutionResult>): Promise<ScheduledTaskExecutionResult>;
19
20
  export declare function startShardSupervisor(environment: BotEnvironment, processEnvironment: NodeJS.ProcessEnv, workerEntry: string): Promise<BotHandle>;
21
+ export declare function shardHostedControlCallbacks(router: Pick<ShardControlRouter, "deliverHostedServerLeave" | "refreshHostedServerOwner">): {
22
+ deliverHostedServerLeave: (input: {
23
+ operationId: string;
24
+ guildId: string;
25
+ }) => Promise<"completed" | "requires_review" | "queued" | "not_currently_installed" | "accepted_for_retry">;
26
+ refreshHostedServerOwner: (guildId: string) => Promise<{
27
+ username: string;
28
+ observedAt: string;
29
+ }>;
30
+ };
20
31
  export declare function splitNumericCommittedBatch(batch: NumericWriteBehindCommittedBatch, index: number): NumericWriteBehindCommittedBatch;
21
32
  export declare function buildShardWorkerEnvironment(source: NodeJS.ProcessEnv, identity: ShardIdentity, restartCount: number): NodeJS.ProcessEnv;
22
33
  export declare function shardRuntimeCompositionHash(modules: readonly ModuleDefinition[], internalGuildId?: string, globalCommandMode?: Parameters<typeof buildApplicationCommandDefinitions>[2]): string;
@@ -1,7 +1,7 @@
1
1
  import { safeError, ServiceRegistry } from "@helyx/core";
2
- import { checkDatabaseConnection, createDatabasePool, AuditRepository, GuildCommandRepository, HostedServerSafetyRepository, InteractionSessionRepository, PremiumAllocationRepository, RateLimitRepository, runConfiguredModuleMigrations, runCoreMigrations, RuntimeStateRepository, } from "@helyx/database";
2
+ import { checkDatabaseConnection, createDatabasePool, AuditRepository, GuildCommandRepository, HostedServerSafetyRepository, InteractionSessionRepository, EckoOperationsRepository, PremiumAllocationRepository, RateLimitRepository, runConfiguredModuleMigrations, runCoreMigrations, RuntimeStateRepository, } from "@helyx/database";
3
3
  import { createPlatformServices, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
4
- import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
4
+ import { AUTOMOD_ENHANCED_CONTRACT_VERSION, AUTOMOD_ENHANCED_PROVIDER_ID, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
5
5
  import { Client, ShardingManager } from "discord.js";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
7
  import { createRequire } from "node:module";
@@ -19,6 +19,11 @@ import { createPermissionsModule } from "./permissions-module.js";
19
19
  import { SHARD_CONTROL_MAX_BYTES, SHARD_CONTROL_TIMEOUT_MS, SHARD_HEALTH_STALE_MS, assertShardMessageSize, controlOperationGuildId, executeShardControlOperation, gatewayBotInformationSchema, guildIdToShardId, shardCloudRequestSchema, shardHealthSnapshotSchema, workerMessageSchema, } from "./shard-contracts.js";
20
20
  import { RedisShardCoordinator, } from "./shard-coordinator.js";
21
21
  import { ShardControlRouter } from "./shard-router.js";
22
+ import { BotAutoModerationScannerRegistry } from "./auto-moderation-scanner-registry.js";
23
+ import { ShardEckoScannerRegistry } from "./shard-ecko-scanner-registry.js";
24
+ import { EckoEvidenceReceiver } from "./ecko-evidence-receiver.js";
25
+ import { EckoTransportAuthenticator } from "./ecko-transport-auth.js";
26
+ import { TransientViolationEvidenceStore } from "./transient-violation-evidence.js";
22
27
  import { createDeveloperOperationsModule } from "./developer-operations-module.js";
23
28
  import { SupervisorCloudDiscordDirectory } from "./supervisor-cloud-directory.js";
24
29
  import { createSupportModule } from "./support-module.js";
@@ -148,6 +153,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
148
153
  let deploymentOrder = 1;
149
154
  let shardCount = 1;
150
155
  let privacyService;
156
+ let leaderScannerRegistry;
151
157
  let manager;
152
158
  let scheduledTasks;
153
159
  let guildCommandWorker;
@@ -180,6 +186,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
180
186
  observedAt: new Date().toISOString(),
181
187
  };
182
188
  const supervisorDirectory = new SupervisorCloudDiscordDirectory(environment.DISCORD_TOKEN, database, environment.HELYX_OWNER_USER_IDS);
189
+ const eckoOperations = new EckoOperationsRepository(database);
183
190
  cloudConnector = new CloudConnectorController({
184
191
  environment,
185
192
  database,
@@ -288,6 +295,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
288
295
  assertStartupLeaseHealthy();
289
296
  const services = new Map(createPlatformServices(database, publicModules, platformServiceOptions(environment, publicModules)));
290
297
  runtimeAvailability = services.get(HELYX_SERVICE_NAMES.moduleAvailability);
298
+ leaderScannerRegistry = new BotAutoModerationScannerRegistry(publicModules, new ServiceRegistry(services), "hosted", undefined, environment.HELYX_DEPLOYMENT_ENVIRONMENT);
291
299
  if (numericWriteBehind)
292
300
  services.set(HELYX_SERVICE_NAMES.numericWriteBehind, numericWriteBehind);
293
301
  privacyService = new BotControlService(database, [
@@ -311,6 +319,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
311
319
  localLeases,
312
320
  dispatchLocal,
313
321
  privacy: privacyService,
322
+ leaderScannerRegistry,
314
323
  availability: availabilityService,
315
324
  localLeaderLease: leaderLease,
316
325
  ...(coordinator ? { coordinator } : {}),
@@ -405,6 +414,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
405
414
  localLeases,
406
415
  dispatchLocal,
407
416
  ...(privacyService ? { privacy: privacyService } : {}),
417
+ ...(leaderScannerRegistry ? { leaderScannerRegistry } : {}),
408
418
  availability: availabilityService,
409
419
  ...(leaderLease ? { localLeaderLease: leaderLease } : {}),
410
420
  ...(coordinator ? { coordinator } : {}),
@@ -437,13 +447,41 @@ export async function startShardSupervisor(environment, processEnvironment, work
437
447
  startScheduledTasks();
438
448
  assertStartupLeaseHealthy();
439
449
  if (environment.HELYX_BOT_CONTROL_SECRET) {
450
+ const supervisorEvidenceStore = new TransientViolationEvidenceStore();
451
+ const shardEckoRegistry = new ShardEckoScannerRegistry({
452
+ configurationRegistry: () => leaderScannerRegistry,
453
+ routeConfiguration: (input) => router.getHostedEckoConfigurationPage(input),
454
+ evidenceStore: supervisorEvidenceStore,
455
+ routeResult: (input) => router.acceptHostedEckoResult(input),
456
+ });
457
+ const ecko = environment.HELYX_ECKO_TRANSPORT_SECRET
458
+ ? {
459
+ authenticator: new EckoTransportAuthenticator(environment.HELYX_DEPLOYMENT_ENVIRONMENT, [
460
+ environment.HELYX_ECKO_TRANSPORT_SECRET,
461
+ ...(environment.HELYX_ECKO_TRANSPORT_PREVIOUS_SECRET
462
+ ? [environment.HELYX_ECKO_TRANSPORT_PREVIOUS_SECRET]
463
+ : []),
464
+ ]),
465
+ registry: shardEckoRegistry,
466
+ evidence: new EckoEvidenceReceiver(supervisorEvidenceStore, environment.HELYX_DEPLOYMENT_ENVIRONMENT, Buffer.from(environment.HELYX_ECKO_TRANSPORT_SECRET, "base64")),
467
+ gateState: () => eckoOperations.readGate(),
468
+ telemetry: {
469
+ accept: (batch) => eckoOperations.acceptTelemetry(batch),
470
+ },
471
+ }
472
+ : undefined;
440
473
  controlServer = await createBotControlServer({
441
474
  service: router,
442
475
  secret: environment.HELYX_BOT_CONTROL_SECRET,
476
+ ...shardHostedControlCallbacks(router),
443
477
  logger: { level: environment.LOG_LEVEL },
478
+ runtimeConfiguration: {
479
+ hostedModerationEnabled: environment.HELYX_HOSTED_MODERATION_ENABLED,
480
+ },
444
481
  shardHealth: buildHealth,
445
482
  readiness: buildDeploymentReadiness,
446
483
  guildCommands: new GuildCommandReadService(database, environment.DISCORD_CLIENT_ID, () => guildCommandWorker?.getHealth(), environment.DISCORD_TOKEN, environment.HELYX_GUILD_COMMAND_WRITES_ENABLED, environment.HELYX_GUILD_COMMAND_WRITE_GUILD_IDS, environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all", commandModules),
484
+ ...(ecko ? { ecko } : {}),
447
485
  });
448
486
  await controlServer.listen({
449
487
  host: "::",
@@ -855,7 +893,10 @@ export async function startShardSupervisor(environment, processEnvironment, work
855
893
  });
856
894
  }
857
895
  async function executeLeaderControl(request) {
858
- if (!leaderLease || !privacyService)
896
+ if (!leaderLease ||
897
+ !privacyService ||
898
+ (request.operation.kind === "automod.enhanced.configuration.page" &&
899
+ !leaderScannerRegistry))
859
900
  return unavailableControl(request.requestId);
860
901
  if (request.generation !== leaderLease.generation ||
861
902
  request.fencingToken !== leaderLease.fencingToken ||
@@ -867,7 +908,22 @@ export async function startShardSupervisor(environment, processEnvironment, work
867
908
  type: "helyx.control.response",
868
909
  requestId: request.requestId,
869
910
  ok: true,
870
- result: await executeShardControlOperation(privacyService, request.operation),
911
+ result: request.operation.kind === "automod.enhanced.configuration.page"
912
+ ? await leaderScannerRegistry?.getConfigurationPage({
913
+ adapter: "hosted_ecko",
914
+ providerId: AUTOMOD_ENHANCED_PROVIDER_ID,
915
+ contractVersion: AUTOMOD_ENHANCED_CONTRACT_VERSION,
916
+ limit: request.operation.limit,
917
+ ...(request.operation.cursor
918
+ ? { cursor: request.operation.cursor }
919
+ : {}),
920
+ ...(request.operation.knownFingerprint
921
+ ? {
922
+ knownFingerprint: request.operation.knownFingerprint,
923
+ }
924
+ : {}),
925
+ })
926
+ : await executeShardControlOperation(privacyService, request.operation),
871
927
  };
872
928
  }
873
929
  catch (error) {
@@ -1454,6 +1510,12 @@ export async function startShardSupervisor(environment, processEnvironment, work
1454
1510
  logger.info({ event: "shard.supervisor_stopped", reason }, "Shard supervisor stopped");
1455
1511
  }
1456
1512
  }
1513
+ export function shardHostedControlCallbacks(router) {
1514
+ return {
1515
+ deliverHostedServerLeave: (input) => router.deliverHostedServerLeave(input),
1516
+ refreshHostedServerOwner: (guildId) => router.refreshHostedServerOwner(guildId),
1517
+ };
1518
+ }
1457
1519
  export function splitNumericCommittedBatch(batch, index) {
1458
1520
  const mutation = batch.mutations[index];
1459
1521
  const result = batch.results[index];
@@ -1583,6 +1645,7 @@ export function buildShardWorkerEnvironment(source, identity, restartCount) {
1583
1645
  "HELYX_MESSAGE_CONTENT_ENABLED",
1584
1646
  "HELYX_GUILD_MEMBERS_ENABLED",
1585
1647
  "HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS",
1648
+ "HELYX_HOSTED_MODERATION_ENABLED",
1586
1649
  "HELYX_MODULE_CATALOGUE_PATH",
1587
1650
  "STRIPE_ENVIRONMENT",
1588
1651
  "HELYX_CLOUD_CONNECT_ENABLED",
@@ -1,5 +1,7 @@
1
1
  import { type CommandRegistration } from "./interactions.js";
2
- import { type ShardIdentity, type ShardScheduledTaskRequest, type ShardScheduledTaskResponse } from "./shard-contracts.js";
2
+ import type { BotAutoModerationScannerRegistry } from "./auto-moderation-scanner-registry.js";
3
+ import type { TransientViolationEvidenceStore } from "./transient-violation-evidence.js";
4
+ import { type ShardControlOperation, type ShardIdentity, type ShardScheduledTaskRequest, type ShardScheduledTaskResponse } from "./shard-contracts.js";
3
5
  import { type BotHandle } from "./index.js";
4
6
  import { type ScheduledTaskRoute } from "./scheduled-task-runner.js";
5
7
  export declare const WORKER_ORPHAN_TERMINATION_BOUND_MS: number;
@@ -28,6 +30,7 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
28
30
  HELYX_SHARD_WORKER_DATABASE_POOL_MAX: number | undefined;
29
31
  DATABASE_URL: string;
30
32
  DATABASE_SSL: "disable" | "require" | "verify-full";
33
+ HELYX_RUNTIME_PROFILE: "hosted" | "self-hosted";
31
34
  DISCORD_TOKEN: string;
32
35
  DISCORD_CLIENT_ID: string;
33
36
  DISCORD_INTERNAL_GUILD_ID: string | undefined;
@@ -35,6 +38,7 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
35
38
  HELYX_MODULE_PACKAGES: string[];
36
39
  HELYX_MODULE_CATALOGUE_PATH: string | undefined;
37
40
  HELYX_DASHBOARD_PUBLIC_URL: string;
41
+ HELYX_DEPLOYMENT_ENVIRONMENT: string;
38
42
  HELYX_HOSTED_MODERATION_ENABLED: boolean;
39
43
  HELYX_SUPPORT_STAFF_CHANNEL_ID: string | undefined;
40
44
  HELYX_VERIFICATION_ENABLED: boolean;
@@ -47,6 +51,11 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
47
51
  HELYX_GUILD_COMMAND_WRITE_GUILD_IDS: string[];
48
52
  HELYX_PRIVACY_ENABLED: boolean;
49
53
  HELYX_MESSAGE_CONTENT_ENABLED: boolean;
54
+ HELYX_AUTOMOD_EMBEDDED_WORKERS: number;
55
+ HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS: number;
56
+ HELYX_AUTOMOD_EMBEDDED_QUEUE_BYTES: number;
57
+ HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS_PER_GUILD: number;
58
+ HELYX_AUTOMOD_EMBEDDED_REFRESH_SECONDS: number;
50
59
  HELYX_GUILD_MEMBERS_ENABLED: boolean;
51
60
  HELYX_CLOUD_CONNECT_ENABLED: boolean;
52
61
  HELYX_CLOUD_ENROLMENT_TOKEN: string | undefined;
@@ -66,6 +75,8 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
66
75
  HELYX_STATUS_HEARTBEAT_INTERVAL_SECONDS: number;
67
76
  STRIPE_ENVIRONMENT?: "sandbox" | "live" | undefined;
68
77
  HELYX_BOT_CONTROL_SECRET?: string | undefined;
78
+ HELYX_ECKO_TRANSPORT_SECRET?: string | undefined;
79
+ HELYX_ECKO_TRANSPORT_PREVIOUS_SECRET?: string | undefined;
69
80
  HELYX_SUPPORT_SERVICE_SECRET?: string | undefined;
70
81
  HELYX_VERIFICATION_INSTALLATION_ID?: string | undefined;
71
82
  HELYX_VERIFICATION_URL?: string | undefined;
@@ -91,4 +102,7 @@ export declare function shardWorkerCommandRegistration(input: {
91
102
  premiumActivationModuleId?: string;
92
103
  oauthModuleId?: string;
93
104
  }): CommandRegistration;
105
+ export declare function acceptEnhancedAutoModerationResult(registry: Pick<BotAutoModerationScannerRegistry, "acceptResult"> | undefined, evidenceStore: TransientViolationEvidenceStore | undefined, operation: Extract<ShardControlOperation, {
106
+ kind: "automod.enhanced.result.accept";
107
+ }>): Promise<import("@helyx/sdk").AutoModerationEnhancedResultOutcome>;
94
108
  //# sourceMappingURL=shard-worker.d.ts.map
@@ -4,14 +4,15 @@ import { checkDatabaseConnection, createDatabasePool, PremiumAllocationRepositor
4
4
  import { REVIEWED_PREMIUM_MODULE_IDS } from "@helyx/module-manifest";
5
5
  import { createRuntime } from "@helyx/framework";
6
6
  import { createPlatformServices, PlatformMemberRoleGrantOwnershipService, PostgresMemberRoleGrantOwnershipStore, } from "@helyx/platform";
7
- import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
7
+ import { AUTOMOD_ENHANCED_CONTRACT_VERSION, AUTOMOD_ENHANCED_PROVIDER_ID, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
8
8
  import { Client, Events, Partials } from "discord.js";
9
9
  import { monitorEventLoopDelay } from "node:perf_hooks";
10
10
  import pino from "pino";
11
11
  import { createCloudManagementModule } from "./cloud-management-module.js";
12
- import { BotControlService, DiscordGuildResourceDirectory, mapBotControlError, } from "./control-server.js";
12
+ import { BotControlService, BotControlTransportError, DiscordGuildResourceDirectory, mapBotControlError, } from "./control-server.js";
13
13
  import { HttpVerificationService } from "./verification-service.js";
14
- import { DiscordMessageDeliveryService, DiscordMemberEnforcementService, DiscordMemberRoleService, DiscordVerificationMemberService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
14
+ import { DiscordMessageDeliveryService, DiscordMemberRoleService, DiscordVerificationMemberService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
15
+ import { DiscordModerationMemberEnforcementAdapter } from "./discord-member-enforcement-service.js";
15
16
  import { installGuildTracking, } from "./guilds.js";
16
17
  import { installInteractions, } from "./interactions.js";
17
18
  import { createManagementModule } from "./management-module.js";
@@ -24,6 +25,7 @@ import { DiscordPremiumGuildAuthority } from "./premium-guild-authority.js";
24
25
  import { assertReviewedPremiumCatalogue, requirePremiumProviderEnvironment, } from "./premium-access.js";
25
26
  import { WorkerCloudConnectorProxy } from "./shard-cloud-proxy.js";
26
27
  import { DiscordOwnedMessageDeletionService } from "./discord-owned-message-service.js";
28
+ import { installModerationPlatformServices } from "./moderation-platform-services.js";
27
29
  import { DiscordGuildHealthInspection } from "./discord-guild-health.js";
28
30
  import { DiscordAnalyticsInspection } from "./discord-analytics.js";
29
31
  import { DiscordInviteInspection } from "./discord-invites.js";
@@ -80,8 +82,9 @@ export async function startShardWorker(processEnvironment = process.env, identit
80
82
  ? {}
81
83
  : { maxConnections: environment.HELYX_SHARD_WORKER_DATABASE_POOL_MAX }),
82
84
  });
85
+ const hostedOwnerObserver = new PostgresHostedGuildSafety(database);
83
86
  const hostedSafety = environment.HELYX_HOSTED_MODERATION_ENABLED
84
- ? new PostgresHostedGuildSafety(database)
87
+ ? hostedOwnerObserver
85
88
  : undefined;
86
89
  const developerModeration = environment.HELYX_HOSTED_MODERATION_ENABLED
87
90
  ? new HostedServerModerationClient(environment.HELYX_CLOUD_API_URL, environment.HELYX_BOT_CONTROL_SECRET)
@@ -100,6 +103,9 @@ export async function startShardWorker(processEnvironment = process.env, identit
100
103
  let client;
101
104
  let cloudProxy;
102
105
  let shardHealthProvider;
106
+ let embeddedScanner;
107
+ let autoModerationScannerRegistry;
108
+ let violationEvidenceStore;
103
109
  let hostedLeaveWorker;
104
110
  let leaseHeartbeatWatchdog;
105
111
  let forcedLeaseExit;
@@ -120,6 +126,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
120
126
  await cloudProxy?.stop().catch(() => undefined);
121
127
  shardHealthProvider?.stop();
122
128
  await moduleEvents?.stop().catch(() => undefined);
129
+ await embeddedScanner?.stop().catch(() => undefined);
123
130
  if (runtimeStarted)
124
131
  await runtime?.stop().catch(() => undefined);
125
132
  await numericWriteBehind?.stop();
@@ -137,7 +144,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
137
144
  assertReviewedPremiumCatalogue(featureModules);
138
145
  client = new Client({
139
146
  rest: discordRestOptions,
140
- intents: gatewayIntentsForModules(featureModules, environment.HELYX_MESSAGE_CONTENT_ENABLED, environment.HELYX_GUILD_MEMBERS_ENABLED),
147
+ intents: gatewayIntentsForModules(featureModules, environment.HELYX_MESSAGE_CONTENT_ENABLED, environment.HELYX_GUILD_MEMBERS_ENABLED, environment.HELYX_RUNTIME_PROFILE),
141
148
  partials: [Partials.Message, Partials.Channel, Partials.Reaction],
142
149
  allowedMentions: { parse: [] },
143
150
  shards: [identity.shardId],
@@ -245,11 +252,29 @@ export async function startShardWorker(processEnvironment = process.env, identit
245
252
  platformServices.set(HELYX_SERVICE_NAMES.resourceAuthorization, new CoreResourceAuthorizationService(platformServices.get(HELYX_SERVICE_NAMES.permissions)));
246
253
  platformServices.set(HELYX_SERVICE_NAMES.discordMessages, new DiscordMessageDeliveryService(client));
247
254
  platformServices.set(HELYX_SERVICE_NAMES.discordOwnedMessages, new DiscordOwnedMessageDeletionService(client));
248
- platformServices.set(HELYX_SERVICE_NAMES.memberEnforcement, new DiscordMemberEnforcementService(client));
255
+ platformServices.set(HELYX_SERVICE_NAMES.memberEnforcement, new DiscordModerationMemberEnforcementAdapter(client));
249
256
  const memberRoles = new DiscordMemberRoleService(client);
250
257
  platformServices.set(HELYX_SERVICE_NAMES.memberRoles, memberRoles);
251
258
  platformServices.set(HELYX_SERVICE_NAMES.memberRoleGrants, new PlatformMemberRoleGrantOwnershipService(new PostgresMemberRoleGrantOwnershipStore(database), memberRoles));
252
259
  platformServices.set(HELYX_SERVICE_NAMES.discordResources, new DiscordResourceInspection(client));
260
+ const moderationPlatformServices = installModerationPlatformServices({
261
+ services: platformServices,
262
+ serviceAccess,
263
+ client,
264
+ modules: publicModules,
265
+ runtimeProfile: environment.HELYX_RUNTIME_PROFILE,
266
+ deploymentEnvironment: environment.HELYX_DEPLOYMENT_ENVIRONMENT,
267
+ embedded: {
268
+ workers: environment.HELYX_AUTOMOD_EMBEDDED_WORKERS,
269
+ maximumItems: environment.HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS,
270
+ maximumBytes: environment.HELYX_AUTOMOD_EMBEDDED_QUEUE_BYTES,
271
+ maximumItemsPerGuild: environment.HELYX_AUTOMOD_EMBEDDED_QUEUE_ITEMS_PER_GUILD,
272
+ refreshIntervalMs: environment.HELYX_AUTOMOD_EMBEDDED_REFRESH_SECONDS * 1_000,
273
+ },
274
+ });
275
+ embeddedScanner = moderationPlatformServices.embeddedScanner;
276
+ autoModerationScannerRegistry = moderationPlatformServices.scannerRegistry;
277
+ violationEvidenceStore = moderationPlatformServices.evidenceStore;
253
278
  platformServices.set(HELYX_SERVICE_NAMES.discordGuildHealth, new DiscordGuildHealthInspection(client, publicModules, serviceAccess));
254
279
  platformServices.set(HELYX_SERVICE_NAMES.discordAnalytics, new DiscordAnalyticsInspection(client));
255
280
  if (serviceOptions.inviteFingerprintKey)
@@ -278,14 +303,12 @@ export async function startShardWorker(processEnvironment = process.env, identit
278
303
  shardId: identity.shardId,
279
304
  fencingToken: BigInt(identity.fencingToken),
280
305
  };
281
- 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);
306
+ 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);
282
307
  guildTracking = prepareGuilds;
283
- hostedLeaveWorker = hostedSafety
284
- ? new HostedServerLeaveWorker(database, client, logger, {
285
- shardId: identity.shardId,
286
- shardCount: identity.shardCount,
287
- })
288
- : undefined;
308
+ hostedLeaveWorker = new HostedServerLeaveWorker(database, client, logger, {
309
+ shardId: identity.shardId,
310
+ shardCount: identity.shardCount,
311
+ });
289
312
  const commandsReady = installInteractions(client, modules, shardWorkerCommandRegistration({
290
313
  ...(environment.DISCORD_INTERNAL_GUILD_ID
291
314
  ? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
@@ -300,7 +323,10 @@ export async function startShardWorker(processEnvironment = process.env, identit
300
323
  : {}),
301
324
  ...(oauthModule ? { oauthModuleId: oauthModule.manifest.id } : {}),
302
325
  }), logger, serviceAccess);
303
- moduleEvents = installModuleEvents(client, publicModules, logger, serviceAccess);
326
+ moduleEvents = installModuleEvents(client, publicModules, logger, serviceAccess, {
327
+ runtimeProfile: environment.HELYX_RUNTIME_PROFILE,
328
+ evidenceStore: moderationPlatformServices.evidenceStore,
329
+ });
304
330
  const controlService = new BotControlService(database, publicModules, guildResources, serviceAccess, undefined, environment.DISCORD_INTERNAL_GUILD_ID &&
305
331
  environment.HELYX_SUPPORT_STAFF_CHANNEL_ID
306
332
  ? {
@@ -313,7 +339,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
313
339
  if (!parsed.success)
314
340
  return;
315
341
  if (parsed.data.type === "helyx.control.request") {
316
- void handleControl(identity, controlService, parsed.data, logger);
342
+ void handleControl(identity, controlService, hostedOwnerObserver, client, hostedLeaveWorker, autoModerationScannerRegistry, violationEvidenceStore, parsed.data, logger);
317
343
  }
318
344
  else if (parsed.data.type === "helyx.lease.heartbeat") {
319
345
  if (sameShardIdentity(parsed.data.identity, identity))
@@ -375,6 +401,9 @@ export async function startShardWorker(processEnvironment = process.env, identit
375
401
  await cloudProxy.start();
376
402
  await runtime.start();
377
403
  runtimeStarted = true;
404
+ await embeddedScanner?.synchronize().catch((error) => {
405
+ logger.warn({ event: "automod.embedded_sync_failed", error: safeError(error) }, "Embedded Auto Moderation scanner started in Standard fallback");
406
+ });
378
407
  await client.login(environment.DISCORD_TOKEN);
379
408
  await commandsReady;
380
409
  hostedLeaveWorker?.start();
@@ -501,7 +530,7 @@ export function shardWorkerCommandRegistration(input) {
501
530
  ]),
502
531
  };
503
532
  }
504
- async function handleControl(identity, service, request, logger) {
533
+ async function handleControl(identity, service, hostedOwnerObserver, client, hostedLeaveWorker, scannerRegistry, evidenceStore, request, logger) {
505
534
  let response;
506
535
  const guildId = controlOperationGuildId(request.operation);
507
536
  if (request.generation !== identity.generation ||
@@ -525,7 +554,13 @@ async function handleControl(identity, service, request, logger) {
525
554
  type: "helyx.control.response",
526
555
  requestId: request.requestId,
527
556
  ok: true,
528
- result: await executeShardControlOperation(service, request.operation),
557
+ result: request.operation.kind === "automod.enhanced.result.accept"
558
+ ? await acceptEnhancedAutoModerationResult(scannerRegistry, evidenceStore, request.operation)
559
+ : request.operation.kind === "hosted-server.leave.deliver"
560
+ ? await deliverHostedLeave(hostedLeaveWorker, request.operation)
561
+ : request.operation.kind === "hosted-server.owner.refresh"
562
+ ? await refreshHostedOwner(hostedOwnerObserver, client, request.operation.guildId)
563
+ : await executeShardControlOperation(service, request.operation),
529
564
  };
530
565
  }
531
566
  catch (error) {
@@ -570,6 +605,49 @@ async function handleControl(identity, service, request, logger) {
570
605
  }
571
606
  process.send?.(response);
572
607
  }
608
+ export async function acceptEnhancedAutoModerationResult(registry, evidenceStore, operation) {
609
+ if (!registry || !evidenceStore)
610
+ return {
611
+ outcome: "rejected",
612
+ code: "module_unavailable",
613
+ };
614
+ const resultGuildId = operation.result && typeof operation.result === "object"
615
+ ? operation.result["guildId"]
616
+ : undefined;
617
+ if (resultGuildId !== operation.guildId)
618
+ return { outcome: "rejected", code: "wrong_owner" };
619
+ const evidenceReference = operation.evidence
620
+ ? evidenceStore.capture({
621
+ source: "enhanced_scanner",
622
+ ...operation.evidence,
623
+ })
624
+ : null;
625
+ if (operation.evidence && !evidenceReference)
626
+ return {
627
+ outcome: "rejected",
628
+ code: "module_unavailable",
629
+ };
630
+ return registry.acceptResult({
631
+ adapter: "hosted_ecko",
632
+ providerId: AUTOMOD_ENHANCED_PROVIDER_ID,
633
+ contractVersion: AUTOMOD_ENHANCED_CONTRACT_VERSION,
634
+ result: operation.result,
635
+ ...(evidenceReference ? { evidenceReference } : {}),
636
+ });
637
+ }
638
+ function deliverHostedLeave(worker, input) {
639
+ if (!worker)
640
+ throw new BotControlTransportError(503, "Hosted server moderation delivery is unavailable");
641
+ return worker.deliverOperation(input);
642
+ }
643
+ function refreshHostedOwner(ownerObserver, client, guildId) {
644
+ if (!client)
645
+ throw new BotControlTransportError(503, "Hosted server owner refresh is unavailable");
646
+ const guild = client.guilds.cache.get(guildId);
647
+ if (!guild)
648
+ throw new BotControlTransportError(404, "The hosted server is not installed");
649
+ return ownerObserver.refreshOwner(guild);
650
+ }
573
651
  function health(identity, client, status, eventLoopDelayNanoseconds, queueDepth) {
574
652
  return {
575
653
  shardId: identity.shardId,
@@ -0,0 +1,39 @@
1
+ import { type ViolationEvidenceReference } from "@helyx/sdk";
2
+ export interface TransientViolationEvidenceStoreLimits {
3
+ entries?: number;
4
+ totalBytes?: number;
5
+ }
6
+ export type TransientEvidenceReadResult = {
7
+ outcome: "available";
8
+ content: string;
9
+ } | {
10
+ outcome: "unavailable";
11
+ } | {
12
+ outcome: "mismatch";
13
+ };
14
+ export type TransientViolationEvidenceCaptureInput = (Omit<Extract<ViolationEvidenceReference, {
15
+ source: "discord_auto_moderation";
16
+ }>, "key"> & {
17
+ content: string;
18
+ }) | (Omit<Extract<ViolationEvidenceReference, {
19
+ source: "enhanced_scanner";
20
+ }>, "key"> & {
21
+ content: string;
22
+ });
23
+ /**
24
+ * Bot-private, memory-only hand-off for message content. The opaque reference is
25
+ * safe to cross module boundaries; the content and this vault are not.
26
+ */
27
+ export declare class TransientViolationEvidenceStore {
28
+ #private;
29
+ private readonly now;
30
+ private readonly createKey;
31
+ constructor(now?: () => number, createKey?: () => string, limits?: TransientViolationEvidenceStoreLimits);
32
+ capture(input: TransientViolationEvidenceCaptureInput): ViolationEvidenceReference | null;
33
+ consume(reference: ViolationEvidenceReference): TransientEvidenceReadResult;
34
+ /** Read without discarding so a failed Discord send can be retried. */
35
+ read(reference: ViolationEvidenceReference): TransientEvidenceReadResult;
36
+ discard(reference: ViolationEvidenceReference): void;
37
+ get size(): number;
38
+ }
39
+ //# sourceMappingURL=transient-violation-evidence.d.ts.map