@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.
@@ -1,6 +1,10 @@
1
1
  import { z } from "zod";
2
+ import { signedAssessmentEnvelopeSchema } from "@helyx/verification-contracts";
2
3
  export const SHARD_CONTRACT_VERSION = 1;
3
- export const SHARD_CONTROL_MAX_BYTES = 64 * 1_024;
4
+ // Guild dashboard state includes the bounded module catalogue as well as the
5
+ // guild's Discord resources. Keep this aligned with the cloud-control message
6
+ // ceiling so hosted and self-hosted transports accept the same valid state.
7
+ export const SHARD_CONTROL_MAX_BYTES = 256 * 1_024;
4
8
  export const SHARD_CONTROL_TIMEOUT_MS = 30_000;
5
9
  export const SHARD_HEALTH_STALE_MS = 30_000;
6
10
  export const SHARD_LEASE_HEARTBEAT_TIMEOUT_MS = 20_000;
@@ -58,7 +62,7 @@ const managedResourceObject = z
58
62
  .refine(hasBoundedManagedResourceBytes);
59
63
  const managedResourceFilters = z
60
64
  .record(z.string().regex(/^[a-z][a-zA-Z0-9_]*$/u), z.string().trim().min(1).max(100))
61
- .refine((value) => Object.keys(value).length <= 5);
65
+ .refine((value) => Object.keys(value).length <= 8);
62
66
  const scheduledTaskPayload = z
63
67
  .record(z.string().regex(/^[a-z][A-Za-z0-9]{0,62}Id$/u), z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u))
64
68
  .refine((value) => Object.keys(value).length <= 20)
@@ -218,6 +222,13 @@ export const shardHealthSnapshotSchema = z
218
222
  });
219
223
  export const shardControlOperationSchema = z.discriminatedUnion("kind", [
220
224
  z.object({ kind: z.literal("guild.state.get"), guildId: snowflake }).strict(),
225
+ z
226
+ .object({
227
+ kind: z.literal("guild.presence.list"),
228
+ guildIds: z.array(snowflake).min(1).max(200),
229
+ correlationId: z.uuid(),
230
+ })
231
+ .strict(),
221
232
  z
222
233
  .object({
223
234
  kind: z.literal("guild.module.status.set"),
@@ -266,6 +277,16 @@ export const shardControlOperationSchema = z.discriminatedUnion("kind", [
266
277
  ...mutationContext.shape,
267
278
  })
268
279
  .strict(),
280
+ z
281
+ .object({
282
+ kind: z.literal("guild.module.resource.access"),
283
+ guildId: snowflake,
284
+ moduleId,
285
+ resourceType,
286
+ operation: z.enum(["list", "read"]),
287
+ actorUserId: snowflake,
288
+ })
289
+ .strict(),
269
290
  z
270
291
  .object({
271
292
  kind: z.literal("guild.module.resource.list"),
@@ -356,6 +377,7 @@ export const shardControlOperationSchema = z.discriminatedUnion("kind", [
356
377
  actionId: resourceType,
357
378
  expectedRevision: z.number().int().min(0),
358
379
  value: managedResourceObject,
380
+ actorAuthenticatedAt: dateTime.optional(),
359
381
  ...mutationContext.shape,
360
382
  })
361
383
  .strict(),
@@ -387,6 +409,12 @@ export const shardControlOperationSchema = z.discriminatedUnion("kind", [
387
409
  }),
388
410
  })
389
411
  .strict(),
412
+ z
413
+ .object({
414
+ kind: z.literal("verification.result.accept"),
415
+ envelope: signedAssessmentEnvelopeSchema,
416
+ })
417
+ .strict(),
390
418
  z.object({ kind: z.literal("privacy.providers.list") }).strict(),
391
419
  z
392
420
  .object({
@@ -568,11 +596,246 @@ export const shardScheduledTaskResponseSchema = z
568
596
  result: scheduledTaskResult,
569
597
  })
570
598
  .strict();
599
+ const numericWireValueSchema = z.discriminatedUnion("type", [
600
+ z
601
+ .object({ type: z.literal("string"), value: z.string().max(4_000) })
602
+ .strict(),
603
+ z.object({ type: z.literal("boolean"), value: z.boolean() }).strict(),
604
+ z.object({ type: z.literal("number"), value: z.number().finite() }).strict(),
605
+ z.object({ type: z.literal("date"), value: dateTime }).strict(),
606
+ ]);
607
+ const numericWireRecordSchema = z
608
+ .record(z.string().regex(/^[a-z][a-z0-9_]*$/u), numericWireValueSchema)
609
+ .refine((value) => Object.keys(value).length <= 50);
610
+ const numericScopeSchema = z.object({
611
+ moduleId,
612
+ collection: z.string().regex(/^[a-z][a-z0-9_]*$/u),
613
+ });
614
+ const numericMemberScopeSchema = {
615
+ ...numericScopeSchema.shape,
616
+ guildId: snowflake,
617
+ subjectId: safeIdentifier,
618
+ };
619
+ const numericOperationSchema = z
620
+ .discriminatedUnion("kind", [
621
+ z
622
+ .object({
623
+ kind: z.literal("enqueue"),
624
+ ...numericMemberScopeSchema,
625
+ occurredAt: dateTime,
626
+ cooldownSeconds: z.number().int().min(1).max(86_400),
627
+ identity: numericWireRecordSchema,
628
+ insert: numericWireRecordSchema,
629
+ increments: z
630
+ .record(z.string().regex(/^[a-z][a-z0-9_]*$/u), z.number().int().positive())
631
+ .refine((value) => {
632
+ const length = Object.keys(value).length;
633
+ return length >= 1 && length <= 20;
634
+ }),
635
+ maxima: z
636
+ .record(z.string().regex(/^[a-z][a-z0-9_]*$/u), dateTime)
637
+ .refine((value) => Object.keys(value).length <= 20)
638
+ .optional(),
639
+ })
640
+ .strict(),
641
+ z
642
+ .object({
643
+ kind: z.literal("pending.member"),
644
+ ...numericMemberScopeSchema,
645
+ })
646
+ .strict(),
647
+ z
648
+ .object({ kind: z.literal("flush.member"), ...numericMemberScopeSchema })
649
+ .strict(),
650
+ z
651
+ .object({
652
+ kind: z.literal("flush.guild"),
653
+ ...numericScopeSchema.shape,
654
+ guildId: snowflake,
655
+ })
656
+ .strict(),
657
+ z
658
+ .object({
659
+ kind: z.literal("flush.subject"),
660
+ ...numericScopeSchema.shape,
661
+ subjectId: safeIdentifier,
662
+ })
663
+ .strict(),
664
+ z
665
+ .object({ kind: z.literal("flush.module"), ...numericScopeSchema.shape })
666
+ .strict(),
667
+ z
668
+ .object({
669
+ kind: z.literal("barrier.acquire"),
670
+ ...numericScopeSchema.shape,
671
+ barrierType: z.enum({
672
+ member: "member",
673
+ guild: "guild",
674
+ subject: "subject",
675
+ }),
676
+ guildId: snowflake.optional(),
677
+ subjectId: safeIdentifier.optional(),
678
+ })
679
+ .strict(),
680
+ z
681
+ .object({
682
+ kind: z.literal("barrier.release"),
683
+ ...numericScopeSchema.shape,
684
+ barrierType: z.enum({
685
+ member: "member",
686
+ guild: "guild",
687
+ subject: "subject",
688
+ }),
689
+ guildId: snowflake.optional(),
690
+ subjectId: safeIdentifier.optional(),
691
+ barrierId: z.uuid(),
692
+ })
693
+ .strict(),
694
+ z
695
+ .object({
696
+ kind: z.literal("barrier.renew"),
697
+ ...numericScopeSchema.shape,
698
+ barrierType: z.enum({
699
+ member: "member",
700
+ guild: "guild",
701
+ subject: "subject",
702
+ }),
703
+ guildId: snowflake.optional(),
704
+ subjectId: safeIdentifier.optional(),
705
+ barrierId: z.uuid(),
706
+ })
707
+ .strict(),
708
+ z
709
+ .object({
710
+ kind: z.literal("suppress.subject"),
711
+ ...numericScopeSchema.shape,
712
+ subjectId: safeIdentifier,
713
+ cutoff: dateTime,
714
+ })
715
+ .strict(),
716
+ z
717
+ .object({
718
+ kind: z.literal("suppress.guild"),
719
+ ...numericScopeSchema.shape,
720
+ guildId: snowflake,
721
+ })
722
+ .strict(),
723
+ z
724
+ .object({
725
+ kind: z.literal("release.guild"),
726
+ ...numericScopeSchema.shape,
727
+ guildId: snowflake,
728
+ suppressionId: z.uuid(),
729
+ })
730
+ .strict(),
731
+ z
732
+ .object({
733
+ kind: z.literal("release.subject"),
734
+ ...numericScopeSchema.shape,
735
+ subjectId: safeIdentifier,
736
+ suppressionId: z.uuid(),
737
+ })
738
+ .strict(),
739
+ z
740
+ .object({
741
+ kind: z.literal("cooldown.member.clear"),
742
+ ...numericMemberScopeSchema,
743
+ })
744
+ .strict(),
745
+ z
746
+ .object({
747
+ kind: z.literal("cooldown.subject.clear"),
748
+ ...numericScopeSchema.shape,
749
+ subjectId: safeIdentifier,
750
+ })
751
+ .strict(),
752
+ ])
753
+ .superRefine((operation, context) => {
754
+ if (operation.kind !== "barrier.acquire" &&
755
+ operation.kind !== "barrier.release" &&
756
+ operation.kind !== "barrier.renew")
757
+ return;
758
+ const valid = operation.barrierType === "member"
759
+ ? operation.guildId !== undefined && operation.subjectId !== undefined
760
+ : operation.barrierType === "guild"
761
+ ? operation.guildId !== undefined && operation.subjectId === undefined
762
+ : operation.guildId === undefined &&
763
+ operation.subjectId !== undefined;
764
+ if (!valid)
765
+ context.addIssue({
766
+ code: "custom",
767
+ message: "Numeric barrier scope does not match its barrier type",
768
+ });
769
+ });
770
+ export const shardNumericRequestSchema = z
771
+ .object({
772
+ version: z.literal(SHARD_CONTRACT_VERSION),
773
+ type: z.literal("helyx.numeric.request"),
774
+ requestId: z.uuid(),
775
+ deadlineAt: dateTime,
776
+ identity: shardIdentitySchema,
777
+ operation: numericOperationSchema,
778
+ })
779
+ .strict();
780
+ export const shardNumericResponseSchema = z.discriminatedUnion("ok", [
781
+ z
782
+ .object({
783
+ version: z.literal(SHARD_CONTRACT_VERSION),
784
+ type: z.literal("helyx.numeric.response"),
785
+ requestId: z.uuid(),
786
+ ok: z.literal(true),
787
+ result: z.unknown(),
788
+ })
789
+ .strict(),
790
+ z
791
+ .object({
792
+ version: z.literal(SHARD_CONTRACT_VERSION),
793
+ type: z.literal("helyx.numeric.response"),
794
+ requestId: z.uuid(),
795
+ ok: z.literal(false),
796
+ safeCode: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,99}$/u),
797
+ })
798
+ .strict(),
799
+ ]);
800
+ export const shardNumericCommittedSchema = z
801
+ .object({
802
+ version: z.literal(SHARD_CONTRACT_VERSION),
803
+ type: z.literal("helyx.numeric.committed"),
804
+ requestId: z.uuid(),
805
+ deadlineAt: dateTime,
806
+ identity: shardIdentitySchema,
807
+ scope: numericScopeSchema.strict(),
808
+ payload: z.unknown(),
809
+ })
810
+ .strict();
811
+ export const shardNumericCommittedResponseSchema = z.discriminatedUnion("ok", [
812
+ z
813
+ .object({
814
+ version: z.literal(SHARD_CONTRACT_VERSION),
815
+ type: z.literal("helyx.numeric.committed.response"),
816
+ requestId: z.uuid(),
817
+ identity: shardIdentitySchema,
818
+ ok: z.literal(true),
819
+ })
820
+ .strict(),
821
+ z
822
+ .object({
823
+ version: z.literal(SHARD_CONTRACT_VERSION),
824
+ type: z.literal("helyx.numeric.committed.response"),
825
+ requestId: z.uuid(),
826
+ identity: shardIdentitySchema,
827
+ ok: z.literal(false),
828
+ safeCode: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,99}$/u),
829
+ })
830
+ .strict(),
831
+ ]);
571
832
  export const supervisorMessageSchema = z.discriminatedUnion("type", [
572
833
  shardControlRequestSchema,
573
834
  shardCloudResponseSchema,
574
835
  shardOperationsHealthResponseSchema,
575
836
  shardScheduledTaskRequestSchema,
837
+ shardNumericResponseSchema,
838
+ shardNumericCommittedSchema,
576
839
  z
577
840
  .object({
578
841
  version: z.literal(SHARD_CONTRACT_VERSION),
@@ -599,6 +862,8 @@ export const workerMessageSchema = z.discriminatedUnion("type", [
599
862
  shardCloudRequestSchema,
600
863
  shardOperationsHealthRequestSchema,
601
864
  shardScheduledTaskResponseSchema,
865
+ shardNumericRequestSchema,
866
+ shardNumericCommittedResponseSchema,
602
867
  z
603
868
  .object({
604
869
  version: z.literal(SHARD_CONTRACT_VERSION),
@@ -615,7 +880,13 @@ export function guildIdToShardId(guildId, shardCount) {
615
880
  return Number((BigInt(parsedGuildId) >> 22n) % BigInt(shardCount));
616
881
  }
617
882
  export function controlOperationGuildId(operation) {
618
- return "guildId" in operation ? operation.guildId : undefined;
883
+ if ("guildId" in operation)
884
+ return operation.guildId;
885
+ if (operation.kind === "verification.result.accept")
886
+ return operation.envelope.payload.guildId;
887
+ return operation.kind === "guild.presence.list"
888
+ ? operation.guildIds[0]
889
+ : undefined;
619
890
  }
620
891
  export function assertShardMessageSize(value) {
621
892
  if (Buffer.byteLength(JSON.stringify(value), "utf8") > SHARD_CONTROL_MAX_BYTES)
@@ -623,8 +894,21 @@ export function assertShardMessageSize(value) {
623
894
  }
624
895
  export async function executeShardControlOperation(handler, operation) {
625
896
  switch (operation.kind) {
897
+ case "guild.presence.list":
898
+ return handler.listGuildPresence({
899
+ guildIds: operation.guildIds,
900
+ correlationId: operation.correlationId,
901
+ });
626
902
  case "guild.state.get":
627
903
  return handler.getGuildState(operation.guildId);
904
+ case "guild.module.resource.access":
905
+ return handler.canAccessManagedResource({
906
+ guildId: operation.guildId,
907
+ moduleId: operation.moduleId,
908
+ resourceType: operation.resourceType,
909
+ operation: operation.operation,
910
+ actorUserId: operation.actorUserId,
911
+ });
628
912
  case "guild.module.status.set": {
629
913
  return handler.setModuleStatus({
630
914
  guildId: operation.guildId,
@@ -759,6 +1043,9 @@ export async function executeShardControlOperation(handler, operation) {
759
1043
  value: operation.value,
760
1044
  actorUserId: operation.actorUserId,
761
1045
  correlationId: operation.correlationId,
1046
+ ...(operation.actorAuthenticatedAt
1047
+ ? { actorAuthenticatedAt: operation.actorAuthenticatedAt }
1048
+ : {}),
762
1049
  });
763
1050
  case "support.notification.send": {
764
1051
  return handler.sendSupportNotification({
@@ -770,6 +1057,8 @@ export async function executeShardControlOperation(handler, operation) {
770
1057
  source: operation.source,
771
1058
  });
772
1059
  }
1060
+ case "verification.result.accept":
1061
+ return handler.acceptVerificationResult(operation.envelope);
773
1062
  case "privacy.providers.list":
774
1063
  return handler.listPrivacyProviders();
775
1064
  case "privacy.capture": {
@@ -16,6 +16,8 @@ export declare class ShardControlRouter implements BotControlHandler {
16
16
  notificationGuildId?: string;
17
17
  });
18
18
  getGuildState(guildId: string): ReturnType<BotControlHandler["getGuildState"]>;
19
+ canAccessManagedResource(input: Parameters<BotControlHandler["canAccessManagedResource"]>[0]): ReturnType<BotControlHandler["canAccessManagedResource"]>;
20
+ listGuildPresence(input: Parameters<BotControlHandler["listGuildPresence"]>[0]): ReturnType<BotControlHandler["listGuildPresence"]>;
19
21
  setModuleStatus(input: Parameters<BotControlHandler["setModuleStatus"]>[0]): ReturnType<BotControlHandler["setModuleStatus"]>;
20
22
  updateConfiguration(input: Parameters<BotControlHandler["updateConfiguration"]>[0]): ReturnType<BotControlHandler["updateConfiguration"]>;
21
23
  updatePermissions(input: Parameters<BotControlHandler["updatePermissions"]>[0]): ReturnType<BotControlHandler["updatePermissions"]>;
@@ -30,6 +32,7 @@ export declare class ShardControlRouter implements BotControlHandler {
30
32
  deleteManagedResource(input: Parameters<BotControlHandler["deleteManagedResource"]>[0]): ReturnType<BotControlHandler["deleteManagedResource"]>;
31
33
  executeManagedResourceAction(input: Parameters<BotControlHandler["executeManagedResourceAction"]>[0]): ReturnType<BotControlHandler["executeManagedResourceAction"]>;
32
34
  sendSupportNotification(input: Parameters<BotControlHandler["sendSupportNotification"]>[0]): ReturnType<BotControlHandler["sendSupportNotification"]>;
35
+ acceptVerificationResult(envelope: Parameters<BotControlHandler["acceptVerificationResult"]>[0]): ReturnType<BotControlHandler["acceptVerificationResult"]>;
33
36
  listPrivacyProviders(): ReturnType<BotControlHandler["listPrivacyProviders"]>;
34
37
  capturePrivacy(input: Parameters<BotControlHandler["capturePrivacy"]>[0]): ReturnType<BotControlHandler["capturePrivacy"]>;
35
38
  erasePrivacy(input: Parameters<BotControlHandler["erasePrivacy"]>[0]): ReturnType<BotControlHandler["erasePrivacy"]>;
@@ -10,6 +10,42 @@ export class ShardControlRouter {
10
10
  getGuildState(guildId) {
11
11
  return this.#route({ kind: "guild.state.get", guildId });
12
12
  }
13
+ canAccessManagedResource(input) {
14
+ return this.#route({
15
+ kind: "guild.module.resource.access",
16
+ ...input,
17
+ });
18
+ }
19
+ async listGuildPresence(input) {
20
+ const observations = new Map();
21
+ const byShard = new Map();
22
+ for (const guildId of [...new Set(input.guildIds)]) {
23
+ const shardId = guildIdToShardId(guildId, this.input.shardCount);
24
+ const guildIds = byShard.get(shardId) ?? [];
25
+ guildIds.push(guildId);
26
+ byShard.set(shardId, guildIds);
27
+ }
28
+ await Promise.all([...byShard.values()].map(async (guildIds) => {
29
+ try {
30
+ const result = await this.#route({
31
+ kind: "guild.presence.list",
32
+ guildIds,
33
+ correlationId: input.correlationId,
34
+ });
35
+ for (const observation of result)
36
+ observations.set(observation.guildId, observation);
37
+ }
38
+ catch {
39
+ // A starting, draining, or reassigned shard is unknown, never absent.
40
+ }
41
+ }));
42
+ const observedAt = new Date().toISOString();
43
+ return [...new Set(input.guildIds)].map((guildId) => observations.get(guildId) ?? {
44
+ guildId,
45
+ state: "unknown",
46
+ observedAt,
47
+ });
48
+ }
13
49
  setModuleStatus(input) {
14
50
  return this.#route({ kind: "guild.module.status.set", ...input });
15
51
  }
@@ -85,6 +121,12 @@ export class ShardControlRouter {
85
121
  ...input,
86
122
  });
87
123
  }
124
+ acceptVerificationResult(envelope) {
125
+ return this.#route({
126
+ kind: "verification.result.accept",
127
+ envelope,
128
+ });
129
+ }
88
130
  listPrivacyProviders() {
89
131
  return this.#routeLeader({ kind: "privacy.providers.list" });
90
132
  }
@@ -1,15 +1,17 @@
1
1
  import type { BotEnvironment } from "@helyx/config";
2
2
  import { type DatabasePool } from "@helyx/database";
3
- import type { ModuleDefinition, ScheduledTaskExecutionResult } from "@helyx/sdk";
3
+ import type { ModuleDefinition, NumericWriteBehindCommittedBatch, ScheduledTaskExecutionResult } from "@helyx/sdk";
4
4
  import { type ShardCloudRequest, type ShardIdentity, type ShardProcessHealth } from "./shard-contracts.js";
5
5
  import { type ShardCoordinator, type ShardGenerationDescriptor, type ShardLeaderLease, type ShardLease } from "./shard-coordinator.js";
6
6
  import type { BotHandle } from "./index.js";
7
7
  export declare const STEADY_STATE_LEASE_LOSS_BOUND_MS: number;
8
+ export declare function supervisorShutdownExitCode(reason: string): number | undefined;
8
9
  export declare function createStartupWorkerHeartbeatMaintenance(heartbeat: () => void, intervalMs?: number): {
9
10
  stop(): void;
10
11
  };
11
12
  export declare function deferScheduledRoutingFailures(route: () => Promise<ScheduledTaskExecutionResult>): Promise<ScheduledTaskExecutionResult>;
12
13
  export declare function startShardSupervisor(environment: BotEnvironment, processEnvironment: NodeJS.ProcessEnv, workerEntry: string): Promise<BotHandle>;
14
+ export declare function splitNumericCommittedBatch(batch: NumericWriteBehindCommittedBatch, index: number): NumericWriteBehindCommittedBatch;
13
15
  export declare function buildShardWorkerEnvironment(source: NodeJS.ProcessEnv, identity: ShardIdentity, restartCount: number): NodeJS.ProcessEnv;
14
16
  export declare function shardRuntimeCompositionHash(modules: readonly ModuleDefinition[], internalGuildId?: string): string;
15
17
  export declare function boundedRespawnDelay(attempt: number): number;
@@ -19,6 +21,7 @@ export interface SteadyStateOwnershipRenewal {
19
21
  leaderRenewed: boolean;
20
22
  leaderAvailable: boolean;
21
23
  }
24
+ export declare function retryOwnershipRenewal<T>(renew: () => Promise<T>, attempts?: number): Promise<T>;
22
25
  export declare function selectFencedShardHealth(input: {
23
26
  generation: string;
24
27
  shardCount: number;