@helyx/bot 1.5.2 → 1.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/command-root-catalogue.json +452 -0
  3. package/dist/cached-message-deletion-authority.d.ts +27 -0
  4. package/dist/cached-message-deletion-authority.js +288 -0
  5. package/dist/channel-audit-configuration.d.ts +11 -1
  6. package/dist/channel-audit-configuration.js +28 -6
  7. package/dist/channel-audit-control.js +42 -2
  8. package/dist/channel-audit-permissions.js +76 -3
  9. package/dist/cloud-command-policy-sync.d.ts +26 -0
  10. package/dist/cloud-command-policy-sync.js +198 -0
  11. package/dist/cloud-connector.d.ts +6 -1
  12. package/dist/cloud-connector.js +224 -53
  13. package/dist/cloud-control-dispatcher.js +18 -0
  14. package/dist/command-policy-runtime.d.ts +23 -0
  15. package/dist/command-policy-runtime.js +53 -0
  16. package/dist/command-root-catalogue.d.ts +5 -0
  17. package/dist/command-root-catalogue.js +16 -0
  18. package/dist/control-error-mapping.d.ts +8 -0
  19. package/dist/control-error-mapping.js +60 -0
  20. package/dist/control-errors.d.ts +3 -1
  21. package/dist/control-errors.js +4 -1
  22. package/dist/control-health-routes.d.ts +10 -0
  23. package/dist/control-health-routes.js +28 -0
  24. package/dist/control-server-schemas.d.ts +7 -0
  25. package/dist/control-server-schemas.js +9 -0
  26. package/dist/control-server.d.ts +27 -6
  27. package/dist/control-server.js +103 -122
  28. package/dist/dashboard-feature-control.d.ts +2 -1
  29. package/dist/dashboard-feature-control.js +1 -0
  30. package/dist/dashboard-logging.d.ts +20 -0
  31. package/dist/dashboard-logging.js +39 -0
  32. package/dist/dashboard-module-projection.d.ts +1 -1
  33. package/dist/dashboard-module-projection.js +5 -2
  34. package/dist/dashboard-preview-import-routes.js +8 -0
  35. package/dist/database-bootstrap.d.ts +1 -1
  36. package/dist/database-bootstrap.js +4 -3
  37. package/dist/discord-channel-audit.js +1 -0
  38. package/dist/discord-user-identities.d.ts +14 -0
  39. package/dist/discord-user-identities.js +64 -0
  40. package/dist/emergency-deletion-dispatcher.d.ts +68 -0
  41. package/dist/emergency-deletion-dispatcher.js +502 -0
  42. package/dist/emergency-deletion-rest-transport.d.ts +24 -0
  43. package/dist/emergency-deletion-rest-transport.js +253 -0
  44. package/dist/events.d.ts +5 -0
  45. package/dist/events.js +53 -10
  46. package/dist/guarded-discord-dispatcher.d.ts +29 -0
  47. package/dist/guarded-discord-dispatcher.js +204 -0
  48. package/dist/guild-command-discord-client.d.ts +2 -0
  49. package/dist/guild-command-discord-client.js +7 -0
  50. package/dist/guild-command-projection.d.ts +4 -1
  51. package/dist/guild-command-projection.js +25 -0
  52. package/dist/guild-command-read-service.d.ts +10 -1
  53. package/dist/guild-command-read-service.js +37 -0
  54. package/dist/guild-command-worker.d.ts +19 -4
  55. package/dist/guild-command-worker.js +356 -26
  56. package/dist/guild-message-protection-author-roles.d.ts +7 -0
  57. package/dist/guild-message-protection-author-roles.js +93 -0
  58. package/dist/guild-message-protection-deletion.d.ts +21 -0
  59. package/dist/guild-message-protection-deletion.js +138 -0
  60. package/dist/guild-message-protection-delivery.d.ts +41 -0
  61. package/dist/guild-message-protection-delivery.js +223 -0
  62. package/dist/guild-message-update-metadata.d.ts +32 -0
  63. package/dist/guild-message-update-metadata.js +255 -0
  64. package/dist/guilds.d.ts +2 -1
  65. package/dist/guilds.js +11 -1
  66. package/dist/hosted-server-moderation.d.ts +2 -2
  67. package/dist/index.js +68 -4
  68. package/dist/interactions.d.ts +3 -1
  69. package/dist/interactions.js +77 -12
  70. package/dist/invite-rollup-maintenance.d.ts +9 -0
  71. package/dist/invite-rollup-maintenance.js +12 -0
  72. package/dist/managed-resource-authorization.d.ts +15 -0
  73. package/dist/managed-resource-authorization.js +37 -0
  74. package/dist/managed-resource-create-defaults.d.ts +21 -0
  75. package/dist/managed-resource-create-defaults.js +83 -0
  76. package/dist/managed-resource-query.d.ts +6 -0
  77. package/dist/managed-resource-query.js +15 -0
  78. package/dist/member-role-ids.d.ts +17 -0
  79. package/dist/member-role-ids.js +66 -0
  80. package/dist/message-protection-admission.d.ts +60 -0
  81. package/dist/message-protection-admission.js +279 -0
  82. package/dist/message-protection-cold-data.d.ts +42 -0
  83. package/dist/message-protection-cold-data.js +180 -0
  84. package/dist/message-protection-coordinator.d.ts +73 -0
  85. package/dist/message-protection-coordinator.js +622 -0
  86. package/dist/message-protection-settings.d.ts +40 -0
  87. package/dist/message-protection-settings.js +275 -0
  88. package/dist/message-protection-summary.d.ts +8 -0
  89. package/dist/message-protection-summary.js +98 -0
  90. package/dist/module-availability-control.js +20 -12
  91. package/dist/module-logging.d.ts +3 -2
  92. package/dist/module-logging.js +6 -1
  93. package/dist/modules.js +56 -1
  94. package/dist/mysql-runtime-persistence.d.ts +1 -0
  95. package/dist/mysql-runtime-persistence.js +4 -1
  96. package/dist/nexus-command-recovery-control-routes.d.ts +5 -0
  97. package/dist/nexus-command-recovery-control-routes.js +48 -0
  98. package/dist/nexus-command-references.d.ts +6 -0
  99. package/dist/nexus-command-references.js +33 -0
  100. package/dist/scheduled-task-runner.js +23 -4
  101. package/dist/shard-command-startup.d.ts +13 -0
  102. package/dist/shard-command-startup.js +27 -0
  103. package/dist/shard-contracts.d.ts +84 -0
  104. package/dist/shard-contracts.js +33 -0
  105. package/dist/shard-core-readiness.d.ts +5 -0
  106. package/dist/shard-core-readiness.js +19 -0
  107. package/dist/shard-router.d.ts +3 -0
  108. package/dist/shard-router.js +10 -1
  109. package/dist/shard-supervisor.d.ts +1 -1
  110. package/dist/shard-supervisor.js +124 -43
  111. package/dist/shard-worker.js +25 -15
  112. package/documentation/README.md +1 -0
  113. package/documentation/examples/.env.example +0 -45
  114. package/documentation/guides/self-hosting.md +54 -0
  115. package/package.json +18 -17
@@ -0,0 +1,40 @@
1
+ import type { BoundedModuleConfigurationReader } from "@helyx/database-contracts";
2
+ import type { ModuleDefinition } from "@helyx/sdk";
3
+ import type { MessageProtectionColdResult } from "./message-protection-cold-data.js";
4
+ type Observation = Extract<MessageProtectionColdResult, {
5
+ kind: "observed";
6
+ }>;
7
+ type Reason = "unsupported-module" | "metadata-mismatch" | "missing" | "oversized" | "read-failed" | "invalid-settings" | "stale";
8
+ export type MessageProtectionSettingsResult = Readonly<{
9
+ kind: "unavailable";
10
+ reason: Reason;
11
+ }> | Readonly<{
12
+ kind: "no-settings";
13
+ revisionFingerprint: string;
14
+ }> | Readonly<{
15
+ kind: "validated";
16
+ revisionFingerprint: string;
17
+ configuration: Readonly<Record<string, unknown>>;
18
+ }>;
19
+ export interface MessageProtectionSettingsLoader {
20
+ read(data: Observation): Promise<MessageProtectionSettingsResult>;
21
+ isCurrent(scope: Readonly<{
22
+ moduleId: string;
23
+ }>): boolean;
24
+ }
25
+ interface Bounds {
26
+ maximumBytes: number;
27
+ maximumDepth: number;
28
+ maximumNodes: number;
29
+ maximumCollectionEntries: number;
30
+ maximumStringLength: number;
31
+ }
32
+ /** Private cold-path observation only; never writes, calls validateUpdate or grants admission. */
33
+ export declare function createMessageProtectionSettingsLoader(options: {
34
+ modules: readonly ModuleDefinition[];
35
+ configurations: BoundedModuleConfigurationReader;
36
+ bounds: Bounds;
37
+ now?: () => number;
38
+ }): MessageProtectionSettingsLoader;
39
+ export {};
40
+ //# sourceMappingURL=message-protection-settings.d.ts.map
@@ -0,0 +1,275 @@
1
+ import { createHash } from "node:crypto";
2
+ /** Private cold-path observation only; never writes, calls validateUpdate or grants admission. */
3
+ export function createMessageProtectionSettingsLoader(options) {
4
+ const limits = Object.freeze({ ...options.bounds });
5
+ for (const [key, maximum] of [
6
+ ["maximumBytes", 1_048_576],
7
+ ["maximumDepth", 32],
8
+ ["maximumNodes", 100_000],
9
+ ["maximumCollectionEntries", 16_384],
10
+ ["maximumStringLength", 1_048_576],
11
+ ]) {
12
+ if (!Number.isSafeInteger(limits[key]) ||
13
+ limits[key] < 1 ||
14
+ limits[key] > maximum)
15
+ throw new TypeError("Invalid protection settings bounds");
16
+ }
17
+ const sourceModules = options.modules;
18
+ const modules = new Map(sourceModules.map((module, index) => [
19
+ module.manifest.id,
20
+ Object.freeze({
21
+ id: module.manifest.id,
22
+ source: module,
23
+ index,
24
+ manifest: module.manifest,
25
+ contribution: module.configuration,
26
+ version: module.manifest.version,
27
+ schemaVersion: module.configuration?.schemaVersion ?? 1,
28
+ // eslint-disable-next-line @typescript-eslint/unbound-method -- Identity pin; invoked only with the captured contribution receiver via Reflect.apply.
29
+ validate: module.configuration?.validate,
30
+ hasConfiguration: module.configuration !== undefined,
31
+ }),
32
+ ]));
33
+ if (modules.size !== options.modules.length)
34
+ throw new TypeError("Duplicate protection settings module");
35
+ const reader = options.configurations;
36
+ const invalidImplementations = new Set();
37
+ const implementationCurrent = (module) => {
38
+ if (invalidImplementations.has(module.id))
39
+ return false;
40
+ try {
41
+ const valid = sourceModules[module.index] === module.source &&
42
+ module.source.manifest === module.manifest &&
43
+ module.source.manifest.id === module.id &&
44
+ module.source.manifest.version === module.version &&
45
+ module.source.configuration === module.contribution &&
46
+ (module.source.configuration?.schemaVersion ?? 1) ===
47
+ module.schemaVersion &&
48
+ module.source.configuration?.validate === module.validate;
49
+ if (valid)
50
+ return true;
51
+ }
52
+ catch {
53
+ // A failed trusted registry read revokes this implementation witness too.
54
+ }
55
+ invalidImplementations.add(module.id);
56
+ return false;
57
+ };
58
+ const clock = options.now ?? (() => performance.now());
59
+ let lastNow = -Infinity;
60
+ let failed = false;
61
+ const current = (expiresAt) => {
62
+ if (failed)
63
+ return false;
64
+ let now;
65
+ try {
66
+ now = clock();
67
+ }
68
+ catch {
69
+ failed = true;
70
+ return false;
71
+ }
72
+ if (!Number.isFinite(now) ||
73
+ now < 0 ||
74
+ now > Number.MAX_SAFE_INTEGER ||
75
+ now < lastNow) {
76
+ failed = true;
77
+ return false;
78
+ }
79
+ lastNow = now;
80
+ return Number.isFinite(expiresAt) && now < expiresAt;
81
+ };
82
+ return Object.freeze({
83
+ isCurrent(scope) {
84
+ const module = modules.get(scope.moduleId);
85
+ return !failed && module !== undefined && implementationCurrent(module);
86
+ },
87
+ async read(data) {
88
+ const module = modules.get(data.scope.moduleId);
89
+ if (!module)
90
+ return unavailable("unsupported-module");
91
+ if (!implementationCurrent(module))
92
+ return unavailable("metadata-mismatch");
93
+ const scope = Object.freeze({ ...data.scope });
94
+ const installation = data.installation && Object.freeze({ ...data.installation });
95
+ const metadata = data.configuration && Object.freeze({ ...data.configuration });
96
+ const expiresAt = data.freshness.expiresAt;
97
+ if (!current(expiresAt))
98
+ return unavailable("stale");
99
+ if (!installation ||
100
+ installation.installedVersion !== module.version ||
101
+ installation.configurationSchemaVersion !== module.schemaVersion ||
102
+ !Number.isSafeInteger(module.schemaVersion) ||
103
+ module.schemaVersion < 1)
104
+ return unavailable("metadata-mismatch");
105
+ if (!module.hasConfiguration) {
106
+ if (metadata !== null)
107
+ return unavailable("metadata-mismatch");
108
+ if (!implementationCurrent(module))
109
+ return unavailable("metadata-mismatch");
110
+ return Object.freeze({
111
+ kind: "no-settings",
112
+ revisionFingerprint: digest([
113
+ 1,
114
+ scope,
115
+ module.id,
116
+ module.version,
117
+ "none",
118
+ ]),
119
+ });
120
+ }
121
+ if (!metadata)
122
+ return unavailable("missing");
123
+ if (metadata.schemaVersion !== module.schemaVersion ||
124
+ !/^[1-9]\d{0,18}$/u.test(metadata.version) ||
125
+ BigInt(metadata.version) > 9223372036854775807n ||
126
+ !validDate(metadata.updatedAt) ||
127
+ typeof module.validate !== "function")
128
+ return unavailable("metadata-mismatch");
129
+ let stored;
130
+ try {
131
+ stored = await reader.read({
132
+ ...scope,
133
+ maximumBytes: limits.maximumBytes,
134
+ });
135
+ }
136
+ catch {
137
+ return unavailable("read-failed");
138
+ }
139
+ if (!current(expiresAt))
140
+ return unavailable("stale");
141
+ try {
142
+ if (!implementationCurrent(module))
143
+ return unavailable("metadata-mismatch");
144
+ if (stored.guildId !== scope.guildId ||
145
+ stored.moduleId !== scope.moduleId)
146
+ return unavailable("metadata-mismatch");
147
+ if (stored.kind === "missing")
148
+ return unavailable("missing");
149
+ if (stored.kind === "oversized")
150
+ return unavailable("oversized");
151
+ if (stored.kind !== "observed" ||
152
+ stored.version !== metadata.version ||
153
+ stored.schemaVersion !== metadata.schemaVersion ||
154
+ stored.updatedAt !== metadata.updatedAt ||
155
+ !Number.isSafeInteger(stored.storedBytes) ||
156
+ stored.storedBytes < 2 ||
157
+ stored.storedBytes > limits.maximumBytes)
158
+ return unavailable("metadata-mismatch");
159
+ const input = copyPlainConfiguration(stored.configuration, limits, false);
160
+ const value = Reflect.apply(module.validate, module.contribution, [input]);
161
+ if (value instanceof Promise) {
162
+ // A mistaken async validator must not create an unhandled rejection.
163
+ void Promise.prototype.then.call(value, undefined, () => undefined);
164
+ return unavailable("invalid-settings");
165
+ }
166
+ const configuration = copyPlainConfiguration(value, limits);
167
+ if (!current(expiresAt))
168
+ return unavailable("stale");
169
+ if (!implementationCurrent(module))
170
+ return unavailable("metadata-mismatch");
171
+ return Object.freeze({
172
+ kind: "validated",
173
+ configuration,
174
+ revisionFingerprint: digest([
175
+ 1,
176
+ scope,
177
+ module.id,
178
+ module.version,
179
+ metadata,
180
+ configuration,
181
+ ]),
182
+ });
183
+ }
184
+ catch {
185
+ // Parser errors and raw settings never leave this private boundary.
186
+ return unavailable("invalid-settings");
187
+ }
188
+ },
189
+ });
190
+ }
191
+ function unavailable(reason) {
192
+ return Object.freeze({ kind: "unavailable", reason });
193
+ }
194
+ function digest(value) {
195
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
196
+ }
197
+ function validDate(value) {
198
+ if (typeof value !== "string" ||
199
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value))
200
+ return false;
201
+ const date = new Date(value);
202
+ return Number.isFinite(date.getTime()) && date.toISOString() === value;
203
+ }
204
+ function copyPlainConfiguration(value, bounds, freeze = true) {
205
+ let nodes = 0;
206
+ let estimatedBytes = 0;
207
+ const seen = new Set();
208
+ const copy = (input, depth) => {
209
+ if (++nodes > bounds.maximumNodes || depth > bounds.maximumDepth)
210
+ throw new Error("Settings structure exceeds bounds");
211
+ if (input === null || typeof input === "boolean")
212
+ return input;
213
+ if (typeof input === "number") {
214
+ if (!Number.isFinite(input))
215
+ throw new Error("Settings number is invalid");
216
+ return Object.is(input, -0) ? 0 : input;
217
+ }
218
+ if (typeof input === "string") {
219
+ if (input.length > bounds.maximumStringLength)
220
+ throw new Error("Settings string exceeds bounds");
221
+ estimatedBytes += Buffer.byteLength(input, "utf8");
222
+ if (estimatedBytes > bounds.maximumBytes)
223
+ throw new Error("Settings bytes exceed bounds");
224
+ return input;
225
+ }
226
+ if (typeof input !== "object")
227
+ throw new Error("Settings are not JSON");
228
+ if (seen.has(input))
229
+ throw new Error("Settings cycle is invalid");
230
+ const array = Array.isArray(input);
231
+ const prototype = Object.getPrototypeOf(input);
232
+ if (!array && prototype !== null && prototype !== Object.prototype)
233
+ throw new Error("Settings object is not plain");
234
+ seen.add(input);
235
+ const descriptors = Object.getOwnPropertyDescriptors(input);
236
+ const keys = Reflect.ownKeys(descriptors).filter((key) => !(array && key === "length"));
237
+ if (keys.length > bounds.maximumCollectionEntries)
238
+ throw new Error("Settings collection exceeds bounds");
239
+ if (array && keys.length !== input.length)
240
+ throw new Error("Settings arrays must be dense");
241
+ const output = array ? [] : {};
242
+ if (keys.some((key) => typeof key !== "string"))
243
+ throw new Error("Settings symbols are invalid");
244
+ for (const key of keys.sort()) {
245
+ if (typeof key !== "string")
246
+ throw new Error("Settings symbols are invalid");
247
+ const descriptor = descriptors[key];
248
+ if (!descriptor.enumerable || !("value" in descriptor))
249
+ throw new Error("Settings accessors are invalid");
250
+ if (array &&
251
+ (!/^(0|[1-9]\d*)$/u.test(key) || Number(key) >= input.length))
252
+ throw new Error("Settings array property is invalid");
253
+ if (key.length > bounds.maximumStringLength)
254
+ throw new Error("Settings key exceeds bounds");
255
+ estimatedBytes += Buffer.byteLength(key, "utf8");
256
+ if (estimatedBytes > bounds.maximumBytes)
257
+ throw new Error("Settings bytes exceed bounds");
258
+ Object.defineProperty(output, key, {
259
+ value: copy(descriptor.value, depth + 1),
260
+ enumerable: true,
261
+ configurable: !freeze,
262
+ writable: !freeze,
263
+ });
264
+ }
265
+ seen.delete(input);
266
+ return freeze ? Object.freeze(output) : output;
267
+ };
268
+ if (!value || typeof value !== "object" || Array.isArray(value))
269
+ throw new Error("Settings must be an object");
270
+ const result = copy(value, 0);
271
+ if (Buffer.byteLength(JSON.stringify(result), "utf8") > bounds.maximumBytes)
272
+ throw new Error("Settings bytes exceed bounds");
273
+ return result;
274
+ }
275
+ //# sourceMappingURL=message-protection-settings.js.map
@@ -0,0 +1,8 @@
1
+ import type { GuildMessageProtectionSnapshot } from "@helyx/sdk";
2
+ import { type Message } from "discord.js";
3
+ export declare function unavailableProtectionSnapshot(reason: Extract<GuildMessageProtectionSnapshot, {
4
+ available: false;
5
+ }>["reason"]): GuildMessageProtectionSnapshot;
6
+ /** Pure, bounded extraction. The Discord source never leaves this adapter. */
7
+ export declare function summarizeProtectionMessage(message: Pick<Message, "content" | "attachments" | "embeds" | "flags" | "stickers" | "poll" | "partial">): GuildMessageProtectionSnapshot;
8
+ //# sourceMappingURL=message-protection-summary.d.ts.map
@@ -0,0 +1,98 @@
1
+ import { Collection, MessageFlags } from "discord.js";
2
+ import { classifyGuildMessage } from "./message-classification.js";
3
+ export function unavailableProtectionSnapshot(reason) {
4
+ return Object.freeze({ available: false, reason });
5
+ }
6
+ /** Pure, bounded extraction. The Discord source never leaves this adapter. */
7
+ export function summarizeProtectionMessage(message) {
8
+ try {
9
+ if (message.partial)
10
+ return unavailableProtectionSnapshot("source-unavailable");
11
+ if (message.flags.has(MessageFlags.IsComponentsV2))
12
+ return unavailableProtectionSnapshot("unsupported-shape");
13
+ const content = message.content;
14
+ const attachments = message.attachments;
15
+ const embeds = message.embeds;
16
+ const stickers = message.stickers;
17
+ if (typeof content !== "string" ||
18
+ content.length > 4096 ||
19
+ attachments.size > 10 ||
20
+ embeds.length > 10 ||
21
+ stickers.size > 3)
22
+ return unavailableProtectionSnapshot("source-unavailable");
23
+ // Check every string the legacy recognizer can inspect before invoking it.
24
+ for (const attachment of attachments.values()) {
25
+ if (!bounded(attachment.name) || !bounded(attachment.contentType))
26
+ return unavailableProtectionSnapshot("source-unavailable");
27
+ }
28
+ for (const embed of embeds) {
29
+ if (![embed.url, embed.image?.url, embed.thumbnail?.url].every(bounded))
30
+ return unavailableProtectionSnapshot("source-unavailable");
31
+ }
32
+ const legacy = classifyGuildMessage({
33
+ content,
34
+ attachments: new Collection(),
35
+ embeds,
36
+ flags: message.flags,
37
+ stickers,
38
+ });
39
+ const kinds = new Set();
40
+ if (legacy.available)
41
+ for (const kind of legacy.kinds) {
42
+ if (kind === "text" || kind === "emoji")
43
+ kinds.add("text");
44
+ if (kind === "link")
45
+ kinds.add("link-or-embed");
46
+ if (kind === "image")
47
+ kinds.add("image");
48
+ if (kind === "gif" || kind === "sticker")
49
+ kinds.add("gif-or-sticker");
50
+ if (kind === "voice-message")
51
+ kinds.add("voice-message");
52
+ }
53
+ const voice = message.flags.has(MessageFlags.IsVoiceMessage);
54
+ if (!voice)
55
+ for (const attachment of attachments.values()) {
56
+ const type = attachment.contentType?.toLowerCase() ?? "";
57
+ if (type === "image/gif" ||
58
+ attachment.name.toLowerCase().endsWith(".gif"))
59
+ kinds.add("gif-or-sticker");
60
+ else if (type.startsWith("image/"))
61
+ kinds.add("image");
62
+ else if (type.startsWith("video/"))
63
+ kinds.add("video");
64
+ else
65
+ kinds.add("file");
66
+ }
67
+ if (embeds.length)
68
+ kinds.add("link-or-embed");
69
+ if (embeds.some((embed) => Boolean(embed.video)))
70
+ kinds.add("video");
71
+ if (message.poll)
72
+ kinds.add("poll");
73
+ const order = [
74
+ "text",
75
+ "image",
76
+ "video",
77
+ "file",
78
+ "link-or-embed",
79
+ "gif-or-sticker",
80
+ "voice-message",
81
+ "poll",
82
+ ];
83
+ return Object.freeze({
84
+ available: true,
85
+ version: 1,
86
+ kinds: Object.freeze(order.filter((kind) => kinds.has(kind))),
87
+ });
88
+ }
89
+ catch {
90
+ return unavailableProtectionSnapshot("source-unavailable");
91
+ }
92
+ }
93
+ function bounded(value) {
94
+ return (value === null ||
95
+ value === undefined ||
96
+ (typeof value === "string" && value.length <= 4096));
97
+ }
98
+ //# sourceMappingURL=message-protection-summary.js.map
@@ -4,22 +4,30 @@ import { createHash } from "node:crypto";
4
4
  export function availabilityCatalogueDefinitions(modules) {
5
5
  return modules
6
6
  .filter((module) => isCustomerSelectableModule(module.manifest))
7
- .map(({ manifest }) => ({
8
- id: manifest.id,
9
- name: manifest.name,
10
- version: manifest.version,
11
- releaseStatus: manifest.releaseStatus,
12
- }))
7
+ .map(({ manifest }) => {
8
+ if (manifest.releaseStatus === "alpha")
9
+ throw new Error("Alpha modules have no persisted availability projection");
10
+ return {
11
+ id: manifest.id,
12
+ name: manifest.name,
13
+ version: manifest.version,
14
+ releaseStatus: manifest.releaseStatus,
15
+ };
16
+ })
13
17
  .sort((left, right) => left.name.localeCompare(right.name, "en-GB"));
14
18
  }
15
19
  export function statusCatalogueDefinitions(modules) {
16
20
  return modules
17
- .map(({ manifest }) => ({
18
- id: manifest.id,
19
- name: manifest.name,
20
- version: manifest.version,
21
- releaseStatus: manifest.releaseStatus,
22
- }))
21
+ .map(({ manifest }) => {
22
+ if (manifest.releaseStatus === "alpha")
23
+ throw new Error("Alpha modules have no persisted status projection");
24
+ return {
25
+ id: manifest.id,
26
+ name: manifest.name,
27
+ version: manifest.version,
28
+ releaseStatus: manifest.releaseStatus,
29
+ };
30
+ })
23
31
  .sort((left, right) => left.name.localeCompare(right.name, "en-GB"));
24
32
  }
25
33
  function catalogueDigest(definitions) {
@@ -1,9 +1,10 @@
1
- import { InstallationRepository, ModuleConfigurationRepository, ModuleLoggingPolicyRepository, type DatabasePool } from "@helyx/database";
2
- import type { InstallationRepository as MysqlInstallationRepository, ModuleConfigurationRepository as MysqlModuleConfigurationRepository, ModuleLoggingPolicyRepository as MysqlModuleLoggingPolicyRepository } from "@helyx/database-mysql";
1
+ import { InstallationRepository, ModuleAvailabilityRepository, ModuleConfigurationRepository, ModuleLoggingPolicyRepository, type DatabasePool } from "@helyx/database";
2
+ import type { InstallationRepository as MysqlInstallationRepository, ModuleAvailabilityRepository as MysqlModuleAvailabilityRepository, ModuleConfigurationRepository as MysqlModuleConfigurationRepository, ModuleLoggingPolicyRepository as MysqlModuleLoggingPolicyRepository } from "@helyx/database-mysql";
3
3
  import type { ModuleDefinition, ModuleLogDetail, ModuleLoggingService } from "@helyx/sdk";
4
4
  import type { Client } from "discord.js";
5
5
  import { type InteractionLogger } from "./interactions.js";
6
6
  export interface ModuleLoggingRepositories {
7
+ availability: Pick<ModuleAvailabilityRepository, "get"> & Pick<MysqlModuleAvailabilityRepository, "get">;
7
8
  installations: Pick<InstallationRepository, "isModuleEnabled"> & Pick<MysqlInstallationRepository, "isModuleEnabled">;
8
9
  configuration: Pick<ModuleConfigurationRepository, "get"> & Pick<MysqlModuleConfigurationRepository, "get">;
9
10
  policies: Pick<ModuleLoggingPolicyRepository, "get"> & Pick<MysqlModuleLoggingPolicyRepository, "get">;
@@ -1,4 +1,4 @@
1
- import { InstallationRepository, ModuleConfigurationRepository, ModuleLoggingPolicyRepository, } from "@helyx/database";
1
+ import { InstallationRepository, ModuleAvailabilityRepository, ModuleConfigurationRepository, ModuleLoggingPolicyRepository, } from "@helyx/database";
2
2
  import { safeError } from "@helyx/core";
3
3
  import { toReplyOptions } from "./interactions.js";
4
4
  import { createHash } from "node:crypto";
@@ -17,6 +17,7 @@ export class DiscordModuleLoggingService {
17
17
  "installations" in databaseOrRepositories
18
18
  ? databaseOrRepositories
19
19
  : {
20
+ availability: new ModuleAvailabilityRepository(databaseOrRepositories),
20
21
  installations: new InstallationRepository(databaseOrRepositories),
21
22
  configuration: new ModuleConfigurationRepository(databaseOrRepositories),
22
23
  policies: new ModuleLoggingPolicyRepository(databaseOrRepositories),
@@ -33,6 +34,10 @@ export class DiscordModuleLoggingService {
33
34
  if (!source || !event) {
34
35
  throw new Error("Module emitted an undeclared logging event");
35
36
  }
37
+ if ((await this.#repositories.availability.get(LOGGING_MODULE_ID))
38
+ ?.globallyDisabled) {
39
+ return { delivered: false, retryable: false };
40
+ }
36
41
  if (!(await this.#repositories.installations.isModuleEnabled(input.guildId, LOGGING_MODULE_ID))) {
37
42
  return { delivered: false, retryable: false };
38
43
  }
package/dist/modules.js CHANGED
@@ -4,7 +4,7 @@ import { createHmac } from "node:crypto";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import { createRequire } from "node:module";
6
6
  import { dirname } from "node:path";
7
- import { assertCommandPathContract } from "./command-path-contract.js";
7
+ import { assertCommandPathContract, collectExecutableCommandPaths, } from "./command-path-contract.js";
8
8
  export function assertConfiguredModuleDatabaseCompatibility(modules, engine) {
9
9
  for (const { definition } of modules) {
10
10
  if (!getModuleDatabaseEngines(definition.manifest).includes(engine)) {
@@ -133,6 +133,8 @@ async function loadDefinitions(specifiers, importer) {
133
133
  throw new Error(`Module package has no valid default export: ${specifier}`);
134
134
  }
135
135
  definition.manifest = parseModuleManifest(definition.manifest);
136
+ if (definition.manifest.releaseStatus === "alpha")
137
+ throw new Error(`Alpha module cannot be loaded into the runtime: ${specifier}`);
136
138
  validateContributionParity(definition);
137
139
  loaded.push({ specifier, definition });
138
140
  }
@@ -147,6 +149,54 @@ export function validateContributionParity(definition) {
147
149
  requiredDiscordCapabilities.some((capability) => capability !== "guild-members" && capability !== "message-content")) {
148
150
  throw new Error("Module requests an unsupported or duplicate Discord capability");
149
151
  }
152
+ for (const contribution of definition.events?.guildMessageCreate ?? []) {
153
+ const protection = contribution.protection;
154
+ if (protection === undefined)
155
+ continue;
156
+ if (!protection ||
157
+ typeof protection !== "object" ||
158
+ Array.isArray(protection) ||
159
+ (Object.getPrototypeOf(protection) !== Object.prototype &&
160
+ Object.getPrototypeOf(protection) !== null) ||
161
+ protection.version !== 1 ||
162
+ Object.keys(protection).some((key) => ![
163
+ "version",
164
+ "otherBots",
165
+ "webhooks",
166
+ "summary",
167
+ "deleteMessage",
168
+ ].includes(key)) ||
169
+ (protection.otherBots !== undefined &&
170
+ typeof protection.otherBots !== "boolean") ||
171
+ (protection.webhooks !== undefined &&
172
+ typeof protection.webhooks !== "boolean") ||
173
+ (protection.summary !== undefined && protection.summary !== true) ||
174
+ (protection.deleteMessage !== undefined &&
175
+ protection.deleteMessage !== true))
176
+ throw new Error("Module requests unsupported guild message protection options");
177
+ if (protection.summary &&
178
+ !requiredDiscordCapabilities.includes("message-content"))
179
+ throw new Error("Message protection summary requires explicit message-content capability");
180
+ if (protection.deleteMessage &&
181
+ !definition.manifest.permissions.includes("MANAGE_MESSAGES"))
182
+ throw new Error("Message protection deletion requires declared MANAGE_MESSAGES permission");
183
+ }
184
+ for (const contribution of definition.events?.guildMessageUpdateMetadata ??
185
+ []) {
186
+ const metadata = contribution.metadata;
187
+ if (!metadata ||
188
+ typeof metadata !== "object" ||
189
+ Array.isArray(metadata) ||
190
+ (Object.getPrototypeOf(metadata) !== Object.prototype &&
191
+ Object.getPrototypeOf(metadata) !== null) ||
192
+ metadata.version !== 1 ||
193
+ Object.keys(metadata).some((key) => !["version", "otherBots", "webhooks"].includes(key)) ||
194
+ (metadata.otherBots !== undefined &&
195
+ typeof metadata.otherBots !== "boolean") ||
196
+ (metadata.webhooks !== undefined &&
197
+ typeof metadata.webhooks !== "boolean"))
198
+ throw new Error("Module requests unsupported update metadata options");
199
+ }
150
200
  const executableCommands = (definition.interactions?.commands ?? []).map(({ name }) => name);
151
201
  const sharedCommandPaths = new Set();
152
202
  for (const command of definition.sharedCommands ?? []) {
@@ -198,6 +248,7 @@ export function validateContributionParity(definition) {
198
248
  ...(definition.events?.messageReactionRemoveAll ?? []),
199
249
  ...(definition.events?.messageReactionRemoveEmoji ?? []),
200
250
  ...(definition.events?.guildMessageUpdate ?? []),
251
+ ...(definition.events?.guildMessageUpdateMetadata ?? []),
201
252
  ...(definition.events?.messageDelete ?? []),
202
253
  ...(definition.events?.guildMessageDeleteBulk ?? []),
203
254
  ...(definition.events?.guildMemberAdd ?? []),
@@ -240,6 +291,10 @@ export function validateContributionParity(definition) {
240
291
  assertCommandPathContract(definition);
241
292
  }
242
293
  if (definition.manifest.manifestVersion === "0.5") {
294
+ const commandIds = new Set(collectExecutableCommandPaths(definition).map(({ permissionId }) => permissionId));
295
+ for (const id of definition.manifest.commandPermissionReferences ?? [])
296
+ if (!commandIds.has(id))
297
+ throw new Error(`Command permission reference ${id} does not resolve to an executable command in ${definition.manifest.id}`);
243
298
  assertSameContributionIds(definition.manifest.permissionOperations.map(({ id }) => id), (definition.permissionOperations ?? []).map(({ id }) => id), "permission operations");
244
299
  }
245
300
  else if ((definition.permissionOperations?.length ?? 0) > 0) {
@@ -39,6 +39,7 @@ export interface OpenMysqlBotRuntimePersistenceInput {
39
39
  readonly environment: MysqlRuntimeEnvironment;
40
40
  readonly modulePackages: readonly ConfiguredModulePackage[];
41
41
  readonly applicationName?: string;
42
+ readonly onCoreReady?: (pool: MysqlPool) => Promise<void>;
42
43
  }
43
44
  /** Connect, migrate, and compose ordinary self-host persistence from one pool. */
44
45
  export declare function openMysqlBotRuntimePersistence(input: OpenMysqlBotRuntimePersistenceInput): Promise<MysqlBotRuntimePersistence>;
@@ -9,7 +9,9 @@ export async function openMysqlBotRuntimePersistence(input) {
9
9
  const adapter = mysql.createMysqlDatabaseAdapter({
10
10
  repositories: (_executor, pool) => ({ pool }),
11
11
  });
12
- const database = await openBotDatabaseWithAdapter(input, adapter);
12
+ const database = await openBotDatabaseWithAdapter(input, adapter, input.onCoreReady
13
+ ? (session) => input.onCoreReady(session.repositories.pool)
14
+ : undefined);
13
15
  try {
14
16
  const pool = database.session.repositories.pool;
15
17
  const installations = new mysql.InstallationRepository(pool);
@@ -38,6 +40,7 @@ export async function openMysqlBotRuntimePersistence(input) {
38
40
  commandRead: { commands, installations },
39
41
  commandRelease: { commands, installations, runtime },
40
42
  logging: {
43
+ availability,
41
44
  installations,
42
45
  configuration: new mysql.ModuleConfigurationRepository(pool),
43
46
  policies: new mysql.ModuleLoggingPolicyRepository(pool),
@@ -0,0 +1,5 @@
1
+ import type { NexusCommandRecoveryService } from "@helyx/platform";
2
+ import type { FastifyInstance } from "fastify";
3
+ /** Register only on the private control server with its existing bearer hook. */
4
+ export declare function registerNexusCommandRecoveryControlRoutes(server: FastifyInstance, recovery: NexusCommandRecoveryService | undefined): void;
5
+ //# sourceMappingURL=nexus-command-recovery-control-routes.d.ts.map