@helyx/bot 1.3.1 → 1.5.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 (97) hide show
  1. package/CHANGELOG.md +100 -28
  2. package/README.md +65 -11
  3. package/dist/channel-audit-configuration.d.ts +7 -0
  4. package/dist/channel-audit-configuration.js +40 -0
  5. package/dist/channel-audit-control.d.ts +14 -0
  6. package/dist/channel-audit-control.js +61 -0
  7. package/dist/channel-audit-permissions.d.ts +56 -0
  8. package/dist/channel-audit-permissions.js +417 -0
  9. package/dist/channel-audit-routes.d.ts +4 -0
  10. package/dist/channel-audit-routes.js +42 -0
  11. package/dist/cloud-connector.d.ts +11 -4
  12. package/dist/cloud-connector.js +54 -15
  13. package/dist/cloud-control-dispatcher.d.ts +8 -3
  14. package/dist/cloud-control-dispatcher.js +84 -4
  15. package/dist/control-errors.d.ts +12 -0
  16. package/dist/control-errors.js +32 -0
  17. package/dist/control-server-schemas.d.ts +84 -0
  18. package/dist/control-server-schemas.js +101 -0
  19. package/dist/control-server.d.ts +25 -8
  20. package/dist/control-server.js +101 -152
  21. package/dist/core-retention.d.ts +26 -0
  22. package/dist/core-retention.js +49 -0
  23. package/dist/creator-guild-join-service.d.ts +25 -0
  24. package/dist/creator-guild-join-service.js +218 -0
  25. package/dist/creator-invite-service.d.ts +37 -0
  26. package/dist/creator-invite-service.js +686 -0
  27. package/dist/creator-onboarding-control.d.ts +12 -0
  28. package/dist/creator-onboarding-control.js +65 -0
  29. package/dist/creator-onboarding-platform.d.ts +20 -0
  30. package/dist/creator-onboarding-platform.js +58 -0
  31. package/dist/dashboard-action-control.d.ts +2 -2
  32. package/dist/dashboard-action-control.js +2 -3
  33. package/dist/database-bootstrap.d.ts +36 -0
  34. package/dist/database-bootstrap.js +101 -0
  35. package/dist/developer-operations-module.d.ts +1 -0
  36. package/dist/developer-operations-module.js +34 -16
  37. package/dist/discord-channel-audit.d.ts +9 -0
  38. package/dist/discord-channel-audit.js +195 -0
  39. package/dist/discord-guild-health.d.ts +2 -2
  40. package/dist/discord-owned-message-service.d.ts +1 -0
  41. package/dist/discord-owned-message-service.js +3 -0
  42. package/dist/discord-role-safety.d.ts +4 -0
  43. package/dist/discord-role-safety.js +30 -0
  44. package/dist/events.d.ts +2 -0
  45. package/dist/events.js +5 -20
  46. package/dist/guild-command-read-service.d.ts +8 -3
  47. package/dist/guild-command-read-service.js +14 -7
  48. package/dist/guild-command-release.d.ts +15 -8
  49. package/dist/guild-command-release.js +10 -3
  50. package/dist/guild-command-worker.d.ts +17 -4
  51. package/dist/guild-command-worker.js +17 -6
  52. package/dist/guilds.d.ts +24 -3
  53. package/dist/guilds.js +185 -64
  54. package/dist/hosted-server-moderation.js +10 -4
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.js +200 -82
  57. package/dist/interactions.js +16 -3
  58. package/dist/module-configuration.d.ts +1 -1
  59. package/dist/module-configuration.js +5 -2
  60. package/dist/module-logging.d.ts +8 -3
  61. package/dist/module-logging.js +13 -7
  62. package/dist/modules.d.ts +1 -0
  63. package/dist/modules.js +8 -1
  64. package/dist/mysql-runtime-persistence.d.ts +46 -0
  65. package/dist/mysql-runtime-persistence.js +91 -0
  66. package/dist/oauth-module.js +2 -2
  67. package/dist/oauth-service.d.ts +7 -0
  68. package/dist/oauth-service.js +3 -0
  69. package/dist/scheduled-task-runner.d.ts +9 -85
  70. package/dist/scheduled-task-runner.js +1 -1
  71. package/dist/shard-contracts.d.ts +222 -0
  72. package/dist/shard-contracts.js +52 -1
  73. package/dist/shard-router.d.ts +4 -1
  74. package/dist/shard-router.js +19 -0
  75. package/dist/shard-supervisor.js +43 -9
  76. package/dist/shard-worker.d.ts +5 -2
  77. package/dist/shard-worker.js +41 -5
  78. package/dist/supervisor-cloud-directory.d.ts +9 -3
  79. package/dist/supervisor-cloud-directory.js +8 -12
  80. package/dist/trivia-data-provider.d.ts +11 -0
  81. package/dist/trivia-data-provider.js +46 -26
  82. package/dist/verification-service.d.ts +2 -2
  83. package/dist/verification-service.js +6 -3
  84. package/documentation/README.md +5 -3
  85. package/documentation/examples/.env.example +15 -11
  86. package/documentation/guides/getting-started.md +29 -12
  87. package/documentation/guides/privacy-and-support.md +6 -5
  88. package/documentation/guides/self-hosting.md +16 -11
  89. package/documentation/modules/auto-moderation.md +7 -5
  90. package/documentation/modules/building-a-module.md +48 -2
  91. package/documentation/modules/moderation.md +5 -5
  92. package/documentation/modules/publishing-a-module.md +6 -4
  93. package/documentation/modules/testing-a-module.md +39 -4
  94. package/documentation/reference/environment.md +43 -9
  95. package/documentation/runbooks/backup-and-recovery.md +26 -7
  96. package/documentation/runbooks/installation-and-upgrades.md +97 -27
  97. package/package.json +20 -16
@@ -13,116 +13,23 @@ import { canAccessModule, PREMIUM_MODULE_UNAVAILABLE_MESSAGE, } from "./premium-
13
13
  import { completeFirstTimeCommandSetup, } from "./first-time-command-setup.js";
14
14
  import { SHARD_HEALTH_STALE_MS, } from "./shard-contracts.js";
15
15
  import { assertVersion, declaredManagedResourcePermission, fromDashboardPolicy, managedResourcePermissionId, permissionPoliciesEqual, permissionPolicyKey, safeOptionalVersion, safeVersion, toDashboardPolicy, } from "./control-permissions.js";
16
- import { assertPermissionMutationAllowed, BotControlForbiddenError, } from "./control-errors.js";
16
+ import { assertPermissionMutationAllowed, BotControlForbiddenError, BotControlNotFoundError, BotControlValidationError, BotControlTransportError, isUnknownGuildError, isUnknownMemberError, } from "./control-errors.js";
17
+ export { BotControlTransportError } from "./control-errors.js";
17
18
  import { dashboardCommands } from "./dashboard-commands.js";
19
+ import { registerChannelAuditRoutes } from "./channel-audit-routes.js";
20
+ import { createChannelAuditControl } from "./channel-audit-control.js";
21
+ import { executeCreatorOnboarding, registerCreatorOnboardingControlRoute, } from "./creator-onboarding-control.js";
18
22
  import { mapDashboardModule, } from "./dashboard-module-projection.js";
19
23
  import { availabilityCatalogueDefinitions, availabilityCatalogue, dashboardModuleStatus, disabledModuleSummary, moduleAvailabilityService, moduleAvailabilityStates, statusCatalogueDefinitions, } from "./module-availability-control.js";
20
24
  import { registerGuildCommandControlRoutes } from "./guild-command-control-routes.js";
21
25
  import { createPrivacyProviderRegistry, describePrivacyProviders, } from "./privacy-provider-registry.js";
22
- import { changedSettingFeatureIds, configurationPreviewFeatureIds, hasBoundedManagedResourceBytes, isBoundedManagedResourceValue, managedResourceColumnKeys, normalizeManagedResourceDetail, normalizeManagedResourcePage, normalizeManagedResourceRelationPage, projectManagedResourceObject, requireDashboardFeatures, resourceFieldFeatureIds, scalarInputFeatureIds, validateManagedResourceInput, } from "./dashboard-feature-control.js";
26
+ import { changedSettingFeatureIds, configurationPreviewFeatureIds, managedResourceColumnKeys, normalizeManagedResourceDetail, normalizeManagedResourcePage, normalizeManagedResourceRelationPage, projectManagedResourceObject, requireDashboardFeatures, resourceFieldFeatureIds, scalarInputFeatureIds, validateManagedResourceInput, } from "./dashboard-feature-control.js";
23
27
  import { registerDashboardPreviewImportRoutes, registerDashboardResourceMutationRoutes, } from "./dashboard-preview-import-routes.js";
24
28
  import { executeDashboardAction as executeDeclaredDashboardAction, previewDashboardAction as previewDeclaredDashboardAction, } from "./dashboard-action-control.js";
25
29
  import { registerDashboardActionRoutes, } from "./dashboard-action-routes.js";
26
30
  import { registerEckoTransportRoutes, } from "./ecko-transport-routes.js";
27
31
  import { validateDiscordUserSettingSelections, validateDiscordSettingSelections, } from "./dashboard-discord-settings.js";
28
- const snowflake = z.string().regex(/^\d{17,20}$/u);
29
- const moduleId = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/u);
30
- const configurationValue = z.record(z.string(), z.union([
31
- z.null(),
32
- z.string(),
33
- z.boolean(),
34
- z.number().finite(),
35
- z.array(z.string()).max(100),
36
- ]));
37
- const permissionPreset = z.enum([
38
- "everyone",
39
- "moderators",
40
- "administrators",
41
- "custom",
42
- ]);
43
- const dashboardPermissionPolicy = z.object({
44
- preset: permissionPreset,
45
- allowedRoleIds: z.array(snowflake).max(100),
46
- deniedRoleIds: z.array(snowflake).max(100),
47
- });
48
- const permissionState = z.object({
49
- version: z.number().int().min(0),
50
- module: dashboardPermissionPolicy,
51
- commands: z
52
- .array(z.object({
53
- commandId: z.string().min(1).max(100),
54
- version: z.number().int().min(0),
55
- enabled: z.boolean(),
56
- policy: dashboardPermissionPolicy,
57
- }))
58
- .max(100),
59
- });
60
- const guildAccessPolicy = z
61
- .object({
62
- administratorRoleIds: z.array(snowflake).max(25),
63
- moderatorRoleIds: z.array(snowflake).max(25),
64
- dashboardModeratorsEnabled: z.boolean(),
65
- dashboardAllowedRoleIds: z.array(snowflake).max(25),
66
- dashboardDeniedRoleIds: z.array(snowflake).max(25),
67
- })
68
- .strict();
69
- const mutationContext = z.object({
70
- actorUserId: snowflake,
71
- correlationId: z.uuid(),
72
- });
73
- const resourceId = z.string().trim().min(1).max(100);
74
- const resourceCursor = z.string().min(1).max(2_048);
75
- const resourceActionId = z.string().regex(/^[a-z][a-z0-9_-]*$/u);
76
- const managedResourceFilters = z
77
- .record(z.string().regex(/^[a-z][a-zA-Z0-9_]*$/u), z.string().trim().min(1).max(100))
78
- .refine((value) => Object.keys(value).length <= 8);
79
- const managedResourceValue = z.custom((value) => isBoundedManagedResourceValue(value), "Managed resource value exceeds the transport limits");
80
- const managedResourceObject = z
81
- .record(z.string().min(1).max(100), managedResourceValue)
82
- .refine(hasBoundedManagedResourceBytes);
83
- const managedResourceSummarySchema = z
84
- .object({
85
- id: resourceId,
86
- revision: z.number().int().min(0),
87
- status: z.string().trim().min(1).max(100),
88
- title: z.string().trim().min(1).max(200),
89
- description: z.string().max(500).optional(),
90
- attributes: managedResourceObject.optional(),
91
- updatedAt: z.iso.datetime({ offset: true }),
92
- })
93
- .strict();
94
- const managedResourceDetailSchema = managedResourceSummarySchema
95
- .extend({
96
- value: managedResourceObject,
97
- availableActionIds: z.array(z.string().min(1).max(100)).max(50).optional(),
98
- })
99
- .strict()
100
- .refine(hasBoundedManagedResourceBytes);
101
- const managedResourcePageSchema = z
102
- .object({
103
- items: z.array(managedResourceSummarySchema).max(50),
104
- nextCursor: resourceCursor.optional(),
105
- total: z.number().int().min(0).optional(),
106
- })
107
- .strict()
108
- .refine(hasBoundedManagedResourceBytes);
109
- const managedResourceRelationPageSchema = z
110
- .object({
111
- items: z
112
- .array(z
113
- .object({
114
- id: resourceId,
115
- value: managedResourceObject,
116
- })
117
- .strict())
118
- .max(50),
119
- nextCursor: resourceCursor.optional(),
120
- })
121
- .strict()
122
- .refine(hasBoundedManagedResourceBytes);
123
- const managedResourceDeleteResultSchema = z
124
- .object({ deleted: z.boolean() })
125
- .strict();
32
+ import { configurationValue, guildAccessPolicy, managedResourceDeleteResultSchema, managedResourceDetailSchema, managedResourceFilters, managedResourceObject, managedResourcePageSchema, managedResourceRelationPageSchema, moduleId, mutationContext, permissionState, resourceActionId, resourceCursor, resourceId, snowflake, } from "./control-server-schemas.js";
126
33
  export class DiscordGuildResourceDirectory {
127
34
  client;
128
35
  constructor(client) {
@@ -265,16 +172,37 @@ export class DiscordGuildResourceDirectory {
265
172
  await sendDiscordMessageToChannel(this.client, channel.id, response, notificationId);
266
173
  }
267
174
  }
268
- export class BotControlTransportError extends Error {
269
- status;
270
- constructor(status, message) {
271
- super(message);
272
- this.status = status;
273
- this.name = "BotControlTransportError";
274
- }
175
+ function createPostgresControlRepositories(connection) {
176
+ return {
177
+ installations: new InstallationRepository(connection),
178
+ accessPolicies: new GuildAccessPolicyRepository(connection),
179
+ loggingPolicies: new ModuleLoggingPolicyRepository(connection),
180
+ permissionPolicies: new PermissionPolicyRepository(connection),
181
+ audit: new AuditRepository(connection),
182
+ };
183
+ }
184
+ function createPostgresControlPersistence(database) {
185
+ return {
186
+ repositories: createPostgresControlRepositories(database),
187
+ withTransaction: (operation) => withTransaction(database, (connection) => operation(createPostgresControlRepositories(connection))),
188
+ };
189
+ }
190
+ export async function createMysqlBotControlPersistence(pool) {
191
+ const mysql = await import("@helyx/database-mysql");
192
+ const repositories = (connection) => ({
193
+ installations: new mysql.InstallationRepository(connection),
194
+ accessPolicies: new mysql.GuildAccessPolicyRepository(connection),
195
+ loggingPolicies: new mysql.ModuleLoggingPolicyRepository(connection),
196
+ permissionPolicies: new mysql.PermissionPolicyRepository(connection),
197
+ audit: new mysql.AuditRepository(connection),
198
+ });
199
+ return {
200
+ engine: "mysql",
201
+ repositories: repositories(pool),
202
+ withTransaction: (operation) => mysql.withMysqlTransaction(pool, (connection) => operation(repositories(mysql.createMysqlQueryExecutor(connection)))),
203
+ };
275
204
  }
276
205
  export class BotControlService {
277
- database;
278
206
  resources;
279
207
  supportNotifications;
280
208
  presenceOwnership;
@@ -288,12 +216,18 @@ export class BotControlService {
288
216
  #permissions;
289
217
  #services;
290
218
  #privacyProviders;
291
- constructor(database, modules, resources, services, privacyExecutor, supportNotifications, presenceOwnership, presenceShardCount, statusModules) {
292
- this.database = database;
219
+ #persistence;
220
+ readChannelAuditConfiguration;
221
+ checkChannelAudit;
222
+ constructor(databaseOrPersistence, modules, resources, services, privacyExecutor, supportNotifications, presenceOwnership, presenceShardCount, statusModules) {
293
223
  this.resources = resources;
294
224
  this.supportNotifications = supportNotifications;
295
225
  this.presenceOwnership = presenceOwnership;
296
226
  this.presenceShardCount = presenceShardCount;
227
+ this.#persistence =
228
+ "engine" in databaseOrPersistence
229
+ ? databaseOrPersistence
230
+ : createPostgresControlPersistence(databaseOrPersistence);
297
231
  this.#modules = new Map(modules.map((module) => [module.manifest.id, module]));
298
232
  this.#dashboardModules = modules
299
233
  .filter((module) => module.manifest.dashboard !== undefined)
@@ -305,6 +239,25 @@ export class BotControlService {
305
239
  this.#permissions = services.get(HELYX_SERVICE_NAMES.permissions);
306
240
  this.#services = services;
307
241
  this.#privacyProviders = createPrivacyProviderRegistry(modules, privacyExecutor, this.#services);
242
+ const audit = createChannelAuditControl({
243
+ modules: this.#modules,
244
+ resources,
245
+ services,
246
+ configurations: this.#configurations,
247
+ permissions: this.#permissions,
248
+ listModules: (guildId) => this.#persistence.repositories.installations.listModules(guildId),
249
+ readGuildPolicy: (guildId) => this.#readGuildAccessPolicy(guildId),
250
+ requireAccessible: (guildId, moduleId) => this.#requireModuleAccessible(guildId, moduleId),
251
+ });
252
+ this.readChannelAuditConfiguration = audit.readChannelAuditConfiguration;
253
+ this.checkChannelAudit = audit.checkChannelAudit;
254
+ }
255
+ async creatorOnboarding(input) {
256
+ return executeCreatorOnboarding(input, {
257
+ modules: this.#modules,
258
+ installations: this.#installations,
259
+ services: this.#services,
260
+ });
308
261
  }
309
262
  async cleanupOAuthConsumer(input) {
310
263
  const module = this.#modules.get(input.moduleId);
@@ -344,7 +297,7 @@ export class BotControlService {
344
297
  .filter(({ state }) => state === "not_installed")
345
298
  .map(({ guildId }) => guildId);
346
299
  if (absent.length > 0) {
347
- const repository = new InstallationRepository(this.database);
300
+ const repository = this.#persistence.repositories.installations;
348
301
  if (this.presenceOwnership)
349
302
  await repository.adoptGuildsForReconciliation({
350
303
  guildIds: absent,
@@ -439,8 +392,8 @@ export class BotControlService {
439
392
  ]);
440
393
  const [resources, installedModules, storedLoggingPolicies, runtime, storedConfigurations, storedPermissions, availabilityStates,] = await Promise.all([
441
394
  this.resources.getGuildResources(guildId),
442
- new InstallationRepository(this.database).listModules(guildId),
443
- new ModuleLoggingPolicyRepository(this.database).listForGuild(guildId),
395
+ this.#persistence.repositories.installations.listModules(guildId),
396
+ this.#persistence.repositories.loggingPolicies.listForGuild(guildId),
444
397
  this.#inspectRuntimeCapabilities(),
445
398
  this.#readConfigurations(guildId, moduleIds),
446
399
  this.#readPermissionPolicies(guildId, permissionTargets),
@@ -516,7 +469,7 @@ export class BotControlService {
516
469
  async getGuildAccessPolicy(input) {
517
470
  const [actor, record] = await Promise.all([
518
471
  this.resources.resolveActor(input.guildId, input.actorUserId),
519
- new GuildAccessPolicyRepository(this.database).get(input.guildId),
472
+ this.#persistence.repositories.accessPolicies.get(input.guildId),
520
473
  ]);
521
474
  const policy = record ?? DEFAULT_GUILD_ACCESS_POLICY;
522
475
  return {
@@ -547,8 +500,8 @@ export class BotControlService {
547
500
  ];
548
501
  if (roleIds.some((roleId) => !availableRoles.has(roleId)))
549
502
  throw new BotControlValidationError("Dashboard access contains a Discord role that is no longer available");
550
- const saved = await withTransaction(this.database, async (connection) => {
551
- const record = await new GuildAccessPolicyRepository(connection).save({
503
+ const saved = await this.#persistence.withTransaction(async (repositories) => {
504
+ const record = await repositories.accessPolicies.save({
552
505
  guildId: input.guildId,
553
506
  administratorRoleIds: [...input.policy.administratorRoleIds],
554
507
  moderatorRoleIds: [...input.policy.moderatorRoleIds],
@@ -558,7 +511,7 @@ export class BotControlService {
558
511
  expectedVersion: input.version === 0 ? null : BigInt(input.version),
559
512
  updatedBy: input.actorUserId,
560
513
  });
561
- await new AuditRepository(connection).append({
514
+ await repositories.audit.append({
562
515
  guildId: input.guildId,
563
516
  actorUserId: input.actorUserId,
564
517
  action: "guild.access.updated",
@@ -590,14 +543,14 @@ export class BotControlService {
590
543
  };
591
544
  }
592
545
  async #readGuildAccessPolicy(guildId) {
593
- return ((await new GuildAccessPolicyRepository(this.database).get(guildId)) ??
546
+ return ((await this.#persistence.repositories.accessPolicies.get(guildId)) ??
594
547
  DEFAULT_GUILD_ACCESS_POLICY);
595
548
  }
596
549
  async readPublicModuleProjection(input) {
597
550
  const module = this.#requireModule(input.moduleId);
598
551
  if (!module.publicProjection)
599
552
  throw new BotControlNotFoundError("Public module projection was not found");
600
- if (!(await new InstallationRepository(this.database).isModuleEnabled(input.guildId, input.moduleId)))
553
+ if (!(await this.#persistence.repositories.installations.isModuleEnabled(input.guildId, input.moduleId)))
601
554
  throw new BotControlValidationError("This module is not enabled");
602
555
  if (input.subjectUserId) {
603
556
  if (!this.#services.has(HELYX_SERVICE_NAMES.discordResources))
@@ -775,9 +728,9 @@ export class BotControlService {
775
728
  if (enabledChanged || !permissionPoliciesEqual(current.policy, target))
776
729
  assertPermissionMutationAllowed(canMutatePermissionPolicy(actor, current.policy, target, guildAccess, rolePositions));
777
730
  }
778
- await withTransaction(this.database, async (connection) => {
779
- const policies = new PermissionPolicyRepository(connection);
780
- const audit = new AuditRepository(connection);
731
+ await this.#persistence.withTransaction(async (repositories) => {
732
+ const policies = repositories.permissionPolicies;
733
+ const audit = repositories.audit;
781
734
  const savedModule = await policies.save({
782
735
  guildId: input.guildId,
783
736
  moduleId: input.moduleId,
@@ -799,6 +752,12 @@ export class BotControlService {
799
752
  for (const command of commands) {
800
753
  const requested = supplied.get(command.id);
801
754
  const current = currentCommands.get(command.id);
755
+ const target = requested.enabled
756
+ ? fromDashboardPolicy(requested.policy, current.policy)
757
+ : requestedModulePolicy;
758
+ const enabledChanged = requested.enabled === current.inherited;
759
+ if (!enabledChanged && permissionPoliciesEqual(current.policy, target))
760
+ continue;
802
761
  if (requested.enabled) {
803
762
  const saved = await policies.save({
804
763
  guildId: input.guildId,
@@ -870,7 +829,7 @@ export class BotControlService {
870
829
  if (!this.#modules.has("helyx.logging")) {
871
830
  throw new BotControlNotFoundError("Logging is not installed");
872
831
  }
873
- if (!(await new InstallationRepository(this.database).isModuleEnabled(input.guildId, "helyx.logging"))) {
832
+ if (!(await this.#persistence.repositories.installations.isModuleEnabled(input.guildId, "helyx.logging"))) {
874
833
  throw new BotControlValidationError("Enable Logging before changing activity events");
875
834
  }
876
835
  const source = this.#requireModule(input.moduleId);
@@ -878,8 +837,8 @@ export class BotControlService {
878
837
  if (!event) {
879
838
  throw new BotControlNotFoundError("Logging event is not declared by this module");
880
839
  }
881
- const saved = await withTransaction(this.database, async (connection) => {
882
- const policy = await new ModuleLoggingPolicyRepository(connection).save({
840
+ const saved = await this.#persistence.withTransaction(async (repositories) => {
841
+ const policy = await repositories.loggingPolicies.save({
883
842
  guildId: input.guildId,
884
843
  sourceModuleId: input.moduleId,
885
844
  eventId: input.eventId,
@@ -887,7 +846,7 @@ export class BotControlService {
887
846
  expectedVersion: input.expectedVersion === 0 ? null : BigInt(input.expectedVersion),
888
847
  updatedBy: input.actorUserId,
889
848
  });
890
- await new AuditRepository(connection).append({
849
+ await repositories.audit.append({
891
850
  guildId: input.guildId,
892
851
  actorUserId: input.actorUserId,
893
852
  action: "module.logging.updated",
@@ -917,7 +876,7 @@ export class BotControlService {
917
876
  }
918
877
  #dashboardActionControl() {
919
878
  return {
920
- database: this.database,
879
+ installations: this.#persistence.repositories.installations,
921
880
  services: this.#services,
922
881
  requireDashboardModule: (moduleIdValue) => this.#requireDashboardModule(moduleIdValue),
923
882
  requireModuleAccessible: (guildId, moduleIdValue) => this.#requireModuleAccessible(guildId, moduleIdValue),
@@ -1103,7 +1062,7 @@ export class BotControlService {
1103
1062
  }
1104
1063
  }
1105
1064
  async #requireEnabledManagedResource(guildId, moduleIdValue) {
1106
- if (!(await new InstallationRepository(this.database).isModuleEnabled(guildId, moduleIdValue)))
1065
+ if (!(await this.#persistence.repositories.installations.isModuleEnabled(guildId, moduleIdValue)))
1107
1066
  throw new BotControlValidationError("Enable this module before managing its resources");
1108
1067
  }
1109
1068
  async #managedResourceContext(input, module, operation, permissionId = managedResourcePermissionId(module, input.resourceType, operation, undefined, declaredManagedResourcePermission(module, input.resourceType, operation))) {
@@ -1309,6 +1268,7 @@ export async function createBotControlServer(input) {
1309
1268
  return reply.code(503).send({ status: "unavailable" });
1310
1269
  }
1311
1270
  server.get("/health", (_request, reply) => readiness(reply));
1271
+ registerCreatorOnboardingControlRoute(server, input.service);
1312
1272
  server.get("/v1/health", (_request, reply) => readiness(reply));
1313
1273
  server.get("/v1/runtime-configuration", (_request, reply) => reply.header("Cache-Control", "no-store").send({
1314
1274
  hostedModerationEnabled: input.runtimeConfiguration?.hostedModerationEnabled ?? false,
@@ -1549,6 +1509,7 @@ export async function createBotControlServer(input) {
1549
1509
  return input.service.setModuleStatus({ ...params, ...body });
1550
1510
  });
1551
1511
  registerDashboardActionRoutes(server, input.service);
1512
+ registerChannelAuditRoutes(server, input.service);
1552
1513
  server.get("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType", async (request) => {
1553
1514
  const params = z
1554
1515
  .object({
@@ -1716,49 +1677,37 @@ export function mapBotControlError(error) {
1716
1677
  return { status: 404, message: error.message };
1717
1678
  if (error instanceof BotControlForbiddenError)
1718
1679
  return { status: 403, message: error.message };
1719
- if (error instanceof OptimisticConcurrencyError)
1680
+ if (error instanceof OptimisticConcurrencyError ||
1681
+ (error instanceof Error &&
1682
+ error.name === "DatabaseFailure" &&
1683
+ "kind" in error &&
1684
+ error.kind === "conflict"))
1720
1685
  return {
1721
1686
  status: 409,
1722
1687
  message: "Configuration changed in another session. Reload and try again.",
1723
1688
  };
1724
- if (error instanceof GuildCommandRevisionConflictError)
1689
+ if (error instanceof GuildCommandRevisionConflictError ||
1690
+ (error instanceof Error &&
1691
+ error.name === "GuildCommandRevisionConflictError"))
1725
1692
  return {
1726
1693
  status: 409,
1727
1694
  message: "Command state changed in another session. Refresh and try again.",
1728
1695
  };
1729
- if (error instanceof GuildCommandActiveLeaseError)
1696
+ if (error instanceof GuildCommandActiveLeaseError ||
1697
+ (error instanceof Error && error.name === "GuildCommandActiveLeaseError"))
1730
1698
  return {
1731
1699
  status: 409,
1732
1700
  message: "A command build is already running. Wait for it to finish.",
1733
1701
  };
1734
- if (error instanceof GuildCommandSetupConflictError)
1702
+ if (error instanceof GuildCommandSetupConflictError ||
1703
+ (error instanceof Error && error.name === "GuildCommandSetupConflictError"))
1735
1704
  return {
1736
1705
  status: 409,
1737
1706
  message: "Command setup changed. Reload the page and try again.",
1738
1707
  };
1739
1708
  return undefined;
1740
1709
  }
1741
- class BotControlValidationError extends Error {
1742
- }
1743
- class BotControlNotFoundError extends Error {
1744
- constructor(message = "Discord server is not available to Helyx") {
1745
- super(message);
1746
- this.name = "BotControlNotFoundError";
1747
- }
1748
- }
1749
1710
  function digest(value) {
1750
1711
  return createHash("sha256").update(value).digest();
1751
1712
  }
1752
- function isUnknownGuildError(error) {
1753
- return (typeof error === "object" &&
1754
- error !== null &&
1755
- "code" in error &&
1756
- error.code === 10_004);
1757
- }
1758
- function isUnknownMemberError(error) {
1759
- return (typeof error === "object" &&
1760
- error !== null &&
1761
- "code" in error &&
1762
- error.code === 10_007);
1763
- }
1764
1713
  //# sourceMappingURL=control-server.js.map
@@ -0,0 +1,26 @@
1
+ import { AuditRepository, GuildCommandRepository, HostedServerSafetyRepository, InteractionSessionRepository, RateLimitRepository, type DatabasePool } from "@helyx/database";
2
+ import type { GuildCommandRepository as MysqlGuildCommandRepository, InteractionSessionRepository as MysqlInteractionSessionRepository, MysqlPool, RateLimitRepository as MysqlRateLimitRepository } from "@helyx/database-mysql";
3
+ import type { Logger } from "@helyx/sdk";
4
+ export interface CoreRetentionRepositories {
5
+ sessions: Pick<InteractionSessionRepository, "deleteExpired"> & Pick<MysqlInteractionSessionRepository, "deleteExpired">;
6
+ rateLimits: Pick<RateLimitRepository, "deleteExpired"> & Pick<MysqlRateLimitRepository, "deleteExpired">;
7
+ commands: Pick<GuildCommandRepository, "deleteExpired"> & Pick<MysqlGuildCommandRepository, "deleteExpired">;
8
+ verificationAudit?: Pick<AuditRepository, "deleteModuleEventsOlderThanMonths">;
9
+ hostedLeaveDeliveries?: Pick<HostedServerSafetyRepository, "deleteTerminalLeaveDeliveriesOlderThanMonths">;
10
+ }
11
+ export interface CoreRetentionCounts {
12
+ interactionSessions: number;
13
+ rateLimitWindows: number;
14
+ verificationAuditEvents: number;
15
+ commandRecords: number;
16
+ hostedLeaveDeliveries: number;
17
+ }
18
+ /** One cleanup pass; callers retain ownership of scheduling and error logging. */
19
+ export declare function cleanupExpiredCoreRecords(repositories: CoreRetentionRepositories, logger: Pick<Logger, "info">): Promise<CoreRetentionCounts>;
20
+ /** Hosted cleanup remains on by default, matching the PostgreSQL runtime. */
21
+ export declare function createPostgresCoreRetentionRepositories(database: DatabasePool, options?: {
22
+ hostedCleanup?: boolean;
23
+ }): CoreRetentionRepositories;
24
+ /** Ordinary self-host cleanup needs only the selected MySQL-compatible DB. */
25
+ export declare function createMysqlCoreRetentionRepositories(database: MysqlPool): Promise<CoreRetentionRepositories>;
26
+ //# sourceMappingURL=core-retention.d.ts.map
@@ -0,0 +1,49 @@
1
+ import { AuditRepository, GuildCommandRepository, HostedServerSafetyRepository, InteractionSessionRepository, RateLimitRepository, } from "@helyx/database";
2
+ /** One cleanup pass; callers retain ownership of scheduling and error logging. */
3
+ export async function cleanupExpiredCoreRecords(repositories, logger) {
4
+ const [sessions, rateLimits, verificationAudit, commandRecords, hostedLeaves,] = await Promise.all([
5
+ repositories.sessions.deleteExpired(),
6
+ repositories.rateLimits.deleteExpired(),
7
+ repositories.verificationAudit?.deleteModuleEventsOlderThanMonths("helyx.verification", 12) ?? 0,
8
+ repositories.commands.deleteExpired(12),
9
+ repositories.hostedLeaveDeliveries?.deleteTerminalLeaveDeliveriesOlderThanMonths(24) ?? 0,
10
+ ]);
11
+ const counts = {
12
+ interactionSessions: sessions,
13
+ rateLimitWindows: rateLimits,
14
+ verificationAuditEvents: verificationAudit,
15
+ commandRecords: commandRecords.history +
16
+ commandRecords.idempotencyKeys +
17
+ commandRecords.creationLedger +
18
+ commandRecords.requests,
19
+ hostedLeaveDeliveries: hostedLeaves,
20
+ };
21
+ if (Object.values(counts).some((count) => count > 0)) {
22
+ logger.info({ event: "database.retention_cleaned", ...counts }, "Expired core records removed");
23
+ }
24
+ return counts;
25
+ }
26
+ /** Hosted cleanup remains on by default, matching the PostgreSQL runtime. */
27
+ export function createPostgresCoreRetentionRepositories(database, options = {}) {
28
+ return {
29
+ sessions: new InteractionSessionRepository(database),
30
+ rateLimits: new RateLimitRepository(database),
31
+ commands: new GuildCommandRepository(database),
32
+ ...(options.hostedCleanup === false
33
+ ? {}
34
+ : {
35
+ verificationAudit: new AuditRepository(database),
36
+ hostedLeaveDeliveries: new HostedServerSafetyRepository(database),
37
+ }),
38
+ };
39
+ }
40
+ /** Ordinary self-host cleanup needs only the selected MySQL-compatible DB. */
41
+ export async function createMysqlCoreRetentionRepositories(database) {
42
+ const { InteractionSessionRepository: MysqlSessions, RateLimitRepository: MysqlRateLimits, GuildCommandRepository: MysqlCommands, } = await import("@helyx/database-mysql");
43
+ return {
44
+ sessions: new MysqlSessions(database),
45
+ rateLimits: new MysqlRateLimits(database),
46
+ commands: new MysqlCommands(database),
47
+ };
48
+ }
49
+ //# sourceMappingURL=core-retention.js.map
@@ -0,0 +1,25 @@
1
+ import { type CreatorGuildJoinService, type ModuleDefinition, type ServiceAccess } from "@helyx/sdk";
2
+ import { type Client } from "discord.js";
3
+ import type { CreatorInviteRepository } from "@helyx/database";
4
+ /** The only onboarding boundary which can add a member. Never supplies roles or other admission overrides. */
5
+ export declare class DiscordCreatorGuildJoinService implements CreatorGuildJoinService {
6
+ #private;
7
+ private readonly client;
8
+ private readonly services;
9
+ private readonly modules;
10
+ private readonly repository;
11
+ private readonly credentials;
12
+ constructor(client: Client, services: ServiceAccess, modules: readonly ModuleDefinition[], repository: CreatorInviteRepository, credentials: (input: Parameters<CreatorGuildJoinService["join"]>[0]) => Promise<{
13
+ accessToken: string;
14
+ applicationId: string;
15
+ }>);
16
+ inspect(input: {
17
+ guildId: string;
18
+ memberUserId?: string;
19
+ }): ReturnType<CreatorGuildJoinService["inspect"]>;
20
+ join(input: Parameters<CreatorGuildJoinService["join"]>[0]): ReturnType<CreatorGuildJoinService["join"]>;
21
+ private logJoined;
22
+ private enabled;
23
+ private readiness;
24
+ }
25
+ //# sourceMappingURL=creator-guild-join-service.d.ts.map