@helyx/bot 0.4.16 → 0.4.17

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.17
4
+
5
+ ### Patch Changes
6
+
7
+ - Remove the legacy Verification automatic setup interfaces and obsolete configuration field.
8
+ - Updated dependencies
9
+ - @helyx/database@0.7.0
10
+ - @helyx/verification-contracts@0.2.0
11
+ - @helyx/sdk@0.6.0
12
+ - @helyx/platform@0.4.1
13
+ - @helyx/core@0.1.13
14
+ - @helyx/framework@0.1.13
15
+
3
16
  ## 0.4.16
4
17
 
5
18
  ### Patch Changes
package/dist/events.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import type { DiscordChannelAudienceInspection, DiscordChannelInspection, DiscordEmojiInspection, DiscordMessageInspection, DiscordGuildCapabilityInspection, DiscordGuildChannelAudienceInspection, DiscordResourceInspectionService, DiscordRoleInspection, DiscordMessageService, MemberEnforcementService, MemberRoleService, ModuleDefinition, ServiceAccess, VerificationMemberService, VerificationSetupGuildInspection, VerificationSetupPreview, VerificationSetupRequest, VerificationSetupService } from "@helyx/sdk";
2
- import type { VerificationSetupRepository } from "@helyx/database";
1
+ import type { DiscordChannelAudienceInspection, DiscordChannelInspection, DiscordEmojiInspection, DiscordMessageInspection, DiscordGuildCapabilityInspection, DiscordGuildChannelAudienceInspection, DiscordResourceInspectionService, DiscordRoleInspection, DiscordMessageService, MemberEnforcementService, MemberRoleService, ModuleDefinition, ServiceAccess, VerificationMemberService } from "@helyx/sdk";
3
2
  import { type Client, type MessageReaction, type PartialMessageReaction } from "discord.js";
4
3
  import { type InteractionLogger } from "./interactions.js";
5
4
  export declare function installModuleEvents(client: Client, modules: readonly ModuleDefinition[], logger: InteractionLogger, services: ServiceAccess): ModuleEventHandle;
@@ -35,26 +34,6 @@ export declare class DiscordVerificationMemberService implements VerificationMem
35
34
  captureSnapshot(input: Parameters<VerificationMemberService["captureSnapshot"]>[0]): ReturnType<VerificationMemberService["captureSnapshot"]>;
36
35
  isCurrentlyBanned(input: Parameters<VerificationMemberService["isCurrentlyBanned"]>[0]): ReturnType<VerificationMemberService["isCurrentlyBanned"]>;
37
36
  }
38
- /**
39
- * The single Discord mutation boundary for Verification setup. It deliberately
40
- * exposes a preview/apply protocol rather than raw role or overwrite writes.
41
- */
42
- export declare class DiscordVerificationSetupService implements VerificationSetupService {
43
- #private;
44
- private readonly client;
45
- private readonly persistence;
46
- constructor(client: Client, persistence: VerificationSetupPersistence);
47
- inspect(input: {
48
- guildId: string;
49
- }): Promise<VerificationSetupGuildInspection>;
50
- preview(input: VerificationSetupRequest): Promise<VerificationSetupPreview>;
51
- apply(input: VerificationSetupRequest & {
52
- previewFingerprint: string;
53
- confirmed: true;
54
- }): Promise<Awaited<ReturnType<VerificationSetupService["apply"]>>>;
55
- hideVerificationChannelForVerifiedMember(input: Parameters<VerificationSetupService["hideVerificationChannelForVerifiedMember"]>[0]): ReturnType<VerificationSetupService["hideVerificationChannelForVerifiedMember"]>;
56
- }
57
- export type VerificationSetupPersistence = Pick<VerificationSetupRepository, "begin" | "checkpointResources" | "checkpointOverwrites" | "complete" | "abort">;
58
37
  export declare class DiscordMemberRoleService implements MemberRoleService {
59
38
  private readonly client;
60
39
  constructor(client: Client);
package/dist/events.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
2
- import { VerificationSetupLeaseLostError } from "@helyx/database";
3
2
  import { ChannelType, Events, GatewayIntentBits, GuildMemberFlags, GuildOnboardingMode, PermissionFlagsBits, RESTJSONErrorCodes, UserFlags, } from "discord.js";
4
3
  import { toReplyOptions } from "./interactions.js";
5
4
  import { createHash, randomUUID } from "node:crypto";
@@ -918,661 +917,6 @@ export class DiscordVerificationMemberService {
918
917
  }
919
918
  }
920
919
  }
921
- const verificationSetupMaximumAccessTargets = 50;
922
- const verificationSetupLeaseMs = 120_000;
923
- const verificationRoleNameMaximum = 100;
924
- const verificationChannelNamePattern = /^[a-z0-9][a-z0-9-_]{0,99}$/u;
925
- const verificationMembershipScreeningFeature = "MEMBER_VERIFICATION_GATE_ENABLED";
926
- /**
927
- * The single Discord mutation boundary for Verification setup. It deliberately
928
- * exposes a preview/apply protocol rather than raw role or overwrite writes.
929
- */
930
- export class DiscordVerificationSetupService {
931
- client;
932
- persistence;
933
- #guildLocks = new Map();
934
- constructor(client, persistence) {
935
- this.client = client;
936
- this.persistence = persistence;
937
- }
938
- async inspect(input) {
939
- validateSnowflake(input.guildId, "guild");
940
- const guild = await this.client.guilds.fetch(input.guildId);
941
- if (guild.id !== input.guildId)
942
- throw new Error("Discord guild validation failed");
943
- const me = guild.members.me ?? (await guild.members.fetchMe());
944
- const permissions = {
945
- viewChannel: me.permissions.has(PermissionFlagsBits.ViewChannel),
946
- sendMessages: me.permissions.has(PermissionFlagsBits.SendMessages),
947
- embedLinks: me.permissions.has(PermissionFlagsBits.EmbedLinks),
948
- manageRoles: me.permissions.has(PermissionFlagsBits.ManageRoles),
949
- manageChannels: me.permissions.has(PermissionFlagsBits.ManageChannels),
950
- kickMembers: me.permissions.has(PermissionFlagsBits.KickMembers),
951
- banMembers: me.permissions.has(PermissionFlagsBits.BanMembers),
952
- administrator: me.permissions.has(PermissionFlagsBits.Administrator),
953
- };
954
- const membershipScreeningEnabled = guild.features.includes(verificationMembershipScreeningFeature);
955
- try {
956
- const onboarding = await guild.fetchOnboarding();
957
- return {
958
- guildId: guild.id,
959
- permissions,
960
- onboarding: {
961
- status: onboarding.enabled ? "enabled" : "disabled",
962
- mode: onboarding.mode === GuildOnboardingMode.OnboardingDefault
963
- ? "default"
964
- : onboarding.mode === GuildOnboardingMode.OnboardingAdvanced
965
- ? "advanced"
966
- : "unknown",
967
- membershipScreeningEnabled,
968
- defaultChannelIds: [...onboarding.defaultChannels.keys()]
969
- .slice(0, verificationSetupMaximumAccessTargets)
970
- .sort((left, right) => left.localeCompare(right, "en")),
971
- unverifiedInteractionChannelRecommended: onboarding.enabled || membershipScreeningEnabled,
972
- },
973
- };
974
- }
975
- catch {
976
- return {
977
- guildId: guild.id,
978
- permissions,
979
- onboarding: {
980
- status: "unavailable",
981
- mode: "unknown",
982
- membershipScreeningEnabled,
983
- defaultChannelIds: [],
984
- unverifiedInteractionChannelRecommended: membershipScreeningEnabled,
985
- },
986
- };
987
- }
988
- }
989
- async preview(input) {
990
- validateVerificationSetupRequest(input);
991
- const guild = await this.client.guilds.fetch(input.guildId);
992
- if (guild.id !== input.guildId)
993
- throw new Error("Discord guild validation failed");
994
- const me = guild.members.me ?? (await guild.members.fetchMe());
995
- const [inspection, verifiedRole, verificationChannel] = await Promise.all([
996
- this.inspect({ guildId: guild.id }),
997
- resolveSetupRole(guild, me, input.verifiedRole),
998
- resolveSetupChannel(guild, me, input.verificationChannel),
999
- ]);
1000
- const accessOverwrites = await Promise.all(input.accessTargetIds.map(async (targetId) => {
1001
- const channel = await fetchVerificationAccessTarget(guild, targetId);
1002
- const current = verifiedRole.role
1003
- ? viewChannelOverwrite(channel, verifiedRole.role.id)
1004
- : null;
1005
- return {
1006
- targetId: channel.id,
1007
- targetType: verificationAccessTargetType(channel),
1008
- action: current === true
1009
- ? "unchanged"
1010
- : "set_verified_view",
1011
- };
1012
- }));
1013
- const missingPermissions = verificationSetupMissingPermissions({
1014
- inspection,
1015
- me,
1016
- verificationChannel,
1017
- needsManageChannels: verificationChannel.action === "create" ||
1018
- accessOverwrites.some(({ action }) => action !== "unchanged"),
1019
- });
1020
- const unsigned = {
1021
- inspection,
1022
- missingPermissions,
1023
- verifiedRole: {
1024
- action: verifiedRole.action,
1025
- name: verifiedRole.name,
1026
- ...(verifiedRole.role ? { roleId: verifiedRole.role.id } : {}),
1027
- manageable: verifiedRole.manageable,
1028
- },
1029
- verificationChannel: {
1030
- action: verificationChannel.action,
1031
- name: verificationChannel.name,
1032
- ...(verificationChannel.channel
1033
- ? { channelId: verificationChannel.channel.id }
1034
- : {}),
1035
- usable: verificationChannel.usable,
1036
- },
1037
- accessOverwrites,
1038
- };
1039
- return {
1040
- fingerprint: verificationSetupFingerprint(input, unsigned),
1041
- ...unsigned,
1042
- };
1043
- }
1044
- async apply(input) {
1045
- if (input.confirmed !== true)
1046
- throw new Error("Verification setup requires explicit confirmation");
1047
- if (!/^[a-f0-9]{64}$/u.test(input.previewFingerprint))
1048
- throw new Error("Verification setup preview fingerprint is invalid");
1049
- return this.#withGuildLock(input.guildId, async () => {
1050
- const request = {
1051
- guildId: input.guildId,
1052
- verifiedRole: input.verifiedRole,
1053
- verificationChannel: input.verificationChannel,
1054
- accessTargetIds: input.accessTargetIds,
1055
- };
1056
- const preview = await this.preview(request);
1057
- if (preview.fingerprint !== input.previewFingerprint)
1058
- throw new Error("Verification setup changed after preview; review the new plan");
1059
- if (preview.missingPermissions.length > 0)
1060
- throw new Error(`Verification setup is missing permissions: ${preview.missingPermissions.join(", ")}`);
1061
- if (!preview.verifiedRole.manageable)
1062
- throw new Error("The verified role cannot be managed safely by Helyx");
1063
- if (!preview.verificationChannel.usable)
1064
- throw new Error("The verification channel is not usable by Helyx");
1065
- const guild = await this.client.guilds.fetch(input.guildId);
1066
- const reason = "Helyx Verification: confirmed setup plan";
1067
- const leaseOwner = `hosted-bot:${randomUUID()}`;
1068
- for (let claimAttempt = 0; claimAttempt < 2; claimAttempt += 1) {
1069
- const claim = await this.persistence.begin({
1070
- guildId: input.guildId,
1071
- fingerprint: input.previewFingerprint,
1072
- selectedTargetIds: input.accessTargetIds,
1073
- leaseOwner,
1074
- leaseMs: verificationSetupLeaseMs,
1075
- });
1076
- if (claim.kind === "applied")
1077
- return claim.result;
1078
- if (claim.kind === "busy")
1079
- throw new Error("Another Verification setup operation is in progress");
1080
- if (claim.kind === "recover") {
1081
- await this.#recoverSetup(guild, claim.state, reason);
1082
- await this.persistence.abort(claim.state);
1083
- continue;
1084
- }
1085
- return this.#applyClaimedSetup(guild, preview, claim.state, reason);
1086
- }
1087
- throw new Error("Verification setup recovery did not converge");
1088
- });
1089
- }
1090
- async #applyClaimedSetup(guild, preview, initialState, reason) {
1091
- let state = initialState;
1092
- let uncheckpointedRole = null;
1093
- let uncheckpointedChannel = null;
1094
- try {
1095
- let role = state.roleId ? await guild.roles.fetch(state.roleId) : null;
1096
- if (!role) {
1097
- if (preview.verifiedRole.action === "create") {
1098
- role = await findPendingVerificationRole(guild, state.requestedFingerprint);
1099
- if (!role)
1100
- try {
1101
- role = await guild.roles.create({
1102
- name: verificationPendingRoleName(state.requestedFingerprint),
1103
- permissions: [],
1104
- reason,
1105
- });
1106
- }
1107
- catch (error) {
1108
- role = await findPendingVerificationRole(guild, state.requestedFingerprint);
1109
- if (!role)
1110
- throw error;
1111
- }
1112
- uncheckpointedRole = role;
1113
- state = await this.persistence.checkpointResources(state, { roleId: role.id, createdRole: true }, verificationSetupLeaseMs);
1114
- uncheckpointedRole = null;
1115
- if (role.name !== preview.verifiedRole.name)
1116
- role = await role.setName(preview.verifiedRole.name, reason);
1117
- }
1118
- else {
1119
- role = await guild.roles.fetch(preview.verifiedRole.roleId);
1120
- if (role)
1121
- state = await this.persistence.checkpointResources(state, { roleId: role.id, createdRole: false }, verificationSetupLeaseMs);
1122
- }
1123
- }
1124
- if (!role || role.guild.id !== guild.id)
1125
- throw new Error("The verified role became unavailable during setup");
1126
- let channel = state.channelId
1127
- ? await fetchVerificationTextChannel(guild, state.channelId)
1128
- : null;
1129
- if (!channel) {
1130
- if (preview.verificationChannel.action === "create") {
1131
- channel = await findPendingVerificationChannel(guild, state.requestedFingerprint);
1132
- if (!channel)
1133
- try {
1134
- channel = await guild.channels.create({
1135
- name: verificationPendingChannelName(state.requestedFingerprint),
1136
- type: ChannelType.GuildText,
1137
- reason,
1138
- });
1139
- }
1140
- catch (error) {
1141
- channel = await findPendingVerificationChannel(guild, state.requestedFingerprint);
1142
- if (!channel)
1143
- throw error;
1144
- }
1145
- uncheckpointedChannel = channel;
1146
- state = await this.persistence.checkpointResources(state, { channelId: channel.id, createdChannel: true }, verificationSetupLeaseMs);
1147
- uncheckpointedChannel = null;
1148
- if (channel.name !== preview.verificationChannel.name)
1149
- channel = await channel.setName(preview.verificationChannel.name, reason);
1150
- }
1151
- else {
1152
- channel = await fetchVerificationTextChannel(guild, preview.verificationChannel.channelId);
1153
- if (channel)
1154
- state = await this.persistence.checkpointResources(state, { channelId: channel.id, createdChannel: false }, verificationSetupLeaseMs);
1155
- }
1156
- }
1157
- if (!channel || channel.guild.id !== guild.id)
1158
- throw new Error("The verification channel became unavailable during setup");
1159
- const desiredKeys = new Set(state.selectedTargetIds.map((targetId) => verificationManagedOverwriteKey(targetId, role.id)));
1160
- const removesPreviousManagedOverwrite = state.managedOverwrites.some((overwrite) => !desiredKeys.has(verificationManagedOverwriteKey(overwrite.targetId, overwrite.roleId)));
1161
- await assertVerificationSetupStillSafe(guild, role, channel, {
1162
- needsManageChannels: state.createdChannel ||
1163
- removesPreviousManagedOverwrite ||
1164
- preview.accessOverwrites.some(({ action }) => action !== "unchanged"),
1165
- });
1166
- for (const overwrite of [...state.managedOverwrites]) {
1167
- const key = verificationManagedOverwriteKey(overwrite.targetId, overwrite.roleId);
1168
- if (desiredKeys.has(key))
1169
- continue;
1170
- state = await this.#checkpointOverwriteState(state, {
1171
- ...overwrite,
1172
- status: "restoring",
1173
- });
1174
- await restoreVerificationViewOverwrite(guild, overwrite, reason);
1175
- state = await this.#removeCheckpointedOverwrite(state, key);
1176
- }
1177
- for (const targetId of state.selectedTargetIds) {
1178
- const target = await fetchVerificationAccessTarget(guild, targetId);
1179
- const key = verificationManagedOverwriteKey(target.id, role.id);
1180
- let tracked = state.managedOverwrites.find((overwrite) => verificationManagedOverwriteKey(overwrite.targetId, overwrite.roleId) === key);
1181
- const current = viewChannelOverwrite(target, role.id);
1182
- if (!tracked && current === true)
1183
- continue;
1184
- if (!tracked) {
1185
- tracked = {
1186
- targetId: target.id,
1187
- roleId: role.id,
1188
- previousView: current,
1189
- status: "planned",
1190
- };
1191
- state = await this.#checkpointOverwriteState(state, tracked);
1192
- }
1193
- else if (tracked.status !== "planned" && current !== true) {
1194
- tracked = { ...tracked, status: "planned" };
1195
- state = await this.#checkpointOverwriteState(state, tracked);
1196
- }
1197
- if (current !== true) {
1198
- await target.permissionOverwrites.edit(role, { ViewChannel: true }, { reason });
1199
- }
1200
- tracked = { ...tracked, status: "applied" };
1201
- state = await this.#checkpointOverwriteState(state, tracked, current === true ? undefined : target.id);
1202
- }
1203
- return this.persistence.complete(state, {
1204
- verifiedRoleId: role.id,
1205
- verificationChannelId: channel.id,
1206
- createdRole: state.createdRole,
1207
- createdChannel: state.createdChannel,
1208
- changedAccessTargetIds: state.changedTargetIds,
1209
- });
1210
- }
1211
- catch (error) {
1212
- if (error instanceof VerificationSetupLeaseLostError)
1213
- throw error;
1214
- const rollbackFailures = [];
1215
- for (const resource of [uncheckpointedChannel, uncheckpointedRole]) {
1216
- if (!resource)
1217
- continue;
1218
- try {
1219
- await resource.delete(`${reason} rollback`);
1220
- }
1221
- catch (rollbackError) {
1222
- rollbackFailures.push(rollbackError);
1223
- }
1224
- }
1225
- try {
1226
- await this.#recoverSetup(guild, state, `${reason} rollback`);
1227
- await this.persistence.abort(state);
1228
- }
1229
- catch (rollbackError) {
1230
- rollbackFailures.push(rollbackError);
1231
- }
1232
- if (rollbackFailures.length > 0)
1233
- throw new AggregateError(rollbackFailures, "Verification setup failed and rollback was incomplete", { cause: error });
1234
- throw error;
1235
- }
1236
- }
1237
- async #recoverSetup(guild, state, reason) {
1238
- const applied = new Map(state.appliedManagedOverwrites.map((overwrite) => [
1239
- verificationManagedOverwriteKey(overwrite.targetId, overwrite.roleId),
1240
- overwrite,
1241
- ]));
1242
- for (const pending of state.managedOverwrites) {
1243
- const key = verificationManagedOverwriteKey(pending.targetId, pending.roleId);
1244
- if (applied.has(key)) {
1245
- await setVerificationViewOverwrite(guild, pending.targetId, pending.roleId, true, reason);
1246
- applied.delete(key);
1247
- }
1248
- else {
1249
- await restoreVerificationViewOverwrite(guild, pending, reason);
1250
- }
1251
- }
1252
- for (const previous of applied.values()) {
1253
- await setVerificationViewOverwrite(guild, previous.targetId, previous.roleId, true, reason);
1254
- }
1255
- const appliedResult = state.appliedResult;
1256
- if (state.createdChannel &&
1257
- state.channelId &&
1258
- state.channelId !== appliedResult?.verificationChannelId) {
1259
- const createdChannel = await guild.channels.fetch(state.channelId);
1260
- if (createdChannel)
1261
- await createdChannel.delete(reason);
1262
- }
1263
- if (state.createdRole &&
1264
- state.roleId &&
1265
- state.roleId !== appliedResult?.verifiedRoleId) {
1266
- const createdRole = await guild.roles.fetch(state.roleId);
1267
- if (createdRole)
1268
- await createdRole.delete(reason);
1269
- }
1270
- const pendingChannel = await findPendingVerificationChannel(guild, state.requestedFingerprint);
1271
- if (pendingChannel &&
1272
- pendingChannel.id !== appliedResult?.verificationChannelId)
1273
- await pendingChannel.delete(reason);
1274
- const pendingRole = await findPendingVerificationRole(guild, state.requestedFingerprint);
1275
- if (pendingRole && pendingRole.id !== appliedResult?.verifiedRoleId)
1276
- await pendingRole.delete(reason);
1277
- }
1278
- async #checkpointOverwriteState(state, overwrite, changedTargetId) {
1279
- const key = verificationManagedOverwriteKey(overwrite.targetId, overwrite.roleId);
1280
- const next = state.managedOverwrites.filter((existing) => verificationManagedOverwriteKey(existing.targetId, existing.roleId) !==
1281
- key);
1282
- next.push(overwrite);
1283
- const changed = changedTargetId
1284
- ? [...new Set([...state.changedTargetIds, changedTargetId])]
1285
- : state.changedTargetIds;
1286
- return this.persistence.checkpointOverwrites(state, next, changed, verificationSetupLeaseMs);
1287
- }
1288
- #removeCheckpointedOverwrite(state, key) {
1289
- return this.persistence.checkpointOverwrites(state, state.managedOverwrites.filter((overwrite) => verificationManagedOverwriteKey(overwrite.targetId, overwrite.roleId) !== key), state.changedTargetIds, verificationSetupLeaseMs);
1290
- }
1291
- async hideVerificationChannelForVerifiedMember(input) {
1292
- validateMutationMetadata({
1293
- guildId: input.guildId,
1294
- memberUserId: input.memberUserId,
1295
- roleId: input.verifiedRoleId,
1296
- moduleId: input.moduleId,
1297
- reason: input.reason,
1298
- });
1299
- validateSnowflake(input.channelId, "channel");
1300
- const guild = await this.client.guilds.fetch(input.guildId);
1301
- const [member, role, fetchedChannel] = await Promise.all([
1302
- guild.members.fetch(input.memberUserId),
1303
- guild.roles.fetch(input.verifiedRoleId),
1304
- guild.channels.fetch(input.channelId),
1305
- ]);
1306
- if (!role || role.guild.id !== guild.id)
1307
- throw new Error("The verified role is unavailable");
1308
- if (!fetchedChannel ||
1309
- fetchedChannel.guild.id !== guild.id ||
1310
- fetchedChannel.type !== ChannelType.GuildText)
1311
- throw new Error("The verification channel is unavailable");
1312
- if (!member.roles.cache.has(role.id))
1313
- throw new Error("The member does not hold the verified role");
1314
- const me = guild.members.me ?? (await guild.members.fetchMe());
1315
- if (!me.permissions.has(PermissionFlagsBits.ManageChannels))
1316
- throw new Error("Helyx requires Manage Channels to hide this channel");
1317
- if (!verificationRoleManageable(role, guild, me))
1318
- throw new Error("The verified role cannot be managed safely by Helyx");
1319
- if (viewChannelOverwrite(fetchedChannel, role.id) === false)
1320
- return { changed: false };
1321
- await fetchedChannel.permissionOverwrites.edit(role, { ViewChannel: false }, { reason: discordAuditReason(input) });
1322
- return { changed: true };
1323
- }
1324
- async #withGuildLock(guildId, operation) {
1325
- validateSnowflake(guildId, "guild");
1326
- const previous = this.#guildLocks.get(guildId) ?? Promise.resolve();
1327
- let release;
1328
- const gate = new Promise((resolve) => {
1329
- release = resolve;
1330
- });
1331
- const current = previous.catch(() => undefined).then(() => gate);
1332
- this.#guildLocks.set(guildId, current);
1333
- await previous.catch(() => undefined);
1334
- try {
1335
- return await operation();
1336
- }
1337
- finally {
1338
- release();
1339
- if (this.#guildLocks.get(guildId) === current)
1340
- this.#guildLocks.delete(guildId);
1341
- }
1342
- }
1343
- }
1344
- async function resolveSetupRole(guild, me, selection) {
1345
- if (selection.mode === "create") {
1346
- const role = selection.trackedRoleId
1347
- ? await guild.roles.fetch(selection.trackedRoleId)
1348
- : null;
1349
- if (!role)
1350
- return {
1351
- action: "create",
1352
- name: selection.name,
1353
- role: null,
1354
- manageable: me.permissions.has(PermissionFlagsBits.ManageRoles),
1355
- };
1356
- return {
1357
- action: "reuse",
1358
- name: role.name,
1359
- role,
1360
- manageable: verificationRoleManageable(role, guild, me),
1361
- };
1362
- }
1363
- const role = await guild.roles.fetch(selection.roleId);
1364
- if (!role || role.guild.id !== guild.id)
1365
- throw new Error("The selected verified role is unavailable");
1366
- return {
1367
- action: "select",
1368
- name: role.name,
1369
- role,
1370
- manageable: verificationRoleManageable(role, guild, me),
1371
- };
1372
- }
1373
- async function resolveSetupChannel(guild, me, selection) {
1374
- if (selection.mode === "create") {
1375
- const fetched = selection.trackedChannelId
1376
- ? await guild.channels.fetch(selection.trackedChannelId)
1377
- : null;
1378
- if (!fetched)
1379
- return {
1380
- action: "create",
1381
- name: selection.name,
1382
- channel: null,
1383
- usable: me.permissions.has(PermissionFlagsBits.ViewChannel) &&
1384
- me.permissions.has(PermissionFlagsBits.SendMessages) &&
1385
- me.permissions.has(PermissionFlagsBits.EmbedLinks) &&
1386
- me.permissions.has(PermissionFlagsBits.ManageChannels),
1387
- };
1388
- if (fetched.type !== ChannelType.GuildText)
1389
- throw new Error("The tracked verification channel is not a text channel");
1390
- return {
1391
- action: "reuse",
1392
- name: fetched.name,
1393
- channel: fetched,
1394
- usable: verificationChannelUsable(fetched, me),
1395
- };
1396
- }
1397
- const fetched = await guild.channels.fetch(selection.channelId);
1398
- if (!fetched || fetched.guild.id !== guild.id)
1399
- throw new Error("The selected verification channel is unavailable");
1400
- if (fetched.type !== ChannelType.GuildText)
1401
- throw new Error("The selected verification channel is not a text channel");
1402
- return {
1403
- action: "select",
1404
- name: fetched.name,
1405
- channel: fetched,
1406
- usable: verificationChannelUsable(fetched, me),
1407
- };
1408
- }
1409
- function verificationChannelUsable(channel, me) {
1410
- const effective = channel.permissionsFor(me);
1411
- return Boolean(effective?.has(PermissionFlagsBits.ViewChannel) &&
1412
- effective.has(PermissionFlagsBits.SendMessages) &&
1413
- effective.has(PermissionFlagsBits.EmbedLinks));
1414
- }
1415
- function verificationRoleManageable(role, guild, me) {
1416
- return (role.guild.id === guild.id &&
1417
- role.id !== guild.id &&
1418
- !role.managed &&
1419
- !role.permissions.has(PermissionFlagsBits.Administrator) &&
1420
- me.permissions.has(PermissionFlagsBits.ManageRoles) &&
1421
- role.comparePositionTo(me.roles.highest) < 0);
1422
- }
1423
- async function fetchVerificationAccessTarget(guild, targetId) {
1424
- const channel = await guild.channels.fetch(targetId);
1425
- if (!channel || channel.guild.id !== guild.id)
1426
- throw new Error("A selected verification access target is unavailable");
1427
- if (channel.type !== ChannelType.GuildText &&
1428
- channel.type !== ChannelType.GuildAnnouncement &&
1429
- channel.type !== ChannelType.GuildCategory)
1430
- throw new Error("Verification access targets must be text channels or categories");
1431
- return channel;
1432
- }
1433
- async function fetchVerificationTextChannel(guild, channelId) {
1434
- const channel = await guild.channels.fetch(channelId);
1435
- return channel?.guild.id === guild.id &&
1436
- channel.type === ChannelType.GuildText
1437
- ? channel
1438
- : null;
1439
- }
1440
- function verificationPendingRoleName(fingerprint) {
1441
- return `Helyx verify pending ${fingerprint.slice(0, 12)}`;
1442
- }
1443
- function verificationPendingChannelName(fingerprint) {
1444
- return `helyx-verify-pending-${fingerprint.slice(0, 12)}`;
1445
- }
1446
- async function findPendingVerificationRole(guild, fingerprint) {
1447
- const expectedName = verificationPendingRoleName(fingerprint);
1448
- const roles = await guild.roles.fetch();
1449
- return roles.find((role) => role.name === expectedName) ?? null;
1450
- }
1451
- async function findPendingVerificationChannel(guild, fingerprint) {
1452
- const expectedName = verificationPendingChannelName(fingerprint);
1453
- const channels = await guild.channels.fetch();
1454
- const channel = channels.find((candidate) => candidate !== null &&
1455
- candidate.type === ChannelType.GuildText &&
1456
- candidate.name === expectedName);
1457
- return channel?.type === ChannelType.GuildText ? channel : null;
1458
- }
1459
- function verificationManagedOverwriteKey(targetId, roleId) {
1460
- return `${targetId}:${roleId}`;
1461
- }
1462
- async function restoreVerificationViewOverwrite(guild, overwrite, reason) {
1463
- await setVerificationViewOverwrite(guild, overwrite.targetId, overwrite.roleId, overwrite.previousView, reason);
1464
- }
1465
- async function setVerificationViewOverwrite(guild, targetId, roleId, viewChannel, reason) {
1466
- const [fetched, role] = await Promise.all([
1467
- guild.channels.fetch(targetId),
1468
- guild.roles.fetch(roleId),
1469
- ]);
1470
- if (!fetched || !role)
1471
- return;
1472
- if (fetched.guild.id !== guild.id ||
1473
- (fetched.type !== ChannelType.GuildText &&
1474
- fetched.type !== ChannelType.GuildAnnouncement &&
1475
- fetched.type !== ChannelType.GuildCategory))
1476
- throw new Error("A tracked verification access target is invalid");
1477
- if (viewChannelOverwrite(fetched, roleId) === viewChannel)
1478
- return;
1479
- await fetched.permissionOverwrites.edit(roleId, { ViewChannel: viewChannel }, { reason });
1480
- }
1481
- function verificationAccessTargetType(channel) {
1482
- if (channel.type === ChannelType.GuildCategory)
1483
- return "category";
1484
- if (channel.type === ChannelType.GuildAnnouncement)
1485
- return "announcement";
1486
- return "text";
1487
- }
1488
- function viewChannelOverwrite(channel, roleId) {
1489
- const overwrite = channel.permissionOverwrites.cache.get(roleId);
1490
- if (!overwrite)
1491
- return null;
1492
- if (overwrite.allow.has(PermissionFlagsBits.ViewChannel))
1493
- return true;
1494
- if (overwrite.deny.has(PermissionFlagsBits.ViewChannel))
1495
- return false;
1496
- return null;
1497
- }
1498
- function verificationSetupMissingPermissions(input) {
1499
- const missing = [];
1500
- const effective = input.verificationChannel.channel?.permissionsFor(input.me);
1501
- const has = (permission, guildFallback) => effective ? effective.has(permission) : guildFallback;
1502
- if (!has(PermissionFlagsBits.ViewChannel, input.inspection.permissions.viewChannel))
1503
- missing.push("ViewChannel");
1504
- if (!has(PermissionFlagsBits.SendMessages, input.inspection.permissions.sendMessages))
1505
- missing.push("SendMessages");
1506
- if (!has(PermissionFlagsBits.EmbedLinks, input.inspection.permissions.embedLinks))
1507
- missing.push("EmbedLinks");
1508
- if (!input.inspection.permissions.manageRoles)
1509
- missing.push("ManageRoles");
1510
- if (input.needsManageChannels && !input.inspection.permissions.manageChannels)
1511
- missing.push("ManageChannels");
1512
- return missing;
1513
- }
1514
- async function assertVerificationSetupStillSafe(guild, role, channel, input) {
1515
- const me = guild.members.me ?? (await guild.members.fetchMe());
1516
- if (!verificationRoleManageable(role, guild, me))
1517
- throw new Error("The verified role is no longer manageable by Helyx");
1518
- if (!verificationChannelUsable(channel, me))
1519
- throw new Error("The verification channel is no longer usable by Helyx");
1520
- if (input.needsManageChannels &&
1521
- !me.permissions.has(PermissionFlagsBits.ManageChannels))
1522
- throw new Error("Helyx no longer has Manage Channels");
1523
- }
1524
- function validateVerificationSetupRequest(input) {
1525
- validateSnowflake(input.guildId, "guild");
1526
- if (input.verifiedRole.mode === "existing")
1527
- validateSnowflake(input.verifiedRole.roleId, "verified role");
1528
- else {
1529
- validateVerificationRoleName(input.verifiedRole.name);
1530
- if (input.verifiedRole.trackedRoleId)
1531
- validateSnowflake(input.verifiedRole.trackedRoleId, "tracked role");
1532
- }
1533
- if (input.verificationChannel.mode === "existing")
1534
- validateSnowflake(input.verificationChannel.channelId, "verification channel");
1535
- else {
1536
- if (!verificationChannelNamePattern.test(input.verificationChannel.name))
1537
- throw new Error("Verification channel name is invalid");
1538
- if (input.verificationChannel.trackedChannelId)
1539
- validateSnowflake(input.verificationChannel.trackedChannelId, "tracked channel");
1540
- }
1541
- if (input.accessTargetIds.length > verificationSetupMaximumAccessTargets ||
1542
- new Set(input.accessTargetIds).size !== input.accessTargetIds.length)
1543
- throw new Error("Verification access targets must be unique and bounded");
1544
- for (const targetId of input.accessTargetIds)
1545
- validateSnowflake(targetId, "access target");
1546
- }
1547
- function validateVerificationRoleName(name) {
1548
- const normalized = name.trim();
1549
- if (normalized.length === 0 ||
1550
- normalized.length > verificationRoleNameMaximum ||
1551
- /[\r\n]/u.test(normalized) ||
1552
- normalized === "@everyone" ||
1553
- normalized === "@here")
1554
- throw new Error("Verification role name is invalid");
1555
- }
1556
- function verificationSetupFingerprint(request, preview) {
1557
- return createHash("sha256")
1558
- .update(stableVerificationSetupJson({
1559
- protocol: "helyx-verification-setup-v1",
1560
- request,
1561
- preview,
1562
- }))
1563
- .digest("hex");
1564
- }
1565
- function stableVerificationSetupJson(value) {
1566
- if (value === null || typeof value !== "object")
1567
- return JSON.stringify(value);
1568
- if (Array.isArray(value))
1569
- return `[${value.map(stableVerificationSetupJson).join(",")}]`;
1570
- const record = value;
1571
- return `{${Object.keys(record)
1572
- .sort((left, right) => left.localeCompare(right, "en"))
1573
- .map((key) => `${JSON.stringify(key)}:${stableVerificationSetupJson(record[key])}`)
1574
- .join(",")}}`;
1575
- }
1576
920
  export class DiscordMemberRoleService {
1577
921
  client;
1578
922
  constructor(client) {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { parseBotEnvironment } from "@helyx/config";
2
2
  import { safeError, ServiceRegistry } from "@helyx/core";
3
- import { checkDatabaseConnection, createDatabasePool, AuditRepository, InteractionSessionRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, VerificationSetupRepository, } from "@helyx/database";
3
+ import { checkDatabaseConnection, createDatabasePool, AuditRepository, InteractionSessionRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
4
4
  import { createRuntime } from "@helyx/framework";
5
5
  import { createPlatformServices, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
6
6
  import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
@@ -18,7 +18,7 @@ import { loadConfiguredModulePackages, platformServiceOptions, } from "./modules
18
18
  import { resolveModulePackageSpecifiers } from "./module-configuration.js";
19
19
  import { createPermissionsModule } from "./permissions-module.js";
20
20
  import { createManagementModule } from "./management-module.js";
21
- import { DiscordMessageDeliveryService, DiscordMemberEnforcementService, DiscordMemberRoleService, DiscordVerificationMemberService, DiscordVerificationSetupService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
21
+ import { DiscordMessageDeliveryService, DiscordMemberEnforcementService, DiscordMemberRoleService, DiscordVerificationMemberService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
22
22
  import { DiscordModuleLoggingService } from "./module-logging.js";
23
23
  import { BotControlService, createBotControlServer, DiscordGuildResourceDirectory, } from "./control-server.js";
24
24
  import { HttpVerificationService } from "./verification-service.js";
@@ -256,7 +256,6 @@ async function startUnshardedBot(processEnvironment) {
256
256
  publicSigningKeys: environment.HELYX_VERIFICATION_SIGNING_PUBLIC_KEYS,
257
257
  }));
258
258
  platformServices.set(HELYX_SERVICE_NAMES.verificationMembers, new DiscordVerificationMemberService(client));
259
- platformServices.set(HELYX_SERVICE_NAMES.verificationSetup, new DiscordVerificationSetupService(client, new VerificationSetupRepository(database)));
260
259
  }
261
260
  const numericWriteBehind = createLocalNumericWriteBehind(platformServices, publicModules);
262
261
  platformServices.set(HELYX_SERVICE_NAMES.numericWriteBehind, numericWriteBehind);
@@ -1,6 +1,6 @@
1
1
  import { parseBotEnvironment } from "@helyx/config";
2
2
  import { safeError, ServiceRegistry } from "@helyx/core";
3
- import { checkDatabaseConnection, createDatabasePool, VerificationSetupRepository, } from "@helyx/database";
3
+ import { checkDatabaseConnection, createDatabasePool } from "@helyx/database";
4
4
  import { createRuntime } from "@helyx/framework";
5
5
  import { createPlatformServices } from "@helyx/platform";
6
6
  import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
@@ -10,7 +10,7 @@ import pino from "pino";
10
10
  import { createCloudManagementModule } from "./cloud-management-module.js";
11
11
  import { BotControlService, DiscordGuildResourceDirectory, mapBotControlError, } from "./control-server.js";
12
12
  import { HttpVerificationService } from "./verification-service.js";
13
- import { DiscordMessageDeliveryService, DiscordMemberEnforcementService, DiscordMemberRoleService, DiscordVerificationMemberService, DiscordVerificationSetupService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
13
+ import { DiscordMessageDeliveryService, DiscordMemberEnforcementService, DiscordMemberRoleService, DiscordVerificationMemberService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
14
14
  import { installGuildTracking, } from "./guilds.js";
15
15
  import { installInteractions, } from "./interactions.js";
16
16
  import { createManagementModule } from "./management-module.js";
@@ -181,7 +181,6 @@ export async function startShardWorker(processEnvironment = process.env, identit
181
181
  publicSigningKeys: environment.HELYX_VERIFICATION_SIGNING_PUBLIC_KEYS,
182
182
  }));
183
183
  platformServices.set(HELYX_SERVICE_NAMES.verificationMembers, new DiscordVerificationMemberService(client));
184
- platformServices.set(HELYX_SERVICE_NAMES.verificationSetup, new DiscordVerificationSetupService(client, new VerificationSetupRepository(database)));
185
184
  }
186
185
  numericWriteBehind =
187
186
  identity.scope === "coordinated"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/bot",
3
- "version": "0.4.16",
3
+ "version": "0.4.17",
4
4
  "description": "Production Discord runtime for the modular Helyx platform.",
5
5
  "keywords": [
6
6
  "helyx",
@@ -50,13 +50,13 @@
50
50
  "dependencies": {
51
51
  "@helyx/config": "0.5.0",
52
52
  "@helyx/control-protocol": "0.3.2",
53
- "@helyx/core": "0.1.12",
54
- "@helyx/database": "0.6.0",
55
- "@helyx/framework": "0.1.12",
53
+ "@helyx/core": "0.1.13",
54
+ "@helyx/database": "0.7.0",
55
+ "@helyx/framework": "0.1.13",
56
56
  "@helyx/module-manifest": "0.6.0",
57
- "@helyx/platform": "0.4.0",
58
- "@helyx/sdk": "0.5.0",
59
- "@helyx/verification-contracts": "0.1.0",
57
+ "@helyx/platform": "0.4.1",
58
+ "@helyx/sdk": "0.6.0",
59
+ "@helyx/verification-contracts": "0.2.0",
60
60
  "discord.js": "14.27.0",
61
61
  "fastify": "5.10.0",
62
62
  "pino": "10.3.1",