@helyx/bot 0.4.2 → 0.4.5

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 (38) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +13 -0
  3. package/dist/cloud-connector.d.ts +7 -0
  4. package/dist/cloud-connector.js +118 -1
  5. package/dist/cloud-control-dispatcher.js +84 -0
  6. package/dist/control-server.d.ts +187 -5
  7. package/dist/control-server.js +673 -6
  8. package/dist/developer-operations-module.js +8 -0
  9. package/dist/discord-owned-message-service.d.ts +39 -0
  10. package/dist/discord-owned-message-service.js +138 -0
  11. package/dist/events.d.ts +70 -1
  12. package/dist/events.js +557 -5
  13. package/dist/index.js +61 -13
  14. package/dist/interactions.d.ts +3 -2
  15. package/dist/interactions.js +66 -31
  16. package/dist/module-logging.d.ts +1 -0
  17. package/dist/module-logging.js +19 -1
  18. package/dist/modules.d.ts +9 -0
  19. package/dist/modules.js +45 -0
  20. package/dist/resource-authorization.d.ts +7 -0
  21. package/dist/resource-authorization.js +12 -0
  22. package/dist/scheduled-task-runner.d.ts +130 -0
  23. package/dist/scheduled-task-runner.js +417 -0
  24. package/dist/shard-cloud-proxy.d.ts +4 -0
  25. package/dist/shard-cloud-proxy.js +34 -0
  26. package/dist/shard-contracts.d.ts +574 -23
  27. package/dist/shard-contracts.js +346 -0
  28. package/dist/shard-coordinator.d.ts +5 -3
  29. package/dist/shard-coordinator.js +85 -2
  30. package/dist/shard-router.d.ts +10 -0
  31. package/dist/shard-router.js +58 -0
  32. package/dist/shard-supervisor.d.ts +2 -1
  33. package/dist/shard-supervisor.js +187 -5
  34. package/dist/shard-worker.d.ts +9 -3
  35. package/dist/shard-worker.js +97 -16
  36. package/dist/support-module.d.ts +9 -0
  37. package/dist/support-module.js +172 -0
  38. package/package.json +9 -9
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.5
4
+
5
+ ### Patch Changes
6
+
7
+ - Rebuild the published bot artefact after removing a Testing-module dependency from the core developer interaction test bundle.
8
+
9
+ ## 0.4.4
10
+
11
+ ### Patch Changes
12
+
13
+ - Add the reusable managed-resource, durable scheduling, secure checkpointed
14
+ selection and hosted/self-hosted runtime support required by the Reaction
15
+ Roles, Polls and Giveaways launch stages.
16
+ - Updated dependencies
17
+ - @helyx/control-protocol@0.1.4
18
+ - @helyx/module-manifest@0.4.1
19
+ - @helyx/database@0.4.2
20
+ - @helyx/sdk@0.3.3
21
+ - @helyx/platform@0.2.5
22
+ - @helyx/core@0.1.8
23
+ - @helyx/framework@0.1.8
24
+
25
+ ## 0.4.3
26
+
27
+ ### Patch Changes
28
+
29
+ - 18b228d: Add the unified Support transport, hosted configuration, shard-safe staff
30
+ notifications and paired self-host bug-report claim flow.
31
+ - Updated dependencies [18b228d]
32
+ - Updated dependencies [18b228d]
33
+ - @helyx/control-protocol@0.1.3
34
+ - @helyx/config@0.4.2
35
+ - @helyx/database@0.4.1
36
+ - @helyx/platform@0.2.4
37
+
3
38
  ## 0.4.2
4
39
 
5
40
  ### Patch Changes
package/README.md CHANGED
@@ -18,6 +18,19 @@ When `HELYX_BOT_CONTROL_SECRET` is configured, the bot also starts a private aut
18
18
 
19
19
  Optional Helyx Cloud Connect is a separate outbound management path for self-hosted deployments. It is disabled unless `HELYX_CLOUD_CONNECT_ENABLED=true` and requires a separate local `HELYX_DATA_ENCRYPTION_KEY`. Preserve that key across every restart and deployment. If it is lost or changed, revoke the old installation in the dashboard and use a new one-use code with `/helyx-cloud connect`; the connector stops retrying until that recovery is completed. The owner-only `/helyx-cloud` command supports connection-code enrolment, status, credential rotation, disconnection and bounded support consent. The bot never sends its Discord token, database URL or connector private key to Helyx Cloud, and local Discord operation continues when Cloud is unavailable.
20
20
 
21
+ Giveaways also requires the existing `HELYX_DATA_ENCRYPTION_KEY` so core can
22
+ resume secure weighted-selection checkpoints without exposing the selection
23
+ key to a module. Keep it stable across deployments; do not rotate it while a
24
+ giveaway is drawing.
25
+
26
+ The always-available guild-only `/bug` command can be used by any guild member.
27
+ Hosted deployments submit through the dedicated `HELYX_SUPPORT_SERVICE_SECRET`;
28
+ paired self-hosted deployments use their authenticated Cloud Connect session
29
+ and may receive a one-time link for claiming, tracking and replying to the
30
+ report. The hosted secret is never part of a self-host package. Configure
31
+ `HELYX_SUPPORT_STAFF_CHANNEL_ID` with `DISCORD_INTERNAL_GUILD_ID` to deliver
32
+ bounded new-case metadata to a private staff channel.
33
+
21
34
  Privacy providers ship in the same hosted and self-hosted module packages.
22
35
  `HELYX_PRIVACY_ENABLED` defaults to `false`; enabling it requires the private
23
36
  bot-control endpoint and a separate least-privilege PostgreSQL executor
@@ -1,4 +1,5 @@
1
1
  import type { BotEnvironment } from "@helyx/config";
2
+ import { type SupportBugCreatePayload, type SupportBugCreateResponsePayload } from "@helyx/control-protocol";
2
3
  import { type CloudConnectorConsentRecord, type DatabasePool } from "@helyx/database";
3
4
  import type { Client } from "discord.js";
4
5
  import type { Logger } from "pino";
@@ -38,6 +39,9 @@ export interface CloudConnector {
38
39
  enrol(rawToken: string, source: "startup" | "discord" | "file"): Promise<CloudConnectorStatus>;
39
40
  disconnect(): Promise<void>;
40
41
  rotate(): Promise<CloudConnectorStatus>;
42
+ submitBug(input: SupportBugCreatePayload): Promise<Extract<SupportBugCreateResponsePayload, {
43
+ ok: true;
44
+ }>["result"]>;
41
45
  stop(): Promise<void>;
42
46
  }
43
47
  export declare class ClientCloudDiscordDirectory implements CloudDiscordDirectory {
@@ -68,6 +72,9 @@ export declare class CloudConnectorController implements CloudConnector {
68
72
  enrol(rawToken: string, source: "startup" | "discord" | "file"): Promise<CloudConnectorStatus>;
69
73
  disconnect(): Promise<void>;
70
74
  rotate(): Promise<CloudConnectorStatus>;
75
+ submitBug(rawInput: SupportBugCreatePayload): Promise<Extract<SupportBugCreateResponsePayload, {
76
+ ok: true;
77
+ }>["result"]>;
71
78
  stop(): Promise<void>;
72
79
  }
73
80
  export declare class CloudConnectorUserError extends Error {
@@ -1,4 +1,4 @@
1
- import { CLOUD_DEFAULT_REQUEST_TIMEOUT_MS, CLOUD_PROTOCOL_VERSION, ConnectionSequenceGuard, assertCloudHandshakeSize, assertCloudMessageSize, cloudControlResponsePayloadSchema, connectedCloudMessageSchema, connectorAcceptedSchema, connectorChallengeSchema, connectorCredentialActionSchema, connectorHelloSchema, connectorRejectedSchema, generateConnectorKeyPair, signConnectorCredentialAction, signConnectorChallenge, } from "@helyx/control-protocol";
1
+ import { CLOUD_DEFAULT_REQUEST_TIMEOUT_MS, CLOUD_PROTOCOL_VERSION, ConnectionSequenceGuard, assertCloudHandshakeSize, assertCloudMessageSize, cloudControlResponsePayloadSchema, connectedCloudMessageSchema, connectorAcceptedSchema, connectorChallengeSchema, connectorCredentialActionSchema, connectorHelloSchema, connectorRejectedSchema, generateConnectorKeyPair, signConnectorCredentialAction, signConnectorChallenge, supportBugCreatePayloadSchema, } from "@helyx/control-protocol";
2
2
  import { CloudConnectorRepository, } from "@helyx/database";
3
3
  import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID, } from "node:crypto";
4
4
  import { readFile, rm } from "node:fs/promises";
@@ -82,6 +82,8 @@ export class CloudConnectorController {
82
82
  #stopping = false;
83
83
  #connectionBlocked = false;
84
84
  #loop;
85
+ #activeSupport;
86
+ #pendingSupport = new Map();
85
87
  constructor(input) {
86
88
  this.input = input;
87
89
  this.#repository = new CloudConnectorRepository(input.database);
@@ -313,10 +315,41 @@ export class CloudConnectorController {
313
315
  this.#state = "connecting";
314
316
  return this.status();
315
317
  }
318
+ async submitBug(rawInput) {
319
+ const input = supportBugCreatePayloadSchema.parse(rawInput);
320
+ if (this.input.environment.HELYX_SUPPORT_SERVICE_SECRET)
321
+ return this.#submitHostedBug(input);
322
+ const session = this.#activeSupport;
323
+ if (!this.input.environment.HELYX_CLOUD_CONNECT_ENABLED ||
324
+ this.#state !== "online" ||
325
+ !session?.capabilities.includes("support.bug.create.v1"))
326
+ throw new CloudConnectorUserError("This deployment is not connected to Helyx Support");
327
+ return new Promise((resolve, reject) => {
328
+ const timer = setTimeout(() => {
329
+ this.#pendingSupport.delete(input.correlationId);
330
+ reject(new Error("support_request_timed_out"));
331
+ }, CLOUD_DEFAULT_REQUEST_TIMEOUT_MS);
332
+ timer.unref();
333
+ this.#pendingSupport.set(input.correlationId, {
334
+ resolve,
335
+ reject,
336
+ timer,
337
+ });
338
+ try {
339
+ session.send(input.correlationId, input);
340
+ }
341
+ catch (error) {
342
+ clearTimeout(timer);
343
+ this.#pendingSupport.delete(input.correlationId);
344
+ reject(error instanceof Error ? error : new Error("support_send_failed"));
345
+ }
346
+ });
347
+ }
316
348
  async stop() {
317
349
  this.#stopping = true;
318
350
  this.#state = "stopped";
319
351
  this.#socket?.close(1001, "Helyx bot shutting down");
352
+ this.#rejectPendingSupport("Cloud Connect stopped");
320
353
  await this.#loop?.catch(() => undefined);
321
354
  }
322
355
  #ensureLoop() {
@@ -392,6 +425,8 @@ export class CloudConnectorController {
392
425
  clearInterval(heartbeat);
393
426
  clearTimeout(plannedReconnect);
394
427
  this.#socket = undefined;
428
+ this.#activeSupport = undefined;
429
+ this.#rejectPendingSupport("Cloud Connect disconnected");
395
430
  if (this.#stopping)
396
431
  resolveFinished();
397
432
  else
@@ -412,6 +447,19 @@ export class CloudConnectorController {
412
447
  if (accepted.success) {
413
448
  incomingGuard = new ConnectionSequenceGuard(identity.installationId, accepted.data.connectionEpoch);
414
449
  this.#state = "online";
450
+ this.#activeSupport = {
451
+ capabilities: accepted.data.capabilities,
452
+ send: (correlationId, payload) => {
453
+ socket.send(JSON.stringify(connectedMessage({
454
+ type: "support.request",
455
+ installationId: identity.installationId,
456
+ connectionEpoch: accepted.data.connectionEpoch,
457
+ sequence: ++outgoingSequence,
458
+ correlationId,
459
+ payload,
460
+ })));
461
+ },
462
+ };
415
463
  this.#lastErrorCode = undefined;
416
464
  await this.#repository.markConnected();
417
465
  this.#identity = {
@@ -455,6 +503,18 @@ export class CloudConnectorController {
455
503
  }
456
504
  const message = connectedCloudMessageSchema.parse(payload);
457
505
  incomingGuard?.accept(message);
506
+ if (message.type === "support.response") {
507
+ const pending = this.#pendingSupport.get(message.correlationId);
508
+ if (!pending)
509
+ return;
510
+ clearTimeout(pending.timer);
511
+ this.#pendingSupport.delete(message.correlationId);
512
+ if (message.payload.ok)
513
+ pending.resolve(message.payload.result);
514
+ else
515
+ pending.reject(new Error(message.payload.error.code));
516
+ return;
517
+ }
458
518
  if (message.type !== "control.request")
459
519
  return;
460
520
  const response = await this.#dispatch(message.payload, message.correlationId);
@@ -535,6 +595,54 @@ export class CloudConnectorController {
535
595
  });
536
596
  }
537
597
  }
598
+ async #submitHostedBug(input) {
599
+ let response;
600
+ try {
601
+ response = await fetch(`${this.input.environment.HELYX_CLOUD_API_URL.replace(/\/$/u, "")}/v1/internal/discord-bugs`, {
602
+ method: "POST",
603
+ headers: {
604
+ Accept: "application/json",
605
+ Authorization: `Bearer ${this.input.environment.HELYX_SUPPORT_SERVICE_SECRET}`,
606
+ "Content-Type": "application/json",
607
+ },
608
+ body: JSON.stringify({
609
+ source: "discord_command",
610
+ transport: "hosted",
611
+ reporterDiscordUserId: input.reporterDiscordUserId,
612
+ guildId: input.guildId,
613
+ moduleOrArea: input.moduleOrArea,
614
+ summary: input.summary,
615
+ whatHappened: input.whatHappened,
616
+ ...(input.extraDetail ? { extraDetail: input.extraDetail } : {}),
617
+ interactionId: input.interactionId,
618
+ correlationId: input.correlationId,
619
+ }),
620
+ signal: AbortSignal.timeout(CLOUD_DEFAULT_REQUEST_TIMEOUT_MS),
621
+ });
622
+ }
623
+ catch {
624
+ throw new Error("support_api_unavailable");
625
+ }
626
+ const payload = z
627
+ .object({
628
+ reference: z.string().trim().min(1).max(40),
629
+ created: z.boolean(),
630
+ })
631
+ .strict()
632
+ .safeParse(await response.json().catch(() => null));
633
+ if (!response.ok || !payload.success)
634
+ throw new Error(response.status === 429
635
+ ? "support_rate_limited"
636
+ : "support_api_rejected");
637
+ return payload.data;
638
+ }
639
+ #rejectPendingSupport(message) {
640
+ for (const pending of this.#pendingSupport.values()) {
641
+ clearTimeout(pending.timer);
642
+ pending.reject(new Error(message));
643
+ }
644
+ this.#pendingSupport.clear();
645
+ }
538
646
  async #readBootstrap() {
539
647
  const environmentToken = this.input.environment.HELYX_CLOUD_ENROLMENT_TOKEN;
540
648
  const filePath = resolve(process.cwd(), this.input.environment.HELYX_CLOUD_ENROLMENT_FILE);
@@ -615,7 +723,16 @@ function connectorCapabilities() {
615
723
  "guild.module.permissions.write.v1",
616
724
  "guild.module.logging.write.v1",
617
725
  "guild.module.action.execute.v1",
726
+ "guild.module.resource.list.v1",
727
+ "guild.module.resource.read.v1",
728
+ "guild.module.resource.relation.list.v1",
729
+ "guild.module.resource.preview.v1",
730
+ "guild.module.resource.create.v1",
731
+ "guild.module.resource.update.v1",
732
+ "guild.module.resource.delete.v1",
733
+ "guild.module.resource.action.execute.v1",
618
734
  "support.guild.snapshot.read.v1",
735
+ "support.bug.create.v1",
619
736
  ];
620
737
  }
621
738
  function normalizeEnrolmentToken(value) {
@@ -70,6 +70,90 @@ export class CloudControlDispatcher {
70
70
  });
71
71
  break;
72
72
  }
73
+ case "guild.module.resource.list.v1": {
74
+ const input = parseOperationInput(request.operation, request.input);
75
+ result = await this.control.listManagedResources({
76
+ guildId: input.guildId,
77
+ moduleId: input.moduleId,
78
+ resourceType: input.resourceType,
79
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
80
+ ...(input.search === undefined ? {} : { search: input.search }),
81
+ ...(input.filters === undefined ? {} : { filters: input.filters }),
82
+ limit: input.limit,
83
+ actorUserId: requiredActor(actorUserId),
84
+ correlationId,
85
+ });
86
+ break;
87
+ }
88
+ case "guild.module.resource.read.v1": {
89
+ const input = parseOperationInput(request.operation, request.input);
90
+ result = await this.control.readManagedResource({
91
+ ...input,
92
+ actorUserId: requiredActor(actorUserId),
93
+ correlationId,
94
+ });
95
+ break;
96
+ }
97
+ case "guild.module.resource.relation.list.v1": {
98
+ const input = parseOperationInput(request.operation, request.input);
99
+ result = await this.control.listManagedResourceRelation({
100
+ guildId: input.guildId,
101
+ moduleId: input.moduleId,
102
+ resourceType: input.resourceType,
103
+ resourceId: input.resourceId,
104
+ relationId: input.relationId,
105
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
106
+ limit: input.limit,
107
+ actorUserId: requiredActor(actorUserId),
108
+ correlationId,
109
+ });
110
+ break;
111
+ }
112
+ case "guild.module.resource.preview.v1": {
113
+ const input = parseOperationInput(request.operation, request.input);
114
+ result = await this.control.previewManagedResource({
115
+ ...input,
116
+ actorUserId: requiredActor(actorUserId),
117
+ correlationId,
118
+ });
119
+ break;
120
+ }
121
+ case "guild.module.resource.create.v1": {
122
+ const input = parseOperationInput(request.operation, request.input);
123
+ result = await this.control.createManagedResource({
124
+ ...input,
125
+ actorUserId: requiredActor(actorUserId),
126
+ correlationId,
127
+ });
128
+ break;
129
+ }
130
+ case "guild.module.resource.update.v1": {
131
+ const input = parseOperationInput(request.operation, request.input);
132
+ result = await this.control.updateManagedResource({
133
+ ...input,
134
+ actorUserId: requiredActor(actorUserId),
135
+ correlationId,
136
+ });
137
+ break;
138
+ }
139
+ case "guild.module.resource.delete.v1": {
140
+ const input = parseOperationInput(request.operation, request.input);
141
+ result = await this.control.deleteManagedResource({
142
+ ...input,
143
+ actorUserId: requiredActor(actorUserId),
144
+ correlationId,
145
+ });
146
+ break;
147
+ }
148
+ case "guild.module.resource.action.execute.v1": {
149
+ const input = parseOperationInput(request.operation, request.input);
150
+ result = await this.control.executeManagedResourceAction({
151
+ ...input,
152
+ actorUserId: requiredActor(actorUserId),
153
+ correlationId,
154
+ });
155
+ break;
156
+ }
73
157
  case "support.guild.snapshot.read.v1": {
74
158
  const input = parseOperationInput(request.operation, request.input);
75
159
  if (request.actor.kind !== "helyx_staff")
@@ -1,5 +1,5 @@
1
1
  import { type DatabasePool } from "@helyx/database";
2
- import { type DashboardActionResult, type MessageResponse, type ModuleDefinition, type PrivacyCapturePage, type PrivacyErasePage, type PrivacyProviderExecutor, type ServiceAccess } from "@helyx/sdk";
2
+ import { type DashboardActionResult, type ManagedResourceCreationIntent, type ManagedResourceDetail, type ManagedResourcePage, type ManagedResourceRelationPage, type ManagedResourceValue, type MessageResponse, type ModuleDefinition, type PermissionActor, type PrivacyCapturePage, type PrivacyErasePage, type PrivacyProviderExecutor, type ServiceAccess } from "@helyx/sdk";
3
3
  import { type Client } from "discord.js";
4
4
  import { type FastifyInstance } from "fastify";
5
5
  import { type ShardHealthSnapshot } from "./shard-contracts.js";
@@ -27,6 +27,7 @@ interface DashboardActionDefinition {
27
27
  buttonLabel: string;
28
28
  style: "primary" | "secondary" | "danger";
29
29
  }
30
+ type DashboardResourceDefinition = NonNullable<NonNullable<ModuleDefinition["manifest"]["dashboard"]>["resources"]>[number];
30
31
  interface DashboardModuleDefinition {
31
32
  id: string;
32
33
  name: string;
@@ -41,6 +42,7 @@ interface DashboardModuleDefinition {
41
42
  commands: DashboardCommandDefinition[];
42
43
  logEvents: DashboardLogEventDefinition[];
43
44
  actions: DashboardActionDefinition[];
45
+ resources: readonly DashboardResourceDefinition[];
44
46
  }
45
47
  interface DashboardLoggingPolicy {
46
48
  eventId: string;
@@ -75,6 +77,10 @@ export interface BotControlGuildResourceDirectory {
75
77
  messageId: string;
76
78
  }>;
77
79
  deleteChannelMessage(channelId: string, messageId: string): Promise<void>;
80
+ editChannelMessage(channelId: string, messageId: string, response: MessageResponse): Promise<void>;
81
+ addChannelMessageReaction(channelId: string, messageId: string, emojiIdentifier: string): Promise<void>;
82
+ resolveActor(guildId: string, actorUserId: string): Promise<PermissionActor>;
83
+ sendPrivateSupportNotification?(guildId: string, channelId: string, response: MessageResponse, notificationId: string): Promise<void>;
78
84
  }
79
85
  export declare class DiscordGuildResourceDirectory implements BotControlGuildResourceDirectory {
80
86
  private readonly client;
@@ -84,6 +90,18 @@ export declare class DiscordGuildResourceDirectory implements BotControlGuildRes
84
90
  messageId: string;
85
91
  }>;
86
92
  deleteChannelMessage(channelId: string, messageId: string): Promise<void>;
93
+ editChannelMessage(channelId: string, messageId: string, response: MessageResponse): Promise<void>;
94
+ addChannelMessageReaction(channelId: string, messageId: string, emojiIdentifier: string): Promise<void>;
95
+ resolveActor(guildId: string, actorUserId: string): Promise<PermissionActor>;
96
+ sendPrivateSupportNotification(guildId: string, channelId: string, response: MessageResponse, notificationId: string): Promise<void>;
97
+ }
98
+ export interface SupportNotificationInput {
99
+ notificationId: string;
100
+ reference: string;
101
+ caseType: "bug" | "general_support" | "security";
102
+ categoryOrModule: string;
103
+ reportedSeverity: "low" | "medium" | "high" | "critical" | null;
104
+ source: "dashboard_bug" | "dashboard_support" | "dashboard_security" | "discord_command";
87
105
  }
88
106
  export interface BotControlState {
89
107
  channels: BotControlGuildResources["channels"];
@@ -98,6 +116,7 @@ export interface BotControlState {
98
116
  logging: Record<string, DashboardLoggingPolicy[]>;
99
117
  }
100
118
  export interface BotControlHandler {
119
+ sendSupportNotification(input: SupportNotificationInput): Promise<void>;
101
120
  getGuildState(guildId: string): Promise<BotControlState>;
102
121
  setModuleStatus(input: {
103
122
  guildId: string;
@@ -140,6 +159,85 @@ export interface BotControlHandler {
140
159
  actorUserId: string;
141
160
  correlationId: string;
142
161
  }): Promise<DashboardActionResult>;
162
+ listManagedResources(input: {
163
+ guildId: string;
164
+ moduleId: string;
165
+ resourceType: string;
166
+ cursor?: string;
167
+ limit: number;
168
+ search?: string;
169
+ filters?: Readonly<Record<string, string>>;
170
+ actorUserId: string;
171
+ correlationId: string;
172
+ }): Promise<ManagedResourcePage>;
173
+ readManagedResource(input: {
174
+ guildId: string;
175
+ moduleId: string;
176
+ resourceType: string;
177
+ resourceId: string;
178
+ actorUserId: string;
179
+ correlationId: string;
180
+ }): Promise<ManagedResourceDetail>;
181
+ listManagedResourceRelation(input: {
182
+ guildId: string;
183
+ moduleId: string;
184
+ resourceType: string;
185
+ resourceId: string;
186
+ relationId: string;
187
+ cursor?: string;
188
+ limit: number;
189
+ actorUserId: string;
190
+ correlationId: string;
191
+ }): Promise<ManagedResourceRelationPage>;
192
+ previewManagedResource(input: {
193
+ guildId: string;
194
+ moduleId: string;
195
+ resourceType: string;
196
+ value: Readonly<Record<string, ManagedResourceValue>>;
197
+ actorUserId: string;
198
+ correlationId: string;
199
+ }): Promise<MessageResponse>;
200
+ createManagedResource(input: {
201
+ guildId: string;
202
+ moduleId: string;
203
+ resourceType: string;
204
+ intent: ManagedResourceCreationIntent;
205
+ value: Readonly<Record<string, ManagedResourceValue>>;
206
+ actorUserId: string;
207
+ correlationId: string;
208
+ }): Promise<ManagedResourceDetail>;
209
+ updateManagedResource(input: {
210
+ guildId: string;
211
+ moduleId: string;
212
+ resourceType: string;
213
+ resourceId: string;
214
+ expectedRevision: number;
215
+ value: Readonly<Record<string, ManagedResourceValue>>;
216
+ actorUserId: string;
217
+ correlationId: string;
218
+ }): Promise<ManagedResourceDetail>;
219
+ deleteManagedResource(input: {
220
+ guildId: string;
221
+ moduleId: string;
222
+ resourceType: string;
223
+ resourceId: string;
224
+ expectedRevision: number;
225
+ actorUserId: string;
226
+ correlationId: string;
227
+ }): Promise<{
228
+ deleted: boolean;
229
+ }>;
230
+ executeManagedResourceAction(input: {
231
+ guildId: string;
232
+ moduleId: string;
233
+ resourceType: string;
234
+ resourceId: string;
235
+ actionId: string;
236
+ expectedRevision: number;
237
+ value: Readonly<Record<string, ManagedResourceValue>>;
238
+ actorUserId: string;
239
+ correlationId: string;
240
+ }): Promise<ManagedResourceDetail | null>;
143
241
  listPrivacyProviders(): Promise<readonly {
144
242
  id: string;
145
243
  version: number;
@@ -163,14 +261,19 @@ export interface BotControlHandler {
163
261
  }): Promise<PrivacyErasePage>;
164
262
  }
165
263
  export declare class BotControlTransportError extends Error {
166
- readonly status: 400 | 404 | 409 | 502 | 503 | 504;
167
- constructor(status: 400 | 404 | 409 | 502 | 503 | 504, message: string);
264
+ readonly status: 400 | 403 | 404 | 409 | 502 | 503 | 504;
265
+ constructor(status: 400 | 403 | 404 | 409 | 502 | 503 | 504, message: string);
168
266
  }
169
267
  export declare class BotControlService implements BotControlHandler {
170
268
  #private;
171
269
  private readonly database;
172
270
  private readonly resources;
173
- constructor(database: DatabasePool, modules: readonly ModuleDefinition[], resources: BotControlGuildResourceDirectory, services: ServiceAccess, privacyExecutor?: PrivacyProviderExecutor);
271
+ private readonly supportNotifications?;
272
+ constructor(database: DatabasePool, modules: readonly ModuleDefinition[], resources: BotControlGuildResourceDirectory, services: ServiceAccess, privacyExecutor?: PrivacyProviderExecutor, supportNotifications?: {
273
+ guildId: string;
274
+ channelId: string;
275
+ } | undefined);
276
+ sendSupportNotification(input: SupportNotificationInput): Promise<void>;
174
277
  listPrivacyProviders(): Promise<readonly {
175
278
  id: string;
176
279
  version: number;
@@ -234,6 +337,85 @@ export declare class BotControlService implements BotControlHandler {
234
337
  actorUserId: string;
235
338
  correlationId: string;
236
339
  }): Promise<DashboardActionResult>;
340
+ listManagedResources(input: {
341
+ guildId: string;
342
+ moduleId: string;
343
+ resourceType: string;
344
+ cursor?: string;
345
+ limit: number;
346
+ search?: string;
347
+ filters?: Readonly<Record<string, string>>;
348
+ actorUserId: string;
349
+ correlationId: string;
350
+ }): Promise<ManagedResourcePage>;
351
+ readManagedResource(input: {
352
+ guildId: string;
353
+ moduleId: string;
354
+ resourceType: string;
355
+ resourceId: string;
356
+ actorUserId: string;
357
+ correlationId: string;
358
+ }): Promise<ManagedResourceDetail>;
359
+ listManagedResourceRelation(input: {
360
+ guildId: string;
361
+ moduleId: string;
362
+ resourceType: string;
363
+ resourceId: string;
364
+ relationId: string;
365
+ cursor?: string;
366
+ limit: number;
367
+ actorUserId: string;
368
+ correlationId: string;
369
+ }): Promise<ManagedResourceRelationPage>;
370
+ previewManagedResource(input: {
371
+ guildId: string;
372
+ moduleId: string;
373
+ resourceType: string;
374
+ value: Readonly<Record<string, ManagedResourceValue>>;
375
+ actorUserId: string;
376
+ correlationId: string;
377
+ }): Promise<MessageResponse>;
378
+ createManagedResource(input: {
379
+ guildId: string;
380
+ moduleId: string;
381
+ resourceType: string;
382
+ intent: ManagedResourceCreationIntent;
383
+ value: Readonly<Record<string, ManagedResourceValue>>;
384
+ actorUserId: string;
385
+ correlationId: string;
386
+ }): Promise<ManagedResourceDetail>;
387
+ updateManagedResource(input: {
388
+ guildId: string;
389
+ moduleId: string;
390
+ resourceType: string;
391
+ resourceId: string;
392
+ expectedRevision: number;
393
+ value: Readonly<Record<string, ManagedResourceValue>>;
394
+ actorUserId: string;
395
+ correlationId: string;
396
+ }): Promise<ManagedResourceDetail>;
397
+ deleteManagedResource(input: {
398
+ guildId: string;
399
+ moduleId: string;
400
+ resourceType: string;
401
+ resourceId: string;
402
+ expectedRevision: number;
403
+ actorUserId: string;
404
+ correlationId: string;
405
+ }): Promise<{
406
+ deleted: boolean;
407
+ }>;
408
+ executeManagedResourceAction(input: {
409
+ guildId: string;
410
+ moduleId: string;
411
+ resourceType: string;
412
+ resourceId: string;
413
+ actionId: string;
414
+ expectedRevision: number;
415
+ value: Readonly<Record<string, ManagedResourceValue>>;
416
+ actorUserId: string;
417
+ correlationId: string;
418
+ }): Promise<ManagedResourceDetail | null>;
237
419
  }
238
420
  export declare function createBotControlServer(input: {
239
421
  service: BotControlHandler;
@@ -244,7 +426,7 @@ export declare function createBotControlServer(input: {
244
426
  shardHealth?: () => Promise<ShardHealthSnapshot>;
245
427
  }): Promise<FastifyInstance>;
246
428
  export declare function mapBotControlError(error: unknown): {
247
- status: 400 | 404 | 409;
429
+ status: 400 | 403 | 404 | 409;
248
430
  message: string;
249
431
  } | undefined;
250
432
  export {};