@helyx/bot 1.3.0 → 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.
- package/CHANGELOG.md +92 -28
- package/README.md +65 -11
- package/dist/cloud-connector.d.ts +11 -4
- package/dist/cloud-connector.js +52 -15
- package/dist/cloud-control-dispatcher.d.ts +8 -3
- package/dist/cloud-control-dispatcher.js +62 -4
- package/dist/control-errors.d.ts +4 -0
- package/dist/control-errors.js +8 -0
- package/dist/control-server-schemas.d.ts +84 -0
- package/dist/control-server-schemas.js +101 -0
- package/dist/control-server.d.ts +21 -7
- package/dist/control-server.js +88 -132
- package/dist/core-retention.d.ts +26 -0
- package/dist/core-retention.js +49 -0
- package/dist/creator-guild-join-service.d.ts +25 -0
- package/dist/creator-guild-join-service.js +218 -0
- package/dist/creator-invite-service.d.ts +37 -0
- package/dist/creator-invite-service.js +686 -0
- package/dist/creator-onboarding-control.d.ts +12 -0
- package/dist/creator-onboarding-control.js +65 -0
- package/dist/creator-onboarding-platform.d.ts +20 -0
- package/dist/creator-onboarding-platform.js +58 -0
- package/dist/dashboard-action-control.d.ts +2 -2
- package/dist/dashboard-action-control.js +2 -3
- package/dist/database-bootstrap.d.ts +36 -0
- package/dist/database-bootstrap.js +101 -0
- package/dist/developer-operations-module.d.ts +1 -0
- package/dist/developer-operations-module.js +34 -16
- package/dist/discord-owned-message-service.d.ts +1 -0
- package/dist/discord-owned-message-service.js +3 -0
- package/dist/guild-command-read-service.d.ts +8 -3
- package/dist/guild-command-read-service.js +14 -7
- package/dist/guild-command-release.d.ts +15 -8
- package/dist/guild-command-release.js +10 -3
- package/dist/guild-command-worker.d.ts +17 -4
- package/dist/guild-command-worker.js +17 -6
- package/dist/guilds.d.ts +24 -3
- package/dist/guilds.js +185 -64
- package/dist/hosted-server-moderation.d.ts +2 -2
- package/dist/hosted-server-moderation.js +10 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +200 -81
- package/dist/interactions.js +16 -3
- package/dist/module-configuration.d.ts +1 -1
- package/dist/module-configuration.js +5 -2
- package/dist/module-logging.d.ts +8 -3
- package/dist/module-logging.js +13 -7
- package/dist/modules.d.ts +1 -0
- package/dist/modules.js +8 -1
- package/dist/mysql-runtime-persistence.d.ts +46 -0
- package/dist/mysql-runtime-persistence.js +91 -0
- package/dist/oauth-module.js +2 -2
- package/dist/oauth-service.d.ts +7 -0
- package/dist/oauth-service.js +3 -0
- package/dist/scheduled-task-runner.d.ts +9 -85
- package/dist/scheduled-task-runner.js +1 -1
- package/dist/shard-contracts.d.ts +135 -0
- package/dist/shard-contracts.js +13 -1
- package/dist/shard-router.d.ts +2 -1
- package/dist/shard-router.js +7 -0
- package/dist/shard-supervisor.js +43 -9
- package/dist/shard-worker.d.ts +5 -2
- package/dist/shard-worker.js +46 -5
- package/dist/supervisor-cloud-directory.d.ts +9 -3
- package/dist/supervisor-cloud-directory.js +8 -12
- package/dist/trivia-data-provider.d.ts +11 -0
- package/dist/trivia-data-provider.js +46 -26
- package/dist/verification-service.d.ts +2 -2
- package/dist/verification-service.js +6 -3
- package/documentation/README.md +5 -3
- package/documentation/examples/.env.example +15 -11
- package/documentation/guides/getting-started.md +29 -12
- package/documentation/guides/privacy-and-support.md +6 -5
- package/documentation/guides/self-hosting.md +16 -11
- package/documentation/modules/auto-moderation.md +7 -5
- package/documentation/modules/building-a-module.md +48 -2
- package/documentation/modules/moderation.md +5 -5
- package/documentation/modules/publishing-a-module.md +6 -4
- package/documentation/modules/testing-a-module.md +39 -4
- package/documentation/reference/environment.md +43 -9
- package/documentation/runbooks/backup-and-recovery.md +26 -7
- package/documentation/runbooks/installation-and-upgrades.md +97 -27
- package/package.json +20 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,94 @@
|
|
|
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
|
+
|
|
77
|
+
## 1.3.1
|
|
78
|
+
|
|
79
|
+
### Patch Changes
|
|
80
|
+
|
|
81
|
+
- Add optional bounded worker-owned module-record recovery. Configure ownership in
|
|
82
|
+
single-worker and sharded bots without exposing deployment topology to modules
|
|
83
|
+
or relaxing ordinary record-query filters.
|
|
84
|
+
- Updated dependencies
|
|
85
|
+
- Updated dependencies
|
|
86
|
+
- @helyx/database@2.3.0
|
|
87
|
+
- @helyx/sdk@0.14.0
|
|
88
|
+
- @helyx/platform@1.2.0
|
|
89
|
+
- @helyx/module-automod@1.0.2
|
|
90
|
+
- @helyx/module-moderation@1.0.2
|
|
91
|
+
|
|
3
92
|
## 1.3.0
|
|
4
93
|
|
|
5
94
|
### Minor Changes
|
|
@@ -420,34 +509,9 @@
|
|
|
420
509
|
|
|
421
510
|
### Patch Changes
|
|
422
511
|
|
|
423
|
-
- Reuse active Verification links when available, report their expiry, and allow members to replace them while atomically invalidating the old request
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
"@helyx/sdk": patch
|
|
427
|
-
"@helyx/verification-contracts": patch
|
|
428
|
-
---
|
|
429
|
-
|
|
430
|
-
Reuse active Verification links when available, report their expiry, and allow members to replace them while atomically invalidating the old request.
|
|
431
|
-
|
|
432
|
-
- Accept compatible internal Helyx releases within explicit minor-safe pre-1.0 or major-safe stable ranges, preventing dependency-only patch cascades.---
|
|
433
|
-
"@helyx/bot": patch
|
|
434
|
-
"@helyx/cli": patch
|
|
435
|
-
"@helyx/core": patch
|
|
436
|
-
"@helyx/framework": patch
|
|
437
|
-
"@helyx/platform": patch
|
|
438
|
-
"@helyx/sdk": patch
|
|
439
|
-
"@helyx/module-account-gate": patch
|
|
440
|
-
"@helyx/module-feedback": patch
|
|
441
|
-
"@helyx/module-giveaways": patch
|
|
442
|
-
"@helyx/module-levels": patch
|
|
443
|
-
"@helyx/module-logging": patch
|
|
444
|
-
"@helyx/module-polls": patch
|
|
445
|
-
"@helyx/module-reactionroles": patch
|
|
446
|
-
"@helyx/module-suggestions": patch
|
|
447
|
-
"@helyx/module-welcome": patch
|
|
448
|
-
---
|
|
449
|
-
|
|
450
|
-
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.
|
|
451
515
|
|
|
452
516
|
## 0.4.23
|
|
453
517
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Helyx bot
|
|
2
2
|
|
|
3
|
-
The Helyx Discord runtime validates its environment
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
69
|
-
Guess
|
|
70
|
-
|
|
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.
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>;
|
package/dist/cloud-connector.js
CHANGED
|
@@ -87,7 +87,8 @@ export class CloudConnectorController {
|
|
|
87
87
|
#pendingSupport = new Map();
|
|
88
88
|
constructor(input) {
|
|
89
89
|
this.input = input;
|
|
90
|
-
this.#repository =
|
|
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 =
|
|
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(() =>
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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 =
|
|
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) {
|
package/dist/control-errors.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/control-errors.js
CHANGED
|
@@ -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
|