@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.
- package/CHANGELOG.md +100 -28
- package/README.md +65 -11
- package/dist/channel-audit-configuration.d.ts +7 -0
- package/dist/channel-audit-configuration.js +40 -0
- package/dist/channel-audit-control.d.ts +14 -0
- package/dist/channel-audit-control.js +61 -0
- package/dist/channel-audit-permissions.d.ts +56 -0
- package/dist/channel-audit-permissions.js +417 -0
- package/dist/channel-audit-routes.d.ts +4 -0
- package/dist/channel-audit-routes.js +42 -0
- package/dist/cloud-connector.d.ts +11 -4
- package/dist/cloud-connector.js +54 -15
- package/dist/cloud-control-dispatcher.d.ts +8 -3
- package/dist/cloud-control-dispatcher.js +84 -4
- package/dist/control-errors.d.ts +12 -0
- package/dist/control-errors.js +32 -0
- package/dist/control-server-schemas.d.ts +84 -0
- package/dist/control-server-schemas.js +101 -0
- package/dist/control-server.d.ts +25 -8
- package/dist/control-server.js +101 -152
- package/dist/core-retention.d.ts +26 -0
- package/dist/core-retention.js +49 -0
- package/dist/creator-guild-join-service.d.ts +25 -0
- package/dist/creator-guild-join-service.js +218 -0
- package/dist/creator-invite-service.d.ts +37 -0
- package/dist/creator-invite-service.js +686 -0
- package/dist/creator-onboarding-control.d.ts +12 -0
- package/dist/creator-onboarding-control.js +65 -0
- package/dist/creator-onboarding-platform.d.ts +20 -0
- package/dist/creator-onboarding-platform.js +58 -0
- package/dist/dashboard-action-control.d.ts +2 -2
- package/dist/dashboard-action-control.js +2 -3
- package/dist/database-bootstrap.d.ts +36 -0
- package/dist/database-bootstrap.js +101 -0
- package/dist/developer-operations-module.d.ts +1 -0
- package/dist/developer-operations-module.js +34 -16
- package/dist/discord-channel-audit.d.ts +9 -0
- package/dist/discord-channel-audit.js +195 -0
- package/dist/discord-guild-health.d.ts +2 -2
- package/dist/discord-owned-message-service.d.ts +1 -0
- package/dist/discord-owned-message-service.js +3 -0
- package/dist/discord-role-safety.d.ts +4 -0
- package/dist/discord-role-safety.js +30 -0
- package/dist/events.d.ts +2 -0
- package/dist/events.js +5 -20
- package/dist/guild-command-read-service.d.ts +8 -3
- package/dist/guild-command-read-service.js +14 -7
- package/dist/guild-command-release.d.ts +15 -8
- package/dist/guild-command-release.js +10 -3
- package/dist/guild-command-worker.d.ts +17 -4
- package/dist/guild-command-worker.js +17 -6
- package/dist/guilds.d.ts +24 -3
- package/dist/guilds.js +185 -64
- package/dist/hosted-server-moderation.js +10 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +200 -82
- package/dist/interactions.js +16 -3
- package/dist/module-configuration.d.ts +1 -1
- package/dist/module-configuration.js +5 -2
- package/dist/module-logging.d.ts +8 -3
- package/dist/module-logging.js +13 -7
- package/dist/modules.d.ts +1 -0
- package/dist/modules.js +8 -1
- package/dist/mysql-runtime-persistence.d.ts +46 -0
- package/dist/mysql-runtime-persistence.js +91 -0
- package/dist/oauth-module.js +2 -2
- package/dist/oauth-service.d.ts +7 -0
- package/dist/oauth-service.js +3 -0
- package/dist/scheduled-task-runner.d.ts +9 -85
- package/dist/scheduled-task-runner.js +1 -1
- package/dist/shard-contracts.d.ts +222 -0
- package/dist/shard-contracts.js +52 -1
- package/dist/shard-router.d.ts +4 -1
- package/dist/shard-router.js +19 -0
- package/dist/shard-supervisor.js +43 -9
- package/dist/shard-worker.d.ts +5 -2
- package/dist/shard-worker.js +41 -5
- package/dist/supervisor-cloud-directory.d.ts +9 -3
- package/dist/supervisor-cloud-directory.js +8 -12
- package/dist/trivia-data-provider.d.ts +11 -0
- package/dist/trivia-data-provider.js +46 -26
- package/dist/verification-service.d.ts +2 -2
- package/dist/verification-service.js +6 -3
- package/documentation/README.md +5 -3
- package/documentation/examples/.env.example +15 -11
- package/documentation/guides/getting-started.md +29 -12
- package/documentation/guides/privacy-and-support.md +6 -5
- package/documentation/guides/self-hosting.md +16 -11
- package/documentation/modules/auto-moderation.md +7 -5
- package/documentation/modules/building-a-module.md +48 -2
- package/documentation/modules/moderation.md +5 -5
- package/documentation/modules/publishing-a-module.md +6 -4
- package/documentation/modules/testing-a-module.md +39 -4
- package/documentation/reference/environment.md +43 -9
- package/documentation/runbooks/backup-and-recovery.md +26 -7
- package/documentation/runbooks/installation-and-upgrades.md +97 -27
- package/package.json +20 -16
package/dist/control-server.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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 =
|
|
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
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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(
|
|
551
|
-
const record = await
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
779
|
-
const policies =
|
|
780
|
-
const audit =
|
|
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
|
|
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(
|
|
882
|
-
const policy = await
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|