@helyx/bot 0.4.32 → 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 +49 -0
- package/dist/cloud-management-module.js +5 -0
- package/dist/control-server.d.ts +29 -8
- package/dist/control-server.js +69 -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 +8 -3
- package/dist/oauth-service.js +1 -1
- package/dist/permissions-module.js +16 -4
- package/dist/scheduled-task-runner.js +7 -0
- package/dist/shard-contracts.d.ts +42 -0
- package/dist/shard-contracts.js +26 -0
- package/dist/shard-router.d.ts +3 -0
- package/dist/shard-router.js +8 -0
- package/dist/shard-supervisor.js +30 -3
- 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,54 @@
|
|
|
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
|
+
|
|
42
|
+
## 0.4.33
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- Add versioned OAuth consumer cleanup contracts, durable and resumable
|
|
47
|
+
revocation state, least-privilege provider scopes, and Creator Connections
|
|
48
|
+
cleanup dispatch. Promote the hosted-only Creator Connections module to live
|
|
49
|
+
availability with clear OAuth activation validation. Ignore shard responses
|
|
50
|
+
that finish after shutdown has already detached their worker.
|
|
51
|
+
|
|
3
52
|
## 0.4.32
|
|
4
53
|
|
|
5
54
|
### 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
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { type DatabasePool, type GuildShardOwnership } from "@helyx/database";
|
|
2
|
-
import { type DashboardActionResult, type GuildAccessPolicy, type ManagedResourceCreationIntent, type ManagedResourceDetail, type ManagedResourcePage, type ManagedResourceRelationPage, type ManagedResourceValue, type MessageResponse, type ModuleDefinition, type PermissionActor, type PrivacyCapturePage, type PrivacyErasePage, type PrivacyProviderExecutor, type PublicAchievementProjectionPage, type ServiceAccess } from "@helyx/sdk";
|
|
2
|
+
import { type DashboardActionResult, type GuildAccessPolicy, type ManagedResourceCreationIntent, type ManagedResourceDetail, type ManagedResourcePage, type ManagedResourceRelationPage, type ManagedResourceValue, type MessageResponse, type ModuleDefinition, type OAuthConsumerCleanupResult, type PermissionActor, type PrivacyCapturePage, type PrivacyErasePage, type PrivacyProviderExecutor, type PublicAchievementProjectionPage, type ServiceAccess } from "@helyx/sdk";
|
|
3
3
|
import { type ActionReceipt, type SignedAssessmentEnvelope } from "@helyx/verification-contracts";
|
|
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>;
|
|
@@ -143,7 +140,19 @@ export interface DashboardGuildAccessState extends GuildAccessPolicy {
|
|
|
143
140
|
updatedAt: string | null;
|
|
144
141
|
}
|
|
145
142
|
export interface BotControlHandler {
|
|
143
|
+
cleanupOAuthConsumer(input: {
|
|
144
|
+
consumerBindingId: string;
|
|
145
|
+
moduleId: string;
|
|
146
|
+
provider: "youtube" | "twitch";
|
|
147
|
+
guildId: string;
|
|
148
|
+
resourceType: string;
|
|
149
|
+
resourceId: string;
|
|
150
|
+
subjectUserId: string;
|
|
151
|
+
contractVersion: number;
|
|
152
|
+
correlationId: string;
|
|
153
|
+
}): Promise<OAuthConsumerCleanupResult>;
|
|
146
154
|
sendSupportNotification(input: SupportNotificationInput): Promise<void>;
|
|
155
|
+
listAvailabilityModules(): Promise<AvailabilityModuleCatalogue>;
|
|
147
156
|
listGuildPresence(input: {
|
|
148
157
|
guildIds: readonly string[];
|
|
149
158
|
correlationId: string;
|
|
@@ -339,6 +348,17 @@ export declare class BotControlService implements BotControlHandler {
|
|
|
339
348
|
guildId: string;
|
|
340
349
|
channelId: string;
|
|
341
350
|
} | undefined, presenceOwnership?: GuildShardOwnership | undefined, presenceShardCount?: number | undefined);
|
|
351
|
+
cleanupOAuthConsumer(input: {
|
|
352
|
+
consumerBindingId: string;
|
|
353
|
+
moduleId: string;
|
|
354
|
+
provider: "youtube" | "twitch";
|
|
355
|
+
guildId: string;
|
|
356
|
+
resourceType: string;
|
|
357
|
+
resourceId: string;
|
|
358
|
+
subjectUserId: string;
|
|
359
|
+
contractVersion: number;
|
|
360
|
+
correlationId: string;
|
|
361
|
+
}): Promise<OAuthConsumerCleanupResult>;
|
|
342
362
|
listGuildPresence(input: {
|
|
343
363
|
guildIds: readonly string[];
|
|
344
364
|
correlationId: string;
|
|
@@ -519,6 +539,7 @@ export declare class BotControlService implements BotControlHandler {
|
|
|
519
539
|
correlationId: string;
|
|
520
540
|
actorAuthenticatedAt?: string;
|
|
521
541
|
}): Promise<ManagedResourceDetail | null>;
|
|
542
|
+
listAvailabilityModules(): Promise<AvailabilityModuleCatalogue>;
|
|
522
543
|
}
|
|
523
544
|
export declare function createBotControlServer(input: {
|
|
524
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);
|
|
@@ -286,6 +290,26 @@ export class BotControlService {
|
|
|
286
290
|
})
|
|
287
291
|
: []);
|
|
288
292
|
}
|
|
293
|
+
async cleanupOAuthConsumer(input) {
|
|
294
|
+
const module = this.#modules.get(input.moduleId);
|
|
295
|
+
if (!module)
|
|
296
|
+
throw new BotControlNotFoundError("OAuth consumer module is unavailable");
|
|
297
|
+
const consumer = module.oauthConsumers?.find((candidate) => candidate.provider === input.provider &&
|
|
298
|
+
candidate.resourceType === input.resourceType &&
|
|
299
|
+
candidate.version === input.contractVersion);
|
|
300
|
+
if (!consumer)
|
|
301
|
+
throw new BotControlNotFoundError("OAuth consumer cleanup contract is unavailable");
|
|
302
|
+
return consumer.cleanup({
|
|
303
|
+
consumerBindingId: input.consumerBindingId,
|
|
304
|
+
provider: input.provider,
|
|
305
|
+
guildId: input.guildId,
|
|
306
|
+
resourceType: input.resourceType,
|
|
307
|
+
resourceId: input.resourceId,
|
|
308
|
+
subjectUserId: input.subjectUserId,
|
|
309
|
+
correlationId: input.correlationId,
|
|
310
|
+
services: this.#services,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
289
313
|
async listGuildPresence(input) {
|
|
290
314
|
const guildIds = [...new Set(input.guildIds)];
|
|
291
315
|
if (guildIds.length > 200)
|
|
@@ -403,14 +427,16 @@ export class BotControlService {
|
|
|
403
427
|
commandId: id,
|
|
404
428
|
})),
|
|
405
429
|
]);
|
|
406
|
-
const [resources, installedModules, storedLoggingPolicies, runtime, storedConfigurations, storedPermissions,] = await Promise.all([
|
|
430
|
+
const [resources, installedModules, storedLoggingPolicies, runtime, storedConfigurations, storedPermissions, availabilityStates,] = await Promise.all([
|
|
407
431
|
this.resources.getGuildResources(guildId),
|
|
408
432
|
new InstallationRepository(this.database).listModules(guildId),
|
|
409
433
|
new ModuleLoggingPolicyRepository(this.database).listForGuild(guildId),
|
|
410
434
|
this.#inspectRuntimeCapabilities(),
|
|
411
435
|
this.#readConfigurations(guildId, moduleIds),
|
|
412
436
|
this.#readPermissionPolicies(guildId, permissionTargets),
|
|
437
|
+
moduleAvailabilityStates(this.#services, moduleIds),
|
|
413
438
|
]);
|
|
439
|
+
const disabledModules = disabledModuleSummary(availabilityStates);
|
|
414
440
|
const installed = new Map(installedModules.map((module) => [module.moduleId, module]));
|
|
415
441
|
const moduleStatuses = {};
|
|
416
442
|
const configurations = {};
|
|
@@ -422,11 +448,7 @@ export class BotControlService {
|
|
|
422
448
|
]));
|
|
423
449
|
for (const { dashboardModule, module } of dashboardEntries) {
|
|
424
450
|
const installation = installed.get(dashboardModule.id);
|
|
425
|
-
moduleStatuses[dashboardModule.id] = installation
|
|
426
|
-
? installation.enabled
|
|
427
|
-
? "enabled"
|
|
428
|
-
: "disabled"
|
|
429
|
-
: "unavailable";
|
|
451
|
+
moduleStatuses[dashboardModule.id] = dashboardModuleStatus(disabledModules.set, dashboardModule.id, installation);
|
|
430
452
|
const configuration = storedConfigurations.get(dashboardModule.id);
|
|
431
453
|
if (configuration)
|
|
432
454
|
configurations[dashboardModule.id] = {
|
|
@@ -450,6 +472,7 @@ export class BotControlService {
|
|
|
450
472
|
...resources,
|
|
451
473
|
modules: structuredClone(this.#dashboardModules),
|
|
452
474
|
moduleStatuses,
|
|
475
|
+
globallyDisabledModuleIds: disabledModules.ids,
|
|
453
476
|
configurations,
|
|
454
477
|
permissions,
|
|
455
478
|
logging,
|
|
@@ -604,6 +627,7 @@ export class BotControlService {
|
|
|
604
627
|
}
|
|
605
628
|
async setModuleStatus(input) {
|
|
606
629
|
const module = this.#requireDashboardModule(input.moduleId);
|
|
630
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
607
631
|
const scopes = Object.entries(module.records ?? {})
|
|
608
632
|
.filter(([, collection]) => (collection.incrementableFields?.length ?? 0) > 0)
|
|
609
633
|
.map(([collection]) => ({
|
|
@@ -657,6 +681,7 @@ export class BotControlService {
|
|
|
657
681
|
}
|
|
658
682
|
async updateConfiguration(input) {
|
|
659
683
|
const module = this.#requireDashboardModule(input.moduleId);
|
|
684
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
660
685
|
await this.#validateDiscordSettings(input.guildId, module, input.value);
|
|
661
686
|
if (module.configuration?.validateUpdate) {
|
|
662
687
|
try {
|
|
@@ -690,6 +715,7 @@ export class BotControlService {
|
|
|
690
715
|
}
|
|
691
716
|
async updatePermissions(input) {
|
|
692
717
|
const module = this.#requireDashboardModule(input.moduleId);
|
|
718
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
693
719
|
const commands = dashboardCommands(module);
|
|
694
720
|
const supplied = new Map(input.state.commands.map((command) => [command.commandId, command]));
|
|
695
721
|
if (supplied.size !== input.state.commands.length ||
|
|
@@ -813,6 +839,10 @@ export class BotControlService {
|
|
|
813
839
|
return this.#getPermissionState(input.guildId, module);
|
|
814
840
|
}
|
|
815
841
|
async updateLoggingPolicy(input) {
|
|
842
|
+
await Promise.all([
|
|
843
|
+
this.#requireModuleAvailable(input.moduleId),
|
|
844
|
+
this.#requireModuleAvailable("helyx.logging"),
|
|
845
|
+
]);
|
|
816
846
|
if (!this.#modules.has("helyx.logging")) {
|
|
817
847
|
throw new BotControlNotFoundError("Logging is not installed");
|
|
818
848
|
}
|
|
@@ -857,6 +887,7 @@ export class BotControlService {
|
|
|
857
887
|
}
|
|
858
888
|
async executeDashboardAction(input) {
|
|
859
889
|
const module = this.#requireDashboardModule(input.moduleId);
|
|
890
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
860
891
|
const declared = module.manifest.dashboard?.actions?.find(({ id }) => id === input.actionId);
|
|
861
892
|
const action = module.dashboardActions?.find(({ id }) => id === input.actionId);
|
|
862
893
|
if (!declared || !action) {
|
|
@@ -915,11 +946,13 @@ export class BotControlService {
|
|
|
915
946
|
})), relation.columns.map(({ key }) => key));
|
|
916
947
|
}
|
|
917
948
|
async previewManagedResource(input) {
|
|
949
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
918
950
|
const { module, resource } = this.#prepareManagedResource(input);
|
|
919
951
|
this.#requireManagedResourceOperation(module, input.resourceType, "preview");
|
|
920
952
|
return resource.preview(await this.#managedResourceContext(input, module, "preview"), input.value);
|
|
921
953
|
}
|
|
922
954
|
async createManagedResource(input) {
|
|
955
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
923
956
|
const { module, resource } = this.#prepareManagedResource(input);
|
|
924
957
|
this.#requireManagedResourceOperation(module, input.resourceType, "create");
|
|
925
958
|
const context = await this.#managedResourceContext(input, module, "create");
|
|
@@ -929,6 +962,7 @@ export class BotControlService {
|
|
|
929
962
|
return normalizeManagedResourceDetail(managedResourceDetailSchema.parse(await resource.create(context, input.value, input.intent)), managedResourceColumnKeys(module, input.resourceType));
|
|
930
963
|
}
|
|
931
964
|
async updateManagedResource(input) {
|
|
965
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
932
966
|
const { module, resource } = this.#prepareManagedResource(input);
|
|
933
967
|
this.#requireManagedResourceOperation(module, input.resourceType, "update");
|
|
934
968
|
return normalizeManagedResourceDetail(managedResourceDetailSchema.parse(await resource.update(await this.#managedResourceContext(input, module, "update"), {
|
|
@@ -938,6 +972,7 @@ export class BotControlService {
|
|
|
938
972
|
})), managedResourceColumnKeys(module, input.resourceType));
|
|
939
973
|
}
|
|
940
974
|
async deleteManagedResource(input) {
|
|
975
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
941
976
|
const { module, resource } = this.#prepareManagedResource(input);
|
|
942
977
|
this.#requireManagedResourceOperation(module, input.resourceType, "delete");
|
|
943
978
|
return managedResourceDeleteResultSchema.parse(await resource.delete(await this.#managedResourceContext(input, module, "delete"), {
|
|
@@ -946,6 +981,7 @@ export class BotControlService {
|
|
|
946
981
|
}));
|
|
947
982
|
}
|
|
948
983
|
async executeManagedResourceAction(input) {
|
|
984
|
+
await this.#requireModuleAvailable(input.moduleId);
|
|
949
985
|
const { module, resource } = this.#prepareManagedResource(input);
|
|
950
986
|
const declared = module.manifest.dashboard?.resources
|
|
951
987
|
?.find(({ id }) => id === input.resourceType)
|
|
@@ -1041,6 +1077,14 @@ export class BotControlService {
|
|
|
1041
1077
|
throw new BotControlNotFoundError("Module is not installed");
|
|
1042
1078
|
return module;
|
|
1043
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
|
+
}
|
|
1044
1088
|
#requireDashboardModule(moduleIdValue) {
|
|
1045
1089
|
const module = this.#requireModule(moduleIdValue);
|
|
1046
1090
|
if (!module.manifest.dashboard)
|
|
@@ -1213,6 +1257,23 @@ export async function createBotControlServer(input) {
|
|
|
1213
1257
|
server.get("/health", (_request, reply) => readiness(reply));
|
|
1214
1258
|
server.get("/v1/health", (_request, reply) => readiness(reply));
|
|
1215
1259
|
server.post("/v1/verification/results", async (request) => input.service.acceptVerificationResult(signedAssessmentEnvelopeSchema.parse(request.body)));
|
|
1260
|
+
server.post("/v1/oauth/consumers/cleanup", async (request) => {
|
|
1261
|
+
const body = z
|
|
1262
|
+
.object({
|
|
1263
|
+
consumerBindingId: z.uuid(),
|
|
1264
|
+
moduleId,
|
|
1265
|
+
provider: z.enum(["youtube", "twitch"]),
|
|
1266
|
+
guildId: snowflake,
|
|
1267
|
+
resourceType: z.string().regex(/^[a-z][a-z0-9_-]*$/u),
|
|
1268
|
+
resourceId,
|
|
1269
|
+
subjectUserId: snowflake,
|
|
1270
|
+
contractVersion: z.number().int().min(1).max(32_767),
|
|
1271
|
+
correlationId: z.string().trim().min(1).max(200),
|
|
1272
|
+
})
|
|
1273
|
+
.strict()
|
|
1274
|
+
.parse(request.body);
|
|
1275
|
+
return input.service.cleanupOAuthConsumer(body);
|
|
1276
|
+
});
|
|
1216
1277
|
server.get("/v1/shards/health", async (request, reply) => {
|
|
1217
1278
|
reply.header("Cache-Control", "no-store");
|
|
1218
1279
|
if (!input.shardHealth)
|
|
@@ -1331,6 +1392,7 @@ export async function createBotControlServer(input) {
|
|
|
1331
1392
|
const { guildId } = z.object({ guildId: snowflake }).parse(request.params);
|
|
1332
1393
|
return input.service.getGuildState(guildId);
|
|
1333
1394
|
});
|
|
1395
|
+
server.get("/v1/modules/availability-catalogue", () => input.service.listAvailabilityModules());
|
|
1334
1396
|
server.post("/v1/guilds/:guildId/dashboard-access", async (request) => {
|
|
1335
1397
|
const { guildId } = z.object({ guildId: snowflake }).parse(request.params);
|
|
1336
1398
|
const { actorUserId } = z
|
|
@@ -1860,35 +1922,6 @@ function mapDashboardModule(module) {
|
|
|
1860
1922
|
: undefined,
|
|
1861
1923
|
};
|
|
1862
1924
|
}
|
|
1863
|
-
function dashboardCommands(module) {
|
|
1864
|
-
const commands = (module.interactions?.commands ?? []).flatMap((command) => command.subcommands?.length
|
|
1865
|
-
? command.subcommands.map((subcommand) => {
|
|
1866
|
-
const id = subcommand.permissionId ??
|
|
1867
|
-
command.permissionId ??
|
|
1868
|
-
`${command.name}.${subcommand.name}`;
|
|
1869
|
-
return {
|
|
1870
|
-
id,
|
|
1871
|
-
name: `/${command.name} ${subcommand.name}`,
|
|
1872
|
-
description: subcommand.description,
|
|
1873
|
-
defaultPreset: toDashboardPolicy(module.defaultPermissions?.commands[id] ??
|
|
1874
|
-
module.defaultPermissions?.module ??
|
|
1875
|
-
emptyPolicy()).preset,
|
|
1876
|
-
};
|
|
1877
|
-
})
|
|
1878
|
-
: [
|
|
1879
|
-
{
|
|
1880
|
-
id: command.permissionId ?? command.name,
|
|
1881
|
-
name: `/${command.name}`,
|
|
1882
|
-
description: command.description,
|
|
1883
|
-
defaultPreset: toDashboardPolicy(module.defaultPermissions?.commands[command.name] ??
|
|
1884
|
-
module.defaultPermissions?.module ??
|
|
1885
|
-
emptyPolicy()).preset,
|
|
1886
|
-
},
|
|
1887
|
-
]);
|
|
1888
|
-
return [
|
|
1889
|
-
...new Map(commands.map((command) => [command.id, command])).values(),
|
|
1890
|
-
];
|
|
1891
|
-
}
|
|
1892
1925
|
function assertPermissionMutationAllowed(decision) {
|
|
1893
1926
|
if (decision.allowed)
|
|
1894
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);
|