@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.
@@ -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
@@ -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({
@@ -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
  }
@@ -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>;
@@ -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;
@@ -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 ("guild-members" | "message-content")[];
167
- missingCapabilities: ("guild-members" | "message-content")[];
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" | "failed" | "requires_review" | "queued" | "not_currently_installed" | "accepted_for_retry" | "already_blacklisted";
48
+ outcome: "completed" | "requires_review" | "failed" | "queued" | "not_currently_installed" | "accepted_for_retry" | "already_blacklisted";
49
49
  paidSlotsReleased: number;
50
50
  }>;
51
51
  }