@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,84 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Allow Premium slots to be released, reassigned and moved immediately without a
8
+ transfer cooldown, while preserving atomic entitlement projection and
9
+ final-source Premium module disablement.
10
+
11
+ Add fail-closed Premium quantity incident controls, recoverable allocation
12
+ management for removed servers, overlapping Stripe webhook-secret rotation and
13
+ restricted exception diagnostics. Complete the production environment
14
+ contract for every API and bot schema field.
15
+
16
+ - Updated dependencies
17
+ - @helyx/config@1.0.0
18
+ - @helyx/database@1.0.0
19
+
20
+ ## 0.6.0
21
+
22
+ ### Minor Changes
23
+
24
+ - 1dfbf5b: Add the reviewed Premium module catalogue and enforce its entitlement across
25
+ hosted command, interaction, event, scheduled-task, configuration and dashboard
26
+ mutation paths. Add account-owned Premium slot allocation, the `/activate`
27
+ Discord flow, final-source-loss repair and optional safe billing lifecycle
28
+ notifications. Add audited, privacy-aware Admin Premium overrides and
29
+ deterministic entitlement convergence for inactive subscriptions and quantity
30
+ reductions, while keeping billing and Premium-only commands out of generated
31
+ self-host projects.
32
+
33
+ ### Patch Changes
34
+
35
+ - Updated dependencies [1dfbf5b]
36
+ - @helyx/config@0.7.0
37
+ - @helyx/database@0.12.0
38
+ - @helyx/module-manifest@0.9.0
39
+ - @helyx/platform@0.8.0
40
+
41
+ ## 0.5.1
42
+
43
+ ### Patch Changes
44
+
45
+ - 579f2c4: Keep transport-public components behind module availability and enablement,
46
+ and classify dashboard permission operations separately from Discord commands.
47
+ - Updated dependencies [579f2c4]
48
+ - Updated dependencies [579f2c4]
49
+ - Updated dependencies [579f2c4]
50
+ - @helyx/sdk@0.11.0
51
+ - @helyx/config@0.6.0
52
+ - @helyx/database@0.11.0
53
+
54
+ ## 0.5.0
55
+
56
+ ### Minor Changes
57
+
58
+ - bbc4eee: Add the shared exact-progress, zoned-time, ordered-retention, multi-schedule, and durable cross-module role-ownership contracts required by the Events and Community Goals Testing modules.
59
+
60
+ Register the PostgreSQL progress and ownership services in core, deliver owned roles through bot roots and shard workers, and package the corresponding database migration and rollback.
61
+
62
+ ### Patch Changes
63
+
64
+ - Updated dependencies [bbc4eee]
65
+ - Updated dependencies
66
+ - @helyx/sdk@0.10.0
67
+ - @helyx/platform@0.7.0
68
+ - @helyx/database@0.10.0
69
+
70
+ ## 0.4.48
71
+
72
+ ### Patch Changes
73
+
74
+ - Add declared command-operation permission targets, project them through both permission-management surfaces, and harden module lifecycle, health, visibility, bounded-work, optional-side-effect, command-permission, and Forms durable-workflow behavior. Preserve existing FAQ, Access Control, Achievements, Giveaways and Levels overrides through additive migrations while assigning every slash-command path its own permission identity.
75
+
76
+ ## 0.4.47
77
+
78
+ ### Patch Changes
79
+
80
+ - Add the bounded Discord inspection, durable record scheduling and recurrence, trusted dashboard workspaces, rich-message composition, optional invite-attribution contracts, module deactivation/configuration hooks, and versioned schedulable-action registry required by the Analytics, Invite Tracking, Reminders, Birthdays, Role Expiry, Scheduler, Message Embedder, Auto-message, and Sticky Messages testing modules.
81
+
3
82
  ## 0.4.46
4
83
 
5
84
  ### Patch Changes
package/README.md CHANGED
@@ -45,10 +45,10 @@ bot-specific `HELYX_STATUS_HEARTBEAT_SECRET`,
45
45
  `CF_ACCESS_CLIENT_ID`/`CF_ACCESS_CLIENT_SECRET` pair. Never reuse another
46
46
  producer's Access identity or heartbeat secret.
47
47
 
48
- Giveaways also requires the existing `HELYX_DATA_ENCRYPTION_KEY` so core can
49
- resume secure weighted-selection checkpoints without exposing the selection
50
- key to a module. Keep it stable across deployments; do not rotate it while a
51
- giveaway is drawing.
48
+ Giveaways and Invite Tracking also require the existing
49
+ `HELYX_DATA_ENCRYPTION_KEY`. Core uses it for secure weighted-selection
50
+ checkpoints and stable invite-member fingerprints without exposing the key to
51
+ a module. Keep it stable across deployments.
52
52
 
53
53
  Trivia uses questions already imported into PostgreSQL. Multiple Choice and
54
54
  Guess both run entirely from the saved question catalogue, and the bot runtime
@@ -1,4 +1,5 @@
1
1
  import { defineModule, } from "@helyx/sdk";
2
+ import { safeError } from "@helyx/core";
2
3
  import { Events } from "discord.js";
3
4
  import { ClientCloudDiscordDirectory, CloudConnectorUserError, } from "./cloud-connector.js";
4
5
  import { INTERNAL_MODULE_MANIFEST_FIELDS } from "./internal-module-manifest.js";
@@ -622,9 +623,4 @@ function restrictedPolicy() {
622
623
  deniedUserIds: [],
623
624
  };
624
625
  }
625
- function safeError(error) {
626
- return error instanceof Error
627
- ? { name: error.name, message: error.message }
628
- : { name: "Error", message: String(error) };
629
- }
630
626
  //# sourceMappingURL=cloud-management-module.js.map
@@ -0,0 +1,26 @@
1
+ import type { CommandContribution, ModuleDefinition, SubcommandContribution } from "@helyx/sdk";
2
+ export interface ExecutableCommandPath {
3
+ path: string;
4
+ permissionId: string;
5
+ explicitPermissionId: boolean;
6
+ name: string;
7
+ description: string;
8
+ source: "direct" | "shared" | "nexus";
9
+ }
10
+ export interface CommandPermissionTarget {
11
+ id: string;
12
+ name: string;
13
+ description: string;
14
+ kind: "command" | "operation";
15
+ }
16
+ export interface CommandPathContractIssue {
17
+ key: string;
18
+ message: string;
19
+ }
20
+ export declare const legacyCommandPathContractIssues: Readonly<Record<string, ReadonlySet<string>>>;
21
+ export declare function collectExecutableCommandPaths(module: ModuleDefinition): ExecutableCommandPath[];
22
+ export declare function collectCommandPermissionTargets(module: ModuleDefinition): CommandPermissionTarget[];
23
+ export declare function resolveDirectCommandPath(command: CommandContribution, subcommand?: SubcommandContribution, groupName?: string | null): Pick<ExecutableCommandPath, "path" | "permissionId">;
24
+ export declare function inspectCommandPathContract(module: ModuleDefinition): CommandPathContractIssue[];
25
+ export declare function assertCommandPathContract(module: ModuleDefinition): void;
26
+ //# sourceMappingURL=command-path-contract.d.ts.map
@@ -0,0 +1,157 @@
1
+ export const legacyCommandPathContractIssues = Object.freeze({});
2
+ export function collectExecutableCommandPaths(module) {
3
+ const paths = (module.interactions?.commands ?? []).flatMap((command) => collectDirectCommandPaths(command));
4
+ paths.push(...(module.sharedCommands ?? []).map((command) => {
5
+ const path = `${command.family}.${command.path}`;
6
+ return {
7
+ path,
8
+ permissionId: command.permissionId ?? path,
9
+ explicitPermissionId: command.permissionId !== undefined,
10
+ name: `/${command.family} ${command.path.replaceAll(".", " ")}`,
11
+ description: command.description,
12
+ source: "shared",
13
+ };
14
+ }), ...(module.nexusCommands ?? []).map((command) => {
15
+ const path = `nexus.${command.path}`;
16
+ return {
17
+ path,
18
+ permissionId: path,
19
+ explicitPermissionId: true,
20
+ name: `/nexus ${command.path.replaceAll(".", " ")}`,
21
+ description: command.description,
22
+ source: "nexus",
23
+ };
24
+ }));
25
+ return paths;
26
+ }
27
+ export function collectCommandPermissionTargets(module) {
28
+ return [
29
+ ...collectExecutableCommandPaths(module).map((command) => ({
30
+ id: command.permissionId,
31
+ name: command.name,
32
+ description: command.description,
33
+ kind: "command",
34
+ })),
35
+ ...(module.interactions?.commands ?? []).flatMap((command) => [
36
+ ...permissionOperations(command.permissionOperations),
37
+ ...(command.subcommands ?? []).flatMap((subcommand) => permissionOperations(subcommand.permissionOperations)),
38
+ ...(command.subcommandGroups ?? []).flatMap((group) => group.subcommands.flatMap((subcommand) => permissionOperations(subcommand.permissionOperations))),
39
+ ]),
40
+ ...(module.sharedCommands ?? []).flatMap((command) => permissionOperations(command.permissionOperations)),
41
+ ...(module.nexusCommands ?? []).flatMap((command) => permissionOperations(command.permissionOperations)),
42
+ ];
43
+ }
44
+ export function resolveDirectCommandPath(command, subcommand, groupName) {
45
+ const path = [command.name, groupName, subcommand?.name]
46
+ .filter(Boolean)
47
+ .join(".");
48
+ return {
49
+ path,
50
+ permissionId: subcommand?.permissionId ?? command.permissionId ?? path,
51
+ };
52
+ }
53
+ export function inspectCommandPathContract(module) {
54
+ const issues = [];
55
+ const paths = collectExecutableCommandPaths(module);
56
+ const operations = collectCommandPermissionTargets(module).filter(({ kind }) => kind === "operation");
57
+ const pathsByPermissionId = new Map();
58
+ const seenPaths = new Set();
59
+ for (const command of paths) {
60
+ if (seenPaths.has(command.path)) {
61
+ issues.push({
62
+ key: `duplicate-command-path:${command.path}`,
63
+ message: `duplicate executable command path ${command.path}`,
64
+ });
65
+ }
66
+ seenPaths.add(command.path);
67
+ if (!command.explicitPermissionId) {
68
+ issues.push({
69
+ key: `missing-permission-id:${command.path}`,
70
+ message: `command ${command.path} has no explicit permission ID`,
71
+ });
72
+ continue;
73
+ }
74
+ const owners = pathsByPermissionId.get(command.permissionId) ?? [];
75
+ pathsByPermissionId.set(command.permissionId, [...owners, command.path]);
76
+ if (!Object.prototype.hasOwnProperty.call(module.defaultPermissions?.commands ?? {}, command.permissionId)) {
77
+ issues.push({
78
+ key: `missing-default-policy:${command.path}`,
79
+ message: `command ${command.path} has no explicit default policy for ${command.permissionId}`,
80
+ });
81
+ }
82
+ }
83
+ for (const operation of operations) {
84
+ if (operation.id.trim().length === 0 ||
85
+ operation.id.length > 100 ||
86
+ !/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/u.test(operation.id) ||
87
+ operation.name.trim().length === 0 ||
88
+ operation.name.length > 100 ||
89
+ operation.description.trim().length === 0) {
90
+ issues.push({
91
+ key: `invalid-permission-operation:${operation.id}`,
92
+ message: "command permission operations require a namespaced ID and a non-empty Discord-safe name and description",
93
+ });
94
+ continue;
95
+ }
96
+ const owners = pathsByPermissionId.get(operation.id) ?? [];
97
+ pathsByPermissionId.set(operation.id, [
98
+ ...owners,
99
+ `operation:${operation.name}`,
100
+ ]);
101
+ if (!Object.prototype.hasOwnProperty.call(module.defaultPermissions?.commands ?? {}, operation.id)) {
102
+ issues.push({
103
+ key: `missing-default-policy:operation:${operation.id}`,
104
+ message: `command operation ${operation.name} has no explicit default policy for ${operation.id}`,
105
+ });
106
+ }
107
+ }
108
+ for (const [permissionId, owners] of pathsByPermissionId) {
109
+ if (owners.length > 1) {
110
+ issues.push({
111
+ key: `duplicate-permission-id:${permissionId}`,
112
+ message: `permission ID ${permissionId} is shared by ${owners.join(", ")}`,
113
+ });
114
+ }
115
+ }
116
+ return issues.sort((left, right) => left.key.localeCompare(right.key, "en-GB"));
117
+ }
118
+ function permissionOperations(operations) {
119
+ return (operations ?? []).map((operation) => ({
120
+ ...operation,
121
+ kind: "operation",
122
+ }));
123
+ }
124
+ export function assertCommandPathContract(module) {
125
+ const issues = inspectCommandPathContract(module);
126
+ const actual = new Set(issues.map(({ key }) => key));
127
+ const expected = legacyCommandPathContractIssues[module.manifest.id] ?? new Set();
128
+ const unexpected = issues.filter(({ key }) => !expected.has(key));
129
+ const stale = [...expected].filter((key) => !actual.has(key));
130
+ if (unexpected.length === 0 && stale.length === 0)
131
+ return;
132
+ const details = [
133
+ ...unexpected.map(({ message }) => message),
134
+ ...stale.map((key) => `legacy command-path exception is no longer needed: ${key}`),
135
+ ];
136
+ throw new Error(`Module ${module.manifest.id} command-path contract failed:\n${details.join("\n")}`);
137
+ }
138
+ function collectDirectCommandPaths(command) {
139
+ const nested = [
140
+ ...(command.subcommands ?? []).map((subcommand) => directCommandPath(command, subcommand)),
141
+ ...(command.subcommandGroups ?? []).flatMap((group) => group.subcommands.map((subcommand) => directCommandPath(command, subcommand, group.name))),
142
+ ];
143
+ if (nested.length > 0 || !command.execute)
144
+ return nested;
145
+ return [directCommandPath(command)];
146
+ }
147
+ function directCommandPath(command, subcommand, groupName) {
148
+ const resolved = resolveDirectCommandPath(command, subcommand, groupName);
149
+ return {
150
+ ...resolved,
151
+ explicitPermissionId: (subcommand?.permissionId ?? command.permissionId) !== undefined,
152
+ name: `/${resolved.path.replaceAll(".", " ")}`,
153
+ description: subcommand?.description ?? command.description,
154
+ source: "direct",
155
+ };
156
+ }
157
+ //# sourceMappingURL=command-path-contract.js.map
@@ -5,6 +5,7 @@ const platformGlobalModuleIds = new Set([
5
5
  "helyx.oauth",
6
6
  "helyx.permissions",
7
7
  "helyx.privacy",
8
+ "helyx.premium-activation",
8
9
  ]);
9
10
  const deploymentModuleIds = new Set(["helyx.cloud-connect"]);
10
11
  const internalGuildModuleIds = new Set(["helyx.developer-operations"]);
@@ -9,6 +9,7 @@ export function dashboardActionPermissionId(module, actionId) {
9
9
  return selectDeclaredPermission(module, [
10
10
  `${namespace}.${actionId.replaceAll("-", ".")}`,
11
11
  `${namespace}.configure`,
12
+ `${namespace}.manage`,
12
13
  ]);
13
14
  }
14
15
  export function managedResourcePermissionId(module, resourceType, operation, actionId, declaredPermissionId) {
@@ -7,41 +7,9 @@ import { type BotModuleStatusMutationResult } from "./module-status-control.js";
7
7
  import { type FirstTimeCommandSetupHandler } from "./first-time-command-setup.js";
8
8
  import { type ShardHealthSnapshot } from "./shard-contracts.js";
9
9
  import { type DashboardPermissionState } from "./control-permissions.js";
10
- import { type DashboardCommandDefinition } from "./dashboard-commands.js";
10
+ import { type DashboardModuleDefinition } from "./dashboard-module-projection.js";
11
11
  import { type AvailabilityModuleCatalogue } from "./module-availability-control.js";
12
12
  import { registerGuildCommandControlRoutes } from "./guild-command-control-routes.js";
13
- interface DashboardLogEventDefinition {
14
- id: string;
15
- name: string;
16
- description: string;
17
- defaultEnabled: boolean;
18
- }
19
- interface DashboardActionDefinition {
20
- id: string;
21
- label: string;
22
- description: string;
23
- buttonLabel: string;
24
- style: "primary" | "secondary" | "danger";
25
- }
26
- type DashboardResourceDefinition = NonNullable<NonNullable<ModuleDefinition["manifest"]["dashboard"]>["resources"]>[number];
27
- interface DashboardModuleDefinition {
28
- id: string;
29
- name: string;
30
- version: string;
31
- summary: string;
32
- category: string;
33
- icon: string;
34
- availability: "available";
35
- releaseStatus: "planned" | "testing" | "beta" | "live";
36
- requiredPermissions: string[];
37
- settings: readonly unknown[];
38
- commands: DashboardCommandDefinition[];
39
- logEvents: DashboardLogEventDefinition[];
40
- actions: DashboardActionDefinition[];
41
- achievementTriggers: NonNullable<ModuleDefinition["manifest"]["contributions"]["achievementTriggers"]>;
42
- resources: readonly DashboardResourceDefinition[];
43
- workspace?: NonNullable<ModuleDefinition["manifest"]["dashboard"]>["workspace"];
44
- }
45
13
  interface DashboardLoggingPolicy {
46
14
  eventId: string;
47
15
  enabled: boolean;
@@ -51,7 +19,7 @@ export interface BotControlGuildResources {
51
19
  channels: Array<{
52
20
  id: string;
53
21
  name: string;
54
- type: "guild_text" | "guild_announcement" | "guild_category";
22
+ type: "guild_text" | "guild_announcement" | "guild_category" | "guild_forum" | "guild_media";
55
23
  }>;
56
24
  roles: Array<{
57
25
  id: string;
@@ -1,4 +1,5 @@
1
1
  import { AuditRepository, GuildAccessPolicyRepository, GuildCommandActiveLeaseError, GuildCommandRevisionConflictError, GuildCommandSetupConflictError, InstallationRepository, ModuleLoggingPolicyRepository, OptimisticConcurrencyError, PermissionPolicyRepository, withTransaction, } from "@helyx/database";
2
+ import { safeError } from "@helyx/core";
2
3
  import { DashboardActionValidationError, DEFAULT_GUILD_ACCESS_POLICY, HELYX_SERVICE_NAMES, ModuleStateValidationError, } from "@helyx/sdk";
3
4
  import { actionReceiptSchema, signedAssessmentEnvelopeSchema, } from "@helyx/verification-contracts";
4
5
  import { canAccessGuildDashboard, canMutatePermissionPolicy, canUseCommand, resolveHelyxAuthority, } from "@helyx/core";
@@ -8,12 +9,13 @@ import { createHash, timingSafeEqual } from "node:crypto";
8
9
  import { z, ZodError } from "zod";
9
10
  import { addChannelMessageReaction as addDiscordChannelMessageReaction, deleteChannelMessage as deleteDiscordChannelMessage, editChannelMessage as editDiscordChannelMessage, normalizeDiscordPermission, sendToChannel as sendDiscordMessageToChannel, } from "./interactions.js";
10
11
  import { mutateDashboardModuleStatus, } from "./module-status-control.js";
12
+ import { canAccessModule, PREMIUM_MODULE_UNAVAILABLE_MESSAGE, } from "./premium-access.js";
11
13
  import { completeFirstTimeCommandSetup, } from "./first-time-command-setup.js";
12
14
  import { SHARD_HEALTH_STALE_MS, } from "./shard-contracts.js";
13
- import { dashboardCategoryLabel } from "./dashboard-category.js";
14
15
  import { assertVersion, dashboardActionPermissionId, declaredManagedResourcePermission, fromDashboardPolicy, managedResourcePermissionId, permissionPoliciesEqual, permissionPolicyKey, safeOptionalVersion, safeVersion, toDashboardPolicy, } from "./control-permissions.js";
15
16
  import { assertPermissionMutationAllowed, BotControlForbiddenError, } from "./control-errors.js";
16
- import { dashboardCommands, } from "./dashboard-commands.js";
17
+ import { dashboardCommands } from "./dashboard-commands.js";
18
+ import { mapDashboardModule, } from "./dashboard-module-projection.js";
17
19
  import { availabilityCatalogueDefinitions, availabilityCatalogue, dashboardModuleStatus, disabledModuleSummary, moduleAvailabilityService, moduleAvailabilityStates, statusCatalogueDefinitions, } from "./module-availability-control.js";
18
20
  import { registerGuildCommandControlRoutes } from "./guild-command-control-routes.js";
19
21
  import { createPrivacyProviderRegistry, describePrivacyProviders, } from "./privacy-provider-registry.js";
@@ -147,7 +149,9 @@ export class DiscordGuildResourceDirectory {
147
149
  channels: [...channels.values()]
148
150
  .filter((channel) => channel?.type === ChannelType.GuildText ||
149
151
  channel?.type === ChannelType.GuildAnnouncement ||
150
- channel?.type === ChannelType.GuildCategory)
152
+ channel?.type === ChannelType.GuildCategory ||
153
+ channel?.type === ChannelType.GuildForum ||
154
+ channel?.type === ChannelType.GuildMedia)
151
155
  .map((channel) => ({
152
156
  id: channel.id,
153
157
  name: channel.name,
@@ -155,7 +159,11 @@ export class DiscordGuildResourceDirectory {
155
159
  ? "guild_announcement"
156
160
  : channel.type === ChannelType.GuildCategory
157
161
  ? "guild_category"
158
- : "guild_text",
162
+ : channel.type === ChannelType.GuildForum
163
+ ? "guild_forum"
164
+ : channel.type === ChannelType.GuildMedia
165
+ ? "guild_media"
166
+ : "guild_text",
159
167
  }))
160
168
  .sort((left, right) => left.name.localeCompare(right.name)),
161
169
  roles: [...roles.values()]
@@ -627,13 +635,13 @@ export class BotControlService {
627
635
  dashboardModuleIds: this.#dashboardModules.map(({ id }) => id),
628
636
  installations: this.#installations,
629
637
  services: this.#services,
630
- requireModuleAvailable: () => this.#requireModuleAvailable(input.moduleId),
638
+ requireModuleAvailable: () => this.#requireModuleAccessible(input.guildId, input.moduleId),
631
639
  });
632
640
  }
633
641
  async completeFirstTimeCommandSetup(input) {
634
642
  for (const moduleIdValue of new Set(input.moduleIds)) {
635
643
  this.#requireDashboardModule(moduleIdValue);
636
- await this.#requireModuleAvailable(moduleIdValue);
644
+ await this.#requireModuleAccessible(input.guildId, moduleIdValue);
637
645
  }
638
646
  return completeFirstTimeCommandSetup({
639
647
  installations: this.#installations,
@@ -643,7 +651,7 @@ export class BotControlService {
643
651
  }
644
652
  async updateConfiguration(input) {
645
653
  const module = this.#requireDashboardModule(input.moduleId);
646
- await this.#requireModuleAvailable(input.moduleId);
654
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
647
655
  await this.#validateDiscordSettings(input.guildId, module, input.value);
648
656
  if (module.configuration?.validateUpdate) {
649
657
  try {
@@ -677,7 +685,7 @@ export class BotControlService {
677
685
  }
678
686
  async updatePermissions(input) {
679
687
  const module = this.#requireDashboardModule(input.moduleId);
680
- await this.#requireModuleAvailable(input.moduleId);
688
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
681
689
  const commands = dashboardCommands(module);
682
690
  const supplied = new Map(input.state.commands.map((command) => [command.commandId, command]));
683
691
  if (supplied.size !== input.state.commands.length ||
@@ -802,8 +810,8 @@ export class BotControlService {
802
810
  }
803
811
  async updateLoggingPolicy(input) {
804
812
  await Promise.all([
805
- this.#requireModuleAvailable(input.moduleId),
806
- this.#requireModuleAvailable("helyx.logging"),
813
+ this.#requireModuleAccessible(input.guildId, input.moduleId),
814
+ this.#requireModuleAccessible(input.guildId, "helyx.logging"),
807
815
  ]);
808
816
  if (!this.#modules.has("helyx.logging")) {
809
817
  throw new BotControlNotFoundError("Logging is not installed");
@@ -849,7 +857,7 @@ export class BotControlService {
849
857
  }
850
858
  async executeDashboardAction(input) {
851
859
  const module = this.#requireDashboardModule(input.moduleId);
852
- await this.#requireModuleAvailable(input.moduleId);
860
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
853
861
  const declared = module.manifest.dashboard?.actions?.find(({ id }) => id === input.actionId);
854
862
  const action = module.dashboardActions?.find(({ id }) => id === input.actionId);
855
863
  if (!declared || !action) {
@@ -908,13 +916,13 @@ export class BotControlService {
908
916
  })), relation.columns.map(({ key }) => key));
909
917
  }
910
918
  async previewManagedResource(input) {
911
- await this.#requireModuleAvailable(input.moduleId);
919
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
912
920
  const { module, resource } = this.#prepareManagedResource(input);
913
921
  this.#requireManagedResourceOperation(module, input.resourceType, "preview");
914
922
  return resource.preview(await this.#managedResourceContext(input, module, "preview"), input.value);
915
923
  }
916
924
  async createManagedResource(input) {
917
- await this.#requireModuleAvailable(input.moduleId);
925
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
918
926
  const { module, resource } = this.#prepareManagedResource(input);
919
927
  this.#requireManagedResourceOperation(module, input.resourceType, "create");
920
928
  const context = await this.#managedResourceContext(input, module, "create");
@@ -924,7 +932,7 @@ export class BotControlService {
924
932
  return normalizeManagedResourceDetail(managedResourceDetailSchema.parse(await resource.create(context, input.value, input.intent)), managedResourceColumnKeys(module, input.resourceType));
925
933
  }
926
934
  async updateManagedResource(input) {
927
- await this.#requireModuleAvailable(input.moduleId);
935
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
928
936
  const { module, resource } = this.#prepareManagedResource(input);
929
937
  this.#requireManagedResourceOperation(module, input.resourceType, "update");
930
938
  return normalizeManagedResourceDetail(managedResourceDetailSchema.parse(await resource.update(await this.#managedResourceContext(input, module, "update"), {
@@ -934,7 +942,7 @@ export class BotControlService {
934
942
  })), managedResourceColumnKeys(module, input.resourceType));
935
943
  }
936
944
  async deleteManagedResource(input) {
937
- await this.#requireModuleAvailable(input.moduleId);
945
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
938
946
  const { module, resource } = this.#prepareManagedResource(input);
939
947
  this.#requireManagedResourceOperation(module, input.resourceType, "delete");
940
948
  return managedResourceDeleteResultSchema.parse(await resource.delete(await this.#managedResourceContext(input, module, "delete"), {
@@ -943,7 +951,7 @@ export class BotControlService {
943
951
  }));
944
952
  }
945
953
  async executeManagedResourceAction(input) {
946
- await this.#requireModuleAvailable(input.moduleId);
954
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
947
955
  const { module, resource } = this.#prepareManagedResource(input);
948
956
  const declared = module.manifest.dashboard?.resources
949
957
  ?.find(({ id }) => id === input.resourceType)
@@ -1047,6 +1055,12 @@ export class BotControlService {
1047
1055
  if (availability && !(await availability.isAvailable(moduleIdValue)))
1048
1056
  throw new BotControlValidationError("This module is temporarily unavailable for all hosted servers.");
1049
1057
  }
1058
+ async #requireModuleAccessible(guildId, moduleIdValue) {
1059
+ await this.#requireModuleAvailable(moduleIdValue);
1060
+ const module = this.#requireModule(moduleIdValue);
1061
+ if (!(await canAccessModule(this.#services, guildId, module)))
1062
+ throw new BotControlValidationError(PREMIUM_MODULE_UNAVAILABLE_MESSAGE);
1063
+ }
1050
1064
  #requireDashboardModule(moduleIdValue) {
1051
1065
  const module = this.#requireModule(moduleIdValue);
1052
1066
  if (!module.manifest.dashboard)
@@ -1697,10 +1711,7 @@ export async function createBotControlServer(input) {
1697
1711
  return reply.code(mapped.status).send({ message: mapped.message });
1698
1712
  if (error instanceof BotControlTransportError)
1699
1713
  return reply.code(error.status).send({ message: error.message });
1700
- const safeError = error instanceof Error
1701
- ? { name: error.name, message: error.message }
1702
- : { name: "Error", message: "Unknown control failure" };
1703
- request.log.error({ error: safeError }, "Private bot control request failed");
1714
+ request.log.error({ error: safeError(error) }, "Private bot control request failed");
1704
1715
  return reply.code(500).send({ message: "The control request failed" });
1705
1716
  });
1706
1717
  return server;
@@ -1879,27 +1890,4 @@ function validateManagedResourceActionValue(inputs, value) {
1879
1890
  throw new DashboardActionValidationError(`${input.label} is not a valid action value.`);
1880
1891
  }
1881
1892
  }
1882
- function mapDashboardModule(module) {
1883
- const dashboard = module.manifest.dashboard;
1884
- return {
1885
- id: module.manifest.id,
1886
- name: module.manifest.name,
1887
- version: module.manifest.version,
1888
- summary: module.manifest.description,
1889
- category: dashboardCategoryLabel(dashboard.category),
1890
- icon: dashboard.icon,
1891
- availability: "available",
1892
- releaseStatus: module.manifest.releaseStatus,
1893
- requiredPermissions: [...module.manifest.permissions],
1894
- settings: dashboard.settings,
1895
- commands: dashboardCommands(module),
1896
- logEvents: (module.logging?.events ?? []).map((event) => ({ ...event })),
1897
- actions: (dashboard.actions ?? []).map((action) => ({ ...action })),
1898
- achievementTriggers: (module.manifest.contributions?.achievementTriggers ?? []).map((trigger) => structuredClone(trigger)),
1899
- resources: (dashboard.resources ?? []).map((resource) => structuredClone(resource)),
1900
- workspace: dashboard.workspace
1901
- ? structuredClone(dashboard.workspace)
1902
- : undefined,
1903
- };
1904
- }
1905
1893
  //# sourceMappingURL=control-server.js.map
@@ -4,6 +4,7 @@ export interface DashboardCommandDefinition {
4
4
  id: string;
5
5
  name: string;
6
6
  description: string;
7
+ kind: "command" | "operation";
7
8
  defaultPreset: DashboardPermissionPolicy["preset"];
8
9
  }
9
10
  export declare function dashboardCommands(module: ModuleDefinition): DashboardCommandDefinition[];
@@ -1,41 +1,12 @@
1
1
  import { emptyPolicy, toDashboardPolicy, } from "./control-permissions.js";
2
+ import { collectCommandPermissionTargets } from "./command-path-contract.js";
2
3
  export function dashboardCommands(module) {
3
- const commands = (module.interactions?.commands ?? []).flatMap((command) => {
4
- const nested = [
5
- ...(command.subcommands ?? []).map((subcommand) => dashboardCommand(module, {
6
- id: subcommand.permissionId ??
7
- command.permissionId ??
8
- `${command.name}.${subcommand.name}`,
9
- name: `/${command.name} ${subcommand.name}`,
10
- description: subcommand.description,
11
- })),
12
- ...(command.subcommandGroups ?? []).flatMap((group) => group.subcommands.map((subcommand) => dashboardCommand(module, {
13
- id: subcommand.permissionId ??
14
- command.permissionId ??
15
- `${command.name}.${group.name}.${subcommand.name}`,
16
- name: `/${command.name} ${group.name} ${subcommand.name}`,
17
- description: subcommand.description,
18
- }))),
19
- ];
20
- if (nested.length > 0)
21
- return nested;
22
- return [
23
- dashboardCommand(module, {
24
- id: command.permissionId ?? command.name,
25
- name: `/${command.name}`,
26
- description: command.description,
27
- }),
28
- ];
29
- });
30
- commands.push(...(module.sharedCommands ?? []).map((command) => dashboardCommand(module, {
31
- id: command.permissionId ?? `${command.family}.${command.path}`,
32
- name: `/${command.family} ${command.path.replace(".", " ")}`,
4
+ const commands = collectCommandPermissionTargets(module).map((command) => dashboardCommand(module, {
5
+ id: command.id,
6
+ name: command.name,
33
7
  description: command.description,
34
- })), ...(module.nexusCommands ?? []).map((command) => dashboardCommand(module, {
35
- id: `nexus.${command.path}`,
36
- name: `/nexus ${command.path.replace(".", " ")}`,
37
- description: command.description,
38
- })));
8
+ kind: command.kind,
9
+ }));
39
10
  return [
40
11
  ...new Map(commands.map((command) => [command.id, command])).values(),
41
12
  ];
@@ -0,0 +1,39 @@
1
+ import { type ModuleTier } from "@helyx/module-manifest";
2
+ import type { ModuleDefinition } from "@helyx/sdk";
3
+ import { type DashboardCommandDefinition } from "./dashboard-commands.js";
4
+ interface DashboardLogEventDefinition {
5
+ id: string;
6
+ name: string;
7
+ description: string;
8
+ defaultEnabled: boolean;
9
+ }
10
+ interface DashboardActionDefinition {
11
+ id: string;
12
+ label: string;
13
+ description: string;
14
+ buttonLabel: string;
15
+ style: "primary" | "secondary" | "danger";
16
+ }
17
+ type DashboardResourceDefinition = NonNullable<NonNullable<ModuleDefinition["manifest"]["dashboard"]>["resources"]>[number];
18
+ export interface DashboardModuleDefinition {
19
+ id: string;
20
+ name: string;
21
+ version: string;
22
+ summary: string;
23
+ category: string;
24
+ icon: string;
25
+ availability: "available";
26
+ releaseStatus: "planned" | "testing" | "beta" | "live";
27
+ tier: ModuleTier;
28
+ requiredPermissions: string[];
29
+ settings: readonly unknown[];
30
+ commands: DashboardCommandDefinition[];
31
+ logEvents: DashboardLogEventDefinition[];
32
+ actions: DashboardActionDefinition[];
33
+ achievementTriggers: NonNullable<ModuleDefinition["manifest"]["contributions"]["achievementTriggers"]>;
34
+ resources: readonly DashboardResourceDefinition[];
35
+ workspace?: NonNullable<ModuleDefinition["manifest"]["dashboard"]>["workspace"];
36
+ }
37
+ export declare function mapDashboardModule(module: ModuleDefinition): DashboardModuleDefinition;
38
+ export {};
39
+ //# sourceMappingURL=dashboard-module-projection.d.ts.map