@helyx/bot 0.4.33 → 0.4.35
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 +39 -0
- package/dist/cloud-management-module.js +5 -0
- package/dist/control-server.d.ts +6 -7
- package/dist/control-server.js +32 -36
- package/dist/dashboard-commands.d.ts +10 -0
- package/dist/dashboard-commands.js +47 -0
- package/dist/developer-operations-module.js +3 -0
- package/dist/discord-guild-health.d.ts +2 -1
- package/dist/discord-guild-health.js +6 -3
- package/dist/events.js +27 -13
- package/dist/index.js +43 -3
- package/dist/interactions.d.ts +3 -2
- package/dist/interactions.js +132 -45
- package/dist/management-module.js +5 -1
- package/dist/module-availability-control.d.ts +24 -0
- package/dist/module-availability-control.js +61 -0
- package/dist/modules.js +24 -1
- package/dist/nexus-commands.d.ts +13 -0
- package/dist/nexus-commands.js +100 -0
- package/dist/oauth-module.js +1 -0
- package/dist/permissions-module.js +16 -4
- package/dist/scheduled-task-runner.js +7 -0
- package/dist/shard-router.d.ts +2 -0
- package/dist/shard-router.js +5 -0
- package/dist/shard-supervisor.js +7 -0
- package/dist/shard-worker.d.ts +8 -2
- package/dist/shard-worker.js +4 -1
- package/dist/support-module.js +1 -0
- package/package.json +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.35
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 26b4c3e: Add versioned hidden module dependencies, automatic installation lifecycle,
|
|
8
|
+
delegated customer health, and transitive self-host build composition for the
|
|
9
|
+
Nexus foundation while preserving legacy manifest and catalogue behavior. Add
|
|
10
|
+
closed, owner-aware composition for the shared `/nexus` command namespace and
|
|
11
|
+
bounded Core reads for terminology, member history and confirmed member value
|
|
12
|
+
changes. Add resumable, idempotent role and server bulk-award jobs.
|
|
13
|
+
Add bounded Nexus privacy capture, a durable hashed erasure fence and atomic
|
|
14
|
+
member detachment that journals any remaining balance as a privacy burn before
|
|
15
|
+
removing account and bulk-recipient identifiers.
|
|
16
|
+
Generalise the durable Achievement contribution bus as member contributions
|
|
17
|
+
while retaining the existing service names as compatibility aliases.
|
|
18
|
+
Add the private Nexus Rewards Testing package, complete initial record schema
|
|
19
|
+
and an atomic timezone-aware Daily Claim path backed by the shared ledger.
|
|
20
|
+
Add reviewed, default-off Nexus eligibility metadata to existing contribution
|
|
21
|
+
catalogues and atomically apply configured fixed rewards with source, role,
|
|
22
|
+
rate-cap, replay, balance-cap and privacy safeguards.
|
|
23
|
+
Add definition-owned Achievement reward amounts and live repeatable-award
|
|
24
|
+
high-water processing so retries credit only newly committed occurrences.
|
|
25
|
+
Add the private Nexus Economy Testing package and its complete initial Exchange,
|
|
26
|
+
item, redemption, inventory, role-grant and durable fulfilment record schema.
|
|
27
|
+
Add a typed atomic Economy redemption path with exact replay, finite stock,
|
|
28
|
+
member limits, role eligibility, collectible inventory and durable fulfilment.
|
|
29
|
+
Add coalesced leased role fulfilment with reviewed Discord safety, observed
|
|
30
|
+
delivery reconciliation, bounded retries and Core-linked automatic refunds.
|
|
31
|
+
- Updated dependencies [26b4c3e]
|
|
32
|
+
- @helyx/module-manifest@0.7.0
|
|
33
|
+
- @helyx/platform@0.6.0
|
|
34
|
+
- @helyx/sdk@0.8.0
|
|
35
|
+
|
|
36
|
+
## 0.4.34
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- Add fail-open production status heartbeat configuration and reporting support.
|
|
41
|
+
|
|
3
42
|
## 0.4.33
|
|
4
43
|
|
|
5
44
|
### Patch Changes
|
|
@@ -106,6 +106,7 @@ export function createCloudManagementModule(input) {
|
|
|
106
106
|
{
|
|
107
107
|
name: "connect",
|
|
108
108
|
description: "Connect this deployment using a one-use code",
|
|
109
|
+
acknowledgement: "immediate-modal",
|
|
109
110
|
rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
|
|
110
111
|
async execute(context) {
|
|
111
112
|
if (!(await requireOwner(context)))
|
|
@@ -136,6 +137,7 @@ export function createCloudManagementModule(input) {
|
|
|
136
137
|
{
|
|
137
138
|
name: "status",
|
|
138
139
|
description: "Show this deployment's Cloud Connect status",
|
|
140
|
+
acknowledgement: "deferred-ephemeral",
|
|
139
141
|
rateLimit: { scope: "user", limit: 20, windowSeconds: 60 },
|
|
140
142
|
async execute(context) {
|
|
141
143
|
if (!(await requireOwner(context)))
|
|
@@ -146,6 +148,7 @@ export function createCloudManagementModule(input) {
|
|
|
146
148
|
{
|
|
147
149
|
name: "disconnect",
|
|
148
150
|
description: "Revoke this deployment's Cloud Connect identity",
|
|
151
|
+
acknowledgement: "immediate-modal",
|
|
149
152
|
rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
|
|
150
153
|
async execute(context) {
|
|
151
154
|
if (!(await requireOwner(context)))
|
|
@@ -185,6 +188,7 @@ export function createCloudManagementModule(input) {
|
|
|
185
188
|
{
|
|
186
189
|
name: "rotate",
|
|
187
190
|
description: "Replace the saved Cloud Connect credential",
|
|
191
|
+
acknowledgement: "immediate-modal",
|
|
188
192
|
rateLimit: { scope: "user", limit: 3, windowSeconds: 60 },
|
|
189
193
|
async execute(context) {
|
|
190
194
|
if (!(await requireOwner(context)))
|
|
@@ -224,6 +228,7 @@ export function createCloudManagementModule(input) {
|
|
|
224
228
|
{
|
|
225
229
|
name: "support",
|
|
226
230
|
description: "Choose what Helyx support may inspect",
|
|
231
|
+
acknowledgement: "immediate-modal",
|
|
227
232
|
rateLimit: { scope: "user", limit: 5, windowSeconds: 60 },
|
|
228
233
|
async execute(context) {
|
|
229
234
|
if (!(await requireOwner(context)))
|
package/dist/control-server.d.ts
CHANGED
|
@@ -4,13 +4,9 @@ import { type ActionReceipt, type SignedAssessmentEnvelope } from "@helyx/verifi
|
|
|
4
4
|
import { type Client } from "discord.js";
|
|
5
5
|
import { type FastifyInstance } from "fastify";
|
|
6
6
|
import { type ShardHealthSnapshot } from "./shard-contracts.js";
|
|
7
|
-
import { type
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
name: string;
|
|
11
|
-
description: string;
|
|
12
|
-
defaultPreset: DashboardPermissionPolicy["preset"];
|
|
13
|
-
}
|
|
7
|
+
import { type DashboardPermissionState } from "./control-permissions.js";
|
|
8
|
+
import { type DashboardCommandDefinition } from "./dashboard-commands.js";
|
|
9
|
+
import { type AvailabilityModuleCatalogue } from "./module-availability-control.js";
|
|
14
10
|
interface DashboardLogEventDefinition {
|
|
15
11
|
id: string;
|
|
16
12
|
name: string;
|
|
@@ -127,6 +123,7 @@ export interface BotControlState {
|
|
|
127
123
|
roles: BotControlGuildResources["roles"];
|
|
128
124
|
modules: DashboardModuleDefinition[];
|
|
129
125
|
moduleStatuses: Record<string, "enabled" | "disabled" | "unavailable">;
|
|
126
|
+
globallyDisabledModuleIds: string[];
|
|
130
127
|
configurations: Record<string, {
|
|
131
128
|
version: number;
|
|
132
129
|
value: Record<string, unknown>;
|
|
@@ -155,6 +152,7 @@ export interface BotControlHandler {
|
|
|
155
152
|
correlationId: string;
|
|
156
153
|
}): Promise<OAuthConsumerCleanupResult>;
|
|
157
154
|
sendSupportNotification(input: SupportNotificationInput): Promise<void>;
|
|
155
|
+
listAvailabilityModules(): Promise<AvailabilityModuleCatalogue>;
|
|
158
156
|
listGuildPresence(input: {
|
|
159
157
|
guildIds: readonly string[];
|
|
160
158
|
correlationId: string;
|
|
@@ -541,6 +539,7 @@ export declare class BotControlService implements BotControlHandler {
|
|
|
541
539
|
correlationId: string;
|
|
542
540
|
actorAuthenticatedAt?: string;
|
|
543
541
|
}): Promise<ManagedResourceDetail | null>;
|
|
542
|
+
listAvailabilityModules(): Promise<AvailabilityModuleCatalogue>;
|
|
544
543
|
}
|
|
545
544
|
export declare function createBotControlServer(input: {
|
|
546
545
|
service: BotControlHandler;
|
package/dist/control-server.js
CHANGED
|
@@ -9,7 +9,9 @@ import { z, ZodError } from "zod";
|
|
|
9
9
|
import { addChannelMessageReaction as addDiscordChannelMessageReaction, deleteChannelMessage as deleteDiscordChannelMessage, editChannelMessage as editDiscordChannelMessage, normalizeDiscordPermission, sendToChannel as sendDiscordMessageToChannel, } from "./interactions.js";
|
|
10
10
|
import { SHARD_HEALTH_STALE_MS, } from "./shard-contracts.js";
|
|
11
11
|
import { dashboardCategoryLabel } from "./dashboard-category.js";
|
|
12
|
-
import { assertVersion, dashboardActionPermissionId, declaredManagedResourcePermission,
|
|
12
|
+
import { assertVersion, dashboardActionPermissionId, declaredManagedResourcePermission, fromDashboardPolicy, managedResourcePermissionId, permissionPoliciesEqual, permissionPolicyKey, safeOptionalVersion, safeVersion, toDashboardPolicy, } from "./control-permissions.js";
|
|
13
|
+
import { dashboardCommands, } from "./dashboard-commands.js";
|
|
14
|
+
import { availabilityCatalogueDefinitions, availabilityCatalogue, dashboardModuleStatus, disabledModuleSummary, moduleAvailabilityService, moduleAvailabilityStates, } from "./module-availability-control.js";
|
|
13
15
|
const snowflake = z.string().regex(/^\d{17,20}$/u);
|
|
14
16
|
const moduleId = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/u);
|
|
15
17
|
const MAX_DISCORD_SETTING_SELECTIONS = 25;
|
|
@@ -258,6 +260,7 @@ export class BotControlService {
|
|
|
258
260
|
presenceShardCount;
|
|
259
261
|
#modules;
|
|
260
262
|
#dashboardModules;
|
|
263
|
+
#availabilityModules;
|
|
261
264
|
#configurations;
|
|
262
265
|
#installations;
|
|
263
266
|
#permissions;
|
|
@@ -273,6 +276,7 @@ export class BotControlService {
|
|
|
273
276
|
this.#dashboardModules = modules
|
|
274
277
|
.filter((module) => module.manifest.dashboard !== undefined)
|
|
275
278
|
.map(mapDashboardModule);
|
|
279
|
+
this.#availabilityModules = availabilityCatalogueDefinitions(modules);
|
|
276
280
|
this.#configurations = services.get(HELYX_SERVICE_NAMES.configuration);
|
|
277
281
|
this.#installations = services.get(HELYX_SERVICE_NAMES.installations);
|
|
278
282
|
this.#permissions = services.get(HELYX_SERVICE_NAMES.permissions);
|
|
@@ -423,14 +427,16 @@ export class BotControlService {
|
|
|
423
427
|
commandId: id,
|
|
424
428
|
})),
|
|
425
429
|
]);
|
|
426
|
-
const [resources, installedModules, storedLoggingPolicies, runtime, storedConfigurations, storedPermissions,] = await Promise.all([
|
|
430
|
+
const [resources, installedModules, storedLoggingPolicies, runtime, storedConfigurations, storedPermissions, availabilityStates,] = await Promise.all([
|
|
427
431
|
this.resources.getGuildResources(guildId),
|
|
428
432
|
new InstallationRepository(this.database).listModules(guildId),
|
|
429
433
|
new ModuleLoggingPolicyRepository(this.database).listForGuild(guildId),
|
|
430
434
|
this.#inspectRuntimeCapabilities(),
|
|
431
435
|
this.#readConfigurations(guildId, moduleIds),
|
|
432
436
|
this.#readPermissionPolicies(guildId, permissionTargets),
|
|
437
|
+
moduleAvailabilityStates(this.#services, moduleIds),
|
|
433
438
|
]);
|
|
439
|
+
const disabledModules = disabledModuleSummary(availabilityStates);
|
|
434
440
|
const installed = new Map(installedModules.map((module) => [module.moduleId, module]));
|
|
435
441
|
const moduleStatuses = {};
|
|
436
442
|
const configurations = {};
|
|
@@ -442,11 +448,7 @@ export class BotControlService {
|
|
|
442
448
|
]));
|
|
443
449
|
for (const { dashboardModule, module } of dashboardEntries) {
|
|
444
450
|
const installation = installed.get(dashboardModule.id);
|
|
445
|
-
moduleStatuses[dashboardModule.id] = installation
|
|
446
|
-
? installation.enabled
|
|
447
|
-
? "enabled"
|
|
448
|
-
: "disabled"
|
|
449
|
-
: "unavailable";
|
|
451
|
+
moduleStatuses[dashboardModule.id] = dashboardModuleStatus(disabledModules.set, dashboardModule.id, installation);
|
|
450
452
|
const configuration = storedConfigurations.get(dashboardModule.id);
|
|
451
453
|
if (configuration)
|
|
452
454
|
configurations[dashboardModule.id] = {
|
|
@@ -470,6 +472,7 @@ export class BotControlService {
|
|
|
470
472
|
...resources,
|
|
471
473
|
modules: structuredClone(this.#dashboardModules),
|
|
472
474
|
moduleStatuses,
|
|
475
|
+
globallyDisabledModuleIds: disabledModules.ids,
|
|
473
476
|
configurations,
|
|
474
477
|
permissions,
|
|
475
478
|
logging,
|
|
@@ -624,6 +627,7 @@ export class BotControlService {
|
|
|
624
627
|
}
|
|
625
628
|
async setModuleStatus(input) {
|
|
626
629
|
const module = this.#requireDashboardModule(input.moduleId);
|
|
630
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
627
631
|
const scopes = Object.entries(module.records ?? {})
|
|
628
632
|
.filter(([, collection]) => (collection.incrementableFields?.length ?? 0) > 0)
|
|
629
633
|
.map(([collection]) => ({
|
|
@@ -677,6 +681,7 @@ export class BotControlService {
|
|
|
677
681
|
}
|
|
678
682
|
async updateConfiguration(input) {
|
|
679
683
|
const module = this.#requireDashboardModule(input.moduleId);
|
|
684
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
680
685
|
await this.#validateDiscordSettings(input.guildId, module, input.value);
|
|
681
686
|
if (module.configuration?.validateUpdate) {
|
|
682
687
|
try {
|
|
@@ -710,6 +715,7 @@ export class BotControlService {
|
|
|
710
715
|
}
|
|
711
716
|
async updatePermissions(input) {
|
|
712
717
|
const module = this.#requireDashboardModule(input.moduleId);
|
|
718
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
713
719
|
const commands = dashboardCommands(module);
|
|
714
720
|
const supplied = new Map(input.state.commands.map((command) => [command.commandId, command]));
|
|
715
721
|
if (supplied.size !== input.state.commands.length ||
|
|
@@ -833,6 +839,10 @@ export class BotControlService {
|
|
|
833
839
|
return this.#getPermissionState(input.guildId, module);
|
|
834
840
|
}
|
|
835
841
|
async updateLoggingPolicy(input) {
|
|
842
|
+
await Promise.all([
|
|
843
|
+
this.#requireModuleAvailable(input.moduleId),
|
|
844
|
+
this.#requireModuleAvailable("helyx.logging"),
|
|
845
|
+
]);
|
|
836
846
|
if (!this.#modules.has("helyx.logging")) {
|
|
837
847
|
throw new BotControlNotFoundError("Logging is not installed");
|
|
838
848
|
}
|
|
@@ -877,6 +887,7 @@ export class BotControlService {
|
|
|
877
887
|
}
|
|
878
888
|
async executeDashboardAction(input) {
|
|
879
889
|
const module = this.#requireDashboardModule(input.moduleId);
|
|
890
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
880
891
|
const declared = module.manifest.dashboard?.actions?.find(({ id }) => id === input.actionId);
|
|
881
892
|
const action = module.dashboardActions?.find(({ id }) => id === input.actionId);
|
|
882
893
|
if (!declared || !action) {
|
|
@@ -935,11 +946,13 @@ export class BotControlService {
|
|
|
935
946
|
})), relation.columns.map(({ key }) => key));
|
|
936
947
|
}
|
|
937
948
|
async previewManagedResource(input) {
|
|
949
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
938
950
|
const { module, resource } = this.#prepareManagedResource(input);
|
|
939
951
|
this.#requireManagedResourceOperation(module, input.resourceType, "preview");
|
|
940
952
|
return resource.preview(await this.#managedResourceContext(input, module, "preview"), input.value);
|
|
941
953
|
}
|
|
942
954
|
async createManagedResource(input) {
|
|
955
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
943
956
|
const { module, resource } = this.#prepareManagedResource(input);
|
|
944
957
|
this.#requireManagedResourceOperation(module, input.resourceType, "create");
|
|
945
958
|
const context = await this.#managedResourceContext(input, module, "create");
|
|
@@ -949,6 +962,7 @@ export class BotControlService {
|
|
|
949
962
|
return normalizeManagedResourceDetail(managedResourceDetailSchema.parse(await resource.create(context, input.value, input.intent)), managedResourceColumnKeys(module, input.resourceType));
|
|
950
963
|
}
|
|
951
964
|
async updateManagedResource(input) {
|
|
965
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
952
966
|
const { module, resource } = this.#prepareManagedResource(input);
|
|
953
967
|
this.#requireManagedResourceOperation(module, input.resourceType, "update");
|
|
954
968
|
return normalizeManagedResourceDetail(managedResourceDetailSchema.parse(await resource.update(await this.#managedResourceContext(input, module, "update"), {
|
|
@@ -958,6 +972,7 @@ export class BotControlService {
|
|
|
958
972
|
})), managedResourceColumnKeys(module, input.resourceType));
|
|
959
973
|
}
|
|
960
974
|
async deleteManagedResource(input) {
|
|
975
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
961
976
|
const { module, resource } = this.#prepareManagedResource(input);
|
|
962
977
|
this.#requireManagedResourceOperation(module, input.resourceType, "delete");
|
|
963
978
|
return managedResourceDeleteResultSchema.parse(await resource.delete(await this.#managedResourceContext(input, module, "delete"), {
|
|
@@ -966,6 +981,7 @@ export class BotControlService {
|
|
|
966
981
|
}));
|
|
967
982
|
}
|
|
968
983
|
async executeManagedResourceAction(input) {
|
|
984
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
969
985
|
const { module, resource } = this.#prepareManagedResource(input);
|
|
970
986
|
const declared = module.manifest.dashboard?.resources
|
|
971
987
|
?.find(({ id }) => id === input.resourceType)
|
|
@@ -1061,6 +1077,14 @@ export class BotControlService {
|
|
|
1061
1077
|
throw new BotControlNotFoundError("Module is not installed");
|
|
1062
1078
|
return module;
|
|
1063
1079
|
}
|
|
1080
|
+
listAvailabilityModules() {
|
|
1081
|
+
return availabilityCatalogue(this.#availabilityModules);
|
|
1082
|
+
}
|
|
1083
|
+
async #requireModuleAvailable(moduleIdValue) {
|
|
1084
|
+
const availability = moduleAvailabilityService(this.#services);
|
|
1085
|
+
if (availability && !(await availability.isAvailable(moduleIdValue)))
|
|
1086
|
+
throw new BotControlValidationError("This module is temporarily unavailable for all hosted servers.");
|
|
1087
|
+
}
|
|
1064
1088
|
#requireDashboardModule(moduleIdValue) {
|
|
1065
1089
|
const module = this.#requireModule(moduleIdValue);
|
|
1066
1090
|
if (!module.manifest.dashboard)
|
|
@@ -1368,6 +1392,7 @@ export async function createBotControlServer(input) {
|
|
|
1368
1392
|
const { guildId } = z.object({ guildId: snowflake }).parse(request.params);
|
|
1369
1393
|
return input.service.getGuildState(guildId);
|
|
1370
1394
|
});
|
|
1395
|
+
server.get("/v1/modules/availability-catalogue", () => input.service.listAvailabilityModules());
|
|
1371
1396
|
server.post("/v1/guilds/:guildId/dashboard-access", async (request) => {
|
|
1372
1397
|
const { guildId } = z.object({ guildId: snowflake }).parse(request.params);
|
|
1373
1398
|
const { actorUserId } = z
|
|
@@ -1897,35 +1922,6 @@ function mapDashboardModule(module) {
|
|
|
1897
1922
|
: undefined,
|
|
1898
1923
|
};
|
|
1899
1924
|
}
|
|
1900
|
-
function dashboardCommands(module) {
|
|
1901
|
-
const commands = (module.interactions?.commands ?? []).flatMap((command) => command.subcommands?.length
|
|
1902
|
-
? command.subcommands.map((subcommand) => {
|
|
1903
|
-
const id = subcommand.permissionId ??
|
|
1904
|
-
command.permissionId ??
|
|
1905
|
-
`${command.name}.${subcommand.name}`;
|
|
1906
|
-
return {
|
|
1907
|
-
id,
|
|
1908
|
-
name: `/${command.name} ${subcommand.name}`,
|
|
1909
|
-
description: subcommand.description,
|
|
1910
|
-
defaultPreset: toDashboardPolicy(module.defaultPermissions?.commands[id] ??
|
|
1911
|
-
module.defaultPermissions?.module ??
|
|
1912
|
-
emptyPolicy()).preset,
|
|
1913
|
-
};
|
|
1914
|
-
})
|
|
1915
|
-
: [
|
|
1916
|
-
{
|
|
1917
|
-
id: command.permissionId ?? command.name,
|
|
1918
|
-
name: `/${command.name}`,
|
|
1919
|
-
description: command.description,
|
|
1920
|
-
defaultPreset: toDashboardPolicy(module.defaultPermissions?.commands[command.name] ??
|
|
1921
|
-
module.defaultPermissions?.module ??
|
|
1922
|
-
emptyPolicy()).preset,
|
|
1923
|
-
},
|
|
1924
|
-
]);
|
|
1925
|
-
return [
|
|
1926
|
-
...new Map(commands.map((command) => [command.id, command])).values(),
|
|
1927
|
-
];
|
|
1928
|
-
}
|
|
1929
1925
|
function assertPermissionMutationAllowed(decision) {
|
|
1930
1926
|
if (decision.allowed)
|
|
1931
1927
|
return;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ModuleDefinition } from "@helyx/sdk";
|
|
2
|
+
import { type DashboardPermissionPolicy } from "./control-permissions.js";
|
|
3
|
+
export interface DashboardCommandDefinition {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
defaultPreset: DashboardPermissionPolicy["preset"];
|
|
8
|
+
}
|
|
9
|
+
export declare function dashboardCommands(module: ModuleDefinition): DashboardCommandDefinition[];
|
|
10
|
+
//# sourceMappingURL=dashboard-commands.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { emptyPolicy, toDashboardPolicy, } from "./control-permissions.js";
|
|
2
|
+
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.nexusCommands ?? []).map((command) => dashboardCommand(module, {
|
|
31
|
+
id: `nexus.${command.path}`,
|
|
32
|
+
name: `/nexus ${command.path.replace(".", " ")}`,
|
|
33
|
+
description: command.description,
|
|
34
|
+
})));
|
|
35
|
+
return [
|
|
36
|
+
...new Map(commands.map((command) => [command.id, command])).values(),
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
function dashboardCommand(module, command) {
|
|
40
|
+
return {
|
|
41
|
+
...command,
|
|
42
|
+
defaultPreset: toDashboardPolicy(module.defaultPermissions?.commands[command.id] ??
|
|
43
|
+
module.defaultPermissions?.module ??
|
|
44
|
+
emptyPolicy()).preset,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=dashboard-commands.js.map
|
|
@@ -33,6 +33,7 @@ export function createDeveloperOperationsModule(input) {
|
|
|
33
33
|
{
|
|
34
34
|
name: "shards",
|
|
35
35
|
description: "Show bounded read-only shard health",
|
|
36
|
+
acknowledgement: "deferred-ephemeral",
|
|
36
37
|
rateLimit: { scope: "user", limit: 10, windowSeconds: 60 },
|
|
37
38
|
async execute(context) {
|
|
38
39
|
if (!(await authorizeDeveloperCommand(context)))
|
|
@@ -81,6 +82,7 @@ export function createDeveloperOperationsModule(input) {
|
|
|
81
82
|
{
|
|
82
83
|
name: "bot",
|
|
83
84
|
description: "Show the bot build and installed module versions",
|
|
85
|
+
acknowledgement: "deferred-ephemeral",
|
|
84
86
|
rateLimit: { scope: "user", limit: 10, windowSeconds: 60 },
|
|
85
87
|
async execute(context) {
|
|
86
88
|
if (!(await authorizeDeveloperCommand(context)))
|
|
@@ -134,6 +136,7 @@ export function createDeveloperOperationsModule(input) {
|
|
|
134
136
|
{
|
|
135
137
|
name: "proc",
|
|
136
138
|
description: "Show safe process resource and uptime information",
|
|
139
|
+
acknowledgement: "deferred-ephemeral",
|
|
137
140
|
rateLimit: { scope: "user", limit: 10, windowSeconds: 60 },
|
|
138
141
|
async execute(context) {
|
|
139
142
|
if (!(await authorizeDeveloperCommand(context)))
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { type ModuleManifest } from "@helyx/module-manifest";
|
|
1
2
|
import type { DiscordGuildHealthInspectionService, DiscordGuildHealthUnavailable, ModuleDefinition, ServiceAccess } from "@helyx/sdk";
|
|
2
3
|
import { type Client } from "discord.js";
|
|
3
4
|
export declare function moduleCatalogueMatches(packagedManifest: unknown, executableManifest: unknown): boolean;
|
|
4
5
|
export declare function missingRequiredPermissions(grantedPermissions: readonly string[], requiredPermissions: readonly string[]): string[];
|
|
5
6
|
export declare function hasRequiredModuleConfiguration(exposesConfiguration: boolean, savedConfiguration: unknown): boolean;
|
|
6
|
-
export declare function isHealthInspectableModule(
|
|
7
|
+
export declare function isHealthInspectableModule(module: ModuleManifest | string): boolean;
|
|
7
8
|
export declare class DiscordGuildHealthInspection implements DiscordGuildHealthInspectionService {
|
|
8
9
|
#private;
|
|
9
10
|
private readonly client;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasDirectModuleHealthExposure, } from "@helyx/module-manifest";
|
|
1
2
|
import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
|
|
2
3
|
import { AuditLogEvent, GatewayIntentBits, } from "discord.js";
|
|
3
4
|
import { isDeepStrictEqual } from "node:util";
|
|
@@ -21,8 +22,10 @@ export function missingRequiredPermissions(grantedPermissions, requiredPermissio
|
|
|
21
22
|
export function hasRequiredModuleConfiguration(exposesConfiguration, savedConfiguration) {
|
|
22
23
|
return !exposesConfiguration || savedConfiguration !== null;
|
|
23
24
|
}
|
|
24
|
-
export function isHealthInspectableModule(
|
|
25
|
-
|
|
25
|
+
export function isHealthInspectableModule(module) {
|
|
26
|
+
const moduleId = typeof module === "string" ? module : module.id;
|
|
27
|
+
return (!INTERNAL_HELYX_MODULE_IDS.has(moduleId) &&
|
|
28
|
+
(typeof module === "string" || hasDirectModuleHealthExposure(module)));
|
|
26
29
|
}
|
|
27
30
|
export class DiscordGuildHealthInspection {
|
|
28
31
|
client;
|
|
@@ -270,7 +273,7 @@ export class DiscordGuildHealthInspection {
|
|
|
270
273
|
return overview;
|
|
271
274
|
const items = [];
|
|
272
275
|
const batchSize = Math.min(20, Math.max(1, input.batchSize ?? 20));
|
|
273
|
-
const inspectableModules = this.modules.filter((module) => isHealthInspectableModule(module.manifest
|
|
276
|
+
const inspectableModules = this.modules.filter((module) => isHealthInspectableModule(module.manifest));
|
|
274
277
|
for (let index = 0; index < inspectableModules.length; index += 1) {
|
|
275
278
|
const module = inspectableModules[index];
|
|
276
279
|
const state = await installations.getModuleState(input.guildId, module.manifest.id);
|
package/dist/events.js
CHANGED
|
@@ -3,6 +3,20 @@ import { ChannelType, Events, GatewayIntentBits, GuildMemberFlags, PermissionFla
|
|
|
3
3
|
import { toReplyOptions } from "./interactions.js";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { classifyGuildMessage, unavailableGuildMessageClassification, } from "./message-classification.js";
|
|
6
|
+
function availabilityAwareInstallations(services) {
|
|
7
|
+
const installations = services.get(HELYX_SERVICE_NAMES.installations);
|
|
8
|
+
const candidate = typeof services.has === "function" &&
|
|
9
|
+
services.has(HELYX_SERVICE_NAMES.moduleAvailability)
|
|
10
|
+
? services.get(HELYX_SERVICE_NAMES.moduleAvailability)
|
|
11
|
+
: null;
|
|
12
|
+
const availability = typeof candidate?.isAvailable === "function" ? candidate : null;
|
|
13
|
+
return {
|
|
14
|
+
async isModuleEnabled(guildId, moduleId) {
|
|
15
|
+
return ((!availability || (await availability.isAvailable(moduleId))) &&
|
|
16
|
+
installations.isModuleEnabled(guildId, moduleId));
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
6
20
|
export function installModuleEvents(client, modules, logger, services) {
|
|
7
21
|
const privilegedMessageContributions = modules.flatMap((module) => (module.events?.privilegedMessageCreate ?? []).map((contribution) => ({
|
|
8
22
|
moduleId: module.manifest.id,
|
|
@@ -113,7 +127,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
113
127
|
if (!message.guildId || !privilegedEligibilityQueue)
|
|
114
128
|
return;
|
|
115
129
|
privilegedEligibilityQueue.enqueue(message.guildId, async () => {
|
|
116
|
-
const installations = services
|
|
130
|
+
const installations = availabilityAwareInstallations(services);
|
|
117
131
|
const eligible = [];
|
|
118
132
|
for (const item of privilegedMessageContributions) {
|
|
119
133
|
try {
|
|
@@ -260,7 +274,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
260
274
|
});
|
|
261
275
|
}
|
|
262
276
|
async function handleResourceLifecycle(contributions, serverId, identifier, resourceId, eventName) {
|
|
263
|
-
const installations = services
|
|
277
|
+
const installations = availabilityAwareInstallations(services);
|
|
264
278
|
for (const { moduleId, contribution } of contributions) {
|
|
265
279
|
try {
|
|
266
280
|
if (!(await installations.isModuleEnabled(serverId, moduleId))) {
|
|
@@ -316,7 +330,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
316
330
|
async function handleGuildMessageCreate(message) {
|
|
317
331
|
if (!message.guildId || !message.channelId)
|
|
318
332
|
return;
|
|
319
|
-
const installations = services
|
|
333
|
+
const installations = availabilityAwareInstallations(services);
|
|
320
334
|
const parentChannelId = message.channel.isThread()
|
|
321
335
|
? (message.channel.parentId ?? undefined)
|
|
322
336
|
: undefined;
|
|
@@ -410,7 +424,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
410
424
|
: reaction.message;
|
|
411
425
|
if (!message.guildId || !message.channelId)
|
|
412
426
|
return;
|
|
413
|
-
const installations = services
|
|
427
|
+
const installations = availabilityAwareInstallations(services);
|
|
414
428
|
const member = await message.guild?.members.fetch(user.id);
|
|
415
429
|
const userRoleIds = member ? [...member.roles.cache.keys()] : [];
|
|
416
430
|
const parentChannelId = messageParentChannelId(message);
|
|
@@ -467,7 +481,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
467
481
|
async function handleMemberAdd(member) {
|
|
468
482
|
if (!member.joinedAt)
|
|
469
483
|
throw new Error("Discord member join timestamp is unavailable");
|
|
470
|
-
const installations = services
|
|
484
|
+
const installations = availabilityAwareInstallations(services);
|
|
471
485
|
for (const { moduleId, contribution } of memberAddContributions) {
|
|
472
486
|
try {
|
|
473
487
|
if (!(await installations.isModuleEnabled(member.guild.id, moduleId))) {
|
|
@@ -498,7 +512,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
498
512
|
}
|
|
499
513
|
}
|
|
500
514
|
async function handleMemberRemove(member) {
|
|
501
|
-
const installations = services
|
|
515
|
+
const installations = availabilityAwareInstallations(services);
|
|
502
516
|
for (const { moduleId, contribution } of memberRemoveContributions) {
|
|
503
517
|
try {
|
|
504
518
|
if (!(await installations.isModuleEnabled(member.guild.id, moduleId))) {
|
|
@@ -536,7 +550,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
536
550
|
: reaction.message;
|
|
537
551
|
if (!message.guildId || !message.channelId)
|
|
538
552
|
return;
|
|
539
|
-
const installations = services
|
|
553
|
+
const installations = availabilityAwareInstallations(services);
|
|
540
554
|
const parentChannelId = messageParentChannelId(message);
|
|
541
555
|
for (const { moduleId, contribution } of reactionRemoveContributions) {
|
|
542
556
|
if (!(await installations.isModuleEnabled(message.guildId, moduleId))) {
|
|
@@ -580,7 +594,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
580
594
|
: receivedMessage;
|
|
581
595
|
if (!message.guildId || !message.channelId)
|
|
582
596
|
return;
|
|
583
|
-
const installations = services
|
|
597
|
+
const installations = availabilityAwareInstallations(services);
|
|
584
598
|
for (const { moduleId, contribution } of reactionRemoveAllContributions) {
|
|
585
599
|
try {
|
|
586
600
|
if (!(await installations.isModuleEnabled(message.guildId, moduleId)))
|
|
@@ -610,7 +624,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
610
624
|
const message = reaction.message;
|
|
611
625
|
if (!message.guildId || !message.channelId)
|
|
612
626
|
return;
|
|
613
|
-
const installations = services
|
|
627
|
+
const installations = availabilityAwareInstallations(services);
|
|
614
628
|
for (const { moduleId, contribution } of reactionRemoveEmojiContributions) {
|
|
615
629
|
try {
|
|
616
630
|
if (!(await installations.isModuleEnabled(message.guildId, moduleId)))
|
|
@@ -638,7 +652,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
638
652
|
async function handleMessageUpdate(receivedMessage) {
|
|
639
653
|
if (!receivedMessage.guildId || !receivedMessage.channelId)
|
|
640
654
|
return;
|
|
641
|
-
const installations = services
|
|
655
|
+
const installations = availabilityAwareInstallations(services);
|
|
642
656
|
for (const { moduleId, contribution } of messageUpdateContributions) {
|
|
643
657
|
try {
|
|
644
658
|
if (!(await installations.isModuleEnabled(receivedMessage.guildId, moduleId)))
|
|
@@ -672,7 +686,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
672
686
|
async function handleMessageDelete(message) {
|
|
673
687
|
if (!message.guildId || !message.channelId)
|
|
674
688
|
return;
|
|
675
|
-
const installations = services
|
|
689
|
+
const installations = availabilityAwareInstallations(services);
|
|
676
690
|
for (const { moduleId, contribution } of messageDeleteContributions) {
|
|
677
691
|
try {
|
|
678
692
|
if (!(await installations.isModuleEnabled(message.guildId, moduleId))) {
|
|
@@ -699,7 +713,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
699
713
|
async function handleMessageDeleteBulk(guildId, channelId, messageIds) {
|
|
700
714
|
if (messageIds.length === 0)
|
|
701
715
|
return;
|
|
702
|
-
const installations = services
|
|
716
|
+
const installations = availabilityAwareInstallations(services);
|
|
703
717
|
const boundedIds = messageIds.slice(0, 100);
|
|
704
718
|
for (const { moduleId, contribution } of messageDeleteBulkContributions) {
|
|
705
719
|
try {
|
|
@@ -733,7 +747,7 @@ export function installModuleEvents(client, modules, logger, services) {
|
|
|
733
747
|
voiceSessions.delete(key);
|
|
734
748
|
const endedAt = new Date();
|
|
735
749
|
const durationSeconds = Math.max(1, Math.floor((endedAt.getTime() - previous.startedAt.getTime()) / 1_000));
|
|
736
|
-
const installations = services
|
|
750
|
+
const installations = availabilityAwareInstallations(services);
|
|
737
751
|
for (const { moduleId, contribution } of guildVoiceSessionContributions) {
|
|
738
752
|
try {
|
|
739
753
|
if (!(await installations.isModuleEnabled(previous.guildId, moduleId)))
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseBotEnvironment } from "@helyx/config";
|
|
2
|
+
import { StatusHeartbeatReporter } from "@helyx/status-client";
|
|
2
3
|
import { safeError, ServiceRegistry } from "@helyx/core";
|
|
3
4
|
import { checkDatabaseConnection, createDatabasePool, AuditRepository, InteractionSessionRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
|
|
4
5
|
import { createRuntime } from "@helyx/framework";
|
|
@@ -44,9 +45,45 @@ export async function startBot(processEnvironment = process.env) {
|
|
|
44
45
|
return startShardWorker(processEnvironment, parseShardWorkerLaunchContract(processEnvironment.HELYX_SHARD_WORKER_CONTRACT));
|
|
45
46
|
const environment = parseBotEnvironment(processEnvironment);
|
|
46
47
|
bootstrapLogger.level = environment.LOG_LEVEL;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
const handle = environment.HELYX_SHARD_MODE !== "off"
|
|
49
|
+
? await startShardSupervisor(environment, processEnvironment, resolveBotWorkerEntryPoint())
|
|
50
|
+
: await startUnshardedBot(processEnvironment);
|
|
51
|
+
if (!environment.HELYX_STATUS_ENABLED)
|
|
52
|
+
return handle;
|
|
53
|
+
const processStartedAt = new Date().toISOString();
|
|
54
|
+
const reporter = new StatusHeartbeatReporter({
|
|
55
|
+
producer: "bot",
|
|
56
|
+
baseUrl: environment.HELYX_STATUS_URL,
|
|
57
|
+
secret: environment.HELYX_STATUS_HEARTBEAT_SECRET,
|
|
58
|
+
accessClientId: environment.CF_ACCESS_CLIENT_ID,
|
|
59
|
+
accessClientSecret: environment.CF_ACCESS_CLIENT_SECRET,
|
|
60
|
+
intervalSeconds: environment.HELYX_STATUS_HEARTBEAT_INTERVAL_SECONDS,
|
|
61
|
+
services: () => [
|
|
62
|
+
{
|
|
63
|
+
key: "discord-bot",
|
|
64
|
+
state: "operational",
|
|
65
|
+
processStartedAt,
|
|
66
|
+
release: readBotVersion(),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
key: "creator-connections",
|
|
70
|
+
state: environment.HELYX_OAUTH_ENABLED ? "operational" : "major_outage",
|
|
71
|
+
reasonCode: environment.HELYX_OAUTH_ENABLED ? null : "feature_disabled",
|
|
72
|
+
processStartedAt,
|
|
73
|
+
release: readBotVersion(),
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
onError(error) {
|
|
77
|
+
bootstrapLogger.warn({ event: "status.heartbeat_failed", reason: error.message }, "Status heartbeat could not be delivered");
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
reporter.start();
|
|
81
|
+
return {
|
|
82
|
+
async shutdown(reason) {
|
|
83
|
+
await reporter.stop();
|
|
84
|
+
await handle.shutdown(reason);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
50
87
|
}
|
|
51
88
|
export function resolveBotWorkerEntryPoint(entryUrl = import.meta.url) {
|
|
52
89
|
return fileURLToPath(entryUrl);
|
|
@@ -254,6 +291,9 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
254
291
|
})) {
|
|
255
292
|
platformServices.set(name, service);
|
|
256
293
|
}
|
|
294
|
+
await serviceAccess
|
|
295
|
+
.get(HELYX_SERVICE_NAMES.moduleAvailability)
|
|
296
|
+
.refresh();
|
|
257
297
|
if (environment.HELYX_VERIFICATION_ENABLED &&
|
|
258
298
|
environment.HELYX_VERIFICATION_URL &&
|
|
259
299
|
environment.HELYX_VERIFICATION_INTERNAL_SECRET) {
|