1import * as AWS from "@/AWS";
2import { makeS3State } from "@/AWS";
3import { createStateBucketName } from "@/AWS/StateStore/State.ts";
4import type { ResourceState, StateService } from "@/State";
5import * as Test from "@/Test/Alchemy";
6import * as kms from "@distilled.cloud/aws/kms";
7import * as s3 from "@distilled.cloud/aws/s3";
8import { expect } from "alchemy-test";
9import * as Effect from "effect/Effect";
10import * as Redacted from "effect/Redacted";
11import * as Schedule from "effect/Schedule";
12import { createHash } from "node:crypto";
13
14const { test } = Test.make({ providers: AWS.providers() });
15
16test.provider(
17 "PR1587 fresh state services skip matching KMS encryption writes",
18 (stack) =>
19 Effect.gen(function* () {
20 yield* stack.destroy();
21 const { bucket, first, second } = yield* stack.deploy(
22 Effect.gen(function* () {
23 const first = yield* AWS.KMS.Key("FirstStateKey", {
24 deletionWindow: "7 days",
25 });
26 const second = yield* AWS.KMS.Key("SecondStateKey", {
27 deletionWindow: "7 days",
28 });
29 const bucket = yield* AWS.S3.Bucket("IdentityStateBucket", {
30 encryption: {
31 sseAlgorithm: "aws:kms",
32 kmsMasterKeyId: first.keyArn,
33 },
34 });
35 return { bucket, first, second };
36 }),
37 );
38 const initialize = (key: string, bucketKeyEnabled?: boolean) =>
39 Effect.gen(function* () {
40 const state = yield* makeS3State({
41 bucketName: bucket.bucketName,
42 prefix: "pr1587",
43 encryption: {
44 sseAlgorithm: "aws:kms",
45 kmsMasterKeyId: key,
46 bucketKeyEnabled,
47 },
48 });
49 return yield* state.listStacks();
50 });
51 expect(yield* initialize(first.keyArn)).toEqual([]);
52 const observed = yield* s3.getBucketEncryption({
53 Bucket: bucket.bucketName,
54 });
55 const decoded =
56 observed.ServerSideEncryptionConfiguration!.Rules[0]!
57 .ApplyServerSideEncryptionByDefault!.KMSMasterKeyID;
58 expect(Redacted.isRedacted(decoded)).toBe(true);
59 expect(
60 Redacted.isRedacted(decoded) ? Redacted.value(decoded) : decoded,
61 ).toBe(first.keyArn);
62 const write = s3.putBucketEncryption({
63 Bucket: bucket.bucketName,
64 ServerSideEncryptionConfiguration:
65 observed.ServerSideEncryptionConfiguration!,
66 });
67 const deniedWrite = write.pipe(
68 Effect.as(false),
69 Effect.catchTag("AccessDeniedException", () => Effect.succeed(true)),
70 );
71 yield* Effect.gen(function* () {
72 yield* s3.putBucketPolicy({
73 Bucket: bucket.bucketName,
74 Policy: JSON.stringify({
75 Version: "2012-10-17",
76 Statement: [
77 {
78 Effect: "Deny",
79 Principal: "*",
80 Action: "s3:PutEncryptionConfiguration",
81 Resource: bucket.bucketArn,
82 },
83 ],
84 }),
85 });
86 expect(
87 yield* deniedWrite.pipe(
88 Effect.repeat({
89 until: Boolean,
90 schedule: Schedule.spaced("1 second"),
91 times: 8,
92 }),
93 ),
94 ).toBe(true);
95 // Each call constructs a fresh service, bypassing the per-service initialization cache.
96 expect(yield* initialize(first.keyArn)).toEqual([]);
97 expect(yield* initialize(first.keyArn, false)).toEqual([]);
98 expect(yield* deniedWrite).toBe(true);
99 }).pipe(
100 Effect.ensuring(
101 s3
102 .deleteBucketPolicy({ Bucket: bucket.bucketName })
103 .pipe(Effect.orDie),
104 ),
105 );
106 yield* write.pipe(
107 Effect.retry({
108 while: (error) => error._tag === "AccessDeniedException",
109 schedule: Schedule.spaced("1 second"),
110 times: 8,
111 }),
112 );
113
114 yield* initialize(second.keyArn);
115 const changed = (yield* s3.getBucketEncryption({
116 Bucket: bucket.bucketName,
117 })).ServerSideEncryptionConfiguration!.Rules[0]!
118 .ApplyServerSideEncryptionByDefault!.KMSMasterKeyID;
119 expect(
120 Redacted.isRedacted(changed) ? Redacted.value(changed) : changed,
121 ).toBe(second.keyArn);
122 yield* initialize(second.keyArn, true);
123 expect(
124 (yield* s3.getBucketEncryption({ Bucket: bucket.bucketName }))
125 .ServerSideEncryptionConfiguration?.Rules[0]?.BucketKeyEnabled,
126 ).toBe(true);
127 const defaultState = yield* makeS3State({
128 bucketName: bucket.bucketName,
129 prefix: "pr1587",
130 });
131 expect(yield* defaultState.listStacks()).toEqual([]);
132 const defaults = (yield* s3.getBucketEncryption({
133 Bucket: bucket.bucketName,
134 })).ServerSideEncryptionConfiguration!.Rules[0]!;
135 expect(defaults.ApplyServerSideEncryptionByDefault?.SSEAlgorithm).toBe(
136 "AES256",
137 );
138 expect(
139 defaults.ApplyServerSideEncryptionByDefault?.KMSMasterKeyID,
140 ).toBeUndefined();
141 expect(defaults.BucketKeyEnabled ?? false).toBe(false);
142 expect(defaults.BlockedEncryptionTypes?.EncryptionType).toEqual(["NONE"]);
143 yield* stack.destroy();
144 const absent = yield* s3
145 .getBucketLocation({ Bucket: bucket.bucketName })
146 .pipe(
147 Effect.as(false),
148 Effect.catchTag("NoSuchBucket", () => Effect.succeed(true)),
149 Effect.repeat({
150 until: Boolean,
151 schedule: Schedule.spaced("1 second"),
152 times: 8,
153 }),
154 );
155 expect(absent).toBe(true);
156 for (const key of [first, second]) {
157 expect(
158 (yield* kms.describeKey({ KeyId: key.keyId })).KeyMetadata?.KeyState,
159 ).toBe("PendingDeletion");
160 }
161 }),
162 {
163 tags: [
164 "provider:aws",
165 "provider:aws:kms",
166 "provider:aws:s3",
167 "provider:aws:statestore",
168 "live",
169 ],
170 timeout: 120_000,
171 },
172);
173
174for (const blocked of ["SSE-C", "NONE"] as const) {
175 test.provider(
176 `state initialization restores encryption defaults after external ${blocked} settings`,
177 (stack) =>
178 Effect.gen(function* () {
179 yield* stack.destroy();
180 const bucket = yield* stack.deploy(
181 AWS.S3.Bucket("EncryptionBlocksStateBucket", {}),
182 );
183 const initialize = Effect.gen(function* () {
184 const state = yield* makeS3State({
185 bucketName: bucket.bucketName,
186 prefix: "pr1588",
187 });
188 return yield* state.listStacks();
189 });
190 expect(yield* initialize).toEqual([]);
191 yield* s3.putBucketEncryption({
192 Bucket: bucket.bucketName,
193 ServerSideEncryptionConfiguration: {
194 Rules: [
195 {
196 ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "aws:kms" },
197 BlockedEncryptionTypes: { EncryptionType: [blocked] },
198 },
199 ],
200 },
201 });
202 const before = (yield* s3.getBucketEncryption({
203 Bucket: bucket.bucketName,
204 })).ServerSideEncryptionConfiguration!.Rules[0]!;
205 expect(before.BlockedEncryptionTypes?.EncryptionType).toEqual([
206 blocked,
207 ]);
208 expect(before.ApplyServerSideEncryptionByDefault?.SSEAlgorithm).toBe(
209 "aws:kms",
210 );
211 // Re-running the generator constructs a fresh state service and rechecks cloud configuration.
212 expect(yield* initialize).toEqual([]);
213 const after = (yield* s3.getBucketEncryption({
214 Bucket: bucket.bucketName,
215 })).ServerSideEncryptionConfiguration!.Rules[0]!;
216 expect(after.ApplyServerSideEncryptionByDefault?.SSEAlgorithm).toBe(
217 "AES256",
218 );
219 expect(
220 after.ApplyServerSideEncryptionByDefault?.KMSMasterKeyID,
221 ).toBeUndefined();
222 expect(after.BucketKeyEnabled ?? false).toBe(false);
223 expect(after.BlockedEncryptionTypes?.EncryptionType).toEqual(["NONE"]);
224 yield* stack.destroy();
225 const absent = yield* s3
226 .getBucketLocation({ Bucket: bucket.bucketName })
227 .pipe(
228 Effect.as(false),
229 Effect.catchTag("NoSuchBucket", () => Effect.succeed(true)),
230 Effect.repeat({
231 until: Boolean,
232 schedule: Schedule.spaced("1 second"),
233 times: 8,
234 }),
235 );
236 expect(absent).toBe(true);
237 }),
238 {
239 tags: [
240 "provider:aws",
241 "provider:aws:s3",
242 "provider:aws:statestore",
243 "live",
244 ],
245 timeout: 120_000,
246 },
247 );
248}
249
250test.provider(
251 "state services manage encryption blocks and restore defaults on removal",
252 (stack) =>
253 Effect.gen(function* () {
254 yield* stack.destroy();
255 const { bucket, blockedBucket } = yield* stack.deploy(
256 Effect.gen(function* () {
257 const bucket = yield* AWS.S3.Bucket(
258 "ManagedStateEncryptionBlocks",
259 {},
260 );
261 const blockedBucket = yield* AWS.S3.Bucket(
262 "BlockedNoOpStateBucket",
263 {},
264 );
265 return { bucket, blockedBucket };
266 }),
267 );
268 const initialize = (
269 blockedEncryptionTypes: AWS.S3.BucketEncryption["blockedEncryptionTypes"],
270 bucketName = bucket.bucketName,
271 ) =>
272 Effect.gen(function* () {
273 const state = yield* makeS3State({
274 bucketName,
275 prefix: "pr1588-managed",
276 encryption: { sseAlgorithm: "AES256", blockedEncryptionTypes },
277 });
278 return yield* state.listStacks();
279 });
280 const readRule = s3
281 .getBucketEncryption({ Bucket: bucket.bucketName })
282 .pipe(
283 Effect.map(
284 (result) => result.ServerSideEncryptionConfiguration!.Rules[0]!,
285 ),
286 );
287 const block = s3.putBucketEncryption({
288 Bucket: bucket.bucketName,
289 ServerSideEncryptionConfiguration: {
290 Rules: [
291 {
292 ApplyServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" },
293 BlockedEncryptionTypes: { EncryptionType: ["SSE-C"] },
294 },
295 ],
296 },
297 });
298 yield* block;
299 expect(yield* initialize([])).toEqual([]);
300 expect((yield* readRule).BlockedEncryptionTypes?.EncryptionType).toEqual([
301 "NONE",
302 ]);
303 yield* block;
304 expect(yield* initialize([])).toEqual([]);
305 expect((yield* readRule).BlockedEncryptionTypes?.EncryptionType).toEqual([
306 "NONE",
307 ]);
308 expect(yield* initialize(["SSE-C"])).toEqual([]);
309 expect((yield* readRule).BlockedEncryptionTypes?.EncryptionType).toEqual([
310 "SSE-C",
311 ]);
312 expect(yield* initialize(undefined)).toEqual([]);
313 expect((yield* readRule).BlockedEncryptionTypes?.EncryptionType).toEqual([
314 "NONE",
315 ]);
316
317 // Keep each deny policy until bucket deletion; policy removal is eventually consistent.
318 const settings: "SSE-C"[][] = [[], ["SSE-C"]];
319 for (const types of settings) {
320 const target = types.length ? blockedBucket : bucket;
321 expect(yield* initialize(types, target.bucketName)).toEqual([]);
322 const rule = (yield* s3.getBucketEncryption({
323 Bucket: target.bucketName,
324 })).ServerSideEncryptionConfiguration!.Rules[0]!;
325 expect(rule.BlockedEncryptionTypes?.EncryptionType).toEqual(
326 types.length ? ["SSE-C"] : ["NONE"],
327 );
328 const probeWrite = s3
329 .putBucketEncryption({
330 Bucket: target.bucketName,
331 ServerSideEncryptionConfiguration: {
332 Rules: [
333 {
334 ApplyServerSideEncryptionByDefault: {
335 SSEAlgorithm: "AES256",
336 },
337 BucketKeyEnabled: false,
338 BlockedEncryptionTypes: {
339 EncryptionType: types.length ? types : ["NONE"],
340 },
341 },
342 ],
343 },
344 })
345 .pipe(
346 Effect.as(false),
347 Effect.catchTag("AccessDeniedException", () =>
348 Effect.succeed(true),
349 ),
350 );
351 yield* s3.putBucketPolicy({
352 Bucket: target.bucketName,
353 Policy: JSON.stringify({
354 Version: "2012-10-17",
355 Statement: [
356 {
357 Effect: "Deny",
358 Principal: "*",
359 Action: "s3:PutEncryptionConfiguration",
360 Resource: target.bucketArn,
361 },
362 ],
363 }),
364 });
365 expect(
366 yield* probeWrite.pipe(
367 Effect.repeat({
368 until: Boolean,
369 schedule: Schedule.spaced("1 second"),
370 times: 8,
371 }),
372 ),
373 ).toBe(true);
374 expect(
375 yield* initialize([...types, ...types], target.bucketName),
376 ).toEqual([]);
377 if (!types.length) {
378 expect(yield* initialize(undefined, target.bucketName)).toEqual([]);
379 }
380 expect(yield* probeWrite).toBe(true);
381 }
382 yield* stack.destroy();
383 for (const target of [bucket, blockedBucket]) {
384 const absent = yield* s3
385 .getBucketLocation({ Bucket: target.bucketName })
386 .pipe(
387 Effect.as(false),
388 Effect.catchTag("NoSuchBucket", () => Effect.succeed(true)),
389 Effect.repeat({
390 until: Boolean,
391 schedule: Schedule.spaced("1 second"),
392 times: 8,
393 }),
394 );
395 expect(absent).toBe(true);
396 }
397 }),
398 {
399 tags: [
400 "provider:aws",
401 "provider:aws:s3",
402 "provider:aws:statestore",
403 "live",
404 ],
405 timeout: 120_000,
406 },
407);
408
409const STACK = "S3StateStoreTestStack";
410
411/**
412 * Guaranteed out-of-band cleanup: `deleteStack` is idempotent (list +
413 * batched delete), and `Effect.orDie` collapses the finalizer's error
414 * channel to `never` so it is a valid `Effect.ensuring` finalizer.
415 * Every test pre-cleans AND finalizes with this so a failing assertion
416 * can never leave state objects behind in the bucket.
417 */
418const cleanStage = (state: StateService, stage: string) =>
419 state.deleteStack({ stack: STACK, stage }).pipe(Effect.orDie);
420
421const resource = (fqn: string, attr: Record<string, unknown>): ResourceState =>
422 ({
423 resourceType: "test:resource",
424 namespace: undefined,
425 fqn,
426 logicalId: fqn,
427 instanceId: `instance-${fqn}`,
428 providerVersion: 1,
429 status: "created",
430 downstream: [],
431 bindings: [],
432 props: {},
433 attr,
434 }) as ResourceState;
435
436test.provider(
437 "set/get/list/delete round-trips state through S3",
438 (stack) =>
439 Effect.gen(function* () {
440 yield* stack.destroy();
441 const bucket = yield* stack.deploy(
442 AWS.S3.Bucket("StateBucket", { forceDestroy: true }),
443 );
444 const state = yield* makeS3State({
445 bucketName: bucket.bucketName,
446 prefix: "test-state",
447 });
448 const stage = "round-trip";
449
450 // start from a clean slate (idempotent)
451 yield* state.deleteStack({ stack: STACK, stage });
452
453 yield* Effect.gen(function* () {
454 const a = resource("Parent/ResourceA", { value: "a" });
455 const b = resource("ResourceB", { value: "b" });
456
457 yield* state.set({ stack: STACK, stage, fqn: a.fqn, value: a });
458 yield* state.set({ stack: STACK, stage, fqn: b.fqn, value: b });
459
460 expect(yield* state.get({ stack: STACK, stage, fqn: a.fqn })).toEqual(
461 a,
462 );
463 expect(
464 yield* state.get({ stack: STACK, stage, fqn: "does-not-exist" }),
465 ).toBeUndefined();
466
467 const fqns = yield* state.list({ stack: STACK, stage });
468 expect([...fqns].sort()).toEqual(["Parent/ResourceA", "ResourceB"]);
469
470 expect(yield* state.listStacks()).toContain(STACK);
471 expect(yield* state.listStages(STACK)).toContain(stage);
472
473 yield* state.delete({ stack: STACK, stage, fqn: a.fqn });
474 expect(
475 yield* state.get({ stack: STACK, stage, fqn: a.fqn }),
476 ).toBeUndefined();
477 // deleting a missing resource is a no-op
478 yield* state.delete({ stack: STACK, stage, fqn: a.fqn });
479
480 yield* state.deleteStack({ stack: STACK, stage });
481 expect(yield* state.list({ stack: STACK, stage })).toEqual([]);
482 }).pipe(Effect.ensuring(cleanStage(state, stage)));
483 yield* stack.destroy();
484 }),
485 {
486 tags: [
487 "provider:aws",
488 "provider:aws:s3",
489 "provider:aws:statestore",
490 "live",
491 ],
492 timeout: 120_000,
493 },
494);
495
496test.provider(
497 "existing state buckets converge to secure defaults",
498 (stack) =>
499 Effect.gen(function* () {
500 const { accountId, region } = yield* AWS.AWSEnvironment.current;
501 const suffix = yield* Effect.sync(() =>
502 createHash("sha256").update(stack.stage).digest("hex").slice(0, 8),
503 );
504 const bucketName = createStateBucketName(
505 `security-${suffix}-${accountId}`,
506 region,
507 );
508 const deleteBucket = s3.deleteBucket({ Bucket: bucketName }).pipe(
509 Effect.catchTag("NoSuchBucket", () => Effect.void),
510 Effect.orDie,
511 );
512
513 yield* s3
514 .deleteBucket({ Bucket: bucketName })
515 .pipe(Effect.catchTag("NoSuchBucket", () => Effect.void));
516
517 yield* Effect.gen(function* () {
518 // First create the bucket, then simulate out-of-band configuration
519 // drift. The second state service targets the SAME existing bucket and
520 // must repair every setting from observed cloud state.
521 const initial = yield* makeS3State({
522 bucketName,
523 prefix: "security-test",
524 });
525 yield* initial.listStacks();
526
527 yield* s3.putBucketVersioning({
528 Bucket: bucketName,
529 VersioningConfiguration: { Status: "Suspended" },
530 });
531 yield* s3.putBucketEncryption({
532 Bucket: bucketName,
533 ServerSideEncryptionConfiguration: {
534 Rules: [
535 {
536 ApplyServerSideEncryptionByDefault: {
537 SSEAlgorithm: "aws:kms",
538 },
539 },
540 ],
541 },
542 });
543 yield* s3.putPublicAccessBlock({
544 Bucket: bucketName,
545 PublicAccessBlockConfiguration: {
546 BlockPublicAcls: false,
547 IgnorePublicAcls: false,
548 BlockPublicPolicy: false,
549 RestrictPublicBuckets: false,
550 },
551 });
552 yield* s3.putBucketOwnershipControls({
553 Bucket: bucketName,
554 OwnershipControls: {
555 Rules: [{ ObjectOwnership: "ObjectWriter" }],
556 },
557 });
558
559 // Bucket configuration reads are eventually consistent. Observe the
560 // injected drift before reconciliation, or a stale AES256 read can
561 // incorrectly make the provider conclude that no repair is needed.
562 const driftedEncryption = yield* s3
563 .getBucketEncryption({ Bucket: bucketName })
564 .pipe(
565 Effect.map(
566 (result) =>
567 result.ServerSideEncryptionConfiguration?.Rules?.[0]
568 ?.ApplyServerSideEncryptionByDefault?.SSEAlgorithm,
569 ),
570 Effect.repeat({
571 until: (algorithm) => algorithm === "aws:kms",
572 schedule: Schedule.spaced("1 second"),
573 times: 8,
574 }),
575 );
576 expect(driftedEncryption).toBe("aws:kms");
577
578 const secured = yield* makeS3State({
579 bucketName,
580 prefix: "security-test",
581 });
582 yield* secured.listStacks();
583
584 const versioning = yield* s3.getBucketVersioning({
585 Bucket: bucketName,
586 });
587 expect(versioning.Status).toBe("Enabled");
588
589 const encryption = yield* s3.getBucketEncryption({
590 Bucket: bucketName,
591 });
592 expect(
593 encryption.ServerSideEncryptionConfiguration?.Rules?.[0]
594 ?.ApplyServerSideEncryptionByDefault?.SSEAlgorithm,
595 ).toBe("AES256");
596
597 const publicAccess = yield* s3.getPublicAccessBlock({
598 Bucket: bucketName,
599 });
600 expect(publicAccess.PublicAccessBlockConfiguration).toEqual({
601 BlockPublicAcls: true,
602 IgnorePublicAcls: true,
603 BlockPublicPolicy: true,
604 RestrictPublicBuckets: true,
605 });
606
607 const ownership = yield* s3.getBucketOwnershipControls({
608 Bucket: bucketName,
609 });
610 expect(ownership.OwnershipControls?.Rules?.[0]?.ObjectOwnership).toBe(
611 "BucketOwnerEnforced",
612 );
613 }).pipe(Effect.ensuring(deleteBucket));
614 }),
615 {
616 tags: [
617 "provider:aws",
618 "provider:aws:s3",
619 "provider:aws:statestore",
620 "live",
621 ],
622 timeout: 120_000,
623 },
624);
625
626test.provider(
627 "stack outputs are stored separately from resources",
628 (stack) =>
629 Effect.gen(function* () {
630 yield* stack.destroy();
631 const bucket = yield* stack.deploy(
632 AWS.S3.Bucket("StateBucket", { forceDestroy: true }),
633 );
634 const state = yield* makeS3State({
635 bucketName: bucket.bucketName,
636 prefix: "test-state",
637 });
638 const stage = "outputs";
639
640 yield* state.deleteStack({ stack: STACK, stage });
641
642 yield* Effect.gen(function* () {
643 expect(yield* state.getOutput({ stack: STACK, stage })).toBeUndefined();
644
645 yield* state.setOutput({
646 stack: STACK,
647 stage,
648 value: { url: "https://example.com" },
649 });
650 expect(yield* state.getOutput({ stack: STACK, stage })).toEqual({
651 url: "https://example.com",
652 });
653
654 // the output bookkeeping object must not leak into list()
655 expect(yield* state.list({ stack: STACK, stage })).toEqual([]);
656
657 yield* state.deleteStack({ stack: STACK, stage });
658 expect(yield* state.getOutput({ stack: STACK, stage })).toBeUndefined();
659 }).pipe(Effect.ensuring(cleanStage(state, stage)));
660 yield* stack.destroy();
661 }),
662 {
663 tags: [
664 "provider:aws",
665 "provider:aws:s3",
666 "provider:aws:statestore",
667 "live",
668 ],
669 timeout: 120_000,
670 },
671);
672
673test.provider(
674 "getReplacedResources returns only replaced resources",
675 (stack) =>
676 Effect.gen(function* () {
677 yield* stack.destroy();
678 const bucket = yield* stack.deploy(
679 AWS.S3.Bucket("StateBucket", { forceDestroy: true }),
680 );
681 const state = yield* makeS3State({
682 bucketName: bucket.bucketName,
683 prefix: "test-state",
684 });
685 const stage = "replaced";
686
687 yield* state.deleteStack({ stack: STACK, stage });
688
689 yield* Effect.gen(function* () {
690 const created = resource("Created", { value: "created" });
691 const replaced = {
692 ...resource("Replaced", { value: "replaced" }),
693 status: "replaced",
694 } as ResourceState;
695
696 yield* state.set({
697 stack: STACK,
698 stage,
699 fqn: created.fqn,
700 value: created,
701 });
702 yield* state.set({
703 stack: STACK,
704 stage,
705 fqn: replaced.fqn,
706 value: replaced,
707 });
708
709 const result = yield* state.getReplacedResources({
710 stack: STACK,
711 stage,
712 });
713 expect(result.map((r) => r.fqn)).toEqual(["Replaced"]);
714 }).pipe(Effect.ensuring(cleanStage(state, stage)));
715 yield* stack.destroy();
716 }),
717 {
718 tags: [
719 "provider:aws",
720 "provider:aws:s3",
721 "provider:aws:statestore",
722 "live",
723 ],
724 timeout: 120_000,
725 },
726);