@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/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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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) {
|
package/dist/control-server.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/control-server.js
CHANGED
|
@@ -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
|
|
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
|
-
:
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
806
|
-
this.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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
|
-
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|