@aws-blocks/core 0.1.18 → 0.2.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 (70) 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 +11 -0
  5. package/dist/cdk/blocks-backend.d.ts.map +1 -1
  6. package/dist/cdk/blocks-backend.js +22 -6
  7. package/dist/cdk/blocks-backend.test.js +66 -0
  8. package/dist/cdk/blocks-defaults.d.ts +44 -0
  9. package/dist/cdk/blocks-defaults.d.ts.map +1 -0
  10. package/dist/cdk/blocks-defaults.js +23 -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 +22 -0
  14. package/dist/cdk/blocks-stack.test.js +8 -1
  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/index.d.ts +46 -7
  25. package/dist/cdk/index.d.ts.map +1 -1
  26. package/dist/cdk/index.js +84 -18
  27. package/dist/cdk/internal.d.ts +24 -0
  28. package/dist/cdk/internal.d.ts.map +1 -0
  29. package/dist/cdk/internal.js +27 -0
  30. package/dist/cdk/mixins.d.ts +18 -4
  31. package/dist/cdk/mixins.d.ts.map +1 -1
  32. package/dist/cdk/mixins.js +31 -7
  33. package/dist/cdk/mixins.test.js +45 -0
  34. package/dist/common/index.d.ts +9 -0
  35. package/dist/common/index.d.ts.map +1 -1
  36. package/dist/index.cdk.d.ts +2 -1
  37. package/dist/index.cdk.d.ts.map +1 -1
  38. package/dist/index.cdk.js +2 -1
  39. package/dist/rpc.d.ts.map +1 -1
  40. package/dist/rpc.js +12 -3
  41. package/dist/rpc.test.js +20 -0
  42. package/dist/scripts/telemetry.test.js +2 -0
  43. package/dist/telemetry/environment.d.ts.map +1 -1
  44. package/dist/telemetry/environment.js +3 -1
  45. package/dist/telemetry/telemetry.test.js +10 -0
  46. package/dist/version.d.ts +1 -1
  47. package/dist/version.d.ts.map +1 -1
  48. package/dist/version.js +1 -1
  49. package/package.json +6 -1
  50. package/src/cdk/__fixtures__/import-meta-handler.ts +16 -0
  51. package/src/cdk/blocks-backend.test.ts +76 -0
  52. package/src/cdk/blocks-backend.ts +35 -6
  53. package/src/cdk/blocks-defaults.test.ts +25 -0
  54. package/src/cdk/blocks-defaults.ts +49 -0
  55. package/src/cdk/blocks-stack.test.ts +8 -1
  56. package/src/cdk/bundling.test.ts +90 -0
  57. package/src/cdk/bundling.ts +76 -0
  58. package/src/cdk/compute/compute.ts +37 -0
  59. package/src/cdk/index.ts +94 -19
  60. package/src/cdk/internal.ts +29 -0
  61. package/src/cdk/mixins.test.ts +56 -1
  62. package/src/cdk/mixins.ts +32 -7
  63. package/src/common/index.ts +9 -0
  64. package/src/index.cdk.ts +5 -1
  65. package/src/rpc.test.ts +22 -0
  66. package/src/rpc.ts +19 -3
  67. package/src/scripts/telemetry.test.ts +2 -0
  68. package/src/telemetry/environment.ts +3 -1
  69. package/src/telemetry/telemetry.test.ts +12 -0
  70. package/src/version.ts +1 -1
@@ -0,0 +1,90 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { test, describe } from 'node:test';
5
+ import assert from 'node:assert';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { dirname, join } from 'node:path';
8
+ import { mkdtempSync, rmSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { createRequire } from 'node:module';
11
+ import { build } from 'esbuild';
12
+ import { OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs';
13
+ import { blocksNodejsBundling } from './bundling.js';
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const importMetaFixture = join(__dirname, '__fixtures__', 'import-meta-handler.js');
17
+ const requireCjs = createRequire(import.meta.url);
18
+
19
+ describe('blocksNodejsBundling', () => {
20
+ test('injects the import.meta.* CJS shim for the default (CJS) output', () => {
21
+ const out = blocksNodejsBundling({ minify: true, esbuildArgs: { '--conditions': 'aws-runtime' } });
22
+
23
+ // Caller options are preserved.
24
+ assert.equal(out.minify, true);
25
+ assert.equal(out.esbuildArgs?.['--conditions'], 'aws-runtime');
26
+
27
+ // All three import.meta path properties are substituted.
28
+ assert.ok(out.esbuildArgs?.['--define:import.meta.url']);
29
+ assert.ok(out.esbuildArgs?.['--define:import.meta.dirname']);
30
+ assert.ok(out.esbuildArgs?.['--define:import.meta.filename']);
31
+
32
+ // The banner defines the substituted identifiers via CommonJS primitives.
33
+ assert.match(out.banner ?? '', /pathToFileURL\(__filename\)/);
34
+ assert.match(out.banner ?? '', /__dirname/);
35
+ assert.match(out.banner ?? '', /__filename/);
36
+ });
37
+
38
+ test('leaves ESM output untouched (import.meta works natively there)', () => {
39
+ const input = { format: OutputFormat.ESM, esbuildArgs: { '--conditions': 'aws-runtime' } };
40
+ const out = blocksNodejsBundling(input);
41
+
42
+ assert.deepEqual(out, input);
43
+ assert.equal(out.esbuildArgs?.['--define:import.meta.url'], undefined);
44
+ assert.equal(out.banner, undefined);
45
+ });
46
+
47
+ test('prepends the shim while keeping a caller-supplied banner', () => {
48
+ const out = blocksNodejsBundling({ banner: '// caller banner' });
49
+ assert.match(out.banner ?? '', /pathToFileURL\(__filename\)/);
50
+ assert.ok((out.banner ?? '').includes('// caller banner'));
51
+ });
52
+
53
+ test('a CJS bundle built with the shim resolves import.meta.url at load (no crash)', async () => {
54
+ // Bundle the fixture exactly as NodejsFunction would: apply the helper's `banner`
55
+ // and its `--define:import.meta.*` esbuildArgs. Without the shim this fixture's
56
+ // top-level `fileURLToPath(import.meta.url)` becomes `fileURLToPath(undefined)`
57
+ // and throws when the module is loaded.
58
+ const opts = blocksNodejsBundling({ minify: true });
59
+ const define: Record<string, string> = {};
60
+ for (const [key, value] of Object.entries(opts.esbuildArgs ?? {})) {
61
+ const m = key.match(/^--define:(.+)$/);
62
+ if (m) define[m[1]] = String(value);
63
+ }
64
+
65
+ const tmp = mkdtempSync(join(tmpdir(), 'bb-shim-'));
66
+ const outfile = join(tmp, 'out.cjs');
67
+ try {
68
+ await build({
69
+ entryPoints: [importMetaFixture],
70
+ bundle: true,
71
+ platform: 'node',
72
+ format: 'cjs',
73
+ minify: true,
74
+ banner: { js: opts.banner ?? '' },
75
+ define,
76
+ outfile,
77
+ logLevel: 'silent',
78
+ });
79
+
80
+ // Loading the bundle must not throw, and import.meta.url must resolve to a real
81
+ // (file-URL-derived) path rather than being undefined.
82
+ const mod = requireCjs(outfile);
83
+ assert.equal(typeof mod.moduleDir, 'string');
84
+ assert.ok(mod.moduleDir.length > 0, 'moduleDir should resolve to a non-empty path');
85
+ assert.equal(typeof mod.handler, 'function');
86
+ } finally {
87
+ rmSync(tmp, { recursive: true, force: true });
88
+ }
89
+ });
90
+ });
@@ -0,0 +1,76 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { OutputFormat, type BundlingOptions } from 'aws-cdk-lib/aws-lambda-nodejs';
5
+
6
+ /** Banner-defined identifiers the shim substitutes `import.meta.*` with (CJS only). */
7
+ const IMPORT_META_SHIM = {
8
+ url: '__blocksImportMetaUrl',
9
+ dirname: '__blocksImportMetaDirname',
10
+ filename: '__blocksImportMetaFilename',
11
+ } as const;
12
+
13
+ /**
14
+ * Wrap a `NodejsFunction` `bundling` config with the framework's hardened esbuild
15
+ * defaults, so every Lambda the framework bundles behaves consistently.
16
+ *
17
+ * **What it fixes.** `NodejsFunction` bundles to **CommonJS**, where `import.meta` is
18
+ * empty. Any bundled code that does `fileURLToPath(import.meta.url)` (a customer
19
+ * handler, a Building Block's `aws-runtime` code, or a dependency) would otherwise
20
+ * become `fileURLToPath(undefined)` and throw at Lambda load — esbuild only *warns*
21
+ * (`empty-import-meta`), so the broken bundle deploys and 502s on first invocation.
22
+ *
23
+ * **How.** For CJS output this shims `import.meta.url` / `import.meta.dirname` /
24
+ * `import.meta.filename` to their CommonJS equivalents (`pathToFileURL(__filename)`,
25
+ * `__dirname`, `__filename`) via an esbuild `--define` + `banner`. This is the same
26
+ * approach esbuild blesses (defining `import.meta` also suppresses the warning) and
27
+ * that Rollup applies by default, so:
28
+ * - a handler that reads `import.meta.url` no longer crashes at load, and
29
+ * - a bundled dependency that merely *contains* `import.meta` (even in dead code) no
30
+ * longer trips a build failure.
31
+ *
32
+ * The value resolves to the **bundled output file** (esbuild flattens the module tree),
33
+ * which is correct for the common cases — a value computed at synth (e.g. a
34
+ * `migrationsPath`) or dead interop fallbacks — but note it does not point at your
35
+ * source layout. Runtime code that must read a file relative to itself should not rely
36
+ * on `import.meta.url` inside a bundle; resolve such paths at synth time or ship the
37
+ * file as an asset. ESM output (`OutputFormat.ESM`) supports `import.meta` natively and
38
+ * is left untouched.
39
+ *
40
+ * All other options (`minify`, `commandHooks`, `externalModules`, other `esbuildArgs`
41
+ * such as `--conditions`, and any caller `banner`) are preserved.
42
+ *
43
+ * @param options - The site-specific `NodejsFunction` bundling options (optional).
44
+ * @returns The same options with the CJS `import.meta` shim merged in.
45
+ *
46
+ * @example
47
+ * new lambda.NodejsFunction(scope, 'Handler', {
48
+ * entry,
49
+ * bundling: blocksNodejsBundling({ minify: true, esbuildArgs: { '--conditions': 'aws-runtime' } }),
50
+ * });
51
+ */
52
+ export function blocksNodejsBundling(options: BundlingOptions = {}): BundlingOptions {
53
+ // ESM output has real `import.meta` — nothing to shim, and `require` in the banner
54
+ // wouldn't resolve. Only the CommonJS bundle needs the shim.
55
+ if (options.format === OutputFormat.ESM) return options;
56
+
57
+ const shimBanner = [
58
+ `const ${IMPORT_META_SHIM.url}=require('url').pathToFileURL(__filename).href;`,
59
+ `const ${IMPORT_META_SHIM.dirname}=__dirname;`,
60
+ `const ${IMPORT_META_SHIM.filename}=__filename;`,
61
+ ].join('');
62
+
63
+ return {
64
+ ...options,
65
+ // Prepend the shim definitions; keep any caller-supplied banner after them.
66
+ banner: options.banner ? `${shimBanner}\n${options.banner}` : shimBanner,
67
+ esbuildArgs: {
68
+ ...options.esbuildArgs,
69
+ // Substitute import.meta.* with the banner identifiers. Also suppresses esbuild's
70
+ // empty-import-meta warning, so import.meta anywhere in the graph is safe.
71
+ '--define:import.meta.url': IMPORT_META_SHIM.url,
72
+ '--define:import.meta.dirname': IMPORT_META_SHIM.dirname,
73
+ '--define:import.meta.filename': IMPORT_META_SHIM.filename,
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,37 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { Scope } from '../index.js';
5
+
6
+ /**
7
+ * Base class for a Blocks *compute* — a runtime that executes handler code
8
+ * (Lambda today; containers later). A compute owns the physical function/service
9
+ * plus its ingress, and receives config via {@link setEnv}.
10
+ *
11
+ * The backend entry and stack name a compute needs are inherited from
12
+ * {@link Scope} (`backendHandlerPath` / `backendStackName`), which resolve them
13
+ * from the owning BlocksStack/BlocksBackend — never caller-supplied, so every
14
+ * compute in an app runs the same backend and agrees on the resource-name
15
+ * namespace.
16
+ *
17
+ * The abstract base lives in core (a framework primitive); concrete computes
18
+ * live in their own packages (e.g. `LambdaCompute` in `@aws-blocks/bb-lambda-compute`).
19
+ *
20
+ * @internal Not exported from the package's public entry points. Customers
21
+ * cannot instantiate a compute until the customer-facing surface exists.
22
+ */
23
+ export abstract class Compute extends Scope {
24
+ /**
25
+ * API namespaces assigned to run on this compute — recorded so request
26
+ * routing can map a namespace to the compute that hosts it. Currently
27
+ * unpopulated (no compute assignment surface yet).
28
+ */
29
+ readonly namespaces: string[] = [];
30
+
31
+ /**
32
+ * Inject a runtime configuration value (an environment variable) into this
33
+ * compute. The framework calls this instead of `handler.addEnvironment()`
34
+ * directly so config targets the right compute.
35
+ */
36
+ abstract setEnv(key: string, value: string): void;
37
+ }
package/src/cdk/index.ts CHANGED
@@ -15,11 +15,17 @@ import {
15
15
  import { setupBlocksInfra, BlocksBackend, assertCdkConditionActive } from './blocks-backend.js';
16
16
  import { addBlocksStackMetadata } from './stack-metadata.js';
17
17
  import { finalizeConfigRegistry } from './config-registry.js';
18
+ import { type BlocksDefaults, BlocksPresets } from './blocks-defaults.js';
18
19
 
19
20
  export { BlocksBackend, type BlocksBackendProps } from './blocks-backend.js';
20
21
  export { DEFAULT_NODE_RUNTIME } from './node-version.js';
22
+ export { blocksNodejsBundling } from './bundling.js';
21
23
  export { SandboxDisableDeletionProtection } from './mixins.js';
22
24
  export { registerConfig, finalizeConfigRegistry } from './config-registry.js';
25
+ export {
26
+ type BlocksDefaults,
27
+ BlocksPresets,
28
+ } from './blocks-defaults.js';
23
29
  export { synthGuard } from './synth-guard.js';
24
30
  export type { ScopeOptions } from '../index.js';
25
31
  export { ApiError, isBlocksError, hasAuthError, DEFAULT_API_ERROR_NAME } from '../errors.js';
@@ -32,11 +38,14 @@ export class BlocksStack extends cdk.Stack implements BaseBlocksStack {
32
38
  public readonly backendHandlerPath: string;
33
39
  /** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */
34
40
  public readonly executionRole: cdk.aws_iam.IRole;
41
+ /** Infrastructure defaults for Building Blocks created under this stack. */
42
+ public readonly defaults: BlocksDefaults;
35
43
 
36
44
  private constructor(scope: Construct, id: string, props: BlocksStackProps) {
37
45
  super(scope, id, props);
38
46
  this.id = id;
39
47
  this.backendHandlerPath = props.backendHandlerPath;
48
+ this.defaults = props.defaults;
40
49
 
41
50
  // Set globalThis so Building Blocks attach directly to this stack
42
51
  (globalThis as any).CURRENT_BLOCKS_STACK = this;
@@ -86,50 +95,116 @@ export class Scope extends Construct {
86
95
  readonly bbName?: string;
87
96
  readonly bbVersion?: string;
88
97
 
98
+ /**
99
+ * The owning stack/backend (the root of the Blocks construct tree), resolved
100
+ * once at construction: the nearest BlocksStack/BlocksBackend up the construct
101
+ * tree, or the ambient `globalThis.CURRENT_BLOCKS_STACK` fallback. All
102
+ * root-derived accessors below read from this instead of each repeating the
103
+ * tree walk.
104
+ */
105
+ private readonly root: BlocksStack | BlocksBackend;
106
+
89
107
  constructor(id: string, options?: ScopeOptions) {
90
108
  const parent = options?.parent || (globalThis as any).CURRENT_BLOCKS_STACK;
91
109
  super(parent, id);
92
110
  this.id = id;
93
111
  this.parent = parent;
112
+ this.root = this.resolveRoot();
94
113
  }
95
114
 
96
- get handler() {
97
- // Walk up the construct tree to find the owning BlocksStack/BlocksBackend
115
+ /**
116
+ * Walk up the construct tree to the nearest owning BlocksStack/BlocksBackend;
117
+ * fall back to the ambient `globalThis.CURRENT_BLOCKS_STACK`. Called once from
118
+ * the constructor; the result is cached in {@link root}.
119
+ */
120
+ private resolveRoot(): BlocksStack | BlocksBackend {
98
121
  let current: Construct = this;
99
122
  while (current.node.scope) {
100
123
  current = current.node.scope as Construct;
101
124
  if (current instanceof BlocksStack || current instanceof BlocksBackend) {
102
- return current.handler;
125
+ return current;
103
126
  }
104
127
  }
105
- // Fallback to globalThis for backward compatibility
106
- return ((globalThis as any).CURRENT_BLOCKS_STACK as { handler: cdk.aws_lambda_nodejs.NodejsFunction }).handler;
128
+ // Fallback to the ambient stack. In production this is always a real
129
+ // BlocksStack/BlocksBackend; the cast also admits the test doubles that set
130
+ // globalThis.CURRENT_BLOCKS_STACK to a stub exposing the same surface.
131
+ return (globalThis as any).CURRENT_BLOCKS_STACK as BlocksStack | BlocksBackend;
132
+ }
133
+
134
+ get handler() {
135
+ return this.root.handler;
107
136
  }
108
137
 
109
138
  /**
110
139
  * The shared IAM role assumed by all Blocks compute. Building Blocks grant
111
- * their permissions to this role instead of to an individual function's
112
- * auto-role. CDK's `grant*()` / `addToPrincipalPolicy()` route those grants
113
- * to the role's default (inline) policy — exactly where they landed on the
114
- * auto-generated role before.
115
- *
116
- * Resolves the same way as {@link handler}: walk up to the owning
117
- * BlocksStack/BlocksBackend, falling back to the ambient stack.
140
+ * their permissions to this role; CDK's `grant*()` / `addToPrincipalPolicy()`
141
+ * route those grants to the role's default (inline) policy.
118
142
  */
119
143
  get executionRole(): cdk.aws_iam.IRole {
144
+ return this.root.executionRole;
145
+ }
146
+
147
+ /**
148
+ * The backend entry file the owning BlocksStack/BlocksBackend runs — the
149
+ * single handler entry shared across the whole app.
150
+ */
151
+ get backendHandlerPath(): string {
152
+ return this.root.backendHandlerPath;
153
+ }
154
+
155
+ /**
156
+ * The owning stack/backend's token-free root identity. This is the value the
157
+ * runtime receives as `BLOCKS_STACK_NAME` and rebuilds `fullId` from, so
158
+ * physical resource names (DynamoDB tables, env-var keys, IAM ARNs) derived
159
+ * from `fullId` match byte-for-byte between synth and runtime — otherwise the
160
+ * runtime looks up names that were never created. `BlocksBackend` exposes this
161
+ * as `fullId` ({@link BlocksBackend.fullId}); `BlocksStack` as `id`.
162
+ */
163
+ get backendStackName(): string {
164
+ const name = this.root instanceof BlocksBackend ? this.root.fullId : this.root.id;
165
+ if (!name) {
166
+ throw new Error('Owning Blocks stack/backend has no id to derive BLOCKS_STACK_NAME');
167
+ }
168
+ return name;
169
+ }
170
+
171
+ get fullId(): string {
172
+ return computeScopeFullId(this);
173
+ }
174
+
175
+ /**
176
+ * The stack-wide infrastructure {@link BlocksDefaults} registered by
177
+ * `BlocksStack.create` / `BlocksBackend.create`. Read these in a Building
178
+ * Block's CDK constructor to resolve a durability value, letting a per-block
179
+ * option override:
180
+ *
181
+ * ```ts
182
+ * const removalPolicy = options?.removalPolicy ?? this.defaults.removalPolicy;
183
+ * ```
184
+ */
185
+ get defaults(): BlocksDefaults {
186
+ // Resolve the same way as handler/executionRole: walk up to the owning
187
+ // BlocksStack/BlocksBackend and read its defaults, so several backends in
188
+ // one stack each keep their own posture. Falls back to the ambient stack,
189
+ // then to the production preset when none was registered.
120
190
  let current: Construct = this;
121
191
  while (current.node.scope) {
122
192
  current = current.node.scope as Construct;
123
193
  if (current instanceof BlocksStack || current instanceof BlocksBackend) {
124
- return current.executionRole;
194
+ return current.defaults;
125
195
  }
126
196
  }
127
- // Fallback to globalThis for backward compatibility
128
- return ((globalThis as any).CURRENT_BLOCKS_STACK as { executionRole: cdk.aws_iam.IRole }).executionRole;
129
- }
130
-
131
- get fullId(): string {
132
- return computeScopeFullId(this);
197
+ const ambient = ((globalThis as any).CURRENT_BLOCKS_STACK as { defaults?: BlocksDefaults } | undefined)?.defaults;
198
+ if (ambient) return ambient;
199
+ // No owning BlocksStack/BlocksBackend in the tree and none ambient — this is
200
+ // usually a deliberate test stub, but could be a real misconfiguration (a
201
+ // block built outside any Blocks backend). Fall back to the safe production
202
+ // posture, and log so it's debuggable if it fires unexpectedly.
203
+ console.warn(
204
+ `[Blocks] Scope "${this.id}" resolved infrastructure defaults with no owning ` +
205
+ 'BlocksStack/BlocksBackend in scope; falling back to BlocksPresets.production.',
206
+ );
207
+ return BlocksPresets.production;
133
208
  }
134
209
 
135
210
  protected buildUserAgentChain(): [string, string][] {
@@ -0,0 +1,29 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Internal CDK entry point — framework- and test-only surface that is
6
+ * intentionally NOT part of the public API (`@aws-blocks/core` /
7
+ * `@aws-blocks/core/cdk`).
8
+ *
9
+ * The compute abstraction lives behind this path while it has no public,
10
+ * customer-facing surface. Importing from here is a signal that you are inside
11
+ * the framework or a test, not a customer.
12
+ *
13
+ * Planned removal: once a customer can assign a compute and have it actually
14
+ * take effect — i.e. `this.compute` resolution and request routing to the
15
+ * chosen compute both exist, plus a synth-time guard that rejects an assignment
16
+ * with no route — these exports move to the public CDK entry point
17
+ * (`@aws-blocks/core/cdk`, re-exported from `index.cdk.ts`) and this file is
18
+ * deleted. It must NOT be made public before then: a compute a customer can
19
+ * declare but that is silently ignored is a worse experience than not having
20
+ * the feature. Until that flip, treat everything here as unstable — no
21
+ * backward-compatibility guarantee.
22
+ *
23
+ * @internal
24
+ */
25
+
26
+ export { Compute } from './compute/compute.js';
27
+ // Reserved `/aws-blocks` path segment, needed by concrete computes (e.g.
28
+ // LambdaCompute in @aws-blocks/bb-lambda-compute) to build their API route tree.
29
+ export { BLOCKS_NAMESPACE } from '../constants.js';
@@ -2,7 +2,6 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  import { test, describe } from 'node:test';
5
- import assert from 'node:assert';
6
5
  import * as cdk from 'aws-cdk-lib';
7
6
  import { Template, Match } from 'aws-cdk-lib/assertions';
8
7
  import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb';
@@ -110,6 +109,62 @@ describe('sandbox removal policy', () => {
110
109
  });
111
110
  });
112
111
 
112
+ test('SandboxDisableDeletionProtection mixin: deletionProtection disabled on DynamoDB table', () => {
113
+ const app = new cdk.App({ context: { sandboxMode: 'true' } });
114
+ const stack = new cdk.Stack(app, 'TestStack');
115
+
116
+ new Table(stack, 'MyTable', {
117
+ partitionKey: { name: 'pk', type: AttributeType.STRING },
118
+ billingMode: BillingMode.PAY_PER_REQUEST,
119
+ deletionProtection: true,
120
+ });
121
+
122
+ RemovalPolicies.of(stack).destroy();
123
+ Mixins.of(stack).apply(new SandboxDisableDeletionProtection());
124
+
125
+ const template = Template.fromStack(stack);
126
+ // DynamoDB refuses DeleteTable while protection is on, regardless of DeletionPolicy
127
+ template.hasResourceProperties('AWS::DynamoDB::Table', {
128
+ DeletionProtectionEnabled: false,
129
+ });
130
+ template.hasResource('AWS::DynamoDB::Table', {
131
+ DeletionPolicy: 'Delete',
132
+ });
133
+ });
134
+
135
+ test('production mode: deletionProtection stays enabled on DynamoDB table when mixin is not applied', () => {
136
+ const app = new cdk.App();
137
+ const stack = new cdk.Stack(app, 'TestStack');
138
+
139
+ new Table(stack, 'MyTable', {
140
+ partitionKey: { name: 'pk', type: AttributeType.STRING },
141
+ billingMode: BillingMode.PAY_PER_REQUEST,
142
+ deletionProtection: true,
143
+ });
144
+
145
+ const template = Template.fromStack(stack);
146
+ template.hasResourceProperties('AWS::DynamoDB::Table', {
147
+ DeletionProtectionEnabled: true,
148
+ });
149
+ });
150
+
151
+ test('SandboxDisableDeletionProtection mixin: does not set DeletionProtectionEnabled on unprotected tables', () => {
152
+ const app = new cdk.App({ context: { sandboxMode: 'true' } });
153
+ const stack = new cdk.Stack(app, 'TestStack');
154
+
155
+ new Table(stack, 'MyTable', {
156
+ partitionKey: { name: 'pk', type: AttributeType.STRING },
157
+ billingMode: BillingMode.PAY_PER_REQUEST,
158
+ });
159
+
160
+ Mixins.of(stack).apply(new SandboxDisableDeletionProtection());
161
+
162
+ const template = Template.fromStack(stack);
163
+ template.hasResourceProperties('AWS::DynamoDB::Table', {
164
+ DeletionProtectionEnabled: Match.absent(),
165
+ });
166
+ });
167
+
113
168
  test('SandboxDisableDeletionProtection mixin: skips constructs without deletionProtection', () => {
114
169
  const app = new cdk.App();
115
170
  const stack = new cdk.Stack(app, 'TestStack');
package/src/cdk/mixins.ts CHANGED
@@ -6,13 +6,36 @@ import type { IConstruct } from 'constructs';
6
6
  import type { IMixin } from 'constructs';
7
7
 
8
8
  /**
9
- * Disables deletion protection on any construct that has a `deletionProtection`
10
- * property (e.g. RDS clusters, RDS instances). Uses duck-typing so it
11
- * automatically covers current and future resource types.
9
+ * Property names used across CloudFormation resources to express deletion
10
+ * protection. L2 constructs and most L1s use `deletionProtection` (e.g. RDS
11
+ * clusters and instances, ELBv2 load balancers), while others spell it
12
+ * `deletionProtectionEnabled` (e.g. the DynamoDB `CfnTable` behind an L2
13
+ * `Table`, Aurora DSQL clusters).
14
+ */
15
+ const DELETION_PROTECTION_PROPERTIES = ['deletionProtection', 'deletionProtectionEnabled'] as const;
16
+
17
+ /**
18
+ * Disables deletion protection on any construct that exposes a deletion
19
+ * protection property (e.g. RDS clusters, RDS instances, DynamoDB tables).
20
+ * Uses duck-typing so it automatically covers current and future resource
21
+ * types, matching both the `deletionProtection` and
22
+ * `deletionProtectionEnabled` spellings.
23
+ *
24
+ * Note that a DynamoDB L2 `Table` only accepts `deletionProtection` as a prop
25
+ * and does not re-expose it, so tables are matched through their underlying
26
+ * L1 `CfnTable.deletionProtectionEnabled`.
12
27
  *
13
28
  * Intended for sandbox teardown — use in the CDK layer alongside
14
29
  * `RemovalPolicies.of(stack).destroy()` to ensure `sandbox:destroy` can
15
- * delete the entire stack without manual cleanup.
30
+ * delete the entire stack without manual cleanup. DynamoDB refuses
31
+ * `DeleteTable` while deletion protection is enabled, regardless of the
32
+ * CloudFormation `DeletionPolicy`.
33
+ *
34
+ * @deprecated Prefer the stack-wide `defaults` prop on `BlocksStack`/`BlocksBackend`
35
+ * (`defaults: BlocksPresets.sandbox` / `.production`, or `sandboxMode ? … : …`).
36
+ * Amazon-authored Building Blocks now read removal policy + deletion protection
37
+ * from `defaults`, so this mixin is no longer needed; it is retained only for
38
+ * backward compatibility.
16
39
  *
17
40
  * @example
18
41
  * ```ts
@@ -27,15 +50,17 @@ import type { IMixin } from 'constructs';
27
50
  */
28
51
  export class SandboxDisableDeletionProtection extends Mixin implements IMixin {
29
52
  supports(construct: any): boolean {
30
- return 'deletionProtection' in construct;
53
+ return DELETION_PROTECTION_PROPERTIES.some((property) => property in construct);
31
54
  }
32
55
  applyTo(node: IConstruct): void {
33
56
  // Only flip explicitly-enabled protection. When undefined (the default),
34
57
  // deletion protection is already off — setting it to false would emit the
35
58
  // property in the CloudFormation template, which breaks Aurora DB instances
36
59
  // (RDS rejects DeletionProtection on cluster members).
37
- if ((node as any).deletionProtection === true) {
38
- (node as any).deletionProtection = false;
60
+ for (const property of DELETION_PROTECTION_PROPERTIES) {
61
+ if ((node as any)[property] === true) {
62
+ (node as any)[property] = false;
63
+ }
39
64
  }
40
65
  }
41
66
  }
@@ -3,6 +3,7 @@
3
3
 
4
4
  import type { StackProps } from 'aws-cdk-lib';
5
5
  import type { Construct } from 'constructs';
6
+ import type { BlocksDefaults } from '../cdk/blocks-defaults.js';
6
7
  import { CORE_VERSION } from '../version.js';
7
8
  import { OFFICIAL_BB_NAMES } from './official-bb-names.generated.js';
8
9
  export { OFFICIAL_BB_NAMES } from './official-bb-names.generated.js';
@@ -325,6 +326,14 @@ export function computeScopeFullId(scope: { id: string; parent?: any }) {
325
326
  export interface BlocksStackProps extends StackProps {
326
327
  backendHandlerPath: string;
327
328
  backendCDKPath: string;
329
+ /**
330
+ * Stack-wide infrastructure defaults applied to every Building Block (removal
331
+ * policy, deletion protection, …). Start from `BlocksPresets.sandbox` or
332
+ * `BlocksPresets.production` and override individual fields as needed. Any
333
+ * field a block also exposes as a per-block option is overridden by that
334
+ * option. See `BlocksDefaults` in `@aws-blocks/core/cdk`.
335
+ */
336
+ defaults: BlocksDefaults;
328
337
  }
329
338
 
330
339
  export class BlocksStack {
package/src/index.cdk.ts CHANGED
@@ -8,7 +8,11 @@ export { EventSourceMapping } from './lambda-handler.js';
8
8
  export { BlocksStackProps } from './common/index.js';
9
9
  export { registerSdkIdentifiers, getSdkIdentifiers, getAllSdkIdentifiers, _resetSdkRegistry } from './common/sdk-registry.js';
10
10
  export { getConfig, getConfigSync, preloadConfig, loadConfigToProcessEnv, _resetConfigCache } from './common/config.js';
11
- export { BlocksStack, Scope, SandboxDisableDeletionProtection, BlocksBackend, registerConfig, finalizeConfigRegistry, synthGuard, DEFAULT_NODE_RUNTIME, type BlocksBackendProps } from './cdk/index.js';
11
+ export { BlocksStack, Scope, SandboxDisableDeletionProtection, BlocksBackend, registerConfig, finalizeConfigRegistry, synthGuard, DEFAULT_NODE_RUNTIME, blocksNodejsBundling, type BlocksBackendProps } from './cdk/index.js';
12
+ export {
13
+ type BlocksDefaults,
14
+ BlocksPresets,
15
+ } from './cdk/index.js';
12
16
  export {
13
17
  Hosting,
14
18
  type HostingProps,
package/src/rpc.test.ts CHANGED
@@ -97,6 +97,28 @@ describe('params decoding', () => {
97
97
  });
98
98
  });
99
99
 
100
+ describe('-32602 Invalid Params validation', () => {
101
+ for (const params of ['abc', 42, true, false, null]) {
102
+ it(`rejects ${JSON.stringify(params)} params`, () => {
103
+ const result = parseRpcRequest(JSON.stringify({
104
+ jsonrpc: '2.0',
105
+ method: 'api.echo',
106
+ params,
107
+ id: 'request-1',
108
+ }));
109
+
110
+ assert.strictEqual(result.ok, false);
111
+ if (!result.ok) {
112
+ const response = JSON.parse(result.response);
113
+ assert.strictEqual(response.error.code, RpcErrorCode.InvalidParams);
114
+ assert.strictEqual(response.error.data.name, 'InvalidParams');
115
+ assert.ok(response.error.message.includes('expected an array or object'));
116
+ assert.strictEqual(response.id, 'request-1');
117
+ }
118
+ });
119
+ }
120
+ });
121
+
100
122
  describe('batch requests (top-level JSON array body)', () => {
101
123
  it('rejects an array body as Invalid Request with a null id', () => {
102
124
  const result = parseRpcRequest(JSON.stringify([
package/src/rpc.ts CHANGED
@@ -118,15 +118,31 @@ export function parseRpcRequest(bodyText: string): RpcParseResult {
118
118
  };
119
119
  }
120
120
 
121
+ const hasParams = Object.hasOwn(parsed, 'params');
122
+ if (hasParams && (parsed.params === null || typeof parsed.params !== 'object')) {
123
+ return {
124
+ ok: false,
125
+ response: errorResponse(
126
+ RpcErrorCode.InvalidParams,
127
+ 'Invalid params: expected an array or object',
128
+ id,
129
+ { name: 'InvalidParams' },
130
+ ),
131
+ };
132
+ }
133
+
134
+ let args: unknown[] = [];
135
+ if (hasParams) {
136
+ args = Array.isArray(parsed.params) ? parsed.params : Object.values(parsed.params);
137
+ }
138
+
121
139
  return {
122
140
  ok: true,
123
141
  request: {
124
142
  apiNamespace: parsed.method.substring(0, dotIndex),
125
143
  method: parsed.method.substring(dotIndex + 1),
126
144
  // JSON-RPC 2.0 §4.2: params may be an array (positional) or object (named).
127
- args: Array.isArray(parsed.params)
128
- ? parsed.params
129
- : Object.values(parsed.params ?? {}),
145
+ args,
130
146
  id,
131
147
  },
132
148
  };
@@ -26,6 +26,8 @@ const NO_CI_ENV: Record<string, string> = {
26
26
  TF_BUILD: '',
27
27
  BITBUCKET_BUILD_NUMBER: '',
28
28
  BUILDKITE: '',
29
+ RENDER: '',
30
+ TASKCLUSTER_ROOT_URL: '',
29
31
  AWS_BLOCKS_DISABLE_TELEMETRY: '',
30
32
  };
31
33
 
@@ -29,7 +29,9 @@ export function isCI(): boolean {
29
29
  process.env.JENKINS_URL ||
30
30
  process.env.TF_BUILD ||
31
31
  process.env.BITBUCKET_BUILD_NUMBER ||
32
- process.env.BUILDKITE
32
+ process.env.BUILDKITE ||
33
+ process.env.RENDER ||
34
+ process.env.TASKCLUSTER_ROOT_URL
33
35
  );
34
36
  }
35
37