@aws-blocks/core 0.1.18 → 0.3.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 (159) hide show
  1. package/dist/cdk/__fixtures__/import-meta-handler.d.ts +6 -0
  2. package/dist/cdk/__fixtures__/import-meta-handler.d.ts.map +1 -0
  3. package/dist/cdk/__fixtures__/import-meta-handler.js +13 -0
  4. package/dist/cdk/blocks-backend.d.ts +46 -9
  5. package/dist/cdk/blocks-backend.d.ts.map +1 -1
  6. package/dist/cdk/blocks-backend.js +66 -67
  7. package/dist/cdk/blocks-backend.test.js +114 -56
  8. package/dist/cdk/blocks-defaults.d.ts +70 -0
  9. package/dist/cdk/blocks-defaults.d.ts.map +1 -0
  10. package/dist/cdk/blocks-defaults.js +27 -0
  11. package/dist/cdk/blocks-defaults.test.d.ts +2 -0
  12. package/dist/cdk/blocks-defaults.test.d.ts.map +1 -0
  13. package/dist/cdk/blocks-defaults.test.js +24 -0
  14. package/dist/cdk/blocks-stack.test.js +74 -31
  15. package/dist/cdk/bundling.d.ts +42 -0
  16. package/dist/cdk/bundling.d.ts.map +1 -0
  17. package/dist/cdk/bundling.js +72 -0
  18. package/dist/cdk/bundling.test.d.ts +2 -0
  19. package/dist/cdk/bundling.test.d.ts.map +1 -0
  20. package/dist/cdk/bundling.test.js +80 -0
  21. package/dist/cdk/compute/compute.d.ts +33 -0
  22. package/dist/cdk/compute/compute.d.ts.map +1 -0
  23. package/dist/cdk/compute/compute.js +28 -0
  24. package/dist/cdk/compute/default-compute-factory.d.ts +33 -0
  25. package/dist/cdk/compute/default-compute-factory.d.ts.map +1 -0
  26. package/dist/cdk/compute/default-compute-factory.js +3 -0
  27. package/dist/cdk/index.d.ts +92 -12
  28. package/dist/cdk/index.d.ts.map +1 -1
  29. package/dist/cdk/index.js +141 -25
  30. package/dist/cdk/internal.d.ts +25 -0
  31. package/dist/cdk/internal.d.ts.map +1 -0
  32. package/dist/cdk/internal.js +27 -0
  33. package/dist/cdk/mixins.d.ts +18 -4
  34. package/dist/cdk/mixins.d.ts.map +1 -1
  35. package/dist/cdk/mixins.js +31 -7
  36. package/dist/cdk/mixins.test.js +45 -0
  37. package/dist/common/index.d.ts +9 -0
  38. package/dist/common/index.d.ts.map +1 -1
  39. package/dist/hosting-secrets.d.ts +48 -0
  40. package/dist/hosting-secrets.d.ts.map +1 -0
  41. package/dist/hosting-secrets.js +57 -0
  42. package/dist/hosting-secrets.test.d.ts +2 -0
  43. package/dist/hosting-secrets.test.d.ts.map +1 -0
  44. package/dist/hosting-secrets.test.js +81 -0
  45. package/dist/hosting.d.ts +88 -6
  46. package/dist/hosting.d.ts.map +1 -1
  47. package/dist/hosting.js +83 -24
  48. package/dist/hosting.test.js +158 -10
  49. package/dist/index.cdk.d.ts +13 -11
  50. package/dist/index.cdk.d.ts.map +1 -1
  51. package/dist/index.cdk.js +13 -8
  52. package/dist/index.d.ts +8 -7
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +9 -5
  55. package/dist/lambda-handler.test.js +66 -0
  56. package/dist/pipeline/index.d.ts +24 -2
  57. package/dist/pipeline/index.d.ts.map +1 -1
  58. package/dist/pipeline/index.js +41 -1
  59. package/dist/pipeline.test.d.ts +2 -0
  60. package/dist/pipeline.test.d.ts.map +1 -0
  61. package/dist/pipeline.test.js +47 -0
  62. package/dist/rpc.d.ts +21 -0
  63. package/dist/rpc.d.ts.map +1 -1
  64. package/dist/rpc.js +46 -3
  65. package/dist/rpc.test.js +58 -1
  66. package/dist/scripts/config.d.ts +15 -0
  67. package/dist/scripts/config.d.ts.map +1 -0
  68. package/dist/scripts/config.js +29 -0
  69. package/dist/scripts/deploy.d.ts.map +1 -1
  70. package/dist/scripts/deploy.js +4 -0
  71. package/dist/scripts/dev-server.d.ts +8 -0
  72. package/dist/scripts/dev-server.d.ts.map +1 -1
  73. package/dist/scripts/dev-server.js +31 -0
  74. package/dist/scripts/generate-client.d.ts.map +1 -1
  75. package/dist/scripts/generate-client.js +9 -4
  76. package/dist/scripts/generate-client.test.js +19 -4
  77. package/dist/scripts/index.d.ts +13 -10
  78. package/dist/scripts/index.d.ts.map +1 -1
  79. package/dist/scripts/index.js +11 -8
  80. package/dist/scripts/preflight-credentials.d.ts +30 -0
  81. package/dist/scripts/preflight-credentials.d.ts.map +1 -0
  82. package/dist/scripts/preflight-credentials.js +108 -0
  83. package/dist/scripts/preflight-credentials.test.d.ts +2 -0
  84. package/dist/scripts/preflight-credentials.test.d.ts.map +1 -0
  85. package/dist/scripts/preflight-credentials.test.js +77 -0
  86. package/dist/scripts/sandbox-args.test.d.ts +2 -0
  87. package/dist/scripts/sandbox-args.test.d.ts.map +1 -0
  88. package/dist/scripts/sandbox-args.test.js +77 -0
  89. package/dist/scripts/sandbox.d.ts +52 -0
  90. package/dist/scripts/sandbox.d.ts.map +1 -1
  91. package/dist/scripts/sandbox.js +92 -15
  92. package/dist/scripts/secret.d.ts +15 -0
  93. package/dist/scripts/secret.d.ts.map +1 -0
  94. package/dist/scripts/secret.js +29 -0
  95. package/dist/scripts/secret.test.d.ts +2 -0
  96. package/dist/scripts/secret.test.d.ts.map +1 -0
  97. package/dist/scripts/secret.test.js +28 -0
  98. package/dist/scripts/telemetry.test.js +2 -0
  99. package/dist/scripts/typegen.d.ts +15 -0
  100. package/dist/scripts/typegen.d.ts.map +1 -0
  101. package/dist/scripts/typegen.js +16 -0
  102. package/dist/secret-naming.d.ts +58 -0
  103. package/dist/secret-naming.d.ts.map +1 -0
  104. package/dist/secret-naming.js +67 -0
  105. package/dist/secret-naming.test.d.ts +2 -0
  106. package/dist/secret-naming.test.d.ts.map +1 -0
  107. package/dist/secret-naming.test.js +40 -0
  108. package/dist/telemetry/environment.d.ts.map +1 -1
  109. package/dist/telemetry/environment.js +3 -1
  110. package/dist/telemetry/telemetry.test.js +10 -0
  111. package/dist/version.d.ts +1 -1
  112. package/dist/version.d.ts.map +1 -1
  113. package/dist/version.js +1 -1
  114. package/package.json +7 -1
  115. package/src/cdk/__fixtures__/import-meta-handler.ts +16 -0
  116. package/src/cdk/blocks-backend.test.ts +308 -237
  117. package/src/cdk/blocks-backend.ts +98 -77
  118. package/src/cdk/blocks-defaults.test.ts +27 -0
  119. package/src/cdk/blocks-defaults.ts +75 -0
  120. package/src/cdk/blocks-stack.test.ts +160 -110
  121. package/src/cdk/bundling.test.ts +90 -0
  122. package/src/cdk/bundling.ts +76 -0
  123. package/src/cdk/compute/compute.ts +37 -0
  124. package/src/cdk/compute/default-compute-factory.ts +37 -0
  125. package/src/cdk/index.ts +178 -27
  126. package/src/cdk/internal.ts +30 -0
  127. package/src/cdk/mixins.test.ts +56 -1
  128. package/src/cdk/mixins.ts +32 -7
  129. package/src/common/index.ts +9 -0
  130. package/src/hosting-secrets.test.ts +106 -0
  131. package/src/hosting-secrets.ts +106 -0
  132. package/src/hosting.test.ts +244 -63
  133. package/src/hosting.ts +180 -68
  134. package/src/index.cdk.ts +79 -36
  135. package/src/index.ts +35 -16
  136. package/src/lambda-handler.test.ts +74 -0
  137. package/src/pipeline/index.ts +64 -11
  138. package/src/pipeline.test.ts +55 -0
  139. package/src/rpc.test.ts +66 -1
  140. package/src/rpc.ts +60 -3
  141. package/src/scripts/config.ts +35 -0
  142. package/src/scripts/deploy.ts +5 -0
  143. package/src/scripts/dev-server.ts +40 -0
  144. package/src/scripts/generate-client.test.ts +43 -8
  145. package/src/scripts/generate-client.ts +70 -65
  146. package/src/scripts/index.ts +20 -17
  147. package/src/scripts/preflight-credentials.test.ts +110 -0
  148. package/src/scripts/preflight-credentials.ts +131 -0
  149. package/src/scripts/sandbox-args.test.ts +92 -0
  150. package/src/scripts/sandbox.ts +102 -15
  151. package/src/scripts/secret.test.ts +35 -0
  152. package/src/scripts/secret.ts +35 -0
  153. package/src/scripts/telemetry.test.ts +2 -0
  154. package/src/scripts/typegen.ts +18 -0
  155. package/src/secret-naming.test.ts +59 -0
  156. package/src/secret-naming.ts +99 -0
  157. package/src/telemetry/environment.ts +3 -1
  158. package/src/telemetry/telemetry.test.ts +12 -0
  159. package/src/version.ts +1 -1
@@ -2,17 +2,17 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import * as cdk from 'aws-cdk-lib';
5
- import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
6
- import * as apigateway from 'aws-cdk-lib/aws-apigateway';
5
+ import type * as apigateway from 'aws-cdk-lib/aws-apigateway';
7
6
  import * as iam from 'aws-cdk-lib/aws-iam';
8
7
  import { CfnGroup } from 'aws-cdk-lib/aws-resourcegroups';
9
8
  import { Construct } from 'constructs';
10
9
  import { pathToFileURL } from 'node:url';
11
- import { DEFAULT_NODE_RUNTIME } from './node-version.js';
12
10
  import { addBlocksStackMetadata } from './stack-metadata.js';
13
11
  import { finalizeConfigRegistry, registerConfig } from './config-registry.js';
14
- import { BLOCKS_NAMESPACE, BLOCKS_RPC_PREFIX } from '../constants.js';
12
+ import type { BlocksDefaults } from './blocks-defaults.js';
15
13
  import { registerBuiltinRoutes } from '../builtin-routes.js';
14
+ import type { Compute } from './compute/compute.js';
15
+ import type { DefaultComputeFactory, LambdaShapedCompute } from './compute/default-compute-factory.js';
16
16
 
17
17
  /**
18
18
  * Validate that the Node.js process was started with `--conditions=cdk`.
@@ -42,21 +42,68 @@ export function assertCdkConditionActive(): void {
42
42
  }
43
43
  }
44
44
 
45
+ /**
46
+ * Timeout of the shared Blocks handler Lambda, and therefore the ceiling on a
47
+ * single invocation. Exported because resources that feed the handler have to
48
+ * size their own timeouts against it (e.g. an SQS queue's visibility timeout).
49
+ */
50
+ export const SHARED_HANDLER_TIMEOUT_SECONDS = 60 * 15;
51
+
45
52
  export interface BlocksBackendProps {
46
53
  backendHandlerPath: string;
47
54
  backendCDKPath: string;
55
+ /**
56
+ * Stack-wide infrastructure defaults applied to every Building Block (removal
57
+ * policy, deletion protection, …). See {@link BlocksDefaults}. Start from
58
+ * `BlocksPresets.sandbox` or `BlocksPresets.production` and override
59
+ * individual fields as needed. A per-block option always wins over the
60
+ * corresponding stack default.
61
+ */
62
+ defaults: BlocksDefaults;
63
+ }
64
+
65
+ /**
66
+ * Core's `create()` props: the public {@link BlocksBackendProps} plus the
67
+ * required `defaultComputeFactory`, spread on by the umbrella (`@aws-blocks/blocks`).
68
+ * Customers use {@link BlocksBackendProps} and never set the factory.
69
+ *
70
+ * @internal
71
+ */
72
+ export interface CoreBlocksBackendProps extends BlocksBackendProps {
73
+ /** Builds the backend's default compute. Injected by `@aws-blocks/blocks`. */
74
+ defaultComputeFactory: DefaultComputeFactory;
48
75
  }
49
76
 
50
- /** Shared infra setup — creates Lambda + API Gateway on the given scope. */
77
+ /**
78
+ * Shared infra setup — provisions the stack-level resources that are NOT owned
79
+ * by a compute: the shared execution role, resource groups, and console-redirect
80
+ * routes.
81
+ */
51
82
  export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id?: string) {
52
- // ── Shared execution role ──────────────────────────────────────────────
83
+ // Fail fast with an actionable message at the create() call site if `defaults`
84
+ // is missing (e.g. a plain-JS caller, `as any`, or a dynamically-built props
85
+ // object) — otherwise the first Building Block to read `scope.defaults` throws
86
+ // a cryptic `Cannot read properties of undefined (reading 'removalPolicy')`.
87
+ if (!props.defaults) {
88
+ throw new Error(
89
+ 'BlocksStack/BlocksBackend requires a `defaults` field. Pass a posture from ' +
90
+ '`@aws-blocks/core/cdk` — typically `defaults: sandboxMode ? BlocksPresets.sandbox : BlocksPresets.production`.',
91
+ );
92
+ }
93
+
94
+ // ── Shared execution role ───────────────────────────────────────────────
53
95
  // A single IAM role that every Building Block grants to. Provisioned here so
54
96
  // it exists before the backend module is imported (Building Blocks reach it
55
97
  // via `scope.executionRole`). Block grants sit on the role's default (inline)
56
- // policy, exactly as they did on the auto-generated NodejsFunction role.
98
+ // policy. AWSLambdaBasicExecutionRole is attached so compute functions retain
99
+ // CloudWatch Logs permissions.
57
100
  //
58
- // AWSLambdaBasicExecutionRole is attached explicitly because the auto-role
59
- // included it by default omitting it would silently break CloudWatch Logs.
101
+ // INVARIANT: this must be a mutable, framework-owned `iam.Role` — never an
102
+ // imported role (`Role.fromRoleArn`/`fromRoleName`), which is immutable by
103
+ // default. On an immutable role, every Building Block's `grant*()` /
104
+ // `addToPrincipalPolicy()` silently becomes a no-op (returns false, no error),
105
+ // so permissions would quietly vanish. If a bring-your-own-role option is ever
106
+ // added, it must resolve to a mutable role (`{ mutable: true }`).
60
107
  const executionRole = new iam.Role(scope, 'BlocksRole', {
61
108
  // CompositePrincipal (rather than a bare ServicePrincipal) so additional
62
109
  // compute types can assume this same shared role as they are introduced
@@ -67,60 +114,7 @@ export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id
67
114
  ],
68
115
  });
69
116
 
70
- const handler = new lambda.NodejsFunction(scope, 'Handler', {
71
- entry: props.backendHandlerPath,
72
- runtime: DEFAULT_NODE_RUNTIME,
73
- handler: 'handler',
74
- role: executionRole,
75
- memorySize: 2048,
76
- timeout: cdk.Duration.seconds(60 * 15),
77
- environment: {
78
- NODE_ENV: 'production',
79
- /**
80
- * BLOCKS_STACK_NAME is used at runtime to derive physical resource names
81
- * (DynamoDB table names, env var prefixes). It must match the CDK-time
82
- * fullId of the BlocksStack/BlocksBackend so resource lookups work correctly.
83
- *
84
- * For BlocksStack: this equals the stack name (id).
85
- * For BlocksBackend: the caller overrides this after construction to include
86
- * the parent stack name for deployment uniqueness.
87
- */
88
- BLOCKS_STACK_NAME: id ?? cdk.Stack.of(scope).stackName,
89
- },
90
- bundling: {
91
- minify: true,
92
- esbuildArgs: { '--conditions': 'aws-runtime' },
93
- },
94
- });
95
-
96
- // In sandbox mode, allow localhost origins so the local dev frontend can
97
- // reach the deployed Lambda API via CORS.
98
- const isSandbox =
99
- scope.node.tryGetContext('sandboxMode') === 'true' ||
100
- scope.node.tryGetContext('sandboxMode') === true;
101
- if (isSandbox) {
102
- handler.addEnvironment('CORS_ALLOWED_ORIGINS', '^https?://(localhost|127\\.0\\.0\\.1)(:\\d+)?$');
103
- }
104
-
105
- const api = new apigateway.RestApi(scope, 'API', {
106
- restApiName: 'Blocks API',
107
- deployOptions: { cachingEnabled: false },
108
- });
109
-
110
- const integration = new apigateway.LambdaIntegration(handler);
111
-
112
- // Build the nested resource tree for /aws-blocks/api.
113
- // Intermediate resource gets a proxy so sub-paths (RawRoutes) still reach Lambda.
114
- const awsBlocksResource = api.root.addResource(BLOCKS_NAMESPACE.slice(1));
115
- awsBlocksResource.addProxy({ defaultIntegration: integration, anyMethod: true });
116
-
117
- const apiResource = awsBlocksResource.addResource('api');
118
- apiResource.addMethod('POST', integration);
119
- apiResource.addMethod('OPTIONS', integration);
120
-
121
- api.root.addProxy({ defaultIntegration: integration, anyMethod: true });
122
-
123
- // ── Resource Groups ────────────────────────────────────────────────────
117
+ // ── Resource Groups ───────────────────────────────────────────────────
124
118
  let rootStack = cdk.Stack.of(scope);
125
119
  while (rootStack.nestedStackParent) rootStack = rootStack.nestedStackParent;
126
120
  const groupPrefix = (id && id !== rootStack.stackName) ? `${rootStack.stackName}-${id}` : rootStack.stackName;
@@ -156,7 +150,7 @@ export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id
156
150
  },
157
151
  });
158
152
 
159
- // ── Console redirect routes ────────────────────────────────────────────
153
+ // ── Console redirect routes ───────────────────────────────────────────
160
154
  const region = cdk.Fn.ref('AWS::Region');
161
155
  const resourcesUrl = cdk.Fn.join('', [
162
156
  'https://', region, '.console.aws.amazon.com/resource-groups/group/',
@@ -172,7 +166,7 @@ export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id
172
166
 
173
167
  registerBuiltinRoutes();
174
168
 
175
- return { handler, gateway: api, apiUrl: `${api.url}${BLOCKS_RPC_PREFIX.slice(1)}`, executionRole };
169
+ return { executionRole };
176
170
  }
177
171
 
178
172
  /**
@@ -193,12 +187,33 @@ export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id
193
187
  * ```
194
188
  */
195
189
  export class BlocksBackend extends Construct {
196
- public readonly apiUrl: string;
197
- public readonly gateway: apigateway.RestApi;
198
- public readonly handler: cdk.aws_lambda_nodejs.NodejsFunction;
199
190
  public readonly backendHandlerPath: string;
200
191
  /** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */
201
192
  public readonly executionRole: iam.IRole;
193
+ /** Infrastructure defaults for Building Blocks created under this backend. */
194
+ public readonly defaults: BlocksDefaults;
195
+ /** The default compute (owns the Lambda function + API Gateway); set in `create()`. @internal */
196
+ _defaultCompute?: Compute;
197
+
198
+ /** The default compute's Lambda function. To be removed once consumers move to the multi-compute model. */
199
+ get handler(): cdk.aws_lambda_nodejs.NodejsFunction {
200
+ return this.requireDefaultCompute().fn;
201
+ }
202
+ /** The default compute's API Gateway REST API. To be removed once consumers move to the multi-compute model. */
203
+ get gateway(): apigateway.RestApi {
204
+ return this.requireDefaultCompute().apiGateway;
205
+ }
206
+ /** The default compute's RPC endpoint URL. To be removed once consumers move to the multi-compute model. */
207
+ get apiUrl(): string {
208
+ return this.requireDefaultCompute().apiUrl;
209
+ }
210
+
211
+ private requireDefaultCompute(): LambdaShapedCompute {
212
+ if (!this._defaultCompute) {
213
+ throw new Error('Blocks backend not fully initialized — access .handler/.gateway/.apiUrl after BlocksBackend.create() resolves.');
214
+ }
215
+ return this._defaultCompute as LambdaShapedCompute;
216
+ }
202
217
 
203
218
  /**
204
219
  * The fullId used by child Scopes to compute their env var names,
@@ -239,21 +254,27 @@ export class BlocksBackend extends Construct {
239
254
  // Expose self to Building Blocks at CDK time
240
255
  (globalThis as any).CURRENT_BLOCKS_STACK = this;
241
256
 
257
+ // Store defaults on the backend (not the stack) so several BlocksBackends
258
+ // in one stack each keep their own posture; Building Blocks resolve them by
259
+ // walking up to their owning backend (see Scope.defaults).
260
+ this.defaults = props.defaults;
261
+
242
262
  const infra = setupBlocksInfra(this, props, id);
243
- this.handler = infra.handler;
244
- this.gateway = infra.gateway;
245
- this.apiUrl = infra.apiUrl;
246
263
  this.executionRole = infra.executionRole;
247
-
248
- // Override BLOCKS_STACK_NAME to include the parent stack name so runtime
249
- // resource lookups (DynamoDB table names) match the CDK-time fullId
250
- // and are unique per deployment.
251
- this.handler.addEnvironment('BLOCKS_STACK_NAME', this.fullId);
264
+ // The default compute (and thus handler/gateway) is created in create(),
265
+ // after construction it derives BLOCKS_STACK_NAME from this.fullId.
252
266
  }
253
267
 
254
- static async create(scope: Construct, id: string, props: BlocksBackendProps) {
268
+ static async create(scope: Construct, id: string, props: CoreBlocksBackendProps) {
255
269
  assertCdkConditionActive();
256
270
  const backend = new BlocksBackend(scope, id, props);
271
+ // Create the default compute before importing the backend: it OWNS the
272
+ // Lambda function + API Gateway (which back .handler/.gateway/.apiUrl), and
273
+ // a block reading `this.compute` in its constructor (during that import)
274
+ // must resolve to it. The factory is supplied by the umbrella
275
+ // @aws-blocks/blocks (which injects LambdaCompute) via props, so core never
276
+ // imports the concrete compute class.
277
+ backend._defaultCompute = props.defaultComputeFactory(backend);
257
278
  // file:// URL (not a raw path) so the cache-busting query works on Windows,
258
279
  // where an absolute path like `D:\...` is rejected as URL scheme `d:`.
259
280
  const backendUrl = pathToFileURL(props.backendCDKPath);
@@ -0,0 +1,27 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * BlocksPresets carry the expected posture. Resolution/anchoring (a block
6
+ * reading its owning backend's defaults, and two backends in one stack keeping
7
+ * separate postures) is covered in blocks-backend.test.ts, which has the
8
+ * fixtures to construct real backends.
9
+ */
10
+ import { test, describe } from 'node:test';
11
+ import assert from 'node:assert';
12
+ import { RemovalPolicy } from 'aws-cdk-lib';
13
+ import { BlocksPresets } from './blocks-defaults.js';
14
+
15
+ describe('BlocksPresets', () => {
16
+ test('sandbox is disposable: DESTROY + deletion protection off + no PITR', () => {
17
+ assert.strictEqual(BlocksPresets.sandbox.removalPolicy, RemovalPolicy.DESTROY);
18
+ assert.strictEqual(BlocksPresets.sandbox.deletionProtection, false);
19
+ assert.strictEqual(BlocksPresets.sandbox.pointInTimeRecovery, false);
20
+ });
21
+
22
+ test('production is durable: RETAIN + deletion protection on + PITR on', () => {
23
+ assert.strictEqual(BlocksPresets.production.removalPolicy, RemovalPolicy.RETAIN);
24
+ assert.strictEqual(BlocksPresets.production.deletionProtection, true);
25
+ assert.strictEqual(BlocksPresets.production.pointInTimeRecovery, true);
26
+ });
27
+ });
@@ -0,0 +1,75 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { RemovalPolicy } from 'aws-cdk-lib';
5
+
6
+ /**
7
+ * Default values for every Amazon-authored Building Block created within a
8
+ * `BlocksStack` or `BlocksBackend`. A per-block option always overrides the
9
+ * corresponding default (`option ?? scope.defaults.field`).
10
+ *
11
+ * Start from {@link BlocksPresets} and override individual fields as needed.
12
+ */
13
+ export interface BlocksDefaults {
14
+ /**
15
+ * What happens to a stateful resource (table, bucket, user pool) when the
16
+ * stack is deleted, via CDK/CloudFormation: `RETAIN` keeps the resource (and
17
+ * its data) behind; `DESTROY` tears it down. Applies only to CDK/CloudFormation
18
+ * deletions.
19
+ */
20
+ removalPolicy: RemovalPolicy;
21
+
22
+ /**
23
+ * Whether to block deletion of a stateful resource. Unlike `removalPolicy`,
24
+ * this guards against deletion through **any** path — CDK/CloudFormation as
25
+ * well as the CLI, API, and AWS console — until it is turned off.
26
+ */
27
+ deletionProtection: boolean;
28
+
29
+ /**
30
+ * CORS origins the compute's API accepts, as regular-expression patterns
31
+ * matched against the request `Origin` header. Empty means no cross-origin
32
+ * requests are allowed. The `sandbox` preset allows localhost so a local dev
33
+ * frontend can reach a deployed API; `production` allows none by default.
34
+ */
35
+ allowedOrigins: string[];
36
+
37
+ /**
38
+ * Whether stateful resources that support continuous backups keep them on by
39
+ * default — e.g. DynamoDB Point-in-Time Recovery, letting you restore to any
40
+ * second in the retention window. On in `production`, off in `sandbox` (where
41
+ * throwaway data isn't worth the backup-storage cost). Blocks whose service
42
+ * has no equivalent simply ignore it.
43
+ *
44
+ * `true` enables backups with the service's default window; `false` disables
45
+ * them; `{ retentionDays: n }` enables them and pins the window (a block
46
+ * clamps/validates `n` to its service's supported range — DynamoDB PITR is
47
+ * 1–35 days). Backups only have a window when on, so the two are one field.
48
+ */
49
+ pointInTimeRecovery: boolean | { retentionDays: number };
50
+ }
51
+
52
+ /**
53
+ * Prepared starting points for {@link BlocksDefaults}. Pick one and override
54
+ * individual fields with a spread:
55
+ *
56
+ * ```ts
57
+ * defaults: { ...BlocksPresets.production, deletionProtection: false }
58
+ * ```
59
+ */
60
+ export const BlocksPresets = {
61
+ /** Disposable development stacks: tear down cleanly, no delete guard, no backups. */
62
+ sandbox: {
63
+ removalPolicy: RemovalPolicy.DESTROY,
64
+ deletionProtection: false,
65
+ allowedOrigins: ['^https?://(localhost|127\\.0\\.0\\.1)(:\\d+)?$'],
66
+ pointInTimeRecovery: false,
67
+ },
68
+ /** Durable, protected posture for permanent deployments. */
69
+ production: {
70
+ removalPolicy: RemovalPolicy.RETAIN,
71
+ deletionProtection: true,
72
+ allowedOrigins: [],
73
+ pointInTimeRecovery: true,
74
+ },
75
+ } satisfies Record<string, BlocksDefaults>;
@@ -1,17 +1,61 @@
1
1
  // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
- import { test, describe, before } from 'node:test';
5
4
  import assert from 'node:assert';
6
- import { fileURLToPath } from 'node:url';
7
5
  import { dirname, join } from 'node:path';
6
+ import { before, describe, test } from 'node:test';
7
+ import { fileURLToPath } from 'node:url';
8
8
  import * as cdk from 'aws-cdk-lib';
9
+ import * as apigateway from 'aws-cdk-lib/aws-apigateway';
10
+ import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
11
+ import type { Construct } from 'constructs';
12
+ import type { ScopeParent } from '../common/index.js';
13
+ import { BLOCKS_RPC_PREFIX } from '../constants.js';
9
14
  import { BlocksBackend } from './blocks-backend.js';
10
- import { BlocksStack, Scope } from './index.js';
15
+ import { Compute } from './compute/compute.js';
16
+ import type { DefaultComputeFactory } from './compute/default-compute-factory.js';
17
+ import { BlocksStack, BlocksPresets, Scope } from './index.js';
18
+
19
+ // A real app gets its default compute from @aws-blocks/bb-lambda-compute (via
20
+ // @aws-blocks/blocks), which core's own tests can't depend on. Use an
21
+ // equivalent inline stub: a Compute that owns a NodejsFunction + API Gateway,
22
+ // so create() can build the default and the handler/gateway/apiUrl accessors
23
+ // and synth-shape assertions have something real to resolve to. It is passed to
24
+ // each create() via the internal `defaultComputeFactory` option (see makeStack /
25
+ // makeBackend), exactly as @aws-blocks/blocks injects LambdaCompute.
26
+ class StubLambdaCompute extends Compute {
27
+ readonly fn: lambda.NodejsFunction;
28
+ readonly apiGateway: apigateway.RestApi;
29
+ readonly apiUrl: string;
30
+
31
+ constructor(scope: ScopeParent, id: string) {
32
+ super(id, { parent: scope });
33
+ this.fn = new lambda.NodejsFunction(this, 'Handler', {
34
+ entry: this.backendHandlerPath,
35
+ runtime: cdk.aws_lambda.Runtime.NODEJS_22_X,
36
+ handler: 'handler',
37
+ role: this.executionRole,
38
+ environment: { BLOCKS_STACK_NAME: this.backendStackName },
39
+ bundling: { minify: true, esbuildArgs: { '--conditions': 'aws-runtime' } },
40
+ });
41
+ this.apiGateway = new apigateway.RestApi(this, 'API', { restApiName: 'Blocks API' });
42
+ this.apiGateway.root.addProxy({
43
+ defaultIntegration: new apigateway.LambdaIntegration(this.fn),
44
+ anyMethod: true,
45
+ });
46
+ this.apiUrl = `${this.apiGateway.url}${BLOCKS_RPC_PREFIX.slice(1)}`;
47
+ }
48
+
49
+ setEnv(key: string, value: string): void {
50
+ this.fn.addEnvironment(key, value);
51
+ }
52
+ }
53
+
54
+ const stubComputeFactory: DefaultComputeFactory = (root) => new StubLambdaCompute(root as never, 'DefaultCompute');
11
55
 
12
56
  // Simulate the CDK condition being active (tests import CDK files directly)
13
57
  before(() => {
14
- process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS ?? '') + ' --conditions=cdk';
58
+ process.env.NODE_OPTIONS = (process.env.NODE_OPTIONS ?? '') + ' --conditions=cdk';
15
59
  });
16
60
 
17
61
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -19,128 +63,134 @@ const handlerPath = join(__dirname, '__fixtures__', 'handler.js');
19
63
  const sideEffectBackendPath = join(__dirname, '__fixtures__', 'side-effect-backend.js');
20
64
  const factoryBackendPath = join(__dirname, '__fixtures__', 'factory-backend.js');
21
65
 
66
+ // Wrap create(), injecting the stub default-compute factory the way
67
+ // @aws-blocks/blocks injects LambdaCompute — so tests don't repeat it.
68
+ const makeStack = (scope: Construct, id: string, backendCDKPath: string) =>
69
+ BlocksStack.create(scope, id, { backendHandlerPath: handlerPath, backendCDKPath, defaults: BlocksPresets.production, defaultComputeFactory: stubComputeFactory });
70
+ const makeBackend = (scope: Construct, id: string, backendCDKPath: string) =>
71
+ BlocksBackend.create(scope, id, { backendHandlerPath: handlerPath, backendCDKPath, defaults: BlocksPresets.production, defaultComputeFactory: stubComputeFactory });
72
+
22
73
  describe('ESM cache-busting (multi-stage)', () => {
23
- test('BlocksStack.create() with same backendCDKPath but different IDs produces constructs in each', async () => {
24
- const app = new cdk.App();
25
-
26
- const stack1 = await BlocksStack.create(app, 'PipelineStage1', {
27
- backendHandlerPath: handlerPath,
28
- backendCDKPath: sideEffectBackendPath,
29
- });
30
-
31
- const stack2 = await BlocksStack.create(app, 'PipelineStage2', {
32
- backendHandlerPath: handlerPath,
33
- backendCDKPath: sideEffectBackendPath,
34
- });
35
-
36
- const findMarker = (scope: any) => scope.node.tryFindChild('SideEffectMarker');
37
-
38
- assert.ok(
39
- findMarker(stack1),
40
- 'First stack should have SideEffectMarker from module side effect',
41
- );
42
- assert.ok(
43
- findMarker(stack2),
44
- 'Second stack should have SideEffectMarker from re-executed module (cache busted)',
45
- );
46
- });
74
+ test('BlocksStack.create() with same backendCDKPath but different IDs produces constructs in each', async () => {
75
+ const app = new cdk.App();
76
+
77
+ const stack1 = await makeStack(app, 'PipelineStage1', sideEffectBackendPath);
78
+
79
+ const stack2 = await makeStack(app, 'PipelineStage2', sideEffectBackendPath);
80
+
81
+ const findMarker = (scope: any) => scope.node.tryFindChild('SideEffectMarker');
82
+
83
+ assert.ok(findMarker(stack1), 'First stack should have SideEffectMarker from module side effect');
84
+ assert.ok(
85
+ findMarker(stack2),
86
+ 'Second stack should have SideEffectMarker from re-executed module (cache busted)',
87
+ );
88
+ });
47
89
  });
48
90
 
49
91
  describe('factory function support', () => {
50
- test('BlocksStack.create() calls default export function with the stack instance', async () => {
51
- const app = new cdk.App();
92
+ test('BlocksStack.create() calls default export function with the stack instance', async () => {
93
+ const app = new cdk.App();
52
94
 
53
- const stack = await BlocksStack.create(app, 'FactoryBlocksStack', {
54
- backendHandlerPath: handlerPath,
55
- backendCDKPath: factoryBackendPath,
56
- });
95
+ const stack = await makeStack(app, 'FactoryBlocksStack', factoryBackendPath);
57
96
 
58
- const marker = stack.node.tryFindChild('FactoryMarker');
59
- assert.ok(marker, 'Factory function should have created FactoryMarker on the stack');
60
- });
97
+ const marker = stack.node.tryFindChild('FactoryMarker');
98
+ assert.ok(marker, 'Factory function should have created FactoryMarker on the stack');
99
+ });
61
100
  });
62
101
 
63
102
  describe('legacy side-effect mode (no default export)', () => {
64
- test('module with only side effects still registers constructs via globalThis', async () => {
65
- const app = new cdk.App();
66
- const stack = new cdk.Stack(app, 'LegacyTestStack');
67
-
68
- const backend = await BlocksBackend.create(stack, 'LegacyStage', {
69
- backendHandlerPath: handlerPath,
70
- backendCDKPath: sideEffectBackendPath,
71
- });
72
-
73
- const marker = backend.node.tryFindChild('SideEffectMarker');
74
- assert.ok(
75
- marker,
76
- 'Side-effect-only module should register construct via globalThis.CURRENT_BLOCKS_STACK',
77
- );
78
- });
103
+ test('module with only side effects still registers constructs via globalThis', async () => {
104
+ const app = new cdk.App();
105
+ const stack = new cdk.Stack(app, 'LegacyTestStack');
106
+
107
+ const backend = await makeBackend(stack, 'LegacyStage', sideEffectBackendPath);
108
+
109
+ const marker = backend.node.tryFindChild('SideEffectMarker');
110
+ assert.ok(marker, 'Side-effect-only module should register construct via globalThis.CURRENT_BLOCKS_STACK');
111
+ });
79
112
  });
80
113
 
81
114
  describe('shared execution role (BlocksStack)', () => {
82
- // The role synth shape and the Scope.executionRole tree-walk are shared code
83
- // (setupBlocksInfra + the getter), covered in blocks-backend.test.ts. The only
84
- // BlocksStack-specific behavior is that its own constructor wires
85
- // executionRole — a separate code path from BlocksBackend's constructor.
86
- test('BlocksStack wires executionRole via its constructor', async () => {
87
- const app = new cdk.App();
88
- const stack = await BlocksStack.create(app, 'StackRoleStack', {
89
- backendHandlerPath: handlerPath,
90
- backendCDKPath: sideEffectBackendPath,
91
- });
92
-
93
- assert.ok(stack.executionRole, 'BlocksStack should expose a populated .executionRole');
94
- });
115
+ // The role synth shape and the Scope.executionRole tree-walk are shared code
116
+ // (setupBlocksInfra + the getter), covered in blocks-backend.test.ts. The only
117
+ // BlocksStack-specific behavior is that its own constructor wires
118
+ // executionRole — a separate code path from BlocksBackend's constructor.
119
+ test('BlocksStack wires executionRole via its constructor', async () => {
120
+ const app = new cdk.App();
121
+ const stack = await makeStack(app, 'StackRoleStack', sideEffectBackendPath);
122
+
123
+ assert.ok(stack.executionRole, 'BlocksStack should expose a populated .executionRole');
124
+ });
95
125
  });
96
126
 
97
127
  describe('executionRole globalThis fallback', () => {
98
- test('resolves via globalThis.CURRENT_BLOCKS_STACK when no owner is in the tree', async () => {
99
- const app = new cdk.App();
100
- const stack = await BlocksStack.create(app, 'FallbackStack', {
101
- backendHandlerPath: handlerPath,
102
- backendCDKPath: sideEffectBackendPath,
103
- });
104
-
105
- // A Scope whose construct-tree ancestry has no BlocksStack/BlocksBackend
106
- // (parented under a plain cdk.Stack) exhausts the tree-walk and falls back
107
- // to globalThis.CURRENT_BLOCKS_STACK. The `as any` is test plumbing — a
108
- // plain Stack isn't a ScopeParent, but it IS a valid Construct parent.
109
- const plainStack = new cdk.Stack(app, 'PlainStack');
110
- (globalThis as any).CURRENT_BLOCKS_STACK = stack;
111
- const orphan = new Scope('orphan', { parent: plainStack as any });
112
-
113
- assert.strictEqual(
114
- orphan.executionRole,
115
- stack.executionRole,
116
- 'fallback resolves to the ambient stack role',
117
- );
118
- });
128
+ test('resolves via globalThis.CURRENT_BLOCKS_STACK when no owner is in the tree', async () => {
129
+ const app = new cdk.App();
130
+ const stack = await makeStack(app, 'FallbackStack', sideEffectBackendPath);
131
+
132
+ // A Scope whose construct-tree ancestry has no BlocksStack/BlocksBackend
133
+ // (parented under a plain cdk.Stack) exhausts the tree-walk and falls back
134
+ // to globalThis.CURRENT_BLOCKS_STACK. The `as any` is test plumbing — a
135
+ // plain Stack isn't a ScopeParent, but it IS a valid Construct parent.
136
+ const plainStack = new cdk.Stack(app, 'PlainStack');
137
+ (globalThis as any).CURRENT_BLOCKS_STACK = stack;
138
+ const orphan = new Scope('orphan', { parent: plainStack as any });
139
+
140
+ assert.strictEqual(orphan.executionRole, stack.executionRole, 'fallback resolves to the ambient stack role');
141
+ });
142
+ });
143
+
144
+ describe('root is bound to the owning stack (multi-stack synth)', () => {
145
+ test('a block under each of two stacks resolves its OWN stack, not the last globalThis', async () => {
146
+ // `root` is resolved once at Scope construction (not re-walked per getter),
147
+ // and `create()` mutates globalThis.CURRENT_BLOCKS_STACK for each stack it
148
+ // builds. With two stacks in one synth, a block constructed under the first
149
+ // must stay bound to the first even after the second stack is created and
150
+ // overwrites globalThis — otherwise its root-derived accessors (handler,
151
+ // executionRole, backendStackName) would silently point at the wrong stack.
152
+ const app = new cdk.App();
153
+
154
+ const stackA = await makeStack(app, 'RootBindingA', sideEffectBackendPath);
155
+ // A block explicitly parented under stackA (construct-tree walk resolves
156
+ // stackA regardless of the ambient globalThis).
157
+ const blockA = new Scope('blockA', { parent: stackA });
158
+
159
+ // Building the second stack overwrites globalThis.CURRENT_BLOCKS_STACK.
160
+ const stackB = await makeStack(app, 'RootBindingB', sideEffectBackendPath);
161
+ const blockB = new Scope('blockB', { parent: stackB });
162
+
163
+ assert.strictEqual(blockA.executionRole, stackA.executionRole, 'blockA stays bound to stackA');
164
+ assert.strictEqual(blockB.executionRole, stackB.executionRole, 'blockB binds to stackB');
165
+ assert.notStrictEqual(stackA.executionRole, stackB.executionRole, 'the two stacks have distinct roles');
166
+ assert.strictEqual(blockA.backendStackName, 'RootBindingA', 'blockA derives its own stack name');
167
+ assert.strictEqual(blockB.backendStackName, 'RootBindingB', 'blockB derives its own stack name');
168
+ });
119
169
  });
120
170
 
121
171
  describe('assertCdkConditionActive', () => {
122
- test('BlocksStack.create() throws when --conditions=cdk is missing', async () => {
123
- const origNodeOptions = process.env.NODE_OPTIONS;
124
- const origExecArgv = process.execArgv;
125
- process.env.NODE_OPTIONS = '';
126
- process.execArgv = [];
127
-
128
- try {
129
- const app = new cdk.App();
130
-
131
- await assert.rejects(
132
- BlocksStack.create(app, 'MissingConditionStack', {
133
- backendHandlerPath: handlerPath,
134
- backendCDKPath: sideEffectBackendPath,
135
- }),
136
- (err: Error) => {
137
- assert.ok(err.message.includes('Missing --conditions=cdk'), `Expected condition error, got: ${err.message}`);
138
- return true;
139
- },
140
- );
141
- } finally {
142
- process.env.NODE_OPTIONS = origNodeOptions;
143
- process.execArgv = origExecArgv;
144
- }
145
- });
172
+ test('BlocksStack.create() throws when --conditions=cdk is missing', async () => {
173
+ const origNodeOptions = process.env.NODE_OPTIONS;
174
+ const origExecArgv = process.execArgv;
175
+ process.env.NODE_OPTIONS = '';
176
+ process.execArgv = [];
177
+
178
+ try {
179
+ const app = new cdk.App();
180
+
181
+ await assert.rejects(
182
+ makeStack(app, 'MissingConditionStack', sideEffectBackendPath),
183
+ (err: Error) => {
184
+ assert.ok(
185
+ err.message.includes('Missing --conditions=cdk'),
186
+ `Expected condition error, got: ${err.message}`,
187
+ );
188
+ return true;
189
+ },
190
+ );
191
+ } finally {
192
+ process.env.NODE_OPTIONS = origNodeOptions;
193
+ process.execArgv = origExecArgv;
194
+ }
195
+ });
146
196
  });