@aws-blocks/core 0.1.18 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/cdk/__fixtures__/import-meta-handler.d.ts +6 -0
  2. package/dist/cdk/__fixtures__/import-meta-handler.d.ts.map +1 -0
  3. package/dist/cdk/__fixtures__/import-meta-handler.js +13 -0
  4. package/dist/cdk/blocks-backend.d.ts +46 -9
  5. package/dist/cdk/blocks-backend.d.ts.map +1 -1
  6. package/dist/cdk/blocks-backend.js +66 -67
  7. package/dist/cdk/blocks-backend.test.js +114 -56
  8. package/dist/cdk/blocks-defaults.d.ts +70 -0
  9. package/dist/cdk/blocks-defaults.d.ts.map +1 -0
  10. package/dist/cdk/blocks-defaults.js +27 -0
  11. package/dist/cdk/blocks-defaults.test.d.ts +2 -0
  12. package/dist/cdk/blocks-defaults.test.d.ts.map +1 -0
  13. package/dist/cdk/blocks-defaults.test.js +24 -0
  14. package/dist/cdk/blocks-stack.test.js +74 -31
  15. package/dist/cdk/bundling.d.ts +42 -0
  16. package/dist/cdk/bundling.d.ts.map +1 -0
  17. package/dist/cdk/bundling.js +72 -0
  18. package/dist/cdk/bundling.test.d.ts +2 -0
  19. package/dist/cdk/bundling.test.d.ts.map +1 -0
  20. package/dist/cdk/bundling.test.js +80 -0
  21. package/dist/cdk/compute/compute.d.ts +33 -0
  22. package/dist/cdk/compute/compute.d.ts.map +1 -0
  23. package/dist/cdk/compute/compute.js +28 -0
  24. package/dist/cdk/compute/default-compute-factory.d.ts +33 -0
  25. package/dist/cdk/compute/default-compute-factory.d.ts.map +1 -0
  26. package/dist/cdk/compute/default-compute-factory.js +3 -0
  27. package/dist/cdk/index.d.ts +92 -12
  28. package/dist/cdk/index.d.ts.map +1 -1
  29. package/dist/cdk/index.js +141 -25
  30. package/dist/cdk/internal.d.ts +25 -0
  31. package/dist/cdk/internal.d.ts.map +1 -0
  32. package/dist/cdk/internal.js +27 -0
  33. package/dist/cdk/mixins.d.ts +18 -4
  34. package/dist/cdk/mixins.d.ts.map +1 -1
  35. package/dist/cdk/mixins.js +31 -7
  36. package/dist/cdk/mixins.test.js +45 -0
  37. package/dist/common/index.d.ts +9 -0
  38. package/dist/common/index.d.ts.map +1 -1
  39. package/dist/hosting-secrets.d.ts +48 -0
  40. package/dist/hosting-secrets.d.ts.map +1 -0
  41. package/dist/hosting-secrets.js +57 -0
  42. package/dist/hosting-secrets.test.d.ts +2 -0
  43. package/dist/hosting-secrets.test.d.ts.map +1 -0
  44. package/dist/hosting-secrets.test.js +81 -0
  45. package/dist/hosting.d.ts +88 -6
  46. package/dist/hosting.d.ts.map +1 -1
  47. package/dist/hosting.js +83 -24
  48. package/dist/hosting.test.js +158 -10
  49. package/dist/index.cdk.d.ts +13 -11
  50. package/dist/index.cdk.d.ts.map +1 -1
  51. package/dist/index.cdk.js +13 -8
  52. package/dist/index.d.ts +8 -7
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +9 -5
  55. package/dist/lambda-handler.test.js +66 -0
  56. package/dist/pipeline/index.d.ts +24 -2
  57. package/dist/pipeline/index.d.ts.map +1 -1
  58. package/dist/pipeline/index.js +41 -1
  59. package/dist/pipeline.test.d.ts +2 -0
  60. package/dist/pipeline.test.d.ts.map +1 -0
  61. package/dist/pipeline.test.js +47 -0
  62. package/dist/rpc.d.ts +21 -0
  63. package/dist/rpc.d.ts.map +1 -1
  64. package/dist/rpc.js +46 -3
  65. package/dist/rpc.test.js +58 -1
  66. package/dist/scripts/config.d.ts +15 -0
  67. package/dist/scripts/config.d.ts.map +1 -0
  68. package/dist/scripts/config.js +29 -0
  69. package/dist/scripts/deploy.d.ts.map +1 -1
  70. package/dist/scripts/deploy.js +4 -0
  71. package/dist/scripts/dev-server.d.ts +8 -0
  72. package/dist/scripts/dev-server.d.ts.map +1 -1
  73. package/dist/scripts/dev-server.js +31 -0
  74. package/dist/scripts/generate-client.d.ts.map +1 -1
  75. package/dist/scripts/generate-client.js +9 -4
  76. package/dist/scripts/generate-client.test.js +19 -4
  77. package/dist/scripts/index.d.ts +13 -10
  78. package/dist/scripts/index.d.ts.map +1 -1
  79. package/dist/scripts/index.js +11 -8
  80. package/dist/scripts/preflight-credentials.d.ts +30 -0
  81. package/dist/scripts/preflight-credentials.d.ts.map +1 -0
  82. package/dist/scripts/preflight-credentials.js +108 -0
  83. package/dist/scripts/preflight-credentials.test.d.ts +2 -0
  84. package/dist/scripts/preflight-credentials.test.d.ts.map +1 -0
  85. package/dist/scripts/preflight-credentials.test.js +77 -0
  86. package/dist/scripts/sandbox-args.test.d.ts +2 -0
  87. package/dist/scripts/sandbox-args.test.d.ts.map +1 -0
  88. package/dist/scripts/sandbox-args.test.js +77 -0
  89. package/dist/scripts/sandbox.d.ts +52 -0
  90. package/dist/scripts/sandbox.d.ts.map +1 -1
  91. package/dist/scripts/sandbox.js +92 -15
  92. package/dist/scripts/secret.d.ts +15 -0
  93. package/dist/scripts/secret.d.ts.map +1 -0
  94. package/dist/scripts/secret.js +29 -0
  95. package/dist/scripts/secret.test.d.ts +2 -0
  96. package/dist/scripts/secret.test.d.ts.map +1 -0
  97. package/dist/scripts/secret.test.js +28 -0
  98. package/dist/scripts/telemetry.test.js +2 -0
  99. package/dist/scripts/typegen.d.ts +15 -0
  100. package/dist/scripts/typegen.d.ts.map +1 -0
  101. package/dist/scripts/typegen.js +16 -0
  102. package/dist/secret-naming.d.ts +58 -0
  103. package/dist/secret-naming.d.ts.map +1 -0
  104. package/dist/secret-naming.js +67 -0
  105. package/dist/secret-naming.test.d.ts +2 -0
  106. package/dist/secret-naming.test.d.ts.map +1 -0
  107. package/dist/secret-naming.test.js +40 -0
  108. package/dist/telemetry/environment.d.ts.map +1 -1
  109. package/dist/telemetry/environment.js +3 -1
  110. package/dist/telemetry/telemetry.test.js +10 -0
  111. package/dist/version.d.ts +1 -1
  112. package/dist/version.d.ts.map +1 -1
  113. package/dist/version.js +1 -1
  114. package/package.json +7 -1
  115. package/src/cdk/__fixtures__/import-meta-handler.ts +16 -0
  116. package/src/cdk/blocks-backend.test.ts +308 -237
  117. package/src/cdk/blocks-backend.ts +98 -77
  118. package/src/cdk/blocks-defaults.test.ts +27 -0
  119. package/src/cdk/blocks-defaults.ts +75 -0
  120. package/src/cdk/blocks-stack.test.ts +160 -110
  121. package/src/cdk/bundling.test.ts +90 -0
  122. package/src/cdk/bundling.ts +76 -0
  123. package/src/cdk/compute/compute.ts +37 -0
  124. package/src/cdk/compute/default-compute-factory.ts +37 -0
  125. package/src/cdk/index.ts +178 -27
  126. package/src/cdk/internal.ts +30 -0
  127. package/src/cdk/mixins.test.ts +56 -1
  128. package/src/cdk/mixins.ts +32 -7
  129. package/src/common/index.ts +9 -0
  130. package/src/hosting-secrets.test.ts +106 -0
  131. package/src/hosting-secrets.ts +106 -0
  132. package/src/hosting.test.ts +244 -63
  133. package/src/hosting.ts +180 -68
  134. package/src/index.cdk.ts +79 -36
  135. package/src/index.ts +35 -16
  136. package/src/lambda-handler.test.ts +74 -0
  137. package/src/pipeline/index.ts +64 -11
  138. package/src/pipeline.test.ts +55 -0
  139. package/src/rpc.test.ts +66 -1
  140. package/src/rpc.ts +60 -3
  141. package/src/scripts/config.ts +35 -0
  142. package/src/scripts/deploy.ts +5 -0
  143. package/src/scripts/dev-server.ts +40 -0
  144. package/src/scripts/generate-client.test.ts +43 -8
  145. package/src/scripts/generate-client.ts +70 -65
  146. package/src/scripts/index.ts +20 -17
  147. package/src/scripts/preflight-credentials.test.ts +110 -0
  148. package/src/scripts/preflight-credentials.ts +131 -0
  149. package/src/scripts/sandbox-args.test.ts +92 -0
  150. package/src/scripts/sandbox.ts +102 -15
  151. package/src/scripts/secret.test.ts +35 -0
  152. package/src/scripts/secret.ts +35 -0
  153. package/src/scripts/telemetry.test.ts +2 -0
  154. package/src/scripts/typegen.ts +18 -0
  155. package/src/secret-naming.test.ts +59 -0
  156. package/src/secret-naming.ts +99 -0
  157. package/src/telemetry/environment.ts +3 -1
  158. package/src/telemetry/telemetry.test.ts +12 -0
  159. package/src/version.ts +1 -1
@@ -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
+ }
@@ -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 type * as cdk from 'aws-cdk-lib';
5
+ import type { Compute } from './compute.js';
6
+ import type { BlocksStack } from '../index.js';
7
+ import type { BlocksBackend } from '../blocks-backend.js';
8
+
9
+ /**
10
+ * Lambda-shaped surface of the default compute that the legacy
11
+ * `handler` / `gateway` / `apiUrl` accessors on BlocksStack/BlocksBackend read.
12
+ * The default compute (`LambdaCompute` from `@aws-blocks/bb-lambda-compute`)
13
+ * satisfies this structurally, so core exposes those accessors without
14
+ * importing the concrete class. To be removed with those accessors once
15
+ * consumers move to the multi-compute model.
16
+ *
17
+ * @internal
18
+ */
19
+ export interface LambdaShapedCompute extends Compute {
20
+ readonly fn: cdk.aws_lambda_nodejs.NodejsFunction;
21
+ readonly apiGateway: cdk.aws_apigateway.RestApi;
22
+ readonly apiUrl: string;
23
+ }
24
+
25
+ /**
26
+ * Builds the default {@link Compute} for a stack/backend. `create()` reads it
27
+ * from its props (`CoreBlocksStackProps.defaultComputeFactory`) and calls it to
28
+ * build the default without importing a concrete compute class — the factory is
29
+ * supplied by whoever owns both core and a concrete compute package (the
30
+ * umbrella `@aws-blocks/blocks`, which injects `LambdaCompute`). It lives on
31
+ * `CoreBlocksStackProps`/`CoreBlocksBackendProps`, which the umbrella derives
32
+ * from the customer-facing props, so it is absent from `BlocksStackProps` and
33
+ * customers cannot set it.
34
+ *
35
+ * @internal
36
+ */
37
+ export type DefaultComputeFactory = (root: BlocksStack | BlocksBackend) => Compute;
package/src/cdk/index.ts CHANGED
@@ -15,40 +15,88 @@ 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';
19
+ import type { Compute } from './compute/compute.js';
20
+ import type { DefaultComputeFactory, LambdaShapedCompute } from './compute/default-compute-factory.js';
18
21
 
19
- export { BlocksBackend, type BlocksBackendProps } from './blocks-backend.js';
22
+ export {
23
+ BlocksBackend,
24
+ type BlocksBackendProps,
25
+ type CoreBlocksBackendProps,
26
+ SHARED_HANDLER_TIMEOUT_SECONDS,
27
+ } from './blocks-backend.js';
20
28
  export { DEFAULT_NODE_RUNTIME } from './node-version.js';
29
+ export { blocksNodejsBundling } from './bundling.js';
21
30
  export { SandboxDisableDeletionProtection } from './mixins.js';
22
31
  export { registerConfig, finalizeConfigRegistry } from './config-registry.js';
32
+ export {
33
+ type BlocksDefaults,
34
+ BlocksPresets,
35
+ } from './blocks-defaults.js';
23
36
  export { synthGuard } from './synth-guard.js';
24
37
  export type { ScopeOptions } from '../index.js';
25
38
  export { ApiError, isBlocksError, hasAuthError, DEFAULT_API_ERROR_NAME } from '../errors.js';
26
39
 
40
+ /**
41
+ * Core's `create()` props: the public {@link BlocksStackProps} plus the required
42
+ * `defaultComputeFactory`. The umbrella (`@aws-blocks/blocks`) supplies the
43
+ * factory (which builds a `LambdaCompute`) by spreading it onto the customer's
44
+ * props; customers use {@link BlocksStackProps} and never set the factory.
45
+ *
46
+ * Kept separate (rather than a `create()` argument) so the factory travels with
47
+ * the props object and core stays free of any concrete compute class.
48
+ *
49
+ * @internal
50
+ */
51
+ export interface CoreBlocksStackProps extends BlocksStackProps {
52
+ /** Builds the stack's default compute. Injected by `@aws-blocks/blocks`. */
53
+ defaultComputeFactory: DefaultComputeFactory;
54
+ }
55
+
27
56
  export class BlocksStack extends cdk.Stack implements BaseBlocksStack {
28
57
  public readonly id: string;
29
- public readonly apiUrl: string;
30
- public readonly gateway: cdk.aws_apigateway.RestApi;
31
- public readonly handler: cdk.aws_lambda_nodejs.NodejsFunction;
32
58
  public readonly backendHandlerPath: string;
33
59
  /** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */
34
60
  public readonly executionRole: cdk.aws_iam.IRole;
61
+ /** Infrastructure defaults for Building Blocks created under this stack. */
62
+ public readonly defaults: BlocksDefaults;
63
+ /** The default compute (owns the Lambda function + API Gateway); set in `create()`. @internal */
64
+ _defaultCompute?: Compute;
65
+
66
+ /** The default compute's Lambda function. To be removed once consumers move to the multi-compute model. */
67
+ get handler(): cdk.aws_lambda_nodejs.NodejsFunction {
68
+ return this.requireDefaultCompute().fn;
69
+ }
70
+ /** The default compute's API Gateway REST API. To be removed once consumers move to the multi-compute model. */
71
+ get gateway(): cdk.aws_apigateway.RestApi {
72
+ return this.requireDefaultCompute().apiGateway;
73
+ }
74
+ /** The default compute's RPC endpoint URL. To be removed once consumers move to the multi-compute model. */
75
+ get apiUrl(): string {
76
+ return this.requireDefaultCompute().apiUrl;
77
+ }
78
+
79
+ private requireDefaultCompute(): LambdaShapedCompute {
80
+ if (!this._defaultCompute) {
81
+ throw new Error('Blocks stack not fully initialized — access .handler/.gateway/.apiUrl after BlocksStack.create() resolves.');
82
+ }
83
+ return this._defaultCompute as LambdaShapedCompute;
84
+ }
35
85
 
36
86
  private constructor(scope: Construct, id: string, props: BlocksStackProps) {
37
87
  super(scope, id, props);
38
88
  this.id = id;
39
89
  this.backendHandlerPath = props.backendHandlerPath;
90
+ this.defaults = props.defaults;
40
91
 
41
92
  // Set globalThis so Building Blocks attach directly to this stack
42
93
  (globalThis as any).CURRENT_BLOCKS_STACK = this;
43
94
 
44
95
  const infra = setupBlocksInfra(this, props, id);
45
- this.handler = infra.handler;
46
- this.gateway = infra.gateway;
47
- this.apiUrl = infra.apiUrl;
48
96
  this.executionRole = infra.executionRole;
49
97
  }
50
98
 
51
- static async create(scope: Construct, id: string, props: BlocksStackProps) {
99
+ static async create(scope: Construct, id: string, props: CoreBlocksStackProps) {
52
100
  assertCdkConditionActive();
53
101
 
54
102
  // Detect ambient pipeline stage scope set by Pipeline appFile imports
@@ -56,6 +104,13 @@ export class BlocksStack extends cdk.Stack implements BaseBlocksStack {
56
104
  const actualScope = pipelineScope || scope;
57
105
 
58
106
  const stack = new BlocksStack(actualScope, id, props);
107
+ // Create the default compute before importing the backend: it OWNS the
108
+ // Lambda function + API Gateway (which back .handler/.gateway/.apiUrl), and
109
+ // a block reading `this.compute` in its constructor (during that import)
110
+ // must resolve to it. The factory is supplied by the umbrella
111
+ // @aws-blocks/blocks (which injects LambdaCompute) via props, so core never
112
+ // imports the concrete compute class.
113
+ stack._defaultCompute = props.defaultComputeFactory(stack);
59
114
  // file:// URL (not a raw path) so the cache-busting query works on Windows,
60
115
  // where an absolute path like `D:\...` is rejected as URL scheme `d:`.
61
116
  const backendUrl = pathToFileURL(props.backendCDKPath);
@@ -86,50 +141,146 @@ export class Scope extends Construct {
86
141
  readonly bbName?: string;
87
142
  readonly bbVersion?: string;
88
143
 
144
+ /**
145
+ * The owning stack/backend (the root of the Blocks construct tree), resolved
146
+ * once at construction: the nearest BlocksStack/BlocksBackend up the construct
147
+ * tree, or the ambient `globalThis.CURRENT_BLOCKS_STACK` fallback. All
148
+ * root-derived accessors below read from this instead of each repeating the
149
+ * tree walk.
150
+ */
151
+ private readonly root: BlocksStack | BlocksBackend;
152
+
153
+ /**
154
+ * Compute assigned at this node. Applies to this block and is inherited by
155
+ * descendants (a nearer assignment wins). Covers both a handler assigned to a
156
+ * specific compute and a scope-level default for its subtree. Internal until
157
+ * the customer-facing surface exists.
158
+ * @internal
159
+ */
160
+ _compute?: Compute;
161
+
89
162
  constructor(id: string, options?: ScopeOptions) {
90
163
  const parent = options?.parent || (globalThis as any).CURRENT_BLOCKS_STACK;
91
164
  super(parent, id);
92
165
  this.id = id;
93
166
  this.parent = parent;
167
+ this.root = this.resolveRoot();
94
168
  }
95
169
 
96
- get handler() {
97
- // Walk up the construct tree to find the owning BlocksStack/BlocksBackend
170
+ /**
171
+ * Walk up the construct tree to the nearest owning BlocksStack/BlocksBackend;
172
+ * fall back to the ambient `globalThis.CURRENT_BLOCKS_STACK`. Called once from
173
+ * the constructor; the result is cached in {@link root}.
174
+ */
175
+ private resolveRoot(): BlocksStack | BlocksBackend {
98
176
  let current: Construct = this;
99
177
  while (current.node.scope) {
100
178
  current = current.node.scope as Construct;
101
179
  if (current instanceof BlocksStack || current instanceof BlocksBackend) {
102
- return current.handler;
180
+ return current;
103
181
  }
104
182
  }
105
- // Fallback to globalThis for backward compatibility
106
- return ((globalThis as any).CURRENT_BLOCKS_STACK as { handler: cdk.aws_lambda_nodejs.NodejsFunction }).handler;
183
+ // Fallback to the ambient stack. In production this is always a real
184
+ // BlocksStack/BlocksBackend; the cast also admits the test doubles that set
185
+ // globalThis.CURRENT_BLOCKS_STACK to a stub exposing the same surface.
186
+ return (globalThis as any).CURRENT_BLOCKS_STACK as BlocksStack | BlocksBackend;
187
+ }
188
+
189
+ get handler() {
190
+ return this.root.handler;
107
191
  }
108
192
 
109
193
  /**
110
194
  * 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.
195
+ * their permissions to this role; CDK's `grant*()` / `addToPrincipalPolicy()`
196
+ * route those grants to the role's default (inline) policy.
118
197
  */
119
198
  get executionRole(): cdk.aws_iam.IRole {
199
+ return this.root.executionRole;
200
+ }
201
+
202
+ /**
203
+ * The compute this block runs on: the nearest `_compute` assigned on this
204
+ * block or an ancestor scope, else the owning stack/backend's default compute.
205
+ *
206
+ * For any app that doesn't assign a compute, this always resolves to the
207
+ * default — so reads are a no-op refactor. `_compute` is internal
208
+ * (test/framework) until the customer-facing surface exists; there is no
209
+ * public option to set it yet.
210
+ */
211
+ get compute(): Compute {
212
+ for (let current: ScopeParent | undefined = this; current; current = (current as Scope).parent) {
213
+ const assigned = (current as Scope)._compute;
214
+ if (assigned) return assigned;
215
+ }
216
+ const defaultCompute = this.root._defaultCompute;
217
+ if (!defaultCompute) {
218
+ throw new Error('Default compute not initialized — BlocksStack/BlocksBackend.create() must run before resolving `compute`.');
219
+ }
220
+ return defaultCompute;
221
+ }
222
+
223
+ /**
224
+ * The backend entry file the owning BlocksStack/BlocksBackend runs — the
225
+ * single handler entry shared across the whole app.
226
+ */
227
+ get backendHandlerPath(): string {
228
+ return this.root.backendHandlerPath;
229
+ }
230
+
231
+ /**
232
+ * The owning stack/backend's token-free root identity. This is the value the
233
+ * runtime receives as `BLOCKS_STACK_NAME` and rebuilds `fullId` from, so
234
+ * physical resource names (DynamoDB tables, env-var keys, IAM ARNs) derived
235
+ * from `fullId` match byte-for-byte between synth and runtime — otherwise the
236
+ * runtime looks up names that were never created. `BlocksBackend` exposes this
237
+ * as `fullId` ({@link BlocksBackend.fullId}); `BlocksStack` as `id`.
238
+ */
239
+ get backendStackName(): string {
240
+ const name = this.root instanceof BlocksBackend ? this.root.fullId : this.root.id;
241
+ if (!name) {
242
+ throw new Error('Owning Blocks stack/backend has no id to derive BLOCKS_STACK_NAME');
243
+ }
244
+ return name;
245
+ }
246
+
247
+ get fullId(): string {
248
+ return computeScopeFullId(this);
249
+ }
250
+
251
+ /**
252
+ * The stack-wide infrastructure {@link BlocksDefaults} registered by
253
+ * `BlocksStack.create` / `BlocksBackend.create`. Read these in a Building
254
+ * Block's CDK constructor to resolve a durability value, letting a per-block
255
+ * option override:
256
+ *
257
+ * ```ts
258
+ * const removalPolicy = options?.removalPolicy ?? this.defaults.removalPolicy;
259
+ * ```
260
+ */
261
+ get defaults(): BlocksDefaults {
262
+ // Resolve the same way as handler/executionRole: walk up to the owning
263
+ // BlocksStack/BlocksBackend and read its defaults, so several backends in
264
+ // one stack each keep their own posture. Falls back to the ambient stack,
265
+ // then to the production preset when none was registered.
120
266
  let current: Construct = this;
121
267
  while (current.node.scope) {
122
268
  current = current.node.scope as Construct;
123
269
  if (current instanceof BlocksStack || current instanceof BlocksBackend) {
124
- return current.executionRole;
270
+ return current.defaults;
125
271
  }
126
272
  }
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);
273
+ const ambient = ((globalThis as any).CURRENT_BLOCKS_STACK as { defaults?: BlocksDefaults } | undefined)?.defaults;
274
+ if (ambient) return ambient;
275
+ // No owning BlocksStack/BlocksBackend in the tree and none ambient — this is
276
+ // usually a deliberate test stub, but could be a real misconfiguration (a
277
+ // block built outside any Blocks backend). Fall back to the safe production
278
+ // posture, and log so it's debuggable if it fires unexpectedly.
279
+ console.warn(
280
+ `[Blocks] Scope "${this.id}" resolved infrastructure defaults with no owning ` +
281
+ 'BlocksStack/BlocksBackend in scope; falling back to BlocksPresets.production.',
282
+ );
283
+ return BlocksPresets.production;
133
284
  }
134
285
 
135
286
  protected buildUserAgentChain(): [string, string][] {
@@ -0,0 +1,30 @@
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
+ export type { DefaultComputeFactory } from './compute/default-compute-factory.js';
28
+ // Reserved `/aws-blocks` path segment, needed by concrete computes (e.g.
29
+ // LambdaCompute in @aws-blocks/bb-lambda-compute) to build their API route tree.
30
+ 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');