@helyx/bot 1.5.2 → 1.5.4

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.
Files changed (115) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/command-root-catalogue.json +452 -0
  3. package/dist/cached-message-deletion-authority.d.ts +27 -0
  4. package/dist/cached-message-deletion-authority.js +288 -0
  5. package/dist/channel-audit-configuration.d.ts +11 -1
  6. package/dist/channel-audit-configuration.js +28 -6
  7. package/dist/channel-audit-control.js +42 -2
  8. package/dist/channel-audit-permissions.js +76 -3
  9. package/dist/cloud-command-policy-sync.d.ts +26 -0
  10. package/dist/cloud-command-policy-sync.js +198 -0
  11. package/dist/cloud-connector.d.ts +6 -1
  12. package/dist/cloud-connector.js +224 -53
  13. package/dist/cloud-control-dispatcher.js +18 -0
  14. package/dist/command-policy-runtime.d.ts +23 -0
  15. package/dist/command-policy-runtime.js +53 -0
  16. package/dist/command-root-catalogue.d.ts +5 -0
  17. package/dist/command-root-catalogue.js +16 -0
  18. package/dist/control-error-mapping.d.ts +8 -0
  19. package/dist/control-error-mapping.js +60 -0
  20. package/dist/control-errors.d.ts +3 -1
  21. package/dist/control-errors.js +4 -1
  22. package/dist/control-health-routes.d.ts +10 -0
  23. package/dist/control-health-routes.js +28 -0
  24. package/dist/control-server-schemas.d.ts +7 -0
  25. package/dist/control-server-schemas.js +9 -0
  26. package/dist/control-server.d.ts +27 -6
  27. package/dist/control-server.js +103 -122
  28. package/dist/dashboard-feature-control.d.ts +2 -1
  29. package/dist/dashboard-feature-control.js +1 -0
  30. package/dist/dashboard-logging.d.ts +20 -0
  31. package/dist/dashboard-logging.js +39 -0
  32. package/dist/dashboard-module-projection.d.ts +1 -1
  33. package/dist/dashboard-module-projection.js +5 -2
  34. package/dist/dashboard-preview-import-routes.js +8 -0
  35. package/dist/database-bootstrap.d.ts +1 -1
  36. package/dist/database-bootstrap.js +4 -3
  37. package/dist/discord-channel-audit.js +1 -0
  38. package/dist/discord-user-identities.d.ts +14 -0
  39. package/dist/discord-user-identities.js +64 -0
  40. package/dist/emergency-deletion-dispatcher.d.ts +68 -0
  41. package/dist/emergency-deletion-dispatcher.js +502 -0
  42. package/dist/emergency-deletion-rest-transport.d.ts +24 -0
  43. package/dist/emergency-deletion-rest-transport.js +253 -0
  44. package/dist/events.d.ts +5 -0
  45. package/dist/events.js +53 -10
  46. package/dist/guarded-discord-dispatcher.d.ts +29 -0
  47. package/dist/guarded-discord-dispatcher.js +204 -0
  48. package/dist/guild-command-discord-client.d.ts +2 -0
  49. package/dist/guild-command-discord-client.js +7 -0
  50. package/dist/guild-command-projection.d.ts +4 -1
  51. package/dist/guild-command-projection.js +25 -0
  52. package/dist/guild-command-read-service.d.ts +10 -1
  53. package/dist/guild-command-read-service.js +37 -0
  54. package/dist/guild-command-worker.d.ts +19 -4
  55. package/dist/guild-command-worker.js +356 -26
  56. package/dist/guild-message-protection-author-roles.d.ts +7 -0
  57. package/dist/guild-message-protection-author-roles.js +93 -0
  58. package/dist/guild-message-protection-deletion.d.ts +21 -0
  59. package/dist/guild-message-protection-deletion.js +138 -0
  60. package/dist/guild-message-protection-delivery.d.ts +41 -0
  61. package/dist/guild-message-protection-delivery.js +223 -0
  62. package/dist/guild-message-update-metadata.d.ts +32 -0
  63. package/dist/guild-message-update-metadata.js +255 -0
  64. package/dist/guilds.d.ts +2 -1
  65. package/dist/guilds.js +11 -1
  66. package/dist/hosted-server-moderation.d.ts +2 -2
  67. package/dist/index.js +68 -4
  68. package/dist/interactions.d.ts +3 -1
  69. package/dist/interactions.js +77 -12
  70. package/dist/invite-rollup-maintenance.d.ts +9 -0
  71. package/dist/invite-rollup-maintenance.js +12 -0
  72. package/dist/managed-resource-authorization.d.ts +15 -0
  73. package/dist/managed-resource-authorization.js +37 -0
  74. package/dist/managed-resource-create-defaults.d.ts +21 -0
  75. package/dist/managed-resource-create-defaults.js +83 -0
  76. package/dist/managed-resource-query.d.ts +6 -0
  77. package/dist/managed-resource-query.js +15 -0
  78. package/dist/member-role-ids.d.ts +17 -0
  79. package/dist/member-role-ids.js +66 -0
  80. package/dist/message-protection-admission.d.ts +60 -0
  81. package/dist/message-protection-admission.js +279 -0
  82. package/dist/message-protection-cold-data.d.ts +42 -0
  83. package/dist/message-protection-cold-data.js +180 -0
  84. package/dist/message-protection-coordinator.d.ts +73 -0
  85. package/dist/message-protection-coordinator.js +622 -0
  86. package/dist/message-protection-settings.d.ts +40 -0
  87. package/dist/message-protection-settings.js +275 -0
  88. package/dist/message-protection-summary.d.ts +8 -0
  89. package/dist/message-protection-summary.js +98 -0
  90. package/dist/module-availability-control.js +20 -12
  91. package/dist/module-logging.d.ts +3 -2
  92. package/dist/module-logging.js +6 -1
  93. package/dist/modules.js +56 -1
  94. package/dist/mysql-runtime-persistence.d.ts +1 -0
  95. package/dist/mysql-runtime-persistence.js +4 -1
  96. package/dist/nexus-command-recovery-control-routes.d.ts +5 -0
  97. package/dist/nexus-command-recovery-control-routes.js +48 -0
  98. package/dist/nexus-command-references.d.ts +6 -0
  99. package/dist/nexus-command-references.js +33 -0
  100. package/dist/scheduled-task-runner.js +23 -4
  101. package/dist/shard-command-startup.d.ts +13 -0
  102. package/dist/shard-command-startup.js +27 -0
  103. package/dist/shard-contracts.d.ts +84 -0
  104. package/dist/shard-contracts.js +33 -0
  105. package/dist/shard-core-readiness.d.ts +5 -0
  106. package/dist/shard-core-readiness.js +19 -0
  107. package/dist/shard-router.d.ts +3 -0
  108. package/dist/shard-router.js +10 -1
  109. package/dist/shard-supervisor.d.ts +1 -1
  110. package/dist/shard-supervisor.js +124 -43
  111. package/dist/shard-worker.js +25 -15
  112. package/documentation/README.md +1 -0
  113. package/documentation/examples/.env.example +0 -45
  114. package/documentation/guides/self-hosting.md +54 -0
  115. package/package.json +18 -17
@@ -0,0 +1,60 @@
1
+ import { GuildCommandActiveLeaseError, GuildCommandRevisionConflictError, GuildCommandSetupConflictError, OptimisticConcurrencyError, } from "@helyx/database";
2
+ import { parseDashboardFieldValidation, } from "@helyx/control-protocol/field-validation";
3
+ import { DashboardActionValidationError, ModuleStateValidationError, } from "@helyx/sdk";
4
+ import { ZodError } from "zod";
5
+ import { BotControlForbiddenError, BotControlNotFoundError, BotControlValidationError, BotControlTransportError, } from "./control-errors.js";
6
+ export function mapBotControlError(error) {
7
+ if (error instanceof ZodError)
8
+ return { status: 400, message: "The control request was invalid" };
9
+ if (error instanceof BotControlValidationError ||
10
+ error instanceof DashboardActionValidationError ||
11
+ error instanceof ModuleStateValidationError) {
12
+ const fieldError = safeBotControlFieldError(error);
13
+ return {
14
+ status: 400,
15
+ message: error.message,
16
+ ...(fieldError ? { fieldError } : {}),
17
+ };
18
+ }
19
+ if (error instanceof BotControlNotFoundError)
20
+ return { status: 404, message: error.message };
21
+ if (error instanceof BotControlForbiddenError)
22
+ return { status: 403, message: error.message };
23
+ if (error instanceof OptimisticConcurrencyError ||
24
+ (error instanceof Error &&
25
+ error.name === "DatabaseFailure" &&
26
+ "kind" in error &&
27
+ error.kind === "conflict"))
28
+ return {
29
+ status: 409,
30
+ message: "Configuration changed in another session. Reload and try again.",
31
+ };
32
+ if (error instanceof GuildCommandRevisionConflictError ||
33
+ (error instanceof Error &&
34
+ error.name === "GuildCommandRevisionConflictError"))
35
+ return {
36
+ status: 409,
37
+ message: "Command state changed in another session. Refresh and try again.",
38
+ };
39
+ if (error instanceof GuildCommandActiveLeaseError ||
40
+ (error instanceof Error && error.name === "GuildCommandActiveLeaseError"))
41
+ return {
42
+ status: 409,
43
+ message: "A command build is already running. Wait for it to finish.",
44
+ };
45
+ if (error instanceof GuildCommandSetupConflictError ||
46
+ (error instanceof Error && error.name === "GuildCommandSetupConflictError"))
47
+ return {
48
+ status: 409,
49
+ message: "Command setup changed. Reload the page and try again.",
50
+ };
51
+ return undefined;
52
+ }
53
+ export function safeBotControlFieldError(error) {
54
+ return error instanceof DashboardActionValidationError ||
55
+ (error instanceof BotControlTransportError &&
56
+ [400, 409].includes(error.status))
57
+ ? parseDashboardFieldValidation(error.fieldError)
58
+ : undefined;
59
+ }
60
+ //# sourceMappingURL=control-error-mapping.js.map
@@ -1,9 +1,11 @@
1
1
  import { type PermissionMutationDenialReason } from "@helyx/core";
2
+ import { type DashboardFieldValidation } from "@helyx/control-protocol/field-validation";
2
3
  export declare function isUnknownGuildError(error: unknown): boolean;
3
4
  export declare function isUnknownMemberError(error: unknown): boolean;
4
5
  export declare class BotControlTransportError extends Error {
5
6
  readonly status: 400 | 403 | 404 | 409 | 502 | 503 | 504;
6
- constructor(status: 400 | 403 | 404 | 409 | 502 | 503 | 504, message: string);
7
+ readonly fieldError: DashboardFieldValidation | undefined;
8
+ constructor(status: 400 | 403 | 404 | 409 | 502 | 503 | 504, message: string, fieldError?: unknown);
7
9
  }
8
10
  export declare class BotControlValidationError extends Error {
9
11
  constructor(message: string);
@@ -1,4 +1,5 @@
1
1
  import { permissionMutationDenialMessage, } from "@helyx/core";
2
+ import { parseDashboardFieldValidation, } from "@helyx/control-protocol/field-validation";
2
3
  export function isUnknownGuildError(error) {
3
4
  return (typeof error === "object" &&
4
5
  error !== null &&
@@ -13,10 +14,12 @@ export function isUnknownMemberError(error) {
13
14
  }
14
15
  export class BotControlTransportError extends Error {
15
16
  status;
16
- constructor(status, message) {
17
+ fieldError;
18
+ constructor(status, message, fieldError) {
17
19
  super(message);
18
20
  this.status = status;
19
21
  this.name = "BotControlTransportError";
22
+ this.fieldError = parseDashboardFieldValidation(fieldError);
20
23
  }
21
24
  }
22
25
  export class BotControlValidationError extends Error {
@@ -0,0 +1,10 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type { ShardHealthSnapshot } from "./shard-contracts.js";
3
+ export declare function registerControlHealthRoutes(server: FastifyInstance, input: {
4
+ readiness?: () => Promise<boolean>;
5
+ shardHealth?: () => Promise<ShardHealthSnapshot>;
6
+ runtimeConfiguration?: {
7
+ hostedModerationEnabled: boolean;
8
+ };
9
+ }): void;
10
+ //# sourceMappingURL=control-health-routes.d.ts.map
@@ -0,0 +1,28 @@
1
+ export function registerControlHealthRoutes(server, input) {
2
+ async function readiness(reply) {
3
+ const deploymentReadiness = input.readiness;
4
+ const shardHealth = input.shardHealth;
5
+ if (!deploymentReadiness && !shardHealth)
6
+ return { status: "ok" };
7
+ try {
8
+ if (deploymentReadiness && (await deploymentReadiness()))
9
+ return { status: "ok" };
10
+ if (deploymentReadiness)
11
+ return reply.code(503).send({ status: "unavailable" });
12
+ if (!shardHealth)
13
+ return reply.code(503).send({ status: "unavailable" });
14
+ if ((await shardHealth()).status === "healthy")
15
+ return { status: "ok" };
16
+ }
17
+ catch {
18
+ // Public readiness deliberately returns no internal detail.
19
+ }
20
+ return reply.code(503).send({ status: "unavailable" });
21
+ }
22
+ server.get("/health", (_request, reply) => readiness(reply));
23
+ server.get("/v1/health", (_request, reply) => readiness(reply));
24
+ server.get("/v1/runtime-configuration", (_request, reply) => reply.header("Cache-Control", "no-store").send({
25
+ hostedModerationEnabled: input.runtimeConfiguration?.hostedModerationEnabled ?? false,
26
+ }));
27
+ }
28
+ //# sourceMappingURL=control-health-routes.js.map
@@ -57,6 +57,13 @@ export declare const managedResourceDetailSchema: z.ZodObject<{
57
57
  updatedAt: z.ZodISODateTime;
58
58
  value: z.ZodRecord<z.ZodString, z.ZodCustom<ManagedResourceValue, ManagedResourceValue>>;
59
59
  availableActionIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
60
+ notices: z.ZodOptional<z.ZodArray<z.ZodObject<{
61
+ kind: z.ZodEnum<{
62
+ info: "info";
63
+ warning: "warning";
64
+ }>;
65
+ message: z.ZodString;
66
+ }, z.core.$strict>>>;
60
67
  }, z.core.$strict>;
61
68
  export declare const managedResourcePageSchema: z.ZodObject<{
62
69
  items: z.ZodArray<z.ZodObject<{
@@ -70,6 +70,15 @@ export const managedResourceDetailSchema = managedResourceSummarySchema
70
70
  .extend({
71
71
  value: managedResourceObject,
72
72
  availableActionIds: z.array(z.string().min(1).max(100)).max(50).optional(),
73
+ notices: z
74
+ .array(z
75
+ .object({
76
+ kind: z.enum(["info", "warning"]),
77
+ message: z.string().trim().min(1).max(500),
78
+ })
79
+ .strict())
80
+ .max(5)
81
+ .optional(),
73
82
  })
74
83
  .strict()
75
84
  .refine(hasBoundedManagedResourceBytes);
@@ -1,6 +1,8 @@
1
+ import { type DiscordUserIdentityControl, type DiscordUserIdentityRequest } from "@helyx/control-protocol/discord-user-identities";
1
2
  import { AuditRepository, GuildAccessPolicyRepository, InstallationRepository, ModuleLoggingPolicyRepository, PermissionPolicyRepository, type DatabasePool, type GuildShardOwnership } from "@helyx/database";
2
3
  import type { MysqlPool } from "@helyx/database-mysql";
3
4
  import type { CreatorOnboardingRequest, CreatorOnboardingResponse } from "@helyx/sdk";
5
+ export { mapBotControlError, safeBotControlFieldError, } from "./control-error-mapping.js";
4
6
  import { type DashboardActionInspection, type DashboardActionResult, type FeatureAccessDecision, 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";
5
7
  import { type ActionReceipt, type SignedAssessmentEnvelope } from "@helyx/verification-contracts";
6
8
  import { type Client } from "discord.js";
@@ -10,10 +12,12 @@ import { type FirstTimeCommandSetupHandler } from "./first-time-command-setup.js
10
12
  import { type ShardHealthSnapshot } from "./shard-contracts.js";
11
13
  import { type DashboardPermissionState } from "./control-permissions.js";
12
14
  export { BotControlTransportError } from "./control-errors.js";
15
+ import { type ManagedResourceCreateDefaultsInput, type ManagedResourceCreateDefaultsControl } from "./managed-resource-create-defaults.js";
13
16
  import type { ChannelAuditControl } from "@helyx/sdk";
14
17
  import { type DashboardModuleDefinition } from "./dashboard-module-projection.js";
15
18
  import { type AvailabilityModuleCatalogue } from "./module-availability-control.js";
16
19
  import { registerGuildCommandControlRoutes } from "./guild-command-control-routes.js";
20
+ import { registerNexusCommandRecoveryControlRoutes } from "./nexus-command-recovery-control-routes.js";
17
21
  import { type DashboardActionInput, type DashboardActionPreviewInput } from "./dashboard-action-control.js";
18
22
  import { type DashboardActionHandler } from "./dashboard-action-routes.js";
19
23
  import { type EckoTelemetrySink } from "./ecko-transport-routes.js";
@@ -52,7 +56,7 @@ export interface BotGuildPresenceObservation {
52
56
  state: BotGuildPresenceState;
53
57
  observedAt: string;
54
58
  }
55
- export interface BotControlGuildResourceDirectory {
59
+ export interface BotControlGuildResourceDirectory extends DiscordUserIdentityControl {
56
60
  listGuildPresence(guildIds: readonly string[]): Promise<BotGuildPresenceObservation[]>;
57
61
  getGuildResources(guildId: string): Promise<BotControlGuildResources>;
58
62
  getGuildPublicSummary(guildId: string): Promise<{
@@ -70,8 +74,17 @@ export interface BotControlGuildResourceDirectory {
70
74
  sendPrivateSupportNotification?(guildId: string, channelId: string, response: MessageResponse, notificationId: string): Promise<void>;
71
75
  }
72
76
  export declare class DiscordGuildResourceDirectory implements BotControlGuildResourceDirectory {
77
+ #private;
73
78
  private readonly client;
74
79
  constructor(client: Client);
80
+ resolveDiscordUserIdentities(input: DiscordUserIdentityRequest): Promise<{
81
+ guildId: string;
82
+ users: {
83
+ userId: string;
84
+ username: string | null;
85
+ displayName: string | null;
86
+ }[];
87
+ }>;
75
88
  listGuildPresence(guildIds: readonly string[]): Promise<BotGuildPresenceObservation[]>;
76
89
  getGuildResources(guildId: string): Promise<BotControlGuildResources>;
77
90
  getGuildPublicSummary(guildId: string): Promise<{
@@ -121,7 +134,7 @@ export interface DashboardGuildAccessState extends GuildAccessPolicy {
121
134
  actorHighestRolePosition: number | null;
122
135
  updatedAt: string | null;
123
136
  }
124
- export interface BotControlHandler extends FirstTimeCommandSetupHandler, DashboardActionHandler, ChannelAuditControl {
137
+ export interface BotControlHandler extends FirstTimeCommandSetupHandler, DashboardActionHandler, ManagedResourceCreateDefaultsControl, ChannelAuditControl, DiscordUserIdentityControl {
125
138
  creatorOnboarding?(input: CreatorOnboardingRequest): Promise<CreatorOnboardingResponse>;
126
139
  cleanupOAuthConsumer(input: {
127
140
  consumerBindingId: string;
@@ -395,6 +408,14 @@ export declare class BotControlService implements BotControlHandler {
395
408
  executeThrough: Date;
396
409
  cursor?: string;
397
410
  }): Promise<PrivacyErasePage>;
411
+ resolveDiscordUserIdentities(input: DiscordUserIdentityRequest): Promise<{
412
+ guildId: string;
413
+ users: {
414
+ userId: string;
415
+ username: string | null;
416
+ displayName: string | null;
417
+ }[];
418
+ }>;
398
419
  getGuildState(guildId: string): Promise<BotControlState>;
399
420
  canAccessDashboard(input: {
400
421
  guildId: string;
@@ -571,6 +592,9 @@ export declare class BotControlService implements BotControlHandler {
571
592
  correlationId: string;
572
593
  actorAuthenticatedAt?: string;
573
594
  }): Promise<ManagedResourceDetail | null>;
595
+ getManagedResourceCreateDefaults(input: ManagedResourceCreateDefaultsInput): Promise<{
596
+ value: Readonly<Record<string, ManagedResourceValue>>;
597
+ }>;
574
598
  listAvailabilityModules(): Promise<AvailabilityModuleCatalogue>;
575
599
  }
576
600
  export declare function createBotControlServer(input: {
@@ -593,6 +617,7 @@ export declare function createBotControlServer(input: {
593
617
  observedAt: string;
594
618
  }>;
595
619
  guildCommands?: Parameters<typeof registerGuildCommandControlRoutes>[1];
620
+ nexusCommandRecovery?: Parameters<typeof registerNexusCommandRecoveryControlRoutes>[1];
596
621
  ecko?: {
597
622
  authenticator: EckoTransportAuthenticator;
598
623
  registry: AutoModerationScannerProviderRegistryService;
@@ -604,8 +629,4 @@ export declare function createBotControlServer(input: {
604
629
  telemetry?: EckoTelemetrySink;
605
630
  };
606
631
  }): Promise<FastifyInstance>;
607
- export declare function mapBotControlError(error: unknown): {
608
- status: 400 | 403 | 404 | 409;
609
- message: string;
610
- } | undefined;
611
632
  //# sourceMappingURL=control-server.d.ts.map
@@ -1,12 +1,18 @@
1
- import { AuditRepository, GuildAccessPolicyRepository, GuildCommandActiveLeaseError, GuildCommandRevisionConflictError, GuildCommandSetupConflictError, InstallationRepository, ModuleLoggingPolicyRepository, OptimisticConcurrencyError, PermissionPolicyRepository, withTransaction, } from "@helyx/database";
1
+ import { discordUserIdentityRequestSchema, } from "@helyx/control-protocol/discord-user-identities";
2
+ import { createDiscordUserIdentityLookup, } from "./discord-user-identities.js";
3
+ import { AuditRepository, GuildAccessPolicyRepository, InstallationRepository, ModuleLoggingPolicyRepository, OptimisticConcurrencyError, PermissionPolicyRepository, withTransaction, } from "@helyx/database";
2
4
  import { safeError } from "@helyx/core";
3
- import { DashboardActionValidationError, DEFAULT_GUILD_ACCESS_POLICY, HELYX_SERVICE_NAMES, ModuleStateValidationError, } from "@helyx/sdk";
5
+ import { MemberRoleSnapshotError, snapshotMemberRoleIds, } from "./member-role-ids.js";
6
+ import { mapBotControlError, safeBotControlFieldError, } from "./control-error-mapping.js";
7
+ export { mapBotControlError, safeBotControlFieldError, } from "./control-error-mapping.js";
8
+ import { DashboardActionValidationError, DEFAULT_GUILD_ACCESS_POLICY, HELYX_SERVICE_NAMES, nexusCommandLoggingControlOwner, ModuleStateValidationError, } from "@helyx/sdk";
4
9
  import { actionReceiptSchema, signedAssessmentEnvelopeSchema, } from "@helyx/verification-contracts";
5
10
  import { canAccessGuildDashboard, canMutatePermissionPolicy, canUseCommand, resolveHelyxAuthority, } from "@helyx/core";
6
11
  import { ChannelType, PermissionFlagsBits } from "discord.js";
7
12
  import Fastify, {} from "fastify";
8
13
  import { createHash, timingSafeEqual } from "node:crypto";
9
- import { z, ZodError } from "zod";
14
+ import { z } from "zod";
15
+ import { nexusModuleLabels } from "./nexus-command-references.js";
10
16
  import { addChannelMessageReaction as addDiscordChannelMessageReaction, deleteChannelMessage as deleteDiscordChannelMessage, editChannelMessage as editDiscordChannelMessage, normalizeDiscordPermission, sendToChannel as sendDiscordMessageToChannel, } from "./interactions.js";
11
17
  import { mutateDashboardModuleStatus, } from "./module-status-control.js";
12
18
  import { canAccessModule, PREMIUM_MODULE_UNAVAILABLE_MESSAGE, } from "./premium-access.js";
@@ -16,12 +22,18 @@ import { assertVersion, declaredManagedResourcePermission, fromDashboardPolicy,
16
22
  import { assertPermissionMutationAllowed, BotControlForbiddenError, BotControlNotFoundError, BotControlValidationError, BotControlTransportError, isUnknownGuildError, isUnknownMemberError, } from "./control-errors.js";
17
23
  export { BotControlTransportError } from "./control-errors.js";
18
24
  import { dashboardCommands } from "./dashboard-commands.js";
25
+ import { dashboardLoggingPolicies } from "./dashboard-logging.js";
26
+ import { authorizeManagedResourceContext } from "./managed-resource-authorization.js";
27
+ import { readManagedResourceCreateDefaults, } from "./managed-resource-create-defaults.js";
28
+ import { validateManagedResourceQuery } from "./managed-resource-query.js";
19
29
  import { registerChannelAuditRoutes } from "./channel-audit-routes.js";
20
30
  import { createChannelAuditControl } from "./channel-audit-control.js";
21
31
  import { executeCreatorOnboarding, registerCreatorOnboardingControlRoute, } from "./creator-onboarding-control.js";
22
32
  import { mapDashboardModule, } from "./dashboard-module-projection.js";
23
33
  import { availabilityCatalogueDefinitions, availabilityCatalogue, dashboardModuleStatus, disabledModuleSummary, moduleAvailabilityService, moduleAvailabilityStates, statusCatalogueDefinitions, } from "./module-availability-control.js";
24
34
  import { registerGuildCommandControlRoutes } from "./guild-command-control-routes.js";
35
+ import { registerControlHealthRoutes } from "./control-health-routes.js";
36
+ import { registerNexusCommandRecoveryControlRoutes } from "./nexus-command-recovery-control-routes.js";
25
37
  import { createPrivacyProviderRegistry, describePrivacyProviders, } from "./privacy-provider-registry.js";
26
38
  import { changedSettingFeatureIds, configurationPreviewFeatureIds, managedResourceColumnKeys, normalizeManagedResourceDetail, normalizeManagedResourcePage, normalizeManagedResourceRelationPage, projectManagedResourceObject, requireDashboardFeatures, resourceFieldFeatureIds, scalarInputFeatureIds, validateManagedResourceInput, } from "./dashboard-feature-control.js";
27
39
  import { registerDashboardPreviewImportRoutes, registerDashboardResourceMutationRoutes, } from "./dashboard-preview-import-routes.js";
@@ -32,8 +44,13 @@ import { validateDiscordUserSettingSelections, validateDiscordSettingSelections,
32
44
  import { configurationValue, guildAccessPolicy, managedResourceDeleteResultSchema, managedResourceDetailSchema, managedResourceFilters, managedResourceObject, managedResourcePageSchema, managedResourceRelationPageSchema, moduleId, mutationContext, permissionState, resourceActionId, resourceCursor, resourceId, snowflake, } from "./control-server-schemas.js";
33
45
  export class DiscordGuildResourceDirectory {
34
46
  client;
47
+ #identities;
35
48
  constructor(client) {
36
49
  this.client = client;
50
+ this.#identities = createDiscordUserIdentityLookup(client);
51
+ }
52
+ resolveDiscordUserIdentities(input) {
53
+ return this.#identities.resolve(input);
37
54
  }
38
55
  listGuildPresence(guildIds) {
39
56
  const observedAt = new Date().toISOString();
@@ -133,7 +150,13 @@ export class DiscordGuildResourceDirectory {
133
150
  });
134
151
  return {
135
152
  userId: member.id,
136
- roleIds: [...member.roles.cache.keys()],
153
+ roleIds: snapshotMemberRoleIds({
154
+ member,
155
+ client: this.client,
156
+ guild,
157
+ guildId,
158
+ actorUserId,
159
+ }),
137
160
  permissions: member.permissions
138
161
  .toArray()
139
162
  .map(normalizeDiscordPermission),
@@ -143,6 +166,8 @@ export class DiscordGuildResourceDirectory {
143
166
  };
144
167
  }
145
168
  catch (error) {
169
+ if (error instanceof MemberRoleSnapshotError)
170
+ throw new BotControlForbiddenError();
146
171
  if (isUnknownGuildError(error))
147
172
  throw new BotControlNotFoundError();
148
173
  if (isUnknownMemberError(error))
@@ -231,7 +256,7 @@ export class BotControlService {
231
256
  this.#modules = new Map(modules.map((module) => [module.manifest.id, module]));
232
257
  this.#dashboardModules = modules
233
258
  .filter((module) => module.manifest.dashboard !== undefined)
234
- .map(mapDashboardModule);
259
+ .map((module) => mapDashboardModule(module, modules));
235
260
  this.#availabilityModules = availabilityCatalogueDefinitions(modules);
236
261
  this.#statusModules = statusCatalogueDefinitions(statusModules ?? modules);
237
262
  this.#configurations = services.get(HELYX_SERVICE_NAMES.configuration);
@@ -377,6 +402,11 @@ export class BotControlService {
377
402
  ...(input.cursor ? { cursor: input.cursor } : {}),
378
403
  });
379
404
  }
405
+ resolveDiscordUserIdentities(input) {
406
+ if (!this.resources.resolveDiscordUserIdentities)
407
+ throw new BotControlTransportError(503, "Discord identity lookup is unavailable");
408
+ return this.resources.resolveDiscordUserIdentities(discordUserIdentityRequestSchema.parse(input));
409
+ }
380
410
  async getGuildState(guildId) {
381
411
  const dashboardEntries = this.#dashboardModules.map((dashboardModule) => ({
382
412
  dashboardModule,
@@ -404,12 +434,8 @@ export class BotControlService {
404
434
  const moduleStatuses = {};
405
435
  const configurations = {};
406
436
  const permissions = {};
407
- const logging = {};
437
+ const logging = dashboardLoggingPolicies(this.#dashboardModules, storedLoggingPolicies);
408
438
  const featureAccess = {};
409
- const loggingPolicies = new Map(storedLoggingPolicies.map((policy) => [
410
- `${policy.sourceModuleId}:${policy.eventId}`,
411
- policy,
412
- ]));
413
439
  for (const { dashboardModule, module } of dashboardEntries) {
414
440
  const installation = installed.get(dashboardModule.id);
415
441
  moduleStatuses[dashboardModule.id] = dashboardModuleStatus(disabledModules.set, dashboardModule.id, installation);
@@ -420,14 +446,6 @@ export class BotControlService {
420
446
  value: configuration.value,
421
447
  };
422
448
  permissions[dashboardModule.id] = this.#buildPermissionState(module, storedPermissions);
423
- logging[dashboardModule.id] = dashboardModule.logEvents.map((event) => {
424
- const stored = loggingPolicies.get(`${dashboardModule.id}:${event.id}`);
425
- return {
426
- eventId: event.id,
427
- enabled: stored?.enabled ?? event.defaultEnabled,
428
- version: stored ? safeVersion(stored.version) : 0,
429
- };
430
- });
431
449
  if (module.manifest.manifestVersion === "0.5" &&
432
450
  module.manifest.features.length > 0) {
433
451
  const access = this.#services.has(HELYX_SERVICE_NAMES.featureAccess)
@@ -450,7 +468,7 @@ export class BotControlService {
450
468
  messageContent: runtime.messageContent,
451
469
  },
452
470
  ...resources,
453
- modules: structuredClone(this.#dashboardModules),
471
+ modules: await nexusModuleLabels(this.#dashboardModules, this.#services, guildId),
454
472
  featureAccess,
455
473
  moduleStatuses,
456
474
  globallyDisabledModuleIds: disabledModules.ids,
@@ -832,7 +850,10 @@ export class BotControlService {
832
850
  if (!(await this.#persistence.repositories.installations.isModuleEnabled(input.guildId, "helyx.logging"))) {
833
851
  throw new BotControlValidationError("Enable Logging before changing activity events");
834
852
  }
835
- const source = this.#requireModule(input.moduleId);
853
+ const ownerModuleId = nexusCommandLoggingControlOwner(input.moduleId, input.eventId);
854
+ if (ownerModuleId !== input.moduleId)
855
+ await this.#requireModuleAccessible(input.guildId, ownerModuleId);
856
+ const source = this.#requireModule(ownerModuleId);
836
857
  const event = source.logging?.events.find(({ id }) => id === input.eventId);
837
858
  if (!event) {
838
859
  throw new BotControlNotFoundError("Logging event is not declared by this module");
@@ -840,7 +861,7 @@ export class BotControlService {
840
861
  const saved = await this.#persistence.withTransaction(async (repositories) => {
841
862
  const policy = await repositories.loggingPolicies.save({
842
863
  guildId: input.guildId,
843
- sourceModuleId: input.moduleId,
864
+ sourceModuleId: ownerModuleId,
844
865
  eventId: input.eventId,
845
866
  enabled: input.enabled,
846
867
  expectedVersion: input.expectedVersion === 0 ? null : BigInt(input.expectedVersion),
@@ -852,7 +873,7 @@ export class BotControlService {
852
873
  action: "module.logging.updated",
853
874
  source: "dashboard",
854
875
  correlationId: input.correlationId,
855
- moduleId: input.moduleId,
876
+ moduleId: ownerModuleId,
856
877
  targetType: "module_log_event",
857
878
  targetId: input.eventId,
858
879
  afterState: {
@@ -1029,6 +1050,16 @@ export class BotControlService {
1029
1050
  ? null
1030
1051
  : normalizeManagedResourceDetail(managedResourceDetailSchema.parse(result), managedResourceColumnKeys(module, input.resourceType));
1031
1052
  }
1053
+ async getManagedResourceCreateDefaults(input) {
1054
+ await this.#requireModuleAccessible(input.guildId, input.moduleId);
1055
+ const { module, resource } = this.#prepareManagedResource(input);
1056
+ this.#requireManagedResourceOperation(module, input.resourceType, "create");
1057
+ return readManagedResourceCreateDefaults({
1058
+ module,
1059
+ resource,
1060
+ context: await this.#managedResourceContext(input, module, "create"),
1061
+ });
1062
+ }
1032
1063
  #prepareManagedResource(input) {
1033
1064
  const module = this.#requireDashboardModule(input.moduleId);
1034
1065
  const resource = this.#requireManagedResource(module, input.resourceType);
@@ -1049,17 +1080,7 @@ export class BotControlService {
1049
1080
  throw new BotControlNotFoundError("Managed resource operation is not declared by this module");
1050
1081
  }
1051
1082
  #validateManagedResourceQuery(module, resourceType, input) {
1052
- const query = module.manifest.dashboard?.resources?.find(({ id }) => id === resourceType)?.query;
1053
- if (input.search !== undefined && !query?.search)
1054
- throw new BotControlValidationError("Managed resource search is not declared by this module");
1055
- for (const [key, value] of Object.entries(input.filters ?? {})) {
1056
- const filter = query?.filters?.find((candidate) => candidate.key === key);
1057
- if (!filter)
1058
- throw new BotControlValidationError("Managed resource filter is not declared by this module");
1059
- if (filter.type === "select" &&
1060
- !filter.options.some((option) => option.value === value))
1061
- throw new BotControlValidationError("Managed resource filter value is not declared by this module");
1062
- }
1083
+ validateManagedResourceQuery(module, resourceType, input);
1063
1084
  }
1064
1085
  async #requireEnabledManagedResource(guildId, moduleIdValue) {
1065
1086
  if (!(await this.#persistence.repositories.installations.isModuleEnabled(guildId, moduleIdValue)))
@@ -1067,26 +1088,31 @@ export class BotControlService {
1067
1088
  }
1068
1089
  async #managedResourceContext(input, module, operation, permissionId = managedResourcePermissionId(module, input.resourceType, operation, undefined, declaredManagedResourcePermission(module, input.resourceType, operation))) {
1069
1090
  const resource = this.#requireManagedResource(module, input.resourceType);
1070
- const [actor] = await Promise.all([
1071
- this.#authorizeAction(input.guildId, module, input.actorUserId, permissionId),
1072
- resource.availableWhileDisabled
1073
- ? Promise.resolve()
1074
- : this.#requireEnabledManagedResource(input.guildId, module.manifest.id),
1075
- ]);
1076
- return {
1077
- guildId: input.guildId,
1078
- actor,
1079
- ...("actorAuthenticatedAt" in input &&
1080
- typeof input.actorAuthenticatedAt === "string"
1081
- ? { actorAuthenticatedAt: input.actorAuthenticatedAt }
1082
- : {}),
1083
- correlationId: input.correlationId,
1084
- services: this.#services,
1085
- sendToChannel: (channelId, response) => this.resources.sendToChannel(channelId, response),
1086
- deleteChannelMessage: (channelId, messageId) => this.resources.deleteChannelMessage(channelId, messageId),
1087
- editChannelMessage: (channelId, messageId, response) => this.resources.editChannelMessage(channelId, messageId, response),
1088
- addChannelMessageReaction: (channelId, messageId, emojiIdentifier) => this.resources.addChannelMessageReaction(channelId, messageId, emojiIdentifier),
1089
- };
1091
+ if (!resource.availableWhileDisabled)
1092
+ await this.#requireEnabledManagedResource(input.guildId, module.manifest.id);
1093
+ return authorizeManagedResourceContext({
1094
+ ...input,
1095
+ module,
1096
+ resource,
1097
+ operation,
1098
+ permissionId,
1099
+ authorize: (id) => this.#authorizeAction(input.guildId, module, input.actorUserId, id),
1100
+ createContext: (actor, authorizationScope) => ({
1101
+ guildId: input.guildId,
1102
+ actor,
1103
+ authorizationScope,
1104
+ ...("actorAuthenticatedAt" in input &&
1105
+ typeof input.actorAuthenticatedAt === "string"
1106
+ ? { actorAuthenticatedAt: input.actorAuthenticatedAt }
1107
+ : {}),
1108
+ correlationId: input.correlationId,
1109
+ services: this.#services,
1110
+ sendToChannel: (channelId, response) => this.resources.sendToChannel(channelId, response),
1111
+ deleteChannelMessage: (channelId, messageId) => this.resources.deleteChannelMessage(channelId, messageId),
1112
+ editChannelMessage: (channelId, messageId, response) => this.resources.editChannelMessage(channelId, messageId, response),
1113
+ addChannelMessageReaction: (channelId, messageId, emojiIdentifier) => this.resources.addChannelMessageReaction(channelId, messageId, emojiIdentifier),
1114
+ }),
1115
+ });
1090
1116
  }
1091
1117
  async #authorizeAction(guildId, module, actorUserId, permissionId) {
1092
1118
  const [actor, policy, guildAccess] = await Promise.all([
@@ -1246,33 +1272,8 @@ export async function createBotControlServer(input) {
1246
1272
  if (!timingSafeEqual(expectedSecret, digest(supplied)))
1247
1273
  return reply.code(401).send({ message: "Control authentication failed" });
1248
1274
  });
1249
- async function readiness(reply) {
1250
- const deploymentReadiness = input.readiness;
1251
- const shardHealth = input.shardHealth;
1252
- if (!deploymentReadiness && !shardHealth)
1253
- return { status: "ok" };
1254
- try {
1255
- if (deploymentReadiness && (await deploymentReadiness()))
1256
- return { status: "ok" };
1257
- if (deploymentReadiness)
1258
- return reply.code(503).send({ status: "unavailable" });
1259
- if (!shardHealth)
1260
- return reply.code(503).send({ status: "unavailable" });
1261
- const health = await shardHealth();
1262
- if (health.status === "healthy")
1263
- return { status: "ok" };
1264
- }
1265
- catch {
1266
- // The public readiness route deliberately returns no internal detail.
1267
- }
1268
- return reply.code(503).send({ status: "unavailable" });
1269
- }
1270
- server.get("/health", (_request, reply) => readiness(reply));
1275
+ registerControlHealthRoutes(server, input);
1271
1276
  registerCreatorOnboardingControlRoute(server, input.service);
1272
- server.get("/v1/health", (_request, reply) => readiness(reply));
1273
- server.get("/v1/runtime-configuration", (_request, reply) => reply.header("Cache-Control", "no-store").send({
1274
- hostedModerationEnabled: input.runtimeConfiguration?.hostedModerationEnabled ?? false,
1275
- }));
1276
1277
  server.post("/v1/hosted-server-moderation/leave/deliver", async (request) => {
1277
1278
  if (!input.deliverHostedServerLeave)
1278
1279
  throw new BotControlTransportError(503, "Hosted server moderation delivery is unavailable");
@@ -1289,6 +1290,7 @@ export async function createBotControlServer(input) {
1289
1290
  return input.refreshHostedServerOwner(guildId);
1290
1291
  });
1291
1292
  registerGuildCommandControlRoutes(server, input.guildCommands, input.service);
1293
+ registerNexusCommandRecoveryControlRoutes(server, input.nexusCommandRecovery);
1292
1294
  if (input.ecko)
1293
1295
  registerEckoTransportRoutes(server, input.ecko);
1294
1296
  server.post("/v1/verification/results", async (request) => input.service.acceptVerificationResult(signedAssessmentEnvelopeSchema.parse(request.body)));
@@ -1423,6 +1425,18 @@ export async function createBotControlServer(input) {
1423
1425
  ...(cursor ? { cursor } : {}),
1424
1426
  });
1425
1427
  });
1428
+ server.post("/v1/guilds/:guildId/user-identities", async (request) => {
1429
+ const { guildId } = z
1430
+ .object({ guildId: snowflake })
1431
+ .strict()
1432
+ .parse(request.params);
1433
+ const body = discordUserIdentityRequestSchema
1434
+ .pick({ userIds: true })
1435
+ .parse(request.body);
1436
+ if (!input.service.resolveDiscordUserIdentities)
1437
+ throw new BotControlTransportError(503, "Discord identity lookup is unavailable");
1438
+ return input.service.resolveDiscordUserIdentities({ guildId, ...body });
1439
+ });
1426
1440
  server.get("/v1/guilds/:guildId/state", async (request) => {
1427
1441
  const { guildId } = z.object({ guildId: snowflake }).parse(request.params);
1428
1442
  return input.service.getGuildState(guildId);
@@ -1653,9 +1667,17 @@ export async function createBotControlServer(input) {
1653
1667
  server.setErrorHandler(async (error, request, reply) => {
1654
1668
  const mapped = mapBotControlError(error);
1655
1669
  if (mapped)
1656
- return reply.code(mapped.status).send({ message: mapped.message });
1670
+ return reply.code(mapped.status).send({
1671
+ message: mapped.message,
1672
+ ...(mapped.fieldError ? { fieldError: mapped.fieldError } : {}),
1673
+ });
1657
1674
  if (error instanceof BotControlTransportError)
1658
- return reply.code(error.status).send({ message: error.message });
1675
+ return reply.code(error.status).send({
1676
+ message: error.message,
1677
+ ...(safeBotControlFieldError(error)
1678
+ ? { fieldError: safeBotControlFieldError(error) }
1679
+ : {}),
1680
+ });
1659
1681
  request.log.error({
1660
1682
  error: safeError(error),
1661
1683
  errorCode: typeof error.code === "string"
@@ -1666,47 +1688,6 @@ export async function createBotControlServer(input) {
1666
1688
  });
1667
1689
  return server;
1668
1690
  }
1669
- export function mapBotControlError(error) {
1670
- if (error instanceof ZodError)
1671
- return { status: 400, message: "The control request was invalid" };
1672
- if (error instanceof BotControlValidationError ||
1673
- error instanceof DashboardActionValidationError ||
1674
- error instanceof ModuleStateValidationError)
1675
- return { status: 400, message: error.message };
1676
- if (error instanceof BotControlNotFoundError)
1677
- return { status: 404, message: error.message };
1678
- if (error instanceof BotControlForbiddenError)
1679
- return { status: 403, message: error.message };
1680
- if (error instanceof OptimisticConcurrencyError ||
1681
- (error instanceof Error &&
1682
- error.name === "DatabaseFailure" &&
1683
- "kind" in error &&
1684
- error.kind === "conflict"))
1685
- return {
1686
- status: 409,
1687
- message: "Configuration changed in another session. Reload and try again.",
1688
- };
1689
- if (error instanceof GuildCommandRevisionConflictError ||
1690
- (error instanceof Error &&
1691
- error.name === "GuildCommandRevisionConflictError"))
1692
- return {
1693
- status: 409,
1694
- message: "Command state changed in another session. Refresh and try again.",
1695
- };
1696
- if (error instanceof GuildCommandActiveLeaseError ||
1697
- (error instanceof Error && error.name === "GuildCommandActiveLeaseError"))
1698
- return {
1699
- status: 409,
1700
- message: "A command build is already running. Wait for it to finish.",
1701
- };
1702
- if (error instanceof GuildCommandSetupConflictError ||
1703
- (error instanceof Error && error.name === "GuildCommandSetupConflictError"))
1704
- return {
1705
- status: 409,
1706
- message: "Command setup changed. Reload the page and try again.",
1707
- };
1708
- return undefined;
1709
- }
1710
1691
  function digest(value) {
1711
1692
  return createHash("sha256").update(value).digest();
1712
1693
  }