@aws-blocks/core 0.3.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 (147) hide show
  1. package/README.md +25 -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 +11 -0
  12. package/dist/cdk/blocks-backend.d.ts.map +1 -1
  13. package/dist/cdk/blocks-backend.js +72 -13
  14. package/dist/cdk/blocks-backend.test.js +88 -1
  15. package/dist/cdk/blocks-defaults.d.ts +76 -1
  16. package/dist/cdk/blocks-defaults.d.ts.map +1 -1
  17. package/dist/cdk/blocks-defaults.js +17 -1
  18. package/dist/cdk/blocks-defaults.test.js +17 -0
  19. package/dist/cdk/blocks-stack.test.js +44 -3
  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 +82 -2
  24. package/dist/cdk/compute/compute.d.ts.map +1 -1
  25. package/dist/cdk/compute/compute.js +64 -2
  26. package/dist/cdk/compute/default-compute-factory.d.ts +1 -0
  27. package/dist/cdk/compute/default-compute-factory.d.ts.map +1 -1
  28. package/dist/cdk/config-registry.d.ts +34 -4
  29. package/dist/cdk/config-registry.d.ts.map +1 -1
  30. package/dist/cdk/config-registry.js +83 -25
  31. package/dist/cdk/config-registry.test.d.ts +2 -0
  32. package/dist/cdk/config-registry.test.d.ts.map +1 -0
  33. package/dist/cdk/config-registry.test.js +127 -0
  34. package/dist/cdk/dashboard-registry.d.ts +41 -0
  35. package/dist/cdk/dashboard-registry.d.ts.map +1 -0
  36. package/dist/cdk/dashboard-registry.js +61 -0
  37. package/dist/cdk/index.d.ts +90 -6
  38. package/dist/cdk/index.d.ts.map +1 -1
  39. package/dist/cdk/index.js +135 -13
  40. package/dist/cdk/internal.d.ts +3 -1
  41. package/dist/cdk/internal.d.ts.map +1 -1
  42. package/dist/cdk/internal.js +4 -1
  43. package/dist/cdk/tracer-registry.d.ts +31 -0
  44. package/dist/cdk/tracer-registry.d.ts.map +1 -0
  45. package/dist/cdk/tracer-registry.js +49 -0
  46. package/dist/cdk/vpc-requirements-registry.d.ts +33 -0
  47. package/dist/cdk/vpc-requirements-registry.d.ts.map +1 -0
  48. package/dist/cdk/vpc-requirements-registry.js +46 -0
  49. package/dist/cdk/vpc-types.d.ts +151 -0
  50. package/dist/cdk/vpc-types.d.ts.map +1 -0
  51. package/dist/cdk/vpc-types.js +3 -0
  52. package/dist/cdk/vpc.d.ts +59 -0
  53. package/dist/cdk/vpc.d.ts.map +1 -0
  54. package/dist/cdk/vpc.js +298 -0
  55. package/dist/cdk/vpc.test.d.ts +2 -0
  56. package/dist/cdk/vpc.test.d.ts.map +1 -0
  57. package/dist/cdk/vpc.test.js +285 -0
  58. package/dist/client/index.d.ts +1 -1
  59. package/dist/client/index.d.ts.map +1 -1
  60. package/dist/client/index.js +1 -1
  61. package/dist/common/config.d.ts +34 -0
  62. package/dist/common/config.d.ts.map +1 -1
  63. package/dist/common/config.js +45 -3
  64. package/dist/common/config.test.js +19 -0
  65. package/dist/common/index.d.ts +8 -0
  66. package/dist/common/index.d.ts.map +1 -1
  67. package/dist/errors.d.ts +21 -0
  68. package/dist/errors.d.ts.map +1 -1
  69. package/dist/errors.js +25 -0
  70. package/dist/hosting.d.ts +9 -0
  71. package/dist/hosting.d.ts.map +1 -1
  72. package/dist/hosting.js +12 -1
  73. package/dist/hosting.test.js +63 -1
  74. package/dist/index.cdk.d.ts +3 -2
  75. package/dist/index.cdk.d.ts.map +1 -1
  76. package/dist/index.cdk.js +2 -2
  77. package/dist/index.d.ts +1 -1
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +1 -1
  80. package/dist/lambda-handler.d.ts +17 -0
  81. package/dist/lambda-handler.d.ts.map +1 -1
  82. package/dist/lambda-handler.js +61 -5
  83. package/dist/lambda-handler.test.js +191 -2
  84. package/dist/raw-route.d.ts +15 -1
  85. package/dist/raw-route.d.ts.map +1 -1
  86. package/dist/raw-route.js +96 -12
  87. package/dist/raw-route.test.js +332 -1
  88. package/dist/scripts/dev-server.d.ts.map +1 -1
  89. package/dist/scripts/dev-server.js +11 -0
  90. package/dist/scripts/extract-ts-types.d.ts.map +1 -1
  91. package/dist/scripts/extract-ts-types.js +107 -23
  92. package/dist/scripts/extract-ts-types.test.js +225 -26
  93. package/dist/scripts/generate-spec.d.ts.map +1 -1
  94. package/dist/scripts/generate-spec.js +14 -5
  95. package/dist/scripts/generate-spec.test.js +93 -0
  96. package/dist/scripts/sandbox-empty-buckets.test.d.ts +2 -0
  97. package/dist/scripts/sandbox-empty-buckets.test.d.ts.map +1 -0
  98. package/dist/scripts/sandbox-empty-buckets.test.js +171 -0
  99. package/dist/scripts/sandbox.d.ts +54 -0
  100. package/dist/scripts/sandbox.d.ts.map +1 -1
  101. package/dist/scripts/sandbox.js +163 -25
  102. package/dist/version.d.ts +1 -1
  103. package/dist/version.js +1 -1
  104. package/package.json +10 -1
  105. package/src/api.test.ts +25 -0
  106. package/src/api.ts +39 -0
  107. package/src/bb-utils.ts +3 -0
  108. package/src/cdk/apigateway-account.ts +66 -0
  109. package/src/cdk/blocks-backend.test.ts +168 -60
  110. package/src/cdk/blocks-backend.ts +299 -227
  111. package/src/cdk/blocks-defaults.test.ts +21 -0
  112. package/src/cdk/blocks-defaults.ts +79 -1
  113. package/src/cdk/blocks-stack.test.ts +57 -13
  114. package/src/cdk/compute/compute-registry.ts +45 -0
  115. package/src/cdk/compute/compute.ts +114 -2
  116. package/src/cdk/compute/default-compute-factory.ts +1 -0
  117. package/src/cdk/config-registry.test.ts +149 -0
  118. package/src/cdk/config-registry.ts +92 -34
  119. package/src/cdk/dashboard-registry.ts +68 -0
  120. package/src/cdk/index.ts +427 -256
  121. package/src/cdk/internal.ts +6 -2
  122. package/src/cdk/tracer-registry.ts +54 -0
  123. package/src/cdk/vpc-requirements-registry.ts +63 -0
  124. package/src/cdk/vpc-types.ts +158 -0
  125. package/src/cdk/vpc.test.ts +348 -0
  126. package/src/cdk/vpc.ts +336 -0
  127. package/src/client/index.ts +1 -1
  128. package/src/common/config.test.ts +21 -0
  129. package/src/common/config.ts +47 -3
  130. package/src/common/index.ts +8 -0
  131. package/src/errors.ts +26 -0
  132. package/src/hosting.test.ts +87 -1
  133. package/src/hosting.ts +24 -1
  134. package/src/index.cdk.ts +11 -1
  135. package/src/index.ts +1 -1
  136. package/src/lambda-handler.test.ts +220 -2
  137. package/src/lambda-handler.ts +65 -4
  138. package/src/raw-route.test.ts +427 -1
  139. package/src/raw-route.ts +125 -12
  140. package/src/scripts/dev-server.ts +12 -1
  141. package/src/scripts/extract-ts-types.test.ts +228 -26
  142. package/src/scripts/extract-ts-types.ts +104 -20
  143. package/src/scripts/generate-spec.test.ts +101 -0
  144. package/src/scripts/generate-spec.ts +15 -5
  145. package/src/scripts/sandbox-empty-buckets.test.ts +191 -0
  146. package/src/scripts/sandbox.ts +185 -24
  147. package/src/version.ts +1 -1
@@ -2,6 +2,21 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import { RemovalPolicy } from 'aws-cdk-lib';
5
+ import { RetentionDays } from 'aws-cdk-lib/aws-logs';
6
+ import type { BlocksVpcOptions } from './vpc-types.js';
7
+
8
+ /**
9
+ * Request-rate limits applied to an API Gateway stage. On a REST API these are
10
+ * requests/second; on a WebSocket API the unit is messages/second across the
11
+ * connection. `rateLimit` is the steady-state ceiling and `burstLimit` the
12
+ * token-bucket size for short spikes.
13
+ */
14
+ export interface BlocksThrottling {
15
+ /** Steady-state request (or WebSocket message) rate ceiling, per second. */
16
+ rateLimit: number;
17
+ /** Token-bucket burst size for short spikes above `rateLimit`. */
18
+ burstLimit: number;
19
+ }
5
20
 
6
21
  /**
7
22
  * Default values for every Amazon-authored Building Block created within a
@@ -47,6 +62,54 @@ export interface BlocksDefaults {
47
62
  * 1–35 days). Backups only have a window when on, so the two are one field.
48
63
  */
49
64
  pointInTimeRecovery: boolean | { retentionDays: number };
65
+
66
+ /**
67
+ * How long CloudWatch Logs keeps log events written by Blocks-managed log
68
+ * groups (the core handler Lambda, migration/GSI Lambdas, hosting compute,
69
+ * `bb-logger`, and API Gateway access logs) before expiring them. Without a
70
+ * retention set, AWS keeps log events forever, which grows cost unbounded —
71
+ * every Blocks-managed log group reads this default so retention is applied
72
+ * consistently.
73
+ */
74
+ logRetention: RetentionDays;
75
+
76
+ /**
77
+ * Request-rate limits applied to every Blocks-managed API Gateway stage: the
78
+ * core REST API, the SSR/hosting REST API, and the `bb-realtime` WebSocket
79
+ * stage. Protects the backend from runaway clients and caps blast radius.
80
+ * The sandbox preset caps tighter (200/400) than production (1000/2000) so a
81
+ * disposable stack is well-protected without throttling real production
82
+ * traffic. See {@link BlocksThrottling}.
83
+ */
84
+ throttling: BlocksThrottling;
85
+
86
+ /**
87
+ * Whether to emit structured JSON access logs from every Blocks-managed API
88
+ * Gateway stage to a dedicated CloudWatch log group (retention follows
89
+ * {@link logRetention}).
90
+ *
91
+ * **Off by default in both presets** — opt in with a per-stack override
92
+ * (`defaults: { ...BlocksPresets.production, accessLogging: true }`). It is
93
+ * off by default (rather than on for production) because enabling it
94
+ * provisions the account-level API Gateway CloudWatch Logs role, which is an
95
+ * account/region-level singleton: a second Blocks stack in the same
96
+ * account+region that also enables access logging can repoint that role on
97
+ * deploy or, on teardown, leave the survivor's access logging broken.
98
+ * Enabling it is therefore safe for **one Blocks stack per region** — see
99
+ * `ensureApiGatewayAccount` for the full multi-stack teardown caveat.
100
+ */
101
+ accessLogging: boolean;
102
+
103
+ /**
104
+ * Place the app's compute and VPC-resident resources in a VPC. Pass a
105
+ * standard CDK VPC — Blocks handles Lambda placement, endpoint provisioning
106
+ * (based on which Building Blocks are in scope), and security-group wiring.
107
+ *
108
+ * Omit for no VPC (the preset default — Lambda runs in the AWS-managed
109
+ * network). A Building Block that can't function without a VPC causes Blocks
110
+ * to derive one on demand even when this is unset.
111
+ */
112
+ vpc?: BlocksVpcOptions;
50
113
  }
51
114
 
52
115
  /**
@@ -64,12 +127,27 @@ export const BlocksPresets = {
64
127
  deletionProtection: false,
65
128
  allowedOrigins: ['^https?://(localhost|127\\.0\\.0\\.1)(:\\d+)?$'],
66
129
  pointInTimeRecovery: false,
130
+ logRetention: RetentionDays.ONE_WEEK,
131
+ throttling: { rateLimit: 200, burstLimit: 400 },
132
+ accessLogging: false,
67
133
  },
68
- /** Durable, protected posture for permanent deployments. */
134
+ /**
135
+ * Durable, protected posture for permanent deployments. A higher throttle
136
+ * ceiling (1000/2000) than sandbox so the default doesn't 429 real
137
+ * production traffic — raise it via a per-stack `throttling` override for
138
+ * higher-volume APIs.
139
+ */
69
140
  production: {
70
141
  removalPolicy: RemovalPolicy.RETAIN,
71
142
  deletionProtection: true,
72
143
  allowedOrigins: [],
73
144
  pointInTimeRecovery: true,
145
+ logRetention: RetentionDays.ONE_YEAR,
146
+ throttling: { rateLimit: 1000, burstLimit: 2000 },
147
+ // Off by default even in production: enabling access logging mutates the
148
+ // account/region-level API Gateway CloudWatch role (a singleton). Opt in
149
+ // per stack once you've confirmed a single Blocks stack owns it in the
150
+ // region — see the `accessLogging` field doc.
151
+ accessLogging: false,
74
152
  },
75
153
  } satisfies Record<string, BlocksDefaults>;
@@ -7,14 +7,16 @@ import { before, describe, test } from 'node:test';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import * as cdk from 'aws-cdk-lib';
9
9
  import * as apigateway from 'aws-cdk-lib/aws-apigateway';
10
+ import type { IWidget } from 'aws-cdk-lib/aws-cloudwatch';
10
11
  import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
11
12
  import type { Construct } from 'constructs';
12
13
  import type { ScopeParent } from '../common/index.js';
13
14
  import { BLOCKS_RPC_PREFIX } from '../constants.js';
14
15
  import { BlocksBackend } from './blocks-backend.js';
15
16
  import { Compute } from './compute/compute.js';
17
+ import { getComputes } from './compute/compute-registry.js';
16
18
  import type { DefaultComputeFactory } from './compute/default-compute-factory.js';
17
- import { BlocksStack, BlocksPresets, Scope } from './index.js';
19
+ import { BlocksPresets, BlocksStack, Scope } from './index.js';
18
20
 
19
21
  // A real app gets its default compute from @aws-blocks/bb-lambda-compute (via
20
22
  // @aws-blocks/blocks), which core's own tests can't depend on. Use an
@@ -27,14 +29,19 @@ class StubLambdaCompute extends Compute {
27
29
  readonly fn: lambda.NodejsFunction;
28
30
  readonly apiGateway: apigateway.RestApi;
29
31
  readonly apiUrl: string;
32
+ readonly logGroup: cdk.aws_logs.LogGroup;
30
33
 
31
34
  constructor(scope: ScopeParent, id: string) {
32
35
  super(id, { parent: scope });
36
+ this.logGroup = new cdk.aws_logs.LogGroup(this, 'HandlerLogGroup', {
37
+ removalPolicy: cdk.RemovalPolicy.DESTROY,
38
+ });
33
39
  this.fn = new lambda.NodejsFunction(this, 'Handler', {
34
40
  entry: this.backendHandlerPath,
35
41
  runtime: cdk.aws_lambda.Runtime.NODEJS_22_X,
36
42
  handler: 'handler',
37
43
  role: this.executionRole,
44
+ logGroup: this.logGroup,
38
45
  environment: { BLOCKS_STACK_NAME: this.backendStackName },
39
46
  bundling: { minify: true, esbuildArgs: { '--conditions': 'aws-runtime' } },
40
47
  });
@@ -49,6 +56,17 @@ class StubLambdaCompute extends Compute {
49
56
  setEnv(key: string, value: string): void {
50
57
  this.fn.addEnvironment(key, value);
51
58
  }
59
+
60
+ protected applyTracing(): void {}
61
+ protected healthWidgets(_region: string): IWidget[][] {
62
+ return [];
63
+ }
64
+ protected loggingWidgets(_region: string): IWidget[][] {
65
+ return [];
66
+ }
67
+ protected tracingWidgets(_region: string): IWidget[][] {
68
+ return [];
69
+ }
52
70
  }
53
71
 
54
72
  const stubComputeFactory: DefaultComputeFactory = (root) => new StubLambdaCompute(root as never, 'DefaultCompute');
@@ -66,9 +84,19 @@ const factoryBackendPath = join(__dirname, '__fixtures__', 'factory-backend.js')
66
84
  // Wrap create(), injecting the stub default-compute factory the way
67
85
  // @aws-blocks/blocks injects LambdaCompute — so tests don't repeat it.
68
86
  const makeStack = (scope: Construct, id: string, backendCDKPath: string) =>
69
- BlocksStack.create(scope, id, { backendHandlerPath: handlerPath, backendCDKPath, defaults: BlocksPresets.production, defaultComputeFactory: stubComputeFactory });
87
+ BlocksStack.create(scope, id, {
88
+ backendHandlerPath: handlerPath,
89
+ backendCDKPath,
90
+ defaults: BlocksPresets.production,
91
+ defaultComputeFactory: stubComputeFactory,
92
+ });
70
93
  const makeBackend = (scope: Construct, id: string, backendCDKPath: string) =>
71
- BlocksBackend.create(scope, id, { backendHandlerPath: handlerPath, backendCDKPath, defaults: BlocksPresets.production, defaultComputeFactory: stubComputeFactory });
94
+ BlocksBackend.create(scope, id, {
95
+ backendHandlerPath: handlerPath,
96
+ backendCDKPath,
97
+ defaults: BlocksPresets.production,
98
+ defaultComputeFactory: stubComputeFactory,
99
+ });
72
100
 
73
101
  describe('ESM cache-busting (multi-stage)', () => {
74
102
  test('BlocksStack.create() with same backendCDKPath but different IDs produces constructs in each', async () => {
@@ -166,6 +194,25 @@ describe('root is bound to the owning stack (multi-stack synth)', () => {
166
194
  assert.strictEqual(blockA.backendStackName, 'RootBindingA', 'blockA derives its own stack name');
167
195
  assert.strictEqual(blockB.backendStackName, 'RootBindingB', 'blockB derives its own stack name');
168
196
  });
197
+
198
+ test('each stack owns an isolated compute registry (no cross-stack bleed)', async () => {
199
+ // Computes self-register on their owning stack (keyed per stack, not a
200
+ // process-global list), so a multi-stack synth keeps each stack's computes
201
+ // separate — finalize steps for one stack never see another's compute.
202
+ const app = new cdk.App();
203
+
204
+ const stackA = await makeStack(app, 'ComputeRegistryA', sideEffectBackendPath);
205
+ const stackB = await makeStack(app, 'ComputeRegistryB', sideEffectBackendPath);
206
+
207
+ const computesA = getComputes(stackA);
208
+ const computesB = getComputes(stackB);
209
+
210
+ assert.strictEqual(computesA.length, 1, 'stackA registered exactly its default compute');
211
+ assert.strictEqual(computesB.length, 1, 'stackB registered exactly its default compute');
212
+ assert.strictEqual(computesA[0], stackA._defaultCompute, 'stackA lists its own default');
213
+ assert.strictEqual(computesB[0], stackB._defaultCompute, 'stackB lists its own default');
214
+ assert.notStrictEqual(computesA[0], computesB[0], 'the two stacks hold distinct computes');
215
+ });
169
216
  });
170
217
 
171
218
  describe('assertCdkConditionActive', () => {
@@ -178,16 +225,13 @@ describe('assertCdkConditionActive', () => {
178
225
  try {
179
226
  const app = new cdk.App();
180
227
 
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
- );
228
+ await assert.rejects(makeStack(app, 'MissingConditionStack', sideEffectBackendPath), (err: Error) => {
229
+ assert.ok(
230
+ err.message.includes('Missing --conditions=cdk'),
231
+ `Expected condition error, got: ${err.message}`,
232
+ );
233
+ return true;
234
+ });
191
235
  } finally {
192
236
  process.env.NODE_OPTIONS = origNodeOptions;
193
237
  process.execArgv = origExecArgv;
@@ -0,0 +1,45 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import * as cdk from 'aws-cdk-lib';
5
+ import type { Construct } from 'constructs';
6
+ import type { Compute } from './compute.js';
7
+
8
+ const REGISTRY_KEY = Symbol.for('BLOCKS_COMPUTE_REGISTRY');
9
+
10
+ /**
11
+ * Get or create the compute list for a given stack. The list is stored on the
12
+ * stack object (keyed by a Symbol), so each stack in a multi-stack synth gets
13
+ * its own — a compute never leaks into another stack's list. Mirrors the config
14
+ * registry (`config-registry.ts`), which scopes its state the same way.
15
+ */
16
+ function getRegistry(stack: cdk.Stack): Compute[] {
17
+ let list = (stack as any)[REGISTRY_KEY] as Compute[] | undefined;
18
+ if (!list) {
19
+ list = [];
20
+ (stack as any)[REGISTRY_KEY] = list;
21
+ }
22
+ return list;
23
+ }
24
+
25
+ /**
26
+ * Register a compute on its owning stack. Called from the {@link Compute} base
27
+ * constructor, so every compute self-registers the moment it is constructed —
28
+ * the finalize steps then enumerate them without a separate discovery pass
29
+ * (mirrors how `registerConfig` accumulates config during the backend import).
30
+ *
31
+ * @param compute - The compute to register (used to locate its stack).
32
+ */
33
+ export function registerCompute(compute: Compute): void {
34
+ getRegistry(cdk.Stack.of(compute)).push(compute);
35
+ }
36
+
37
+ /**
38
+ * The computes registered on the stack that owns `scope`, in construction
39
+ * order. Returns an empty array before any compute is constructed.
40
+ *
41
+ * @param scope - Any construct in the stack (used to locate the stack).
42
+ */
43
+ export function getComputes(scope: Construct): readonly Compute[] {
44
+ return getRegistry(cdk.Stack.of(scope));
45
+ }
@@ -1,7 +1,10 @@
1
1
  // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
+ import type { IWidget } from 'aws-cdk-lib/aws-cloudwatch';
5
+ import type { ScopeOptions } from '../../common/index.js';
4
6
  import { Scope } from '../index.js';
7
+ import { registerCompute } from './compute-registry.js';
5
8
 
6
9
  /**
7
10
  * Base class for a Blocks *compute* — a runtime that executes handler code
@@ -14,11 +17,17 @@ import { Scope } from '../index.js';
14
17
  * compute in an app runs the same backend and agrees on the resource-name
15
18
  * namespace.
16
19
  *
20
+ * **Observability is compute-owned at deploy time.** Logging is always on (a
21
+ * compute owns its log group and captures stdout; retention is a compute-level
22
+ * setting), so there is no "enable logging" — logs always exist. Tracing, by
23
+ * contrast, provisions real infrastructure (X-Ray) and has cost, so it is
24
+ * enabled explicitly via {@link enableTracing} — which the framework calls on
25
+ * every compute when the app contains a `Tracer` (presence-gated).
26
+ *
17
27
  * The abstract base lives in core (a framework primitive); concrete computes
18
28
  * live in their own packages (e.g. `LambdaCompute` in `@aws-blocks/bb-lambda-compute`).
19
29
  *
20
- * @internal Not exported from the package's public entry points. Customers
21
- * cannot instantiate a compute until the customer-facing surface exists.
30
+ * @internal Not exported from the package's public entry points.
22
31
  */
23
32
  export abstract class Compute extends Scope {
24
33
  /**
@@ -28,10 +37,113 @@ export abstract class Compute extends Scope {
28
37
  */
29
38
  readonly namespaces: string[] = [];
30
39
 
40
+ /**
41
+ * Whether tracing has been enabled on this compute — flipped by
42
+ * {@link enableTracing}. Private so it can't be set independently of the
43
+ * infra; read internally by {@link dashboardSection} to decide whether to
44
+ * render the traces section.
45
+ */
46
+ private tracerEnabled = false;
47
+
48
+ /** Read-only view of {@link tracerEnabled} for subclasses (e.g. to guard
49
+ * their `tracingWidgets` builder). Subclasses can read but not set it. */
50
+ protected get isTracerEnabled(): boolean {
51
+ return this.tracerEnabled;
52
+ }
53
+
54
+ constructor(id: string, options?: ScopeOptions) {
55
+ super(id, options);
56
+ // Self-register on the owning stack so finalize steps (tracing, routing,
57
+ // dashboards) can enumerate every compute without a separate discovery
58
+ // pass. Scoped per stack, so a multi-stack synth keeps lists isolated.
59
+ registerCompute(this);
60
+ }
61
+
31
62
  /**
32
63
  * Inject a runtime configuration value (an environment variable) into this
33
64
  * compute. The framework calls this instead of `handler.addEnvironment()`
34
65
  * directly so config targets the right compute.
35
66
  */
36
67
  abstract setEnv(key: string, value: string): void;
68
+
69
+ /**
70
+ * Enable distributed tracing on this compute: mark it traced (so the Dashboard
71
+ * renders its traces section) and turn on the compute's active tracing via
72
+ * {@link applyTracing}. Idempotent — the framework calls this on **every**
73
+ * compute when the app contains a `Tracer` (tracing is presence-gated, not
74
+ * per-compute), so calling it more than once is a no-op.
75
+ */
76
+ enableTracing(): void {
77
+ if (this.tracerEnabled) return;
78
+ this.tracerEnabled = true;
79
+ this.applyTracing();
80
+ }
81
+
82
+ /**
83
+ * Turn on this compute's active tracing (e.g. X-Ray) and grant its role the
84
+ * permission to publish trace segments. Called by {@link enableTracing};
85
+ * `protected` so tracing can't be turned on without marking the compute
86
+ * traced.
87
+ */
88
+ protected abstract applyTracing(): void;
89
+
90
+ /**
91
+ * Build this compute's CloudWatch Dashboard section: health widgets and log
92
+ * widgets **always** (logs are always captured for a compute), plus trace
93
+ * widgets **only when** tracing is enabled on this compute (via
94
+ * {@link enableTracing}).
95
+ *
96
+ * This is the single public entry the Dashboard Building Block uses; the
97
+ * per-kind builders below are `protected`. Whether the logs / traces sections
98
+ * are actually shown is a display choice the Dashboard makes on top (its
99
+ * `logs` / `traces` options) — this returns what the compute *has*.
100
+ *
101
+ * @param region - AWS region the widgets query metrics in.
102
+ */
103
+ dashboardSection(region: string): ComputeDashboardSection {
104
+ return {
105
+ // The scope id (e.g. 'DefaultCompute', 'api') — short and readable for a
106
+ // section header, and distinct per compute within a stack.
107
+ label: this.id,
108
+ health: this.healthWidgets(region),
109
+ logging: this.loggingWidgets(region),
110
+ tracing: this.tracerEnabled ? this.tracingWidgets(region) : undefined,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Build this compute's health widget rows. Implemented by a concrete compute;
116
+ * obtained only via {@link dashboardSection}.
117
+ */
118
+ protected abstract healthWidgets(region: string): IWidget[][];
119
+
120
+ /**
121
+ * Build this compute's **log** widget rows for its own log group. Logs always
122
+ * exist, so this is always available; the Dashboard's `logs` option decides
123
+ * whether to render it.
124
+ */
125
+ protected abstract loggingWidgets(region: string): IWidget[][];
126
+
127
+ /**
128
+ * Build this compute's **trace** widget rows. Gated behind
129
+ * {@link dashboardSection} so it is only used when tracing is enabled.
130
+ */
131
+ protected abstract tracingWidgets(region: string): IWidget[][];
132
+ }
133
+
134
+ /**
135
+ * A compute's self-reported CloudWatch Dashboard section. `health` and `logging`
136
+ * are always present; `tracing` is populated only when tracing is enabled on the
137
+ * compute. (The Dashboard may still hide `logging` / `tracing` via its display
138
+ * options.)
139
+ */
140
+ export interface ComputeDashboardSection {
141
+ /** Display label used as the compute's group header. */
142
+ label: string;
143
+ /** Health widget rows — always present. */
144
+ health: IWidget[][];
145
+ /** Log widget rows — always present (logs are always captured). */
146
+ logging?: IWidget[][];
147
+ /** Trace widget rows — present only when tracing is enabled. */
148
+ tracing?: IWidget[][];
37
149
  }
@@ -20,6 +20,7 @@ export interface LambdaShapedCompute extends Compute {
20
20
  readonly fn: cdk.aws_lambda_nodejs.NodejsFunction;
21
21
  readonly apiGateway: cdk.aws_apigateway.RestApi;
22
22
  readonly apiUrl: string;
23
+ readonly logGroup: cdk.aws_logs.ILogGroup;
23
24
  }
24
25
 
25
26
  /**
@@ -0,0 +1,149 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * CDK tests for the config registry — specifically the getConfigLocation ↔ finalizeConfigRegistry
6
+ * interaction. `getConfigLocation()` creates the config bucket eagerly (so co-located compute can
7
+ * inject BLOCKS_CONFIG_BUCKET/KEY at construction); finalize must therefore still upload the config
8
+ * object + wire the computes whenever a bucket exists, even if zero entries were registered —
9
+ * otherwise that compute's loadConfigToProcessEnv() would 404 forever against a created-but-empty
10
+ * bucket.
11
+ */
12
+ import assert from 'node:assert';
13
+ import { afterEach, test } from 'node:test';
14
+ import * as cdk from 'aws-cdk-lib';
15
+ import { Match, Template } from 'aws-cdk-lib/assertions';
16
+ import type { IWidget } from 'aws-cdk-lib/aws-cloudwatch';
17
+ import { Construct } from 'constructs';
18
+ import { Compute } from './compute/compute.js';
19
+ import { finalizeConfigRegistry, getConfigLocation, registerConfig } from './config-registry.js';
20
+ import { DEFAULT_NODE_RUNTIME } from './node-version.js';
21
+
22
+ // getConfigLocation reads globalThis.CURRENT_BLOCKS_STACK to place the bucket under the owning
23
+ // stack/backend; clear it between tests so one test's owner never leaks into another.
24
+ afterEach(() => {
25
+ delete (globalThis as any).CURRENT_BLOCKS_STACK;
26
+ });
27
+
28
+ // A real app's compute comes from @aws-blocks/bb-lambda-compute, which core's own tests can't
29
+ // depend on. This is the same shape: a Compute that owns a real Lambda function and injects config
30
+ // via addEnvironment — enough for finalizeConfigRegistry to stamp BLOCKS_CONFIG_BUCKET/KEY on it.
31
+ class TestCompute extends Compute {
32
+ readonly fn: cdk.aws_lambda.Function;
33
+
34
+ constructor(scope: Construct, id: string) {
35
+ super(id, { parent: scope as never });
36
+ this.fn = new cdk.aws_lambda.Function(this, 'Handler', {
37
+ runtime: DEFAULT_NODE_RUNTIME,
38
+ handler: 'index.handler',
39
+ code: cdk.aws_lambda.Code.fromInline('exports.handler = async () => {};'),
40
+ });
41
+ }
42
+
43
+ setEnv(key: string, value: string): void {
44
+ this.fn.addEnvironment(key, value);
45
+ }
46
+
47
+ // Observability hooks are irrelevant to config-registry tests — stub them so
48
+ // this test double satisfies Compute's abstract contract.
49
+ protected applyTracing(): void {}
50
+ protected healthWidgets(): IWidget[][] {
51
+ return [];
52
+ }
53
+ protected loggingWidgets(): IWidget[][] {
54
+ return [];
55
+ }
56
+ protected tracingWidgets(): IWidget[][] {
57
+ return [];
58
+ }
59
+ }
60
+
61
+ function stackWithCompute(id: string): {
62
+ stack: cdk.Stack;
63
+ role: cdk.aws_iam.Role;
64
+ computes: readonly Compute[];
65
+ } {
66
+ const app = new cdk.App();
67
+ const stack = new cdk.Stack(app, id);
68
+ const role = new cdk.aws_iam.Role(stack, 'BlocksRole', {
69
+ assumedBy: new cdk.aws_iam.ServicePrincipal('lambda.amazonaws.com'),
70
+ });
71
+ const compute = new TestCompute(stack, 'Compute');
72
+ return { stack, role, computes: [compute] };
73
+ }
74
+
75
+ test('finalize uploads + wires the computes even with zero entries when a bucket was created', () => {
76
+ const { stack, role, computes } = stackWithCompute('EmptyWithBucket');
77
+ // Simulate a co-located BB that creates the bucket but registers no config of its own.
78
+ getConfigLocation(stack);
79
+ finalizeConfigRegistry(stack, role, computes);
80
+
81
+ const t = Template.fromStack(stack);
82
+ assert.strictEqual(Object.keys(t.findResources('AWS::S3::Bucket')).length, 1, 'one config bucket');
83
+ t.resourceCountIs('Custom::CDKBucketDeployment', 1); // the (empty) blocks-config.json is uploaded
84
+ t.hasResourceProperties('AWS::Lambda::Function', {
85
+ Environment: { Variables: Match.objectLike({ BLOCKS_CONFIG_KEY: 'blocks-config.json' }) },
86
+ });
87
+ // Read is granted once to the shared role (not per-function), so every compute that assumes it
88
+ // — including co-located compute that never went through finalize — can read the object.
89
+ t.hasResourceProperties('AWS::IAM::Policy', {
90
+ PolicyDocument: {
91
+ Statement: Match.arrayWith([
92
+ Match.objectLike({ Action: Match.arrayWith([Match.stringLikeRegexp('^s3:GetObject')]) }),
93
+ ]),
94
+ },
95
+ Roles: Match.arrayWith([Match.objectLike({ Ref: Match.stringLikeRegexp('BlocksRole') })]),
96
+ });
97
+ });
98
+
99
+ test('finalize is a no-op with zero entries and no bucket', () => {
100
+ const { stack, role, computes } = stackWithCompute('EmptyNoBucket');
101
+ finalizeConfigRegistry(stack, role, computes);
102
+
103
+ const t = Template.fromStack(stack);
104
+ assert.strictEqual(Object.keys(t.findResources('AWS::S3::Bucket')).length, 0, 'no config bucket created');
105
+ t.resourceCountIs('Custom::CDKBucketDeployment', 0);
106
+ });
107
+
108
+ test('finalize uploads + wires the computes when config was registered (bucket auto-created)', () => {
109
+ const { stack, role, computes } = stackWithCompute('WithEntries');
110
+ registerConfig(stack, 'BLOCKS_SOMETHING', 'value');
111
+ finalizeConfigRegistry(stack, role, computes);
112
+
113
+ const t = Template.fromStack(stack);
114
+ assert.strictEqual(Object.keys(t.findResources('AWS::S3::Bucket')).length, 1, 'one config bucket');
115
+ t.resourceCountIs('Custom::CDKBucketDeployment', 1);
116
+ t.hasResourceProperties('AWS::Lambda::Function', {
117
+ Environment: { Variables: Match.objectLike({ BLOCKS_CONFIG_KEY: 'blocks-config.json' }) },
118
+ });
119
+ });
120
+
121
+ test('the config bucket is created under the owning stack/backend, not the (deep) caller scope', () => {
122
+ // Mimic a BlocksBackend embedded in a customer stack: the owner is a nested construct, and the
123
+ // first caller of getConfigLocation is a *deep* construct (like the AgentCore Runtime).
124
+ const app = new cdk.App();
125
+ const stack = new cdk.Stack(app, 'CustomerStack');
126
+ const owner = new Construct(stack, 'Embedded'); // stands in for the BlocksBackend construct
127
+ (globalThis as any).CURRENT_BLOCKS_STACK = owner;
128
+ const deepScope = new Construct(new Construct(owner, 'agent'), 'runtime');
129
+
130
+ getConfigLocation(deepScope);
131
+
132
+ const t = Template.fromStack(stack);
133
+ const bucketIds = Object.keys(t.findResources('AWS::S3::Bucket'));
134
+ assert.strictEqual(bucketIds.length, 1, 'exactly one config bucket');
135
+ // Logical IDs encode the construct path — under the owner it's `EmbeddedBlocksConfigBucket…`,
136
+ // at the stack root it would be `BlocksConfigBucket…`. Pin that it follows the owner.
137
+ assert.ok(bucketIds[0].startsWith('Embedded'), `bucket should be nested under the owner, got ${bucketIds[0]}`);
138
+ });
139
+
140
+ test('getConfigLocation creates exactly one bucket across repeated calls (idempotent)', () => {
141
+ const app = new cdk.App();
142
+ const stack = new cdk.Stack(app, 'Idempotent');
143
+ const a = getConfigLocation(stack);
144
+ const b = getConfigLocation(stack);
145
+ assert.strictEqual(a.key, b.key, 'same config key');
146
+ assert.strictEqual(a.bucketName, b.bucketName, 'same bucket');
147
+ const t = Template.fromStack(stack);
148
+ assert.strictEqual(Object.keys(t.findResources('AWS::S3::Bucket')).length, 1, 'exactly one bucket');
149
+ });