@helyx/bot 1.3.1 → 1.4.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +77 -28
  2. package/README.md +65 -11
  3. package/dist/cloud-connector.d.ts +11 -4
  4. package/dist/cloud-connector.js +52 -15
  5. package/dist/cloud-control-dispatcher.d.ts +8 -3
  6. package/dist/cloud-control-dispatcher.js +62 -4
  7. package/dist/control-errors.d.ts +4 -0
  8. package/dist/control-errors.js +8 -0
  9. package/dist/control-server-schemas.d.ts +84 -0
  10. package/dist/control-server-schemas.js +101 -0
  11. package/dist/control-server.d.ts +21 -7
  12. package/dist/control-server.js +88 -132
  13. package/dist/core-retention.d.ts +26 -0
  14. package/dist/core-retention.js +49 -0
  15. package/dist/creator-guild-join-service.d.ts +25 -0
  16. package/dist/creator-guild-join-service.js +218 -0
  17. package/dist/creator-invite-service.d.ts +37 -0
  18. package/dist/creator-invite-service.js +686 -0
  19. package/dist/creator-onboarding-control.d.ts +12 -0
  20. package/dist/creator-onboarding-control.js +65 -0
  21. package/dist/creator-onboarding-platform.d.ts +20 -0
  22. package/dist/creator-onboarding-platform.js +58 -0
  23. package/dist/dashboard-action-control.d.ts +2 -2
  24. package/dist/dashboard-action-control.js +2 -3
  25. package/dist/database-bootstrap.d.ts +36 -0
  26. package/dist/database-bootstrap.js +101 -0
  27. package/dist/developer-operations-module.d.ts +1 -0
  28. package/dist/developer-operations-module.js +34 -16
  29. package/dist/discord-owned-message-service.d.ts +1 -0
  30. package/dist/discord-owned-message-service.js +3 -0
  31. package/dist/guild-command-read-service.d.ts +8 -3
  32. package/dist/guild-command-read-service.js +14 -7
  33. package/dist/guild-command-release.d.ts +15 -8
  34. package/dist/guild-command-release.js +10 -3
  35. package/dist/guild-command-worker.d.ts +17 -4
  36. package/dist/guild-command-worker.js +17 -6
  37. package/dist/guilds.d.ts +24 -3
  38. package/dist/guilds.js +185 -64
  39. package/dist/hosted-server-moderation.d.ts +2 -2
  40. package/dist/hosted-server-moderation.js +10 -4
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.js +200 -82
  43. package/dist/interactions.js +16 -3
  44. package/dist/module-configuration.d.ts +1 -1
  45. package/dist/module-configuration.js +5 -2
  46. package/dist/module-logging.d.ts +8 -3
  47. package/dist/module-logging.js +13 -7
  48. package/dist/modules.d.ts +1 -0
  49. package/dist/modules.js +8 -1
  50. package/dist/mysql-runtime-persistence.d.ts +46 -0
  51. package/dist/mysql-runtime-persistence.js +91 -0
  52. package/dist/oauth-module.js +2 -2
  53. package/dist/oauth-service.d.ts +7 -0
  54. package/dist/oauth-service.js +3 -0
  55. package/dist/scheduled-task-runner.d.ts +9 -85
  56. package/dist/scheduled-task-runner.js +1 -1
  57. package/dist/shard-contracts.d.ts +135 -0
  58. package/dist/shard-contracts.js +13 -1
  59. package/dist/shard-router.d.ts +2 -1
  60. package/dist/shard-router.js +7 -0
  61. package/dist/shard-supervisor.js +43 -9
  62. package/dist/shard-worker.d.ts +5 -2
  63. package/dist/shard-worker.js +41 -5
  64. package/dist/supervisor-cloud-directory.d.ts +9 -3
  65. package/dist/supervisor-cloud-directory.js +8 -12
  66. package/dist/trivia-data-provider.d.ts +11 -0
  67. package/dist/trivia-data-provider.js +46 -26
  68. package/dist/verification-service.d.ts +2 -2
  69. package/dist/verification-service.js +6 -3
  70. package/documentation/README.md +5 -3
  71. package/documentation/examples/.env.example +15 -11
  72. package/documentation/guides/getting-started.md +29 -12
  73. package/documentation/guides/privacy-and-support.md +6 -5
  74. package/documentation/guides/self-hosting.md +16 -11
  75. package/documentation/modules/auto-moderation.md +7 -5
  76. package/documentation/modules/building-a-module.md +48 -2
  77. package/documentation/modules/moderation.md +5 -5
  78. package/documentation/modules/publishing-a-module.md +6 -4
  79. package/documentation/modules/testing-a-module.md +39 -4
  80. package/documentation/reference/environment.md +43 -9
  81. package/documentation/runbooks/backup-and-recovery.md +26 -7
  82. package/documentation/runbooks/installation-and-upgrades.md +97 -27
  83. package/package.json +20 -16
package/CHANGELOG.md CHANGED
@@ -1,5 +1,79 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Report Discord readiness to owned-message cleanup so module startup cannot consume recovery attempts before bot login.
8
+
9
+ - Drain active guild tracking and owner observations before closing Discord and
10
+ database resources; stop scheduling new reconciliation work during shutdown.
11
+
12
+ - Prepare the coordinated Phase 8 minor release and align internal package dependencies. Existing deployment and database-support restrictions are unchanged.
13
+
14
+ Support checksum-only PostgreSQL startup inspection after a dedicated hosted
15
+ migration job, while preserving automatic migrations as the default and for
16
+ every Self-Hosted database.
17
+
18
+ Keep Server Health report links inside the current Cloud Connect installation for self-hosted bots. Hosted report links retain their existing route.
19
+
20
+ Keep hosted leave workers out of self-hosted runtimes and fence revoked
21
+ blacklist deliveries before a Discord leave. Preserve stopped delivery evidence
22
+ for review without changing independent leave requests.
23
+
24
+ Correct the generated starter's Cloud Connect command and document explicit server-command allowlists, required Discord intents, database TLS modes and the one-database Cloud Connect topology without changing runtime defaults.
25
+
26
+ Leave worker-owned server command collections intact when command writes are
27
+ disabled, including unsharded and sharded startup. Do not replace active module
28
+ commands with the internal developer root during a restart.
29
+ Keep public creator-page validation read-only rather than competing with the
30
+ invite cleanup/create/revoke lock; recheck active ownership before returning.
31
+
32
+ ### Patch Changes
33
+
34
+ - Patch Fastify to5.12.7 before publication. Keep the reviewed Phase8 minor
35
+ version and existing Discord/Undici transport compatibility unchanged.
36
+
37
+ - Updated dependencies
38
+ - @helyx/config@1.4.0
39
+ - @helyx/control-protocol@0.7.0
40
+ - @helyx/core@0.2.0
41
+ - @helyx/database-contracts@0.2.0
42
+ - @helyx/database-mysql@0.2.0
43
+ - @helyx/database@2.4.0
44
+ - @helyx/framework@0.2.0
45
+ - @helyx/module-automod@1.1.0
46
+ - @helyx/module-manifest@0.13.0
47
+ - @helyx/module-moderation@1.1.0
48
+ - @helyx/platform@1.3.0
49
+ - @helyx/sdk@0.15.0
50
+ - @helyx/status-client@0.3.0
51
+ - @helyx/verification-contracts@0.4.0
52
+
53
+ ## 1.3.3
54
+
55
+ ### Patch Changes
56
+
57
+ - Updated dependencies
58
+ - @helyx/control-protocol@0.6.0
59
+
60
+ ## 1.3.2
61
+
62
+ ### Patch Changes
63
+
64
+ - Support deferred source-message updates for modal submissions so poll draft
65
+ edits replace their previous controls instead of creating duplicate messages.
66
+ - Move the complete installed feature-module list to `/dev modules` and show
67
+ compact names and versions for modules and `/dev bot` core functions.
68
+ - Bound internal dependencies to compatible releases while preserving required
69
+ minimum API versions.
70
+ - Forward the hosted moderation control credential to shard workers only when
71
+ hosted moderation is enabled, preserving the existing worker credential boundary.
72
+ - Forward the explicit database-engine selection to shard workers while
73
+ preserving PostgreSQL as the default.
74
+ - Update the HTTP client to 6.29.0 to include the current WebSocket and retry
75
+ interceptor security fixes.
76
+
3
77
  ## 1.3.1
4
78
 
5
79
  ### Patch Changes
@@ -435,34 +509,9 @@
435
509
 
436
510
  ### Patch Changes
437
511
 
438
- - Reuse active Verification links when available, report their expiry, and allow members to replace them while atomically invalidating the old request.---
439
- "@helyx/bot": patch
440
- "@helyx/database": patch
441
- "@helyx/sdk": patch
442
- "@helyx/verification-contracts": patch
443
- ---
444
-
445
- Reuse active Verification links when available, report their expiry, and allow members to replace them while atomically invalidating the old request.
446
-
447
- - Accept compatible internal Helyx releases within explicit minor-safe pre-1.0 or major-safe stable ranges, preventing dependency-only patch cascades.---
448
- "@helyx/bot": patch
449
- "@helyx/cli": patch
450
- "@helyx/core": patch
451
- "@helyx/framework": patch
452
- "@helyx/platform": patch
453
- "@helyx/sdk": patch
454
- "@helyx/module-account-gate": patch
455
- "@helyx/module-feedback": patch
456
- "@helyx/module-giveaways": patch
457
- "@helyx/module-levels": patch
458
- "@helyx/module-logging": patch
459
- "@helyx/module-polls": patch
460
- "@helyx/module-reactionroles": patch
461
- "@helyx/module-suggestions": patch
462
- "@helyx/module-welcome": patch
463
- ---
464
-
465
- Accept compatible internal Helyx releases within explicit minor-safe pre-1.0 or major-safe stable ranges, preventing dependency-only patch cascades.
512
+ - Reuse active Verification links when available, report their expiry, and allow members to replace them while atomically invalidating the old request.
513
+
514
+ - Accept compatible internal Helyx releases within explicit minor-safe pre-1.0 or major-safe stable ranges, preventing dependency-only patch cascades.
466
515
 
467
516
  ## 0.4.23
468
517
 
package/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # Helyx bot
2
2
 
3
- The Helyx Discord runtime validates its environment, verifies and migrates PostgreSQL, validates explicitly configured module packages, applies their migrations, composes platform services, synchronises guild installations and commands, logs in with the non-privileged Guilds intent, and shuts down Discord, modules, and PostgreSQL safely.
3
+ The Helyx Discord runtime validates its environment and explicitly configured
4
+ module packages, loads the selected database adapter, prepares core and module
5
+ migrations, composes platform services, synchronises installations and commands,
6
+ and connects to Discord. Shutdown closes modules, Discord and database resources.
7
+ Hosted services use PostgreSQL; Self-Hosted bots support PostgreSQL or a
8
+ capability-compatible MySQL database with matching module support.
4
9
 
5
10
  Version-matched public guides, operator runbooks, environment reference
6
11
  material, and module-authoring instructions ship at `documentation/README.md`
@@ -21,7 +26,14 @@ for production and self-hosted deployments during migration. Use
21
26
  that mode startup publishes only platform globals and migrated customer
22
27
  commands come from each guild's command profile and enabled module state.
23
28
 
24
- The built-in global `/helyx` and `/permissions` commands are core infrastructure. `/helyx` gives server owners, administrators and members with Manage Server permission an ephemeral module-management flow. It lists installed feature packages, enables or disables them, stages schema-defined settings, uses native Discord channel and role selectors, validates the complete configuration, and saves through the same versioned configuration and audit services as the dashboard. Dependency rules prevent a required module being disabled or a dependent module being enabled out of order.
29
+ The built-in global `/helyx` and `/permissions` commands are core infrastructure.
30
+ `/helyx modules` gives server owners, administrators and members with Manage
31
+ Server permission an ephemeral module-management flow. It lists installed
32
+ feature packages, enables or disables them, stages schema-defined settings,
33
+ uses native Discord channel and role selectors, validates the complete
34
+ configuration, and saves through the same versioned configuration and audit
35
+ services as the dashboard. Dependency rules prevent a required module being
36
+ disabled or a dependent module being enabled out of order.
25
37
 
26
38
  `/permissions` uses an owner/administrator-protected select and modal flow with native Discord selectors to manage inherited module policies and command overrides. Feature packages require an explicit allowlist: hosted distributions carry a reviewed `hosted-modules.json` catalogue, generated self-host projects use `HELYX_MODULE_CATALOGUE_PATH`, and `HELYX_MODULE_PACKAGES` remains an intentional comma-separated override. The sources are merged and de-duplicated. A core distribution has no catalogue and runs core-only when both self-host selectors are empty.
27
39
 
@@ -32,7 +44,19 @@ to `/helyx` or the dashboard. In platform-only mode, migrated commands are
32
44
  included in a guild only when its command profile selects them. Package
33
45
  upgrades preserve each server's existing enabled or disabled state.
34
46
 
35
- Gateway intents are derived from installed event contributions. The bot always requests Guilds, adds non-privileged reaction access only for reaction modules, and adds the privileged Server Members intent only when an installed module handles member joins. Deployments containing such a module must enable the matching intent in the Discord Developer Portal before startup.
47
+ Gateway intents are derived from installed contributions and explicit runtime
48
+ settings. The bot always requests Guilds and adds the reaction, message, voice
49
+ and Auto Moderation intents required by the loaded modules. Server Members is
50
+ requested when member events/capabilities require it or
51
+ `HELYX_GUILD_MEMBERS_ENABLED=true`; enable the matching privileged intent in the
52
+ Discord Developer Portal before login. Set that flag when a selected workflow
53
+ requires Helyx to advertise the Guild Members capability as available.
54
+
55
+ Message Content requires `HELYX_MESSAGE_CONTENT_ENABLED=true` as well as the
56
+ Developer Portal intent and a loaded contribution that needs it. Self-Hosted
57
+ Enhanced Auto Moderation uses this local capability; Hosted Enhanced scanning
58
+ uses the separately operated Ecko adapter. Match intents to the selected
59
+ features rather than enabling every privileged intent.
36
60
 
37
61
  Moderation integrations use closed, versioned service contracts for native
38
62
  Discord Auto Moderation rules, member and message enforcement, restricted
@@ -51,7 +75,16 @@ results before they cross the control boundary.
51
75
 
52
76
  When `HELYX_BOT_CONTROL_SECRET` is configured, the bot also starts a private authenticated HTTP control server on `HELYX_BOT_CONTROL_PORT`. The dashboard API uses this service to obtain real Discord roles and channels and to apply validated module, configuration and permission changes. `/health` is unauthenticated. Ordinary `/v1` routes require the shared bearer secret; the narrow `/v1/ecko/*` routes instead require their dedicated rotating HMAC transport key. Keep this endpoint on a private service network and do not assign it a public domain.
53
77
 
54
- 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.
78
+ Optional Helyx Cloud Connect is a separate outbound management path for
79
+ self-hosted deployments. It is disabled unless
80
+ `HELYX_CLOUD_CONNECT_ENABLED=true` and requires a stable local
81
+ `HELYX_DATA_ENCRYPTION_KEY`. Generated starters enable the connector but remain
82
+ unpaired until an authorised owner supplies a one-use code with
83
+ `/helyx cloud connect`. The `/helyx cloud` command group also supports status,
84
+ credential rotation, disconnection and bounded support consent. If the key is
85
+ lost or changed, revoke the old installation and enrol a new identity. The bot
86
+ never sends its Discord token, database URL or connector private key to Helyx
87
+ Cloud, and local Discord operation continues when Cloud is unavailable.
55
88
 
56
89
  Production Status reporting is opt-in and disabled by default. Set
57
90
  `HELYX_STATUS_ENABLED=true` only with the reviewed `HELYX_STATUS_URL`, the
@@ -65,11 +98,11 @@ Giveaways and Invite Tracking also require the existing
65
98
  checkpoints and stable invite-member fingerprints without exposing the key to
66
99
  a module. Keep it stable across deployments.
67
100
 
68
- Trivia uses questions already imported into PostgreSQL. Multiple Choice and
69
- Guess both run entirely from the saved question catalogue, and the bot runtime
70
- does not call an external trivia provider.
101
+ Trivia uses the module's migrated question catalogue in the selected database.
102
+ Multiple Choice, Guess and True or False run locally; gameplay does not call an
103
+ external trivia provider.
71
104
 
72
- The always-available guild-only `/bug` command can be used by any guild member.
105
+ The always-available guild-only `/helyx bug` command can be used by any guild member.
73
106
  Hosted deployments submit through the dedicated `HELYX_SUPPORT_SERVICE_SECRET`;
74
107
  paired self-hosted deployments use their authenticated Cloud Connect session
75
108
  and may receive a one-time link for claiming, tracking and replying to the
@@ -157,8 +190,29 @@ self-hosting is outside the first release.
157
190
  Changing the total shard count redistributes guild ownership. Treat it as a
158
191
  generation change: drain the old generation before admitting the new one and
159
192
  never run both as active owners. Shard-capable code does not authorize hosted
160
- activation. Phase 10 production activation, replica scaling, or any Railway
161
- variable change requires separate owner authorization; do not provision a new
193
+ activation. New hosted shard activation, replica scaling, or provider
194
+ configuration changes require separate owner authorization; preserve an existing
195
+ reviewed topology rather than resetting it to the new-install default. Do not provision a new
162
196
  Redis or coordinator service for sharding.
163
197
 
164
- For packaged operation use `helyx start`. `helyx migrate` applies core database migrations without Discord, and read-only `helyx doctor` verifies environment, PostgreSQL migration history, and configured package metadata.
198
+ ## Packaged operation and database checks
199
+
200
+ Generated starters install `@helyx/cli` and expose `npm run setup`,
201
+ `npm run doctor`, `npm start`, `npm run update:check` and `npm run update`.
202
+ Follow their README for Node.js, Docker or managed-host commands; the bot
203
+ package itself does not provide the `helyx` executable. Compiled npm packages
204
+ do not require a source build before normal startup.
205
+
206
+ `helyx start` applies pending core and selected-module migrations by default.
207
+ `helyx migrate` deliberately applies only core migrations without Discord.
208
+ Read-only `helyx doctor` validates environment, database connectivity, existing
209
+ core migration history and configured package metadata; it inspects module
210
+ migration history when core migrations are current. Pending migrations are
211
+ reported without applying them. Startup performs database capability probes
212
+ and the complete runtime/Discord checks that doctor does not perform.
213
+
214
+ `DATABASE_ENGINE` defaults to `postgresql`; `mysql` selects the optional adapter
215
+ and currently requires `HELYX_SHARD_MODE=off`. Changing the setting does not
216
+ convert existing data between engines. Hosted PostgreSQL deployments may use
217
+ `HELYX_DATABASE_STARTUP_MODE=inspect` after their separate approved migration
218
+ job; Self-Hosted startup keeps automatic `apply` mode.
@@ -4,6 +4,8 @@ import { type CloudConnectorConsentRecord, type DatabasePool } from "@helyx/data
4
4
  import type { Client } from "discord.js";
5
5
  import type { Logger } from "pino";
6
6
  import type { ConfiguredModulePackage } from "./modules.js";
7
+ import type { GuildCommandReadHandler } from "./guild-command-read-service.js";
8
+ import { type CloudConnectorStore } from "./cloud-control-dispatcher.js";
7
9
  import { type BotControlHandler } from "./control-server.js";
8
10
  type ConnectorState = "disabled" | "unpaired" | "connecting" | "online" | "offline" | "recovery_required" | "revoked" | "stopped";
9
11
  export interface CloudConnectorStatus {
@@ -27,7 +29,7 @@ export interface CloudDiscordDirectory {
27
29
  snapshot(): Promise<CloudDiscordSnapshot>;
28
30
  }
29
31
  export interface CloudConnector {
30
- setControlHandler(handler: BotControlHandler): void;
32
+ setControlHandler(handler: BotControlHandler, commands?: GuildCommandReadHandler): void;
31
33
  start(): Promise<void>;
32
34
  status(): CloudConnectorStatus;
33
35
  getSupportConsent(): Promise<CloudConnectorConsentRecord>;
@@ -54,13 +56,18 @@ export declare class CloudConnectorController implements CloudConnector {
54
56
  private readonly input;
55
57
  constructor(input: {
56
58
  environment: BotEnvironment;
57
- database: DatabasePool;
58
59
  discord: CloudDiscordDirectory;
59
60
  modules: readonly ConfiguredModulePackage[];
60
61
  coreVersion: string;
61
62
  logger: Logger;
62
- });
63
- setControlHandler(handler: BotControlHandler): void;
63
+ } & ({
64
+ database: DatabasePool;
65
+ repository?: never;
66
+ } | {
67
+ repository: CloudConnectorStore;
68
+ database?: never;
69
+ }));
70
+ setControlHandler(handler: BotControlHandler, commands?: GuildCommandReadHandler): void;
64
71
  start(): Promise<void>;
65
72
  status(): CloudConnectorStatus;
66
73
  getSupportConsent(): Promise<CloudConnectorConsentRecord>;
@@ -87,7 +87,8 @@ export class CloudConnectorController {
87
87
  #pendingSupport = new Map();
88
88
  constructor(input) {
89
89
  this.input = input;
90
- this.#repository = new CloudConnectorRepository(input.database);
90
+ this.#repository =
91
+ input.repository ?? new CloudConnectorRepository(input.database);
91
92
  this.#state = input.environment.HELYX_CLOUD_CONNECT_ENABLED
92
93
  ? "unpaired"
93
94
  : "disabled";
@@ -95,8 +96,10 @@ export class CloudConnectorController {
95
96
  ? Buffer.from(input.environment.HELYX_DATA_ENCRYPTION_KEY, "base64")
96
97
  : undefined;
97
98
  }
98
- setControlHandler(handler) {
99
- this.#dispatcher = new CloudControlDispatcher(this.input.database, handler);
99
+ setControlHandler(handler, commands) {
100
+ this.#dispatcher = this.input.database
101
+ ? new CloudControlDispatcher(this.input.database, handler, commands)
102
+ : new CloudControlDispatcher({ repository: this.#repository }, handler, commands);
100
103
  }
101
104
  async start() {
102
105
  if (!this.input.environment.HELYX_CLOUD_CONNECT_ENABLED)
@@ -414,6 +417,8 @@ export class CloudConnectorController {
414
417
  let outgoingSequence = 0;
415
418
  let heartbeat;
416
419
  let plannedReconnect;
420
+ let activeControlRequests = 0;
421
+ let draining = false;
417
422
  const privateKey = decryptPrivateKey(identity.privateKey, this.#dataKey, identity.installationId);
418
423
  const finished = new Promise((resolveFinished, rejectFinished) => {
419
424
  const fail = (error) => {
@@ -475,10 +480,32 @@ export class CloudConnectorController {
475
480
  installationId: identity.installationId,
476
481
  connectionEpoch: accepted.data.connectionEpoch,
477
482
  sequence: ++outgoingSequence,
478
- payload: { state: "online" },
483
+ payload: { state: draining ? "draining" : "online" },
479
484
  })));
480
485
  }, accepted.data.heartbeatIntervalSeconds * 1_000);
481
- plannedReconnect = setTimeout(() => socket.close(1012, "Planned Cloud Connect session rotation"), accepted.data.reconnectBeforeSeconds * 1_000);
486
+ plannedReconnect = setTimeout(() => {
487
+ if (socket.readyState !== WebSocket.OPEN)
488
+ return;
489
+ draining = true;
490
+ socket.send(JSON.stringify(connectedMessage({
491
+ type: "connector.draining",
492
+ installationId: identity.installationId,
493
+ connectionEpoch: accepted.data.connectionEpoch,
494
+ sequence: ++outgoingSequence,
495
+ payload: { reason: "session_rotation" },
496
+ })));
497
+ const deadline = Date.now() + 45_000;
498
+ const closeWhenIdle = () => {
499
+ if (socket.readyState !== WebSocket.OPEN)
500
+ return;
501
+ if (activeControlRequests === 0 || Date.now() >= deadline) {
502
+ socket.close(1012, "Planned Cloud Connect session rotation");
503
+ return;
504
+ }
505
+ plannedReconnect = setTimeout(closeWhenIdle, 250);
506
+ };
507
+ plannedReconnect = setTimeout(closeWhenIdle, 1_000);
508
+ }, accepted.data.reconnectBeforeSeconds * 1_000);
482
509
  return;
483
510
  }
484
511
  const rejected = connectorRejectedSchema.safeParse(payload);
@@ -518,16 +545,22 @@ export class CloudConnectorController {
518
545
  }
519
546
  if (message.type !== "control.request")
520
547
  return;
521
- const response = await this.#dispatch(message.payload, message.correlationId);
522
- if (socket.readyState === WebSocket.OPEN)
523
- socket.send(JSON.stringify(connectedMessage({
524
- type: "control.response",
525
- installationId: identity.installationId,
526
- connectionEpoch: message.connectionEpoch,
527
- sequence: ++outgoingSequence,
528
- correlationId: message.correlationId,
529
- payload: response,
530
- })));
548
+ activeControlRequests += 1;
549
+ try {
550
+ const response = await this.#dispatch(message.payload, message.correlationId);
551
+ if (socket.readyState === WebSocket.OPEN)
552
+ socket.send(JSON.stringify(connectedMessage({
553
+ type: "control.response",
554
+ installationId: identity.installationId,
555
+ connectionEpoch: message.connectionEpoch,
556
+ sequence: ++outgoingSequence,
557
+ correlationId: message.correlationId,
558
+ payload: response,
559
+ })));
560
+ }
561
+ finally {
562
+ activeControlRequests -= 1;
563
+ }
531
564
  }
532
565
  catch (error) {
533
566
  this.input.logger.warn({
@@ -722,6 +755,10 @@ function connectorCapabilities() {
722
755
  "guild.dashboard.access.read.v1",
723
756
  "guild.access-policy.read.v1",
724
757
  "guild.access-policy.write.v1",
758
+ "guild.commands.status.read.v1",
759
+ "guild.commands.history.read.v1",
760
+ "guild.commands.status.refresh.v1",
761
+ "guild.commands.rebuild.v1",
725
762
  "guild.module.public.read.v1",
726
763
  "guild.module.status.write.v1",
727
764
  "guild.module.configuration.write.v1",
@@ -1,11 +1,16 @@
1
1
  import { type CloudControlRequestPayload } from "@helyx/control-protocol";
2
- import { type DatabasePool } from "@helyx/database";
2
+ import { CloudConnectorRepository, type DatabasePool } from "@helyx/database";
3
3
  import type { BotControlHandler, BotControlState } from "./control-server.js";
4
+ import type { GuildCommandReadHandler } from "./guild-command-read-service.js";
5
+ export type CloudConnectorStore = Pick<CloudConnectorRepository, "getIdentity" | "createIdentity" | "rotateIdentity" | "markConnected" | "revoke" | "getConsent" | "updateConsent" | "appendAudit" | "cleanupAudit">;
4
6
  export declare class CloudControlDispatcher {
5
7
  #private;
6
- private readonly database;
7
8
  private readonly control;
8
- constructor(database: DatabasePool, control: BotControlHandler);
9
+ private readonly commands?;
10
+ constructor(database: DatabasePool, control: BotControlHandler, commands?: GuildCommandReadHandler);
11
+ constructor(input: {
12
+ repository: CloudConnectorStore;
13
+ }, control: BotControlHandler, commands?: GuildCommandReadHandler);
9
14
  execute(request: CloudControlRequestPayload, correlationId: string): Promise<unknown>;
10
15
  }
11
16
  export declare class CloudControlDeniedError extends Error {
@@ -1,11 +1,16 @@
1
1
  import { assertCloudResponseSize, parseOperationInput, } from "@helyx/control-protocol";
2
2
  import { CloudConnectorRepository } from "@helyx/database";
3
3
  export class CloudControlDispatcher {
4
- database;
5
4
  control;
6
- constructor(database, control) {
7
- this.database = database;
5
+ commands;
6
+ #repository;
7
+ constructor(databaseOrInput, control, commands) {
8
8
  this.control = control;
9
+ this.commands = commands;
10
+ this.#repository =
11
+ "repository" in databaseOrInput
12
+ ? databaseOrInput.repository
13
+ : new CloudConnectorRepository(databaseOrInput);
9
14
  }
10
15
  async execute(request, correlationId) {
11
16
  const actorUserId = request.actor.kind === "customer"
@@ -52,6 +57,48 @@ export class CloudControlDispatcher {
52
57
  });
53
58
  break;
54
59
  }
60
+ case "guild.commands.status.read.v1": {
61
+ const input = parseOperationInput(request.operation, request.input);
62
+ await this.#requireCommandAccess(input.guildId, actorUserId);
63
+ result = await this.#commands().getStatus(input.guildId);
64
+ break;
65
+ }
66
+ case "guild.commands.history.read.v1": {
67
+ const input = parseOperationInput(request.operation, request.input);
68
+ await this.#requireCommandAccess(input.guildId, actorUserId);
69
+ result = await this.#commands().listHistory({
70
+ guildId: input.guildId,
71
+ ...(input.before ? { beforeEventId: BigInt(input.before) } : {}),
72
+ ...(input.since ? { since: new Date(input.since) } : {}),
73
+ limit: input.limit,
74
+ });
75
+ break;
76
+ }
77
+ case "guild.commands.status.refresh.v1": {
78
+ const input = parseOperationInput(request.operation, request.input);
79
+ const actor = await this.#requireCommandAccess(input.guildId, actorUserId);
80
+ result = await this.#commands().refreshStatus({
81
+ guildId: input.guildId,
82
+ actorUserId: actor,
83
+ correlationId,
84
+ });
85
+ break;
86
+ }
87
+ case "guild.commands.rebuild.v1": {
88
+ const input = parseOperationInput(request.operation, request.input);
89
+ const actor = await this.#requireCommandAccess(input.guildId, actorUserId);
90
+ result = await this.#commands().requestReconciliation({
91
+ guildId: input.guildId,
92
+ actorUserId: actor,
93
+ correlationId,
94
+ expectedRevision: BigInt(input.expectedRevision),
95
+ source: "customer_rebuild",
96
+ budgetClass: "customer",
97
+ delayMs: 120_000,
98
+ summary: "A server manager requested a command rebuild.",
99
+ });
100
+ break;
101
+ }
55
102
  case "guild.module.public.read.v1": {
56
103
  const input = parseOperationInput(request.operation, request.input);
57
104
  if (request.actor.kind !== "public")
@@ -245,8 +292,19 @@ export class CloudControlDispatcher {
245
292
  assertCloudResponseSize(result);
246
293
  return result;
247
294
  }
295
+ #commands() {
296
+ if (!this.commands)
297
+ throw new Error("guild_command_state_unavailable");
298
+ return this.commands;
299
+ }
300
+ async #requireCommandAccess(guildId, actorUserId) {
301
+ const actor = requiredActor(actorUserId);
302
+ if (!(await this.control.canAccessDashboard({ guildId, actorUserId: actor })))
303
+ throw new CloudControlDeniedError("guild_command_access_denied");
304
+ return actor;
305
+ }
248
306
  async #supportSnapshot(input) {
249
- const repository = new CloudConnectorRepository(this.database);
307
+ const repository = this.#repository;
250
308
  const consent = await repository.getConsent();
251
309
  const requested = input.dataClasses.filter((dataClass) => consent.supportDataClasses.includes(dataClass));
252
310
  if (!consent.supportInspectionEnabled || requested.length === 0) {
@@ -1,4 +1,8 @@
1
1
  import { type PermissionMutationDenialReason } from "@helyx/core";
2
+ export declare class BotControlTransportError extends Error {
3
+ readonly status: 400 | 403 | 404 | 409 | 502 | 503 | 504;
4
+ constructor(status: 400 | 403 | 404 | 409 | 502 | 503 | 504, message: string);
5
+ }
2
6
  export declare class BotControlForbiddenError extends Error {
3
7
  constructor(message?: string);
4
8
  }
@@ -1,4 +1,12 @@
1
1
  import { permissionMutationDenialMessage, } from "@helyx/core";
2
+ export class BotControlTransportError extends Error {
3
+ status;
4
+ constructor(status, message) {
5
+ super(message);
6
+ this.status = status;
7
+ this.name = "BotControlTransportError";
8
+ }
9
+ }
2
10
  export class BotControlForbiddenError extends Error {
3
11
  constructor(message = "You no longer have permission to perform this action") {
4
12
  super(message);
@@ -0,0 +1,84 @@
1
+ import type { ManagedResourceValue } from "@helyx/sdk";
2
+ import { z } from "zod";
3
+ export declare const snowflake: z.ZodString;
4
+ export declare const moduleId: z.ZodString;
5
+ export declare const configurationValue: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodNull, z.ZodString, z.ZodBoolean, z.ZodNumber, z.ZodArray<z.ZodString>]>>;
6
+ export declare const permissionState: z.ZodObject<{
7
+ version: z.ZodNumber;
8
+ module: z.ZodObject<{
9
+ preset: z.ZodEnum<{
10
+ custom: "custom";
11
+ everyone: "everyone";
12
+ moderators: "moderators";
13
+ administrators: "administrators";
14
+ }>;
15
+ allowedRoleIds: z.ZodArray<z.ZodString>;
16
+ deniedRoleIds: z.ZodArray<z.ZodString>;
17
+ }, z.core.$strip>;
18
+ commands: z.ZodArray<z.ZodObject<{
19
+ commandId: z.ZodString;
20
+ version: z.ZodNumber;
21
+ enabled: z.ZodBoolean;
22
+ policy: z.ZodObject<{
23
+ preset: z.ZodEnum<{
24
+ custom: "custom";
25
+ everyone: "everyone";
26
+ moderators: "moderators";
27
+ administrators: "administrators";
28
+ }>;
29
+ allowedRoleIds: z.ZodArray<z.ZodString>;
30
+ deniedRoleIds: z.ZodArray<z.ZodString>;
31
+ }, z.core.$strip>;
32
+ }, z.core.$strip>>;
33
+ }, z.core.$strip>;
34
+ export declare const guildAccessPolicy: z.ZodObject<{
35
+ administratorRoleIds: z.ZodArray<z.ZodString>;
36
+ moderatorRoleIds: z.ZodArray<z.ZodString>;
37
+ dashboardModeratorsEnabled: z.ZodBoolean;
38
+ dashboardAllowedRoleIds: z.ZodArray<z.ZodString>;
39
+ dashboardDeniedRoleIds: z.ZodArray<z.ZodString>;
40
+ }, z.core.$strict>;
41
+ export declare const mutationContext: z.ZodObject<{
42
+ actorUserId: z.ZodString;
43
+ correlationId: z.ZodUUID;
44
+ }, z.core.$strip>;
45
+ export declare const resourceId: z.ZodString;
46
+ export declare const resourceCursor: z.ZodString;
47
+ export declare const resourceActionId: z.ZodString;
48
+ export declare const managedResourceFilters: z.ZodRecord<z.ZodString, z.ZodString>;
49
+ export declare const managedResourceObject: z.ZodRecord<z.ZodString, z.ZodCustom<ManagedResourceValue, ManagedResourceValue>>;
50
+ export declare const managedResourceDetailSchema: z.ZodObject<{
51
+ id: z.ZodString;
52
+ revision: z.ZodNumber;
53
+ status: z.ZodString;
54
+ title: z.ZodString;
55
+ description: z.ZodOptional<z.ZodString>;
56
+ attributes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodCustom<ManagedResourceValue, ManagedResourceValue>>>;
57
+ updatedAt: z.ZodISODateTime;
58
+ value: z.ZodRecord<z.ZodString, z.ZodCustom<ManagedResourceValue, ManagedResourceValue>>;
59
+ availableActionIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
60
+ }, z.core.$strict>;
61
+ export declare const managedResourcePageSchema: z.ZodObject<{
62
+ items: z.ZodArray<z.ZodObject<{
63
+ id: z.ZodString;
64
+ revision: z.ZodNumber;
65
+ status: z.ZodString;
66
+ title: z.ZodString;
67
+ description: z.ZodOptional<z.ZodString>;
68
+ attributes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodCustom<ManagedResourceValue, ManagedResourceValue>>>;
69
+ updatedAt: z.ZodISODateTime;
70
+ }, z.core.$strict>>;
71
+ nextCursor: z.ZodOptional<z.ZodString>;
72
+ total: z.ZodOptional<z.ZodNumber>;
73
+ }, z.core.$strict>;
74
+ export declare const managedResourceRelationPageSchema: z.ZodObject<{
75
+ items: z.ZodArray<z.ZodObject<{
76
+ id: z.ZodString;
77
+ value: z.ZodRecord<z.ZodString, z.ZodCustom<ManagedResourceValue, ManagedResourceValue>>;
78
+ }, z.core.$strict>>;
79
+ nextCursor: z.ZodOptional<z.ZodString>;
80
+ }, z.core.$strict>;
81
+ export declare const managedResourceDeleteResultSchema: z.ZodObject<{
82
+ deleted: z.ZodBoolean;
83
+ }, z.core.$strict>;
84
+ //# sourceMappingURL=control-server-schemas.d.ts.map