@helyx/bot 0.4.22 → 0.4.24

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 CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.24
4
+
5
+ ### Patch Changes
6
+
7
+ - Reuse active Verification links when available, report their expiry, and allow members to replace them while atomically invalidating the old request.---
8
+ "@helyx/bot": patch
9
+ "@helyx/database": patch
10
+ "@helyx/sdk": patch
11
+ "@helyx/verification-contracts": patch
12
+ ---
13
+
14
+ Reuse active Verification links when available, report their expiry, and allow members to replace them while atomically invalidating the old request.
15
+
16
+ - Accept compatible internal Helyx releases within explicit minor-safe pre-1.0 or major-safe stable ranges, preventing dependency-only patch cascades.---
17
+ "@helyx/bot": patch
18
+ "@helyx/cli": patch
19
+ "@helyx/core": patch
20
+ "@helyx/framework": patch
21
+ "@helyx/platform": patch
22
+ "@helyx/sdk": patch
23
+ "@helyx/module-account-gate": patch
24
+ "@helyx/module-feedback": patch
25
+ "@helyx/module-giveaways": patch
26
+ "@helyx/module-levels": patch
27
+ "@helyx/module-logging": patch
28
+ "@helyx/module-polls": patch
29
+ "@helyx/module-reactionroles": patch
30
+ "@helyx/module-suggestions": patch
31
+ "@helyx/module-welcome": patch
32
+ ---
33
+
34
+ Accept compatible internal Helyx releases within explicit minor-safe pre-1.0 or major-safe stable ranges, preventing dependency-only patch cascades.
35
+
36
+ ## 0.4.23
37
+
38
+ ### Patch Changes
39
+
40
+ - Rebuild and publish Verification package artifacts that match the current source.
41
+ - Updated dependencies
42
+ - @helyx/verification-contracts@0.3.1
43
+ - @helyx/sdk@0.6.3
44
+ - @helyx/core@0.1.16
45
+ - @helyx/framework@0.1.16
46
+ - @helyx/platform@0.4.7
47
+
3
48
  ## 0.4.22
4
49
 
5
50
  ### Patch Changes
@@ -426,16 +426,16 @@ export declare const shardControlOperationSchema: z.ZodDiscriminatedUnion<[z.Zod
426
426
  }>>;
427
427
  actionBoundary: z.ZodObject<{
428
428
  recommendedAction: z.ZodEnum<{
429
+ none: "none";
429
430
  grant_role: "grant_role";
430
431
  remove_role: "remove_role";
431
- none: "none";
432
432
  kick: "kick";
433
433
  ban: "ban";
434
434
  }>;
435
435
  allowedActions: z.ZodArray<z.ZodEnum<{
436
+ none: "none";
436
437
  grant_role: "grant_role";
437
438
  remove_role: "remove_role";
438
- none: "none";
439
439
  kick: "kick";
440
440
  ban: "ban";
441
441
  }>>;
@@ -735,16 +735,16 @@ export declare const shardControlRequestSchema: z.ZodObject<{
735
735
  }>>;
736
736
  actionBoundary: z.ZodObject<{
737
737
  recommendedAction: z.ZodEnum<{
738
+ none: "none";
738
739
  grant_role: "grant_role";
739
740
  remove_role: "remove_role";
740
- none: "none";
741
741
  kick: "kick";
742
742
  ban: "ban";
743
743
  }>;
744
744
  allowedActions: z.ZodArray<z.ZodEnum<{
745
+ none: "none";
745
746
  grant_role: "grant_role";
746
747
  remove_role: "remove_role";
747
- none: "none";
748
748
  kick: "kick";
749
749
  ban: "ban";
750
750
  }>>;
@@ -1549,16 +1549,16 @@ export declare const supervisorMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObje
1549
1549
  }>>;
1550
1550
  actionBoundary: z.ZodObject<{
1551
1551
  recommendedAction: z.ZodEnum<{
1552
+ none: "none";
1552
1553
  grant_role: "grant_role";
1553
1554
  remove_role: "remove_role";
1554
- none: "none";
1555
1555
  kick: "kick";
1556
1556
  ban: "ban";
1557
1557
  }>;
1558
1558
  allowedActions: z.ZodArray<z.ZodEnum<{
1559
+ none: "none";
1559
1560
  grant_role: "grant_role";
1560
1561
  remove_role: "remove_role";
1561
- none: "none";
1562
1562
  kick: "kick";
1563
1563
  ban: "ban";
1564
1564
  }>>;
@@ -9,7 +9,7 @@ export declare function createLeaseHeartbeatWatchdog(onExpired: () => void, time
9
9
  };
10
10
  export declare function startShardWorker(processEnvironment?: NodeJS.ProcessEnv, identity?: {
11
11
  version: 1;
12
- mode: "fixed" | "auto";
12
+ mode: "auto" | "fixed";
13
13
  scope: "local" | "coordinated";
14
14
  generation: string;
15
15
  supervisorId: string;
@@ -1,4 +1,5 @@
1
1
  import { actionReceiptSchema, challengeCreateRequestSchema, challengeCreateResponseSchema, dashboardAttemptPageSchema, dashboardAttemptQuerySchema, dashboardSafeAttemptDetailSchema, signedAssessmentEnvelopeSchema, } from "@helyx/verification-contracts";
2
+ import { VerificationChallengeActiveError } from "@helyx/sdk";
2
3
  import { createPublicKey, verify } from "node:crypto";
3
4
  import { z } from "zod";
4
5
  const healthSchema = z
@@ -8,12 +9,19 @@ const healthSchema = z
8
9
  allowanceRemaining: z.number().int().min(0).optional(),
9
10
  })
10
11
  .strict();
12
+ const activeChallengeSchema = z
13
+ .object({
14
+ message: z.literal("A verification link is already active."),
15
+ expiresAt: z.iso.datetime(),
16
+ })
17
+ .strict();
11
18
  export class HttpVerificationService {
12
19
  #baseUrl;
13
20
  #internalSecret;
14
21
  #publicSigningKeys;
15
22
  #fetch;
16
23
  #installationId;
24
+ #challengeCache = new Map();
17
25
  #handler;
18
26
  constructor(options) {
19
27
  this.#baseUrl = options.baseUrl.replace(/\/$/u, "");
@@ -40,19 +48,36 @@ export class HttpVerificationService {
40
48
  });
41
49
  }
42
50
  async createChallenge(input) {
43
- return challengeCreateResponseSchema.parse(await this.#request("/internal/v1/challenges", {
51
+ const cacheKey = challengeCacheKey(input.installation.installationId, input.guildId, input.userId);
52
+ this.#pruneChallengeCache();
53
+ const cached = this.#challengeCache.get(cacheKey);
54
+ if (input.replaceActive !== true &&
55
+ cached &&
56
+ Date.parse(cached.expiresAt) > Date.now()) {
57
+ return challengeCreateResponseSchema.parse({ ...cached, reused: true });
58
+ }
59
+ const created = challengeCreateResponseSchema.parse(await this.#request("/internal/v1/challenges", {
44
60
  method: "POST",
45
61
  body: challengeCreateRequestSchema.parse(input),
46
62
  }));
63
+ this.#challengeCache.set(cacheKey, created);
64
+ return created;
47
65
  }
48
66
  async cancelChallenges(input) {
49
- return z
67
+ const cancelled = z
50
68
  .object({ cancelled: z.number().int().min(0) })
51
69
  .strict()
52
70
  .parse(await this.#request("/internal/v1/challenges/cancel", {
53
71
  method: "POST",
54
72
  body: input,
55
73
  }));
74
+ const prefix = `${input.installationId}:${input.guildId}:`;
75
+ for (const key of this.#challengeCache.keys()) {
76
+ if (key.startsWith(prefix) &&
77
+ (input.userId === undefined || key === `${prefix}${input.userId}`))
78
+ this.#challengeCache.delete(key);
79
+ }
80
+ return cancelled;
56
81
  }
57
82
  async listAttempts(input) {
58
83
  const { installationId, guildId, ...query } = input;
@@ -99,10 +124,12 @@ export class HttpVerificationService {
99
124
  throw new Error("Verification assessment is outside its valid lifetime");
100
125
  if (!this.#handler)
101
126
  throw new Error("Verification result handler is unavailable");
102
- return actionReceiptSchema.parse(await this.#handler({
127
+ const receipt = actionReceiptSchema.parse(await this.#handler({
103
128
  envelope,
104
129
  correlationId: envelope.payload.attemptId,
105
130
  }));
131
+ this.#challengeCache.delete(challengeCacheKey(envelope.payload.installationId, envelope.payload.guildId, envelope.payload.userId));
132
+ return receipt;
106
133
  }
107
134
  async health() {
108
135
  const health = healthSchema.parse(await this.#request("/internal/v1/health", { method: "GET" }));
@@ -138,8 +165,12 @@ export class HttpVerificationService {
138
165
  if (allowNotFound && response.status === 404)
139
166
  return null;
140
167
  if (!response.ok)
141
- if (response.status === 409)
142
- throw new Error("A verification link is already active. Wait for it to expire before requesting another.");
168
+ if (response.status === 409) {
169
+ const active = activeChallengeSchema.safeParse(await response.json());
170
+ if (active.success)
171
+ throw new VerificationChallengeActiveError(active.data.expiresAt);
172
+ throw new Error("Verification request was rejected");
173
+ }
143
174
  else if (response.status === 429)
144
175
  throw new Error("Verification requests are temporarily limited. Try again shortly.");
145
176
  else
@@ -153,6 +184,22 @@ export class HttpVerificationService {
153
184
  throw new Error("Verification service returned an invalid response");
154
185
  }
155
186
  }
187
+ #pruneChallengeCache() {
188
+ const now = Date.now();
189
+ for (const [key, challenge] of this.#challengeCache) {
190
+ if (Date.parse(challenge.expiresAt) <= now)
191
+ this.#challengeCache.delete(key);
192
+ }
193
+ while (this.#challengeCache.size >= 10_000) {
194
+ const oldestKey = this.#challengeCache.keys().next().value;
195
+ if (oldestKey === undefined)
196
+ break;
197
+ this.#challengeCache.delete(oldestKey);
198
+ }
199
+ }
200
+ }
201
+ function challengeCacheKey(installationId, guildId, userId) {
202
+ return `${installationId}:${guildId}:${userId}`;
156
203
  }
157
204
  function verifyEnvelope(envelope, publicKeySpkiBase64) {
158
205
  let key;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/bot",
3
- "version": "0.4.22",
3
+ "version": "0.4.24",
4
4
  "description": "Production Discord runtime for the modular Helyx platform.",
5
5
  "keywords": [
6
6
  "helyx",
@@ -48,15 +48,15 @@
48
48
  "start": "node --env-file-if-exists=../../.env dist/index.js"
49
49
  },
50
50
  "dependencies": {
51
- "@helyx/config": "0.5.0",
52
- "@helyx/control-protocol": "0.3.2",
53
- "@helyx/core": "0.1.15",
54
- "@helyx/database": "0.7.5",
55
- "@helyx/framework": "0.1.15",
56
- "@helyx/module-manifest": "0.6.0",
57
- "@helyx/platform": "0.4.6",
58
- "@helyx/sdk": "0.6.2",
59
- "@helyx/verification-contracts": "0.3.0",
51
+ "@helyx/config": ">=0.5.0 <0.6.0",
52
+ "@helyx/control-protocol": ">=0.3.0 <0.4.0",
53
+ "@helyx/core": ">=0.1.0 <0.2.0",
54
+ "@helyx/database": ">=0.7.0 <0.8.0",
55
+ "@helyx/framework": ">=0.1.0 <0.2.0",
56
+ "@helyx/module-manifest": ">=0.6.0 <0.7.0",
57
+ "@helyx/platform": ">=0.4.0 <0.5.0",
58
+ "@helyx/sdk": ">=0.6.0 <0.7.0",
59
+ "@helyx/verification-contracts": ">=0.3.0 <0.4.0",
60
60
  "discord.js": "14.27.0",
61
61
  "fastify": "5.10.0",
62
62
  "pino": "10.3.1",