@helyx/bot 1.1.0 → 1.2.1

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 (85) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +16 -1
  3. package/dist/auto-moderation-scanner-registry.d.ts +27 -0
  4. package/dist/auto-moderation-scanner-registry.js +172 -0
  5. package/dist/cloud-connector.js +3 -0
  6. package/dist/cloud-control-dispatcher.js +32 -0
  7. package/dist/command-path-contract.js +1 -0
  8. package/dist/control-permissions.d.ts +2 -2
  9. package/dist/control-permissions.js +4 -2
  10. package/dist/control-server.d.ts +72 -16
  11. package/dist/control-server.js +150 -250
  12. package/dist/dashboard-action-control.d.ts +25 -0
  13. package/dist/dashboard-action-control.js +73 -0
  14. package/dist/dashboard-action-routes.d.ts +9 -0
  15. package/dist/dashboard-action-routes.js +36 -0
  16. package/dist/dashboard-feature-control.d.ts +37 -0
  17. package/dist/dashboard-feature-control.js +206 -0
  18. package/dist/dashboard-module-projection.d.ts +9 -0
  19. package/dist/dashboard-module-projection.js +15 -0
  20. package/dist/dashboard-preview-import-routes.d.ts +6 -0
  21. package/dist/dashboard-preview-import-routes.js +121 -0
  22. package/dist/discord-analytics.d.ts +3 -0
  23. package/dist/discord-analytics.js +49 -5
  24. package/dist/discord-auto-moderation-service.d.ts +21 -0
  25. package/dist/discord-auto-moderation-service.js +425 -0
  26. package/dist/discord-guild-health.d.ts +2 -2
  27. package/dist/discord-member-enforcement-service.d.ts +26 -0
  28. package/dist/discord-member-enforcement-service.js +590 -0
  29. package/dist/discord-message-enforcement-service.d.ts +9 -0
  30. package/dist/discord-message-enforcement-service.js +102 -0
  31. package/dist/discord-violation-thread-service.d.ts +33 -0
  32. package/dist/discord-violation-thread-service.js +789 -0
  33. package/dist/ecko-evidence-receiver.d.ts +32 -0
  34. package/dist/ecko-evidence-receiver.js +173 -0
  35. package/dist/ecko-transport-auth.d.ts +19 -0
  36. package/dist/ecko-transport-auth.js +80 -0
  37. package/dist/ecko-transport-routes.d.ts +19 -0
  38. package/dist/ecko-transport-routes.js +130 -0
  39. package/dist/embedded-auto-moderation-pool.d.ts +46 -0
  40. package/dist/embedded-auto-moderation-pool.js +324 -0
  41. package/dist/embedded-auto-moderation-scanner.d.ts +34 -0
  42. package/dist/embedded-auto-moderation-scanner.js +263 -0
  43. package/dist/embedded-auto-moderation-worker-runtime.d.ts +37 -0
  44. package/dist/embedded-auto-moderation-worker-runtime.js +37 -0
  45. package/dist/embedded-auto-moderation-worker.d.ts +2 -0
  46. package/dist/embedded-auto-moderation-worker.js +13 -0
  47. package/dist/events.d.ts +5 -1
  48. package/dist/events.js +243 -4
  49. package/dist/guilds.d.ts +2 -2
  50. package/dist/guilds.js +14 -8
  51. package/dist/hosted-server-moderation.d.ts +16 -5
  52. package/dist/hosted-server-moderation.js +32 -4
  53. package/dist/index.d.ts +1 -1
  54. package/dist/index.js +84 -14
  55. package/dist/moderation-action-registry.d.ts +11 -0
  56. package/dist/moderation-action-registry.js +375 -0
  57. package/dist/moderation-case-disassociation.d.ts +13 -0
  58. package/dist/moderation-case-disassociation.js +83 -0
  59. package/dist/moderation-platform-services.d.ts +27 -0
  60. package/dist/moderation-platform-services.js +50 -0
  61. package/dist/modules.d.ts +6 -0
  62. package/dist/modules.js +65 -3
  63. package/dist/privileged-message-metadata-cache.d.ts +35 -0
  64. package/dist/privileged-message-metadata-cache.js +70 -0
  65. package/dist/scheduled-task-runner.js +24 -1
  66. package/dist/shard-contracts.d.ts +151 -1
  67. package/dist/shard-contracts.js +142 -1
  68. package/dist/shard-coordinator.d.ts +1 -1
  69. package/dist/shard-ecko-scanner-registry.d.ts +38 -0
  70. package/dist/shard-ecko-scanner-registry.js +96 -0
  71. package/dist/shard-router.d.ts +23 -0
  72. package/dist/shard-router.js +69 -5
  73. package/dist/shard-supervisor.d.ts +11 -0
  74. package/dist/shard-supervisor.js +67 -4
  75. package/dist/shard-worker.d.ts +15 -1
  76. package/dist/shard-worker.js +95 -17
  77. package/dist/transient-violation-evidence.d.ts +39 -0
  78. package/dist/transient-violation-evidence.js +132 -0
  79. package/documentation/README.md +2 -0
  80. package/documentation/examples/.env.example +12 -0
  81. package/documentation/guides/self-hosting.md +17 -1
  82. package/documentation/modules/auto-moderation.md +51 -0
  83. package/documentation/modules/moderation.md +42 -0
  84. package/documentation/reference/environment.md +11 -0
  85. package/package.json +17 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,76 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Load the embedded Auto Moderation engine only for a self-hosted runtime with its
8
+ scanner provider installed. Core-only and public-module installations no longer
9
+ require the optional Testing module to import or start the bot. Refresh the
10
+ self-host catalogue to reference the corrected bot release.
11
+
12
+ ## 1.2.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 00d33b3: Add the Testing Moderation and Auto Moderation modules, including accountable
17
+ staff cases, private violation threads, Discord-native rule ownership, bounded
18
+ rule imports, deterministic repeat-offender actions and shared Hosted and
19
+ Self-Hosted Enhanced matching. Add the private Helyx Ecko transport, operational
20
+ gate, aggregate health reporting, content-free scanner telemetry and portable
21
+ Self-Hosted composition while preserving Standard native enforcement as the
22
+ failure fallback.
23
+
24
+ ### Patch Changes
25
+
26
+ - Updated dependencies [00d33b3]
27
+ - @helyx/config@1.2.0
28
+ - @helyx/control-protocol@0.5.0
29
+ - @helyx/database@2.2.0
30
+ - @helyx/module-manifest@0.10.0
31
+ - @helyx/platform@1.1.0
32
+ - @helyx/sdk@0.12.0
33
+ - @helyx/status-client@0.2.0
34
+ - @helyx/module-automod@1.0.1
35
+ - @helyx/module-moderation@1.0.1
36
+
37
+ ## Unreleased
38
+
39
+ ### Minor Changes
40
+
41
+ - Add deployment-neutral Moderation and Auto Moderation adapters, closed
42
+ provider registries, feature-scoped message event handling, and bounded
43
+ dashboard preview/import feature gates for the Phase 6 module boundary.
44
+ - Replace the unused action-v1 and public staff-thread drafts with policy-owned
45
+ action v2, bounded demotion, role-gated private violation threads and a
46
+ memory-only single-delivery evidence path.
47
+ - Keep enhanced scanning local to the embedded adapter for self-hosted
48
+ deployments and explicitly bind hosted scanner calls to the Ecko adapter.
49
+
50
+ ## 1.1.2
51
+
52
+ ### Patch Changes
53
+
54
+ - 707b68c: Keep hosted server owner observation, manual refresh, and admin leave delivery available independently of the hosted moderation incident gate.
55
+ - Preserve successful support-notification acknowledgements across child-process IPC so delivered Discord notifications are not retried and falsely marked as failed.
56
+
57
+ ## 1.1.1
58
+
59
+ ### Patch Changes
60
+
61
+ - Bound Discord thread inspection to the scheduled-task deadline, reuse recent
62
+ active-thread snapshots, allow existing Analytics filter exclusions to be
63
+ replaced atomically, and remove duplicate dashboard columns that contributed to
64
+ mobile overflow.
65
+ - Expose the hosted-moderation runtime flag through the authenticated bot control
66
+ boundary so Admin incident-gate availability cannot report a disabled runtime
67
+ as enabled.
68
+ - Deliver Admin leave and blacklist requests immediately through the owning bot
69
+ shard while retaining their durable retry queue, and persist cached Discord
70
+ owner usernames atomically with hosted server reconciliation. Add an audited
71
+ Admin fallback that refreshes a missing owner username through the owning bot
72
+ shard and saves the observation.
73
+
3
74
  ## 1.1.0
4
75
 
5
76
  ### Minor Changes
package/README.md CHANGED
@@ -34,7 +34,22 @@ upgrades preserve each server's existing enabled or disabled state.
34
34
 
35
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.
36
36
 
37
- 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. Only `/health` is unauthenticated; all `/v1` routes require the shared bearer secret. Keep this endpoint on a private service network and do not assign it a public domain.
37
+ Moderation integrations use closed, versioned service contracts for native
38
+ Discord Auto Moderation rules, member and message enforcement, restricted
39
+ private violation threads, transient evidence and module-owned action/policy
40
+ providers. Auto Moderation supplies the live rule policy and Moderation owns the
41
+ effect; the caller cannot choose an offence step or action. Feature-scoped
42
+ privileged message listeners are excluded from the hosted main bot; self-hosted
43
+ runtimes may compose the same scanning contract through the explicitly selected
44
+ embedded adapter when the module feature is available. Hosted Ecko transport and
45
+ embedded callers are identity-bound and cannot be interchanged.
46
+
47
+ Dashboard settings previews and managed-resource imports are bounded,
48
+ non-persisting module operations. Core validates their declared inputs and
49
+ fresh feature access before execution, then validates and projects structured
50
+ results before they cross the control boundary.
51
+
52
+ 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.
38
53
 
39
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.
40
55
 
@@ -0,0 +1,27 @@
1
+ import type { AutoModerationEnhancedResultOutcome, AutoModerationScannerAdapterSelection, AutoModerationScannerConfigurationPageV1, AutoModerationScannerProviderRegistryService, ModuleDefinition, ServiceAccess } from "@helyx/sdk";
2
+ import { AUTOMOD_ENHANCED_CONTRACT_VERSION, AUTOMOD_ENHANCED_PROVIDER_ID } from "@helyx/sdk";
3
+ import type { TransientViolationEvidenceStore } from "./transient-violation-evidence.js";
4
+ /** Closed scanner registry local to an unsharded bot or one guild-owning worker. */
5
+ export declare class BotAutoModerationScannerRegistry implements AutoModerationScannerProviderRegistryService {
6
+ #private;
7
+ private readonly services;
8
+ private readonly runtimeProfile;
9
+ private readonly evidenceStore?;
10
+ private readonly deploymentEnvironment;
11
+ constructor(modules: readonly ModuleDefinition[], services: ServiceAccess, runtimeProfile: "hosted" | "self-hosted", evidenceStore?: TransientViolationEvidenceStore | undefined, deploymentEnvironment?: string);
12
+ select(input: {
13
+ guildId: string;
14
+ moduleId: string;
15
+ featureId: string;
16
+ }): Promise<AutoModerationScannerAdapterSelection>;
17
+ getConfigurationPage(input: {
18
+ adapter: "hosted_ecko" | "embedded";
19
+ providerId: typeof AUTOMOD_ENHANCED_PROVIDER_ID;
20
+ contractVersion: typeof AUTOMOD_ENHANCED_CONTRACT_VERSION;
21
+ cursor?: string;
22
+ limit: number;
23
+ knownFingerprint?: string;
24
+ }): Promise<AutoModerationScannerConfigurationPageV1>;
25
+ acceptResult(input: Parameters<AutoModerationScannerProviderRegistryService["acceptResult"]>[0]): Promise<AutoModerationEnhancedResultOutcome>;
26
+ }
27
+ //# sourceMappingURL=auto-moderation-scanner-registry.d.ts.map
@@ -0,0 +1,172 @@
1
+ import { HELYX_AUTOMOD_ENHANCED_SCANNING_FEATURE } from "@helyx/module-manifest";
2
+ import { AUTOMOD_ENHANCED_CONFIG_PROTOCOL, AUTOMOD_ENHANCED_CONTRACT_VERSION, AUTOMOD_ENHANCED_EVIDENCE_PROTOCOL, AUTOMOD_ENHANCED_PROVIDER_ID, AUTOMOD_ENHANCED_RESULT_PROTOCOL, AUTOMOD_SCANNER_LIMITS, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
3
+ const AUTOMOD_MODULE_ID = "helyx.automod";
4
+ /** Closed scanner registry local to an unsharded bot or one guild-owning worker. */
5
+ export class BotAutoModerationScannerRegistry {
6
+ services;
7
+ runtimeProfile;
8
+ evidenceStore;
9
+ deploymentEnvironment;
10
+ #provider;
11
+ constructor(modules, services, runtimeProfile, evidenceStore, deploymentEnvironment = runtimeProfile === "hosted"
12
+ ? "production"
13
+ : "self-hosted") {
14
+ this.services = services;
15
+ this.runtimeProfile = runtimeProfile;
16
+ this.evidenceStore = evidenceStore;
17
+ this.deploymentEnvironment = deploymentEnvironment;
18
+ const providers = modules.flatMap((module) => module.autoModerationScannerProvider
19
+ ? [
20
+ {
21
+ moduleId: module.manifest.id,
22
+ provider: module.autoModerationScannerProvider,
23
+ },
24
+ ]
25
+ : []);
26
+ if (providers.length > 1)
27
+ throw new Error("Auto Moderation scanner provider ownership is ambiguous");
28
+ const registered = providers[0];
29
+ if (!registered) {
30
+ this.#provider = null;
31
+ return;
32
+ }
33
+ if (registered.moduleId !== AUTOMOD_MODULE_ID ||
34
+ registered.provider.providerId !== AUTOMOD_ENHANCED_PROVIDER_ID ||
35
+ registered.provider.contractVersion !==
36
+ AUTOMOD_ENHANCED_CONTRACT_VERSION ||
37
+ registered.provider.configurationProtocol !==
38
+ AUTOMOD_ENHANCED_CONFIG_PROTOCOL ||
39
+ registered.provider.resultProtocol !== AUTOMOD_ENHANCED_RESULT_PROTOCOL ||
40
+ registered.provider.evidenceProtocol !==
41
+ AUTOMOD_ENHANCED_EVIDENCE_PROTOCOL)
42
+ throw new Error("Auto Moderation scanner provider registration is invalid");
43
+ this.#provider = registered.provider;
44
+ }
45
+ async select(input) {
46
+ if (!this.#provider ||
47
+ input.moduleId !== AUTOMOD_MODULE_ID ||
48
+ input.featureId !== HELYX_AUTOMOD_ENHANCED_SCANNING_FEATURE)
49
+ return unavailable("dependency_unhealthy");
50
+ try {
51
+ if (!(await this.#moduleEnabled(input.guildId)))
52
+ return unavailable("dependency_unhealthy");
53
+ const access = await this.services
54
+ .get(HELYX_SERVICE_NAMES.featureAccess)
55
+ .check(input);
56
+ if (access.decision !== "granted")
57
+ return unavailable(access.decision === "not_entitled"
58
+ ? "feature_unavailable"
59
+ : access.decision);
60
+ return this.runtimeProfile === "hosted"
61
+ ? { adapter: "hosted_ecko", available: true }
62
+ : { adapter: "embedded", available: true };
63
+ }
64
+ catch {
65
+ return unavailable("dependency_unhealthy");
66
+ }
67
+ }
68
+ async getConfigurationPage(input) {
69
+ if (!this.#provider ||
70
+ input.adapter !== expectedAdapter(this.runtimeProfile) ||
71
+ input.providerId !== AUTOMOD_ENHANCED_PROVIDER_ID ||
72
+ input.contractVersion !== AUTOMOD_ENHANCED_CONTRACT_VERSION ||
73
+ !Number.isInteger(input.limit) ||
74
+ input.limit < 1 ||
75
+ input.limit > AUTOMOD_SCANNER_LIMITS.configurationPageServers)
76
+ throw new Error("Auto Moderation scanner configuration request is invalid");
77
+ const page = await this.#provider.getConfigurationPage({ services: this.services }, {
78
+ limit: input.limit,
79
+ ...(input.cursor ? { cursor: input.cursor } : {}),
80
+ ...(input.knownFingerprint
81
+ ? { knownFingerprint: input.knownFingerprint }
82
+ : {}),
83
+ });
84
+ validateConfigurationPage(page);
85
+ return page;
86
+ }
87
+ async acceptResult(input) {
88
+ try {
89
+ if (input.adapter !== expectedAdapter(this.runtimeProfile))
90
+ return rejected("wrong_owner");
91
+ if (!this.#provider ||
92
+ input.providerId !== AUTOMOD_ENHANCED_PROVIDER_ID ||
93
+ input.contractVersion !== AUTOMOD_ENHANCED_CONTRACT_VERSION)
94
+ return rejected("unknown_version");
95
+ let result;
96
+ try {
97
+ result = this.#provider.validateResult(input.result);
98
+ }
99
+ catch {
100
+ return rejected("invalid_envelope");
101
+ }
102
+ if (result.protocol !== AUTOMOD_ENHANCED_RESULT_PROTOCOL ||
103
+ result.providerId !== AUTOMOD_ENHANCED_PROVIDER_ID ||
104
+ result.contractVersion !== AUTOMOD_ENHANCED_CONTRACT_VERSION)
105
+ return rejected("unknown_version");
106
+ if (result.environment !== this.deploymentEnvironment)
107
+ return rejected("wrong_owner");
108
+ const evidenceReference = input.evidenceReference ?? null;
109
+ if (evidenceReference &&
110
+ (evidenceReference.source !== "enhanced_scanner" ||
111
+ evidenceReference.sourceEventId !== result.operationId ||
112
+ evidenceReference.configurationRevision !==
113
+ result.configurationRevision ||
114
+ evidenceReference.guildId !== result.guildId ||
115
+ evidenceReference.channelId !== result.channelId ||
116
+ evidenceReference.messageId !== result.messageId ||
117
+ evidenceReference.authorUserId !== result.authorUserId))
118
+ return rejected("invalid_envelope");
119
+ if (!(await this.#moduleEnabled(result.guildId)))
120
+ return rejected("module_unavailable");
121
+ const access = await this.services
122
+ .get(HELYX_SERVICE_NAMES.featureAccess)
123
+ .check({
124
+ guildId: result.guildId,
125
+ moduleId: AUTOMOD_MODULE_ID,
126
+ featureId: HELYX_AUTOMOD_ENHANCED_SCANNING_FEATURE,
127
+ })
128
+ .catch(() => ({ decision: "dependency_unhealthy" }));
129
+ if (access.decision !== "granted")
130
+ return rejected("feature_unavailable");
131
+ try {
132
+ return await this.#provider.handleResult({ services: this.services, evidenceReference }, result);
133
+ }
134
+ catch {
135
+ return rejected("module_unavailable");
136
+ }
137
+ }
138
+ finally {
139
+ if (input.evidenceReference)
140
+ this.evidenceStore?.discard(input.evidenceReference);
141
+ }
142
+ }
143
+ async #moduleEnabled(guildId) {
144
+ const installation = this.services.get(HELYX_SERVICE_NAMES.installations);
145
+ if (!(await installation.isModuleEnabled(guildId, AUTOMOD_MODULE_ID)))
146
+ return false;
147
+ if (!this.services.has(HELYX_SERVICE_NAMES.moduleAvailability))
148
+ return true;
149
+ return this.services
150
+ .get(HELYX_SERVICE_NAMES.moduleAvailability)
151
+ .isAvailable(AUTOMOD_MODULE_ID);
152
+ }
153
+ }
154
+ function expectedAdapter(runtimeProfile) {
155
+ return runtimeProfile === "hosted" ? "hosted_ecko" : "embedded";
156
+ }
157
+ function validateConfigurationPage(page) {
158
+ if (page.protocol !== AUTOMOD_ENHANCED_CONFIG_PROTOCOL ||
159
+ page.providerId !== AUTOMOD_ENHANCED_PROVIDER_ID ||
160
+ page.contractVersion !== AUTOMOD_ENHANCED_CONTRACT_VERSION ||
161
+ page.items.length > AUTOMOD_SCANNER_LIMITS.configurationPageServers ||
162
+ Buffer.byteLength(JSON.stringify(page), "utf8") >
163
+ AUTOMOD_SCANNER_LIMITS.configurationPageBytes)
164
+ throw new Error("Auto Moderation scanner configuration page is invalid");
165
+ }
166
+ function unavailable(reason) {
167
+ return { adapter: "unavailable", available: false, reason };
168
+ }
169
+ function rejected(code) {
170
+ return { outcome: "rejected", code };
171
+ }
172
+ //# sourceMappingURL=auto-moderation-scanner-registry.js.map
@@ -725,13 +725,16 @@ function connectorCapabilities() {
725
725
  "guild.module.public.read.v1",
726
726
  "guild.module.status.write.v1",
727
727
  "guild.module.configuration.write.v1",
728
+ "guild.module.configuration.preview.v1",
728
729
  "guild.module.permissions.write.v1",
729
730
  "guild.module.logging.write.v1",
730
731
  "guild.module.action.execute.v1",
732
+ "guild.module.action.preview.v1",
731
733
  "guild.module.resource.list.v1",
732
734
  "guild.module.resource.read.v1",
733
735
  "guild.module.resource.relation.list.v1",
734
736
  "guild.module.resource.preview.v1",
737
+ "guild.module.resource.import.v1",
735
738
  "guild.module.resource.create.v1",
736
739
  "guild.module.resource.update.v1",
737
740
  "guild.module.resource.delete.v1",
@@ -83,6 +83,15 @@ export class CloudControlDispatcher {
83
83
  });
84
84
  break;
85
85
  }
86
+ case "guild.module.configuration.preview.v1": {
87
+ const input = parseOperationInput(request.operation, request.input);
88
+ result = await this.control.previewConfiguration({
89
+ ...input,
90
+ actorUserId: requiredActor(actorUserId),
91
+ correlationId,
92
+ });
93
+ break;
94
+ }
86
95
  case "guild.module.permissions.write.v1": {
87
96
  const input = parseOperationInput(request.operation, request.input);
88
97
  result = await this.control.updatePermissions({
@@ -104,6 +113,20 @@ export class CloudControlDispatcher {
104
113
  case "guild.module.action.execute.v1": {
105
114
  const input = parseOperationInput(request.operation, request.input);
106
115
  result = await this.control.executeDashboardAction({
116
+ guildId: input.guildId,
117
+ moduleId: input.moduleId,
118
+ actionId: input.actionId,
119
+ ...(input.inspectionRevision === undefined
120
+ ? {}
121
+ : { inspectionRevision: input.inspectionRevision }),
122
+ actorUserId: requiredActor(actorUserId),
123
+ correlationId,
124
+ });
125
+ break;
126
+ }
127
+ case "guild.module.action.preview.v1": {
128
+ const input = parseOperationInput(request.operation, request.input);
129
+ result = await this.control.previewDashboardAction({
107
130
  ...input,
108
131
  actorUserId: requiredActor(actorUserId),
109
132
  correlationId,
@@ -160,6 +183,15 @@ export class CloudControlDispatcher {
160
183
  });
161
184
  break;
162
185
  }
186
+ case "guild.module.resource.import.v1": {
187
+ const input = parseOperationInput(request.operation, request.input);
188
+ result = await this.control.importManagedResource({
189
+ ...input,
190
+ actorUserId: requiredActor(actorUserId),
191
+ correlationId,
192
+ });
193
+ break;
194
+ }
163
195
  case "guild.module.resource.create.v1": {
164
196
  const input = parseOperationInput(request.operation, request.input);
165
197
  result = await this.control.createManagedResource({
@@ -32,6 +32,7 @@ export function collectCommandPermissionTargets(module) {
32
32
  description: command.description,
33
33
  kind: "command",
34
34
  })),
35
+ ...permissionOperations(module.permissionOperations),
35
36
  ...(module.interactions?.commands ?? []).flatMap((command) => [
36
37
  ...permissionOperations(command.permissionOperations),
37
38
  ...(command.subcommands ?? []).flatMap((subcommand) => permissionOperations(subcommand.permissionOperations)),
@@ -23,9 +23,9 @@ export interface ResolvedPermissionPolicy extends PermissionPolicyTarget {
23
23
  version: bigint | null;
24
24
  inherited: boolean;
25
25
  }
26
- export type ManagedResourceOperation = "list" | "read" | "preview" | "create" | "update" | "delete" | "action";
26
+ export type ManagedResourceOperation = "list" | "read" | "preview" | "create" | "update" | "delete" | "action" | "import";
27
27
  export declare function declaredManagedResourcePermission(module: ModuleDefinition, resourceType: string, operation: ManagedResourceOperation): string | undefined;
28
- export declare function dashboardActionPermissionId(module: ModuleDefinition, actionId: string): string;
28
+ export declare function dashboardActionPermissionId(module: ModuleDefinition, actionId: string, declaredPermissionId?: string): string;
29
29
  export declare function managedResourcePermissionId(module: ModuleDefinition, resourceType: string, operation: ManagedResourceOperation, actionId?: string, declaredPermissionId?: string): string;
30
30
  export declare function permissionPolicyKey(target: PermissionPolicyTarget): string;
31
31
  export declare function toDashboardPolicy(policy: PermissionPolicy): DashboardPermissionPolicy;
@@ -4,7 +4,9 @@ export function declaredManagedResourcePermission(module, resourceType, operatio
4
4
  return undefined;
5
5
  return module.manifest.dashboard?.resources?.find(({ id }) => id === resourceType)?.permissions?.[operation];
6
6
  }
7
- export function dashboardActionPermissionId(module, actionId) {
7
+ export function dashboardActionPermissionId(module, actionId, declaredPermissionId) {
8
+ if (declaredPermissionId)
9
+ return declaredPermissionId;
8
10
  const namespace = modulePermissionNamespace(module);
9
11
  return selectDeclaredPermission(module, [
10
12
  `${namespace}.${actionId.replaceAll("-", ".")}`,
@@ -17,7 +19,7 @@ export function managedResourcePermissionId(module, resourceType, operation, act
17
19
  return declaredPermissionId;
18
20
  const namespace = modulePermissionNamespace(module);
19
21
  const resourcePermission = `resource.${resourceType}`;
20
- const candidates = operation === "create" || operation === "preview"
22
+ const candidates = operation === "create" || operation === "preview" || operation === "import"
21
23
  ? [
22
24
  `${resourcePermission}.create`,
23
25
  `${namespace}.create`,
@@ -1,5 +1,5 @@
1
1
  import { type DatabasePool, type GuildShardOwnership } from "@helyx/database";
2
- import { type DashboardActionResult, type GuildAccessPolicy, type ManagedResourceCreationIntent, type ManagedResourceDetail, type ManagedResourcePage, type ManagedResourceRelationPage, type ManagedResourceValue, type MessageResponse, type ModuleDefinition, type OAuthConsumerCleanupResult, type PermissionActor, type PrivacyCapturePage, type PrivacyErasePage, type PrivacyProviderExecutor, type PublicAchievementProjectionPage, type ServiceAccess } from "@helyx/sdk";
2
+ import { type DashboardActionInspection, type DashboardActionResult, type FeatureAccessDecision, type GuildAccessPolicy, type ManagedResourceCreationIntent, type ManagedResourceDetail, type ManagedResourcePage, type ManagedResourceRelationPage, type ManagedResourceValue, type MessageResponse, type ModuleDefinition, type OAuthConsumerCleanupResult, type PermissionActor, type PrivacyCapturePage, type PrivacyErasePage, type PrivacyProviderExecutor, type PublicAchievementProjectionPage, type ServiceAccess } from "@helyx/sdk";
3
3
  import { type ActionReceipt, type SignedAssessmentEnvelope } from "@helyx/verification-contracts";
4
4
  import { type Client } from "discord.js";
5
5
  import { type FastifyInstance } from "fastify";
@@ -10,6 +10,12 @@ import { type DashboardPermissionState } from "./control-permissions.js";
10
10
  import { type DashboardModuleDefinition } from "./dashboard-module-projection.js";
11
11
  import { type AvailabilityModuleCatalogue } from "./module-availability-control.js";
12
12
  import { registerGuildCommandControlRoutes } from "./guild-command-control-routes.js";
13
+ import { type DashboardActionInput, type DashboardActionPreviewInput } from "./dashboard-action-control.js";
14
+ import { type DashboardActionHandler } from "./dashboard-action-routes.js";
15
+ import { type EckoTelemetrySink } from "./ecko-transport-routes.js";
16
+ import type { EckoEvidenceReceiver } from "./ecko-evidence-receiver.js";
17
+ import type { EckoTransportAuthenticator } from "./ecko-transport-auth.js";
18
+ import type { AutoModerationScannerProviderRegistryService } from "@helyx/sdk";
13
19
  interface DashboardLoggingPolicy {
14
20
  eventId: string;
15
21
  enabled: boolean;
@@ -93,6 +99,7 @@ export interface BotControlState {
93
99
  channels: BotControlGuildResources["channels"];
94
100
  roles: BotControlGuildResources["roles"];
95
101
  modules: DashboardModuleDefinition[];
102
+ featureAccess?: Record<string, Record<string, FeatureAccessDecision>>;
96
103
  moduleStatuses: Record<string, "enabled" | "disabled" | "unavailable">;
97
104
  globallyDisabledModuleIds: string[];
98
105
  configurations: Record<string, {
@@ -110,7 +117,7 @@ export interface DashboardGuildAccessState extends GuildAccessPolicy {
110
117
  actorHighestRolePosition: number | null;
111
118
  updatedAt: string | null;
112
119
  }
113
- export interface BotControlHandler extends FirstTimeCommandSetupHandler {
120
+ export interface BotControlHandler extends FirstTimeCommandSetupHandler, DashboardActionHandler {
114
121
  cleanupOAuthConsumer(input: {
115
122
  consumerBindingId: string;
116
123
  moduleId: string;
@@ -176,6 +183,14 @@ export interface BotControlHandler extends FirstTimeCommandSetupHandler {
176
183
  value: Record<string, unknown>;
177
184
  version: number;
178
185
  }>;
186
+ previewConfiguration(input: {
187
+ guildId: string;
188
+ moduleId: string;
189
+ value: Record<string, unknown>;
190
+ input: Readonly<Record<string, ManagedResourceValue>>;
191
+ actorUserId: string;
192
+ correlationId: string;
193
+ }): Promise<MessageResponse>;
179
194
  updatePermissions(input: {
180
195
  guildId: string;
181
196
  moduleId: string;
@@ -192,13 +207,6 @@ export interface BotControlHandler extends FirstTimeCommandSetupHandler {
192
207
  actorUserId: string;
193
208
  correlationId: string;
194
209
  }): Promise<DashboardLoggingPolicy>;
195
- executeDashboardAction(input: {
196
- guildId: string;
197
- moduleId: string;
198
- actionId: string;
199
- actorUserId: string;
200
- correlationId: string;
201
- }): Promise<DashboardActionResult>;
202
210
  listManagedResources(input: {
203
211
  guildId: string;
204
212
  moduleId: string;
@@ -239,6 +247,18 @@ export interface BotControlHandler extends FirstTimeCommandSetupHandler {
239
247
  actorUserId: string;
240
248
  correlationId: string;
241
249
  }): Promise<MessageResponse>;
250
+ importManagedResource(input: {
251
+ guildId: string;
252
+ moduleId: string;
253
+ resourceType: string;
254
+ format: "utf8_text";
255
+ content: string;
256
+ actorUserId: string;
257
+ correlationId: string;
258
+ }): Promise<{
259
+ value: Readonly<Record<string, ManagedResourceValue>>;
260
+ preview: Readonly<Record<string, ManagedResourceValue>>;
261
+ }>;
242
262
  createManagedResource(input: {
243
263
  guildId: string;
244
264
  moduleId: string;
@@ -413,6 +433,14 @@ export declare class BotControlService implements BotControlHandler {
413
433
  value: Record<string, unknown>;
414
434
  version: number;
415
435
  }>;
436
+ previewConfiguration(input: {
437
+ guildId: string;
438
+ moduleId: string;
439
+ value: Record<string, unknown>;
440
+ input: Readonly<Record<string, ManagedResourceValue>>;
441
+ actorUserId: string;
442
+ correlationId: string;
443
+ }): Promise<Readonly<Record<string, ManagedResourceValue>>>;
416
444
  updatePermissions(input: {
417
445
  guildId: string;
418
446
  moduleId: string;
@@ -429,13 +457,8 @@ export declare class BotControlService implements BotControlHandler {
429
457
  actorUserId: string;
430
458
  correlationId: string;
431
459
  }): Promise<DashboardLoggingPolicy>;
432
- executeDashboardAction(input: {
433
- guildId: string;
434
- moduleId: string;
435
- actionId: string;
436
- actorUserId: string;
437
- correlationId: string;
438
- }): Promise<DashboardActionResult>;
460
+ executeDashboardAction(input: DashboardActionInput): Promise<DashboardActionResult>;
461
+ previewDashboardAction(input: DashboardActionPreviewInput): Promise<DashboardActionInspection>;
439
462
  listManagedResources(input: {
440
463
  guildId: string;
441
464
  moduleId: string;
@@ -476,6 +499,18 @@ export declare class BotControlService implements BotControlHandler {
476
499
  actorUserId: string;
477
500
  correlationId: string;
478
501
  }): Promise<MessageResponse>;
502
+ importManagedResource(input: {
503
+ guildId: string;
504
+ moduleId: string;
505
+ resourceType: string;
506
+ format: "utf8_text";
507
+ content: string;
508
+ actorUserId: string;
509
+ correlationId: string;
510
+ }): Promise<{
511
+ value: Readonly<Record<string, ManagedResourceValue>>;
512
+ preview: Readonly<Record<string, ManagedResourceValue>>;
513
+ }>;
479
514
  createManagedResource(input: {
480
515
  guildId: string;
481
516
  moduleId: string;
@@ -526,9 +561,30 @@ export declare function createBotControlServer(input: {
526
561
  logger?: boolean | {
527
562
  level: string;
528
563
  };
564
+ runtimeConfiguration?: {
565
+ hostedModerationEnabled: boolean;
566
+ };
529
567
  shardHealth?: () => Promise<ShardHealthSnapshot>;
530
568
  readiness?: () => Promise<boolean>;
569
+ deliverHostedServerLeave?: (input: {
570
+ operationId: string;
571
+ guildId: string;
572
+ }) => Promise<"queued" | "completed" | "not_currently_installed" | "accepted_for_retry" | "requires_review">;
573
+ refreshHostedServerOwner?: (guildId: string) => Promise<{
574
+ username: string;
575
+ observedAt: string;
576
+ }>;
531
577
  guildCommands?: Parameters<typeof registerGuildCommandControlRoutes>[1];
578
+ ecko?: {
579
+ authenticator: EckoTransportAuthenticator;
580
+ registry: AutoModerationScannerProviderRegistryService;
581
+ evidence: EckoEvidenceReceiver;
582
+ gateState(): Promise<{
583
+ state: "enabled" | "draining" | "disabled";
584
+ updatedAt: Date;
585
+ }>;
586
+ telemetry?: EckoTelemetrySink;
587
+ };
532
588
  }): Promise<FastifyInstance>;
533
589
  export declare function mapBotControlError(error: unknown): {
534
590
  status: 400 | 403 | 404 | 409;