@helyx/bot 0.4.43 → 0.4.46

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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.46
4
+
5
+ ### Patch Changes
6
+
7
+ - Add explicit role-mention allowlists and member-leave identity metadata to the
8
+ module runtime contract. Upgrade Welcome with multiline user, role and channel
9
+ placeholders, an inline dashboard test send, and independent member-left
10
+ logging.
11
+
12
+ ## 0.4.45
13
+
14
+ ### Patch Changes
15
+
16
+ - Expose bounded structural user mentions to guild message event contributions without exposing message content, and include AFK in the reviewed self-host catalogue.
17
+
18
+ ## 0.4.44
19
+
20
+ ### Patch Changes
21
+
22
+ - c369ae4: Publish the complete loaded feature catalogue to the Status synchroniser while keeping global availability controls limited to customer-selectable modules.
23
+
3
24
  ## 0.4.43
4
25
 
5
26
  ### Patch Changes
@@ -0,0 +1,11 @@
1
+ import { type PermissionMutationDenialReason } from "@helyx/core";
2
+ export declare class BotControlForbiddenError extends Error {
3
+ constructor(message?: string);
4
+ }
5
+ export declare function assertPermissionMutationAllowed(decision: {
6
+ allowed: true;
7
+ } | {
8
+ allowed: false;
9
+ reason: PermissionMutationDenialReason;
10
+ }): void;
11
+ //# sourceMappingURL=control-errors.d.ts.map
@@ -0,0 +1,13 @@
1
+ import { permissionMutationDenialMessage, } from "@helyx/core";
2
+ export class BotControlForbiddenError extends Error {
3
+ constructor(message = "You no longer have permission to perform this action") {
4
+ super(message);
5
+ this.name = "BotControlForbiddenError";
6
+ }
7
+ }
8
+ export function assertPermissionMutationAllowed(decision) {
9
+ if (decision.allowed)
10
+ return;
11
+ throw new BotControlForbiddenError(permissionMutationDenialMessage(decision.reason));
12
+ }
13
+ //# sourceMappingURL=control-errors.js.map
@@ -350,7 +350,7 @@ export declare class BotControlService implements BotControlHandler {
350
350
  constructor(database: DatabasePool, modules: readonly ModuleDefinition[], resources: BotControlGuildResourceDirectory, services: ServiceAccess, privacyExecutor?: PrivacyProviderExecutor, supportNotifications?: {
351
351
  guildId: string;
352
352
  channelId: string;
353
- } | undefined, presenceOwnership?: GuildShardOwnership | undefined, presenceShardCount?: number | undefined);
353
+ } | undefined, presenceOwnership?: GuildShardOwnership | undefined, presenceShardCount?: number | undefined, statusModules?: readonly ModuleDefinition[]);
354
354
  cleanupOAuthConsumer(input: {
355
355
  consumerBindingId: string;
356
356
  moduleId: string;
@@ -1,7 +1,7 @@
1
1
  import { AuditRepository, GuildAccessPolicyRepository, GuildCommandActiveLeaseError, GuildCommandRevisionConflictError, GuildCommandSetupConflictError, InstallationRepository, ModuleLoggingPolicyRepository, OptimisticConcurrencyError, PermissionPolicyRepository, withTransaction, } from "@helyx/database";
2
2
  import { DashboardActionValidationError, DEFAULT_GUILD_ACCESS_POLICY, HELYX_SERVICE_NAMES, ModuleStateValidationError, } from "@helyx/sdk";
3
3
  import { actionReceiptSchema, signedAssessmentEnvelopeSchema, } from "@helyx/verification-contracts";
4
- import { canAccessGuildDashboard, canMutatePermissionPolicy, canUseCommand, permissionMutationDenialMessage, resolveHelyxAuthority, } from "@helyx/core";
4
+ import { canAccessGuildDashboard, canMutatePermissionPolicy, canUseCommand, resolveHelyxAuthority, } from "@helyx/core";
5
5
  import { ChannelType, PermissionFlagsBits } from "discord.js";
6
6
  import Fastify, {} from "fastify";
7
7
  import { createHash, timingSafeEqual } from "node:crypto";
@@ -12,8 +12,9 @@ import { completeFirstTimeCommandSetup, } from "./first-time-command-setup.js";
12
12
  import { SHARD_HEALTH_STALE_MS, } from "./shard-contracts.js";
13
13
  import { dashboardCategoryLabel } from "./dashboard-category.js";
14
14
  import { assertVersion, dashboardActionPermissionId, declaredManagedResourcePermission, fromDashboardPolicy, managedResourcePermissionId, permissionPoliciesEqual, permissionPolicyKey, safeOptionalVersion, safeVersion, toDashboardPolicy, } from "./control-permissions.js";
15
+ import { assertPermissionMutationAllowed, BotControlForbiddenError, } from "./control-errors.js";
15
16
  import { dashboardCommands, } from "./dashboard-commands.js";
16
- import { availabilityCatalogueDefinitions, availabilityCatalogue, dashboardModuleStatus, disabledModuleSummary, moduleAvailabilityService, moduleAvailabilityStates, } from "./module-availability-control.js";
17
+ import { availabilityCatalogueDefinitions, availabilityCatalogue, dashboardModuleStatus, disabledModuleSummary, moduleAvailabilityService, moduleAvailabilityStates, statusCatalogueDefinitions, } from "./module-availability-control.js";
17
18
  import { registerGuildCommandControlRoutes } from "./guild-command-control-routes.js";
18
19
  import { createPrivacyProviderRegistry, describePrivacyProviders, } from "./privacy-provider-registry.js";
19
20
  const snowflake = z.string().regex(/^\d{17,20}$/u);
@@ -265,12 +266,13 @@ export class BotControlService {
265
266
  #modules;
266
267
  #dashboardModules;
267
268
  #availabilityModules;
269
+ #statusModules;
268
270
  #configurations;
269
271
  #installations;
270
272
  #permissions;
271
273
  #services;
272
274
  #privacyProviders;
273
- constructor(database, modules, resources, services, privacyExecutor, supportNotifications, presenceOwnership, presenceShardCount) {
275
+ constructor(database, modules, resources, services, privacyExecutor, supportNotifications, presenceOwnership, presenceShardCount, statusModules) {
274
276
  this.database = database;
275
277
  this.resources = resources;
276
278
  this.supportNotifications = supportNotifications;
@@ -281,6 +283,7 @@ export class BotControlService {
281
283
  .filter((module) => module.manifest.dashboard !== undefined)
282
284
  .map(mapDashboardModule);
283
285
  this.#availabilityModules = availabilityCatalogueDefinitions(modules);
286
+ this.#statusModules = statusCatalogueDefinitions(statusModules ?? modules);
284
287
  this.#configurations = services.get(HELYX_SERVICE_NAMES.configuration);
285
288
  this.#installations = services.get(HELYX_SERVICE_NAMES.installations);
286
289
  this.#permissions = services.get(HELYX_SERVICE_NAMES.permissions);
@@ -1037,7 +1040,7 @@ export class BotControlService {
1037
1040
  return module;
1038
1041
  }
1039
1042
  listAvailabilityModules() {
1040
- return availabilityCatalogue(this.#availabilityModules);
1043
+ return availabilityCatalogue(this.#availabilityModules, this.#statusModules);
1041
1044
  }
1042
1045
  async #requireModuleAvailable(moduleIdValue) {
1043
1046
  const availability = moduleAvailabilityService(this.#services);
@@ -1743,12 +1746,6 @@ class BotControlNotFoundError extends Error {
1743
1746
  this.name = "BotControlNotFoundError";
1744
1747
  }
1745
1748
  }
1746
- class BotControlForbiddenError extends Error {
1747
- constructor(message = "You no longer have permission to perform this action") {
1748
- super(message);
1749
- this.name = "BotControlForbiddenError";
1750
- }
1751
- }
1752
1749
  function digest(value) {
1753
1750
  return createHash("sha256").update(value).digest();
1754
1751
  }
@@ -1905,9 +1902,4 @@ function mapDashboardModule(module) {
1905
1902
  : undefined,
1906
1903
  };
1907
1904
  }
1908
- function assertPermissionMutationAllowed(decision) {
1909
- if (decision.allowed)
1910
- return;
1911
- throw new BotControlForbiddenError(permissionMutationDenialMessage(decision.reason));
1912
- }
1913
1905
  //# sourceMappingURL=control-server.js.map
package/dist/events.js CHANGED
@@ -343,6 +343,7 @@ export function installModuleEvents(client, modules, logger, services) {
343
343
  const authorDisplayName = message.member?.displayName ??
344
344
  message.author.globalName ??
345
345
  message.author.username;
346
+ const mentionedUserIds = [...message.mentions.users.keys()].slice(0, 25);
346
347
  let classifiedMessage;
347
348
  for (const { moduleId, classificationRequested, contribution, } of guildMessageContributions) {
348
349
  try {
@@ -362,6 +363,7 @@ export function installModuleEvents(client, modules, logger, services) {
362
363
  authorIsBot: false,
363
364
  authorIsWebhook: false,
364
365
  authorIsSystem: false,
366
+ mentionedUserIds,
365
367
  get classification() {
366
368
  if (!classificationRequested || !messageClassificationAvailable)
367
369
  return unavailableGuildMessageClassification;
@@ -523,7 +525,9 @@ export function installModuleEvents(client, modules, logger, services) {
523
525
  }
524
526
  await contribution.execute({
525
527
  serverId: member.guild.id,
528
+ serverName: member.guild.name,
526
529
  memberId: member.id,
530
+ memberDisplayName: member.displayName,
527
531
  memberIsBot: member.user.bot,
528
532
  accountCreatedAt: member.user.createdAt,
529
533
  joinedAt: member.joinedAt,
package/dist/index.js CHANGED
@@ -403,7 +403,7 @@ async function startUnshardedBot(processEnvironment) {
403
403
  guildId: environment.DISCORD_INTERNAL_GUILD_ID,
404
404
  channelId: environment.HELYX_SUPPORT_STAFF_CHANNEL_ID,
405
405
  }
406
- : undefined);
406
+ : undefined, undefined, undefined, featureModules);
407
407
  cloudConnector.setControlHandler(controlService);
408
408
  let controlServer;
409
409
  let shuttingDown = false;
@@ -1003,7 +1003,7 @@ export function normalizeDiscordPermission(permission) {
1003
1003
  return permission.replaceAll(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
1004
1004
  }
1005
1005
  export function toReplyOptions(response) {
1006
- const allowedMentions = toAllowedUserMentions(response.allowedUserMentionIds);
1006
+ const allowedMentions = toAllowedMentions(response.allowedUserMentionIds, response.allowedRoleMentionIds);
1007
1007
  const selectRows = toSelectRows(response);
1008
1008
  if (response.componentsV2) {
1009
1009
  if (response.content !== undefined) {
@@ -1154,15 +1154,25 @@ export function toUpdateOptions(response) {
1154
1154
  ephemeral: false,
1155
1155
  });
1156
1156
  }
1157
- function toAllowedUserMentions(userIds) {
1158
- if (userIds === undefined)
1157
+ function toAllowedMentions(userIds, roleIds) {
1158
+ if (userIds === undefined && roleIds === undefined)
1159
1159
  return undefined;
1160
- if (userIds.length > 100 ||
1161
- new Set(userIds).size !== userIds.length ||
1162
- userIds.some((userId) => !/^\d{17,20}$/u.test(userId))) {
1163
- throw new Error("Allowed user mentions must be unique Discord snowflakes");
1160
+ validateAllowedMentionIds(userIds, "user");
1161
+ validateAllowedMentionIds(roleIds, "role");
1162
+ return {
1163
+ parse: [],
1164
+ ...(userIds === undefined ? {} : { users: [...userIds] }),
1165
+ ...(roleIds === undefined ? {} : { roles: [...roleIds] }),
1166
+ };
1167
+ }
1168
+ function validateAllowedMentionIds(ids, label) {
1169
+ if (ids === undefined)
1170
+ return;
1171
+ if (ids.length > 100 ||
1172
+ new Set(ids).size !== ids.length ||
1173
+ ids.some((id) => !/^\d{17,20}$/u.test(id))) {
1174
+ throw new Error(`Allowed ${label} mentions must be unique Discord snowflakes`);
1164
1175
  }
1165
- return { parse: [], users: [...userIds] };
1166
1176
  }
1167
1177
  function toButtonEmoji(emojiIdentifier) {
1168
1178
  const custom = /^([A-Za-z0-9_]{2,32}):(\d{17,20})$/u.exec(emojiIdentifier);
@@ -8,10 +8,13 @@ export interface AvailabilityModuleDefinition {
8
8
  export interface AvailabilityModuleCatalogue {
9
9
  catalogueDigest: string;
10
10
  modules: AvailabilityModuleDefinition[];
11
+ statusCatalogueDigest?: string;
12
+ statusModules?: AvailabilityModuleDefinition[];
11
13
  }
12
14
  export declare function availabilityCatalogueDefinitions(modules: readonly ModuleDefinition[]): AvailabilityModuleDefinition[];
13
- export declare function availabilityCatalogueSnapshot(definitions: readonly AvailabilityModuleDefinition[]): AvailabilityModuleCatalogue;
14
- export declare function availabilityCatalogue(definitions: readonly AvailabilityModuleDefinition[]): Promise<AvailabilityModuleCatalogue>;
15
+ export declare function statusCatalogueDefinitions(modules: readonly ModuleDefinition[]): AvailabilityModuleDefinition[];
16
+ export declare function availabilityCatalogueSnapshot(definitions: readonly AvailabilityModuleDefinition[], statusDefinitions?: readonly AvailabilityModuleDefinition[]): AvailabilityModuleCatalogue;
17
+ export declare function availabilityCatalogue(definitions: readonly AvailabilityModuleDefinition[], statusDefinitions?: readonly AvailabilityModuleDefinition[]): Promise<AvailabilityModuleCatalogue>;
15
18
  export declare function moduleAvailabilityService(services: ServiceAccess): ModuleAvailabilityService | undefined;
16
19
  export declare function moduleAvailabilityStates(services: ServiceAccess, moduleIds: readonly string[]): Promise<ModuleAvailabilityState[]>;
17
20
  export declare function dashboardModuleStatus(globallyDisabled: ReadonlySet<string>, moduleId: string, installation: {
@@ -12,17 +12,31 @@ export function availabilityCatalogueDefinitions(modules) {
12
12
  }))
13
13
  .sort((left, right) => left.name.localeCompare(right.name, "en-GB"));
14
14
  }
15
- export function availabilityCatalogueSnapshot(definitions) {
15
+ export function statusCatalogueDefinitions(modules) {
16
+ return modules
17
+ .map(({ manifest }) => ({
18
+ id: manifest.id,
19
+ name: manifest.name,
20
+ version: manifest.version,
21
+ releaseStatus: manifest.releaseStatus,
22
+ }))
23
+ .sort((left, right) => left.name.localeCompare(right.name, "en-GB"));
24
+ }
25
+ function catalogueDigest(definitions) {
26
+ return createHash("sha256").update(JSON.stringify(definitions)).digest("hex");
27
+ }
28
+ export function availabilityCatalogueSnapshot(definitions, statusDefinitions = definitions) {
16
29
  const modules = definitions.map((module) => ({ ...module }));
30
+ const statusModules = statusDefinitions.map((module) => ({ ...module }));
17
31
  return {
18
- catalogueDigest: createHash("sha256")
19
- .update(JSON.stringify(modules))
20
- .digest("hex"),
32
+ catalogueDigest: catalogueDigest(modules),
21
33
  modules,
34
+ statusCatalogueDigest: catalogueDigest(statusModules),
35
+ statusModules,
22
36
  };
23
37
  }
24
- export function availabilityCatalogue(definitions) {
25
- return Promise.resolve(availabilityCatalogueSnapshot(definitions));
38
+ export function availabilityCatalogue(definitions, statusDefinitions = definitions) {
39
+ return Promise.resolve(availabilityCatalogueSnapshot(definitions, statusDefinitions));
26
40
  }
27
41
  export function moduleAvailabilityService(services) {
28
42
  if (!services.has(HELYX_SERVICE_NAMES.moduleAvailability))
@@ -25,7 +25,7 @@ import { createSupportModule } from "./support-module.js";
25
25
  import { createOAuthModule } from "./oauth-module.js";
26
26
  import { createPrivacyModule } from "./privacy-module.js";
27
27
  import { discordRestOptions } from "./discord-rest.js";
28
- import { availabilityCatalogue, availabilityCatalogueDefinitions, } from "./module-availability-control.js";
28
+ import { availabilityCatalogue, availabilityCatalogueDefinitions, statusCatalogueDefinitions, } from "./module-availability-control.js";
29
29
  import { createScheduledTaskRunner, schedulerOperationalHealth, } from "./scheduled-task-runner.js";
30
30
  import { numericWriteBehindRegistrations } from "./numeric-write-behind.js";
31
31
  import { PostgresTriviaDataProvider } from "./trivia-data-provider.js";
@@ -257,8 +257,9 @@ export async function startShardSupervisor(environment, processEnvironment, work
257
257
  ];
258
258
  commandModules = modules;
259
259
  const availabilityModules = availabilityCatalogueDefinitions(modules);
260
+ const statusModules = statusCatalogueDefinitions(featureModules);
260
261
  const availabilityService = {
261
- listAvailabilityModules: () => availabilityCatalogue(availabilityModules),
262
+ listAvailabilityModules: () => availabilityCatalogue(availabilityModules, statusModules),
262
263
  };
263
264
  const initialiseLeader = async () => {
264
265
  await runLeaderStartup(database, environment, modulePackages, modules, logger);
@@ -259,7 +259,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
259
259
  guildId: environment.DISCORD_INTERNAL_GUILD_ID,
260
260
  channelId: environment.HELYX_SUPPORT_STAFF_CHANNEL_ID,
261
261
  }
262
- : undefined, shardOwnership, identity.shardCount);
262
+ : undefined, shardOwnership, identity.shardCount, featureModules);
263
263
  process.on("message", (raw) => {
264
264
  const parsed = supervisorMessageSchema.safeParse(raw);
265
265
  if (!parsed.success)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/bot",
3
- "version": "0.4.43",
3
+ "version": "0.4.46",
4
4
  "description": "Production Discord runtime for the modular Helyx platform.",
5
5
  "keywords": [
6
6
  "helyx",