@helyx/bot 0.4.9 → 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.
@@ -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.9",
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.4.2",
52
- "@helyx/control-protocol": "0.1.5",
53
- "@helyx/core": "0.1.9",
54
- "@helyx/database": "0.4.3",
55
- "@helyx/framework": "0.1.9",
56
- "@helyx/module-manifest": "0.4.2",
57
- "@helyx/platform": "0.2.7",
58
- "@helyx/sdk": "0.3.4",
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",