@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +4 -4
  3. package/dist/cloud-management-module.js +1 -5
  4. package/dist/command-path-contract.d.ts +26 -0
  5. package/dist/command-path-contract.js +157 -0
  6. package/dist/command-registration-policy.js +1 -0
  7. package/dist/control-permissions.js +1 -0
  8. package/dist/control-server.d.ts +2 -34
  9. package/dist/control-server.js +31 -43
  10. package/dist/dashboard-commands.d.ts +1 -0
  11. package/dist/dashboard-commands.js +6 -35
  12. package/dist/dashboard-module-projection.d.ts +39 -0
  13. package/dist/dashboard-module-projection.js +28 -0
  14. package/dist/discord-analytics.d.ts +30 -0
  15. package/dist/discord-analytics.js +337 -0
  16. package/dist/discord-invites.d.ts +24 -0
  17. package/dist/discord-invites.js +91 -0
  18. package/dist/events.d.ts +1 -1
  19. package/dist/events.js +78 -22
  20. package/dist/guilds.js +1 -4
  21. package/dist/index.js +49 -8
  22. package/dist/interactions.d.ts +1 -1
  23. package/dist/interactions.js +89 -68
  24. package/dist/management-module.d.ts +1 -1
  25. package/dist/module-logging.js +1 -4
  26. package/dist/module-status-control.js +1 -1
  27. package/dist/modules.d.ts +2 -0
  28. package/dist/modules.js +27 -4
  29. package/dist/permissions-module.js +13 -16
  30. package/dist/premium-access.d.ts +7 -0
  31. package/dist/premium-access.js +38 -0
  32. package/dist/premium-activation-module.d.ts +20 -0
  33. package/dist/premium-activation-module.js +542 -0
  34. package/dist/premium-catalogue-enforcement.d.ts +5 -0
  35. package/dist/premium-catalogue-enforcement.js +29 -0
  36. package/dist/premium-guild-authority.d.ts +12 -0
  37. package/dist/premium-guild-authority.js +25 -0
  38. package/dist/scheduled-task-runner.js +14 -1
  39. package/dist/shard-contracts.js +1 -1
  40. package/dist/shard-supervisor.js +30 -1
  41. package/dist/shard-worker.d.ts +2 -0
  42. package/dist/shard-worker.js +36 -4
  43. package/documentation/examples/.env.example +2 -2
  44. package/package.json +6 -6
@@ -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
- [registered.contribution.name, groupName, subcommand.name]
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 commandId = subcommandName
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.contribution.authorization === "public") {
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
- if (!(await moduleIsGloballyAvailable(services, moduleId))) {
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 (!interaction.guildId)
873
- return true;
874
- const installations = services.get(HELYX_SERVICE_NAMES.installations);
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
- const embed = response.embed
1057
- ? new EmbedBuilder()
1058
- .setTitle(response.embed.title)
1059
- .setDescription(response.embed.description)
1060
- .setColor(response.embed.color ?? 0x7c3aed)
1061
- : undefined;
1062
- if (embed && response.embed?.footer)
1063
- embed.setFooter({ text: response.embed.footer });
1064
- if (embed && response.embed?.url)
1065
- embed.setURL(response.embed.url);
1066
- if (embed && response.embed?.author)
1067
- embed.setAuthor({
1068
- name: response.embed.author.name,
1069
- ...(response.embed.author.iconUrl
1070
- ? { iconURL: response.embed.author.iconUrl }
1071
- : {}),
1072
- });
1073
- if (embed && response.embed?.timestamp)
1074
- embed.setTimestamp(new Date(response.embed.timestamp));
1075
- if (embed && response.embed?.imageUrl)
1076
- embed.setImage(response.embed.imageUrl);
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 || response.suppressEmbeds
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
- ...(embed ? { embeds: [embed] } : {}),
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;
@@ -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 (writeBehind)
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: decodeSecureRandomSelectionKey(internalSelectionKey, secureRandomSelectionWorkerKeyName),
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
- if (modules.some(({ manifest }) => manifest.id === giveawaysModuleId)) {
42
- throw new Error("HELYX_DATA_ENCRYPTION_KEY is required when Giveaways is loaded");
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: deriveSecureRandomSelectionKey(encodedKey),
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 && !commandTargets(module).includes(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
- const targets = (module.interactions?.commands ?? []).flatMap((command) => {
441
- const nested = [
442
- ...(command.subcommands ?? []).map((subcommand) => `${command.name}.${subcommand.name}`),
443
- ...(command.subcommandGroups ?? []).flatMap((group) => group.subcommands.map((subcommand) => `${command.name}.${group.name}.${subcommand.name}`)),
444
- ];
445
- if (nested.length > 0)
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 one command. Command policies override the module policy." +
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: `/${target.replaceAll(".", " ")}`,
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