@helyx/bot 0.4.11 → 0.4.13
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 +33 -0
- package/dist/control-server.d.ts +38 -3
- package/dist/control-server.js +124 -5
- package/dist/events.d.ts +29 -1
- package/dist/events.js +786 -2
- package/dist/guilds.d.ts +20 -1
- package/dist/guilds.js +149 -9
- package/dist/index.js +21 -5
- package/dist/interactions.js +22 -6
- package/dist/management-module.d.ts +1 -1
- package/dist/shard-contracts.d.ts +324 -0
- package/dist/shard-contracts.js +47 -2
- package/dist/shard-router.d.ts +3 -0
- package/dist/shard-router.js +42 -0
- package/dist/shard-supervisor.d.ts +2 -0
- package/dist/shard-supervisor.js +54 -6
- package/dist/shard-worker.d.ts +6 -0
- package/dist/shard-worker.js +23 -6
- package/dist/verification-service.d.ts +50 -0
- package/dist/verification-service.js +166 -0
- package/package.json +10 -9
package/dist/shard-supervisor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { safeError, ServiceRegistry } from "@helyx/core";
|
|
2
|
-
import { checkDatabaseConnection, createDatabasePool, InteractionSessionRepository, RateLimitRepository, runConfiguredModuleMigrations, runCoreMigrations, RuntimeStateRepository, } from "@helyx/database";
|
|
2
|
+
import { checkDatabaseConnection, createDatabasePool, AuditRepository, InteractionSessionRepository, RateLimitRepository, runConfiguredModuleMigrations, runCoreMigrations, RuntimeStateRepository, } from "@helyx/database";
|
|
3
3
|
import { createPlatformServices, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
|
|
4
4
|
import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
|
|
5
5
|
import { Client, ShardingManager } from "discord.js";
|
|
@@ -29,12 +29,25 @@ import { encodeNumericIpcValue } from "./numeric-write-behind-proxy.js";
|
|
|
29
29
|
const LEASE_RENEW_INTERVAL_MS = 10_000;
|
|
30
30
|
const STARTUP_LEASE_RENEW_INTERVAL_MS = 5_000;
|
|
31
31
|
const LEASE_VALIDATION_TIMEOUT_MS = 1_000;
|
|
32
|
+
const LEASE_RENEW_ATTEMPTS = 2;
|
|
32
33
|
const RETENTION_CLEANUP_INTERVAL_MS = 60 * 60 * 1_000;
|
|
33
34
|
const STARTUP_WAIT_MS = 60_000;
|
|
34
35
|
const WORKER_SHUTDOWN_MS = 5_000;
|
|
35
36
|
const MAX_RESTARTS_PER_WINDOW = 5;
|
|
36
37
|
const RESTART_WINDOW_MS = 10 * 60 * 1_000;
|
|
37
|
-
export const STEADY_STATE_LEASE_LOSS_BOUND_MS = LEASE_RENEW_INTERVAL_MS +
|
|
38
|
+
export const STEADY_STATE_LEASE_LOSS_BOUND_MS = LEASE_RENEW_INTERVAL_MS +
|
|
39
|
+
LEASE_VALIDATION_TIMEOUT_MS * LEASE_RENEW_ATTEMPTS +
|
|
40
|
+
WORKER_SHUTDOWN_MS;
|
|
41
|
+
const FATAL_SUPERVISOR_SHUTDOWN_REASONS = new Set([
|
|
42
|
+
"generation-lease-lost",
|
|
43
|
+
"leader-lease-lost",
|
|
44
|
+
"leader-promotion-failed",
|
|
45
|
+
"rebalance-failed",
|
|
46
|
+
"coordination-unavailable",
|
|
47
|
+
]);
|
|
48
|
+
export function supervisorShutdownExitCode(reason) {
|
|
49
|
+
return FATAL_SUPERVISOR_SHUTDOWN_REASONS.has(reason) ? 1 : undefined;
|
|
50
|
+
}
|
|
38
51
|
export function createStartupWorkerHeartbeatMaintenance(heartbeat, intervalMs = STARTUP_LEASE_RENEW_INTERVAL_MS) {
|
|
39
52
|
const timer = setInterval(heartbeat, intervalMs);
|
|
40
53
|
timer.unref();
|
|
@@ -150,6 +163,11 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
150
163
|
});
|
|
151
164
|
const commandClient = new Client({ intents: [] });
|
|
152
165
|
const unavailableResources = {
|
|
166
|
+
listGuildPresence: (guildIds) => Promise.resolve(guildIds.map((guildId) => ({
|
|
167
|
+
guildId,
|
|
168
|
+
state: "unknown",
|
|
169
|
+
observedAt: new Date().toISOString(),
|
|
170
|
+
}))),
|
|
153
171
|
getGuildResources: () => Promise.reject(new Error("Guild resources are worker-owned")),
|
|
154
172
|
sendToChannel: () => Promise.reject(new Error("Guild resources are worker-owned")),
|
|
155
173
|
deleteChannelMessage: () => Promise.reject(new Error("Guild resources are worker-owned")),
|
|
@@ -883,8 +901,19 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
883
901
|
try {
|
|
884
902
|
if (!coordinator)
|
|
885
903
|
return;
|
|
886
|
-
|
|
887
|
-
|
|
904
|
+
let renewal;
|
|
905
|
+
try {
|
|
906
|
+
renewal = await retryOwnershipRenewal(() => renewSteadyStateOwnership(coordinator, generationDescriptor, deploymentOrder, localLeases, leaderLease));
|
|
907
|
+
}
|
|
908
|
+
catch (error) {
|
|
909
|
+
logger.error({
|
|
910
|
+
event: "shard.coordination_renewal_failed",
|
|
911
|
+
error: safeError(error),
|
|
912
|
+
}, "Shard coordination could not be renewed after a bounded retry");
|
|
913
|
+
await shutdown("coordination-unavailable");
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (!renewal.generationRenewed) {
|
|
888
917
|
logger.error({ event: "shard.generation_lease_lost" }, "Shard generation lease was lost; stopping this supervisor safely");
|
|
889
918
|
await shutdown("generation-lease-lost");
|
|
890
919
|
return;
|
|
@@ -1164,6 +1193,9 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
1164
1193
|
if (shuttingDown)
|
|
1165
1194
|
return;
|
|
1166
1195
|
shuttingDown = true;
|
|
1196
|
+
const shutdownExitCode = supervisorShutdownExitCode(reason);
|
|
1197
|
+
if (shutdownExitCode !== undefined)
|
|
1198
|
+
process.exitCode = shutdownExitCode;
|
|
1167
1199
|
if (leaseTimer)
|
|
1168
1200
|
clearInterval(leaseTimer);
|
|
1169
1201
|
if (startupLeaseTimer)
|
|
@@ -1450,15 +1482,17 @@ function failedCloud(requestId, message) {
|
|
|
1450
1482
|
}
|
|
1451
1483
|
async function cleanupExpiredCoreRecords(database, logger) {
|
|
1452
1484
|
try {
|
|
1453
|
-
const [sessions, rateLimits] = await Promise.all([
|
|
1485
|
+
const [sessions, rateLimits, verificationAudit] = await Promise.all([
|
|
1454
1486
|
new InteractionSessionRepository(database).deleteExpired(),
|
|
1455
1487
|
new RateLimitRepository(database).deleteExpired(),
|
|
1488
|
+
new AuditRepository(database).deleteModuleEventsOlderThanMonths("helyx.verification", 12),
|
|
1456
1489
|
]);
|
|
1457
|
-
if (sessions > 0 || rateLimits > 0)
|
|
1490
|
+
if (sessions > 0 || rateLimits > 0 || verificationAudit > 0)
|
|
1458
1491
|
logger.info({
|
|
1459
1492
|
event: "database.retention_cleaned",
|
|
1460
1493
|
interactionSessions: sessions,
|
|
1461
1494
|
rateLimitWindows: rateLimits,
|
|
1495
|
+
verificationAuditEvents: verificationAudit,
|
|
1462
1496
|
}, "Expired core records removed");
|
|
1463
1497
|
}
|
|
1464
1498
|
catch (error) {
|
|
@@ -1509,6 +1543,20 @@ async function waitForDeath(shard, timeoutMs) {
|
|
|
1509
1543
|
export function boundedRespawnDelay(attempt) {
|
|
1510
1544
|
return Math.min(30_000, 1_000 * 2 ** Math.max(0, attempt - 1));
|
|
1511
1545
|
}
|
|
1546
|
+
export async function retryOwnershipRenewal(renew, attempts = LEASE_RENEW_ATTEMPTS) {
|
|
1547
|
+
if (!Number.isSafeInteger(attempts) || attempts < 1)
|
|
1548
|
+
throw new Error("Ownership renewal attempts must be a positive integer");
|
|
1549
|
+
let lastError;
|
|
1550
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
1551
|
+
try {
|
|
1552
|
+
return await renew();
|
|
1553
|
+
}
|
|
1554
|
+
catch (error) {
|
|
1555
|
+
lastError = error;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
throw lastError;
|
|
1559
|
+
}
|
|
1512
1560
|
export function selectFencedShardHealth(input) {
|
|
1513
1561
|
const liveByShard = new Map(input.liveLeases.map((lease) => [lease.shardId, lease]));
|
|
1514
1562
|
const selected = new Map();
|
package/dist/shard-worker.d.ts
CHANGED
|
@@ -35,7 +35,10 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
35
35
|
HELYX_MODULE_PACKAGES: string[];
|
|
36
36
|
HELYX_MODULE_CATALOGUE_PATH: string | undefined;
|
|
37
37
|
HELYX_SUPPORT_STAFF_CHANNEL_ID: string | undefined;
|
|
38
|
+
HELYX_VERIFICATION_ENABLED: boolean;
|
|
39
|
+
HELYX_VERIFICATION_SIGNING_PUBLIC_KEYS: Record<string, string>;
|
|
38
40
|
HELYX_BOT_CONTROL_PORT: number;
|
|
41
|
+
HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS: number;
|
|
39
42
|
HELYX_PRIVACY_ENABLED: boolean;
|
|
40
43
|
HELYX_MESSAGE_CONTENT_ENABLED: boolean;
|
|
41
44
|
HELYX_CLOUD_CONNECT_ENABLED: boolean;
|
|
@@ -52,6 +55,9 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
|
|
|
52
55
|
NODE_ENV: "development" | "test" | "production";
|
|
53
56
|
HELYX_BOT_CONTROL_SECRET?: string | undefined;
|
|
54
57
|
HELYX_SUPPORT_SERVICE_SECRET?: string | undefined;
|
|
58
|
+
HELYX_VERIFICATION_INSTALLATION_ID?: string | undefined;
|
|
59
|
+
HELYX_VERIFICATION_URL?: string | undefined;
|
|
60
|
+
HELYX_VERIFICATION_INTERNAL_SECRET?: string | undefined;
|
|
55
61
|
HELYX_PRIVACY_EXECUTOR_DATABASE_URL?: string | undefined;
|
|
56
62
|
HELYX_PRIVACY_EXECUTOR_DATABASE_SSL?: "disable" | "require" | "verify-full" | undefined;
|
|
57
63
|
HELYX_SHARD_COUNT?: number | undefined;
|
package/dist/shard-worker.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseBotEnvironment } from "@helyx/config";
|
|
2
2
|
import { safeError, ServiceRegistry } from "@helyx/core";
|
|
3
|
-
import { checkDatabaseConnection, createDatabasePool } from "@helyx/database";
|
|
3
|
+
import { checkDatabaseConnection, createDatabasePool, VerificationSetupRepository, } from "@helyx/database";
|
|
4
4
|
import { createRuntime } from "@helyx/framework";
|
|
5
5
|
import { createPlatformServices } from "@helyx/platform";
|
|
6
6
|
import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
|
|
@@ -9,8 +9,9 @@ import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
|
9
9
|
import pino from "pino";
|
|
10
10
|
import { createCloudManagementModule } from "./cloud-management-module.js";
|
|
11
11
|
import { BotControlService, DiscordGuildResourceDirectory, mapBotControlError, } from "./control-server.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { HttpVerificationService } from "./verification-service.js";
|
|
13
|
+
import { DiscordMessageDeliveryService, DiscordMemberEnforcementService, DiscordMemberRoleService, DiscordVerificationMemberService, DiscordVerificationSetupService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
|
|
14
|
+
import { installGuildTracking, } from "./guilds.js";
|
|
14
15
|
import { installInteractions, } from "./interactions.js";
|
|
15
16
|
import { createManagementModule } from "./management-module.js";
|
|
16
17
|
import { DiscordModuleLoggingService } from "./module-logging.js";
|
|
@@ -73,6 +74,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
73
74
|
let shuttingDown = false;
|
|
74
75
|
let runtimeStarted = false;
|
|
75
76
|
let moduleEvents;
|
|
77
|
+
let guildTracking;
|
|
76
78
|
let client;
|
|
77
79
|
let cloudProxy;
|
|
78
80
|
let shardHealthProvider;
|
|
@@ -84,6 +86,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
84
86
|
return;
|
|
85
87
|
shuttingDown = true;
|
|
86
88
|
status = "stopped";
|
|
89
|
+
guildTracking?.stop();
|
|
87
90
|
if (healthTimer)
|
|
88
91
|
clearInterval(healthTimer);
|
|
89
92
|
leaseHeartbeatWatchdog?.stop();
|
|
@@ -168,6 +171,18 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
168
171
|
})) {
|
|
169
172
|
platformServices.set(name, service);
|
|
170
173
|
}
|
|
174
|
+
if (environment.HELYX_VERIFICATION_ENABLED &&
|
|
175
|
+
environment.HELYX_VERIFICATION_URL &&
|
|
176
|
+
environment.HELYX_VERIFICATION_INTERNAL_SECRET) {
|
|
177
|
+
platformServices.set(HELYX_SERVICE_NAMES.verification, new HttpVerificationService({
|
|
178
|
+
baseUrl: environment.HELYX_VERIFICATION_URL,
|
|
179
|
+
internalSecret: environment.HELYX_VERIFICATION_INTERNAL_SECRET,
|
|
180
|
+
installationId: environment.HELYX_VERIFICATION_INSTALLATION_ID,
|
|
181
|
+
publicSigningKeys: environment.HELYX_VERIFICATION_SIGNING_PUBLIC_KEYS,
|
|
182
|
+
}));
|
|
183
|
+
platformServices.set(HELYX_SERVICE_NAMES.verificationMembers, new DiscordVerificationMemberService(client));
|
|
184
|
+
platformServices.set(HELYX_SERVICE_NAMES.verificationSetup, new DiscordVerificationSetupService(client, new VerificationSetupRepository(database)));
|
|
185
|
+
}
|
|
171
186
|
numericWriteBehind =
|
|
172
187
|
identity.scope === "coordinated"
|
|
173
188
|
? new WorkerNumericWriteBehindProxy(identity)
|
|
@@ -194,13 +209,15 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
194
209
|
...(cloudManagementModule ? [cloudManagementModule.manifest.id] : []),
|
|
195
210
|
...(operationsModule ? [operationsModule.manifest.id] : []),
|
|
196
211
|
]);
|
|
197
|
-
const
|
|
212
|
+
const shardOwnership = {
|
|
198
213
|
generation: identity.generation,
|
|
199
214
|
deploymentOrder: BigInt(identity.deploymentOrder ?? 1),
|
|
200
215
|
supervisorId: identity.supervisorId,
|
|
201
216
|
shardId: identity.shardId,
|
|
202
217
|
fencingToken: BigInt(identity.fencingToken),
|
|
203
|
-
}
|
|
218
|
+
};
|
|
219
|
+
const prepareGuilds = installGuildTracking(client, database, publicModules, logger, enabledCoreModuleIds, shardOwnership, environment.HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS * 1_000, identity.shardCount);
|
|
220
|
+
guildTracking = prepareGuilds;
|
|
204
221
|
const commandsReady = installInteractions(client, modules, shardWorkerCommandRegistration({
|
|
205
222
|
...(environment.DISCORD_INTERNAL_GUILD_ID
|
|
206
223
|
? { internalGuildId: environment.DISCORD_INTERNAL_GUILD_ID }
|
|
@@ -218,7 +235,7 @@ export async function startShardWorker(processEnvironment = process.env, identit
|
|
|
218
235
|
guildId: environment.DISCORD_INTERNAL_GUILD_ID,
|
|
219
236
|
channelId: environment.HELYX_SUPPORT_STAFF_CHANNEL_ID,
|
|
220
237
|
}
|
|
221
|
-
: undefined);
|
|
238
|
+
: undefined, shardOwnership, identity.shardCount);
|
|
222
239
|
process.on("message", (raw) => {
|
|
223
240
|
const parsed = supervisorMessageSchema.safeParse(raw);
|
|
224
241
|
if (!parsed.success)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ActionReceipt, type ChallengeCreateRequest, type ChallengeCreateResponse, type DashboardAttemptPage, type DashboardAttemptQuery, type DashboardSafeAttemptDetail, type SignedAssessmentEnvelope, type VerificationInstallationClaims } from "@helyx/verification-contracts";
|
|
2
|
+
import type { VerificationResultContext, VerificationService } from "@helyx/sdk";
|
|
3
|
+
export interface HttpVerificationServiceOptions {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
internalSecret: string;
|
|
6
|
+
publicSigningKeys: Readonly<Record<string, string>>;
|
|
7
|
+
installationId: string;
|
|
8
|
+
fetchImplementation?: typeof fetch;
|
|
9
|
+
}
|
|
10
|
+
export declare class HttpVerificationService implements VerificationService {
|
|
11
|
+
#private;
|
|
12
|
+
constructor(options: HttpVerificationServiceOptions);
|
|
13
|
+
getInstallationContext(): Promise<{
|
|
14
|
+
installation: VerificationInstallationClaims;
|
|
15
|
+
}>;
|
|
16
|
+
createChallenge(input: ChallengeCreateRequest): Promise<ChallengeCreateResponse>;
|
|
17
|
+
cancelChallenges(input: {
|
|
18
|
+
installationId: string;
|
|
19
|
+
guildId: string;
|
|
20
|
+
userId?: string;
|
|
21
|
+
}): Promise<{
|
|
22
|
+
cancelled: number;
|
|
23
|
+
}>;
|
|
24
|
+
listAttempts(input: DashboardAttemptQuery & {
|
|
25
|
+
installationId: string;
|
|
26
|
+
guildId: string;
|
|
27
|
+
}): Promise<DashboardAttemptPage>;
|
|
28
|
+
getAttempt(input: {
|
|
29
|
+
installationId: string;
|
|
30
|
+
guildId: string;
|
|
31
|
+
attemptId: string;
|
|
32
|
+
}): Promise<DashboardSafeAttemptDetail | null>;
|
|
33
|
+
decideAttempt(input: {
|
|
34
|
+
installationId: string;
|
|
35
|
+
guildId: string;
|
|
36
|
+
attemptId: string;
|
|
37
|
+
expectedVersion: number;
|
|
38
|
+
actorUserId: string;
|
|
39
|
+
decision: "approve" | "deny" | "retry" | "dismiss_relationship";
|
|
40
|
+
reason: string;
|
|
41
|
+
}): Promise<DashboardSafeAttemptDetail>;
|
|
42
|
+
registerResultHandler(handler: (context: VerificationResultContext) => Promise<ActionReceipt>): () => void;
|
|
43
|
+
acceptResult(envelopeInput: SignedAssessmentEnvelope): Promise<ActionReceipt>;
|
|
44
|
+
health(): Promise<{
|
|
45
|
+
status: "healthy" | "degraded" | "unavailable";
|
|
46
|
+
provider: "available" | "degraded" | "unavailable";
|
|
47
|
+
allowanceRemaining?: number;
|
|
48
|
+
}>;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=verification-service.d.ts.map
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { actionReceiptSchema, challengeCreateRequestSchema, challengeCreateResponseSchema, dashboardAttemptPageSchema, dashboardAttemptQuerySchema, dashboardSafeAttemptDetailSchema, signedAssessmentEnvelopeSchema, } from "@helyx/verification-contracts";
|
|
2
|
+
import { createPublicKey, verify } from "node:crypto";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
const healthSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
status: z.enum(["healthy", "degraded", "unavailable"]),
|
|
7
|
+
provider: z.enum(["available", "degraded", "unavailable"]),
|
|
8
|
+
allowanceRemaining: z.number().int().min(0).optional(),
|
|
9
|
+
})
|
|
10
|
+
.strict();
|
|
11
|
+
export class HttpVerificationService {
|
|
12
|
+
#baseUrl;
|
|
13
|
+
#internalSecret;
|
|
14
|
+
#publicSigningKeys;
|
|
15
|
+
#fetch;
|
|
16
|
+
#installationId;
|
|
17
|
+
#handler;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.#baseUrl = options.baseUrl.replace(/\/$/u, "");
|
|
20
|
+
this.#internalSecret = options.internalSecret;
|
|
21
|
+
this.#publicSigningKeys = { ...options.publicSigningKeys };
|
|
22
|
+
this.#installationId = options.installationId;
|
|
23
|
+
this.#fetch = options.fetchImplementation ?? fetch;
|
|
24
|
+
}
|
|
25
|
+
getInstallationContext() {
|
|
26
|
+
return Promise.resolve({
|
|
27
|
+
installation: {
|
|
28
|
+
installationId: this.#installationId,
|
|
29
|
+
sourceClassification: "helyx_hosted",
|
|
30
|
+
capabilities: [
|
|
31
|
+
"challenge.create.v1",
|
|
32
|
+
"assessment.receive.v1",
|
|
33
|
+
"action.receipt.write.v1",
|
|
34
|
+
"member.current.read.v1",
|
|
35
|
+
"ban.current.read.v1",
|
|
36
|
+
"service.health.read.v1",
|
|
37
|
+
],
|
|
38
|
+
authenticatedAt: new Date().toISOString(),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async createChallenge(input) {
|
|
43
|
+
return challengeCreateResponseSchema.parse(await this.#request("/internal/v1/challenges", {
|
|
44
|
+
method: "POST",
|
|
45
|
+
body: challengeCreateRequestSchema.parse(input),
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
async cancelChallenges(input) {
|
|
49
|
+
return z
|
|
50
|
+
.object({ cancelled: z.number().int().min(0) })
|
|
51
|
+
.strict()
|
|
52
|
+
.parse(await this.#request("/internal/v1/challenges/cancel", {
|
|
53
|
+
method: "POST",
|
|
54
|
+
body: input,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
async listAttempts(input) {
|
|
58
|
+
const { installationId, guildId, ...query } = input;
|
|
59
|
+
return dashboardAttemptPageSchema.parse(await this.#request("/internal/v1/attempts/query", {
|
|
60
|
+
method: "POST",
|
|
61
|
+
body: {
|
|
62
|
+
installationId,
|
|
63
|
+
guildId,
|
|
64
|
+
query: dashboardAttemptQuerySchema.parse(query),
|
|
65
|
+
},
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
async getAttempt(input) {
|
|
69
|
+
const result = await this.#request(`/internal/v1/attempts/${encodeURIComponent(input.attemptId)}`, { method: "POST", body: input }, true);
|
|
70
|
+
return result === null
|
|
71
|
+
? null
|
|
72
|
+
: dashboardSafeAttemptDetailSchema.parse(result);
|
|
73
|
+
}
|
|
74
|
+
async decideAttempt(input) {
|
|
75
|
+
return dashboardSafeAttemptDetailSchema.parse(await this.#request(`/internal/v1/attempts/${encodeURIComponent(input.attemptId)}/decision`, { method: "POST", body: input }));
|
|
76
|
+
}
|
|
77
|
+
registerResultHandler(handler) {
|
|
78
|
+
if (this.#handler !== undefined)
|
|
79
|
+
throw new Error("Verification result handler is already registered");
|
|
80
|
+
this.#handler = handler;
|
|
81
|
+
return () => {
|
|
82
|
+
if (this.#handler === handler)
|
|
83
|
+
this.#handler = undefined;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async acceptResult(envelopeInput) {
|
|
87
|
+
const envelope = signedAssessmentEnvelopeSchema.parse(envelopeInput);
|
|
88
|
+
if (envelope.payload.deploymentSourceClassification !== "helyx_hosted" ||
|
|
89
|
+
envelope.payload.evidenceSources.discord !== "helyx_hosted" ||
|
|
90
|
+
envelope.payload.installationId !== this.#installationId)
|
|
91
|
+
throw new Error("Verification assessment is not bound to hosted Helyx");
|
|
92
|
+
const publicKey = this.#publicSigningKeys[envelope.keyId];
|
|
93
|
+
if (publicKey === undefined || !verifyEnvelope(envelope, publicKey))
|
|
94
|
+
throw new Error("Verification assessment signature is invalid");
|
|
95
|
+
const expiresAt = Date.parse(envelope.payload.expiresAt);
|
|
96
|
+
const issuedAt = Date.parse(envelope.payload.issuedAt);
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
if (issuedAt > now + 30_000 || expiresAt <= now)
|
|
99
|
+
throw new Error("Verification assessment is outside its valid lifetime");
|
|
100
|
+
if (!this.#handler)
|
|
101
|
+
throw new Error("Verification result handler is unavailable");
|
|
102
|
+
return actionReceiptSchema.parse(await this.#handler({
|
|
103
|
+
envelope,
|
|
104
|
+
correlationId: envelope.payload.attemptId,
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
async health() {
|
|
108
|
+
const health = healthSchema.parse(await this.#request("/internal/v1/health", { method: "GET" }));
|
|
109
|
+
return {
|
|
110
|
+
status: health.status,
|
|
111
|
+
provider: health.provider,
|
|
112
|
+
...(health.allowanceRemaining === undefined
|
|
113
|
+
? {}
|
|
114
|
+
: { allowanceRemaining: health.allowanceRemaining }),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async #request(path, input, allowNotFound = false) {
|
|
118
|
+
let response;
|
|
119
|
+
try {
|
|
120
|
+
response = await this.#fetch(`${this.#baseUrl}${path}`, {
|
|
121
|
+
method: input.method,
|
|
122
|
+
headers: {
|
|
123
|
+
authorization: `Bearer ${this.#internalSecret}`,
|
|
124
|
+
accept: "application/json",
|
|
125
|
+
...(input.body === undefined
|
|
126
|
+
? {}
|
|
127
|
+
: { "content-type": "application/json" }),
|
|
128
|
+
},
|
|
129
|
+
...(input.body === undefined
|
|
130
|
+
? {}
|
|
131
|
+
: { body: JSON.stringify(input.body) }),
|
|
132
|
+
signal: AbortSignal.timeout(10_000),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
throw new Error("Verification service is unavailable");
|
|
137
|
+
}
|
|
138
|
+
if (allowNotFound && response.status === 404)
|
|
139
|
+
return null;
|
|
140
|
+
if (!response.ok)
|
|
141
|
+
throw new Error(response.status >= 500
|
|
142
|
+
? "Verification service is unavailable"
|
|
143
|
+
: "Verification request was rejected");
|
|
144
|
+
try {
|
|
145
|
+
return await response.json();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
throw new Error("Verification service returned an invalid response");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function verifyEnvelope(envelope, publicKeySpkiBase64) {
|
|
153
|
+
let key;
|
|
154
|
+
try {
|
|
155
|
+
key = createPublicKey({
|
|
156
|
+
key: Buffer.from(publicKeySpkiBase64, "base64"),
|
|
157
|
+
format: "der",
|
|
158
|
+
type: "spki",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return verify(null, Buffer.from(JSON.stringify(envelope.payload), "utf8"), key, Buffer.from(envelope.signature, "base64url"));
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=verification-service.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helyx/bot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.13",
|
|
4
4
|
"description": "Production Discord runtime for the modular Helyx platform.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"helyx",
|
|
@@ -48,14 +48,15 @@
|
|
|
48
48
|
"start": "node --env-file-if-exists=../../.env dist/index.js"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@helyx/config": "0.
|
|
52
|
-
"@helyx/control-protocol": "0.
|
|
53
|
-
"@helyx/core": "0.1.
|
|
54
|
-
"@helyx/database": "0.
|
|
55
|
-
"@helyx/framework": "0.1.
|
|
56
|
-
"@helyx/module-manifest": "0.
|
|
57
|
-
"@helyx/platform": "0.
|
|
58
|
-
"@helyx/sdk": "0.
|
|
51
|
+
"@helyx/config": "0.5.0",
|
|
52
|
+
"@helyx/control-protocol": "0.3.0",
|
|
53
|
+
"@helyx/core": "0.1.12",
|
|
54
|
+
"@helyx/database": "0.6.0",
|
|
55
|
+
"@helyx/framework": "0.1.12",
|
|
56
|
+
"@helyx/module-manifest": "0.6.0",
|
|
57
|
+
"@helyx/platform": "0.4.0",
|
|
58
|
+
"@helyx/sdk": "0.5.0",
|
|
59
|
+
"@helyx/verification-contracts": "0.1.0",
|
|
59
60
|
"discord.js": "14.27.0",
|
|
60
61
|
"fastify": "5.10.0",
|
|
61
62
|
"pino": "10.3.1",
|