@helyx/bot 1.4.0 → 1.5.0
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 +23 -0
- package/dist/channel-audit-configuration.d.ts +7 -0
- package/dist/channel-audit-configuration.js +40 -0
- package/dist/channel-audit-control.d.ts +14 -0
- package/dist/channel-audit-control.js +61 -0
- package/dist/channel-audit-permissions.d.ts +56 -0
- package/dist/channel-audit-permissions.js +417 -0
- package/dist/channel-audit-routes.d.ts +4 -0
- package/dist/channel-audit-routes.js +42 -0
- package/dist/cloud-connector.js +2 -0
- package/dist/cloud-control-dispatcher.js +22 -0
- package/dist/control-errors.d.ts +8 -0
- package/dist/control-errors.js +24 -0
- package/dist/control-server-schemas.d.ts +2 -2
- package/dist/control-server.d.ts +4 -1
- package/dist/control-server.js +18 -25
- package/dist/discord-channel-audit.d.ts +9 -0
- package/dist/discord-channel-audit.js +195 -0
- package/dist/discord-guild-health.d.ts +2 -2
- package/dist/discord-role-safety.d.ts +4 -0
- package/dist/discord-role-safety.js +30 -0
- package/dist/events.d.ts +2 -0
- package/dist/events.js +5 -20
- package/dist/hosted-server-moderation.d.ts +2 -2
- package/dist/shard-contracts.d.ts +87 -0
- package/dist/shard-contracts.js +39 -0
- package/dist/shard-router.d.ts +2 -0
- package/dist/shard-router.js +12 -0
- package/package.json +5 -5
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { channelAuditModuleIdSchema, channelAuditInputSchema, channelAuditConfigurationSchema, channelAuditReportSchema, } from "@helyx/control-protocol/channel-audit";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { BotControlTransportError } from "./control-errors.js";
|
|
4
|
+
const parameters = z
|
|
5
|
+
.object({
|
|
6
|
+
guildId: z.string().regex(/^\d{17,20}$/u),
|
|
7
|
+
moduleId: channelAuditModuleIdSchema,
|
|
8
|
+
})
|
|
9
|
+
.strict();
|
|
10
|
+
const actor = z
|
|
11
|
+
.object({
|
|
12
|
+
actorUserId: z.string().regex(/^\d{17,20}$/u),
|
|
13
|
+
correlationId: z.uuid(),
|
|
14
|
+
})
|
|
15
|
+
.strict();
|
|
16
|
+
export function registerChannelAuditRoutes(server, control) {
|
|
17
|
+
server.post("/v1/guilds/:guildId/modules/:moduleId/channel-audit/config", async (request) => {
|
|
18
|
+
const input = {
|
|
19
|
+
...parameters.parse(request.params),
|
|
20
|
+
...actor.parse(request.body),
|
|
21
|
+
};
|
|
22
|
+
if (!control.readChannelAuditConfiguration)
|
|
23
|
+
throw unsupported();
|
|
24
|
+
return channelAuditConfigurationSchema.parse(await control.readChannelAuditConfiguration(input));
|
|
25
|
+
});
|
|
26
|
+
server.post("/v1/guilds/:guildId/modules/:moduleId/channel-audit/check", async (request) => {
|
|
27
|
+
const input = {
|
|
28
|
+
...parameters.parse(request.params),
|
|
29
|
+
...actor
|
|
30
|
+
.extend({ assumptions: channelAuditInputSchema })
|
|
31
|
+
.strict()
|
|
32
|
+
.parse(request.body),
|
|
33
|
+
};
|
|
34
|
+
if (!control.checkChannelAudit)
|
|
35
|
+
throw unsupported();
|
|
36
|
+
return channelAuditReportSchema.parse(await control.checkChannelAudit(input));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function unsupported() {
|
|
40
|
+
return new BotControlTransportError(503, "This bot version does not support Channel audit. Update the bot before trying again.");
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=channel-audit-routes.js.map
|
package/dist/cloud-connector.js
CHANGED
|
@@ -763,6 +763,8 @@ function connectorCapabilities() {
|
|
|
763
763
|
"guild.module.status.write.v1",
|
|
764
764
|
"guild.module.configuration.write.v1",
|
|
765
765
|
"guild.module.configuration.preview.v1",
|
|
766
|
+
"guild.module.channel-audit.config.read.v1",
|
|
767
|
+
"guild.module.channel-audit.check.v1",
|
|
766
768
|
"guild.module.permissions.write.v1",
|
|
767
769
|
"guild.module.logging.write.v1",
|
|
768
770
|
"guild.module.action.execute.v1",
|
|
@@ -121,6 +121,28 @@ export class CloudControlDispatcher {
|
|
|
121
121
|
});
|
|
122
122
|
break;
|
|
123
123
|
}
|
|
124
|
+
case "guild.module.channel-audit.config.read.v1": {
|
|
125
|
+
const input = parseOperationInput(request.operation, request.input);
|
|
126
|
+
if (!this.control.readChannelAuditConfiguration)
|
|
127
|
+
throw new Error("Channel audit is not supported by this bot version");
|
|
128
|
+
result = await this.control.readChannelAuditConfiguration({
|
|
129
|
+
...input,
|
|
130
|
+
actorUserId: requiredActor(actorUserId),
|
|
131
|
+
correlationId,
|
|
132
|
+
});
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case "guild.module.channel-audit.check.v1": {
|
|
136
|
+
const input = parseOperationInput(request.operation, request.input);
|
|
137
|
+
if (!this.control.checkChannelAudit)
|
|
138
|
+
throw new Error("Channel audit is not supported by this bot version");
|
|
139
|
+
result = await this.control.checkChannelAudit({
|
|
140
|
+
...input,
|
|
141
|
+
actorUserId: requiredActor(actorUserId),
|
|
142
|
+
correlationId,
|
|
143
|
+
});
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
124
146
|
case "guild.module.configuration.write.v1": {
|
|
125
147
|
const input = parseOperationInput(request.operation, request.input);
|
|
126
148
|
result = await this.control.updateConfiguration({
|
package/dist/control-errors.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { type PermissionMutationDenialReason } from "@helyx/core";
|
|
2
|
+
export declare function isUnknownGuildError(error: unknown): boolean;
|
|
3
|
+
export declare function isUnknownMemberError(error: unknown): boolean;
|
|
2
4
|
export declare class BotControlTransportError extends Error {
|
|
3
5
|
readonly status: 400 | 403 | 404 | 409 | 502 | 503 | 504;
|
|
4
6
|
constructor(status: 400 | 403 | 404 | 409 | 502 | 503 | 504, message: string);
|
|
5
7
|
}
|
|
8
|
+
export declare class BotControlValidationError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
export declare class BotControlNotFoundError extends Error {
|
|
12
|
+
constructor(message?: string);
|
|
13
|
+
}
|
|
6
14
|
export declare class BotControlForbiddenError extends Error {
|
|
7
15
|
constructor(message?: string);
|
|
8
16
|
}
|
package/dist/control-errors.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { permissionMutationDenialMessage, } from "@helyx/core";
|
|
2
|
+
export function isUnknownGuildError(error) {
|
|
3
|
+
return (typeof error === "object" &&
|
|
4
|
+
error !== null &&
|
|
5
|
+
"code" in error &&
|
|
6
|
+
error.code === 10_004);
|
|
7
|
+
}
|
|
8
|
+
export function isUnknownMemberError(error) {
|
|
9
|
+
return (typeof error === "object" &&
|
|
10
|
+
error !== null &&
|
|
11
|
+
"code" in error &&
|
|
12
|
+
error.code === 10_007);
|
|
13
|
+
}
|
|
2
14
|
export class BotControlTransportError extends Error {
|
|
3
15
|
status;
|
|
4
16
|
constructor(status, message) {
|
|
@@ -7,6 +19,18 @@ export class BotControlTransportError extends Error {
|
|
|
7
19
|
this.name = "BotControlTransportError";
|
|
8
20
|
}
|
|
9
21
|
}
|
|
22
|
+
export class BotControlValidationError extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "BotControlValidationError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class BotControlNotFoundError extends Error {
|
|
29
|
+
constructor(message = "Discord server is not available to Helyx") {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "BotControlNotFoundError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
10
34
|
export class BotControlForbiddenError extends Error {
|
|
11
35
|
constructor(message = "You no longer have permission to perform this action") {
|
|
12
36
|
super(message);
|
|
@@ -7,10 +7,10 @@ export declare const permissionState: z.ZodObject<{
|
|
|
7
7
|
version: z.ZodNumber;
|
|
8
8
|
module: z.ZodObject<{
|
|
9
9
|
preset: z.ZodEnum<{
|
|
10
|
-
custom: "custom";
|
|
11
10
|
everyone: "everyone";
|
|
12
11
|
moderators: "moderators";
|
|
13
12
|
administrators: "administrators";
|
|
13
|
+
custom: "custom";
|
|
14
14
|
}>;
|
|
15
15
|
allowedRoleIds: z.ZodArray<z.ZodString>;
|
|
16
16
|
deniedRoleIds: z.ZodArray<z.ZodString>;
|
|
@@ -21,10 +21,10 @@ export declare const permissionState: z.ZodObject<{
|
|
|
21
21
|
enabled: z.ZodBoolean;
|
|
22
22
|
policy: z.ZodObject<{
|
|
23
23
|
preset: z.ZodEnum<{
|
|
24
|
-
custom: "custom";
|
|
25
24
|
everyone: "everyone";
|
|
26
25
|
moderators: "moderators";
|
|
27
26
|
administrators: "administrators";
|
|
27
|
+
custom: "custom";
|
|
28
28
|
}>;
|
|
29
29
|
allowedRoleIds: z.ZodArray<z.ZodString>;
|
|
30
30
|
deniedRoleIds: z.ZodArray<z.ZodString>;
|
package/dist/control-server.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { type FirstTimeCommandSetupHandler } from "./first-time-command-setup.js
|
|
|
10
10
|
import { type ShardHealthSnapshot } from "./shard-contracts.js";
|
|
11
11
|
import { type DashboardPermissionState } from "./control-permissions.js";
|
|
12
12
|
export { BotControlTransportError } from "./control-errors.js";
|
|
13
|
+
import type { ChannelAuditControl } from "@helyx/sdk";
|
|
13
14
|
import { type DashboardModuleDefinition } from "./dashboard-module-projection.js";
|
|
14
15
|
import { type AvailabilityModuleCatalogue } from "./module-availability-control.js";
|
|
15
16
|
import { registerGuildCommandControlRoutes } from "./guild-command-control-routes.js";
|
|
@@ -120,7 +121,7 @@ export interface DashboardGuildAccessState extends GuildAccessPolicy {
|
|
|
120
121
|
actorHighestRolePosition: number | null;
|
|
121
122
|
updatedAt: string | null;
|
|
122
123
|
}
|
|
123
|
-
export interface BotControlHandler extends FirstTimeCommandSetupHandler, DashboardActionHandler {
|
|
124
|
+
export interface BotControlHandler extends FirstTimeCommandSetupHandler, DashboardActionHandler, ChannelAuditControl {
|
|
124
125
|
creatorOnboarding?(input: CreatorOnboardingRequest): Promise<CreatorOnboardingResponse>;
|
|
125
126
|
cleanupOAuthConsumer(input: {
|
|
126
127
|
consumerBindingId: string;
|
|
@@ -349,6 +350,8 @@ export declare class BotControlService implements BotControlHandler {
|
|
|
349
350
|
private readonly supportNotifications?;
|
|
350
351
|
private readonly presenceOwnership?;
|
|
351
352
|
private readonly presenceShardCount?;
|
|
353
|
+
readonly readChannelAuditConfiguration: Required<ChannelAuditControl>["readChannelAuditConfiguration"];
|
|
354
|
+
readonly checkChannelAudit: Required<ChannelAuditControl>["checkChannelAudit"];
|
|
352
355
|
constructor(databaseOrPersistence: DatabasePool | BotControlPersistence, modules: readonly ModuleDefinition[], resources: BotControlGuildResourceDirectory, services: ServiceAccess, privacyExecutor?: PrivacyProviderExecutor, supportNotifications?: {
|
|
353
356
|
guildId: string;
|
|
354
357
|
channelId: string;
|
package/dist/control-server.js
CHANGED
|
@@ -13,9 +13,11 @@ import { canAccessModule, PREMIUM_MODULE_UNAVAILABLE_MESSAGE, } from "./premium-
|
|
|
13
13
|
import { completeFirstTimeCommandSetup, } from "./first-time-command-setup.js";
|
|
14
14
|
import { SHARD_HEALTH_STALE_MS, } from "./shard-contracts.js";
|
|
15
15
|
import { assertVersion, declaredManagedResourcePermission, fromDashboardPolicy, managedResourcePermissionId, permissionPoliciesEqual, permissionPolicyKey, safeOptionalVersion, safeVersion, toDashboardPolicy, } from "./control-permissions.js";
|
|
16
|
-
import { assertPermissionMutationAllowed, BotControlForbiddenError, BotControlTransportError, } from "./control-errors.js";
|
|
16
|
+
import { assertPermissionMutationAllowed, BotControlForbiddenError, BotControlNotFoundError, BotControlValidationError, BotControlTransportError, isUnknownGuildError, isUnknownMemberError, } from "./control-errors.js";
|
|
17
17
|
export { BotControlTransportError } from "./control-errors.js";
|
|
18
18
|
import { dashboardCommands } from "./dashboard-commands.js";
|
|
19
|
+
import { registerChannelAuditRoutes } from "./channel-audit-routes.js";
|
|
20
|
+
import { createChannelAuditControl } from "./channel-audit-control.js";
|
|
19
21
|
import { executeCreatorOnboarding, registerCreatorOnboardingControlRoute, } from "./creator-onboarding-control.js";
|
|
20
22
|
import { mapDashboardModule, } from "./dashboard-module-projection.js";
|
|
21
23
|
import { availabilityCatalogueDefinitions, availabilityCatalogue, dashboardModuleStatus, disabledModuleSummary, moduleAvailabilityService, moduleAvailabilityStates, statusCatalogueDefinitions, } from "./module-availability-control.js";
|
|
@@ -215,6 +217,8 @@ export class BotControlService {
|
|
|
215
217
|
#services;
|
|
216
218
|
#privacyProviders;
|
|
217
219
|
#persistence;
|
|
220
|
+
readChannelAuditConfiguration;
|
|
221
|
+
checkChannelAudit;
|
|
218
222
|
constructor(databaseOrPersistence, modules, resources, services, privacyExecutor, supportNotifications, presenceOwnership, presenceShardCount, statusModules) {
|
|
219
223
|
this.resources = resources;
|
|
220
224
|
this.supportNotifications = supportNotifications;
|
|
@@ -235,6 +239,18 @@ export class BotControlService {
|
|
|
235
239
|
this.#permissions = services.get(HELYX_SERVICE_NAMES.permissions);
|
|
236
240
|
this.#services = services;
|
|
237
241
|
this.#privacyProviders = createPrivacyProviderRegistry(modules, privacyExecutor, this.#services);
|
|
242
|
+
const audit = createChannelAuditControl({
|
|
243
|
+
modules: this.#modules,
|
|
244
|
+
resources,
|
|
245
|
+
services,
|
|
246
|
+
configurations: this.#configurations,
|
|
247
|
+
permissions: this.#permissions,
|
|
248
|
+
listModules: (guildId) => this.#persistence.repositories.installations.listModules(guildId),
|
|
249
|
+
readGuildPolicy: (guildId) => this.#readGuildAccessPolicy(guildId),
|
|
250
|
+
requireAccessible: (guildId, moduleId) => this.#requireModuleAccessible(guildId, moduleId),
|
|
251
|
+
});
|
|
252
|
+
this.readChannelAuditConfiguration = audit.readChannelAuditConfiguration;
|
|
253
|
+
this.checkChannelAudit = audit.checkChannelAudit;
|
|
238
254
|
}
|
|
239
255
|
async creatorOnboarding(input) {
|
|
240
256
|
return executeCreatorOnboarding(input, {
|
|
@@ -1493,6 +1509,7 @@ export async function createBotControlServer(input) {
|
|
|
1493
1509
|
return input.service.setModuleStatus({ ...params, ...body });
|
|
1494
1510
|
});
|
|
1495
1511
|
registerDashboardActionRoutes(server, input.service);
|
|
1512
|
+
registerChannelAuditRoutes(server, input.service);
|
|
1496
1513
|
server.get("/v1/guilds/:guildId/modules/:moduleId/resources/:resourceType", async (request) => {
|
|
1497
1514
|
const params = z
|
|
1498
1515
|
.object({
|
|
@@ -1690,31 +1707,7 @@ export function mapBotControlError(error) {
|
|
|
1690
1707
|
};
|
|
1691
1708
|
return undefined;
|
|
1692
1709
|
}
|
|
1693
|
-
class BotControlValidationError extends Error {
|
|
1694
|
-
constructor(message) {
|
|
1695
|
-
super(message);
|
|
1696
|
-
this.name = "BotControlValidationError";
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
class BotControlNotFoundError extends Error {
|
|
1700
|
-
constructor(message = "Discord server is not available to Helyx") {
|
|
1701
|
-
super(message);
|
|
1702
|
-
this.name = "BotControlNotFoundError";
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
1710
|
function digest(value) {
|
|
1706
1711
|
return createHash("sha256").update(value).digest();
|
|
1707
1712
|
}
|
|
1708
|
-
function isUnknownGuildError(error) {
|
|
1709
|
-
return (typeof error === "object" &&
|
|
1710
|
-
error !== null &&
|
|
1711
|
-
"code" in error &&
|
|
1712
|
-
error.code === 10_004);
|
|
1713
|
-
}
|
|
1714
|
-
function isUnknownMemberError(error) {
|
|
1715
|
-
return (typeof error === "object" &&
|
|
1716
|
-
error !== null &&
|
|
1717
|
-
"code" in error &&
|
|
1718
|
-
error.code === 10_007);
|
|
1719
|
-
}
|
|
1720
1713
|
//# sourceMappingURL=control-server.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Client } from "discord.js";
|
|
2
|
+
import type { ChannelAuditInput, ChannelAuditModuleId, ChannelAuditReport } from "@helyx/sdk";
|
|
3
|
+
/** Fresh guild roles/channels and the bot member only. Never reads messages or enumerates members. */
|
|
4
|
+
export declare function inspectDiscordChannelAudit(client: Client, input: {
|
|
5
|
+
guildId: string;
|
|
6
|
+
moduleId: ChannelAuditModuleId;
|
|
7
|
+
assumptions: ChannelAuditInput;
|
|
8
|
+
}): Promise<ChannelAuditReport>;
|
|
9
|
+
//# sourceMappingURL=discord-channel-audit.d.ts.map
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { PermissionFlagsBits } from "discord.js";
|
|
2
|
+
import { channelAuditInputSchema, channelAuditReportSchema, } from "@helyx/control-protocol/channel-audit";
|
|
3
|
+
import { buildChannelAuditReport, } from "./channel-audit-permissions.js";
|
|
4
|
+
import { dangerousRolePermissions } from "./discord-role-safety.js";
|
|
5
|
+
async function readBefore(read, deadline) {
|
|
6
|
+
const remaining = deadline - Date.now();
|
|
7
|
+
if (remaining <= 0)
|
|
8
|
+
throw new Error("Channel audit read deadline reached");
|
|
9
|
+
let timer;
|
|
10
|
+
try {
|
|
11
|
+
return await Promise.race([
|
|
12
|
+
read(),
|
|
13
|
+
new Promise((_, reject) => {
|
|
14
|
+
timer = setTimeout(() => reject(new Error("Channel audit read deadline reached")), remaining);
|
|
15
|
+
timer.unref();
|
|
16
|
+
}),
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
if (timer)
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Fresh guild roles/channels and the bot member only. Never reads messages or enumerates members. */
|
|
25
|
+
export async function inspectDiscordChannelAudit(client, input) {
|
|
26
|
+
const assumptions = channelAuditInputSchema.parse(input.assumptions);
|
|
27
|
+
const deadline = Date.now() + 8_500;
|
|
28
|
+
const snapshot = {
|
|
29
|
+
guildId: input.guildId,
|
|
30
|
+
roles: [],
|
|
31
|
+
rolesComplete: false,
|
|
32
|
+
channels: [],
|
|
33
|
+
fetchedChannels: 0,
|
|
34
|
+
totalChannelsKnown: false,
|
|
35
|
+
fetchLimitations: [],
|
|
36
|
+
};
|
|
37
|
+
const guild = await readBefore(() => client.guilds.fetch(input.guildId), deadline).catch(() => null);
|
|
38
|
+
if (!guild) {
|
|
39
|
+
snapshot.fetchLimitations.push("The current Discord server could not be fetched; channel and role coverage is incomplete.");
|
|
40
|
+
return channelAuditReportSchema.parse(buildChannelAuditReport(snapshot, input.moduleId, assumptions, new Date().toISOString()));
|
|
41
|
+
}
|
|
42
|
+
if (guild.id !== input.guildId)
|
|
43
|
+
throw new Error("Discord returned a different server for this inspection");
|
|
44
|
+
const [rolesResult, channelsResult, memberResult] = await Promise.allSettled([
|
|
45
|
+
readBefore(() => guild.roles.fetch(), deadline),
|
|
46
|
+
readBefore(() => guild.channels.fetch(), deadline),
|
|
47
|
+
readBefore(() => guild.members.fetchMe({ force: true }), deadline),
|
|
48
|
+
]);
|
|
49
|
+
const me = memberResult.status === "fulfilled" ? memberResult.value : null;
|
|
50
|
+
if (me && me.guild.id !== guild.id)
|
|
51
|
+
throw new Error("Discord bot member belongs to a different server");
|
|
52
|
+
if (!me)
|
|
53
|
+
snapshot.fetchLimitations.push("Helyx's current server member could not be fetched; bot publishing and assignment checks may be unknown.");
|
|
54
|
+
if (rolesResult.status === "fulfilled") {
|
|
55
|
+
snapshot.rolesComplete = true;
|
|
56
|
+
const canManage = me?.permissions.has(PermissionFlagsBits.ManageRoles) ?? false;
|
|
57
|
+
snapshot.roles = [...rolesResult.value.values()].map((role) => {
|
|
58
|
+
if (role.guild.id !== guild.id)
|
|
59
|
+
throw new Error("Discord role belongs to a different server");
|
|
60
|
+
const reason = role.id === guild.id
|
|
61
|
+
? "The everyone role cannot be assigned."
|
|
62
|
+
: role.managed
|
|
63
|
+
? "This role is managed by Discord or an integration and cannot be assigned by Helyx."
|
|
64
|
+
: !me
|
|
65
|
+
? undefined
|
|
66
|
+
: !canManage
|
|
67
|
+
? "Helyx lacks Manage Roles in this server."
|
|
68
|
+
: role.comparePositionTo(me.roles.highest) >= 0
|
|
69
|
+
? "The selected role is at or above Helyx's highest role."
|
|
70
|
+
: undefined;
|
|
71
|
+
const dangerous = dangerousRolePermissions(role.permissions.bitfield);
|
|
72
|
+
return {
|
|
73
|
+
id: role.id,
|
|
74
|
+
permissions: role.permissions.bitfield,
|
|
75
|
+
managed: role.managed,
|
|
76
|
+
assignable: reason ? false : me ? true : null,
|
|
77
|
+
...(reason ? { assignmentReason: reason } : {}),
|
|
78
|
+
...(dangerous.length
|
|
79
|
+
? {
|
|
80
|
+
assignmentPolicyReason: `Helyx's role-safety policy blocks managing roles with ${dangerous.join(", ")}. Discord hierarchy alone does not make this role eligible.`,
|
|
81
|
+
}
|
|
82
|
+
: {}),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else
|
|
87
|
+
snapshot.fetchLimitations.push("The current Discord role list could not be fetched; missing roles are unknown, not confirmed deleted.");
|
|
88
|
+
const observedIds = new Set();
|
|
89
|
+
if (channelsResult.status === "fulfilled") {
|
|
90
|
+
snapshot.fetchedChannels = channelsResult.value.size;
|
|
91
|
+
for (const channel of channelsResult.value.values()) {
|
|
92
|
+
if (!channel)
|
|
93
|
+
continue;
|
|
94
|
+
if (channel.guild.id !== guild.id)
|
|
95
|
+
throw new Error("Discord channel belongs to a different server");
|
|
96
|
+
observedIds.add(channel.id);
|
|
97
|
+
const flags = "flags" in channel ? BigInt(channel.flags.bitfield) : 0n;
|
|
98
|
+
const obfuscated = (flags & 131072n) !== 0n || channel.name === "___hidden___";
|
|
99
|
+
const overwrites = "permissionOverwrites" in channel
|
|
100
|
+
? [...channel.permissionOverwrites.cache.values()].map((overwrite) => ({
|
|
101
|
+
id: overwrite.id,
|
|
102
|
+
type: overwrite.type,
|
|
103
|
+
allow: overwrite.allow.bitfield,
|
|
104
|
+
deny: overwrite.deny.bitfield,
|
|
105
|
+
}))
|
|
106
|
+
: [];
|
|
107
|
+
const botPermissions = !obfuscated && me
|
|
108
|
+
? (channel.permissionsFor(me)?.bitfield ?? null)
|
|
109
|
+
: null;
|
|
110
|
+
snapshot.channels.push({
|
|
111
|
+
id: channel.id,
|
|
112
|
+
name: obfuscated ? "Unknown channel" : channel.name,
|
|
113
|
+
type: channel.type,
|
|
114
|
+
obfuscated,
|
|
115
|
+
overwrites: obfuscated ? [] : overwrites,
|
|
116
|
+
botPermissions,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
snapshot.totalChannelsKnown =
|
|
120
|
+
me?.permissions.has(PermissionFlagsBits.Administrator) ?? false;
|
|
121
|
+
}
|
|
122
|
+
else
|
|
123
|
+
snapshot.fetchLimitations.push("Discord's channel list could not be fetched; cached metadata is not used as fresh permission evidence.");
|
|
124
|
+
// Bulk lists can omit invisible channels. Resolve only explicitly selected
|
|
125
|
+
// missing IDs; a permission failure or deadline remains unknown.
|
|
126
|
+
const selected = new Set([
|
|
127
|
+
assumptions.entryChannelId,
|
|
128
|
+
...assumptions.allowedChannelIds,
|
|
129
|
+
...(assumptions.reviewChannelId ? [assumptions.reviewChannelId] : []),
|
|
130
|
+
]);
|
|
131
|
+
snapshot.invalidSelectedChannels = [];
|
|
132
|
+
for (const id of selected) {
|
|
133
|
+
if (observedIds.has(id))
|
|
134
|
+
continue;
|
|
135
|
+
try {
|
|
136
|
+
const channel = await readBefore(() => guild.channels.fetch(id, { force: true, cache: false }), deadline);
|
|
137
|
+
if (!channel)
|
|
138
|
+
continue;
|
|
139
|
+
if (channel.guild.id !== guild.id) {
|
|
140
|
+
snapshot.invalidSelectedChannels.push({ id, reason: "wrong_guild" });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
observedIds.add(id);
|
|
144
|
+
const obfuscated = (BigInt("flags" in channel ? channel.flags.bitfield : 0) & 131072n) !==
|
|
145
|
+
0n || channel.name === "___hidden___";
|
|
146
|
+
snapshot.channels.push({
|
|
147
|
+
id,
|
|
148
|
+
name: obfuscated ? "Unknown channel" : channel.name,
|
|
149
|
+
type: channel.type,
|
|
150
|
+
obfuscated,
|
|
151
|
+
overwrites: !obfuscated && "permissionOverwrites" in channel
|
|
152
|
+
? [...channel.permissionOverwrites.cache.values()].map((overwrite) => ({
|
|
153
|
+
id: overwrite.id,
|
|
154
|
+
type: overwrite.type,
|
|
155
|
+
allow: overwrite.allow.bitfield,
|
|
156
|
+
deny: overwrite.deny.bitfield,
|
|
157
|
+
}))
|
|
158
|
+
: [],
|
|
159
|
+
botPermissions: !obfuscated && me
|
|
160
|
+
? (channel.permissionsFor(me)?.bitfield ?? null)
|
|
161
|
+
: null,
|
|
162
|
+
});
|
|
163
|
+
snapshot.fetchedChannels += 1;
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
const code = error && typeof error === "object" && "code" in error
|
|
167
|
+
? error.code
|
|
168
|
+
: undefined;
|
|
169
|
+
if (code === 10003 || code === "GuildChannelUnowned")
|
|
170
|
+
snapshot.invalidSelectedChannels.push({
|
|
171
|
+
id,
|
|
172
|
+
reason: code === 10003 ? "not_found" : "wrong_guild",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Cache-only entries establish known inventory, never current permissions or deletion.
|
|
177
|
+
for (const channel of guild.channels.cache.values()) {
|
|
178
|
+
if (observedIds.has(channel.id))
|
|
179
|
+
continue;
|
|
180
|
+
snapshot.channels.push(unknownChannel(channel));
|
|
181
|
+
snapshot.totalChannelsKnown = false;
|
|
182
|
+
}
|
|
183
|
+
return channelAuditReportSchema.parse(buildChannelAuditReport(snapshot, input.moduleId, assumptions, new Date().toISOString()));
|
|
184
|
+
}
|
|
185
|
+
function unknownChannel(channel) {
|
|
186
|
+
return {
|
|
187
|
+
id: channel.id,
|
|
188
|
+
name: "Unknown channel",
|
|
189
|
+
type: channel.type,
|
|
190
|
+
obfuscated: true,
|
|
191
|
+
overwrites: [],
|
|
192
|
+
botPermissions: null,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=discord-channel-audit.js.map
|
|
@@ -163,8 +163,8 @@ export declare class DiscordGuildHealthInspection implements DiscordGuildHealthI
|
|
|
163
163
|
resourceValidationValid: boolean | null;
|
|
164
164
|
activationValid: boolean | null;
|
|
165
165
|
catalogueMatches: boolean;
|
|
166
|
-
requiredCapabilities: readonly ("
|
|
167
|
-
missingCapabilities: ("
|
|
166
|
+
requiredCapabilities: readonly ("message-content" | "guild-members")[];
|
|
167
|
+
missingCapabilities: ("message-content" | "guild-members")[];
|
|
168
168
|
missingPermissions: string[];
|
|
169
169
|
healthStatus: "healthy" | "degraded" | "unhealthy";
|
|
170
170
|
healthMessage: string | null;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The existing Helyx role-grant policy, shared by delivery and inspection. */
|
|
2
|
+
export declare const dangerousRolePermissionNames: Set<string>;
|
|
3
|
+
export declare function dangerousRolePermissions(permissions: bigint): string[];
|
|
4
|
+
//# sourceMappingURL=discord-role-safety.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { PermissionsBitField } from "discord.js";
|
|
2
|
+
/** The existing Helyx role-grant policy, shared by delivery and inspection. */
|
|
3
|
+
export const dangerousRolePermissionNames = new Set([
|
|
4
|
+
"ADMINISTRATOR",
|
|
5
|
+
"BAN_MEMBERS",
|
|
6
|
+
"DEAFEN_MEMBERS",
|
|
7
|
+
"KICK_MEMBERS",
|
|
8
|
+
"MANAGE_CHANNELS",
|
|
9
|
+
"MANAGE_EVENTS",
|
|
10
|
+
"MANAGE_GUILD",
|
|
11
|
+
"MANAGE_GUILD_EXPRESSIONS",
|
|
12
|
+
"MANAGE_MESSAGES",
|
|
13
|
+
"MANAGE_NICKNAMES",
|
|
14
|
+
"MANAGE_ROLES",
|
|
15
|
+
"MANAGE_THREADS",
|
|
16
|
+
"MANAGE_WEBHOOKS",
|
|
17
|
+
"MENTION_EVERYONE",
|
|
18
|
+
"MODERATE_MEMBERS",
|
|
19
|
+
"MOVE_MEMBERS",
|
|
20
|
+
"MUTE_MEMBERS",
|
|
21
|
+
"VIEW_AUDIT_LOG",
|
|
22
|
+
]);
|
|
23
|
+
export function dangerousRolePermissions(permissions) {
|
|
24
|
+
return new PermissionsBitField(permissions)
|
|
25
|
+
.toArray()
|
|
26
|
+
.map((permission) => permission.replaceAll(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase())
|
|
27
|
+
.filter((permission) => dangerousRolePermissionNames.has(permission))
|
|
28
|
+
.sort();
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=discord-role-safety.js.map
|
package/dist/events.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DiscordChannelAudienceInspection, DiscordChannelInspection, DiscordEmojiInspection, DiscordMessageInspection, DiscordGuildCapabilityInspection, DiscordGuildChannelAudienceInspection, DiscordResourceInspectionService, DiscordRoleInspection, DiscordMessageService, InstallationService, MemberEnforcementService, MemberRoleService, ModuleDefinition, ServiceAccess, VerificationMemberService } from "@helyx/sdk";
|
|
2
|
+
import { inspectDiscordChannelAudit } from "./discord-channel-audit.js";
|
|
2
3
|
import { type Client, type MessageReaction, type PartialMessageReaction } from "discord.js";
|
|
3
4
|
import { type InteractionLogger } from "./interactions.js";
|
|
4
5
|
import type { TransientViolationEvidenceStore } from "./transient-violation-evidence.js";
|
|
@@ -80,6 +81,7 @@ export declare class DiscordResourceInspection implements DiscordResourceInspect
|
|
|
80
81
|
#private;
|
|
81
82
|
private readonly client;
|
|
82
83
|
constructor(client: Client);
|
|
84
|
+
inspectChannelAudit(input: Parameters<typeof inspectDiscordChannelAudit>[1]): Promise<import("@helyx/sdk").ChannelAuditReport>;
|
|
83
85
|
inspectGuildCapabilities(input: {
|
|
84
86
|
guildId: string;
|
|
85
87
|
}): Promise<DiscordGuildCapabilityInspection>;
|
package/dist/events.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
|
|
2
|
+
import { inspectDiscordChannelAudit } from "./discord-channel-audit.js";
|
|
3
|
+
import { dangerousRolePermissionNames } from "./discord-role-safety.js";
|
|
2
4
|
import { safeError } from "@helyx/core";
|
|
3
5
|
import { ChannelType, Events, GatewayIntentBits, GuildMemberFlags, PermissionFlagsBits, RESTJSONErrorCodes, UserFlags, } from "discord.js";
|
|
4
6
|
import { toReplyOptions } from "./interactions.js";
|
|
@@ -1272,26 +1274,6 @@ export function reactionCacheKey(emojiIdentifier) {
|
|
|
1272
1274
|
const discordSnowflake = /^\d{17,20}$/u;
|
|
1273
1275
|
const moduleIdentifier = /^helyx\.[a-z0-9][a-z0-9-]*$/u;
|
|
1274
1276
|
const customEmojiIdentifier = /^(?:<a?:([A-Za-z0-9_]{2,32}):(\d{17,20})>|([A-Za-z0-9_]{2,32}):(\d{17,20}))$/u;
|
|
1275
|
-
const dangerousRolePermissionNames = new Set([
|
|
1276
|
-
"ADMINISTRATOR",
|
|
1277
|
-
"BAN_MEMBERS",
|
|
1278
|
-
"DEAFEN_MEMBERS",
|
|
1279
|
-
"KICK_MEMBERS",
|
|
1280
|
-
"MANAGE_CHANNELS",
|
|
1281
|
-
"MANAGE_EVENTS",
|
|
1282
|
-
"MANAGE_GUILD",
|
|
1283
|
-
"MANAGE_GUILD_EXPRESSIONS",
|
|
1284
|
-
"MANAGE_MESSAGES",
|
|
1285
|
-
"MANAGE_NICKNAMES",
|
|
1286
|
-
"MANAGE_ROLES",
|
|
1287
|
-
"MANAGE_THREADS",
|
|
1288
|
-
"MANAGE_WEBHOOKS",
|
|
1289
|
-
"MENTION_EVERYONE",
|
|
1290
|
-
"MODERATE_MEMBERS",
|
|
1291
|
-
"MOVE_MEMBERS",
|
|
1292
|
-
"MUTE_MEMBERS",
|
|
1293
|
-
"VIEW_AUDIT_LOG",
|
|
1294
|
-
]);
|
|
1295
1277
|
const directMessageAttemptTimeoutMs = 5_000;
|
|
1296
1278
|
const guildChannelAudienceInspectionLimit = 500;
|
|
1297
1279
|
export class DiscordMessageDeliveryService {
|
|
@@ -1696,6 +1678,9 @@ export class DiscordResourceInspection {
|
|
|
1696
1678
|
constructor(client) {
|
|
1697
1679
|
this.client = client;
|
|
1698
1680
|
}
|
|
1681
|
+
inspectChannelAudit(input) {
|
|
1682
|
+
return inspectDiscordChannelAudit(this.client, input);
|
|
1683
|
+
}
|
|
1699
1684
|
async inspectGuildCapabilities(input) {
|
|
1700
1685
|
validateSnowflake(input.guildId, "guild");
|
|
1701
1686
|
const guild = await this.client.guilds.fetch(input.guildId);
|
|
@@ -11,8 +11,8 @@ declare const resultSchema: z.ZodObject<{
|
|
|
11
11
|
}>;
|
|
12
12
|
outcome: z.ZodEnum<{
|
|
13
13
|
completed: "completed";
|
|
14
|
-
failed: "failed";
|
|
15
14
|
requires_review: "requires_review";
|
|
15
|
+
failed: "failed";
|
|
16
16
|
queued: "queued";
|
|
17
17
|
not_currently_installed: "not_currently_installed";
|
|
18
18
|
accepted_for_retry: "accepted_for_retry";
|
|
@@ -45,7 +45,7 @@ export declare class HostedServerModerationClient implements DeveloperServerMode
|
|
|
45
45
|
idempotencyKey: string;
|
|
46
46
|
}): Promise<{
|
|
47
47
|
operationId: string;
|
|
48
|
-
outcome: "completed" | "
|
|
48
|
+
outcome: "completed" | "requires_review" | "failed" | "queued" | "not_currently_installed" | "accepted_for_retry" | "already_blacklisted";
|
|
49
49
|
paidSlotsReleased: number;
|
|
50
50
|
}>;
|
|
51
51
|
}
|