@helyx/bot 1.0.1 → 1.2.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 +80 -0
- package/README.md +27 -2
- package/dist/auto-moderation-scanner-registry.d.ts +27 -0
- package/dist/auto-moderation-scanner-registry.js +172 -0
- package/dist/cloud-connector.js +3 -0
- package/dist/cloud-control-dispatcher.js +32 -0
- package/dist/command-path-contract.js +1 -0
- package/dist/control-permissions.d.ts +2 -2
- package/dist/control-permissions.js +4 -2
- package/dist/control-server.d.ts +72 -16
- package/dist/control-server.js +150 -250
- package/dist/dashboard-action-control.d.ts +25 -0
- package/dist/dashboard-action-control.js +73 -0
- package/dist/dashboard-action-routes.d.ts +9 -0
- package/dist/dashboard-action-routes.js +36 -0
- package/dist/dashboard-feature-control.d.ts +37 -0
- package/dist/dashboard-feature-control.js +206 -0
- package/dist/dashboard-module-projection.d.ts +9 -0
- package/dist/dashboard-module-projection.js +15 -0
- package/dist/dashboard-preview-import-routes.d.ts +6 -0
- package/dist/dashboard-preview-import-routes.js +121 -0
- package/dist/developer-operations-module.d.ts +2 -0
- package/dist/developer-operations-module.js +136 -2
- package/dist/discord-analytics.d.ts +3 -0
- package/dist/discord-analytics.js +49 -5
- package/dist/discord-auto-moderation-service.d.ts +21 -0
- package/dist/discord-auto-moderation-service.js +425 -0
- package/dist/discord-guild-health.d.ts +2 -2
- package/dist/discord-member-enforcement-service.d.ts +26 -0
- package/dist/discord-member-enforcement-service.js +590 -0
- package/dist/discord-message-enforcement-service.d.ts +9 -0
- package/dist/discord-message-enforcement-service.js +102 -0
- package/dist/discord-violation-thread-service.d.ts +33 -0
- package/dist/discord-violation-thread-service.js +789 -0
- package/dist/ecko-evidence-receiver.d.ts +32 -0
- package/dist/ecko-evidence-receiver.js +173 -0
- package/dist/ecko-transport-auth.d.ts +19 -0
- package/dist/ecko-transport-auth.js +80 -0
- package/dist/ecko-transport-routes.d.ts +19 -0
- package/dist/ecko-transport-routes.js +130 -0
- package/dist/embedded-auto-moderation-pool.d.ts +46 -0
- package/dist/embedded-auto-moderation-pool.js +324 -0
- package/dist/embedded-auto-moderation-scanner.d.ts +34 -0
- package/dist/embedded-auto-moderation-scanner.js +263 -0
- package/dist/embedded-auto-moderation-worker-runtime.d.ts +37 -0
- package/dist/embedded-auto-moderation-worker-runtime.js +37 -0
- package/dist/embedded-auto-moderation-worker.d.ts +2 -0
- package/dist/embedded-auto-moderation-worker.js +13 -0
- package/dist/events.d.ts +5 -1
- package/dist/events.js +275 -9
- package/dist/guilds.d.ts +2 -1
- package/dist/guilds.js +164 -5
- package/dist/hosted-server-moderation.d.ts +100 -0
- package/dist/hosted-server-moderation.js +226 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +101 -12
- package/dist/moderation-action-registry.d.ts +11 -0
- package/dist/moderation-action-registry.js +375 -0
- package/dist/moderation-case-disassociation.d.ts +13 -0
- package/dist/moderation-case-disassociation.js +83 -0
- package/dist/moderation-platform-services.d.ts +27 -0
- package/dist/moderation-platform-services.js +47 -0
- package/dist/modules.d.ts +6 -0
- package/dist/modules.js +65 -3
- package/dist/premium-activation-module.js +2 -0
- package/dist/privileged-message-metadata-cache.d.ts +35 -0
- package/dist/privileged-message-metadata-cache.js +70 -0
- package/dist/scheduled-task-runner.js +24 -1
- package/dist/shard-contracts.d.ts +151 -1
- package/dist/shard-contracts.js +142 -1
- package/dist/shard-coordinator.d.ts +1 -1
- package/dist/shard-ecko-scanner-registry.d.ts +38 -0
- package/dist/shard-ecko-scanner-registry.js +96 -0
- package/dist/shard-router.d.ts +23 -0
- package/dist/shard-router.js +69 -5
- package/dist/shard-supervisor.d.ts +11 -0
- package/dist/shard-supervisor.js +72 -6
- package/dist/shard-worker.d.ts +16 -1
- package/dist/shard-worker.js +108 -10
- package/dist/transient-violation-evidence.d.ts +39 -0
- package/dist/transient-violation-evidence.js +132 -0
- package/documentation/README.md +2 -0
- package/documentation/examples/.env.example +13 -0
- package/documentation/guides/self-hosting.md +17 -1
- package/documentation/modules/auto-moderation.md +51 -0
- package/documentation/modules/moderation.md +42 -0
- package/documentation/reference/environment.md +11 -0
- package/package.json +17 -5
package/dist/shard-contracts.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { CLOUD_MAX_RESOURCE_IMPORT_BYTES } from "@helyx/control-protocol";
|
|
3
|
+
import { AUTOMOD_SCANNER_LIMITS } from "@helyx/sdk";
|
|
2
4
|
import { signedAssessmentEnvelopeSchema } from "@helyx/verification-contracts";
|
|
3
5
|
export const SHARD_CONTRACT_VERSION = 1;
|
|
4
6
|
// Guild dashboard state includes the bounded module catalogue as well as the
|
|
@@ -9,8 +11,30 @@ export const SHARD_CONTROL_TIMEOUT_MS = 30_000;
|
|
|
9
11
|
export const SHARD_HEALTH_STALE_MS = 30_000;
|
|
10
12
|
export const SHARD_LEASE_HEARTBEAT_TIMEOUT_MS = 20_000;
|
|
11
13
|
const snowflake = z.string().regex(/^\d{17,20}$/u);
|
|
14
|
+
const operationId = z.uuid();
|
|
12
15
|
const safeIdentifier = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u);
|
|
13
16
|
const dateTime = z.iso.datetime({ offset: true });
|
|
17
|
+
const boundedEnhancedResult = z
|
|
18
|
+
.unknown()
|
|
19
|
+
.refine((value) => value !== null &&
|
|
20
|
+
typeof value === "object" &&
|
|
21
|
+
boundedJsonBytes(value, AUTOMOD_SCANNER_LIMITS.resultBytes), "Enhanced Auto Moderation result exceeds the transport limit");
|
|
22
|
+
const enhancedEvidence = z
|
|
23
|
+
.object({
|
|
24
|
+
sourceEventId: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
25
|
+
configurationRevision: z.string().min(1).max(160),
|
|
26
|
+
guildId: snowflake,
|
|
27
|
+
channelId: snowflake,
|
|
28
|
+
messageId: snowflake,
|
|
29
|
+
authorUserId: snowflake,
|
|
30
|
+
content: z
|
|
31
|
+
.string()
|
|
32
|
+
.min(1)
|
|
33
|
+
.max(AUTOMOD_SCANNER_LIMITS.messageCharacters)
|
|
34
|
+
.refine((value) => Buffer.byteLength(value, "utf8") <=
|
|
35
|
+
AUTOMOD_SCANNER_LIMITS.messageUtf8Bytes, "Enhanced Auto Moderation evidence exceeds the transport limit"),
|
|
36
|
+
})
|
|
37
|
+
.strict();
|
|
14
38
|
const moduleId = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/u);
|
|
15
39
|
const mutationContext = z
|
|
16
40
|
.object({
|
|
@@ -230,6 +254,39 @@ export const shardHealthSnapshotSchema = z
|
|
|
230
254
|
});
|
|
231
255
|
});
|
|
232
256
|
export const shardControlOperationSchema = z.discriminatedUnion("kind", [
|
|
257
|
+
z
|
|
258
|
+
.object({
|
|
259
|
+
kind: z.literal("automod.enhanced.configuration.page"),
|
|
260
|
+
cursor: z.string().min(1).max(512).optional(),
|
|
261
|
+
limit: z
|
|
262
|
+
.number()
|
|
263
|
+
.int()
|
|
264
|
+
.min(1)
|
|
265
|
+
.max(AUTOMOD_SCANNER_LIMITS.configurationPageServers),
|
|
266
|
+
knownFingerprint: z.string().min(1).max(128).optional(),
|
|
267
|
+
})
|
|
268
|
+
.strict(),
|
|
269
|
+
z
|
|
270
|
+
.object({
|
|
271
|
+
kind: z.literal("automod.enhanced.result.accept"),
|
|
272
|
+
guildId: snowflake,
|
|
273
|
+
result: boundedEnhancedResult,
|
|
274
|
+
evidence: enhancedEvidence.optional(),
|
|
275
|
+
})
|
|
276
|
+
.strict(),
|
|
277
|
+
z
|
|
278
|
+
.object({
|
|
279
|
+
kind: z.literal("hosted-server.leave.deliver"),
|
|
280
|
+
guildId: snowflake,
|
|
281
|
+
operationId,
|
|
282
|
+
})
|
|
283
|
+
.strict(),
|
|
284
|
+
z
|
|
285
|
+
.object({
|
|
286
|
+
kind: z.literal("hosted-server.owner.refresh"),
|
|
287
|
+
guildId: snowflake,
|
|
288
|
+
})
|
|
289
|
+
.strict(),
|
|
233
290
|
z.object({ kind: z.literal("guild.state.get"), guildId: snowflake }).strict(),
|
|
234
291
|
z
|
|
235
292
|
.object({
|
|
@@ -300,6 +357,16 @@ export const shardControlOperationSchema = z.discriminatedUnion("kind", [
|
|
|
300
357
|
...mutationContext.shape,
|
|
301
358
|
})
|
|
302
359
|
.strict(),
|
|
360
|
+
z
|
|
361
|
+
.object({
|
|
362
|
+
kind: z.literal("guild.module.configuration.preview"),
|
|
363
|
+
guildId: snowflake,
|
|
364
|
+
moduleId,
|
|
365
|
+
value: configurationValue,
|
|
366
|
+
input: managedResourceObject,
|
|
367
|
+
...mutationContext.shape,
|
|
368
|
+
})
|
|
369
|
+
.strict(),
|
|
303
370
|
z
|
|
304
371
|
.object({
|
|
305
372
|
kind: z.literal("guild.module.permissions.update"),
|
|
@@ -326,6 +393,16 @@ export const shardControlOperationSchema = z.discriminatedUnion("kind", [
|
|
|
326
393
|
guildId: snowflake,
|
|
327
394
|
moduleId,
|
|
328
395
|
actionId: z.string().regex(/^[a-z][a-z0-9_-]*$/u),
|
|
396
|
+
inspectionRevision: z.string().min(1).max(200).optional(),
|
|
397
|
+
...mutationContext.shape,
|
|
398
|
+
})
|
|
399
|
+
.strict(),
|
|
400
|
+
z
|
|
401
|
+
.object({
|
|
402
|
+
kind: z.literal("guild.module.action.preview"),
|
|
403
|
+
guildId: snowflake,
|
|
404
|
+
moduleId,
|
|
405
|
+
actionId: z.string().regex(/^[a-z][a-z0-9_-]*$/u),
|
|
329
406
|
...mutationContext.shape,
|
|
330
407
|
})
|
|
331
408
|
.strict(),
|
|
@@ -387,6 +464,19 @@ export const shardControlOperationSchema = z.discriminatedUnion("kind", [
|
|
|
387
464
|
...mutationContext.shape,
|
|
388
465
|
})
|
|
389
466
|
.strict(),
|
|
467
|
+
z
|
|
468
|
+
.object({
|
|
469
|
+
kind: z.literal("guild.module.resource.import"),
|
|
470
|
+
guildId: snowflake,
|
|
471
|
+
moduleId,
|
|
472
|
+
resourceType,
|
|
473
|
+
format: z.literal("utf8_text"),
|
|
474
|
+
content: z
|
|
475
|
+
.string()
|
|
476
|
+
.refine((value) => Buffer.byteLength(value, "utf8") <= CLOUD_MAX_RESOURCE_IMPORT_BYTES, "Managed resource import exceeds the transport limit"),
|
|
477
|
+
...mutationContext.shape,
|
|
478
|
+
})
|
|
479
|
+
.strict(),
|
|
390
480
|
z
|
|
391
481
|
.object({
|
|
392
482
|
kind: z.literal("guild.module.resource.create"),
|
|
@@ -962,6 +1052,13 @@ export function assertShardMessageSize(value) {
|
|
|
962
1052
|
}
|
|
963
1053
|
export async function executeShardControlOperation(handler, operation) {
|
|
964
1054
|
switch (operation.kind) {
|
|
1055
|
+
case "automod.enhanced.configuration.page":
|
|
1056
|
+
throw new Error("Enhanced Auto Moderation configuration requires the shard leader");
|
|
1057
|
+
case "automod.enhanced.result.accept":
|
|
1058
|
+
throw new Error("Enhanced Auto Moderation result delivery requires the shard worker");
|
|
1059
|
+
case "hosted-server.leave.deliver":
|
|
1060
|
+
case "hosted-server.owner.refresh":
|
|
1061
|
+
throw new Error("Hosted leave delivery requires the shard worker");
|
|
965
1062
|
case "guild.presence.list":
|
|
966
1063
|
return handler.listGuildPresence({
|
|
967
1064
|
guildIds: operation.guildIds,
|
|
@@ -1022,6 +1119,15 @@ export async function executeShardControlOperation(handler, operation) {
|
|
|
1022
1119
|
correlationId: operation.correlationId,
|
|
1023
1120
|
});
|
|
1024
1121
|
}
|
|
1122
|
+
case "guild.module.configuration.preview":
|
|
1123
|
+
return handler.previewConfiguration({
|
|
1124
|
+
guildId: operation.guildId,
|
|
1125
|
+
moduleId: operation.moduleId,
|
|
1126
|
+
value: operation.value,
|
|
1127
|
+
input: operation.input,
|
|
1128
|
+
actorUserId: operation.actorUserId,
|
|
1129
|
+
correlationId: operation.correlationId,
|
|
1130
|
+
});
|
|
1025
1131
|
case "guild.module.permissions.update": {
|
|
1026
1132
|
return handler.updatePermissions({
|
|
1027
1133
|
guildId: operation.guildId,
|
|
@@ -1044,6 +1150,18 @@ export async function executeShardControlOperation(handler, operation) {
|
|
|
1044
1150
|
}
|
|
1045
1151
|
case "guild.module.action.execute": {
|
|
1046
1152
|
return handler.executeDashboardAction({
|
|
1153
|
+
guildId: operation.guildId,
|
|
1154
|
+
moduleId: operation.moduleId,
|
|
1155
|
+
actionId: operation.actionId,
|
|
1156
|
+
...(operation.inspectionRevision === undefined
|
|
1157
|
+
? {}
|
|
1158
|
+
: { inspectionRevision: operation.inspectionRevision }),
|
|
1159
|
+
actorUserId: operation.actorUserId,
|
|
1160
|
+
correlationId: operation.correlationId,
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
case "guild.module.action.preview": {
|
|
1164
|
+
return handler.previewDashboardAction({
|
|
1047
1165
|
guildId: operation.guildId,
|
|
1048
1166
|
moduleId: operation.moduleId,
|
|
1049
1167
|
actionId: operation.actionId,
|
|
@@ -1099,6 +1217,16 @@ export async function executeShardControlOperation(handler, operation) {
|
|
|
1099
1217
|
actorUserId: operation.actorUserId,
|
|
1100
1218
|
correlationId: operation.correlationId,
|
|
1101
1219
|
});
|
|
1220
|
+
case "guild.module.resource.import":
|
|
1221
|
+
return handler.importManagedResource({
|
|
1222
|
+
guildId: operation.guildId,
|
|
1223
|
+
moduleId: operation.moduleId,
|
|
1224
|
+
resourceType: operation.resourceType,
|
|
1225
|
+
format: operation.format,
|
|
1226
|
+
content: operation.content,
|
|
1227
|
+
actorUserId: operation.actorUserId,
|
|
1228
|
+
correlationId: operation.correlationId,
|
|
1229
|
+
});
|
|
1102
1230
|
case "guild.module.resource.create":
|
|
1103
1231
|
return handler.createManagedResource({
|
|
1104
1232
|
guildId: operation.guildId,
|
|
@@ -1158,7 +1286,7 @@ export async function executeShardControlOperation(handler, operation) {
|
|
|
1158
1286
|
correlationId: operation.correlationId,
|
|
1159
1287
|
});
|
|
1160
1288
|
case "support.notification.send": {
|
|
1161
|
-
|
|
1289
|
+
await handler.sendSupportNotification({
|
|
1162
1290
|
notificationId: operation.notificationId,
|
|
1163
1291
|
reference: operation.reference,
|
|
1164
1292
|
caseType: operation.caseType,
|
|
@@ -1166,6 +1294,11 @@ export async function executeShardControlOperation(handler, operation) {
|
|
|
1166
1294
|
reportedSeverity: operation.reportedSeverity,
|
|
1167
1295
|
source: operation.source,
|
|
1168
1296
|
});
|
|
1297
|
+
// Child-process IPC uses JSON serialization. Returning the handler's void
|
|
1298
|
+
// result would remove the required `result` property from the success
|
|
1299
|
+
// envelope, causing the supervisor to discard a successful Discord send
|
|
1300
|
+
// and retry it after a timeout.
|
|
1301
|
+
return null;
|
|
1169
1302
|
}
|
|
1170
1303
|
case "verification.result.accept":
|
|
1171
1304
|
return handler.acceptVerificationResult(operation.envelope);
|
|
@@ -1221,4 +1354,12 @@ function isBoundedManagedResourceValue(value, depth = 0, budget = { keys: 0 }) {
|
|
|
1221
1354
|
function hasBoundedManagedResourceBytes(value) {
|
|
1222
1355
|
return (new TextEncoder().encode(JSON.stringify(value)).byteLength <= 48 * 1_024);
|
|
1223
1356
|
}
|
|
1357
|
+
function boundedJsonBytes(value, maximumBytes) {
|
|
1358
|
+
try {
|
|
1359
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8") <= maximumBytes;
|
|
1360
|
+
}
|
|
1361
|
+
catch {
|
|
1362
|
+
return false;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1224
1365
|
//# sourceMappingURL=shard-contracts.js.map
|
|
@@ -5,9 +5,9 @@ declare const generationDescriptorSchema: z.ZodObject<{
|
|
|
5
5
|
version: z.ZodLiteral<1>;
|
|
6
6
|
applicationId: z.ZodString;
|
|
7
7
|
environment: z.ZodEnum<{
|
|
8
|
+
production: "production";
|
|
8
9
|
development: "development";
|
|
9
10
|
test: "test";
|
|
10
|
-
production: "production";
|
|
11
11
|
}>;
|
|
12
12
|
generation: z.ZodString;
|
|
13
13
|
shardCount: z.ZodNumber;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type AutoModerationEnhancedResultOutcome, type AutoModerationScannerAdapterSelection, type AutoModerationScannerProviderRegistryService } from "@helyx/sdk";
|
|
2
|
+
import type { TransientViolationEvidenceStore } from "./transient-violation-evidence.js";
|
|
3
|
+
export interface ShardEckoResultRoute {
|
|
4
|
+
guildId: string;
|
|
5
|
+
result: unknown;
|
|
6
|
+
evidence?: {
|
|
7
|
+
sourceEventId: string;
|
|
8
|
+
configurationRevision: string;
|
|
9
|
+
guildId: string;
|
|
10
|
+
channelId: string;
|
|
11
|
+
messageId: string;
|
|
12
|
+
authorUserId: string;
|
|
13
|
+
content: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Supervisor-only Ecko facade. Configuration is read from the fenced leader's
|
|
18
|
+
* allowlisted provider. Results and one-use evidence are routed to the guild
|
|
19
|
+
* owner, where the worker registry performs all validation and actions.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ShardEckoScannerRegistry implements AutoModerationScannerProviderRegistryService {
|
|
22
|
+
#private;
|
|
23
|
+
private readonly input;
|
|
24
|
+
constructor(input: {
|
|
25
|
+
configurationRegistry(): AutoModerationScannerProviderRegistryService | undefined;
|
|
26
|
+
routeConfiguration?(input: {
|
|
27
|
+
cursor?: string;
|
|
28
|
+
limit: number;
|
|
29
|
+
knownFingerprint?: string;
|
|
30
|
+
}): ReturnType<AutoModerationScannerProviderRegistryService["getConfigurationPage"]>;
|
|
31
|
+
evidenceStore: TransientViolationEvidenceStore;
|
|
32
|
+
routeResult(input: ShardEckoResultRoute): Promise<AutoModerationEnhancedResultOutcome>;
|
|
33
|
+
});
|
|
34
|
+
select(): Promise<AutoModerationScannerAdapterSelection>;
|
|
35
|
+
getConfigurationPage(input: Parameters<AutoModerationScannerProviderRegistryService["getConfigurationPage"]>[0]): Promise<import("@helyx/sdk").AutoModerationScannerConfigurationPageV1>;
|
|
36
|
+
acceptResult(input: Parameters<AutoModerationScannerProviderRegistryService["acceptResult"]>[0]): Promise<AutoModerationEnhancedResultOutcome>;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=shard-ecko-scanner-registry.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { AUTOMOD_ENHANCED_CONTRACT_VERSION, AUTOMOD_ENHANCED_PROVIDER_ID, } from "@helyx/sdk";
|
|
2
|
+
/**
|
|
3
|
+
* Supervisor-only Ecko facade. Configuration is read from the fenced leader's
|
|
4
|
+
* allowlisted provider. Results and one-use evidence are routed to the guild
|
|
5
|
+
* owner, where the worker registry performs all validation and actions.
|
|
6
|
+
*/
|
|
7
|
+
export class ShardEckoScannerRegistry {
|
|
8
|
+
input;
|
|
9
|
+
constructor(input) {
|
|
10
|
+
this.input = input;
|
|
11
|
+
}
|
|
12
|
+
select() {
|
|
13
|
+
return Promise.resolve({
|
|
14
|
+
adapter: "unavailable",
|
|
15
|
+
available: false,
|
|
16
|
+
reason: "dependency_unhealthy",
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
async getConfigurationPage(input) {
|
|
20
|
+
const registry = this.input.configurationRegistry();
|
|
21
|
+
if (registry)
|
|
22
|
+
return registry.getConfigurationPage(input);
|
|
23
|
+
if (!this.input.routeConfiguration)
|
|
24
|
+
throw new Error("Ecko configuration leader is unavailable");
|
|
25
|
+
return this.input.routeConfiguration({
|
|
26
|
+
limit: input.limit,
|
|
27
|
+
...(input.cursor ? { cursor: input.cursor } : {}),
|
|
28
|
+
...(input.knownFingerprint
|
|
29
|
+
? { knownFingerprint: input.knownFingerprint }
|
|
30
|
+
: {}),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async acceptResult(input) {
|
|
34
|
+
if (input.adapter !== "hosted_ecko" ||
|
|
35
|
+
input.providerId !== AUTOMOD_ENHANCED_PROVIDER_ID ||
|
|
36
|
+
input.contractVersion !== AUTOMOD_ENHANCED_CONTRACT_VERSION) {
|
|
37
|
+
this.#discard(input.evidenceReference);
|
|
38
|
+
return rejected("wrong_owner");
|
|
39
|
+
}
|
|
40
|
+
const guildId = resultGuildId(input.result);
|
|
41
|
+
if (!guildId) {
|
|
42
|
+
this.#discard(input.evidenceReference);
|
|
43
|
+
return rejected("invalid_envelope");
|
|
44
|
+
}
|
|
45
|
+
const evidence = input.evidenceReference
|
|
46
|
+
? this.#takeEvidence(input.evidenceReference)
|
|
47
|
+
: undefined;
|
|
48
|
+
try {
|
|
49
|
+
return await this.input.routeResult({
|
|
50
|
+
guildId,
|
|
51
|
+
result: input.result,
|
|
52
|
+
...(evidence ? { evidence } : {}),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
return rejected(isDeadlineError(error) ? "stale" : "wrong_owner");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
#takeEvidence(reference) {
|
|
60
|
+
const read = this.input.evidenceStore.consume(reference);
|
|
61
|
+
if (read.outcome !== "available" ||
|
|
62
|
+
reference.source !== "enhanced_scanner" ||
|
|
63
|
+
reference.messageId === null)
|
|
64
|
+
return undefined;
|
|
65
|
+
return {
|
|
66
|
+
sourceEventId: reference.sourceEventId,
|
|
67
|
+
configurationRevision: reference.configurationRevision,
|
|
68
|
+
guildId: reference.guildId,
|
|
69
|
+
channelId: reference.channelId,
|
|
70
|
+
messageId: reference.messageId,
|
|
71
|
+
authorUserId: reference.authorUserId,
|
|
72
|
+
content: read.content,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
#discard(reference) {
|
|
76
|
+
if (reference)
|
|
77
|
+
this.input.evidenceStore.discard(reference);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function resultGuildId(value) {
|
|
81
|
+
if (!value || typeof value !== "object")
|
|
82
|
+
return undefined;
|
|
83
|
+
const guildId = value["guildId"];
|
|
84
|
+
return typeof guildId === "string" && /^\d{17,20}$/u.test(guildId)
|
|
85
|
+
? guildId
|
|
86
|
+
: undefined;
|
|
87
|
+
}
|
|
88
|
+
function isDeadlineError(error) {
|
|
89
|
+
return (error instanceof Error &&
|
|
90
|
+
(error.message.toLowerCase().includes("time") ||
|
|
91
|
+
error.message.toLowerCase().includes("expired")));
|
|
92
|
+
}
|
|
93
|
+
function rejected(code) {
|
|
94
|
+
return { outcome: "rejected", code };
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=shard-ecko-scanner-registry.js.map
|
package/dist/shard-router.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { type AutoModerationEnhancedResultOutcome } from "@helyx/sdk";
|
|
1
2
|
import { type BotControlHandler } from "./control-server.js";
|
|
2
3
|
import { type ShardControlRequest, type ShardControlResponse } from "./shard-contracts.js";
|
|
3
4
|
import type { ShardCoordinator, ShardLeaderLease, ShardLease } from "./shard-coordinator.js";
|
|
5
|
+
import type { ShardEckoResultRoute } from "./shard-ecko-scanner-registry.js";
|
|
6
|
+
import type { AutoModerationScannerProviderRegistryService } from "@helyx/sdk";
|
|
4
7
|
type LocalDispatcher = (shardId: number, request: ShardControlRequest) => Promise<ShardControlResponse>;
|
|
5
8
|
export declare class ShardControlRouter implements BotControlHandler {
|
|
6
9
|
#private;
|
|
@@ -11,13 +14,30 @@ export declare class ShardControlRouter implements BotControlHandler {
|
|
|
11
14
|
localLeases: ReadonlyMap<number, ShardLease>;
|
|
12
15
|
dispatchLocal: LocalDispatcher;
|
|
13
16
|
privacy?: Pick<BotControlHandler, "listPrivacyProviders" | "capturePrivacy" | "erasePrivacy">;
|
|
17
|
+
leaderScannerRegistry?: AutoModerationScannerProviderRegistryService;
|
|
14
18
|
availability?: Pick<BotControlHandler, "listAvailabilityModules">;
|
|
15
19
|
localLeaderLease?: ShardLeaderLease;
|
|
16
20
|
coordinator?: ShardCoordinator;
|
|
17
21
|
notificationGuildId?: string;
|
|
18
22
|
});
|
|
19
23
|
cleanupOAuthConsumer(input: Parameters<BotControlHandler["cleanupOAuthConsumer"]>[0]): ReturnType<BotControlHandler["cleanupOAuthConsumer"]>;
|
|
24
|
+
deliverHostedServerLeave(input: {
|
|
25
|
+
operationId: string;
|
|
26
|
+
guildId: string;
|
|
27
|
+
}): Promise<"queued" | "completed" | "not_currently_installed" | "accepted_for_retry" | "requires_review">;
|
|
28
|
+
refreshHostedServerOwner(guildId: string): Promise<{
|
|
29
|
+
username: string;
|
|
30
|
+
observedAt: string;
|
|
31
|
+
}>;
|
|
20
32
|
listAvailabilityModules(): ReturnType<BotControlHandler["listAvailabilityModules"]>;
|
|
33
|
+
/** Routes an authenticated Ecko result to the current guild owner. */
|
|
34
|
+
acceptHostedEckoResult(input: ShardEckoResultRoute): Promise<AutoModerationEnhancedResultOutcome>;
|
|
35
|
+
/** Reads the fenced leader's current Enhanced configuration snapshot. */
|
|
36
|
+
getHostedEckoConfigurationPage(input: {
|
|
37
|
+
cursor?: string;
|
|
38
|
+
limit: number;
|
|
39
|
+
knownFingerprint?: string;
|
|
40
|
+
}): ReturnType<AutoModerationScannerProviderRegistryService["getConfigurationPage"]>;
|
|
21
41
|
getGuildState(guildId: string): ReturnType<BotControlHandler["getGuildState"]>;
|
|
22
42
|
canAccessDashboard(input: Parameters<BotControlHandler["canAccessDashboard"]>[0]): ReturnType<BotControlHandler["canAccessDashboard"]>;
|
|
23
43
|
getGuildAccessPolicy(input: Parameters<BotControlHandler["getGuildAccessPolicy"]>[0]): ReturnType<BotControlHandler["getGuildAccessPolicy"]>;
|
|
@@ -28,13 +48,16 @@ export declare class ShardControlRouter implements BotControlHandler {
|
|
|
28
48
|
setModuleStatus(input: Parameters<BotControlHandler["setModuleStatus"]>[0]): ReturnType<BotControlHandler["setModuleStatus"]>;
|
|
29
49
|
completeFirstTimeCommandSetup(input: Parameters<BotControlHandler["completeFirstTimeCommandSetup"]>[0]): ReturnType<BotControlHandler["completeFirstTimeCommandSetup"]>;
|
|
30
50
|
updateConfiguration(input: Parameters<BotControlHandler["updateConfiguration"]>[0]): ReturnType<BotControlHandler["updateConfiguration"]>;
|
|
51
|
+
previewConfiguration(input: Parameters<BotControlHandler["previewConfiguration"]>[0]): ReturnType<BotControlHandler["previewConfiguration"]>;
|
|
31
52
|
updatePermissions(input: Parameters<BotControlHandler["updatePermissions"]>[0]): ReturnType<BotControlHandler["updatePermissions"]>;
|
|
32
53
|
updateLoggingPolicy(input: Parameters<BotControlHandler["updateLoggingPolicy"]>[0]): ReturnType<BotControlHandler["updateLoggingPolicy"]>;
|
|
33
54
|
executeDashboardAction(input: Parameters<BotControlHandler["executeDashboardAction"]>[0]): ReturnType<BotControlHandler["executeDashboardAction"]>;
|
|
55
|
+
previewDashboardAction(input: Parameters<BotControlHandler["previewDashboardAction"]>[0]): ReturnType<BotControlHandler["previewDashboardAction"]>;
|
|
34
56
|
listManagedResources(input: Parameters<BotControlHandler["listManagedResources"]>[0]): ReturnType<BotControlHandler["listManagedResources"]>;
|
|
35
57
|
readManagedResource(input: Parameters<BotControlHandler["readManagedResource"]>[0]): ReturnType<BotControlHandler["readManagedResource"]>;
|
|
36
58
|
listManagedResourceRelation(input: Parameters<BotControlHandler["listManagedResourceRelation"]>[0]): ReturnType<BotControlHandler["listManagedResourceRelation"]>;
|
|
37
59
|
previewManagedResource(input: Parameters<BotControlHandler["previewManagedResource"]>[0]): ReturnType<BotControlHandler["previewManagedResource"]>;
|
|
60
|
+
importManagedResource(input: Parameters<BotControlHandler["importManagedResource"]>[0]): ReturnType<BotControlHandler["importManagedResource"]>;
|
|
38
61
|
createManagedResource(input: Parameters<BotControlHandler["createManagedResource"]>[0]): ReturnType<BotControlHandler["createManagedResource"]>;
|
|
39
62
|
updateManagedResource(input: Parameters<BotControlHandler["updateManagedResource"]>[0]): ReturnType<BotControlHandler["updateManagedResource"]>;
|
|
40
63
|
deleteManagedResource(input: Parameters<BotControlHandler["deleteManagedResource"]>[0]): ReturnType<BotControlHandler["deleteManagedResource"]>;
|
package/dist/shard-router.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { AUTOMOD_ENHANCED_CONTRACT_VERSION, AUTOMOD_ENHANCED_PROVIDER_ID, AUTOMOD_SCANNER_LIMITS, } from "@helyx/sdk";
|
|
2
3
|
import { BotControlTransportError, } from "./control-server.js";
|
|
3
4
|
import { SHARD_CONTROL_TIMEOUT_MS, controlOperationGuildId, guildIdToShardId, shardControlOperationSchema, shardControlResponseSchema, } from "./shard-contracts.js";
|
|
4
5
|
// Cross-region/public Redis connections used by local staging routinely take
|
|
@@ -7,6 +8,7 @@ import { SHARD_CONTROL_TIMEOUT_MS, controlOperationGuildId, guildIdToShardId, sh
|
|
|
7
8
|
// deadline turned healthy leases into deterministic 503s. Keep the check
|
|
8
9
|
// bounded while allowing normal network variance.
|
|
9
10
|
const LEASE_VALIDATION_TIMEOUT_MS = 3_000;
|
|
11
|
+
const ECKO_RESULT_TIMEOUT_MS = AUTOMOD_SCANNER_LIMITS.actionDeadlineSeconds * 1_000;
|
|
10
12
|
export class ShardControlRouter {
|
|
11
13
|
input;
|
|
12
14
|
constructor(input) {
|
|
@@ -15,11 +17,28 @@ export class ShardControlRouter {
|
|
|
15
17
|
cleanupOAuthConsumer(input) {
|
|
16
18
|
return this.#route({ kind: "oauth.consumer.cleanup", ...input });
|
|
17
19
|
}
|
|
20
|
+
deliverHostedServerLeave(input) {
|
|
21
|
+
return this.#route({ kind: "hosted-server.leave.deliver", ...input });
|
|
22
|
+
}
|
|
23
|
+
refreshHostedServerOwner(guildId) {
|
|
24
|
+
return this.#route({ kind: "hosted-server.owner.refresh", guildId });
|
|
25
|
+
}
|
|
18
26
|
listAvailabilityModules() {
|
|
19
27
|
if (!this.input.availability)
|
|
20
28
|
return Promise.reject(new BotControlTransportError(503, "Module availability catalogue is unavailable"));
|
|
21
29
|
return this.input.availability.listAvailabilityModules();
|
|
22
30
|
}
|
|
31
|
+
/** Routes an authenticated Ecko result to the current guild owner. */
|
|
32
|
+
acceptHostedEckoResult(input) {
|
|
33
|
+
return this.#route({ kind: "automod.enhanced.result.accept", ...input }, remainingEnhancedDeadline(input.result));
|
|
34
|
+
}
|
|
35
|
+
/** Reads the fenced leader's current Enhanced configuration snapshot. */
|
|
36
|
+
getHostedEckoConfigurationPage(input) {
|
|
37
|
+
return this.#routeLeader({
|
|
38
|
+
kind: "automod.enhanced.configuration.page",
|
|
39
|
+
...input,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
23
42
|
getGuildState(guildId) {
|
|
24
43
|
return this.#route({ kind: "guild.state.get", guildId });
|
|
25
44
|
}
|
|
@@ -97,6 +116,12 @@ export class ShardControlRouter {
|
|
|
97
116
|
...input,
|
|
98
117
|
}));
|
|
99
118
|
}
|
|
119
|
+
previewConfiguration(input) {
|
|
120
|
+
return this.#route(shardControlOperationSchema.parse({
|
|
121
|
+
kind: "guild.module.configuration.preview",
|
|
122
|
+
...input,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
100
125
|
updatePermissions(input) {
|
|
101
126
|
return this.#route({ kind: "guild.module.permissions.update", ...input });
|
|
102
127
|
}
|
|
@@ -106,6 +131,9 @@ export class ShardControlRouter {
|
|
|
106
131
|
executeDashboardAction(input) {
|
|
107
132
|
return this.#route({ kind: "guild.module.action.execute", ...input });
|
|
108
133
|
}
|
|
134
|
+
previewDashboardAction(input) {
|
|
135
|
+
return this.#route({ kind: "guild.module.action.preview", ...input });
|
|
136
|
+
}
|
|
109
137
|
listManagedResources(input) {
|
|
110
138
|
return this.#route(shardControlOperationSchema.parse({
|
|
111
139
|
kind: "guild.module.resource.list",
|
|
@@ -130,6 +158,12 @@ export class ShardControlRouter {
|
|
|
130
158
|
...input,
|
|
131
159
|
}));
|
|
132
160
|
}
|
|
161
|
+
importManagedResource(input) {
|
|
162
|
+
return this.#route(shardControlOperationSchema.parse({
|
|
163
|
+
kind: "guild.module.resource.import",
|
|
164
|
+
...input,
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
133
167
|
createManagedResource(input) {
|
|
134
168
|
return this.#route(shardControlOperationSchema.parse({
|
|
135
169
|
kind: "guild.module.resource.create",
|
|
@@ -188,7 +222,7 @@ export class ShardControlRouter {
|
|
|
188
222
|
executeThrough: executeThrough.toISOString(),
|
|
189
223
|
});
|
|
190
224
|
}
|
|
191
|
-
async #route(operation) {
|
|
225
|
+
async #route(operation, timeoutMs = SHARD_CONTROL_TIMEOUT_MS) {
|
|
192
226
|
const guildId = controlOperationGuildId(operation);
|
|
193
227
|
if (!guildId)
|
|
194
228
|
throw new BotControlTransportError(502, "The control operation has no shard route");
|
|
@@ -212,13 +246,13 @@ export class ShardControlRouter {
|
|
|
212
246
|
requestId: randomUUID(),
|
|
213
247
|
generation: this.input.generation,
|
|
214
248
|
fencingToken: lease.fencingToken,
|
|
215
|
-
deadlineAt: new Date(Date.now() +
|
|
249
|
+
deadlineAt: new Date(Date.now() + timeoutMs).toISOString(),
|
|
216
250
|
operation,
|
|
217
251
|
};
|
|
218
252
|
let response;
|
|
219
253
|
try {
|
|
220
254
|
response = localLease
|
|
221
|
-
? await withDeadline(this.input.dispatchLocal(shardId, request),
|
|
255
|
+
? await withDeadline(this.input.dispatchLocal(shardId, request), timeoutMs)
|
|
222
256
|
: await this.input.coordinator.routeRemote(lease, request);
|
|
223
257
|
}
|
|
224
258
|
catch (error) {
|
|
@@ -231,13 +265,13 @@ export class ShardControlRouter {
|
|
|
231
265
|
return unwrapResponse(response);
|
|
232
266
|
}
|
|
233
267
|
async #routeLeader(operation) {
|
|
234
|
-
if (this.input.localLeaderLease
|
|
268
|
+
if (this.input.localLeaderLease) {
|
|
235
269
|
if (this.input.coordinator) {
|
|
236
270
|
const current = await this.#currentLeaderLease();
|
|
237
271
|
if (!current || !sameLeaderLease(this.input.localLeaderLease, current))
|
|
238
272
|
throw new BotControlTransportError(503, "The shard leader is temporarily unavailable");
|
|
239
273
|
}
|
|
240
|
-
return
|
|
274
|
+
return executeLeaderOperation(this.input, operation);
|
|
241
275
|
}
|
|
242
276
|
const coordinator = this.input.coordinator;
|
|
243
277
|
const lease = await this.#currentLeaderLease();
|
|
@@ -283,6 +317,36 @@ export class ShardControlRouter {
|
|
|
283
317
|
}
|
|
284
318
|
}
|
|
285
319
|
}
|
|
320
|
+
async function executeLeaderOperation(input, operation) {
|
|
321
|
+
if (operation.kind === "automod.enhanced.configuration.page") {
|
|
322
|
+
if (!input.leaderScannerRegistry)
|
|
323
|
+
throw new BotControlTransportError(503, "The Enhanced configuration leader is unavailable");
|
|
324
|
+
return input.leaderScannerRegistry.getConfigurationPage({
|
|
325
|
+
adapter: "hosted_ecko",
|
|
326
|
+
providerId: AUTOMOD_ENHANCED_PROVIDER_ID,
|
|
327
|
+
contractVersion: AUTOMOD_ENHANCED_CONTRACT_VERSION,
|
|
328
|
+
limit: operation.limit,
|
|
329
|
+
...(operation.cursor ? { cursor: operation.cursor } : {}),
|
|
330
|
+
...(operation.knownFingerprint
|
|
331
|
+
? { knownFingerprint: operation.knownFingerprint }
|
|
332
|
+
: {}),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
if (!input.privacy)
|
|
336
|
+
throw new BotControlTransportError(503, "The shard leader is temporarily unavailable");
|
|
337
|
+
return executePrivacy(input.privacy, operation);
|
|
338
|
+
}
|
|
339
|
+
function remainingEnhancedDeadline(result) {
|
|
340
|
+
if (!result || typeof result !== "object")
|
|
341
|
+
return ECKO_RESULT_TIMEOUT_MS;
|
|
342
|
+
const deadlineAt = result["deadlineAt"];
|
|
343
|
+
if (typeof deadlineAt !== "string")
|
|
344
|
+
return ECKO_RESULT_TIMEOUT_MS;
|
|
345
|
+
const parsed = Date.parse(deadlineAt);
|
|
346
|
+
if (!Number.isFinite(parsed))
|
|
347
|
+
return ECKO_RESULT_TIMEOUT_MS;
|
|
348
|
+
return Math.max(1, Math.min(ECKO_RESULT_TIMEOUT_MS, parsed - Date.now()));
|
|
349
|
+
}
|
|
286
350
|
async function executePrivacy(privacy, operation) {
|
|
287
351
|
if (operation.kind === "privacy.providers.list")
|
|
288
352
|
return (await privacy.listPrivacyProviders());
|
|
@@ -4,6 +4,7 @@ import type { ModuleDefinition, NumericWriteBehindCommittedBatch, ScheduledTaskE
|
|
|
4
4
|
import { buildApplicationCommandDefinitions } from "./interactions.js";
|
|
5
5
|
import { type ShardCloudRequest, type ShardHealthSnapshot, type ShardIdentity, type ShardProcessHealth } from "./shard-contracts.js";
|
|
6
6
|
import { type ShardCoordinator, type ShardGenerationDescriptor, type ShardLeaderLease, type ShardLease } from "./shard-coordinator.js";
|
|
7
|
+
import { ShardControlRouter } from "./shard-router.js";
|
|
7
8
|
import type { BotHandle } from "./index.js";
|
|
8
9
|
export declare const STEADY_STATE_LEASE_LOSS_BOUND_MS: number;
|
|
9
10
|
export declare function supervisorShutdownExitCode(reason: string): number | undefined;
|
|
@@ -17,6 +18,16 @@ export declare function relinquishRestartCircuitOwnership(input: {
|
|
|
17
18
|
}): Promise<boolean>;
|
|
18
19
|
export declare function deferScheduledRoutingFailures(route: () => Promise<ScheduledTaskExecutionResult>): Promise<ScheduledTaskExecutionResult>;
|
|
19
20
|
export declare function startShardSupervisor(environment: BotEnvironment, processEnvironment: NodeJS.ProcessEnv, workerEntry: string): Promise<BotHandle>;
|
|
21
|
+
export declare function shardHostedControlCallbacks(router: Pick<ShardControlRouter, "deliverHostedServerLeave" | "refreshHostedServerOwner">): {
|
|
22
|
+
deliverHostedServerLeave: (input: {
|
|
23
|
+
operationId: string;
|
|
24
|
+
guildId: string;
|
|
25
|
+
}) => Promise<"completed" | "requires_review" | "queued" | "not_currently_installed" | "accepted_for_retry">;
|
|
26
|
+
refreshHostedServerOwner: (guildId: string) => Promise<{
|
|
27
|
+
username: string;
|
|
28
|
+
observedAt: string;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
20
31
|
export declare function splitNumericCommittedBatch(batch: NumericWriteBehindCommittedBatch, index: number): NumericWriteBehindCommittedBatch;
|
|
21
32
|
export declare function buildShardWorkerEnvironment(source: NodeJS.ProcessEnv, identity: ShardIdentity, restartCount: number): NodeJS.ProcessEnv;
|
|
22
33
|
export declare function shardRuntimeCompositionHash(modules: readonly ModuleDefinition[], internalGuildId?: string, globalCommandMode?: Parameters<typeof buildApplicationCommandDefinitions>[2]): string;
|