@helyx/bot 0.4.46 → 0.6.1
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 +79 -0
- package/README.md +4 -4
- package/dist/cloud-management-module.js +1 -5
- package/dist/command-path-contract.d.ts +26 -0
- package/dist/command-path-contract.js +157 -0
- package/dist/command-registration-policy.js +1 -0
- package/dist/control-permissions.js +1 -0
- package/dist/control-server.d.ts +2 -34
- package/dist/control-server.js +31 -43
- package/dist/dashboard-commands.d.ts +1 -0
- package/dist/dashboard-commands.js +6 -35
- package/dist/dashboard-module-projection.d.ts +39 -0
- package/dist/dashboard-module-projection.js +28 -0
- package/dist/discord-analytics.d.ts +30 -0
- package/dist/discord-analytics.js +337 -0
- package/dist/discord-invites.d.ts +24 -0
- package/dist/discord-invites.js +91 -0
- package/dist/events.d.ts +1 -1
- package/dist/events.js +78 -22
- package/dist/guilds.js +1 -4
- package/dist/index.js +49 -8
- package/dist/interactions.d.ts +1 -1
- package/dist/interactions.js +89 -68
- package/dist/management-module.d.ts +1 -1
- package/dist/module-logging.js +1 -4
- package/dist/module-status-control.js +1 -1
- package/dist/modules.d.ts +2 -0
- package/dist/modules.js +27 -4
- package/dist/permissions-module.js +13 -16
- package/dist/premium-access.d.ts +7 -0
- package/dist/premium-access.js +38 -0
- package/dist/premium-activation-module.d.ts +20 -0
- package/dist/premium-activation-module.js +542 -0
- package/dist/premium-catalogue-enforcement.d.ts +5 -0
- package/dist/premium-catalogue-enforcement.js +29 -0
- package/dist/premium-guild-authority.d.ts +12 -0
- package/dist/premium-guild-authority.js +25 -0
- package/dist/scheduled-task-runner.js +14 -1
- package/dist/shard-contracts.js +1 -1
- package/dist/shard-supervisor.js +30 -1
- package/dist/shard-worker.d.ts +2 -0
- package/dist/shard-worker.js +36 -4
- package/documentation/examples/.env.example +2 -2
- package/package.json +6 -6
package/dist/interactions.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { lookup as lookupDns } from "node:dns/promises";
|
|
2
2
|
import { HELYX_SERVICE_NAMES, NexusEconomyError, UserFacingInteractionError, } from "@helyx/sdk";
|
|
3
|
-
import { canUseCommand } from "@helyx/core";
|
|
3
|
+
import { canUseCommand, safeError } from "@helyx/core";
|
|
4
4
|
import { commandRegistrationPolicy, contributionRegistrationPolicy, } from "./command-registration-policy.js";
|
|
5
5
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, EmbedBuilder, Events, GuildMember, ApplicationIntegrationType, ChannelType, InteractionContextType, LabelBuilder, MessageFlags, MentionableSelectMenuBuilder, ModalBuilder, RoleSelectMenuBuilder, Routes, StringSelectMenuBuilder, SlashCommandBuilder, TextInputBuilder, TextInputStyle, UserSelectMenuBuilder, TextDisplayBuilder, PermissionFlagsBits, escapeMarkdown, } from "discord.js";
|
|
6
6
|
import { collectCommandRegistrations, } from "./shared-commands.js";
|
|
7
|
+
import { resolveDirectCommandPath } from "./command-path-contract.js";
|
|
8
|
+
import { canAccessModule, PREMIUM_MODULE_UNAVAILABLE_MESSAGE, } from "./premium-access.js";
|
|
7
9
|
const buttonStyles = {
|
|
8
10
|
primary: ButtonStyle.Primary,
|
|
9
11
|
secondary: ButtonStyle.Secondary,
|
|
@@ -108,11 +110,8 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
108
110
|
if (!registered ||
|
|
109
111
|
!subcommand?.autocomplete ||
|
|
110
112
|
!ownerModuleId ||
|
|
111
|
-
!(await authorizeAutocomplete(interaction, ownerModuleId, subcommand.permissionId ??
|
|
112
|
-
|
|
113
|
-
.filter(Boolean)
|
|
114
|
-
.join("."), services, registration.selfAuthorizingGuildModuleIds?.has(ownerModuleId) ??
|
|
115
|
-
false, subcommand.authorization === "resource-scoped", isInstallationIndependentModule(modulesById, ownerModuleId), subcommand.authorization))) {
|
|
113
|
+
!(await authorizeAutocomplete(interaction, ownerModuleId, resolveDirectCommandPath(registered.contribution, subcommand, groupName).permissionId, services, registration.selfAuthorizingGuildModuleIds?.has(ownerModuleId) ??
|
|
114
|
+
false, subcommand.authorization === "resource-scoped", isInstallationIndependentModule(modulesById, ownerModuleId), subcommand.authorization, requiredCommandModule(modulesById, ownerModuleId)))) {
|
|
116
115
|
await response.respond([]);
|
|
117
116
|
return;
|
|
118
117
|
}
|
|
@@ -175,12 +174,7 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
175
174
|
});
|
|
176
175
|
return;
|
|
177
176
|
}
|
|
178
|
-
const
|
|
179
|
-
? [command.name, groupName, subcommandName]
|
|
180
|
-
.filter(Boolean)
|
|
181
|
-
.join(".")
|
|
182
|
-
: command.name;
|
|
183
|
-
const permissionId = subcommand?.permissionId ?? command.permissionId ?? commandId;
|
|
177
|
+
const { permissionId } = resolveDirectCommandPath(command, subcommand, groupName);
|
|
184
178
|
const owner = resolveCommandOwner(registered, subcommandName, groupName);
|
|
185
179
|
const ownerModuleId = owner?.moduleId ?? moduleId;
|
|
186
180
|
const installationIndependentCommand = isInstallationIndependentModule(modulesById, ownerModuleId);
|
|
@@ -197,7 +191,7 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
197
191
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
198
192
|
}
|
|
199
193
|
if (await authorizeCommand(interaction, ownerModuleId, permissionId, rateLimit, services, registration.selfAuthorizingGuildModuleIds?.has(ownerModuleId) ??
|
|
200
|
-
false, subcommand?.authorization === "resource-scoped", owner?.moduleName, installationIndependentCommand, authorization)) {
|
|
194
|
+
false, subcommand?.authorization === "resource-scoped", owner?.moduleName, installationIndependentCommand, authorization, requiredCommandModule(modulesById, ownerModuleId))) {
|
|
201
195
|
const context = createCommandContext(interaction, services);
|
|
202
196
|
await execute(context);
|
|
203
197
|
}
|
|
@@ -224,12 +218,7 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
224
218
|
}
|
|
225
219
|
if (button &&
|
|
226
220
|
context &&
|
|
227
|
-
button.
|
|
228
|
-
await button.contribution.execute(context);
|
|
229
|
-
}
|
|
230
|
-
else if (button &&
|
|
231
|
-
context &&
|
|
232
|
-
(await authorizeComponent(interaction, button.moduleId, services, undefined, isInstallationIndependentModule(modulesById, button.moduleId)))) {
|
|
221
|
+
(await authorizeComponent(interaction, button.moduleId, services, undefined, isInstallationIndependentModule(modulesById, button.moduleId), modulesById.get(button.moduleId)))) {
|
|
233
222
|
await button.contribution.execute(context);
|
|
234
223
|
}
|
|
235
224
|
}
|
|
@@ -246,7 +235,7 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
246
235
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
247
236
|
}
|
|
248
237
|
if (modal &&
|
|
249
|
-
(await authorizeComponent(interaction, modal.moduleId, services, undefined, isInstallationIndependentModule(modulesById, modal.moduleId)))) {
|
|
238
|
+
(await authorizeComponent(interaction, modal.moduleId, services, undefined, isInstallationIndependentModule(modulesById, modal.moduleId), modulesById.get(modal.moduleId)))) {
|
|
250
239
|
await modal.contribution.execute(createModalContext(interaction, services));
|
|
251
240
|
}
|
|
252
241
|
}
|
|
@@ -271,7 +260,7 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
271
260
|
}
|
|
272
261
|
if (select &&
|
|
273
262
|
context &&
|
|
274
|
-
(await authorizeComponent(interaction, select.moduleId, services, undefined, isInstallationIndependentModule(modulesById, select.moduleId)))) {
|
|
263
|
+
(await authorizeComponent(interaction, select.moduleId, services, undefined, isInstallationIndependentModule(modulesById, select.moduleId), modulesById.get(select.moduleId)))) {
|
|
275
264
|
await select.contribution.execute(context);
|
|
276
265
|
}
|
|
277
266
|
}
|
|
@@ -515,6 +504,8 @@ function addCommandOption(builder, option) {
|
|
|
515
504
|
guild_text: ChannelType.GuildText,
|
|
516
505
|
guild_announcement: ChannelType.GuildAnnouncement,
|
|
517
506
|
guild_category: ChannelType.GuildCategory,
|
|
507
|
+
guild_forum: ChannelType.GuildForum,
|
|
508
|
+
guild_media: ChannelType.GuildMedia,
|
|
518
509
|
};
|
|
519
510
|
builder.addChannelOption((optionBuilder) => {
|
|
520
511
|
optionBuilder
|
|
@@ -862,19 +853,31 @@ export async function addChannelMessageReaction(client, channelId, messageId, em
|
|
|
862
853
|
const message = await channel.messages.fetch(messageId);
|
|
863
854
|
await message.react(emojiIdentifier);
|
|
864
855
|
}
|
|
865
|
-
async function authorizeComponent(interaction, moduleId, services, requiredModuleName, installationIndependent = false) {
|
|
856
|
+
async function authorizeComponent(interaction, moduleId, services, requiredModuleName, installationIndependent = false, module) {
|
|
866
857
|
if (installationIndependent)
|
|
867
858
|
return true;
|
|
868
|
-
|
|
859
|
+
const installations = interaction.guildId
|
|
860
|
+
? services.get(HELYX_SERVICE_NAMES.installations)
|
|
861
|
+
: undefined;
|
|
862
|
+
const [globallyAvailable, moduleEnabled, premiumAccessible] = await Promise.all([
|
|
863
|
+
moduleIsGloballyAvailable(services, moduleId),
|
|
864
|
+
installations
|
|
865
|
+
? installations.isModuleEnabled(interaction.guildId, moduleId)
|
|
866
|
+
: Promise.resolve(true),
|
|
867
|
+
interaction.guildId && module
|
|
868
|
+
? canAccessModule(services, interaction.guildId, module)
|
|
869
|
+
: Promise.resolve(true),
|
|
870
|
+
]);
|
|
871
|
+
if (!globallyAvailable) {
|
|
869
872
|
await replyEphemeralText(interaction, GLOBAL_MODULE_UNAVAILABLE_MESSAGE);
|
|
870
873
|
return false;
|
|
871
874
|
}
|
|
872
|
-
if (!
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
if (await installations.isModuleEnabled(interaction.guildId, moduleId)) {
|
|
876
|
-
return true;
|
|
875
|
+
if (!premiumAccessible) {
|
|
876
|
+
await replyEphemeralText(interaction, PREMIUM_MODULE_UNAVAILABLE_MESSAGE);
|
|
877
|
+
return false;
|
|
877
878
|
}
|
|
879
|
+
if (moduleEnabled)
|
|
880
|
+
return true;
|
|
878
881
|
await replyEphemeralText(interaction, requiredModuleName
|
|
879
882
|
? `This command needs ${requiredModuleName}. Ask a server administrator to enable it in the Helyx dashboard.`
|
|
880
883
|
: "This module is disabled in this server. Ask a server administrator to enable it with `/helyx` or in the Helyx dashboard.");
|
|
@@ -887,22 +890,29 @@ async function replyEphemeralText(interaction, content) {
|
|
|
887
890
|
}
|
|
888
891
|
await interaction.reply({ content, flags: MessageFlags.Ephemeral });
|
|
889
892
|
}
|
|
890
|
-
export async function authorizeCommand(interaction, moduleId, commandId, rateLimit, services, selfAuthorizingGuildCommand, resourceScopedCommand = false, requiredModuleName, installationIndependentCommand = false, authorization) {
|
|
893
|
+
export async function authorizeCommand(interaction, moduleId, commandId, rateLimit, services, selfAuthorizingGuildCommand, resourceScopedCommand = false, requiredModuleName, installationIndependentCommand = false, authorization, module) {
|
|
891
894
|
const bypassPlatformAuthorization = authorization === "public" || authorization === "self-authorized";
|
|
892
895
|
if (!installationIndependentCommand && !selfAuthorizingGuildCommand) {
|
|
893
896
|
const installations = interaction.guildId
|
|
894
897
|
? services.get(HELYX_SERVICE_NAMES.installations)
|
|
895
898
|
: undefined;
|
|
896
|
-
const [globallyAvailable, moduleEnabled] = await Promise.all([
|
|
899
|
+
const [globallyAvailable, moduleEnabled, premiumAccessible] = await Promise.all([
|
|
897
900
|
moduleIsGloballyAvailable(services, moduleId),
|
|
898
901
|
installations && !selfAuthorizingGuildCommand
|
|
899
902
|
? installations.isModuleEnabled(interaction.guildId, moduleId)
|
|
900
903
|
: Promise.resolve(true),
|
|
904
|
+
interaction.guildId && module
|
|
905
|
+
? canAccessModule(services, interaction.guildId, module)
|
|
906
|
+
: Promise.resolve(true),
|
|
901
907
|
]);
|
|
902
908
|
if (!globallyAvailable) {
|
|
903
909
|
await replyEphemeralText(interaction, GLOBAL_MODULE_UNAVAILABLE_MESSAGE);
|
|
904
910
|
return false;
|
|
905
911
|
}
|
|
912
|
+
if (!premiumAccessible) {
|
|
913
|
+
await replyEphemeralText(interaction, PREMIUM_MODULE_UNAVAILABLE_MESSAGE);
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
906
916
|
if (!moduleEnabled) {
|
|
907
917
|
await replyEphemeralText(interaction, requiredModuleName
|
|
908
918
|
? `This command needs ${requiredModuleName}. Ask a server administrator to enable it in the Helyx dashboard.`
|
|
@@ -944,19 +954,22 @@ export async function authorizeCommand(interaction, moduleId, commandId, rateLim
|
|
|
944
954
|
}
|
|
945
955
|
return true;
|
|
946
956
|
}
|
|
947
|
-
async function authorizeAutocomplete(interaction, moduleId, commandId, services, selfAuthorizingGuildCommand, resourceScopedCommand, installationIndependentCommand = false, authorization) {
|
|
957
|
+
async function authorizeAutocomplete(interaction, moduleId, commandId, services, selfAuthorizingGuildCommand, resourceScopedCommand, installationIndependentCommand = false, authorization, module) {
|
|
948
958
|
const bypassPlatformAuthorization = authorization === "public" || authorization === "self-authorized";
|
|
949
959
|
if (!installationIndependentCommand && !selfAuthorizingGuildCommand) {
|
|
950
960
|
const installations = interaction.guildId
|
|
951
961
|
? services.get(HELYX_SERVICE_NAMES.installations)
|
|
952
962
|
: undefined;
|
|
953
|
-
const [globallyAvailable, moduleEnabled] = await Promise.all([
|
|
963
|
+
const [globallyAvailable, moduleEnabled, premiumAccessible] = await Promise.all([
|
|
954
964
|
moduleIsGloballyAvailable(services, moduleId),
|
|
955
965
|
installations && !selfAuthorizingGuildCommand
|
|
956
966
|
? installations.isModuleEnabled(interaction.guildId, moduleId)
|
|
957
967
|
: Promise.resolve(true),
|
|
968
|
+
interaction.guildId && module
|
|
969
|
+
? canAccessModule(services, interaction.guildId, module)
|
|
970
|
+
: Promise.resolve(true),
|
|
958
971
|
]);
|
|
959
|
-
if (!globallyAvailable || !moduleEnabled)
|
|
972
|
+
if (!globallyAvailable || !moduleEnabled || !premiumAccessible)
|
|
960
973
|
return false;
|
|
961
974
|
}
|
|
962
975
|
if (bypassPlatformAuthorization ||
|
|
@@ -1053,27 +1066,37 @@ export function toReplyOptions(response) {
|
|
|
1053
1066
|
],
|
|
1054
1067
|
};
|
|
1055
1068
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1069
|
+
if (response.embed && response.embeds)
|
|
1070
|
+
throw new Error("Message responses cannot use both embed and embeds");
|
|
1071
|
+
const embeds = (response.embeds ?? (response.embed ? [response.embed] : [])).map((value) => {
|
|
1072
|
+
const embed = new EmbedBuilder().setColor(value.color ?? 0x7c3aed);
|
|
1073
|
+
if (value.title)
|
|
1074
|
+
embed.setTitle(value.title);
|
|
1075
|
+
if (value.description)
|
|
1076
|
+
embed.setDescription(value.description);
|
|
1077
|
+
if (value.footer)
|
|
1078
|
+
embed.setFooter({
|
|
1079
|
+
text: value.footer,
|
|
1080
|
+
...(value.footerIconUrl ? { iconURL: value.footerIconUrl } : {}),
|
|
1081
|
+
});
|
|
1082
|
+
if (value.url)
|
|
1083
|
+
embed.setURL(value.url);
|
|
1084
|
+
if (value.author)
|
|
1085
|
+
embed.setAuthor({
|
|
1086
|
+
name: value.author.name,
|
|
1087
|
+
...(value.author.url ? { url: value.author.url } : {}),
|
|
1088
|
+
...(value.author.iconUrl ? { iconURL: value.author.iconUrl } : {}),
|
|
1089
|
+
});
|
|
1090
|
+
if (value.timestamp)
|
|
1091
|
+
embed.setTimestamp(new Date(value.timestamp));
|
|
1092
|
+
if (value.imageUrl)
|
|
1093
|
+
embed.setImage(value.imageUrl);
|
|
1094
|
+
if (value.thumbnailUrl)
|
|
1095
|
+
embed.setThumbnail(value.thumbnailUrl);
|
|
1096
|
+
if (value.fields?.length)
|
|
1097
|
+
embed.addFields(...value.fields);
|
|
1098
|
+
return embed;
|
|
1099
|
+
});
|
|
1077
1100
|
const components = response.buttons?.length
|
|
1078
1101
|
? [
|
|
1079
1102
|
new ActionRowBuilder().addComponents(response.buttons.map((button) => {
|
|
@@ -1096,15 +1119,20 @@ export function toReplyOptions(response) {
|
|
|
1096
1119
|
: [];
|
|
1097
1120
|
components.push(...selectRows);
|
|
1098
1121
|
return {
|
|
1099
|
-
...(response.ephemeral ||
|
|
1122
|
+
...(response.ephemeral ||
|
|
1123
|
+
response.suppressEmbeds ||
|
|
1124
|
+
response.suppressNotifications
|
|
1100
1125
|
? {
|
|
1101
1126
|
flags: (response.ephemeral ? MessageFlags.Ephemeral : 0) |
|
|
1102
|
-
(response.suppressEmbeds ? MessageFlags.SuppressEmbeds : 0)
|
|
1127
|
+
(response.suppressEmbeds ? MessageFlags.SuppressEmbeds : 0) |
|
|
1128
|
+
(response.suppressNotifications
|
|
1129
|
+
? MessageFlags.SuppressNotifications
|
|
1130
|
+
: 0),
|
|
1103
1131
|
}
|
|
1104
1132
|
: {}),
|
|
1105
1133
|
...(allowedMentions ? { allowedMentions } : {}),
|
|
1106
1134
|
...(response.content === undefined ? {} : { content: response.content }),
|
|
1107
|
-
...(
|
|
1135
|
+
...(embeds.length > 0 ? { embeds } : {}),
|
|
1108
1136
|
components,
|
|
1109
1137
|
};
|
|
1110
1138
|
}
|
|
@@ -1299,6 +1327,10 @@ function discordChannelType(type) {
|
|
|
1299
1327
|
return ChannelType.GuildAnnouncement;
|
|
1300
1328
|
if (type === "guild_category")
|
|
1301
1329
|
return ChannelType.GuildCategory;
|
|
1330
|
+
if (type === "guild_forum")
|
|
1331
|
+
return ChannelType.GuildForum;
|
|
1332
|
+
if (type === "guild_media")
|
|
1333
|
+
return ChannelType.GuildMedia;
|
|
1302
1334
|
return ChannelType.GuildText;
|
|
1303
1335
|
}
|
|
1304
1336
|
export function safeInteractionFailureMessage(error) {
|
|
@@ -1351,17 +1383,6 @@ async function sendFailure(interaction, error) {
|
|
|
1351
1383
|
.reply({ content: message, flags: MessageFlags.Ephemeral })
|
|
1352
1384
|
.catch(() => undefined);
|
|
1353
1385
|
}
|
|
1354
|
-
function safeError(error) {
|
|
1355
|
-
const value = error instanceof Error ? error : new Error(String(error));
|
|
1356
|
-
return {
|
|
1357
|
-
name: value.name,
|
|
1358
|
-
message: value.message,
|
|
1359
|
-
...("code" in value &&
|
|
1360
|
-
(typeof value.code === "string" || typeof value.code === "number")
|
|
1361
|
-
? { code: value.code }
|
|
1362
|
-
: {}),
|
|
1363
|
-
};
|
|
1364
|
-
}
|
|
1365
1386
|
function interactionLogContext(interaction) {
|
|
1366
1387
|
return {
|
|
1367
1388
|
interactionId: interaction.id,
|
|
@@ -2,7 +2,7 @@ import { type ModuleDefinition } from "@helyx/sdk";
|
|
|
2
2
|
export interface ManagementGuildResources {
|
|
3
3
|
channels: readonly {
|
|
4
4
|
id: string;
|
|
5
|
-
type: "guild_text" | "guild_announcement" | "guild_category";
|
|
5
|
+
type: "guild_text" | "guild_announcement" | "guild_category" | "guild_forum" | "guild_media";
|
|
6
6
|
}[];
|
|
7
7
|
roles: readonly {
|
|
8
8
|
id: string;
|
package/dist/module-logging.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { InstallationRepository, ModuleConfigurationRepository, ModuleLoggingPolicyRepository, } from "@helyx/database";
|
|
2
|
+
import { safeError } from "@helyx/core";
|
|
2
3
|
import { toReplyOptions } from "./interactions.js";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
4
5
|
const LOGGING_MODULE_ID = "helyx.logging";
|
|
@@ -108,8 +109,4 @@ function normalizeText(value, maximum, label) {
|
|
|
108
109
|
}
|
|
109
110
|
return normalized;
|
|
110
111
|
}
|
|
111
|
-
function safeError(error) {
|
|
112
|
-
const value = error instanceof Error ? error : new Error(String(error));
|
|
113
|
-
return { name: value.name, message: value.message };
|
|
114
|
-
}
|
|
115
112
|
//# sourceMappingURL=module-logging.js.map
|
|
@@ -13,7 +13,7 @@ export async function mutateDashboardModuleStatus(input) {
|
|
|
13
13
|
input.services.has(HELYX_SERVICE_NAMES.numericWriteBehind)
|
|
14
14
|
? input.services.get(HELYX_SERVICE_NAMES.numericWriteBehind)
|
|
15
15
|
: null;
|
|
16
|
-
if (
|
|
16
|
+
if (change.enabled)
|
|
17
17
|
await input.requireModuleAvailable();
|
|
18
18
|
let mutationResult;
|
|
19
19
|
const setEnabled = async () => {
|
package/dist/modules.d.ts
CHANGED
|
@@ -20,8 +20,10 @@ export declare function deriveSecureRandomSelectionKey(encodedDataEncryptionKey:
|
|
|
20
20
|
export declare function platformServiceOptions(environment: PlatformServiceEnvironment, modules: readonly ModuleDefinition[]): {
|
|
21
21
|
applicationId?: string;
|
|
22
22
|
secureRandomSelectionKey?: Uint8Array;
|
|
23
|
+
inviteFingerprintKey?: Uint8Array;
|
|
23
24
|
dashboardBaseUrl?: string;
|
|
24
25
|
};
|
|
26
|
+
export declare function deriveInviteFingerprintKey(selectionKey: Uint8Array): Uint8Array;
|
|
25
27
|
export declare function loadConfiguredModules(specifiers: readonly string[], importer?: ModuleImporter): Promise<ModuleDefinition[]>;
|
|
26
28
|
export declare function loadConfiguredModulePackages(specifiers: readonly string[], importer?: ModuleImporter, resolvePackageJson?: PackageJsonResolver, readPackageJson?: PackageJsonReader): Promise<ConfiguredModulePackage[]>;
|
|
27
29
|
export declare function validateContributionParity(definition: ModuleDefinition): void;
|
package/dist/modules.js
CHANGED
|
@@ -4,10 +4,13 @@ import { createHmac } from "node:crypto";
|
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { dirname } from "node:path";
|
|
7
|
+
import { assertCommandPathContract } from "./command-path-contract.js";
|
|
7
8
|
const officialModulePackage = /^@helyx\/module-[a-z0-9][a-z0-9-]*$/u;
|
|
8
9
|
const giveawaysModuleId = "helyx.giveaways";
|
|
10
|
+
const inviteTrackingModuleId = "helyx.invite-tracking";
|
|
9
11
|
export const secureRandomSelectionWorkerKeyName = "HELYX_INTERNAL_SECURE_RANDOM_SELECTION_KEY";
|
|
10
12
|
const secureRandomSelectionKeyContext = "helyx.secure-random-selection.worker.v1";
|
|
13
|
+
const inviteFingerprintKeyContext = "helyx.discord-invite-fingerprint.v1";
|
|
11
14
|
function decodeSecureRandomSelectionKey(encodedKey, variableName) {
|
|
12
15
|
const key = Buffer.from(encodedKey, "base64");
|
|
13
16
|
if (key.byteLength !== 32) {
|
|
@@ -30,25 +33,38 @@ export function platformServiceOptions(environment, modules) {
|
|
|
30
33
|
: {};
|
|
31
34
|
const internalSelectionKey = environment.HELYX_INTERNAL_SECURE_RANDOM_SELECTION_KEY;
|
|
32
35
|
if (internalSelectionKey) {
|
|
36
|
+
const selectionKey = decodeSecureRandomSelectionKey(internalSelectionKey, secureRandomSelectionWorkerKeyName);
|
|
33
37
|
return {
|
|
34
38
|
...application,
|
|
35
39
|
...urls,
|
|
36
|
-
secureRandomSelectionKey:
|
|
40
|
+
secureRandomSelectionKey: selectionKey,
|
|
41
|
+
inviteFingerprintKey: deriveInviteFingerprintKey(selectionKey),
|
|
37
42
|
};
|
|
38
43
|
}
|
|
39
44
|
const encodedKey = environment.HELYX_DATA_ENCRYPTION_KEY;
|
|
40
45
|
if (!encodedKey) {
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
const keyRequired = modules.some(({ manifest }) => manifest.id === giveawaysModuleId ||
|
|
47
|
+
manifest.id === inviteTrackingModuleId);
|
|
48
|
+
if (keyRequired) {
|
|
49
|
+
throw new Error("HELYX_DATA_ENCRYPTION_KEY is required when Giveaways or Invite Tracking is loaded");
|
|
43
50
|
}
|
|
44
51
|
return { ...application, ...urls };
|
|
45
52
|
}
|
|
53
|
+
const selectionKey = deriveSecureRandomSelectionKey(encodedKey);
|
|
46
54
|
return {
|
|
47
55
|
...application,
|
|
48
56
|
...urls,
|
|
49
|
-
secureRandomSelectionKey:
|
|
57
|
+
secureRandomSelectionKey: selectionKey,
|
|
58
|
+
inviteFingerprintKey: deriveInviteFingerprintKey(selectionKey),
|
|
50
59
|
};
|
|
51
60
|
}
|
|
61
|
+
export function deriveInviteFingerprintKey(selectionKey) {
|
|
62
|
+
if (selectionKey.byteLength !== 32)
|
|
63
|
+
throw new Error("Secure random selection key must contain exactly 32 bytes");
|
|
64
|
+
return createHmac("sha256", selectionKey)
|
|
65
|
+
.update(inviteFingerprintKeyContext, "utf8")
|
|
66
|
+
.digest();
|
|
67
|
+
}
|
|
52
68
|
function isModuleDefinition(value) {
|
|
53
69
|
if (typeof value !== "object" || value === null)
|
|
54
70
|
return false;
|
|
@@ -165,13 +181,20 @@ export function validateContributionParity(definition) {
|
|
|
165
181
|
...(definition.events?.guildEmojiUpdate ?? []),
|
|
166
182
|
].map(({ type }) => type);
|
|
167
183
|
const executableLogs = (definition.logging?.events ?? []).map(({ id }) => id);
|
|
184
|
+
const executableSchedulableActions = [
|
|
185
|
+
...new Set((definition.schedulableActions ?? []).map(({ id }) => id)),
|
|
186
|
+
];
|
|
168
187
|
const executableDashboardActions = (definition.dashboardActions ?? []).map(({ id }) => id);
|
|
169
188
|
const executableManagedResources = (definition.managedResources ?? []).map(({ id }) => id);
|
|
170
189
|
assertSameContributionIds(definition.manifest.contributions.commands, executableCommands, "commands");
|
|
171
190
|
assertSameContributionIds(definition.manifest.contributions.events, executableEvents, "events");
|
|
172
191
|
assertSameContributionIds(definition.manifest.contributions.logs ?? [], executableLogs, "logs");
|
|
192
|
+
assertSameContributionIds(definition.manifest.contributions.schedulableActions ?? [], executableSchedulableActions, "schedulable actions");
|
|
173
193
|
assertSameContributionIds(definition.manifest.dashboard?.actions?.map(({ id }) => id) ?? [], executableDashboardActions, "dashboard actions");
|
|
174
194
|
assertSameContributionIds(definition.manifest.dashboard?.resources?.map(({ id }) => id) ?? [], executableManagedResources, "dashboard resources");
|
|
195
|
+
if (definition.manifest.manifestVersion === "0.4") {
|
|
196
|
+
assertCommandPathContract(definition);
|
|
197
|
+
}
|
|
175
198
|
validatePrivacyParity(definition);
|
|
176
199
|
}
|
|
177
200
|
function validatePrivacyParity(definition) {
|
|
@@ -2,6 +2,7 @@ import { defineModule, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
|
2
2
|
import { isCustomerSelectableModule } from "@helyx/module-manifest";
|
|
3
3
|
import { PermissionFlagsBits } from "discord.js";
|
|
4
4
|
import { canMutatePermissionPolicy, permissionMutationDenialMessage, } from "@helyx/core";
|
|
5
|
+
import { collectCommandPermissionTargets } from "./command-path-contract.js";
|
|
5
6
|
import { INTERNAL_MODULE_MANIFEST_FIELDS } from "./internal-module-manifest.js";
|
|
6
7
|
const MODULE_SELECT_ID = "helyx.permissions.module";
|
|
7
8
|
const TARGET_SELECT_ID = "helyx.permissions.target";
|
|
@@ -129,7 +130,8 @@ export function createPermissionsModule(configurableModules) {
|
|
|
129
130
|
if (!module || !target)
|
|
130
131
|
throw new Error("Permission target expired");
|
|
131
132
|
const commandId = target === "__module__" ? undefined : target;
|
|
132
|
-
if (commandId &&
|
|
133
|
+
if (commandId &&
|
|
134
|
+
!commandTargets(module).some(({ id }) => id === commandId)) {
|
|
133
135
|
throw new Error("Selected command is unavailable");
|
|
134
136
|
}
|
|
135
137
|
const current = await context.services
|
|
@@ -437,17 +439,12 @@ function restrictedPolicy() {
|
|
|
437
439
|
};
|
|
438
440
|
}
|
|
439
441
|
function commandTargets(module) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
]
|
|
445
|
-
|
|
446
|
-
return nested;
|
|
447
|
-
return [command.name];
|
|
448
|
-
});
|
|
449
|
-
targets.push(...(module.sharedCommands ?? []).map((command) => command.permissionId ?? `${command.family}.${command.path}`), ...(module.nexusCommands ?? []).map((command) => `nexus.${command.path}`));
|
|
450
|
-
return [...new Set(targets)];
|
|
442
|
+
return [
|
|
443
|
+
...new Map(collectCommandPermissionTargets(module).map(({ id, name }) => [
|
|
444
|
+
id,
|
|
445
|
+
{ id, label: name },
|
|
446
|
+
])).values(),
|
|
447
|
+
];
|
|
451
448
|
}
|
|
452
449
|
function modulePickerResponse(modules, requestedPage) {
|
|
453
450
|
const pages = pageCount(modules.length, MODULE_PAGE_SIZE);
|
|
@@ -483,7 +480,7 @@ function targetPickerResponse(module, targets, requestedPage) {
|
|
|
483
480
|
ephemeral: true,
|
|
484
481
|
embed: {
|
|
485
482
|
title: `${module.manifest.name} permissions`,
|
|
486
|
-
description: "Choose the whole module or
|
|
483
|
+
description: "Choose the whole module, a command, or a declared command operation. Target policies override the module policy." +
|
|
487
484
|
(pages > 1 ? ` Page ${String(page + 1)} of ${String(pages)}.` : ""),
|
|
488
485
|
color: VIOLET,
|
|
489
486
|
},
|
|
@@ -495,11 +492,11 @@ function targetPickerResponse(module, targets, requestedPage) {
|
|
|
495
492
|
{
|
|
496
493
|
label: "Entire module",
|
|
497
494
|
value: "__module__",
|
|
498
|
-
description: "Default policy for every command",
|
|
495
|
+
description: "Default policy for every command and operation",
|
|
499
496
|
},
|
|
500
497
|
...visible.map((target) => ({
|
|
501
|
-
label:
|
|
502
|
-
value: target,
|
|
498
|
+
label: target.label,
|
|
499
|
+
value: target.id,
|
|
503
500
|
})),
|
|
504
501
|
],
|
|
505
502
|
},
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ModuleDefinition, type ServiceAccess } from "@helyx/sdk";
|
|
2
|
+
export declare const PREMIUM_MODULE_UNAVAILABLE_MESSAGE = "This Premium module is not assigned to this server. A Premium subscriber can assign a slot in Billing or with `/activate`.";
|
|
3
|
+
export declare function moduleRequiresPremium(module: ModuleDefinition): boolean;
|
|
4
|
+
export declare function requirePremiumProviderEnvironment(modules: readonly ModuleDefinition[], environment: "sandbox" | "live" | undefined): "sandbox" | "live" | undefined;
|
|
5
|
+
export declare function canAccessModule(services: ServiceAccess, guildId: string, module: ModuleDefinition): Promise<boolean>;
|
|
6
|
+
export declare function assertReviewedPremiumCatalogue(modules: readonly ModuleDefinition[]): void;
|
|
7
|
+
//# sourceMappingURL=premium-access.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getModuleTier, HELYX_PREMIUM_ENTITLEMENT_CAPABILITY, isReviewedPremiumModuleId, REVIEWED_PREMIUM_MODULE_IDS, } from "@helyx/module-manifest";
|
|
2
|
+
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
3
|
+
export const PREMIUM_MODULE_UNAVAILABLE_MESSAGE = "This Premium module is not assigned to this server. A Premium subscriber can assign a slot in Billing or with `/activate`.";
|
|
4
|
+
export function moduleRequiresPremium(module) {
|
|
5
|
+
return getModuleTier(module.manifest) === "premium";
|
|
6
|
+
}
|
|
7
|
+
export function requirePremiumProviderEnvironment(modules, environment) {
|
|
8
|
+
if (!modules.some(moduleRequiresPremium))
|
|
9
|
+
return undefined;
|
|
10
|
+
if (!environment)
|
|
11
|
+
throw new Error("STRIPE_ENVIRONMENT is required when a reviewed Premium module is loaded");
|
|
12
|
+
return environment;
|
|
13
|
+
}
|
|
14
|
+
export async function canAccessModule(services, guildId, module) {
|
|
15
|
+
if (!moduleRequiresPremium(module))
|
|
16
|
+
return true;
|
|
17
|
+
if (!isReviewedPremiumModuleId(module.manifest.id))
|
|
18
|
+
return false;
|
|
19
|
+
if (typeof services.has === "function" &&
|
|
20
|
+
!services.has(HELYX_SERVICE_NAMES.entitlements))
|
|
21
|
+
return false;
|
|
22
|
+
const entitlements = services.get(HELYX_SERVICE_NAMES.entitlements);
|
|
23
|
+
return entitlements.has(guildId, HELYX_PREMIUM_ENTITLEMENT_CAPABILITY);
|
|
24
|
+
}
|
|
25
|
+
export function assertReviewedPremiumCatalogue(modules) {
|
|
26
|
+
const byId = new Map(modules.map((module) => [module.manifest.id, module]));
|
|
27
|
+
for (const module of modules) {
|
|
28
|
+
if (moduleRequiresPremium(module) &&
|
|
29
|
+
!isReviewedPremiumModuleId(module.manifest.id))
|
|
30
|
+
throw new Error(`Premium module has not passed catalogue review: ${module.manifest.id}`);
|
|
31
|
+
}
|
|
32
|
+
for (const moduleId of REVIEWED_PREMIUM_MODULE_IDS) {
|
|
33
|
+
const module = byId.get(moduleId);
|
|
34
|
+
if (module && !moduleRequiresPremium(module))
|
|
35
|
+
throw new Error(`Reviewed Premium module is missing its Premium manifest tier: ${moduleId}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=premium-access.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BillingEnvironment, PremiumAllocationRepository } from "@helyx/database";
|
|
2
|
+
import { type ModuleDefinition } from "@helyx/sdk";
|
|
3
|
+
type AllocationService = Pick<PremiumAllocationRepository, "getAccountAllocation" | "inspectTarget" | "assignLowestAvailable" | "moveSlot" | "releaseSlot" | "auditDenied">;
|
|
4
|
+
export interface PremiumGuildAuthority {
|
|
5
|
+
canManageGuild(input: {
|
|
6
|
+
guildId: string;
|
|
7
|
+
userId: string;
|
|
8
|
+
}): Promise<boolean>;
|
|
9
|
+
}
|
|
10
|
+
export interface PremiumActivationModuleOptions {
|
|
11
|
+
allocations: AllocationService;
|
|
12
|
+
authority: PremiumGuildAuthority;
|
|
13
|
+
provider: string;
|
|
14
|
+
providerEnvironment: BillingEnvironment;
|
|
15
|
+
reviewedPremiumModuleIds: readonly string[];
|
|
16
|
+
now?: () => Date;
|
|
17
|
+
}
|
|
18
|
+
export declare function createPremiumActivationModule(options: PremiumActivationModuleOptions): ModuleDefinition;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=premium-activation-module.d.ts.map
|