@aws-blocks/core 0.4.0 → 0.5.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 (90) hide show
  1. package/README.md +12 -0
  2. package/dist/cdk/blocks-backend.d.ts +4 -1
  3. package/dist/cdk/blocks-backend.d.ts.map +1 -1
  4. package/dist/cdk/blocks-backend.js +56 -10
  5. package/dist/cdk/blocks-backend.test.js +71 -1
  6. package/dist/cdk/blocks-defaults.d.ts +11 -0
  7. package/dist/cdk/blocks-defaults.d.ts.map +1 -1
  8. package/dist/cdk/blocks-stack.test.js +23 -3
  9. package/dist/cdk/compute/compute.d.ts +80 -2
  10. package/dist/cdk/compute/compute.d.ts.map +1 -1
  11. package/dist/cdk/compute/compute.js +57 -3
  12. package/dist/cdk/config-registry.test.js +12 -0
  13. package/dist/cdk/dashboard-registry.d.ts +41 -0
  14. package/dist/cdk/dashboard-registry.d.ts.map +1 -0
  15. package/dist/cdk/dashboard-registry.js +61 -0
  16. package/dist/cdk/index.d.ts +67 -11
  17. package/dist/cdk/index.d.ts.map +1 -1
  18. package/dist/cdk/index.js +100 -18
  19. package/dist/cdk/internal.d.ts +3 -1
  20. package/dist/cdk/internal.d.ts.map +1 -1
  21. package/dist/cdk/internal.js +4 -1
  22. package/dist/cdk/tracer-registry.d.ts +31 -0
  23. package/dist/cdk/tracer-registry.d.ts.map +1 -0
  24. package/dist/cdk/tracer-registry.js +49 -0
  25. package/dist/cdk/vpc-requirements-registry.d.ts +33 -0
  26. package/dist/cdk/vpc-requirements-registry.d.ts.map +1 -0
  27. package/dist/cdk/vpc-requirements-registry.js +46 -0
  28. package/dist/cdk/vpc-types.d.ts +151 -0
  29. package/dist/cdk/vpc-types.d.ts.map +1 -0
  30. package/dist/cdk/vpc-types.js +3 -0
  31. package/dist/cdk/vpc.d.ts +59 -0
  32. package/dist/cdk/vpc.d.ts.map +1 -0
  33. package/dist/cdk/vpc.js +298 -0
  34. package/dist/cdk/vpc.test.d.ts +2 -0
  35. package/dist/cdk/vpc.test.d.ts.map +1 -0
  36. package/dist/cdk/vpc.test.js +285 -0
  37. package/dist/errors.d.ts +5 -0
  38. package/dist/errors.d.ts.map +1 -1
  39. package/dist/errors.js +5 -0
  40. package/dist/hosting.d.ts.map +1 -1
  41. package/dist/hosting.js +2 -0
  42. package/dist/hosting.test.js +39 -1
  43. package/dist/index.cdk.d.ts +2 -1
  44. package/dist/index.cdk.d.ts.map +1 -1
  45. package/dist/index.cdk.js +1 -1
  46. package/dist/lambda-handler.js +9 -2
  47. package/dist/lambda-handler.test.js +61 -1
  48. package/dist/raw-route.d.ts +15 -1
  49. package/dist/raw-route.d.ts.map +1 -1
  50. package/dist/raw-route.js +96 -12
  51. package/dist/raw-route.test.js +332 -1
  52. package/dist/scripts/dev-server.d.ts.map +1 -1
  53. package/dist/scripts/dev-server.js +11 -0
  54. package/dist/scripts/extract-ts-types.d.ts.map +1 -1
  55. package/dist/scripts/extract-ts-types.js +107 -23
  56. package/dist/scripts/extract-ts-types.test.js +225 -26
  57. package/dist/scripts/generate-spec.d.ts.map +1 -1
  58. package/dist/scripts/generate-spec.js +14 -5
  59. package/dist/scripts/generate-spec.test.js +93 -0
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.js +1 -1
  62. package/package.json +8 -1
  63. package/src/cdk/blocks-backend.test.ts +144 -60
  64. package/src/cdk/blocks-backend.ts +298 -239
  65. package/src/cdk/blocks-defaults.ts +12 -0
  66. package/src/cdk/blocks-stack.test.ts +32 -13
  67. package/src/cdk/compute/compute.ts +105 -3
  68. package/src/cdk/config-registry.test.ts +14 -0
  69. package/src/cdk/dashboard-registry.ts +68 -0
  70. package/src/cdk/index.ts +426 -298
  71. package/src/cdk/internal.ts +6 -2
  72. package/src/cdk/tracer-registry.ts +54 -0
  73. package/src/cdk/vpc-requirements-registry.ts +63 -0
  74. package/src/cdk/vpc-types.ts +158 -0
  75. package/src/cdk/vpc.test.ts +348 -0
  76. package/src/cdk/vpc.ts +336 -0
  77. package/src/errors.ts +5 -0
  78. package/src/hosting.test.ts +59 -1
  79. package/src/hosting.ts +3 -0
  80. package/src/index.cdk.ts +7 -0
  81. package/src/lambda-handler.test.ts +79 -1
  82. package/src/lambda-handler.ts +11 -2
  83. package/src/raw-route.test.ts +427 -1
  84. package/src/raw-route.ts +125 -12
  85. package/src/scripts/dev-server.ts +12 -1
  86. package/src/scripts/extract-ts-types.test.ts +228 -26
  87. package/src/scripts/extract-ts-types.ts +104 -20
  88. package/src/scripts/generate-spec.test.ts +101 -0
  89. package/src/scripts/generate-spec.ts +15 -5
  90. package/src/version.ts +1 -1
package/src/cdk/index.ts CHANGED
@@ -1,44 +1,53 @@
1
1
  // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
- import * as cdk from 'aws-cdk-lib';
5
- import { Construct } from 'constructs';
6
4
  import { pathToFileURL } from 'node:url';
7
5
  import { __PIPELINE_STAGE_SCOPE__ } from '@aws-blocks/pipeline';
6
+ import * as cdk from 'aws-cdk-lib';
7
+ import { Construct } from 'constructs';
8
8
  import {
9
- type BlocksStackProps,
10
- type BlocksStack as BaseBlocksStack,
11
- type ScopeParent,
12
- type ScopeOptions,
13
- computeScopeFullId,
9
+ type BlocksStack as BaseBlocksStack,
10
+ type BlocksStackProps,
11
+ computeScopeFullId,
12
+ type ScopeOptions,
13
+ type ScopeParent,
14
14
  } from '../common/index.js';
15
- import { setupBlocksInfra, BlocksBackend, assertCdkConditionActive } from './blocks-backend.js';
16
- import { addBlocksStackMetadata } from './stack-metadata.js';
17
- import { finalizeConfigRegistry } from './config-registry.js';
15
+ import { assertCdkConditionActive, BlocksBackend, setupBlocksInfra } from './blocks-backend.js';
18
16
  import { type BlocksDefaults, BlocksPresets } from './blocks-defaults.js';
19
17
  import type { Compute } from './compute/compute.js';
20
18
  import { getComputes } from './compute/compute-registry.js';
21
19
  import type { DefaultComputeFactory, LambdaShapedCompute } from './compute/default-compute-factory.js';
20
+ import { finalizeConfigRegistry } from './config-registry.js';
21
+ import { finalizeDashboards } from './dashboard-registry.js';
22
+ import { addBlocksStackMetadata } from './stack-metadata.js';
23
+ import { finalizeTracing } from './tracer-registry.js';
24
+ import { anyRequirementNeedsVpc, finalizeVpc, getOrCreateVpc, initializeVpc } from './vpc.js';
25
+ import { registerVpcRequirements } from './vpc-requirements-registry.js';
26
+ import type { BlocksVpcOptions, VpcRequirements } from './vpc-types.js';
22
27
 
28
+ export { ApiError, DEFAULT_API_ERROR_NAME, hasAuthError, isBlocksError } from '../errors.js';
29
+ export type { ScopeOptions } from '../index.js';
30
+ export { ensureApiGatewayAccount } from './apigateway-account.js';
23
31
  export {
24
32
  BlocksBackend,
25
33
  type BlocksBackendProps,
26
34
  type CoreBlocksBackendProps,
27
35
  SHARED_HANDLER_TIMEOUT_SECONDS,
28
36
  } from './blocks-backend.js';
29
- export { DEFAULT_NODE_RUNTIME } from './node-version.js';
30
- export { blocksNodejsBundling } from './bundling.js';
31
- export { SandboxDisableDeletionProtection } from './mixins.js';
32
- export { registerConfig, finalizeConfigRegistry, getConfigLocation } from './config-registry.js';
33
- export { ensureApiGatewayAccount } from './apigateway-account.js';
34
37
  export {
35
- type BlocksDefaults,
36
- type BlocksThrottling,
37
- BlocksPresets,
38
+ type BlocksDefaults,
39
+ BlocksPresets,
40
+ type BlocksThrottling,
38
41
  } from './blocks-defaults.js';
42
+ export { blocksNodejsBundling } from './bundling.js';
43
+ export { finalizeConfigRegistry, getConfigLocation, registerConfig } from './config-registry.js';
44
+ export { finalizeDashboards, registerDashboardFinalizer } from './dashboard-registry.js';
45
+ export { SandboxDisableDeletionProtection } from './mixins.js';
46
+ export { DEFAULT_NODE_RUNTIME } from './node-version.js';
39
47
  export { synthGuard } from './synth-guard.js';
40
- export type { ScopeOptions } from '../index.js';
41
- export { ApiError, isBlocksError, hasAuthError, DEFAULT_API_ERROR_NAME } from '../errors.js';
48
+ export { finalizeTracing, registerTracer } from './tracer-registry.js';
49
+ export { getVpcContext } from './vpc.js';
50
+ export type { BlocksVpcOptions, SubnetRole, VpcContext, VpcRequirements } from './vpc-types.js';
42
51
 
43
52
  /**
44
53
  * Core's `create()` props: the public {@link BlocksStackProps} plus the required
@@ -52,288 +61,407 @@ export { ApiError, isBlocksError, hasAuthError, DEFAULT_API_ERROR_NAME } from '.
52
61
  * @internal
53
62
  */
54
63
  export interface CoreBlocksStackProps extends BlocksStackProps {
55
- /** Builds the stack's default compute. Injected by `@aws-blocks/blocks`. */
56
- defaultComputeFactory: DefaultComputeFactory;
64
+ /** Builds the stack's default compute. Injected by `@aws-blocks/blocks`. */
65
+ defaultComputeFactory: DefaultComputeFactory;
57
66
  }
58
67
 
59
68
  export class BlocksStack extends cdk.Stack implements BaseBlocksStack {
60
- public readonly id: string;
61
- public readonly backendHandlerPath: string;
62
- /**
63
- * Path to the app's backend module (`props.backendCDKPath`). Exposed so Building Blocks that
64
- * co-bundle the backend at synth (e.g. the Agent BB's AgentCore Runtime) can discover it via
65
- * `globalThis.CURRENT_BLOCKS_STACK.backendModulePath`.
66
- */
67
- public readonly backendModulePath: string;
68
- /** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */
69
- public readonly executionRole: cdk.aws_iam.IRole;
70
- /** Infrastructure defaults for Building Blocks created under this stack. */
71
- public readonly defaults: BlocksDefaults;
72
- /** The default compute (owns the Lambda function + API Gateway); set in `create()`. @internal */
73
- _defaultCompute?: Compute;
74
-
75
- /** The default compute's Lambda function. To be removed once consumers move to the multi-compute model. */
76
- get handler(): cdk.aws_lambda_nodejs.NodejsFunction {
77
- return this.requireDefaultCompute().fn;
78
- }
79
- /** The default compute's API Gateway REST API. To be removed once consumers move to the multi-compute model. */
80
- get gateway(): cdk.aws_apigateway.RestApi {
81
- return this.requireDefaultCompute().apiGateway;
82
- }
83
- /** The default compute's RPC endpoint URL. To be removed once consumers move to the multi-compute model. */
84
- get apiUrl(): string {
85
- return this.requireDefaultCompute().apiUrl;
86
- }
87
- /** The default compute's handler CloudWatch log group. `bb-logger` reconfigures its retention. */
88
- get handlerLogGroup(): cdk.aws_logs.ILogGroup {
89
- return this.requireDefaultCompute().logGroup;
90
- }
91
-
92
- private requireDefaultCompute(): LambdaShapedCompute {
93
- if (!this._defaultCompute) {
94
- throw new Error('Blocks stack not fully initialized — access .handler/.gateway/.apiUrl after BlocksStack.create() resolves.');
95
- }
96
- return this._defaultCompute as LambdaShapedCompute;
97
- }
98
-
99
- private constructor(scope: Construct, id: string, props: BlocksStackProps) {
100
- super(scope, id, props);
101
- this.id = id;
102
- this.backendHandlerPath = props.backendHandlerPath;
103
- this.defaults = props.defaults;
104
- this.backendModulePath = props.backendCDKPath;
105
-
106
- // Set globalThis so Building Blocks attach directly to this stack
107
- (globalThis as any).CURRENT_BLOCKS_STACK = this;
108
-
109
- const infra = setupBlocksInfra(this, props, id);
110
- this.executionRole = infra.executionRole;
111
- }
112
-
113
- static async create(scope: Construct, id: string, props: CoreBlocksStackProps) {
114
- assertCdkConditionActive();
115
-
116
- // Detect ambient pipeline stage scope set by Pipeline appFile imports
117
- const pipelineScope = (globalThis as any)[__PIPELINE_STAGE_SCOPE__];
118
- const actualScope = pipelineScope || scope;
119
-
120
- const stack = new BlocksStack(actualScope, id, props);
121
- // Create the default compute before importing the backend: it OWNS the
122
- // Lambda function + API Gateway (which back .handler/.gateway/.apiUrl), and
123
- // a block reading `this.compute` in its constructor (during that import)
124
- // must resolve to it. The factory is supplied by the umbrella
125
- // @aws-blocks/blocks (which injects LambdaCompute) via props, so core never
126
- // imports the concrete compute class.
127
- stack._defaultCompute = props.defaultComputeFactory(stack);
128
- // file:// URL (not a raw path) so the cache-busting query works on Windows,
129
- // where an absolute path like `D:\...` is rejected as URL scheme `d:`.
130
- const backendUrl = pathToFileURL(props.backendCDKPath);
131
- backendUrl.searchParams.set('stack', id);
132
- const mod = await import(backendUrl.href);
133
- if (typeof mod.default === 'function') {
134
- try {
135
- await mod.default(stack);
136
- } catch (error) {
137
- throw new Error(`Error executing default export function for stack "${id}": ${error instanceof Error ? error.message : error}`, { cause: error });
138
- }
139
- }
140
- // Finalize BB config S3 (after all BBs have registered their config)
141
- finalizeConfigRegistry(stack, stack.executionRole, getComputes(stack));
142
-
143
- new cdk.CfnOutput(stack, 'ApiUrl', { value: stack.apiUrl });
144
-
145
- addBlocksStackMetadata(stack);
146
-
147
- return stack;
148
- }
69
+ public readonly id: string;
70
+ public readonly backendHandlerPath: string;
71
+ /**
72
+ * Path to the app's backend module (`props.backendCDKPath`). Exposed so Building Blocks that
73
+ * co-bundle the backend at synth (e.g. the Agent BB's AgentCore Runtime) can discover it via
74
+ * `globalThis.CURRENT_BLOCKS_STACK.backendModulePath`.
75
+ */
76
+ public readonly backendModulePath: string;
77
+ /** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */
78
+ public readonly executionRole: cdk.aws_iam.IRole;
79
+ /** Infrastructure defaults for Building Blocks created under this stack. */
80
+ public readonly defaults: BlocksDefaults;
81
+ /** The default compute (owns the Lambda function + API Gateway); set in `create()`. @internal */
82
+ _defaultCompute?: Compute;
83
+
84
+ /** The default compute's Lambda function. To be removed once consumers move to the multi-compute model. */
85
+ get handler(): cdk.aws_lambda_nodejs.NodejsFunction {
86
+ return this.requireDefaultCompute().fn;
87
+ }
88
+ /** The default compute's API Gateway REST API. To be removed once consumers move to the multi-compute model. */
89
+ get gateway(): cdk.aws_apigateway.RestApi {
90
+ return this.requireDefaultCompute().apiGateway;
91
+ }
92
+ /** The default compute's RPC endpoint URL. To be removed once consumers move to the multi-compute model. */
93
+ get apiUrl(): string {
94
+ return this.requireDefaultCompute().apiUrl;
95
+ }
96
+ /** The default compute's handler CloudWatch log group. Its retention comes from
97
+ * the compute's `logRetention` (falling back to `defaults.logRetention`); the
98
+ * `bb-logger` CDK construct is a no-op and no longer touches it. */
99
+ get handlerLogGroup(): cdk.aws_logs.ILogGroup {
100
+ return this.requireDefaultCompute().logGroup;
101
+ }
102
+
103
+ private requireDefaultCompute(): LambdaShapedCompute {
104
+ if (!this._defaultCompute) {
105
+ throw new Error(
106
+ 'Blocks stack not fully initialized — access .handler/.gateway/.apiUrl after BlocksStack.create() resolves.',
107
+ );
108
+ }
109
+ return this._defaultCompute as LambdaShapedCompute;
110
+ }
111
+
112
+ private _vpcOptions?: BlocksVpcOptions;
113
+
114
+ private constructor(scope: Construct, id: string, props: BlocksStackProps) {
115
+ super(scope, id, props);
116
+ this.id = id;
117
+ this.backendHandlerPath = props.backendHandlerPath;
118
+ this.backendModulePath = props.backendCDKPath;
119
+ this.defaults = props.defaults;
120
+ this._vpcOptions = props.defaults.vpc;
121
+
122
+ // Set globalThis so Building Blocks attach directly to this stack
123
+ (globalThis as any).CURRENT_BLOCKS_STACK = this;
124
+
125
+ // Initialize VPC context before the default compute is created and before
126
+ // BBs are constructed, so both can discover it: the default compute
127
+ // (LambdaCompute) reads it via getVpcContext(this) to place its function in
128
+ // the VPC, and BBs (e.g. bb-data) read it to co-locate their resources.
129
+ if (this._vpcOptions) {
130
+ initializeVpc(this, this._vpcOptions);
131
+ }
132
+
133
+ const infra = setupBlocksInfra(this, props, id);
134
+ this.executionRole = infra.executionRole;
135
+ }
136
+
137
+ static async create(scope: Construct, id: string, props: CoreBlocksStackProps) {
138
+ assertCdkConditionActive();
139
+
140
+ // Detect ambient pipeline stage scope set by Pipeline appFile imports
141
+ const pipelineScope = (globalThis as any)[__PIPELINE_STAGE_SCOPE__];
142
+ const actualScope = pipelineScope || scope;
143
+
144
+ const stack = new BlocksStack(actualScope, id, props);
145
+ // Create the default compute before importing the backend: it OWNS the
146
+ // Lambda function + API Gateway (which back .handler/.gateway/.apiUrl), and
147
+ // a block reading `this.compute` in its constructor (during that import)
148
+ // must resolve to it. The factory is supplied by the umbrella
149
+ // @aws-blocks/blocks (which injects LambdaCompute) via props, so core never
150
+ // imports the concrete compute class.
151
+ stack._defaultCompute = props.defaultComputeFactory(stack);
152
+ // file:// URL (not a raw path) so the cache-busting query works on Windows,
153
+ // where an absolute path like `D:\...` is rejected as URL scheme `d:`.
154
+ const backendUrl = pathToFileURL(props.backendCDKPath);
155
+ backendUrl.searchParams.set('stack', id);
156
+ const mod = await import(backendUrl.href);
157
+ if (typeof mod.default === 'function') {
158
+ try {
159
+ await mod.default(stack);
160
+ } catch (error) {
161
+ throw new Error(
162
+ `Error executing default export function for stack "${id}": ${error instanceof Error ? error.message : error}`,
163
+ { cause: error },
164
+ );
165
+ }
166
+ }
167
+ // Finalize BB config → S3 (after all BBs have registered their config)
168
+ finalizeConfigRegistry(stack, stack.executionRole, getComputes(stack));
169
+
170
+ // Tracing is presence-gated: if the app contains a Tracer, enable X-Ray on
171
+ // every compute. Runs before the dashboard finalize so tracingEnabled is
172
+ // set when the dashboard reads it.
173
+ finalizeTracing(stack, stack.executionRole);
174
+
175
+ // Build any deferred Dashboards now that every compute's observability
176
+ // state is settled — so the dashboard is order-independent.
177
+ finalizeDashboards(stack);
178
+
179
+ // Finalize VPC. A VPC is a derived resource: use the customer's if they
180
+ // brought one, else lazily create one only if a Building Block genuinely
181
+ // requires it (requiresVpc). Most apps need neither — Lambda reaches AWS
182
+ // services from the managed network without a VPC.
183
+ if (stack._vpcOptions) {
184
+ finalizeVpc(stack, stack._vpcOptions);
185
+ } else if (anyRequirementNeedsVpc(stack)) {
186
+ const derived = getOrCreateVpc(stack);
187
+ const options = { network: derived };
188
+ initializeVpc(stack, options);
189
+ finalizeVpc(stack, options);
190
+ cdk.Annotations.of(stack).addInfoV2(
191
+ 'blocks:vpc:derived',
192
+ 'A Building Block required a VPC and none was provided, so Blocks created one ' +
193
+ '(with a NAT gateway, which has an ongoing cost). Pass `defaults.vpc: { network }` to ' +
194
+ 'BlocksStack.create to bring your own. See packages/blocks/VPC.md.',
195
+ );
196
+ }
197
+
198
+ new cdk.CfnOutput(stack, 'ApiUrl', { value: stack.apiUrl });
199
+
200
+ addBlocksStackMetadata(stack);
201
+
202
+ return stack;
203
+ }
149
204
  }
150
205
 
151
206
  export class Scope extends Construct {
152
- public readonly id: string;
153
- public readonly parent: ScopeParent;
154
-
155
- readonly bbName?: string;
156
- readonly bbVersion?: string;
157
-
158
- /**
159
- * The owning stack/backend (the root of the Blocks construct tree), resolved
160
- * once at construction: the nearest BlocksStack/BlocksBackend up the construct
161
- * tree, or the ambient `globalThis.CURRENT_BLOCKS_STACK` fallback. All
162
- * root-derived accessors below read from this instead of each repeating the
163
- * tree walk.
164
- */
165
- private readonly root: BlocksStack | BlocksBackend;
166
-
167
- /**
168
- * Compute assigned at this node. Applies to this block and is inherited by
169
- * descendants (a nearer assignment wins). Covers both a handler assigned to a
170
- * specific compute and a scope-level default for its subtree. Internal until
171
- * the customer-facing surface exists.
172
- * @internal
173
- */
174
- _compute?: Compute;
175
-
176
- constructor(id: string, options?: ScopeOptions) {
177
- const parent = options?.parent || (globalThis as any).CURRENT_BLOCKS_STACK;
178
- super(parent, id);
179
- this.id = id;
180
- this.parent = parent;
181
- this.root = this.resolveRoot();
182
- }
183
-
184
- /**
185
- * Walk up the construct tree to the nearest owning BlocksStack/BlocksBackend;
186
- * fall back to the ambient `globalThis.CURRENT_BLOCKS_STACK`. Called once from
187
- * the constructor; the result is cached in {@link root}.
188
- */
189
- private resolveRoot(): BlocksStack | BlocksBackend {
190
- let current: Construct = this;
191
- while (current.node.scope) {
192
- current = current.node.scope as Construct;
193
- if (current instanceof BlocksStack || current instanceof BlocksBackend) {
194
- return current;
195
- }
196
- }
197
- // Fallback to the ambient stack. In production this is always a real
198
- // BlocksStack/BlocksBackend; the cast also admits the test doubles that set
199
- // globalThis.CURRENT_BLOCKS_STACK to a stub exposing the same surface.
200
- return (globalThis as any).CURRENT_BLOCKS_STACK as BlocksStack | BlocksBackend;
201
- }
202
-
203
- get handler() {
204
- return this.root.handler;
205
- }
206
-
207
- /**
208
- * The shared IAM role assumed by all Blocks compute. Building Blocks grant
209
- * their permissions to this role; CDK's `grant*()` / `addToPrincipalPolicy()`
210
- * route those grants to the role's default (inline) policy.
211
- */
212
- get executionRole(): cdk.aws_iam.IRole {
213
- return this.root.executionRole;
214
- }
215
-
216
- /**
217
- * The compute this block runs on: the nearest `_compute` assigned on this
218
- * block or an ancestor scope, else the owning stack/backend's default compute.
219
- *
220
- * For any app that doesn't assign a compute, this always resolves to the
221
- * default — so reads are a no-op refactor. `_compute` is internal
222
- * (test/framework) until the customer-facing surface exists; there is no
223
- * public option to set it yet.
224
- */
225
- get compute(): Compute {
226
- for (let current: ScopeParent | undefined = this; current; current = (current as Scope).parent) {
227
- const assigned = (current as Scope)._compute;
228
- if (assigned) return assigned;
229
- }
230
- const defaultCompute = this.root._defaultCompute;
231
- if (!defaultCompute) {
232
- throw new Error('Default compute not initialized — BlocksStack/BlocksBackend.create() must run before resolving `compute`.');
233
- }
234
- return defaultCompute;
235
- }
236
-
237
- /**
238
- * The stack's default compute, **ignoring** any per-scope `_compute`
239
- * assignment (unlike {@link compute}, which resolves the nearest assigned
240
- * one). Use when a resource is a stack-level singleton that must bind to one
241
- * deterministic compute regardless of the block's resolved compute — e.g.
242
- * Realtime's shared WebSocket route integration, where one WebSocket API
243
- * integrates to a single target and connection bookkeeping is compute-agnostic.
244
- *
245
- * @internal Not a customer surface; for framework/BB singleton infra only.
246
- */
247
- get defaultCompute(): Compute {
248
- const defaultCompute = this.root._defaultCompute;
249
- if (!defaultCompute) {
250
- throw new Error('Default compute not initialized — BlocksStack/BlocksBackend.create() must run before resolving `defaultCompute`.');
251
- }
252
- return defaultCompute;
253
- }
254
-
255
- /**
256
- * The backend entry file the owning BlocksStack/BlocksBackend runs — the
257
- * single handler entry shared across the whole app.
258
- */
259
- get backendHandlerPath(): string {
260
- return this.root.backendHandlerPath;
261
- }
262
-
263
- /**
264
- * The owning stack/backend's token-free root identity. This is the value the
265
- * runtime receives as `BLOCKS_STACK_NAME` and rebuilds `fullId` from, so
266
- * physical resource names (DynamoDB tables, env-var keys, IAM ARNs) derived
267
- * from `fullId` match byte-for-byte between synth and runtime — otherwise the
268
- * runtime looks up names that were never created. `BlocksBackend` exposes this
269
- * as `fullId` ({@link BlocksBackend.fullId}); `BlocksStack` as `id`.
270
- */
271
- get backendStackName(): string {
272
- const name = this.root instanceof BlocksBackend ? this.root.fullId : this.root.id;
273
- if (!name) {
274
- throw new Error('Owning Blocks stack/backend has no id to derive BLOCKS_STACK_NAME');
275
- }
276
- return name;
277
- }
278
-
279
- /**
280
- * The shared handler Lambda's CloudWatch log group (the default compute's).
281
- * Resolves the same way as {@link handler} — via the owning
282
- * BlocksStack/BlocksBackend. `bb-logger` uses this to reconfigure retention on
283
- * the single, framework-owned group rather than creating a second one that
284
- * would collide on the log-group name.
285
- */
286
- get handlerLogGroup(): cdk.aws_logs.ILogGroup {
287
- return this.root.handlerLogGroup;
288
- }
289
-
290
- get fullId(): string {
291
- return computeScopeFullId(this);
292
- }
293
-
294
- /**
295
- * The stack-wide infrastructure {@link BlocksDefaults} registered by
296
- * `BlocksStack.create` / `BlocksBackend.create`. Read these in a Building
297
- * Block's CDK constructor to resolve a durability value, letting a per-block
298
- * option override:
299
- *
300
- * ```ts
301
- * const removalPolicy = options?.removalPolicy ?? this.defaults.removalPolicy;
302
- * ```
303
- */
304
- get defaults(): BlocksDefaults {
305
- // Resolve the same way as handler/executionRole: walk up to the owning
306
- // BlocksStack/BlocksBackend and read its defaults, so several backends in
307
- // one stack each keep their own posture. Falls back to the ambient stack,
308
- // then to the production preset when none was registered.
309
- let current: Construct = this;
310
- while (current.node.scope) {
311
- current = current.node.scope as Construct;
312
- if (current instanceof BlocksStack || current instanceof BlocksBackend) {
313
- return current.defaults;
314
- }
315
- }
316
- const ambient = ((globalThis as any).CURRENT_BLOCKS_STACK as { defaults?: BlocksDefaults } | undefined)?.defaults;
317
- if (ambient) return ambient;
318
- // No owning BlocksStack/BlocksBackend in the tree and none ambient — this is
319
- // usually a deliberate test stub, but could be a real misconfiguration (a
320
- // block built outside any Blocks backend). Fall back to the safe production
321
- // posture, and log so it's debuggable if it fires unexpectedly.
322
- console.warn(
323
- `[Blocks] Scope "${this.id}" resolved infrastructure defaults with no owning ` +
324
- 'BlocksStack/BlocksBackend in scope; falling back to BlocksPresets.production.',
325
- );
326
- return BlocksPresets.production;
327
- }
328
-
329
- protected buildUserAgentChain(): [string, string][] {
330
- return [];
331
- }
332
-
333
- // Plugin registration — no-ops in CDK context (plugins are only used at dev/build time)
334
- registerClientMiddleware(_packageSpecifier: string): void {}
335
- registerDevAttachment(_packageSpecifier: string): void {}
336
- registerLambdaEventHandler(_eventSource: string, _identifier: string, _handler: (record: any) => Promise<void>): void {}
337
- get clientMiddleware(): readonly string[] { return []; }
338
- get devAttachments(): readonly string[] { return []; }
207
+ public readonly id: string;
208
+ public readonly parent: ScopeParent;
209
+
210
+ readonly bbName?: string;
211
+ readonly bbVersion?: string;
212
+
213
+ /**
214
+ * The owning stack/backend (the root of the Blocks construct tree), resolved
215
+ * once at construction: the nearest BlocksStack/BlocksBackend up the construct
216
+ * tree, or the ambient `globalThis.CURRENT_BLOCKS_STACK` fallback. All
217
+ * root-derived accessors below read from this instead of each repeating the
218
+ * tree walk.
219
+ */
220
+ private readonly root: BlocksStack | BlocksBackend;
221
+
222
+ /**
223
+ * Compute assigned at this node. Applies to this block and is inherited by
224
+ * descendants (a nearer assignment wins). Covers both a handler assigned to a
225
+ * specific compute and a scope-level default for its subtree. Internal until
226
+ * the customer-facing surface exists.
227
+ * @internal
228
+ */
229
+ _compute?: Compute;
230
+
231
+ constructor(id: string, options?: ScopeOptions) {
232
+ const parent = options?.parent || (globalThis as any).CURRENT_BLOCKS_STACK;
233
+ super(parent, id);
234
+ this.id = id;
235
+ this.parent = parent;
236
+ this.root = this.resolveRoot();
237
+ }
238
+
239
+ /**
240
+ * Walk up the construct tree to the nearest owning BlocksStack/BlocksBackend;
241
+ * fall back to the ambient `globalThis.CURRENT_BLOCKS_STACK`. Called once from
242
+ * the constructor; the result is cached in {@link root}.
243
+ */
244
+ private resolveRoot(): BlocksStack | BlocksBackend {
245
+ let current: Construct = this;
246
+ while (current.node.scope) {
247
+ current = current.node.scope as Construct;
248
+ if (current instanceof BlocksStack || current instanceof BlocksBackend) {
249
+ return current;
250
+ }
251
+ }
252
+ // Fallback to the ambient stack. In production this is always a real
253
+ // BlocksStack/BlocksBackend; the cast also admits the test doubles that set
254
+ // globalThis.CURRENT_BLOCKS_STACK to a stub exposing the same surface.
255
+ return (globalThis as any).CURRENT_BLOCKS_STACK as BlocksStack | BlocksBackend;
256
+ }
257
+
258
+ get handler() {
259
+ return this.root.handler;
260
+ }
261
+
262
+ /**
263
+ * The shared IAM role assumed by all Blocks compute. Building Blocks grant
264
+ * their permissions to this role; CDK's `grant*()` / `addToPrincipalPolicy()`
265
+ * route those grants to the role's default (inline) policy.
266
+ */
267
+ get executionRole(): cdk.aws_iam.IRole {
268
+ return this.root.executionRole;
269
+ }
270
+
271
+ /**
272
+ * The compute this block runs on: the nearest `_compute` assigned on this
273
+ * block or an ancestor scope, else the owning stack/backend's default compute.
274
+ *
275
+ * For any app that doesn't assign a compute, this always resolves to the
276
+ * default — so reads are a no-op refactor. `_compute` is internal
277
+ * (test/framework) until the customer-facing surface exists; there is no
278
+ * public option to set it yet.
279
+ */
280
+ get compute(): Compute {
281
+ for (let current: ScopeParent | undefined = this; current; current = (current as Scope).parent) {
282
+ const assigned = (current as Scope)._compute;
283
+ if (assigned) return assigned;
284
+ }
285
+ const defaultCompute = this.root._defaultCompute;
286
+ if (!defaultCompute) {
287
+ throw new Error(
288
+ 'Default compute not initialized — BlocksStack/BlocksBackend.create() must run before resolving `compute`.',
289
+ );
290
+ }
291
+ return defaultCompute;
292
+ }
293
+
294
+ /**
295
+ * The stack's default compute, **ignoring** any per-scope `_compute`
296
+ * assignment (unlike {@link compute}, which resolves the nearest assigned
297
+ * one). Use when a resource is a stack-level singleton that must bind to one
298
+ * deterministic compute regardless of the block's resolved compute e.g.
299
+ * Realtime's shared WebSocket route integration, where one WebSocket API
300
+ * integrates to a single target and connection bookkeeping is compute-agnostic.
301
+ *
302
+ * @internal Not a customer surface; for framework/BB singleton infra only.
303
+ */
304
+ get defaultCompute(): Compute {
305
+ const defaultCompute = this.root._defaultCompute;
306
+ if (!defaultCompute) {
307
+ throw new Error(
308
+ 'Default compute not initialized — BlocksStack/BlocksBackend.create() must run before resolving `defaultCompute`.',
309
+ );
310
+ }
311
+ return defaultCompute;
312
+ }
313
+
314
+ /**
315
+ * The backend entry file the owning BlocksStack/BlocksBackend runs — the
316
+ * single handler entry shared across the whole app.
317
+ */
318
+ get backendHandlerPath(): string {
319
+ return this.root.backendHandlerPath;
320
+ }
321
+
322
+ /**
323
+ * The owning stack/backend's token-free root identity. This is the value the
324
+ * runtime receives as `BLOCKS_STACK_NAME` and rebuilds `fullId` from, so
325
+ * physical resource names (DynamoDB tables, env-var keys, IAM ARNs) derived
326
+ * from `fullId` match byte-for-byte between synth and runtime — otherwise the
327
+ * runtime looks up names that were never created. `BlocksBackend` exposes this
328
+ * as `fullId` ({@link BlocksBackend.fullId}); `BlocksStack` as `id`.
329
+ */
330
+ get backendStackName(): string {
331
+ const name = this.root instanceof BlocksBackend ? this.root.fullId : this.root.id;
332
+ if (!name) {
333
+ throw new Error('Owning Blocks stack/backend has no id to derive BLOCKS_STACK_NAME');
334
+ }
335
+ return name;
336
+ }
337
+
338
+ /**
339
+ * The shared handler Lambda's CloudWatch log group (the default compute's).
340
+ * Resolves the same way as {@link handler} — via the owning
341
+ * BlocksStack/BlocksBackend. Its retention comes from the compute's
342
+ * `logRetention` (falling back to `defaults.logRetention`); the `bb-logger`
343
+ * CDK construct is a no-op and no longer reconfigures it.
344
+ */
345
+ get handlerLogGroup(): cdk.aws_logs.ILogGroup {
346
+ return this.root.handlerLogGroup;
347
+ }
348
+
349
+ get fullId(): string {
350
+ return computeScopeFullId(this);
351
+ }
352
+
353
+ /**
354
+ * The stack-wide infrastructure {@link BlocksDefaults} registered by
355
+ * `BlocksStack.create` / `BlocksBackend.create`. Read these in a Building
356
+ * Block's CDK constructor to resolve a durability value, letting a per-block
357
+ * option override:
358
+ *
359
+ * ```ts
360
+ * const removalPolicy = options?.removalPolicy ?? this.defaults.removalPolicy;
361
+ * ```
362
+ */
363
+ get defaults(): BlocksDefaults {
364
+ // Resolve the same way as handler/executionRole: walk up to the owning
365
+ // BlocksStack/BlocksBackend and read its defaults, so several backends in
366
+ // one stack each keep their own posture. Falls back to the ambient stack,
367
+ // then to the production preset when none was registered.
368
+ let current: Construct = this;
369
+ while (current.node.scope) {
370
+ current = current.node.scope as Construct;
371
+ if (current instanceof BlocksStack || current instanceof BlocksBackend) {
372
+ return current.defaults;
373
+ }
374
+ }
375
+ const ambient = ((globalThis as any).CURRENT_BLOCKS_STACK as { defaults?: BlocksDefaults } | undefined)
376
+ ?.defaults;
377
+ if (ambient) return ambient;
378
+ // No owning BlocksStack/BlocksBackend in the tree and none ambient — this is
379
+ // usually a deliberate test stub, but could be a real misconfiguration (a
380
+ // block built outside any Blocks backend). Fall back to the safe production
381
+ // posture, and log so it's debuggable if it fires unexpectedly.
382
+ console.warn(
383
+ `[Blocks] Scope "${this.id}" resolved infrastructure defaults with no owning ` +
384
+ 'BlocksStack/BlocksBackend in scope; falling back to BlocksPresets.production.',
385
+ );
386
+ return BlocksPresets.production;
387
+ }
388
+
389
+ protected buildUserAgentChain(): [string, string][] {
390
+ return [];
391
+ }
392
+
393
+ // Plugin registration — no-ops in CDK context (plugins are only used at dev/build time)
394
+ registerClientMiddleware(_packageSpecifier: string): void {}
395
+ registerDevAttachment(_packageSpecifier: string): void {}
396
+ registerLambdaEventHandler(
397
+ _eventSource: string,
398
+ _identifier: string,
399
+ _handler: (record: any) => Promise<void>,
400
+ ): void {}
401
+ get clientMiddleware(): readonly string[] {
402
+ return [];
403
+ }
404
+ get devAttachments(): readonly string[] {
405
+ return [];
406
+ }
407
+ }
408
+
409
+ /**
410
+ * A VPC-requirements provider: either the requirements directly, or a callback
411
+ * that returns them. Use the callback form when the value depends on `fullId`
412
+ * or other post-construction state — it is evaluated by the base constructor
413
+ * *after* `super()` runs, so `this` is fully available.
414
+ */
415
+ export type VpcRequirementsProvider = VpcRequirements | (() => VpcRequirements);
416
+
417
+ /**
418
+ * Constructor options for a {@link BuildingBlockScope} — the {@link ScopeOptions}
419
+ * every Scope takes, plus the block's VPC requirements. `vpc` is **required** so
420
+ * a BB author can't silently omit it; pass `{}` when the block needs nothing
421
+ * VPC-specific.
422
+ */
423
+ export interface BuildingBlockScopeOptions extends ScopeOptions {
424
+ /**
425
+ * What this block needs from the VPC — endpoints, runtime egress, whether it
426
+ * requires a VPC at all. A value, or a callback (evaluated after `super()`,
427
+ * so it may read `this.fullId`). See {@link VpcRequirements}.
428
+ */
429
+ vpc: VpcRequirementsProvider;
430
+ }
431
+
432
+ /**
433
+ * Base class for Building Block CDK constructs.
434
+ *
435
+ * BBs extend this instead of `Scope` directly and **must** declare their VPC
436
+ * requirements as a constructor argument — the base registers them centrally
437
+ * (see `vpc-requirements-registry.ts`) so `finalizeVpc` can pull, deduplicate,
438
+ * and provision endpoints, and so the lazy VPC can answer "does anything here
439
+ * need a VPC?". Passing the requirements is required by the constructor
440
+ * signature, so a BB author cannot silently forget to declare them — the same
441
+ * forcing the previous `abstract getVpcRequirements()` gave, but without a
442
+ * standing method on every subclass.
443
+ *
444
+ * Declare `{}` when the BB needs nothing VPC-specific.
445
+ *
446
+ * @example
447
+ * ```ts
448
+ * export class KVStore extends BuildingBlockScope {
449
+ * constructor(scope: ScopeParent, id: string) {
450
+ * super(id, { parent: scope, vpc: { gatewayEndpoints: [ec2.GatewayVpcEndpointAwsService.DYNAMODB] } });
451
+ * // …
452
+ * }
453
+ * }
454
+ * ```
455
+ */
456
+ export class BuildingBlockScope extends Scope {
457
+ constructor(id: string, options: BuildingBlockScopeOptions) {
458
+ const { vpc, ...scopeOptions } = options;
459
+ super(id, scopeOptions);
460
+ // Resolve the provider (callback form is evaluated here, after super(), so
461
+ // values that depend on this.fullId are available) and self-register on the
462
+ // owning stack. Register even when empty so the registry is a faithful
463
+ // census of every BB — the lazy VPC and finalizeVpc both rely on that.
464
+ const requirements = typeof vpc === 'function' ? vpc() : vpc;
465
+ registerVpcRequirements(this, requirements);
466
+ }
339
467
  }