@aws-blocks/core 0.2.0 → 0.4.0

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.
Files changed (178) hide show
  1. package/README.md +13 -0
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +32 -0
  4. package/dist/api.test.js +22 -0
  5. package/dist/bb-utils.d.ts +1 -0
  6. package/dist/bb-utils.d.ts.map +1 -1
  7. package/dist/bb-utils.js +3 -0
  8. package/dist/cdk/apigateway-account.d.ts +33 -0
  9. package/dist/cdk/apigateway-account.d.ts.map +1 -0
  10. package/dist/cdk/apigateway-account.js +60 -0
  11. package/dist/cdk/blocks-backend.d.ts +43 -9
  12. package/dist/cdk/blocks-backend.d.ts.map +1 -1
  13. package/dist/cdk/blocks-backend.js +69 -73
  14. package/dist/cdk/blocks-backend.test.js +78 -69
  15. package/dist/cdk/blocks-defaults.d.ts +92 -2
  16. package/dist/cdk/blocks-defaults.d.ts.map +1 -1
  17. package/dist/cdk/blocks-defaults.js +22 -2
  18. package/dist/cdk/blocks-defaults.test.js +21 -2
  19. package/dist/cdk/blocks-stack.test.js +94 -37
  20. package/dist/cdk/compute/compute-registry.d.ts +19 -0
  21. package/dist/cdk/compute/compute-registry.d.ts.map +1 -0
  22. package/dist/cdk/compute/compute-registry.js +38 -0
  23. package/dist/cdk/compute/compute.d.ts +2 -0
  24. package/dist/cdk/compute/compute.d.ts.map +1 -1
  25. package/dist/cdk/compute/compute.js +8 -0
  26. package/dist/cdk/compute/default-compute-factory.d.ts +34 -0
  27. package/dist/cdk/compute/default-compute-factory.d.ts.map +1 -0
  28. package/dist/cdk/compute/default-compute-factory.js +3 -0
  29. package/dist/cdk/config-registry.d.ts +34 -4
  30. package/dist/cdk/config-registry.d.ts.map +1 -1
  31. package/dist/cdk/config-registry.js +83 -25
  32. package/dist/cdk/config-registry.test.d.ts +2 -0
  33. package/dist/cdk/config-registry.test.d.ts.map +1 -0
  34. package/dist/cdk/config-registry.test.js +115 -0
  35. package/dist/cdk/index.d.ts +76 -7
  36. package/dist/cdk/index.d.ts.map +1 -1
  37. package/dist/cdk/index.js +99 -9
  38. package/dist/cdk/internal.d.ts +1 -0
  39. package/dist/cdk/internal.d.ts.map +1 -1
  40. package/dist/client/index.d.ts +1 -1
  41. package/dist/client/index.d.ts.map +1 -1
  42. package/dist/client/index.js +1 -1
  43. package/dist/common/config.d.ts +34 -0
  44. package/dist/common/config.d.ts.map +1 -1
  45. package/dist/common/config.js +45 -3
  46. package/dist/common/config.test.js +19 -0
  47. package/dist/common/index.d.ts +8 -0
  48. package/dist/common/index.d.ts.map +1 -1
  49. package/dist/errors.d.ts +16 -0
  50. package/dist/errors.d.ts.map +1 -1
  51. package/dist/errors.js +20 -0
  52. package/dist/hosting-secrets.d.ts +48 -0
  53. package/dist/hosting-secrets.d.ts.map +1 -0
  54. package/dist/hosting-secrets.js +57 -0
  55. package/dist/hosting-secrets.test.d.ts +2 -0
  56. package/dist/hosting-secrets.test.d.ts.map +1 -0
  57. package/dist/hosting-secrets.test.js +81 -0
  58. package/dist/hosting.d.ts +97 -6
  59. package/dist/hosting.d.ts.map +1 -1
  60. package/dist/hosting.js +91 -23
  61. package/dist/hosting.test.js +182 -10
  62. package/dist/index.cdk.d.ts +13 -12
  63. package/dist/index.cdk.d.ts.map +1 -1
  64. package/dist/index.cdk.js +13 -9
  65. package/dist/index.d.ts +8 -7
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +9 -5
  68. package/dist/lambda-handler.d.ts +17 -0
  69. package/dist/lambda-handler.d.ts.map +1 -1
  70. package/dist/lambda-handler.js +52 -3
  71. package/dist/lambda-handler.test.js +196 -1
  72. package/dist/pipeline/index.d.ts +24 -2
  73. package/dist/pipeline/index.d.ts.map +1 -1
  74. package/dist/pipeline/index.js +41 -1
  75. package/dist/pipeline.test.d.ts +2 -0
  76. package/dist/pipeline.test.d.ts.map +1 -0
  77. package/dist/pipeline.test.js +47 -0
  78. package/dist/rpc.d.ts +21 -0
  79. package/dist/rpc.d.ts.map +1 -1
  80. package/dist/rpc.js +34 -0
  81. package/dist/rpc.test.js +38 -1
  82. package/dist/scripts/config.d.ts +15 -0
  83. package/dist/scripts/config.d.ts.map +1 -0
  84. package/dist/scripts/config.js +29 -0
  85. package/dist/scripts/deploy.d.ts.map +1 -1
  86. package/dist/scripts/deploy.js +4 -0
  87. package/dist/scripts/dev-server.d.ts +8 -0
  88. package/dist/scripts/dev-server.d.ts.map +1 -1
  89. package/dist/scripts/dev-server.js +31 -0
  90. package/dist/scripts/generate-client.d.ts.map +1 -1
  91. package/dist/scripts/generate-client.js +9 -4
  92. package/dist/scripts/generate-client.test.js +19 -4
  93. package/dist/scripts/index.d.ts +13 -10
  94. package/dist/scripts/index.d.ts.map +1 -1
  95. package/dist/scripts/index.js +11 -8
  96. package/dist/scripts/preflight-credentials.d.ts +30 -0
  97. package/dist/scripts/preflight-credentials.d.ts.map +1 -0
  98. package/dist/scripts/preflight-credentials.js +108 -0
  99. package/dist/scripts/preflight-credentials.test.d.ts +2 -0
  100. package/dist/scripts/preflight-credentials.test.d.ts.map +1 -0
  101. package/dist/scripts/preflight-credentials.test.js +77 -0
  102. package/dist/scripts/sandbox-args.test.d.ts +2 -0
  103. package/dist/scripts/sandbox-args.test.d.ts.map +1 -0
  104. package/dist/scripts/sandbox-args.test.js +77 -0
  105. package/dist/scripts/sandbox-empty-buckets.test.d.ts +2 -0
  106. package/dist/scripts/sandbox-empty-buckets.test.d.ts.map +1 -0
  107. package/dist/scripts/sandbox-empty-buckets.test.js +171 -0
  108. package/dist/scripts/sandbox.d.ts +106 -0
  109. package/dist/scripts/sandbox.d.ts.map +1 -1
  110. package/dist/scripts/sandbox.js +255 -40
  111. package/dist/scripts/secret.d.ts +15 -0
  112. package/dist/scripts/secret.d.ts.map +1 -0
  113. package/dist/scripts/secret.js +29 -0
  114. package/dist/scripts/secret.test.d.ts +2 -0
  115. package/dist/scripts/secret.test.d.ts.map +1 -0
  116. package/dist/scripts/secret.test.js +28 -0
  117. package/dist/scripts/typegen.d.ts +15 -0
  118. package/dist/scripts/typegen.d.ts.map +1 -0
  119. package/dist/scripts/typegen.js +16 -0
  120. package/dist/secret-naming.d.ts +58 -0
  121. package/dist/secret-naming.d.ts.map +1 -0
  122. package/dist/secret-naming.js +67 -0
  123. package/dist/secret-naming.test.d.ts +2 -0
  124. package/dist/secret-naming.test.d.ts.map +1 -0
  125. package/dist/secret-naming.test.js +40 -0
  126. package/dist/version.d.ts +1 -1
  127. package/dist/version.js +1 -1
  128. package/package.json +4 -1
  129. package/src/api.test.ts +25 -0
  130. package/src/api.ts +39 -0
  131. package/src/bb-utils.ts +3 -0
  132. package/src/cdk/apigateway-account.ts +66 -0
  133. package/src/cdk/blocks-backend.test.ts +291 -272
  134. package/src/cdk/blocks-backend.ts +88 -83
  135. package/src/cdk/blocks-defaults.test.ts +25 -2
  136. package/src/cdk/blocks-defaults.ts +94 -2
  137. package/src/cdk/blocks-stack.test.ts +186 -118
  138. package/src/cdk/compute/compute-registry.ts +45 -0
  139. package/src/cdk/compute/compute.ts +10 -0
  140. package/src/cdk/compute/default-compute-factory.ts +38 -0
  141. package/src/cdk/config-registry.test.ts +135 -0
  142. package/src/cdk/config-registry.ts +92 -34
  143. package/src/cdk/index.ts +129 -10
  144. package/src/cdk/internal.ts +1 -0
  145. package/src/client/index.ts +1 -1
  146. package/src/common/config.test.ts +21 -0
  147. package/src/common/config.ts +47 -3
  148. package/src/common/index.ts +8 -0
  149. package/src/errors.ts +21 -0
  150. package/src/hosting-secrets.test.ts +106 -0
  151. package/src/hosting-secrets.ts +106 -0
  152. package/src/hosting.test.ts +272 -63
  153. package/src/hosting.ts +200 -68
  154. package/src/index.cdk.ts +80 -38
  155. package/src/index.ts +35 -16
  156. package/src/lambda-handler.test.ts +215 -1
  157. package/src/lambda-handler.ts +54 -2
  158. package/src/pipeline/index.ts +64 -11
  159. package/src/pipeline.test.ts +55 -0
  160. package/src/rpc.test.ts +44 -1
  161. package/src/rpc.ts +41 -0
  162. package/src/scripts/config.ts +35 -0
  163. package/src/scripts/deploy.ts +5 -0
  164. package/src/scripts/dev-server.ts +40 -0
  165. package/src/scripts/generate-client.test.ts +43 -8
  166. package/src/scripts/generate-client.ts +70 -65
  167. package/src/scripts/index.ts +20 -17
  168. package/src/scripts/preflight-credentials.test.ts +110 -0
  169. package/src/scripts/preflight-credentials.ts +131 -0
  170. package/src/scripts/sandbox-args.test.ts +92 -0
  171. package/src/scripts/sandbox-empty-buckets.test.ts +191 -0
  172. package/src/scripts/sandbox.ts +287 -39
  173. package/src/scripts/secret.test.ts +35 -0
  174. package/src/scripts/secret.ts +35 -0
  175. package/src/scripts/typegen.ts +18 -0
  176. package/src/secret-naming.test.ts +59 -0
  177. package/src/secret-naming.ts +99 -0
  178. package/src/version.ts +1 -1
@@ -0,0 +1,171 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { describe, it } from 'node:test';
4
+ import assert from 'node:assert';
5
+ import { emptyBucket, listStackBucketNames, runDestroyWithRetries, toDeleteObjects } from './sandbox.js';
6
+ // The teardown bucket-emptying must delete BOTH live versions and delete
7
+ // markers — a versioned bucket left with either still blocks `cdk destroy`.
8
+ // This is the exact combination that, done wrong, silently no-ops.
9
+ describe('toDeleteObjects', () => {
10
+ it('combines versions and delete markers', () => {
11
+ const out = toDeleteObjects({
12
+ Versions: [
13
+ { Key: 'a', VersionId: 'v1' },
14
+ { Key: 'b', VersionId: 'v2' },
15
+ ],
16
+ DeleteMarkers: [{ Key: 'a', VersionId: 'dm1' }],
17
+ });
18
+ assert.deepStrictEqual(out, [
19
+ { Key: 'a', VersionId: 'v1' },
20
+ { Key: 'b', VersionId: 'v2' },
21
+ { Key: 'a', VersionId: 'dm1' },
22
+ ]);
23
+ });
24
+ it('returns [] for an empty bucket (no Versions/DeleteMarkers keys)', () => {
25
+ assert.deepStrictEqual(toDeleteObjects({}), []);
26
+ });
27
+ it('tolerates only versions or only delete markers', () => {
28
+ assert.deepStrictEqual(toDeleteObjects({ Versions: [{ Key: 'x', VersionId: 'v' }] }), [
29
+ { Key: 'x', VersionId: 'v' },
30
+ ]);
31
+ assert.deepStrictEqual(toDeleteObjects({ DeleteMarkers: [{ Key: 'y', VersionId: 'd' }] }), [
32
+ { Key: 'y', VersionId: 'd' },
33
+ ]);
34
+ });
35
+ it('drops entries without a Key rather than emitting a bad DeleteObjects payload', () => {
36
+ assert.deepStrictEqual(toDeleteObjects({ Versions: [{ VersionId: 'orphan' }] }), []);
37
+ });
38
+ });
39
+ describe('listStackBucketNames', () => {
40
+ it('follows NextToken across pages and returns only S3 buckets', async () => {
41
+ const seen = [];
42
+ const send = async (cmd) => {
43
+ seen.push(cmd.input.NextToken);
44
+ if (cmd.input.NextToken === undefined) {
45
+ return {
46
+ StackResourceSummaries: [
47
+ { ResourceType: 'AWS::S3::Bucket', PhysicalResourceId: 'b1' },
48
+ { ResourceType: 'AWS::Lambda::Function', PhysicalResourceId: 'fn' },
49
+ ],
50
+ NextToken: 't2',
51
+ };
52
+ }
53
+ return { StackResourceSummaries: [{ ResourceType: 'AWS::S3::Bucket', PhysicalResourceId: 'b2' }] };
54
+ };
55
+ const cfn = { send };
56
+ const buckets = await listStackBucketNames(cfn, 'stack');
57
+ assert.deepStrictEqual(buckets, ['b1', 'b2']);
58
+ assert.deepStrictEqual(seen, [undefined, 't2']); // paged exactly twice
59
+ });
60
+ });
61
+ describe('emptyBucket', () => {
62
+ it('deletes versions + markers, re-lists, and stops when empty', async () => {
63
+ let lists = 0;
64
+ const deletePayloads = [];
65
+ const send = async (cmd) => {
66
+ if (cmd.constructor.name === 'ListObjectVersionsCommand') {
67
+ lists++;
68
+ return lists === 1
69
+ ? { Versions: [{ Key: 'a', VersionId: 'v1' }], DeleteMarkers: [{ Key: 'a', VersionId: 'd1' }] }
70
+ : {};
71
+ }
72
+ if (cmd.constructor.name === 'DeleteObjectsCommand') {
73
+ deletePayloads.push(cmd.input.Delete.Objects);
74
+ return {};
75
+ }
76
+ throw new Error(`unexpected ${cmd.constructor.name}`);
77
+ };
78
+ await emptyBucket({ send }, 'bucket');
79
+ assert.strictEqual(lists, 2); // listed, deleted, re-listed → empty
80
+ assert.deepStrictEqual(deletePayloads, [
81
+ [
82
+ { Key: 'a', VersionId: 'v1' },
83
+ { Key: 'a', VersionId: 'd1' },
84
+ ],
85
+ ]);
86
+ });
87
+ it('stops (no infinite loop) when DeleteObjects reports Errors', async () => {
88
+ let lists = 0;
89
+ let deletes = 0;
90
+ const send = async (cmd) => {
91
+ if (cmd.constructor.name === 'ListObjectVersionsCommand') {
92
+ lists++;
93
+ return { Versions: [{ Key: 'locked', VersionId: 'v1' }] };
94
+ }
95
+ if (cmd.constructor.name === 'DeleteObjectsCommand') {
96
+ deletes++;
97
+ return { Errors: [{ Key: 'locked', Code: 'AccessDenied' }] };
98
+ }
99
+ throw new Error(`unexpected ${cmd.constructor.name}`);
100
+ };
101
+ await emptyBucket({ send }, 'bucket');
102
+ assert.strictEqual(lists, 1); // did NOT re-list after Errors
103
+ assert.strictEqual(deletes, 1);
104
+ });
105
+ });
106
+ // The sev2 fix is the wiring: on a destroy failure, buckets get emptied BEFORE
107
+ // the retry. runDestroyWithRetries takes its side effects as deps so we can
108
+ // exercise that ordering without spawning cdk.
109
+ describe('runDestroyWithRetries', () => {
110
+ it('empties buckets before retrying a failed destroy', async () => {
111
+ const events = [];
112
+ let attempts = 0;
113
+ await runDestroyWithRetries({
114
+ runDestroy: () => {
115
+ attempts++;
116
+ events.push(`destroy#${attempts}`);
117
+ if (attempts === 1)
118
+ throw new Error('bucket not empty');
119
+ },
120
+ listStackNames: () => {
121
+ events.push('list');
122
+ return ['stackA', 'stackB'];
123
+ },
124
+ emptyBuckets: async (names) => {
125
+ events.push(`empty:${names.join(',')}`);
126
+ },
127
+ sleep: async () => {
128
+ events.push('sleep');
129
+ },
130
+ retryDelays: [1],
131
+ });
132
+ assert.strictEqual(attempts, 2);
133
+ // buckets emptied (with the resolved stack names) before the retry destroy
134
+ assert.deepStrictEqual(events, ['destroy#1', 'list', 'empty:stackA,stackB', 'sleep', 'destroy#2']);
135
+ });
136
+ it('does not empty or sleep when the first destroy succeeds', async () => {
137
+ let emptied = false;
138
+ let slept = false;
139
+ let listed = false;
140
+ await runDestroyWithRetries({
141
+ runDestroy: () => { },
142
+ listStackNames: () => {
143
+ listed = true;
144
+ return [];
145
+ },
146
+ emptyBuckets: async () => {
147
+ emptied = true;
148
+ },
149
+ sleep: async () => {
150
+ slept = true;
151
+ },
152
+ });
153
+ assert.strictEqual(emptied, false);
154
+ assert.strictEqual(slept, false);
155
+ assert.strictEqual(listed, false);
156
+ });
157
+ it('throws after exhausting retries', async () => {
158
+ let attempts = 0;
159
+ await assert.rejects(runDestroyWithRetries({
160
+ runDestroy: () => {
161
+ attempts++;
162
+ throw new Error('still failing');
163
+ },
164
+ listStackNames: () => [],
165
+ emptyBuckets: async () => { },
166
+ sleep: async () => { },
167
+ retryDelays: [1, 1],
168
+ }), /still failing/);
169
+ assert.strictEqual(attempts, 3); // initial attempt + 2 retries
170
+ });
171
+ });
@@ -1,3 +1,5 @@
1
+ import type { CloudFormationClient } from '@aws-sdk/client-cloudformation';
2
+ import type { S3Client } from '@aws-sdk/client-s3';
1
3
  export interface SandboxOptions {
2
4
  backendPath: string;
3
5
  outDir?: string;
@@ -6,6 +8,110 @@ export interface SandboxOptions {
6
8
  /** Custom dev server command. Defaults to 'npx vite'. For Next.js use 'npx next dev'. */
7
9
  devCommand?: string;
8
10
  }
11
+ export interface SandboxDeployArgsOptions {
12
+ /** Directory CDK writes stack outputs to (relative to the project root). */
13
+ outDir: string;
14
+ /** Project root passed to synth as `--context projectRoot=…` (usually `process.cwd()`). */
15
+ projectRoot: string;
16
+ /** Backend entry passed to `--app` so CDK synthesizes from the app definition. */
17
+ backendPath: string;
18
+ }
19
+ /**
20
+ * Build the `npm exec cdk -- deploy …` argv used by the sandbox deploy.
21
+ *
22
+ * Kept pure (no I/O) so the argv contract — in particular the flags below — can
23
+ * be asserted directly, the same way {@link buildCdkDeployArgs} is for the
24
+ * production path.
25
+ *
26
+ * - `--all`: an app that uses Lambda@Edge (e.g. a Next.js route with
27
+ * `export const runtime = 'edge'`) synthesizes a SECOND stack
28
+ * (`edge-lambda-stack-*`, region us-east-1) in addition to the main hosting
29
+ * stack. Without `--all`, CDK refuses with "specify which stacks to use".
30
+ * Deploying every stack in a sandbox app is the intended behavior.
31
+ * - `--method direct`: skip CloudFormation change-set creation and call
32
+ * UpdateStack directly. This is the baseline sandbox deployment method that
33
+ * `--express` builds on.
34
+ * - `--express` (**CloudFormation Express Mode — sandbox only**): the real
35
+ * speedup. CloudFormation reports each stack operation complete as soon as
36
+ * the resource's configuration is applied, WITHOUT waiting for full
37
+ * stabilization. Because it skips stabilization it also disables automatic
38
+ * rollback by default, so a failed deploy can leave the stack in a failed
39
+ * state — acceptable for the throwaway sandbox iteration loop, never for
40
+ * production. We leave rollback at Express Mode's default (off) and do NOT
41
+ * pass `--rollback`. The production deploy path (`deploy.ts`) deliberately
42
+ * uses neither `--express` nor `--method direct`: it keeps a reviewable
43
+ * change set and full stabilization with rollback, which are exactly the
44
+ * safety signals a production deploy should keep. Requires an aws-cdk CLI
45
+ * new enough to expose `--express`; our pinned `^2.1138.0` has it.
46
+ */
47
+ export declare function buildSandboxDeployArgs({ outDir, projectRoot, backendPath }: SandboxDeployArgsOptions): string[];
48
+ /**
49
+ * Operator-facing recovery guidance printed when a sandbox deploy fails.
50
+ *
51
+ * Express Mode (see {@link buildSandboxDeployArgs}) disables CloudFormation's
52
+ * automatic rollback, so a failed deploy does NOT clean up after itself: a
53
+ * failed *first* deploy leaves the stack in `CREATE_FAILED`/`ROLLBACK_COMPLETE`
54
+ * and a failed update can land in `UPDATE_ROLLBACK_FAILED`. In those states
55
+ * CloudFormation refuses the next `UpdateStack`, so a plain re-run of
56
+ * `npm run sandbox` would hit the same wall — a silent dead-end for the fast
57
+ * loop. This spells out the exact way out instead of leaving the operator to
58
+ * discover it via a cryptic CloudFormation error.
59
+ *
60
+ * Kept pure so it can be asserted in a unit test.
61
+ */
62
+ export declare function sandboxFailureRecoveryHint(): string;
9
63
  export declare function startSandbox(options: SandboxOptions): Promise<string | undefined>;
64
+ /**
65
+ * Flatten a ListObjectVersions response into the `{ Key, VersionId }[]` shape
66
+ * DeleteObjects expects, combining live versions AND delete markers. Exported
67
+ * for unit testing — getting this combination wrong (e.g. missing the delete
68
+ * markers) leaves a "versioned" bucket that still can't be deleted.
69
+ */
70
+ export declare function toDeleteObjects(listed: {
71
+ Versions?: Array<{
72
+ Key?: string;
73
+ VersionId?: string;
74
+ }>;
75
+ DeleteMarkers?: Array<{
76
+ Key?: string;
77
+ VersionId?: string;
78
+ }>;
79
+ }): Array<{
80
+ Key: string;
81
+ VersionId?: string;
82
+ }>;
83
+ /** Collect a stack's S3 bucket physical names (paginated — a large stack has
84
+ * >100 resources, so a single page can miss the hosting bucket that blocks the
85
+ * delete). Returns [] if the stack is already gone / not accessible. */
86
+ export declare function listStackBucketNames(cfn: CloudFormationClient, stackName: string): Promise<string[]>;
87
+ /** Empty a versioned bucket: delete every object version + delete marker in
88
+ * pages of 1000 (the DeleteObjects limit). Stops if a page reports per-key
89
+ * errors (object-lock / retention) so it can't loop forever. */
90
+ export declare function emptyBucket(s3: S3Client, bucket: string): Promise<void>;
91
+ /** Injectable dependencies for {@link runDestroyWithRetries} — real ones in
92
+ * `destroySandbox`, fakes in tests so the retry/empty-before-retry wiring (the
93
+ * sev2 fix) is exercised without spawning cdk. */
94
+ export interface DestroyRetryDeps {
95
+ /** Run one `cdk destroy` attempt; throws on failure. */
96
+ runDestroy: () => void;
97
+ /** Resolve the app's stack names (for bucket enumeration). */
98
+ listStackNames: () => string[];
99
+ /** Empty the given stacks' versioned S3 buckets. */
100
+ emptyBuckets: (stackNames: string[]) => Promise<void>;
101
+ /** Sleep (VPC-ENI detach window). */
102
+ sleep: (ms: number) => Promise<void>;
103
+ /** Backoff between retries. Default: 1min, then 2min. */
104
+ retryDelays?: number[];
105
+ }
106
+ /**
107
+ * Retry `cdk destroy`, clearing the two known teardown blockers before each
108
+ * retry: (1) non-empty versioned S3 buckets — `cdk destroy` relies on each
109
+ * bucket's autoDeleteObjects Lambda, which never provisioned if the CREATE
110
+ * failed, so the bucket blocks the delete and the stack (+ its IAM roles) leaks;
111
+ * we empty them out-of-band — and (2) VPC ENIs that take 60-120s to detach (the
112
+ * backoff). Stack names are resolved once and reused (topology is stable across
113
+ * attempts). Factored out with injectable deps so this behavior is unit-testable.
114
+ */
115
+ export declare function runDestroyWithRetries(deps: DestroyRetryDeps): Promise<void>;
10
116
  export declare function destroySandbox(backendPath: string): Promise<void>;
11
117
  //# sourceMappingURL=sandbox.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/scripts/sandbox.ts"],"names":[],"mappings":"AA+BA,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,cAAc,+BAkMzD;AAED,wBAAsB,cAAc,CAAC,WAAW,EAAE,MAAM,iBAwCvD"}
1
+ {"version":3,"file":"sandbox.d.ts","sourceRoot":"","sources":["../../src/scripts/sandbox.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAkBnD,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,yFAAyF;IACzF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,wBAAwB;IACvC,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,2FAA2F;IAC3F,WAAW,EAAE,MAAM,CAAC;IACpB,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,EAAE,wBAAwB,GAAG,MAAM,EAAE,CAY/G;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAYnD;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,cAAc,+BAwMzD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE;IACvC,QAAQ,CAAC,EAAE,KAAK,CAAC;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvD,aAAa,CAAC,EAAE,KAAK,CAAC;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC5D,GAAG,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAI7C;AAED;;wEAEwE;AACxE,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAY1G;AAED;;gEAEgE;AAChE,wBAAsB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB7E;AAmED;;kDAEkD;AAClD,MAAM,WAAW,gBAAgB;IAChC,wDAAwD;IACxD,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,8DAA8D;IAC9D,cAAc,EAAE,MAAM,MAAM,EAAE,CAAC;IAC/B,oDAAoD;IACpD,YAAY,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,qCAAqC;IACrC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBjF;AAED,wBAAsB,cAAc,CAAC,WAAW,EAAE,MAAM,iBAyBvD"}
@@ -5,9 +5,11 @@ import { writeFileSync, mkdirSync, readFileSync } from "node:fs";
5
5
  import { join, resolve, dirname } from "node:path";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
7
  import { ensureSecrets, loadEnvFile } from './ensure-secrets.js';
8
+ import { assertAwsCredentials } from './preflight-credentials.js';
8
9
  import { applyExternalMigrations } from './external-migrations-step.js';
9
10
  import { trackCommand } from '../telemetry/trackCommand.js';
10
11
  import { buildAndSendEvent } from '../telemetry/client.js';
12
+ import { classifyError } from '../telemetry/trackCommand.js';
11
13
  import { getCdkTelemetryEnv } from './cdk-telemetry-env.js';
12
14
  import { runSync, spawnCommand } from './run-command.js';
13
15
  import { terminateProcessTree } from './process-tree.js';
@@ -27,6 +29,74 @@ async function importBackendForRegistry(backendPath) {
27
29
  // best-effort — telemetry never affects the command
28
30
  }
29
31
  }
32
+ /**
33
+ * Build the `npm exec cdk -- deploy …` argv used by the sandbox deploy.
34
+ *
35
+ * Kept pure (no I/O) so the argv contract — in particular the flags below — can
36
+ * be asserted directly, the same way {@link buildCdkDeployArgs} is for the
37
+ * production path.
38
+ *
39
+ * - `--all`: an app that uses Lambda@Edge (e.g. a Next.js route with
40
+ * `export const runtime = 'edge'`) synthesizes a SECOND stack
41
+ * (`edge-lambda-stack-*`, region us-east-1) in addition to the main hosting
42
+ * stack. Without `--all`, CDK refuses with "specify which stacks to use".
43
+ * Deploying every stack in a sandbox app is the intended behavior.
44
+ * - `--method direct`: skip CloudFormation change-set creation and call
45
+ * UpdateStack directly. This is the baseline sandbox deployment method that
46
+ * `--express` builds on.
47
+ * - `--express` (**CloudFormation Express Mode — sandbox only**): the real
48
+ * speedup. CloudFormation reports each stack operation complete as soon as
49
+ * the resource's configuration is applied, WITHOUT waiting for full
50
+ * stabilization. Because it skips stabilization it also disables automatic
51
+ * rollback by default, so a failed deploy can leave the stack in a failed
52
+ * state — acceptable for the throwaway sandbox iteration loop, never for
53
+ * production. We leave rollback at Express Mode's default (off) and do NOT
54
+ * pass `--rollback`. The production deploy path (`deploy.ts`) deliberately
55
+ * uses neither `--express` nor `--method direct`: it keeps a reviewable
56
+ * change set and full stabilization with rollback, which are exactly the
57
+ * safety signals a production deploy should keep. Requires an aws-cdk CLI
58
+ * new enough to expose `--express`; our pinned `^2.1138.0` has it.
59
+ */
60
+ export function buildSandboxDeployArgs({ outDir, projectRoot, backendPath }) {
61
+ return [
62
+ "exec", "cdk", "--", "deploy",
63
+ "--all",
64
+ "--method", "direct",
65
+ "--express",
66
+ "--require-approval", "never",
67
+ "--outputs-file", `${outDir}/outputs.json`,
68
+ "--context", `projectRoot=${projectRoot}`,
69
+ "--context", "sandboxMode=true",
70
+ "--app", `npm exec tsx -- -C cdk ${backendPath}`,
71
+ ];
72
+ }
73
+ /**
74
+ * Operator-facing recovery guidance printed when a sandbox deploy fails.
75
+ *
76
+ * Express Mode (see {@link buildSandboxDeployArgs}) disables CloudFormation's
77
+ * automatic rollback, so a failed deploy does NOT clean up after itself: a
78
+ * failed *first* deploy leaves the stack in `CREATE_FAILED`/`ROLLBACK_COMPLETE`
79
+ * and a failed update can land in `UPDATE_ROLLBACK_FAILED`. In those states
80
+ * CloudFormation refuses the next `UpdateStack`, so a plain re-run of
81
+ * `npm run sandbox` would hit the same wall — a silent dead-end for the fast
82
+ * loop. This spells out the exact way out instead of leaving the operator to
83
+ * discover it via a cryptic CloudFormation error.
84
+ *
85
+ * Kept pure so it can be asserted in a unit test.
86
+ */
87
+ export function sandboxFailureRecoveryHint() {
88
+ return [
89
+ "",
90
+ "ℹ️ Express Mode leaves failed sandbox stacks in place (automatic rollback is off).",
91
+ " If the next `npm run sandbox` reports the stack cannot be updated, recover with:",
92
+ "",
93
+ " npm run sandbox:destroy # tears the failed stack down (handles ROLLBACK_COMPLETE)",
94
+ " npm run sandbox # redeploy from a clean slate",
95
+ "",
96
+ " If destroy reports UPDATE_ROLLBACK_FAILED, first run:",
97
+ " aws cloudformation continue-update-rollback --stack-name <your-sandbox-stack>",
98
+ ].join("\n");
99
+ }
30
100
  export async function startSandbox(options) {
31
101
  const { backendPath, outDir = ".blocks-sandbox", clientPort = 3000, deployOnly = false, devCommand } = options;
32
102
  const sandboxStartTime = Date.now();
@@ -39,6 +109,23 @@ export async function startSandbox(options) {
39
109
  throw e;
40
110
  }
41
111
  process.env.BLOCKS_STAGE = 'sandbox';
112
+ // Fail fast if AWS credentials are missing/expired, before spending ~10s in
113
+ // synth only to hit an opaque CDK credential error. Unlike deploy(), startSandbox
114
+ // isn't wrapped in trackCommand, so emit the FAIL event manually (mirroring the
115
+ // CDK-deploy failure path below) before rethrowing — otherwise a no-creds abort
116
+ // sends no telemetry.
117
+ try {
118
+ await assertAwsCredentials('sandbox');
119
+ }
120
+ catch (error) {
121
+ buildAndSendEvent({
122
+ command: 'sandbox',
123
+ state: 'FAIL',
124
+ duration: Date.now() - sandboxStartTime,
125
+ error: classifyError(error),
126
+ });
127
+ throw error;
128
+ }
42
129
  // Provision connection string to SSM SecureString.
43
130
  // On first deploy, creates the parameter. On subsequent deploys, updates if changed.
44
131
  // projectRoot is process.cwd() — the same value passed to cdk as --context
@@ -59,21 +146,10 @@ export async function startSandbox(options) {
59
146
  console.log("🚀 Deploying to AWS...");
60
147
  console.log(" (This may take a few minutes on first deploy)");
61
148
  try {
62
- runSync("npm", [
63
- "exec", "cdk", "--", "deploy",
64
- // `--all`: an app that uses Lambda@Edge (e.g. a Next.js route with
65
- // `export const runtime = 'edge'`) synthesizes a SECOND stack
66
- // (`edge-lambda-stack-*`, region us-east-1) in addition to the main
67
- // hosting stack. Without `--all`, CDK refuses with "specify which
68
- // stacks to use". Deploying every stack in a sandbox app is the
69
- // intended behavior, so select them all.
70
- "--all",
71
- "--require-approval", "never",
72
- "--outputs-file", `${outDir}/outputs.json`,
73
- "--context", `projectRoot=${process.cwd()}`,
74
- "--context", "sandboxMode=true",
75
- "--app", `npm exec tsx -- -C cdk ${backendPath}`,
76
- ], {
149
+ runSync("npm",
150
+ // Argv (including sandbox-only CloudFormation Express Mode, `--express`)
151
+ // is built by buildSandboxDeployArgs see its doc for why each flag exists.
152
+ buildSandboxDeployArgs({ outDir, projectRoot: process.cwd(), backendPath }), {
77
153
  stdio: "inherit",
78
154
  env: { ...process.env, NODE_OPTIONS: "--conditions=cdk", ...getCdkTelemetryEnv('sandbox') },
79
155
  });
@@ -86,6 +162,7 @@ export async function startSandbox(options) {
86
162
  error: { code: 'CDK_DEPLOY_FAILED', phase: 'deploy' },
87
163
  });
88
164
  console.error("\n❌ Deployment failed.");
165
+ console.error(sandboxFailureRecoveryHint());
89
166
  throw error;
90
167
  }
91
168
  const outputs = JSON.parse(readFileSync(`${outDir}/outputs.json`, "utf-8"));
@@ -202,6 +279,160 @@ export async function startSandbox(options) {
202
279
  process.on("SIGTERM", cleanup);
203
280
  await new Promise(() => { });
204
281
  }
282
+ /**
283
+ * Flatten a ListObjectVersions response into the `{ Key, VersionId }[]` shape
284
+ * DeleteObjects expects, combining live versions AND delete markers. Exported
285
+ * for unit testing — getting this combination wrong (e.g. missing the delete
286
+ * markers) leaves a "versioned" bucket that still can't be deleted.
287
+ */
288
+ export function toDeleteObjects(listed) {
289
+ return [...(listed.Versions ?? []), ...(listed.DeleteMarkers ?? [])]
290
+ .filter((v) => v.Key !== undefined)
291
+ .map((v) => ({ Key: v.Key, VersionId: v.VersionId }));
292
+ }
293
+ /** Collect a stack's S3 bucket physical names (paginated — a large stack has
294
+ * >100 resources, so a single page can miss the hosting bucket that blocks the
295
+ * delete). Returns [] if the stack is already gone / not accessible. */
296
+ export async function listStackBucketNames(cfn, stackName) {
297
+ const { ListStackResourcesCommand } = await import('@aws-sdk/client-cloudformation');
298
+ const buckets = [];
299
+ let token;
300
+ do {
301
+ const res = await cfn.send(new ListStackResourcesCommand({ StackName: stackName, NextToken: token }));
302
+ for (const r of res.StackResourceSummaries ?? []) {
303
+ if (r.ResourceType === 'AWS::S3::Bucket' && r.PhysicalResourceId)
304
+ buckets.push(r.PhysicalResourceId);
305
+ }
306
+ token = res.NextToken;
307
+ } while (token);
308
+ return buckets;
309
+ }
310
+ /** Empty a versioned bucket: delete every object version + delete marker in
311
+ * pages of 1000 (the DeleteObjects limit). Stops if a page reports per-key
312
+ * errors (object-lock / retention) so it can't loop forever. */
313
+ export async function emptyBucket(s3, bucket) {
314
+ const { ListObjectVersionsCommand, DeleteObjectsCommand } = await import('@aws-sdk/client-s3');
315
+ while (true) {
316
+ const listed = await s3.send(new ListObjectVersionsCommand({ Bucket: bucket, MaxKeys: 1000 }));
317
+ const objects = toDeleteObjects(listed);
318
+ if (objects.length === 0)
319
+ return;
320
+ const res = await s3.send(new DeleteObjectsCommand({ Bucket: bucket, Delete: { Objects: objects, Quiet: true } }));
321
+ if (res.Errors && res.Errors.length > 0) {
322
+ // A bucket we can't fully empty (object-lock / retention / AccessDenied)
323
+ // will keep blocking `cdk destroy`, so the stack won't tear down until it's
324
+ // resolved by hand. Log loudly with the first error code — this is the
325
+ // kind of silent skip that leaks a stack, so make it visible in CI output.
326
+ const first = res.Errors[0];
327
+ console.warn(` ⚠️ ${bucket}: ${res.Errors.length} object(s) could NOT be deleted ` +
328
+ `(e.g. ${first.Key}: ${first.Code}). This bucket will block stack teardown ` +
329
+ `until cleared manually.`);
330
+ return;
331
+ }
332
+ }
333
+ }
334
+ /**
335
+ * Empty every versioned S3 bucket owned by the given stacks. `cdk destroy`
336
+ * relies on each bucket's `autoDeleteObjects` custom-resource Lambda to empty
337
+ * it on teardown — but that Lambda never provisions if the CREATE failed (e.g.
338
+ * a partial/aborted deploy), so the versioned bucket blocks the delete and the
339
+ * stack (and its IAM roles) leak. Emptying out-of-band before the retry lets
340
+ * the delete complete. Best-effort: any per-stack/per-bucket error is logged
341
+ * and skipped so teardown still proceeds.
342
+ */
343
+ async function emptySandboxBuckets(stackNames) {
344
+ try {
345
+ const { CloudFormationClient } = await import('@aws-sdk/client-cloudformation');
346
+ const { S3Client } = await import('@aws-sdk/client-s3');
347
+ const cfn = new CloudFormationClient({});
348
+ // `followRegionRedirects` makes the client transparently retry against a
349
+ // bucket's real region on a PermanentRedirect, so a bucket that lives
350
+ // outside AWS_REGION (e.g. a us-east-1 Lambda@Edge stack's bucket) is still
351
+ // emptied instead of failing silently — closing the exact no-op this fix
352
+ // targets. It needs no GetBucketLocation permission.
353
+ const s3 = new S3Client({ followRegionRedirects: true });
354
+ for (const stackName of stackNames) {
355
+ let buckets = [];
356
+ try {
357
+ buckets = await listStackBucketNames(cfn, stackName);
358
+ }
359
+ catch {
360
+ continue; // stack already gone / not accessible
361
+ }
362
+ for (const b of buckets) {
363
+ try {
364
+ await emptyBucket(s3, b);
365
+ }
366
+ catch (e) {
367
+ console.warn(` ⚠️ could not empty ${b}: ${e.message}`);
368
+ }
369
+ }
370
+ }
371
+ }
372
+ catch (e) {
373
+ console.warn(` ⚠️ bucket-emptying step skipped: ${e.message}`);
374
+ }
375
+ }
376
+ /** Resolve the sandbox app's stack names via `cdk ls` (best-effort; [] on error). */
377
+ function listSandboxStackNames(backendPath, cdkEnv) {
378
+ try {
379
+ const out = execFileSync('npm',
380
+ // Single-quote backendPath: CDK re-runs the --app string through a shell,
381
+ // so an unquoted path containing a space would split.
382
+ ['exec', 'cdk', '--', 'ls', '--context', 'sandboxMode=true', '--app', `npm exec tsx -- -C cdk '${backendPath}'`],
383
+ // Capture stderr so a genuine synth/`cdk ls` failure can be logged below
384
+ // rather than being indistinguishable from "app has zero stacks".
385
+ { env: cdkEnv, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
386
+ return out
387
+ .split('\n')
388
+ .map((s) => s.trim())
389
+ .filter(Boolean);
390
+ }
391
+ catch (e) {
392
+ // Warn (don't stay silent): if this returns [] on a real failure, the retry
393
+ // skips bucket-emptying — the exact silent-no-op this change exists to avoid.
394
+ const detail = e.stderr || e.message;
395
+ console.warn(` ⚠️ could not list sandbox stacks via 'cdk ls'; skipping bucket-emptying: ${detail}`);
396
+ return [];
397
+ }
398
+ }
399
+ /**
400
+ * Retry `cdk destroy`, clearing the two known teardown blockers before each
401
+ * retry: (1) non-empty versioned S3 buckets — `cdk destroy` relies on each
402
+ * bucket's autoDeleteObjects Lambda, which never provisioned if the CREATE
403
+ * failed, so the bucket blocks the delete and the stack (+ its IAM roles) leaks;
404
+ * we empty them out-of-band — and (2) VPC ENIs that take 60-120s to detach (the
405
+ * backoff). Stack names are resolved once and reused (topology is stable across
406
+ * attempts). Factored out with injectable deps so this behavior is unit-testable.
407
+ */
408
+ export async function runDestroyWithRetries(deps) {
409
+ const retryDelays = deps.retryDelays ?? [60_000, 120_000];
410
+ let stackNames;
411
+ for (let attempt = 0;; attempt++) {
412
+ try {
413
+ deps.runDestroy();
414
+ console.log(attempt === 0 ? '\n✅ Sandbox destroyed!' : '\n✅ Sandbox destroyed on retry!');
415
+ return;
416
+ }
417
+ catch (error) {
418
+ if (attempt < retryDelays.length) {
419
+ if (stackNames === undefined)
420
+ stackNames = deps.listStackNames();
421
+ if (stackNames.length > 0) {
422
+ console.log('\n🧹 Emptying versioned S3 buckets before retry...');
423
+ await deps.emptyBuckets(stackNames);
424
+ }
425
+ const delaySec = retryDelays[attempt] / 1000;
426
+ console.log(`\n⏳ Stack deletion failed. Retrying in ${delaySec}s (waiting for resource cleanup)...`);
427
+ await deps.sleep(retryDelays[attempt]);
428
+ }
429
+ else {
430
+ console.error('\n❌ Destroy failed after retries.');
431
+ throw error;
432
+ }
433
+ }
434
+ }
435
+ }
205
436
  export async function destroySandbox(backendPath) {
206
437
  return trackCommand('sandbox:destroy', async () => {
207
438
  console.log("🗑️ Destroying sandbox...");
@@ -217,32 +448,16 @@ export async function destroySandbox(backendPath) {
217
448
  "exec", "cdk", "--", "destroy",
218
449
  "--force",
219
450
  "--context", "sandboxMode=true",
220
- "--app", `npm exec tsx -- -C cdk ${backendPath}`,
451
+ // Single-quote backendPath: CDK re-runs the --app string through a shell,
452
+ // so an unquoted path containing a space would split.
453
+ "--app", `npm exec tsx -- -C cdk '${backendPath}'`,
221
454
  ];
222
455
  const cdkEnv = { ...process.env, NODE_OPTIONS: "--conditions=cdk", ...getCdkTelemetryEnv('sandbox') };
223
- // Retry with backoff for VPC-dependent resources (e.g. Aurora clusters).
224
- // CloudFormation deletes the cluster first, but its ENIs take 60-120s to
225
- // detach from the VPC subnets asynchronously. The initial destroy fails
226
- // because the subnets still have attached ENIs; retrying after the cleanup
227
- // window lets the VPC delete succeed.
228
- const retryDelays = [60_000, 120_000]; // 1min, then 2min
229
- for (let attempt = 0;; attempt++) {
230
- try {
231
- runSync("npm", cdkArgs, { stdio: "inherit", env: cdkEnv });
232
- console.log(attempt === 0 ? "\n✅ Sandbox destroyed!" : "\n✅ Sandbox destroyed on retry!");
233
- return;
234
- }
235
- catch (error) {
236
- if (attempt < retryDelays.length) {
237
- const delaySec = retryDelays[attempt] / 1000;
238
- console.log(`\n⏳ Stack deletion failed. Retrying in ${delaySec}s (waiting for resource cleanup)...`);
239
- await new Promise(r => setTimeout(r, retryDelays[attempt]));
240
- }
241
- else {
242
- console.error("\n❌ Destroy failed after retries.");
243
- throw error;
244
- }
245
- }
246
- }
456
+ await runDestroyWithRetries({
457
+ runDestroy: () => runSync('npm', cdkArgs, { stdio: 'inherit', env: cdkEnv }),
458
+ listStackNames: () => listSandboxStackNames(backendPath, cdkEnv),
459
+ emptyBuckets: emptySandboxBuckets,
460
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
461
+ });
247
462
  });
248
463
  }
@@ -0,0 +1,15 @@
1
+ /** Set (create or overwrite) a Blocks secret. */
2
+ export declare function setSecret(key: string, value: string, opts?: {
3
+ stage?: string;
4
+ }): Promise<void>;
5
+ /** List Blocks secret keys (names only). */
6
+ export declare function listSecrets(opts?: {
7
+ stage?: string;
8
+ }): Promise<string[]>;
9
+ /** Remove a Blocks secret. */
10
+ export declare function removeSecret(key: string, opts?: {
11
+ stage?: string;
12
+ }): Promise<boolean>;
13
+ /** CLI dispatcher for `blocks secret <set|list|remove> …`. */
14
+ export declare function runSecretCli(argv: string[]): Promise<void>;
15
+ //# sourceMappingURL=secret.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret.d.ts","sourceRoot":"","sources":["../../src/scripts/secret.ts"],"names":[],"mappings":"AAgBA,iDAAiD;AACjD,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAElG;AAED,4CAA4C;AAC5C,wBAAgB,WAAW,CAAC,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAE5E;AAED,8BAA8B;AAC9B,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAEzF;AAED,8DAA8D;AAC9D,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1D"}