@helyx/bot 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +92 -28
  2. package/README.md +65 -11
  3. package/dist/cloud-connector.d.ts +11 -4
  4. package/dist/cloud-connector.js +52 -15
  5. package/dist/cloud-control-dispatcher.d.ts +8 -3
  6. package/dist/cloud-control-dispatcher.js +62 -4
  7. package/dist/control-errors.d.ts +4 -0
  8. package/dist/control-errors.js +8 -0
  9. package/dist/control-server-schemas.d.ts +84 -0
  10. package/dist/control-server-schemas.js +101 -0
  11. package/dist/control-server.d.ts +21 -7
  12. package/dist/control-server.js +88 -132
  13. package/dist/core-retention.d.ts +26 -0
  14. package/dist/core-retention.js +49 -0
  15. package/dist/creator-guild-join-service.d.ts +25 -0
  16. package/dist/creator-guild-join-service.js +218 -0
  17. package/dist/creator-invite-service.d.ts +37 -0
  18. package/dist/creator-invite-service.js +686 -0
  19. package/dist/creator-onboarding-control.d.ts +12 -0
  20. package/dist/creator-onboarding-control.js +65 -0
  21. package/dist/creator-onboarding-platform.d.ts +20 -0
  22. package/dist/creator-onboarding-platform.js +58 -0
  23. package/dist/dashboard-action-control.d.ts +2 -2
  24. package/dist/dashboard-action-control.js +2 -3
  25. package/dist/database-bootstrap.d.ts +36 -0
  26. package/dist/database-bootstrap.js +101 -0
  27. package/dist/developer-operations-module.d.ts +1 -0
  28. package/dist/developer-operations-module.js +34 -16
  29. package/dist/discord-owned-message-service.d.ts +1 -0
  30. package/dist/discord-owned-message-service.js +3 -0
  31. package/dist/guild-command-read-service.d.ts +8 -3
  32. package/dist/guild-command-read-service.js +14 -7
  33. package/dist/guild-command-release.d.ts +15 -8
  34. package/dist/guild-command-release.js +10 -3
  35. package/dist/guild-command-worker.d.ts +17 -4
  36. package/dist/guild-command-worker.js +17 -6
  37. package/dist/guilds.d.ts +24 -3
  38. package/dist/guilds.js +185 -64
  39. package/dist/hosted-server-moderation.d.ts +2 -2
  40. package/dist/hosted-server-moderation.js +10 -4
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.js +200 -81
  43. package/dist/interactions.js +16 -3
  44. package/dist/module-configuration.d.ts +1 -1
  45. package/dist/module-configuration.js +5 -2
  46. package/dist/module-logging.d.ts +8 -3
  47. package/dist/module-logging.js +13 -7
  48. package/dist/modules.d.ts +1 -0
  49. package/dist/modules.js +8 -1
  50. package/dist/mysql-runtime-persistence.d.ts +46 -0
  51. package/dist/mysql-runtime-persistence.js +91 -0
  52. package/dist/oauth-module.js +2 -2
  53. package/dist/oauth-service.d.ts +7 -0
  54. package/dist/oauth-service.js +3 -0
  55. package/dist/scheduled-task-runner.d.ts +9 -85
  56. package/dist/scheduled-task-runner.js +1 -1
  57. package/dist/shard-contracts.d.ts +135 -0
  58. package/dist/shard-contracts.js +13 -1
  59. package/dist/shard-router.d.ts +2 -1
  60. package/dist/shard-router.js +7 -0
  61. package/dist/shard-supervisor.js +43 -9
  62. package/dist/shard-worker.d.ts +5 -2
  63. package/dist/shard-worker.js +46 -5
  64. package/dist/supervisor-cloud-directory.d.ts +9 -3
  65. package/dist/supervisor-cloud-directory.js +8 -12
  66. package/dist/trivia-data-provider.d.ts +11 -0
  67. package/dist/trivia-data-provider.js +46 -26
  68. package/dist/verification-service.d.ts +2 -2
  69. package/dist/verification-service.js +6 -3
  70. package/documentation/README.md +5 -3
  71. package/documentation/examples/.env.example +15 -11
  72. package/documentation/guides/getting-started.md +29 -12
  73. package/documentation/guides/privacy-and-support.md +6 -5
  74. package/documentation/guides/self-hosting.md +16 -11
  75. package/documentation/modules/auto-moderation.md +7 -5
  76. package/documentation/modules/building-a-module.md +48 -2
  77. package/documentation/modules/moderation.md +5 -5
  78. package/documentation/modules/publishing-a-module.md +6 -4
  79. package/documentation/modules/testing-a-module.md +39 -4
  80. package/documentation/reference/environment.md +43 -9
  81. package/documentation/runbooks/backup-and-recovery.md +26 -7
  82. package/documentation/runbooks/installation-and-upgrades.md +97 -27
  83. package/package.json +20 -16
@@ -0,0 +1,101 @@
1
+ import { z } from "zod";
2
+ import { hasBoundedManagedResourceBytes, isBoundedManagedResourceValue, } from "./dashboard-feature-control.js";
3
+ export const snowflake = z.string().regex(/^\d{17,20}$/u);
4
+ export const moduleId = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/u);
5
+ export const configurationValue = z.record(z.string(), z.union([
6
+ z.null(),
7
+ z.string(),
8
+ z.boolean(),
9
+ z.number().finite(),
10
+ z.array(z.string()).max(100),
11
+ ]));
12
+ const permissionPreset = z.enum([
13
+ "everyone",
14
+ "moderators",
15
+ "administrators",
16
+ "custom",
17
+ ]);
18
+ const dashboardPermissionPolicy = z.object({
19
+ preset: permissionPreset,
20
+ allowedRoleIds: z.array(snowflake).max(100),
21
+ deniedRoleIds: z.array(snowflake).max(100),
22
+ });
23
+ export const permissionState = z.object({
24
+ version: z.number().int().min(0),
25
+ module: dashboardPermissionPolicy,
26
+ commands: z
27
+ .array(z.object({
28
+ commandId: z.string().min(1).max(100),
29
+ version: z.number().int().min(0),
30
+ enabled: z.boolean(),
31
+ policy: dashboardPermissionPolicy,
32
+ }))
33
+ .max(100),
34
+ });
35
+ export const guildAccessPolicy = z
36
+ .object({
37
+ administratorRoleIds: z.array(snowflake).max(25),
38
+ moderatorRoleIds: z.array(snowflake).max(25),
39
+ dashboardModeratorsEnabled: z.boolean(),
40
+ dashboardAllowedRoleIds: z.array(snowflake).max(25),
41
+ dashboardDeniedRoleIds: z.array(snowflake).max(25),
42
+ })
43
+ .strict();
44
+ export const mutationContext = z.object({
45
+ actorUserId: snowflake,
46
+ correlationId: z.uuid(),
47
+ });
48
+ export const resourceId = z.string().trim().min(1).max(100);
49
+ export const resourceCursor = z.string().min(1).max(2_048);
50
+ export const resourceActionId = z.string().regex(/^[a-z][a-z0-9_-]*$/u);
51
+ export const managedResourceFilters = z
52
+ .record(z.string().regex(/^[a-z][a-zA-Z0-9_]*$/u), z.string().trim().min(1).max(100))
53
+ .refine((value) => Object.keys(value).length <= 8);
54
+ const managedResourceValue = z.custom((value) => isBoundedManagedResourceValue(value), "Managed resource value exceeds the transport limits");
55
+ export const managedResourceObject = z
56
+ .record(z.string().min(1).max(100), managedResourceValue)
57
+ .refine(hasBoundedManagedResourceBytes);
58
+ const managedResourceSummarySchema = z
59
+ .object({
60
+ id: resourceId,
61
+ revision: z.number().int().min(0),
62
+ status: z.string().trim().min(1).max(100),
63
+ title: z.string().trim().min(1).max(200),
64
+ description: z.string().max(500).optional(),
65
+ attributes: managedResourceObject.optional(),
66
+ updatedAt: z.iso.datetime({ offset: true }),
67
+ })
68
+ .strict();
69
+ export const managedResourceDetailSchema = managedResourceSummarySchema
70
+ .extend({
71
+ value: managedResourceObject,
72
+ availableActionIds: z.array(z.string().min(1).max(100)).max(50).optional(),
73
+ })
74
+ .strict()
75
+ .refine(hasBoundedManagedResourceBytes);
76
+ export const managedResourcePageSchema = z
77
+ .object({
78
+ items: z.array(managedResourceSummarySchema).max(50),
79
+ nextCursor: resourceCursor.optional(),
80
+ total: z.number().int().min(0).optional(),
81
+ })
82
+ .strict()
83
+ .refine(hasBoundedManagedResourceBytes);
84
+ export const managedResourceRelationPageSchema = z
85
+ .object({
86
+ items: z
87
+ .array(z
88
+ .object({
89
+ id: resourceId,
90
+ value: managedResourceObject,
91
+ })
92
+ .strict())
93
+ .max(50),
94
+ nextCursor: resourceCursor.optional(),
95
+ })
96
+ .strict()
97
+ .refine(hasBoundedManagedResourceBytes);
98
+ export const managedResourceDeleteResultSchema = z
99
+ .object({ deleted: z.boolean() })
100
+ .strict();
101
+ //# sourceMappingURL=control-server-schemas.js.map
@@ -1,4 +1,6 @@
1
- import { type DatabasePool, type GuildShardOwnership } from "@helyx/database";
1
+ import { AuditRepository, GuildAccessPolicyRepository, InstallationRepository, ModuleLoggingPolicyRepository, PermissionPolicyRepository, type DatabasePool, type GuildShardOwnership } from "@helyx/database";
2
+ import type { MysqlPool } from "@helyx/database-mysql";
3
+ import type { CreatorOnboardingRequest, CreatorOnboardingResponse } from "@helyx/sdk";
2
4
  import { type DashboardActionInspection, type DashboardActionResult, type FeatureAccessDecision, type GuildAccessPolicy, type ManagedResourceCreationIntent, type ManagedResourceDetail, type ManagedResourcePage, type ManagedResourceRelationPage, type ManagedResourceValue, type MessageResponse, type ModuleDefinition, type OAuthConsumerCleanupResult, type PermissionActor, type PrivacyCapturePage, type PrivacyErasePage, type PrivacyProviderExecutor, type PublicAchievementProjectionPage, type ServiceAccess } from "@helyx/sdk";
3
5
  import { type ActionReceipt, type SignedAssessmentEnvelope } from "@helyx/verification-contracts";
4
6
  import { type Client } from "discord.js";
@@ -7,6 +9,7 @@ import { type BotModuleStatusMutationResult } from "./module-status-control.js";
7
9
  import { type FirstTimeCommandSetupHandler } from "./first-time-command-setup.js";
8
10
  import { type ShardHealthSnapshot } from "./shard-contracts.js";
9
11
  import { type DashboardPermissionState } from "./control-permissions.js";
12
+ export { BotControlTransportError } from "./control-errors.js";
10
13
  import { type DashboardModuleDefinition } from "./dashboard-module-projection.js";
11
14
  import { type AvailabilityModuleCatalogue } from "./module-availability-control.js";
12
15
  import { registerGuildCommandControlRoutes } from "./guild-command-control-routes.js";
@@ -118,6 +121,7 @@ export interface DashboardGuildAccessState extends GuildAccessPolicy {
118
121
  updatedAt: string | null;
119
122
  }
120
123
  export interface BotControlHandler extends FirstTimeCommandSetupHandler, DashboardActionHandler {
124
+ creatorOnboarding?(input: CreatorOnboardingRequest): Promise<CreatorOnboardingResponse>;
121
125
  cleanupOAuthConsumer(input: {
122
126
  consumerBindingId: string;
123
127
  moduleId: string;
@@ -324,21 +328,32 @@ export interface BotControlHandler extends FirstTimeCommandSetupHandler, Dashboa
324
328
  cursor?: string;
325
329
  }): Promise<PrivacyErasePage>;
326
330
  }
327
- export declare class BotControlTransportError extends Error {
328
- readonly status: 400 | 403 | 404 | 409 | 502 | 503 | 504;
329
- constructor(status: 400 | 403 | 404 | 409 | 502 | 503 | 504, message: string);
331
+ export interface BotControlRepositories {
332
+ installations: Pick<InstallationRepository, "adoptGuildsForReconciliation" | "markGuildsRemovedForReconciliation" | "listModules" | "isModuleEnabled">;
333
+ accessPolicies: Pick<GuildAccessPolicyRepository, "get" | "save">;
334
+ loggingPolicies: Pick<ModuleLoggingPolicyRepository, "listForGuild" | "save">;
335
+ permissionPolicies: Pick<PermissionPolicyRepository, "save" | "deleteCommandPolicy">;
336
+ audit: Pick<AuditRepository, "append">;
330
337
  }
338
+ interface ControlPersistence {
339
+ readonly repositories: BotControlRepositories;
340
+ withTransaction<Value>(operation: (repositories: BotControlRepositories) => Promise<Value>): Promise<Value>;
341
+ }
342
+ export interface BotControlPersistence extends ControlPersistence {
343
+ readonly engine: "mysql";
344
+ }
345
+ export declare function createMysqlBotControlPersistence(pool: MysqlPool): Promise<BotControlPersistence>;
331
346
  export declare class BotControlService implements BotControlHandler {
332
347
  #private;
333
- private readonly database;
334
348
  private readonly resources;
335
349
  private readonly supportNotifications?;
336
350
  private readonly presenceOwnership?;
337
351
  private readonly presenceShardCount?;
338
- constructor(database: DatabasePool, modules: readonly ModuleDefinition[], resources: BotControlGuildResourceDirectory, services: ServiceAccess, privacyExecutor?: PrivacyProviderExecutor, supportNotifications?: {
352
+ constructor(databaseOrPersistence: DatabasePool | BotControlPersistence, modules: readonly ModuleDefinition[], resources: BotControlGuildResourceDirectory, services: ServiceAccess, privacyExecutor?: PrivacyProviderExecutor, supportNotifications?: {
339
353
  guildId: string;
340
354
  channelId: string;
341
355
  } | undefined, presenceOwnership?: GuildShardOwnership | undefined, presenceShardCount?: number | undefined, statusModules?: readonly ModuleDefinition[]);
356
+ creatorOnboarding(input: CreatorOnboardingRequest): Promise<CreatorOnboardingResponse>;
342
357
  cleanupOAuthConsumer(input: {
343
358
  consumerBindingId: string;
344
359
  moduleId: string;
@@ -590,5 +605,4 @@ export declare function mapBotControlError(error: unknown): {
590
605
  status: 400 | 403 | 404 | 409;
591
606
  message: string;
592
607
  } | undefined;
593
- export {};
594
608
  //# sourceMappingURL=control-server.d.ts.map
@@ -13,116 +13,21 @@ 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, BotControlTransportError, } from "./control-errors.js";
17
+ export { BotControlTransportError } from "./control-errors.js";
17
18
  import { dashboardCommands } from "./dashboard-commands.js";
19
+ import { executeCreatorOnboarding, registerCreatorOnboardingControlRoute, } from "./creator-onboarding-control.js";
18
20
  import { mapDashboardModule, } from "./dashboard-module-projection.js";
19
21
  import { availabilityCatalogueDefinitions, availabilityCatalogue, dashboardModuleStatus, disabledModuleSummary, moduleAvailabilityService, moduleAvailabilityStates, statusCatalogueDefinitions, } from "./module-availability-control.js";
20
22
  import { registerGuildCommandControlRoutes } from "./guild-command-control-routes.js";
21
23
  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";
24
+ import { changedSettingFeatureIds, configurationPreviewFeatureIds, managedResourceColumnKeys, normalizeManagedResourceDetail, normalizeManagedResourcePage, normalizeManagedResourceRelationPage, projectManagedResourceObject, requireDashboardFeatures, resourceFieldFeatureIds, scalarInputFeatureIds, validateManagedResourceInput, } from "./dashboard-feature-control.js";
23
25
  import { registerDashboardPreviewImportRoutes, registerDashboardResourceMutationRoutes, } from "./dashboard-preview-import-routes.js";
24
26
  import { executeDashboardAction as executeDeclaredDashboardAction, previewDashboardAction as previewDeclaredDashboardAction, } from "./dashboard-action-control.js";
25
27
  import { registerDashboardActionRoutes, } from "./dashboard-action-routes.js";
26
28
  import { registerEckoTransportRoutes, } from "./ecko-transport-routes.js";
27
29
  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();
30
+ import { configurationValue, guildAccessPolicy, managedResourceDeleteResultSchema, managedResourceDetailSchema, managedResourceFilters, managedResourceObject, managedResourcePageSchema, managedResourceRelationPageSchema, moduleId, mutationContext, permissionState, resourceActionId, resourceCursor, resourceId, snowflake, } from "./control-server-schemas.js";
126
31
  export class DiscordGuildResourceDirectory {
127
32
  client;
128
33
  constructor(client) {
@@ -265,16 +170,37 @@ export class DiscordGuildResourceDirectory {
265
170
  await sendDiscordMessageToChannel(this.client, channel.id, response, notificationId);
266
171
  }
267
172
  }
268
- export class BotControlTransportError extends Error {
269
- status;
270
- constructor(status, message) {
271
- super(message);
272
- this.status = status;
273
- this.name = "BotControlTransportError";
274
- }
173
+ function createPostgresControlRepositories(connection) {
174
+ return {
175
+ installations: new InstallationRepository(connection),
176
+ accessPolicies: new GuildAccessPolicyRepository(connection),
177
+ loggingPolicies: new ModuleLoggingPolicyRepository(connection),
178
+ permissionPolicies: new PermissionPolicyRepository(connection),
179
+ audit: new AuditRepository(connection),
180
+ };
181
+ }
182
+ function createPostgresControlPersistence(database) {
183
+ return {
184
+ repositories: createPostgresControlRepositories(database),
185
+ withTransaction: (operation) => withTransaction(database, (connection) => operation(createPostgresControlRepositories(connection))),
186
+ };
187
+ }
188
+ export async function createMysqlBotControlPersistence(pool) {
189
+ const mysql = await import("@helyx/database-mysql");
190
+ const repositories = (connection) => ({
191
+ installations: new mysql.InstallationRepository(connection),
192
+ accessPolicies: new mysql.GuildAccessPolicyRepository(connection),
193
+ loggingPolicies: new mysql.ModuleLoggingPolicyRepository(connection),
194
+ permissionPolicies: new mysql.PermissionPolicyRepository(connection),
195
+ audit: new mysql.AuditRepository(connection),
196
+ });
197
+ return {
198
+ engine: "mysql",
199
+ repositories: repositories(pool),
200
+ withTransaction: (operation) => mysql.withMysqlTransaction(pool, (connection) => operation(repositories(mysql.createMysqlQueryExecutor(connection)))),
201
+ };
275
202
  }
276
203
  export class BotControlService {
277
- database;
278
204
  resources;
279
205
  supportNotifications;
280
206
  presenceOwnership;
@@ -288,12 +214,16 @@ export class BotControlService {
288
214
  #permissions;
289
215
  #services;
290
216
  #privacyProviders;
291
- constructor(database, modules, resources, services, privacyExecutor, supportNotifications, presenceOwnership, presenceShardCount, statusModules) {
292
- this.database = database;
217
+ #persistence;
218
+ constructor(databaseOrPersistence, modules, resources, services, privacyExecutor, supportNotifications, presenceOwnership, presenceShardCount, statusModules) {
293
219
  this.resources = resources;
294
220
  this.supportNotifications = supportNotifications;
295
221
  this.presenceOwnership = presenceOwnership;
296
222
  this.presenceShardCount = presenceShardCount;
223
+ this.#persistence =
224
+ "engine" in databaseOrPersistence
225
+ ? databaseOrPersistence
226
+ : createPostgresControlPersistence(databaseOrPersistence);
297
227
  this.#modules = new Map(modules.map((module) => [module.manifest.id, module]));
298
228
  this.#dashboardModules = modules
299
229
  .filter((module) => module.manifest.dashboard !== undefined)
@@ -306,6 +236,13 @@ export class BotControlService {
306
236
  this.#services = services;
307
237
  this.#privacyProviders = createPrivacyProviderRegistry(modules, privacyExecutor, this.#services);
308
238
  }
239
+ async creatorOnboarding(input) {
240
+ return executeCreatorOnboarding(input, {
241
+ modules: this.#modules,
242
+ installations: this.#installations,
243
+ services: this.#services,
244
+ });
245
+ }
309
246
  async cleanupOAuthConsumer(input) {
310
247
  const module = this.#modules.get(input.moduleId);
311
248
  if (!module)
@@ -344,7 +281,7 @@ export class BotControlService {
344
281
  .filter(({ state }) => state === "not_installed")
345
282
  .map(({ guildId }) => guildId);
346
283
  if (absent.length > 0) {
347
- const repository = new InstallationRepository(this.database);
284
+ const repository = this.#persistence.repositories.installations;
348
285
  if (this.presenceOwnership)
349
286
  await repository.adoptGuildsForReconciliation({
350
287
  guildIds: absent,
@@ -439,8 +376,8 @@ export class BotControlService {
439
376
  ]);
440
377
  const [resources, installedModules, storedLoggingPolicies, runtime, storedConfigurations, storedPermissions, availabilityStates,] = await Promise.all([
441
378
  this.resources.getGuildResources(guildId),
442
- new InstallationRepository(this.database).listModules(guildId),
443
- new ModuleLoggingPolicyRepository(this.database).listForGuild(guildId),
379
+ this.#persistence.repositories.installations.listModules(guildId),
380
+ this.#persistence.repositories.loggingPolicies.listForGuild(guildId),
444
381
  this.#inspectRuntimeCapabilities(),
445
382
  this.#readConfigurations(guildId, moduleIds),
446
383
  this.#readPermissionPolicies(guildId, permissionTargets),
@@ -516,7 +453,7 @@ export class BotControlService {
516
453
  async getGuildAccessPolicy(input) {
517
454
  const [actor, record] = await Promise.all([
518
455
  this.resources.resolveActor(input.guildId, input.actorUserId),
519
- new GuildAccessPolicyRepository(this.database).get(input.guildId),
456
+ this.#persistence.repositories.accessPolicies.get(input.guildId),
520
457
  ]);
521
458
  const policy = record ?? DEFAULT_GUILD_ACCESS_POLICY;
522
459
  return {
@@ -547,8 +484,8 @@ export class BotControlService {
547
484
  ];
548
485
  if (roleIds.some((roleId) => !availableRoles.has(roleId)))
549
486
  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({
487
+ const saved = await this.#persistence.withTransaction(async (repositories) => {
488
+ const record = await repositories.accessPolicies.save({
552
489
  guildId: input.guildId,
553
490
  administratorRoleIds: [...input.policy.administratorRoleIds],
554
491
  moderatorRoleIds: [...input.policy.moderatorRoleIds],
@@ -558,7 +495,7 @@ export class BotControlService {
558
495
  expectedVersion: input.version === 0 ? null : BigInt(input.version),
559
496
  updatedBy: input.actorUserId,
560
497
  });
561
- await new AuditRepository(connection).append({
498
+ await repositories.audit.append({
562
499
  guildId: input.guildId,
563
500
  actorUserId: input.actorUserId,
564
501
  action: "guild.access.updated",
@@ -590,14 +527,14 @@ export class BotControlService {
590
527
  };
591
528
  }
592
529
  async #readGuildAccessPolicy(guildId) {
593
- return ((await new GuildAccessPolicyRepository(this.database).get(guildId)) ??
530
+ return ((await this.#persistence.repositories.accessPolicies.get(guildId)) ??
594
531
  DEFAULT_GUILD_ACCESS_POLICY);
595
532
  }
596
533
  async readPublicModuleProjection(input) {
597
534
  const module = this.#requireModule(input.moduleId);
598
535
  if (!module.publicProjection)
599
536
  throw new BotControlNotFoundError("Public module projection was not found");
600
- if (!(await new InstallationRepository(this.database).isModuleEnabled(input.guildId, input.moduleId)))
537
+ if (!(await this.#persistence.repositories.installations.isModuleEnabled(input.guildId, input.moduleId)))
601
538
  throw new BotControlValidationError("This module is not enabled");
602
539
  if (input.subjectUserId) {
603
540
  if (!this.#services.has(HELYX_SERVICE_NAMES.discordResources))
@@ -775,9 +712,9 @@ export class BotControlService {
775
712
  if (enabledChanged || !permissionPoliciesEqual(current.policy, target))
776
713
  assertPermissionMutationAllowed(canMutatePermissionPolicy(actor, current.policy, target, guildAccess, rolePositions));
777
714
  }
778
- await withTransaction(this.database, async (connection) => {
779
- const policies = new PermissionPolicyRepository(connection);
780
- const audit = new AuditRepository(connection);
715
+ await this.#persistence.withTransaction(async (repositories) => {
716
+ const policies = repositories.permissionPolicies;
717
+ const audit = repositories.audit;
781
718
  const savedModule = await policies.save({
782
719
  guildId: input.guildId,
783
720
  moduleId: input.moduleId,
@@ -799,6 +736,12 @@ export class BotControlService {
799
736
  for (const command of commands) {
800
737
  const requested = supplied.get(command.id);
801
738
  const current = currentCommands.get(command.id);
739
+ const target = requested.enabled
740
+ ? fromDashboardPolicy(requested.policy, current.policy)
741
+ : requestedModulePolicy;
742
+ const enabledChanged = requested.enabled === current.inherited;
743
+ if (!enabledChanged && permissionPoliciesEqual(current.policy, target))
744
+ continue;
802
745
  if (requested.enabled) {
803
746
  const saved = await policies.save({
804
747
  guildId: input.guildId,
@@ -870,7 +813,7 @@ export class BotControlService {
870
813
  if (!this.#modules.has("helyx.logging")) {
871
814
  throw new BotControlNotFoundError("Logging is not installed");
872
815
  }
873
- if (!(await new InstallationRepository(this.database).isModuleEnabled(input.guildId, "helyx.logging"))) {
816
+ if (!(await this.#persistence.repositories.installations.isModuleEnabled(input.guildId, "helyx.logging"))) {
874
817
  throw new BotControlValidationError("Enable Logging before changing activity events");
875
818
  }
876
819
  const source = this.#requireModule(input.moduleId);
@@ -878,8 +821,8 @@ export class BotControlService {
878
821
  if (!event) {
879
822
  throw new BotControlNotFoundError("Logging event is not declared by this module");
880
823
  }
881
- const saved = await withTransaction(this.database, async (connection) => {
882
- const policy = await new ModuleLoggingPolicyRepository(connection).save({
824
+ const saved = await this.#persistence.withTransaction(async (repositories) => {
825
+ const policy = await repositories.loggingPolicies.save({
883
826
  guildId: input.guildId,
884
827
  sourceModuleId: input.moduleId,
885
828
  eventId: input.eventId,
@@ -887,7 +830,7 @@ export class BotControlService {
887
830
  expectedVersion: input.expectedVersion === 0 ? null : BigInt(input.expectedVersion),
888
831
  updatedBy: input.actorUserId,
889
832
  });
890
- await new AuditRepository(connection).append({
833
+ await repositories.audit.append({
891
834
  guildId: input.guildId,
892
835
  actorUserId: input.actorUserId,
893
836
  action: "module.logging.updated",
@@ -917,7 +860,7 @@ export class BotControlService {
917
860
  }
918
861
  #dashboardActionControl() {
919
862
  return {
920
- database: this.database,
863
+ installations: this.#persistence.repositories.installations,
921
864
  services: this.#services,
922
865
  requireDashboardModule: (moduleIdValue) => this.#requireDashboardModule(moduleIdValue),
923
866
  requireModuleAccessible: (guildId, moduleIdValue) => this.#requireModuleAccessible(guildId, moduleIdValue),
@@ -1103,7 +1046,7 @@ export class BotControlService {
1103
1046
  }
1104
1047
  }
1105
1048
  async #requireEnabledManagedResource(guildId, moduleIdValue) {
1106
- if (!(await new InstallationRepository(this.database).isModuleEnabled(guildId, moduleIdValue)))
1049
+ if (!(await this.#persistence.repositories.installations.isModuleEnabled(guildId, moduleIdValue)))
1107
1050
  throw new BotControlValidationError("Enable this module before managing its resources");
1108
1051
  }
1109
1052
  async #managedResourceContext(input, module, operation, permissionId = managedResourcePermissionId(module, input.resourceType, operation, undefined, declaredManagedResourcePermission(module, input.resourceType, operation))) {
@@ -1309,6 +1252,7 @@ export async function createBotControlServer(input) {
1309
1252
  return reply.code(503).send({ status: "unavailable" });
1310
1253
  }
1311
1254
  server.get("/health", (_request, reply) => readiness(reply));
1255
+ registerCreatorOnboardingControlRoute(server, input.service);
1312
1256
  server.get("/v1/health", (_request, reply) => readiness(reply));
1313
1257
  server.get("/v1/runtime-configuration", (_request, reply) => reply.header("Cache-Control", "no-store").send({
1314
1258
  hostedModerationEnabled: input.runtimeConfiguration?.hostedModerationEnabled ?? false,
@@ -1716,22 +1660,30 @@ export function mapBotControlError(error) {
1716
1660
  return { status: 404, message: error.message };
1717
1661
  if (error instanceof BotControlForbiddenError)
1718
1662
  return { status: 403, message: error.message };
1719
- if (error instanceof OptimisticConcurrencyError)
1663
+ if (error instanceof OptimisticConcurrencyError ||
1664
+ (error instanceof Error &&
1665
+ error.name === "DatabaseFailure" &&
1666
+ "kind" in error &&
1667
+ error.kind === "conflict"))
1720
1668
  return {
1721
1669
  status: 409,
1722
1670
  message: "Configuration changed in another session. Reload and try again.",
1723
1671
  };
1724
- if (error instanceof GuildCommandRevisionConflictError)
1672
+ if (error instanceof GuildCommandRevisionConflictError ||
1673
+ (error instanceof Error &&
1674
+ error.name === "GuildCommandRevisionConflictError"))
1725
1675
  return {
1726
1676
  status: 409,
1727
1677
  message: "Command state changed in another session. Refresh and try again.",
1728
1678
  };
1729
- if (error instanceof GuildCommandActiveLeaseError)
1679
+ if (error instanceof GuildCommandActiveLeaseError ||
1680
+ (error instanceof Error && error.name === "GuildCommandActiveLeaseError"))
1730
1681
  return {
1731
1682
  status: 409,
1732
1683
  message: "A command build is already running. Wait for it to finish.",
1733
1684
  };
1734
- if (error instanceof GuildCommandSetupConflictError)
1685
+ if (error instanceof GuildCommandSetupConflictError ||
1686
+ (error instanceof Error && error.name === "GuildCommandSetupConflictError"))
1735
1687
  return {
1736
1688
  status: 409,
1737
1689
  message: "Command setup changed. Reload the page and try again.",
@@ -1739,6 +1691,10 @@ export function mapBotControlError(error) {
1739
1691
  return undefined;
1740
1692
  }
1741
1693
  class BotControlValidationError extends Error {
1694
+ constructor(message) {
1695
+ super(message);
1696
+ this.name = "BotControlValidationError";
1697
+ }
1742
1698
  }
1743
1699
  class BotControlNotFoundError extends Error {
1744
1700
  constructor(message = "Discord server is not available to Helyx") {
@@ -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