@helyx/bot 1.5.2 → 1.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/command-root-catalogue.json +452 -0
  3. package/dist/cached-message-deletion-authority.d.ts +27 -0
  4. package/dist/cached-message-deletion-authority.js +288 -0
  5. package/dist/channel-audit-configuration.d.ts +11 -1
  6. package/dist/channel-audit-configuration.js +28 -6
  7. package/dist/channel-audit-control.js +42 -2
  8. package/dist/channel-audit-permissions.js +76 -3
  9. package/dist/cloud-command-policy-sync.d.ts +26 -0
  10. package/dist/cloud-command-policy-sync.js +198 -0
  11. package/dist/cloud-connector.d.ts +6 -1
  12. package/dist/cloud-connector.js +224 -53
  13. package/dist/cloud-control-dispatcher.js +18 -0
  14. package/dist/command-policy-runtime.d.ts +23 -0
  15. package/dist/command-policy-runtime.js +53 -0
  16. package/dist/command-root-catalogue.d.ts +5 -0
  17. package/dist/command-root-catalogue.js +16 -0
  18. package/dist/control-error-mapping.d.ts +8 -0
  19. package/dist/control-error-mapping.js +60 -0
  20. package/dist/control-errors.d.ts +3 -1
  21. package/dist/control-errors.js +4 -1
  22. package/dist/control-health-routes.d.ts +10 -0
  23. package/dist/control-health-routes.js +28 -0
  24. package/dist/control-server-schemas.d.ts +7 -0
  25. package/dist/control-server-schemas.js +9 -0
  26. package/dist/control-server.d.ts +27 -6
  27. package/dist/control-server.js +103 -122
  28. package/dist/dashboard-feature-control.d.ts +2 -1
  29. package/dist/dashboard-feature-control.js +1 -0
  30. package/dist/dashboard-logging.d.ts +20 -0
  31. package/dist/dashboard-logging.js +39 -0
  32. package/dist/dashboard-module-projection.d.ts +1 -1
  33. package/dist/dashboard-module-projection.js +5 -2
  34. package/dist/dashboard-preview-import-routes.js +8 -0
  35. package/dist/database-bootstrap.d.ts +1 -1
  36. package/dist/database-bootstrap.js +4 -3
  37. package/dist/discord-channel-audit.js +1 -0
  38. package/dist/discord-user-identities.d.ts +14 -0
  39. package/dist/discord-user-identities.js +64 -0
  40. package/dist/emergency-deletion-dispatcher.d.ts +68 -0
  41. package/dist/emergency-deletion-dispatcher.js +502 -0
  42. package/dist/emergency-deletion-rest-transport.d.ts +24 -0
  43. package/dist/emergency-deletion-rest-transport.js +253 -0
  44. package/dist/events.d.ts +5 -0
  45. package/dist/events.js +53 -10
  46. package/dist/guarded-discord-dispatcher.d.ts +29 -0
  47. package/dist/guarded-discord-dispatcher.js +204 -0
  48. package/dist/guild-command-discord-client.d.ts +2 -0
  49. package/dist/guild-command-discord-client.js +7 -0
  50. package/dist/guild-command-projection.d.ts +4 -1
  51. package/dist/guild-command-projection.js +25 -0
  52. package/dist/guild-command-read-service.d.ts +10 -1
  53. package/dist/guild-command-read-service.js +37 -0
  54. package/dist/guild-command-worker.d.ts +19 -4
  55. package/dist/guild-command-worker.js +356 -26
  56. package/dist/guild-message-protection-author-roles.d.ts +7 -0
  57. package/dist/guild-message-protection-author-roles.js +93 -0
  58. package/dist/guild-message-protection-deletion.d.ts +21 -0
  59. package/dist/guild-message-protection-deletion.js +138 -0
  60. package/dist/guild-message-protection-delivery.d.ts +41 -0
  61. package/dist/guild-message-protection-delivery.js +223 -0
  62. package/dist/guild-message-update-metadata.d.ts +32 -0
  63. package/dist/guild-message-update-metadata.js +255 -0
  64. package/dist/guilds.d.ts +2 -1
  65. package/dist/guilds.js +11 -1
  66. package/dist/hosted-server-moderation.d.ts +2 -2
  67. package/dist/index.js +68 -4
  68. package/dist/interactions.d.ts +3 -1
  69. package/dist/interactions.js +77 -12
  70. package/dist/invite-rollup-maintenance.d.ts +9 -0
  71. package/dist/invite-rollup-maintenance.js +12 -0
  72. package/dist/managed-resource-authorization.d.ts +15 -0
  73. package/dist/managed-resource-authorization.js +37 -0
  74. package/dist/managed-resource-create-defaults.d.ts +21 -0
  75. package/dist/managed-resource-create-defaults.js +83 -0
  76. package/dist/managed-resource-query.d.ts +6 -0
  77. package/dist/managed-resource-query.js +15 -0
  78. package/dist/member-role-ids.d.ts +17 -0
  79. package/dist/member-role-ids.js +66 -0
  80. package/dist/message-protection-admission.d.ts +60 -0
  81. package/dist/message-protection-admission.js +279 -0
  82. package/dist/message-protection-cold-data.d.ts +42 -0
  83. package/dist/message-protection-cold-data.js +180 -0
  84. package/dist/message-protection-coordinator.d.ts +73 -0
  85. package/dist/message-protection-coordinator.js +622 -0
  86. package/dist/message-protection-settings.d.ts +40 -0
  87. package/dist/message-protection-settings.js +275 -0
  88. package/dist/message-protection-summary.d.ts +8 -0
  89. package/dist/message-protection-summary.js +98 -0
  90. package/dist/module-availability-control.js +20 -12
  91. package/dist/module-logging.d.ts +3 -2
  92. package/dist/module-logging.js +6 -1
  93. package/dist/modules.js +56 -1
  94. package/dist/mysql-runtime-persistence.d.ts +1 -0
  95. package/dist/mysql-runtime-persistence.js +4 -1
  96. package/dist/nexus-command-recovery-control-routes.d.ts +5 -0
  97. package/dist/nexus-command-recovery-control-routes.js +48 -0
  98. package/dist/nexus-command-references.d.ts +6 -0
  99. package/dist/nexus-command-references.js +33 -0
  100. package/dist/scheduled-task-runner.js +23 -4
  101. package/dist/shard-command-startup.d.ts +13 -0
  102. package/dist/shard-command-startup.js +27 -0
  103. package/dist/shard-contracts.d.ts +84 -0
  104. package/dist/shard-contracts.js +33 -0
  105. package/dist/shard-core-readiness.d.ts +5 -0
  106. package/dist/shard-core-readiness.js +19 -0
  107. package/dist/shard-router.d.ts +3 -0
  108. package/dist/shard-router.js +10 -1
  109. package/dist/shard-supervisor.d.ts +1 -1
  110. package/dist/shard-supervisor.js +124 -43
  111. package/dist/shard-worker.js +25 -15
  112. package/documentation/README.md +1 -0
  113. package/documentation/examples/.env.example +0 -45
  114. package/documentation/guides/self-hosting.md +54 -0
  115. package/package.json +18 -17
package/dist/guilds.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { MysqlPool } from "@helyx/database-mysql";
3
3
  import type { ModuleDefinition } from "@helyx/sdk";
4
4
  import { type Client } from "discord.js";
5
5
  import type { InteractionLogger } from "./interactions.js";
6
+ import type { MessageProtectionGuildObserver } from "./message-protection-coordinator.js";
6
7
  import type { HostedBotInviteGate, HostedGuildOwnerObserver, HostedGuildSafety } from "./hosted-server-moderation.js";
7
8
  export interface GuildReconciliationHealth {
8
9
  status: "pending" | "running" | "healthy" | "failed" | "stopped";
@@ -42,6 +43,6 @@ export interface GuildTrackingPersistence extends ReconciliationRepository {
42
43
  markGuildRemoved(guildId: string, ownership: GuildShardOwnership | undefined, applicationId: string | undefined): Promise<boolean>;
43
44
  }
44
45
  export declare function createMysqlGuildTrackingPersistence(database: MysqlPool): Promise<GuildTrackingPersistence>;
45
- export declare function installGuildTracking(client: Client, database: DatabasePool | GuildTrackingPersistence, modules: readonly ModuleDefinition[], logger: InteractionLogger, enabledByDefaultModuleIds?: ReadonlySet<string>, ownership?: GuildShardOwnership, reconciliationIntervalMs?: number, shardCount?: number, applicationId?: string, hostedSafety?: HostedGuildSafety, hostedInviteGate?: HostedBotInviteGate, hostedOwnerObserver?: HostedGuildOwnerObserver | undefined): GuildTrackingPreparation;
46
+ export declare function installGuildTracking(client: Client, database: DatabasePool | GuildTrackingPersistence, modules: readonly ModuleDefinition[], logger: InteractionLogger, enabledByDefaultModuleIds?: ReadonlySet<string>, ownership?: GuildShardOwnership, reconciliationIntervalMs?: number, shardCount?: number, applicationId?: string, hostedSafety?: HostedGuildSafety, hostedInviteGate?: HostedBotInviteGate, hostedOwnerObserver?: HostedGuildOwnerObserver | undefined, protectionObserver?: MessageProtectionGuildObserver): GuildTrackingPreparation;
46
47
  export {};
47
48
  //# sourceMappingURL=guilds.d.ts.map
package/dist/guilds.js CHANGED
@@ -105,7 +105,7 @@ export async function createMysqlGuildTrackingPersistence(database) {
105
105
  },
106
106
  };
107
107
  }
108
- export function installGuildTracking(client, database, modules, logger, enabledByDefaultModuleIds = new Set(), ownership, reconciliationIntervalMs = DEFAULT_RECONCILIATION_INTERVAL_MS, shardCount, applicationId, hostedSafety, hostedInviteGate, hostedOwnerObserver = hostedSafety) {
108
+ export function installGuildTracking(client, database, modules, logger, enabledByDefaultModuleIds = new Set(), ownership, reconciliationIntervalMs = DEFAULT_RECONCILIATION_INTERVAL_MS, shardCount, applicationId, hostedSafety, hostedInviteGate, hostedOwnerObserver = hostedSafety, protectionObserver) {
109
109
  if (!Number.isSafeInteger(reconciliationIntervalMs) ||
110
110
  reconciliationIntervalMs < 60_000)
111
111
  throw new Error("Guild reconciliation interval must be at least one minute");
@@ -140,6 +140,7 @@ export function installGuildTracking(client, database, modules, logger, enabledB
140
140
  return operation;
141
141
  }
142
142
  async function syncGuild(guild, firstObservation = false) {
143
+ const accepted = protectionObserver?.beforeSync(guild);
143
144
  const cachedOwner = guild.members?.cache.get(guild.ownerId)?.user ??
144
145
  guild.client?.users.cache.get(guild.ownerId);
145
146
  await persistence.syncGuild({
@@ -156,6 +157,7 @@ export function installGuildTracking(client, database, modules, logger, enabledB
156
157
  });
157
158
  if (stopped)
158
159
  return;
160
+ accepted?.();
159
161
  if (cachedOwner && hostedOwnerObserver)
160
162
  await hostedOwnerObserver.observeOwner(guild);
161
163
  else
@@ -260,6 +262,7 @@ export function installGuildTracking(client, database, modules, logger, enabledB
260
262
  client.on(Events.GuildDelete, (guild) => {
261
263
  if (stopped)
262
264
  return;
265
+ protectionObserver?.invalidateGuild(guild.id);
263
266
  const retry = admissionRetries.get(guild.id);
264
267
  if (retry)
265
268
  clearTimeout(retry);
@@ -281,11 +284,13 @@ export function installGuildTracking(client, database, modules, logger, enabledB
281
284
  });
282
285
  });
283
286
  client.on(Events.GuildUnavailable, (guild) => {
287
+ protectionObserver?.invalidateGuild(guild.id);
284
288
  logger.warn({ event: "guild.unavailable", guildId: guild.id }, "Discord guild is temporarily unavailable; installation remains active");
285
289
  });
286
290
  async function runReconciliation(reason) {
287
291
  if (stopped || !prepared || !client.isReady())
288
292
  return;
293
+ protectionObserver?.ready();
289
294
  if (reconciliation)
290
295
  return reconciliation;
291
296
  reconciliation = (async () => {
@@ -423,12 +428,17 @@ export function installGuildTracking(client, database, modules, logger, enabledB
423
428
  client.on(Events.ShardResume, () => {
424
429
  void runReconciliation("resume");
425
430
  });
431
+ if (protectionObserver) {
432
+ client.on(Events.ShardDisconnect, () => protectionObserver.invalidateAll());
433
+ client.on(Events.ShardReconnecting, () => protectionObserver.invalidateAll());
434
+ }
426
435
  prepare.reconcile = runReconciliation;
427
436
  prepare.health = () => ({ ...health });
428
437
  prepare.stop = () => {
429
438
  if (stopPromise)
430
439
  return stopPromise;
431
440
  stopped = true;
441
+ protectionObserver?.invalidateAll();
432
442
  if (timer)
433
443
  clearInterval(timer);
434
444
  timer = undefined;
@@ -10,9 +10,9 @@ declare const resultSchema: z.ZodObject<{
10
10
  blacklist: "blacklist";
11
11
  }>;
12
12
  outcome: z.ZodEnum<{
13
+ failed: "failed";
13
14
  completed: "completed";
14
15
  requires_review: "requires_review";
15
- failed: "failed";
16
16
  queued: "queued";
17
17
  not_currently_installed: "not_currently_installed";
18
18
  accepted_for_retry: "accepted_for_retry";
@@ -45,7 +45,7 @@ export declare class HostedServerModerationClient implements DeveloperServerMode
45
45
  idempotencyKey: string;
46
46
  }): Promise<{
47
47
  operationId: string;
48
- outcome: "completed" | "requires_review" | "failed" | "queued" | "not_currently_installed" | "accepted_for_retry" | "already_blacklisted";
48
+ outcome: "failed" | "completed" | "requires_review" | "queued" | "not_currently_installed" | "accepted_for_retry" | "already_blacklisted";
49
49
  paidSlotsReleased: number;
50
50
  }>;
51
51
  }
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { parseBotEnvironment } from "@helyx/config";
2
2
  import { StatusHeartbeatReporter } from "@helyx/status-client";
3
+ import { admitStandaloneCommandPolicy, } from "./command-policy-runtime.js";
3
4
  import { safeError, ServiceRegistry } from "@helyx/core";
4
5
  import { checkDatabaseConnection, createDatabasePool, EckoOperationsRepository, InstallationRepository, inspectConfiguredModuleMigrations, inspectCoreMigrations, inspectCreatorOnboardingMigrations, runCreatorOnboardingMigrations, PremiumAllocationRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
5
6
  import { createRuntime } from "@helyx/framework";
6
7
  import { assertRepositoryEngineSupported } from "@helyx/database/adapters";
7
- import { createPlatformServices, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, PlatformMemberRoleGrantOwnershipService, PostgresMemberRoleGrantOwnershipStore, } from "@helyx/platform";
8
- import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
8
+ import { createPlatformServices, createPostgresCommandRestrictionRecovery, createMysqlCommandRestrictionRecovery, createPostgresNexusCommandRecovery, createMysqlNexusCommandRecovery, createPostgresCommandCatalogueRecovery, createMysqlCommandCatalogueRecovery, createPostgresCommandPolicyDelivery, createMysqlCommandPolicyDelivery, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, PlatformMemberRoleGrantOwnershipService, PostgresMemberRoleGrantOwnershipStore, } from "@helyx/platform";
9
+ import { HELYX_SERVICE_NAMES, collectReservedCommandNames, } from "@helyx/sdk";
9
10
  import { Client, Events, GatewayIntentBits, Partials } from "discord.js";
10
11
  import pino from "pino";
11
12
  import { randomUUID } from "node:crypto";
@@ -15,6 +16,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
15
16
  import { ClientCloudDiscordDirectory, CloudConnectorController, } from "./cloud-connector.js";
16
17
  import { createCloudManagementModule } from "./cloud-management-module.js";
17
18
  import { installInteractions } from "./interactions.js";
19
+ import { loadBundledCommandCatalogue } from "./command-root-catalogue.js";
18
20
  import { installGuildTracking } from "./guilds.js";
19
21
  import { assertConfiguredModuleDatabaseCompatibility, loadConfiguredModulePackages, platformServiceOptions, } from "./modules.js";
20
22
  import { resolveModulePackageSpecifiers } from "./module-configuration.js";
@@ -150,6 +152,8 @@ function assertNoPendingMigrations(scope, pending) {
150
152
  }
151
153
  async function startUnshardedBot(processEnvironment) {
152
154
  const environment = parseBotEnvironment(processEnvironment);
155
+ const unshardedSupervisorId = randomUUID();
156
+ let commandPolicyRuntime;
153
157
  const mysqlModulePackages = environment.DATABASE_ENGINE === "mysql"
154
158
  ? await loadConfiguredModulePackages(await resolveModulePackageSpecifiers(environment.HELYX_MODULE_PACKAGES, undefined, undefined, environment.HELYX_MODULE_CATALOGUE_PATH, environment.HELYX_RUNTIME_PROFILE))
155
159
  : undefined;
@@ -157,6 +161,19 @@ async function startUnshardedBot(processEnvironment) {
157
161
  ? await openMysqlBotRuntimePersistence({
158
162
  environment,
159
163
  modulePackages: mysqlModulePackages,
164
+ ...(environment.HELYX_RUNTIME_PROFILE === "self-hosted"
165
+ ? {
166
+ onCoreReady: async (pool) => {
167
+ commandPolicyRuntime = await admitStandaloneCommandPolicy(createMysqlCommandPolicyDelivery(pool, {
168
+ applicationId: environment.DISCORD_CLIENT_ID,
169
+ scope: {
170
+ mode: "standalone",
171
+ instanceId: unshardedSupervisorId,
172
+ },
173
+ }));
174
+ },
175
+ }
176
+ : {}),
160
177
  })
161
178
  : undefined;
162
179
  const database = mysql
@@ -220,6 +237,11 @@ async function startUnshardedBot(processEnvironment) {
220
237
  ? await inspectRequiredCoreMigrations(database)
221
238
  : await runCoreMigrations(database)
222
239
  : mysql.database.migrations.core;
240
+ if (database && environment.HELYX_RUNTIME_PROFILE === "self-hosted")
241
+ commandPolicyRuntime = await admitStandaloneCommandPolicy(createPostgresCommandPolicyDelivery(database, {
242
+ applicationId: environment.DISCORD_CLIENT_ID,
243
+ scope: { mode: "standalone", instanceId: unshardedSupervisorId },
244
+ }));
223
245
  bootstrapLogger.info({
224
246
  event: "database.ready",
225
247
  appliedMigrations: migrationResult.applied,
@@ -302,6 +324,8 @@ async function startUnshardedBot(processEnvironment) {
302
324
  coreVersion: readBotVersion(),
303
325
  logger: bootstrapLogger,
304
326
  });
327
+ if (commandPolicyRuntime)
328
+ await cloudConnector.setCommandPolicyDelivery(commandPolicyRuntime);
305
329
  const permissionsModule = createPermissionsModule(featureModules);
306
330
  const managementModule = createManagementModule(featureModules, guildResources);
307
331
  const diagnosticInstallations = mysql
@@ -363,7 +387,6 @@ async function startUnshardedBot(processEnvironment) {
363
387
  ];
364
388
  let scheduledTasks;
365
389
  let guildCommandWorker;
366
- const unshardedSupervisorId = randomUUID();
367
390
  const deployedCommit = deploymentReference(processEnvironment);
368
391
  const buildHealth = async () => {
369
392
  let databaseHealthy = true;
@@ -454,9 +477,21 @@ async function startUnshardedBot(processEnvironment) {
454
477
  ...publicModules,
455
478
  ...(operationsModule ? [operationsModule] : []),
456
479
  ];
480
+ const commandCatalogue = modules.some(({ manifest }) => manifest.id === "helyx.nexus-core")
481
+ ? mysql
482
+ ? createMysqlCommandCatalogueRecovery(mysql.pool, environment.DISCORD_CLIENT_ID)
483
+ : createPostgresCommandCatalogueRecovery(database, environment.DISCORD_CLIENT_ID)
484
+ : undefined;
485
+ if (commandCatalogue)
486
+ await commandCatalogue.publish(await loadBundledCommandCatalogue());
457
487
  const platformServices = new Map();
458
488
  const serviceAccess = new ServiceRegistry(platformServices);
459
- const serviceOptions = platformServiceOptions(environment, publicModules);
489
+ const serviceOptions = {
490
+ ...platformServiceOptions(environment, publicModules),
491
+ ...(commandPolicyRuntime
492
+ ? { commandPolicyDeliveryScope: commandPolicyRuntime.owner.scope }
493
+ : {}),
494
+ };
460
495
  const platformFactory = mysql
461
496
  ? mysql.createPlatformServices(publicModules, {
462
497
  ...serviceOptions,
@@ -575,6 +610,7 @@ async function startUnshardedBot(processEnvironment) {
575
610
  : undefined;
576
611
  const commandsReady = installInteractions(client, modules, {
577
612
  globalCommandMode: environment.HELYX_GLOBAL_COMMAND_MODE,
613
+ resolveGuildCommandName: (input) => guildCommandReads.resolveInteractionCommand(input),
578
614
  ...(environment.DISCORD_INTERNAL_GUILD_ID
579
615
  ? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
580
616
  : {}),
@@ -719,6 +755,17 @@ async function startUnshardedBot(processEnvironment) {
719
755
  });
720
756
  scheduledTasks.start();
721
757
  guildCommandWorker = new GuildCommandWorker({
758
+ ...(commandPolicyRuntime
759
+ ? { commandPolicyDelivery: commandPolicyRuntime.delivery }
760
+ : {}),
761
+ ...(commandCatalogue ? { commandCatalogue } : {}),
762
+ ...(modules.some(({ manifest }) => manifest.id === "helyx.nexus-core")
763
+ ? {
764
+ commandRestrictions: mysql
765
+ ? createMysqlCommandRestrictionRecovery(mysql.pool, environment.DISCORD_CLIENT_ID)
766
+ : createPostgresCommandRestrictionRecovery(database, environment.DISCORD_CLIENT_ID),
767
+ }
768
+ : {}),
722
769
  ...(mysql
723
770
  ? { repositories: mysql.commandWorker }
724
771
  : { database: database }),
@@ -778,6 +825,22 @@ async function startUnshardedBot(processEnvironment) {
778
825
  : undefined;
779
826
  controlServer = await createBotControlServer({
780
827
  service: controlService,
828
+ ...(environment.HELYX_RUNTIME_PROFILE === "hosted" &&
829
+ modules.some(({ manifest }) => manifest.id === "helyx.nexus-core")
830
+ ? {
831
+ nexusCommandRecovery: mysql
832
+ ? createMysqlNexusCommandRecovery(mysql.pool, {
833
+ applicationId: environment.DISCORD_CLIENT_ID,
834
+ persistedCommandCatalogue: true,
835
+ reservedCommandNames: collectReservedCommandNames(modules),
836
+ })
837
+ : createPostgresNexusCommandRecovery(database, {
838
+ applicationId: environment.DISCORD_CLIENT_ID,
839
+ persistedCommandCatalogue: true,
840
+ reservedCommandNames: collectReservedCommandNames(modules),
841
+ }),
842
+ }
843
+ : {}),
781
844
  secret: environment.HELYX_BOT_CONTROL_SECRET,
782
845
  ...(hostedLeaveWorker && hostedOwnerObserver
783
846
  ? {
@@ -893,6 +956,7 @@ export function gatewayIntentsForModules(modules, messageContentEnabled = false,
893
956
  messageContentEnabled) ||
894
957
  modules.some((module) => module.events?.guildMessageCreate?.length ||
895
958
  module.events?.guildMessageUpdate?.length ||
959
+ module.events?.guildMessageUpdateMetadata?.length ||
896
960
  module.events?.messageDelete?.length ||
897
961
  module.events?.guildMessageDeleteBulk?.length)) {
898
962
  intents.push(GatewayIntentBits.GuildMessages);
@@ -1,6 +1,7 @@
1
1
  import type { CommandAutocompleteContext, CommandAuthorization, CommandContribution, MessageResponse, ModalDefinition, ModuleDefinition, ServiceAccess } from "@helyx/sdk";
2
2
  import { type GlobalCommandRegistrationMode } from "./command-registration-policy.js";
3
3
  import { ModalBuilder, type AutocompleteInteraction, type ChatInputCommandInteraction, type Client, type InteractionReplyOptions, type InteractionUpdateOptions } from "discord.js";
4
+ import type { GuildCommandInteractionRoute } from "./guild-command-read-service.js";
4
5
  export declare const GLOBAL_MODULE_UNAVAILABLE_MESSAGE = "This module is temporarily unavailable for all hosted servers.";
5
6
  export declare const AUTOCOMPLETE_RESPONSE_DEADLINE_MS = 2000;
6
7
  export declare const DISCORD_DNS_REFRESH_INTERVAL_MS = 30000;
@@ -16,6 +17,7 @@ export interface CommandRegistration {
16
17
  synchroniseInternalGuild?: boolean;
17
18
  globalCommandMode?: GlobalCommandRegistrationMode;
18
19
  selfAuthorizingGuildModuleIds?: ReadonlySet<string>;
20
+ resolveGuildCommandName?(input: GuildCommandInteractionRoute): Promise<string | null>;
19
21
  }
20
22
  export declare function installInteractions(client: Client, modules: readonly ModuleDefinition[], registration: CommandRegistration, logger: InteractionLogger, services: ServiceAccess): Promise<void>;
21
23
  export declare function warmDiscordRestConnection(client: Client<true>, logger: InteractionLogger): Promise<void>;
@@ -44,5 +46,5 @@ export declare function normalizeDiscordPermission(permission: string): string;
44
46
  export declare function toReplyOptions(response: MessageResponse): InteractionReplyOptions;
45
47
  export declare function toUpdateOptions(response: MessageResponse): InteractionUpdateOptions;
46
48
  export declare function toModal(definition: ModalDefinition): ModalBuilder;
47
- export declare function safeInteractionFailureMessage(error: unknown): string;
49
+ export declare function safeInteractionFailureMessage(error: unknown, nexusRoot?: string | null): string;
48
50
  //# sourceMappingURL=interactions.d.ts.map
@@ -1,5 +1,7 @@
1
1
  import { lookup as lookupDns } from "node:dns/promises";
2
- import { HELYX_SERVICE_NAMES, NexusEconomyError, UserFacingInteractionError, } from "@helyx/sdk";
2
+ import { HELYX_SERVICE_NAMES, formatNexusCommandReference, NexusEconomyError, UserFacingInteractionError, } from "@helyx/sdk";
3
+ import { nexusObservedRoot } from "./nexus-command-references.js";
4
+ import { snapshotMemberRoleIds } from "./member-role-ids.js";
3
5
  import { canUseCommand, safeError } from "@helyx/core";
4
6
  import { commandRegistrationPolicy, contributionRegistrationPolicy, } from "./command-registration-policy.js";
5
7
  import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, EmbedBuilder, Events, GuildMember, ApplicationIntegrationType, ChannelType, InteractionContextType, LabelBuilder, MessageFlags, MentionableSelectMenuBuilder, ModalBuilder, RoleSelectMenuBuilder, Routes, StringSelectMenuBuilder, SlashCommandBuilder, TextInputBuilder, TextInputStyle, UserSelectMenuBuilder, TextDisplayBuilder, PermissionFlagsBits, escapeMarkdown, } from "discord.js";
@@ -92,12 +94,44 @@ export function installInteractions(client, modules, registration, logger, servi
92
94
  client.on(Events.InteractionCreate, (interaction) => {
93
95
  void handleInteraction(interaction);
94
96
  });
97
+ function requiresGuildRootResolution(interaction) {
98
+ // Only known non-Nexus application globals are independent of guild root
99
+ // mappings. A newly loaded guild root can collide with an old observation.
100
+ return Boolean(registration.resolveGuildCommandName &&
101
+ (interaction.commandName === "nexus" ||
102
+ !commands.has(interaction.commandName) ||
103
+ interaction.commandGuildId !== null));
104
+ }
105
+ async function resolveRegisteredCommand(interaction) {
106
+ if (!registration.resolveGuildCommandName)
107
+ return commands.get(interaction.commandName);
108
+ try {
109
+ const canonicalName = await registration.resolveGuildCommandName({
110
+ applicationId: interaction.applicationId,
111
+ guildId: interaction.guildId,
112
+ commandGuildId: interaction.commandGuildId,
113
+ commandId: interaction.commandId,
114
+ commandName: interaction.commandName,
115
+ });
116
+ return canonicalName === null ? undefined : commands.get(canonicalName);
117
+ }
118
+ catch (error) {
119
+ logger.warn({
120
+ event: "interaction.command_route_unavailable",
121
+ ...interactionLogContext(interaction),
122
+ error: safeError(error),
123
+ }, "Observed guild command routing is unavailable");
124
+ return undefined;
125
+ }
126
+ }
95
127
  async function handleInteraction(interaction) {
96
128
  try {
97
129
  if (interaction.isAutocomplete()) {
98
130
  const response = createAutocompleteResponseController(interaction, logger);
99
131
  try {
100
- const registered = commands.get(interaction.commandName);
132
+ const registered = requiresGuildRootResolution(interaction)
133
+ ? await resolveRegisteredCommand(interaction)
134
+ : commands.get(interaction.commandName);
101
135
  const groupName = registered?.contribution.subcommandGroups?.length
102
136
  ? interaction.options.getSubcommandGroup(false)
103
137
  : null;
@@ -136,7 +170,9 @@ export function installInteractions(client, modules, registration, logger, servi
136
170
  return;
137
171
  }
138
172
  else if (interaction.isChatInputCommand()) {
139
- const registered = commands.get(interaction.commandName);
173
+ const registered = requiresGuildRootResolution(interaction)
174
+ ? await resolveRegisteredCommand(interaction)
175
+ : commands.get(interaction.commandName);
140
176
  if (!registered) {
141
177
  logger.warn({
142
178
  event: "interaction.stale_command",
@@ -277,7 +313,7 @@ export function installInteractions(client, modules, registration, logger, servi
277
313
  ...interactionLogContext(interaction),
278
314
  error: safeError(error),
279
315
  }, "Module interaction failed");
280
- await sendFailure(interaction, error);
316
+ await sendFailure(interaction, error, services);
281
317
  }
282
318
  }
283
319
  return registrationReady;
@@ -580,7 +616,13 @@ function createContext(interaction, services) {
580
616
  userId: interaction.user.id,
581
617
  userDisplayName: interaction.user.globalName ?? interaction.user.username,
582
618
  userRoleIds: interaction.member instanceof GuildMember
583
- ? [...interaction.member.roles.cache.keys()]
619
+ ? snapshotMemberRoleIds({
620
+ member: interaction.member,
621
+ client: interaction.client,
622
+ guild: interaction.guild,
623
+ guildId: interaction.guildId,
624
+ actorUserId: interaction.user.id,
625
+ })
584
626
  : (interaction.member?.roles ?? []),
585
627
  ...(interaction.member instanceof GuildMember
586
628
  ? { userHighestRolePosition: interaction.member.roles.highest.position }
@@ -749,7 +791,13 @@ function createModalContext(interaction, services) {
749
791
  userId: interaction.user.id,
750
792
  userDisplayName: interaction.user.globalName ?? interaction.user.username,
751
793
  userRoleIds: interaction.member instanceof GuildMember
752
- ? [...interaction.member.roles.cache.keys()]
794
+ ? snapshotMemberRoleIds({
795
+ member: interaction.member,
796
+ client: interaction.client,
797
+ guild: interaction.guild,
798
+ guildId: interaction.guildId,
799
+ actorUserId: interaction.user.id,
800
+ })
753
801
  : (interaction.member?.roles ?? []),
754
802
  ...(interaction.member instanceof GuildMember
755
803
  ? { userHighestRolePosition: interaction.member.roles.highest.position }
@@ -1012,7 +1060,13 @@ function permissionActor(interaction) {
1012
1060
  return {
1013
1061
  userId: interaction.user.id,
1014
1062
  roleIds: interaction.member instanceof GuildMember
1015
- ? [...interaction.member.roles.cache.keys()]
1063
+ ? snapshotMemberRoleIds({
1064
+ member: interaction.member,
1065
+ client: interaction.client,
1066
+ guild: interaction.guild,
1067
+ guildId: interaction.guildId,
1068
+ actorUserId: interaction.user.id,
1069
+ })
1016
1070
  : (interaction.member?.roles ?? []),
1017
1071
  permissions: interaction.memberPermissions
1018
1072
  ?.toArray()
@@ -1346,15 +1400,19 @@ function discordChannelType(type) {
1346
1400
  return ChannelType.GuildMedia;
1347
1401
  return ChannelType.GuildText;
1348
1402
  }
1349
- export function safeInteractionFailureMessage(error) {
1403
+ export function safeInteractionFailureMessage(error, nexusRoot = "nexus") {
1350
1404
  if (error instanceof UserFacingInteractionError) {
1405
+ const redeem = formatNexusCommandReference(nexusRoot, "redeem", true);
1406
+ const reopen = redeem
1407
+ ? `Run ${redeem} again.`
1408
+ : "Reopen the redeem command from Discord's command picker.";
1351
1409
  const messages = {
1352
1410
  nexus_exchange_slow: "The Exchange is taking too long to respond. Try again shortly.",
1353
1411
  nexus_exchange_item_unavailable: "That Exchange item is not available to you right now.",
1354
1412
  nexus_exchange_item_changed: "That Exchange item changed or is no longer available. Start again.",
1355
1413
  nexus_exchange_balance_changed: "Your Nexus balance changed after the preview. Start again.",
1356
- nexus_exchange_form_expired: "This request form expired. Run /nexus redeem again.",
1357
- nexus_exchange_form_replayed: "This request form was already submitted. Run /nexus redeem again.",
1414
+ nexus_exchange_form_expired: `This request form expired. ${reopen}`,
1415
+ nexus_exchange_form_replayed: `This request form was already submitted. ${reopen}`,
1358
1416
  nexus_exchange_preview_expired: "This redemption preview expired. Start the command again.",
1359
1417
  nexus_exchange_invalid_request: "This Exchange request is invalid. Start the command again.",
1360
1418
  nexus_exchange_response_required: "This custom request needs a response.",
@@ -1380,14 +1438,21 @@ export function safeInteractionFailureMessage(error) {
1380
1438
  }
1381
1439
  return "Something went wrong while handling that interaction.";
1382
1440
  }
1383
- async function sendFailure(interaction, error) {
1441
+ async function sendFailure(interaction, error, services) {
1384
1442
  if (interaction.isAutocomplete()) {
1385
1443
  await interaction.respond([]).catch(() => undefined);
1386
1444
  return;
1387
1445
  }
1388
1446
  if (!interaction.isRepliable())
1389
1447
  return;
1390
- const message = safeInteractionFailureMessage(error);
1448
+ const needsRoot = error instanceof UserFacingInteractionError &&
1449
+ ["nexus_exchange_form_expired", "nexus_exchange_form_replayed"].includes(error.code);
1450
+ const root = needsRoot
1451
+ ? interaction.guildId
1452
+ ? await nexusObservedRoot(services, interaction.guildId)
1453
+ : null
1454
+ : "nexus";
1455
+ const message = safeInteractionFailureMessage(error, root);
1391
1456
  if (interaction.replied || interaction.deferred) {
1392
1457
  await interaction.editReply({ content: message }).catch(() => undefined);
1393
1458
  return;
@@ -0,0 +1,9 @@
1
+ import type { ModuleDefinition } from "@helyx/sdk";
2
+ /** Private historical-work exception; never grants the refresh/provider lane. */
3
+ export declare function isInviteRollupMaintenanceTask(task: {
4
+ moduleId: string;
5
+ taskKind: string;
6
+ taskVersion: number;
7
+ delivery: string;
8
+ }, module: ModuleDefinition | undefined): boolean;
9
+ //# sourceMappingURL=invite-rollup-maintenance.d.ts.map
@@ -0,0 +1,12 @@
1
+ /** Private historical-work exception; never grants the refresh/provider lane. */
2
+ export function isInviteRollupMaintenanceTask(task, module) {
3
+ return Boolean(task.moduleId === "helyx.invite-tracking" &&
4
+ task.taskKind === "invite-tracking.finalized-rollup" &&
5
+ task.taskVersion === 1 &&
6
+ task.delivery === "one-shot" &&
7
+ module?.manifest.id === task.moduleId &&
8
+ module.scheduledTasks?.some((contribution) => contribution.kind === task.taskKind &&
9
+ contribution.version === task.taskVersion &&
10
+ contribution.delivery === task.delivery));
11
+ }
12
+ //# sourceMappingURL=invite-rollup-maintenance.js.map
@@ -0,0 +1,15 @@
1
+ import type { ManagedResourceContext, ModuleDefinition, ModuleManagedResource, PermissionActor } from "@helyx/sdk";
2
+ import type { ManagedResourceOperation } from "./control-permissions.js";
3
+ /** Explicit, target-bound delegation after the ordinary permission has denied. */
4
+ export declare function authorizeManagedResourceContext(input: {
5
+ module: ModuleDefinition;
6
+ resource: ModuleManagedResource;
7
+ resourceType: string;
8
+ resourceId?: string;
9
+ actionId?: string;
10
+ operation: ManagedResourceOperation;
11
+ permissionId: string;
12
+ authorize(permissionId: string): Promise<PermissionActor>;
13
+ createContext(actor: PermissionActor, scope: "full" | "owner"): ManagedResourceContext;
14
+ }): Promise<ManagedResourceContext>;
15
+ //# sourceMappingURL=managed-resource-authorization.d.ts.map
@@ -0,0 +1,37 @@
1
+ import { BotControlForbiddenError } from "./control-errors.js";
2
+ /** Explicit, target-bound delegation after the ordinary permission has denied. */
3
+ export async function authorizeManagedResourceContext(input) {
4
+ let actor;
5
+ try {
6
+ actor = await input.authorize(input.permissionId);
7
+ }
8
+ catch (error) {
9
+ if (!(error instanceof BotControlForbiddenError))
10
+ throw error;
11
+ const declaration = input.module.manifest.dashboard?.resources?.find(({ id }) => id === input.resourceType);
12
+ const ownerPermissionId = input.operation === "update"
13
+ ? declaration?.ownerPermissions?.update
14
+ : input.operation === "action"
15
+ ? declaration?.actions?.find(({ id }) => id === input.actionId)
16
+ ?.ownerPermissionId
17
+ : undefined;
18
+ if (!ownerPermissionId ||
19
+ !input.resourceId ||
20
+ !input.resource.resolveOwnerUserId)
21
+ throw error;
22
+ actor = await input.authorize(ownerPermissionId);
23
+ const context = input.createContext(actor, "owner");
24
+ let ownerUserId;
25
+ try {
26
+ ownerUserId = await input.resource.resolveOwnerUserId(context, input.resourceId);
27
+ }
28
+ catch {
29
+ throw new BotControlForbiddenError();
30
+ }
31
+ if (!ownerUserId || ownerUserId !== actor.userId)
32
+ throw new BotControlForbiddenError();
33
+ return context;
34
+ }
35
+ return input.createContext(actor, "full");
36
+ }
37
+ //# sourceMappingURL=managed-resource-authorization.js.map
@@ -0,0 +1,21 @@
1
+ import type { ManagedResourceContext, ManagedResourceValue, ModuleDefinition, ModuleManagedResource } from "@helyx/sdk";
2
+ export interface ManagedResourceCreateDefaultsInput {
3
+ guildId: string;
4
+ moduleId: string;
5
+ resourceType: string;
6
+ actorUserId: string;
7
+ correlationId: string;
8
+ }
9
+ export interface ManagedResourceCreateDefaultsControl {
10
+ getManagedResourceCreateDefaults?(input: ManagedResourceCreateDefaultsInput): Promise<{
11
+ value: Readonly<Record<string, ManagedResourceValue>>;
12
+ }>;
13
+ }
14
+ export declare function readManagedResourceCreateDefaults(input: {
15
+ module: ModuleDefinition;
16
+ resource: ModuleManagedResource;
17
+ context: ManagedResourceContext;
18
+ }): Promise<{
19
+ value: Readonly<Record<string, ManagedResourceValue>>;
20
+ }>;
21
+ //# sourceMappingURL=managed-resource-create-defaults.d.ts.map
@@ -0,0 +1,83 @@
1
+ import { z } from "zod";
2
+ import { BotControlNotFoundError, BotControlValidationError, } from "./control-errors.js";
3
+ import { managedResourceObject } from "./control-server-schemas.js";
4
+ import { requireDashboardFeatures, resourceFieldFeatureIds, validateManagedResourceInput, isBoundedManagedResourceValue, } from "./dashboard-feature-control.js";
5
+ const optionalUrl = z
6
+ .string()
7
+ .max(2048)
8
+ .refine((value) => value === "" || /^https?:\/\//u.test(value));
9
+ const embed = z
10
+ .object({
11
+ title: z.string().max(256).optional(),
12
+ description: z.string().max(4000).optional(),
13
+ url: optionalUrl.optional(),
14
+ imageUrl: optionalUrl.optional(),
15
+ thumbnailUrl: optionalUrl.optional(),
16
+ color: z.number().int().min(0).max(0xffffff).optional(),
17
+ timestamp: z.string().max(100).optional(),
18
+ author: z
19
+ .object({
20
+ name: z.string().max(256).optional(),
21
+ url: optionalUrl.optional(),
22
+ iconUrl: optionalUrl.optional(),
23
+ })
24
+ .strict()
25
+ .optional(),
26
+ footer: z
27
+ .object({
28
+ text: z.string().max(2048).optional(),
29
+ iconUrl: optionalUrl.optional(),
30
+ })
31
+ .strict()
32
+ .optional(),
33
+ fields: z
34
+ .array(z
35
+ .object({
36
+ name: z.string().max(256),
37
+ value: z.string().max(1024),
38
+ inline: z.boolean().optional(),
39
+ })
40
+ .strict())
41
+ .max(25)
42
+ .optional(),
43
+ })
44
+ .strict();
45
+ function validatePartialFields(fields, values) {
46
+ for (const [key, value] of Object.entries(values)) {
47
+ const field = fields.find((candidate) => candidate.key === key);
48
+ if (!field)
49
+ throw new BotControlValidationError("Creation defaults contain an undeclared field");
50
+ if (field.type === "list" || field.type === "discord-embeds") {
51
+ if (!Array.isArray(value) ||
52
+ value.length < field.minItems ||
53
+ value.length > field.maxItems)
54
+ throw new BotControlValidationError("Creation defaults contain an invalid list");
55
+ for (const item of value) {
56
+ if (field.type === "discord-embeds") {
57
+ embed.parse(item);
58
+ continue;
59
+ }
60
+ const entry = managedResourceObject.parse(item);
61
+ validateManagedResourceInput(field.fields, entry);
62
+ }
63
+ }
64
+ else
65
+ validateManagedResourceInput([{ ...field, required: false }], {
66
+ [key]: value,
67
+ });
68
+ }
69
+ }
70
+ export async function readManagedResourceCreateDefaults(input) {
71
+ const declaration = input.module.manifest.dashboard?.resources?.find(({ id }) => id === input.resource.id);
72
+ if (declaration?.createDefaults !== true || !input.resource.createDefaults)
73
+ throw new BotControlNotFoundError("Creation defaults are not declared by this module");
74
+ await requireDashboardFeatures(input.context.services, input.context.guildId, input.module.manifest.id, [declaration.featureId]);
75
+ const result = await input.resource.createDefaults(input.context);
76
+ if (!isBoundedManagedResourceValue(result))
77
+ throw new BotControlValidationError("Creation defaults contain unsafe or unbounded values");
78
+ const value = managedResourceObject.parse(result);
79
+ validatePartialFields(declaration.fields, value);
80
+ await requireDashboardFeatures(input.context.services, input.context.guildId, input.module.manifest.id, resourceFieldFeatureIds(declaration.fields, value));
81
+ return { value };
82
+ }
83
+ //# sourceMappingURL=managed-resource-create-defaults.js.map
@@ -0,0 +1,6 @@
1
+ import type { ModuleDefinition } from "@helyx/sdk";
2
+ export declare function validateManagedResourceQuery(module: ModuleDefinition, resourceType: string, input: {
3
+ search?: string;
4
+ filters?: Readonly<Record<string, string>>;
5
+ }): void;
6
+ //# sourceMappingURL=managed-resource-query.d.ts.map