@aws-blocks/core 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +12 -0
  2. package/dist/cdk/blocks-backend.d.ts +4 -1
  3. package/dist/cdk/blocks-backend.d.ts.map +1 -1
  4. package/dist/cdk/blocks-backend.js +56 -10
  5. package/dist/cdk/blocks-backend.test.js +71 -1
  6. package/dist/cdk/blocks-defaults.d.ts +11 -0
  7. package/dist/cdk/blocks-defaults.d.ts.map +1 -1
  8. package/dist/cdk/blocks-stack.test.js +23 -3
  9. package/dist/cdk/compute/compute.d.ts +80 -2
  10. package/dist/cdk/compute/compute.d.ts.map +1 -1
  11. package/dist/cdk/compute/compute.js +57 -3
  12. package/dist/cdk/config-registry.test.js +12 -0
  13. package/dist/cdk/dashboard-registry.d.ts +41 -0
  14. package/dist/cdk/dashboard-registry.d.ts.map +1 -0
  15. package/dist/cdk/dashboard-registry.js +61 -0
  16. package/dist/cdk/index.d.ts +67 -11
  17. package/dist/cdk/index.d.ts.map +1 -1
  18. package/dist/cdk/index.js +100 -18
  19. package/dist/cdk/internal.d.ts +3 -1
  20. package/dist/cdk/internal.d.ts.map +1 -1
  21. package/dist/cdk/internal.js +4 -1
  22. package/dist/cdk/tracer-registry.d.ts +31 -0
  23. package/dist/cdk/tracer-registry.d.ts.map +1 -0
  24. package/dist/cdk/tracer-registry.js +49 -0
  25. package/dist/cdk/vpc-requirements-registry.d.ts +33 -0
  26. package/dist/cdk/vpc-requirements-registry.d.ts.map +1 -0
  27. package/dist/cdk/vpc-requirements-registry.js +46 -0
  28. package/dist/cdk/vpc-types.d.ts +151 -0
  29. package/dist/cdk/vpc-types.d.ts.map +1 -0
  30. package/dist/cdk/vpc-types.js +3 -0
  31. package/dist/cdk/vpc.d.ts +59 -0
  32. package/dist/cdk/vpc.d.ts.map +1 -0
  33. package/dist/cdk/vpc.js +298 -0
  34. package/dist/cdk/vpc.test.d.ts +2 -0
  35. package/dist/cdk/vpc.test.d.ts.map +1 -0
  36. package/dist/cdk/vpc.test.js +285 -0
  37. package/dist/errors.d.ts +5 -0
  38. package/dist/errors.d.ts.map +1 -1
  39. package/dist/errors.js +5 -0
  40. package/dist/hosting.d.ts.map +1 -1
  41. package/dist/hosting.js +2 -0
  42. package/dist/hosting.test.js +39 -1
  43. package/dist/index.cdk.d.ts +2 -1
  44. package/dist/index.cdk.d.ts.map +1 -1
  45. package/dist/index.cdk.js +1 -1
  46. package/dist/lambda-handler.js +9 -2
  47. package/dist/lambda-handler.test.js +61 -1
  48. package/dist/raw-route.d.ts +15 -1
  49. package/dist/raw-route.d.ts.map +1 -1
  50. package/dist/raw-route.js +96 -12
  51. package/dist/raw-route.test.js +332 -1
  52. package/dist/scripts/dev-server.d.ts.map +1 -1
  53. package/dist/scripts/dev-server.js +11 -0
  54. package/dist/scripts/extract-ts-types.d.ts.map +1 -1
  55. package/dist/scripts/extract-ts-types.js +107 -23
  56. package/dist/scripts/extract-ts-types.test.js +225 -26
  57. package/dist/scripts/generate-spec.d.ts.map +1 -1
  58. package/dist/scripts/generate-spec.js +14 -5
  59. package/dist/scripts/generate-spec.test.js +93 -0
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.js +1 -1
  62. package/package.json +8 -1
  63. package/src/cdk/blocks-backend.test.ts +144 -60
  64. package/src/cdk/blocks-backend.ts +298 -239
  65. package/src/cdk/blocks-defaults.ts +12 -0
  66. package/src/cdk/blocks-stack.test.ts +32 -13
  67. package/src/cdk/compute/compute.ts +105 -3
  68. package/src/cdk/config-registry.test.ts +14 -0
  69. package/src/cdk/dashboard-registry.ts +68 -0
  70. package/src/cdk/index.ts +426 -298
  71. package/src/cdk/internal.ts +6 -2
  72. package/src/cdk/tracer-registry.ts +54 -0
  73. package/src/cdk/vpc-requirements-registry.ts +63 -0
  74. package/src/cdk/vpc-types.ts +158 -0
  75. package/src/cdk/vpc.test.ts +348 -0
  76. package/src/cdk/vpc.ts +336 -0
  77. package/src/errors.ts +5 -0
  78. package/src/hosting.test.ts +59 -1
  79. package/src/hosting.ts +3 -0
  80. package/src/index.cdk.ts +7 -0
  81. package/src/lambda-handler.test.ts +79 -1
  82. package/src/lambda-handler.ts +11 -2
  83. package/src/raw-route.test.ts +427 -1
  84. package/src/raw-route.ts +125 -12
  85. package/src/scripts/dev-server.ts +12 -1
  86. package/src/scripts/extract-ts-types.test.ts +228 -26
  87. package/src/scripts/extract-ts-types.ts +104 -20
  88. package/src/scripts/generate-spec.test.ts +101 -0
  89. package/src/scripts/generate-spec.ts +15 -5
  90. package/src/version.ts +1 -1
@@ -0,0 +1,151 @@
1
+ import type * as ec2 from 'aws-cdk-lib/aws-ec2';
2
+ /**
3
+ * A subnet role — the kind of subnet a Building Block needs, expressed as an
4
+ * intent the VPC resolves to a concrete {@link ec2.SubnetSelection}:
5
+ *
6
+ * - `'private-with-egress'` — private subnets with outbound internet access via
7
+ * a NAT gateway. Required by anything that must reach a public AWS endpoint at
8
+ * runtime (e.g. a service with no interface endpoint).
9
+ * - `'isolated'` — private subnets with no internet route at all. Best for
10
+ * resources reached entirely over VPC endpoints or in-VPC (e.g. a database).
11
+ * - `'public'` — subnets with a direct internet gateway route.
12
+ */
13
+ export type SubnetRole = 'private-with-egress' | 'isolated' | 'public';
14
+ /**
15
+ * Options for VPC integration on BlocksStack / BlocksBackend.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const vpc = new ec2.Vpc(app, 'AppVpc', { maxAzs: 2, natGateways: 1 });
20
+ * await BlocksStack.create(app, stackName, {
21
+ * backendHandlerPath: join(__dirname, 'index.handler.ts'),
22
+ * backendCDKPath: join(__dirname, 'index.ts'),
23
+ * defaults: { ...BlocksPresets.production, vpc: { network: vpc } },
24
+ * });
25
+ * ```
26
+ */
27
+ export interface BlocksVpcOptions {
28
+ /**
29
+ * The VPC to place Lambdas and VPC-resident resources into.
30
+ * Create this however you like — standard CDK:
31
+ *
32
+ * @example
33
+ * const vpc = new ec2.Vpc(stack, 'AppVpc', { maxAzs: 2, natGateways: 1 });
34
+ * // or
35
+ * const vpc = ec2.Vpc.fromLookup(stack, 'SharedVpc', { vpcId: 'vpc-abc123' });
36
+ */
37
+ network: ec2.IVpc;
38
+ /**
39
+ * Subnet selection for Lambda and Blocks-managed compute placement.
40
+ *
41
+ * Any subnets you select must belong to {@link network}. This is not
42
+ * validated at synth — CDK can't check it for an imported subnet (which
43
+ * carries no `vpcId`) — so a mismatch surfaces as a CloudFormation error at
44
+ * deploy, not a synth-time failure.
45
+ *
46
+ * @default { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }
47
+ */
48
+ subnets?: ec2.SubnetSelection;
49
+ /**
50
+ * Whether to auto-provision VPC endpoints based on BB registrations.
51
+ * Set to `false` to disable (e.g., when using a shared VPC that already has endpoints).
52
+ *
53
+ * @default true
54
+ */
55
+ provisionEndpoints?: boolean;
56
+ }
57
+ /**
58
+ * VPC requirements declared by a Building Block.
59
+ * Supplied to the `BuildingBlockScope` constructor and collected in the central
60
+ * requirements registry; `finalizeVpc` pulls them to provision endpoints, and the
61
+ * lazy VPC uses `requiresVpc` to decide whether to derive a VPC when none was
62
+ * provided.
63
+ */
64
+ export interface VpcRequirements {
65
+ /** Gateway VPC endpoints this BB needs (e.g., S3, DynamoDB). */
66
+ gatewayEndpoints?: ec2.GatewayVpcEndpointAwsService[];
67
+ /** Interface VPC endpoints this BB needs (e.g., SQS, SSM, Secrets Manager). */
68
+ interfaceEndpoints?: ec2.InterfaceVpcEndpointAwsService[];
69
+ /**
70
+ * Whether this BB **cannot function without a VPC** — i.e. it must run in (or
71
+ * provision resources into) a VPC regardless of whether the customer asked
72
+ * for one. When any registered requirement sets this and no `vpc` was
73
+ * provided, the framework **lazily derives** a default VPC (see
74
+ * `getOrCreateVpc`) rather than erroring — mirroring how bb-data creates its
75
+ * own VPC when no shared one exists.
76
+ *
77
+ * Most BBs leave this unset: they reach AWS services from the AWS-managed
78
+ * Lambda network and only *benefit* from a VPC (endpoints) when one is present.
79
+ * Set it only for a BB whose resources are intrinsically VPC-resident.
80
+ *
81
+ * @default false
82
+ */
83
+ requiresVpc?: boolean;
84
+ /**
85
+ * Whether the BB's **parent runtime** — the shared Blocks handler Lambda (or,
86
+ * in future, a container) that executes this BB's operations — must be able to
87
+ * reach the internet (outbound egress) for the BB to work at runtime.
88
+ *
89
+ * Set this when your BB's runtime code calls a service it can only reach over
90
+ * the public internet. The canonical case: a service with no VPC endpoint
91
+ * (e.g. Aurora DSQL) is reached over a public HTTPS endpoint, so the runtime
92
+ * must sit in a subnet with an egress route; if it's placed in isolated
93
+ * subnets the deploy still succeeds but every call times out at runtime.
94
+ *
95
+ * The framework **validates** this at synth against the runtime's resolved
96
+ * placement (egress is satisfied by a `private-with-egress` or `public`
97
+ * selection) and fails the build with an actionable message on a mismatch. It
98
+ * never moves the runtime — reassigning a customer's explicit placement is not
99
+ * a BB's responsibility, so an unsatisfiable requirement is an error, not a
100
+ * silent relocation.
101
+ *
102
+ * This constrains the BB's **host**. To place compute the BB provisions
103
+ * **itself** (e.g. an Aurora cluster), resolve a subnet inline in your
104
+ * constructor via {@link VpcContext.selectSubnets} instead.
105
+ *
106
+ * @default false
107
+ */
108
+ requiresEgress?: boolean;
109
+ }
110
+ /**
111
+ * The minimal shape {@link VpcContext.selectSubnets} needs from a Building
112
+ * Block to produce an instructive, BB-named error — just its `fullId`. Typed
113
+ * structurally so `vpc-types.ts` stays type-only and free of a dependency on
114
+ * the `Scope` class.
115
+ */
116
+ export interface SubnetScope {
117
+ readonly fullId: string;
118
+ }
119
+ /**
120
+ * Internal VPC context propagated through the construct tree.
121
+ * Set by the CDK-level VPC option. BBs read this to determine their VPC placement.
122
+ * @internal
123
+ */
124
+ export interface VpcContext {
125
+ readonly vpc: ec2.IVpc;
126
+ readonly computeSecurityGroup: ec2.ISecurityGroup;
127
+ readonly computeSubnets: ec2.SubnetSelection;
128
+ /**
129
+ * Resolve a {@link SubnetRole} to a concrete {@link ec2.SubnetSelection} for a
130
+ * resource this Building Block provisions itself, verifying the VPC actually
131
+ * has subnets of that role.
132
+ *
133
+ * Prefer this over building an `ec2.SubnetSelection` by hand: if the VPC has
134
+ * no matching subnet, it throws an actionable, BB-named error at synth
135
+ * ("`KVStore 'app/cache'` needs an 'isolated' subnet, but VPC 'vpc-…' has
136
+ * none …") instead of the opaque CDK "no subnet groups" error thrown later.
137
+ *
138
+ * @param scope the BB requesting the subnet (its `fullId` names the error)
139
+ * @param role the subnet role the BB's own resource needs
140
+ * @param opts.fallback an alternate role to use when `role` is absent from
141
+ * the VPC. Provide it to **explicitly** allow graceful degradation (e.g.
142
+ * Aurora over the Data API works from `'private-with-egress'` when there is
143
+ * no isolated tier); omit it to require `role` strictly and fail otherwise.
144
+ * The downgrade is never silent — it only happens when you opt in here.
145
+ * @throws if the VPC has neither `role` nor (when given) `opts.fallback`
146
+ */
147
+ selectSubnets(scope: SubnetScope, role: SubnetRole, opts?: {
148
+ fallback?: SubnetRole;
149
+ }): ec2.SubnetSelection;
150
+ }
151
+ //# sourceMappingURL=vpc-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vpc-types.d.ts","sourceRoot":"","sources":["../../src/cdk/vpc-types.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,GAAG,MAAM,qBAAqB,CAAC;AAEhD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,UAAU,GAAG,qBAAqB,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEvE;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAChC;;;;;;;;OAQG;IACH,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC;IAElB;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,GAAG,CAAC,eAAe,CAAC;IAE9B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC/B,gEAAgE;IAChE,gBAAgB,CAAC,EAAE,GAAG,CAAC,4BAA4B,EAAE,CAAC;IACtD,+EAA+E;IAC/E,kBAAkB,CAAC,EAAE,GAAG,CAAC,8BAA8B,EAAE,CAAC;IAC1D;;;;;;;;;;;;;OAaG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IAC1B,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC;IACvB,QAAQ,CAAC,oBAAoB,EAAE,GAAG,CAAC,cAAc,CAAC;IAClD,QAAQ,CAAC,cAAc,EAAE,GAAG,CAAC,eAAe,CAAC;IAC7C;;;;;;;;;;;;;;;;;;OAkBG;IACH,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,UAAU,CAAA;KAAE,GAAG,GAAG,CAAC,eAAe,CAAC;CAC3G"}
@@ -0,0 +1,3 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ export {};
@@ -0,0 +1,59 @@
1
+ import * as ec2 from 'aws-cdk-lib/aws-ec2';
2
+ import type { Construct } from 'constructs';
3
+ import type { BlocksVpcOptions, VpcContext } from './vpc-types.js';
4
+ /**
5
+ * Set the VPC context on a scope (BlocksStack or BlocksBackend).
6
+ * Called during stack creation when `vpc` prop is provided.
7
+ * @internal
8
+ */
9
+ export declare function setVpcContext(scope: Construct, context: VpcContext): void;
10
+ /**
11
+ * Get the VPC context from a scope by walking up the construct tree.
12
+ * Used by BBs (e.g., bb-data) to discover the shared VPC.
13
+ * @internal
14
+ */
15
+ export declare function getVpcContext(scope: Construct): VpcContext | undefined;
16
+ /**
17
+ * Does any registered Building Block *require* a VPC (i.e. cannot function
18
+ * without one)? Used to decide whether to lazily derive a VPC when the customer
19
+ * didn't provide one. A BB that merely benefits from endpoints does NOT count —
20
+ * only `requiresVpc: true`.
21
+ * @internal
22
+ */
23
+ export declare function anyRequirementNeedsVpc(scope: Construct): boolean;
24
+ /**
25
+ * Get the framework-owned VPC for a stack, creating one on first use.
26
+ *
27
+ * VPC is a **derived** resource: when a Building Block (or, later, a container
28
+ * compute) requires one and the customer didn't bring their own, Blocks
29
+ * materializes a sensible default here — the same create-if-absent pattern
30
+ * `bb-data` uses for Aurora, lifted to the framework so the whole app shares a
31
+ * single VPC. Keyed by a Symbol on the stack so it's a true singleton: the first
32
+ * caller creates, everyone else reuses. Passing a customer VPC (`vpc:` prop)
33
+ * pre-seeds this via {@link initializeVpc}, so this default is only built when
34
+ * nothing was provided.
35
+ *
36
+ * The default has both a `private-with-egress` tier (for a runtime that needs
37
+ * outbound access — the derivation trigger) and public subnets for the NAT
38
+ * gateway. NAT has a real cost, so this is only created on genuine need, and
39
+ * `create()` emits a notice when it does.
40
+ * @internal
41
+ */
42
+ export declare function getOrCreateVpc(scope: Construct): ec2.IVpc;
43
+ /**
44
+ * Initialize VPC support on a Blocks scope (BlocksStack or BlocksBackend).
45
+ * Creates the security group, sets VPC context, and returns the VpcContext
46
+ * that is used for Lambda placement configuration.
47
+ *
48
+ * Called during setupBlocksInfra when `vpc` prop is present.
49
+ * @internal
50
+ */
51
+ export declare function initializeVpc(scope: Construct, options: BlocksVpcOptions): VpcContext;
52
+ /**
53
+ * Finalize VPC: query all BuildingBlockScope children for their VPC requirements,
54
+ * deduplicate, and provision endpoints.
55
+ * Called after all BBs are constructed (alongside finalizeConfigRegistry).
56
+ * @internal
57
+ */
58
+ export declare function finalizeVpc(scope: Construct, options: BlocksVpcOptions): void;
59
+ //# sourceMappingURL=vpc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vpc.d.ts","sourceRoot":"","sources":["../../src/cdk/vpc.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,GAAG,MAAM,qBAAqB,CAAC;AAC3C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,OAAO,KAAK,EAAE,gBAAgB,EAA2B,UAAU,EAAmB,MAAM,gBAAgB,CAAC;AA4B7G;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI,CAEzE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,GAAG,SAAS,CAQtE;AAID;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAEhE;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,CAOzD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,GAAG,UAAU,CAkDrF;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAyH7E"}
@@ -0,0 +1,298 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { Annotations } from 'aws-cdk-lib';
4
+ import * as ec2 from 'aws-cdk-lib/aws-ec2';
5
+ import { getVpcRequirements as getRegisteredVpcRequirements } from './vpc-requirements-registry.js';
6
+ const VPC_CONTEXT_KEY = Symbol.for('BLOCKS_VPC_CONTEXT');
7
+ /** Map a subnet role to its concrete CDK subnet type. */
8
+ function subnetTypeForRole(role) {
9
+ switch (role) {
10
+ case 'isolated':
11
+ return ec2.SubnetType.PRIVATE_ISOLATED;
12
+ case 'public':
13
+ return ec2.SubnetType.PUBLIC;
14
+ case 'private-with-egress':
15
+ return ec2.SubnetType.PRIVATE_WITH_EGRESS;
16
+ }
17
+ }
18
+ /** Does the VPC actually contain at least one subnet of the given role? */
19
+ function vpcHasRole(vpc, role) {
20
+ switch (role) {
21
+ case 'isolated':
22
+ return vpc.isolatedSubnets.length > 0;
23
+ case 'public':
24
+ return vpc.publicSubnets.length > 0;
25
+ case 'private-with-egress':
26
+ return vpc.privateSubnets.length > 0;
27
+ }
28
+ }
29
+ /**
30
+ * Set the VPC context on a scope (BlocksStack or BlocksBackend).
31
+ * Called during stack creation when `vpc` prop is provided.
32
+ * @internal
33
+ */
34
+ export function setVpcContext(scope, context) {
35
+ scope[VPC_CONTEXT_KEY] = context;
36
+ }
37
+ /**
38
+ * Get the VPC context from a scope by walking up the construct tree.
39
+ * Used by BBs (e.g., bb-data) to discover the shared VPC.
40
+ * @internal
41
+ */
42
+ export function getVpcContext(scope) {
43
+ let current = scope;
44
+ while (current) {
45
+ const ctx = current[VPC_CONTEXT_KEY];
46
+ if (ctx)
47
+ return ctx;
48
+ current = current.node.scope;
49
+ }
50
+ return undefined;
51
+ }
52
+ const LAZY_VPC_KEY = Symbol.for('BLOCKS_LAZY_VPC');
53
+ /**
54
+ * Does any registered Building Block *require* a VPC (i.e. cannot function
55
+ * without one)? Used to decide whether to lazily derive a VPC when the customer
56
+ * didn't provide one. A BB that merely benefits from endpoints does NOT count —
57
+ * only `requiresVpc: true`.
58
+ * @internal
59
+ */
60
+ export function anyRequirementNeedsVpc(scope) {
61
+ return getRegisteredVpcRequirements(scope).some((r) => r.requirements.requiresVpc === true);
62
+ }
63
+ /**
64
+ * Get the framework-owned VPC for a stack, creating one on first use.
65
+ *
66
+ * VPC is a **derived** resource: when a Building Block (or, later, a container
67
+ * compute) requires one and the customer didn't bring their own, Blocks
68
+ * materializes a sensible default here — the same create-if-absent pattern
69
+ * `bb-data` uses for Aurora, lifted to the framework so the whole app shares a
70
+ * single VPC. Keyed by a Symbol on the stack so it's a true singleton: the first
71
+ * caller creates, everyone else reuses. Passing a customer VPC (`vpc:` prop)
72
+ * pre-seeds this via {@link initializeVpc}, so this default is only built when
73
+ * nothing was provided.
74
+ *
75
+ * The default has both a `private-with-egress` tier (for a runtime that needs
76
+ * outbound access — the derivation trigger) and public subnets for the NAT
77
+ * gateway. NAT has a real cost, so this is only created on genuine need, and
78
+ * `create()` emits a notice when it does.
79
+ * @internal
80
+ */
81
+ export function getOrCreateVpc(scope) {
82
+ const holder = scope;
83
+ const existing = holder[LAZY_VPC_KEY];
84
+ if (existing)
85
+ return existing;
86
+ const vpc = new ec2.Vpc(scope, 'BlocksVpc', { maxAzs: 2, natGateways: 1 });
87
+ holder[LAZY_VPC_KEY] = vpc;
88
+ return vpc;
89
+ }
90
+ /**
91
+ * Initialize VPC support on a Blocks scope (BlocksStack or BlocksBackend).
92
+ * Creates the security group, sets VPC context, and returns the VpcContext
93
+ * that is used for Lambda placement configuration.
94
+ *
95
+ * Called during setupBlocksInfra when `vpc` prop is present.
96
+ * @internal
97
+ */
98
+ export function initializeVpc(scope, options) {
99
+ const { network: vpc, subnets } = options;
100
+ // Default Lambda placement to private-with-egress. Guard the default: if the
101
+ // caller didn't specify `subnets` and the VPC has no such tier, fail now with
102
+ // an actionable message instead of letting Lambda placement throw a cryptic
103
+ // CDK error later. (A caller who *explicitly* passes an isolated selection is
104
+ // honored — per-BB `requiresEgress` validation catches BBs that can't run there.)
105
+ if (!subnets && !vpcHasRole(vpc, 'private-with-egress')) {
106
+ throw new Error(`VPC '${vpc.vpcId}' has no private-with-egress subnets, which Blocks uses for Lambda ` +
107
+ `placement by default. Add a PRIVATE_WITH_EGRESS subnet tier (a private subnet with a ` +
108
+ `NAT gateway), or pass 'vpc.subnets' to choose a different placement explicitly. ` +
109
+ `See packages/blocks/VPC.md.`);
110
+ }
111
+ const resolvedSubnets = subnets ?? { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS };
112
+ const computeSecurityGroup = new ec2.SecurityGroup(scope, 'BlocksLambdaSg', {
113
+ vpc,
114
+ description: 'Security group for Blocks Lambda functions in VPC',
115
+ allowAllOutbound: true,
116
+ });
117
+ const context = {
118
+ vpc,
119
+ computeSecurityGroup,
120
+ computeSubnets: resolvedSubnets,
121
+ selectSubnets(scope, role, opts) {
122
+ if (vpcHasRole(vpc, role)) {
123
+ return { subnetType: subnetTypeForRole(role) };
124
+ }
125
+ // Requested role is absent. Fall back only if the BB explicitly opted in
126
+ // AND the fallback role actually exists — never downgrade silently.
127
+ if (opts?.fallback && vpcHasRole(vpc, opts.fallback)) {
128
+ return { subnetType: subnetTypeForRole(opts.fallback) };
129
+ }
130
+ const wanted = opts?.fallback ? `'${role}' (or '${opts.fallback}')` : `'${role}'`;
131
+ throw new Error(`${scope.fullId} needs a ${wanted} subnet, but VPC '${vpc.vpcId}' has none. ` +
132
+ `Add a matching subnet tier to your VPC (e.g. a 'subnetConfiguration' entry of the ` +
133
+ `required type), or place the Building Block differently. ` +
134
+ `See packages/blocks/VPC.md for guidance.`);
135
+ },
136
+ };
137
+ setVpcContext(scope, context);
138
+ return context;
139
+ }
140
+ /**
141
+ * Finalize VPC: query all BuildingBlockScope children for their VPC requirements,
142
+ * deduplicate, and provision endpoints.
143
+ * Called after all BBs are constructed (alongside finalizeConfigRegistry).
144
+ * @internal
145
+ */
146
+ export function finalizeVpc(scope, options) {
147
+ const { network: vpc, subnets } = options;
148
+ // Whether the runtime's placement provides egress (outbound internet). This is
149
+ // the only capability the requirement check needs — a BB either needs egress
150
+ // or it doesn't. Resolved from the actual selected subnets rather than guessed
151
+ // from a subnet role, so an explicit subnet *list* (no subnetType) is handled
152
+ // correctly. `undefined` = couldn't determine (e.g. an imported VPC whose
153
+ // subnets aren't known at synth) → we skip validation with a warning rather
154
+ // than fabricate an answer.
155
+ const placementHasEgress = placementProvidesEgress(vpc, subnets);
156
+ const gatewayEndpoints = [];
157
+ const interfaceEndpoints = [];
158
+ // Pull requirements from the central registry — every BuildingBlockScope
159
+ // self-registers in its constructor, so this is a faithful census without a
160
+ // separate tree walk (mirrors how config/compute registries are consumed).
161
+ for (const { fullId, requirements: reqs } of getRegisteredVpcRequirements(scope)) {
162
+ if (reqs.gatewayEndpoints) {
163
+ gatewayEndpoints.push(...reqs.gatewayEndpoints);
164
+ }
165
+ if (reqs.interfaceEndpoints) {
166
+ interfaceEndpoints.push(...reqs.interfaceEndpoints);
167
+ }
168
+ // Validate the BB's runtime egress need against where the runtime is
169
+ // actually placed. We never move the runtime (that's the customer's
170
+ // explicit choice) — an unsatisfiable requirement is a synth error, not a
171
+ // silent relocation. This turns an otherwise-silent runtime failure (e.g.
172
+ // DSQL in an isolated Lambda: deploys clean, times out on every call) into
173
+ // an actionable build error.
174
+ if (reqs.requiresEgress) {
175
+ if (placementHasEgress === false) {
176
+ throw new Error(`${fullId} requires its runtime to reach the internet (outbound egress), but the ` +
177
+ `Blocks runtime is placed in subnets with no egress route. ` +
178
+ `Set 'vpc.subnets' to a 'private-with-egress' (or 'public') selection, or remove ` +
179
+ `the Building Block that needs it. See packages/blocks/VPC.md.`);
180
+ }
181
+ if (placementHasEgress === undefined) {
182
+ // Couldn't determine egress (e.g. imported VPC with unknown subnets).
183
+ // Don't fabricate a pass/fail — warn so a real mismatch isn't silent.
184
+ Annotations.of(scope).addWarningV2('blocks:vpc:egress-unverified', `${fullId} requires runtime egress, but Blocks couldn't determine whether the ` +
185
+ `configured subnets provide it (e.g. an imported VPC). Ensure the runtime's subnets ` +
186
+ `have an outbound internet route. See packages/blocks/VPC.md.`);
187
+ }
188
+ }
189
+ }
190
+ if (options.provisionEndpoints === false) {
191
+ return;
192
+ }
193
+ // Always add the S3 gateway endpoint. The runtime pulls config/secrets and
194
+ // migrations from S3 at cold start, so an in-VPC runtime needs it. Gateway
195
+ // endpoints are free (route-table entries, no ENI), so this is unconditional.
196
+ gatewayEndpoints.push(ec2.GatewayVpcEndpointAwsService.S3);
197
+ // Provision gateway endpoints (deduplicated). Gateway endpoints attach to
198
+ // route tables, not ENIs, so they have no security-group layer.
199
+ const provisionedGateway = new Set();
200
+ for (const service of gatewayEndpoints) {
201
+ const key = service.name;
202
+ if (provisionedGateway.has(key))
203
+ continue;
204
+ provisionedGateway.add(key);
205
+ const constructId = `VpcGw${key.replace(/[^a-zA-Z0-9]/g, '')}`;
206
+ new ec2.GatewayVpcEndpoint(scope, constructId, { vpc, service });
207
+ }
208
+ // Always add CloudWatch Logs — every in-VPC Lambda needs it for log delivery,
209
+ // regardless of which Building Blocks are present. SSM is NOT added here: it
210
+ // flows from BB requirements (AppSetting and the auth blocks, which compose
211
+ // AppSetting, declare it), so an app that uses neither doesn't pay for an
212
+ // unused interface endpoint.
213
+ interfaceEndpoints.push(ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS);
214
+ // Dedicated security group for the interface endpoints. Without an explicit
215
+ // SG, CDK creates a default that allows 443 from the entire VPC CIDR — on a
216
+ // bring-your-own VPC that exposes every endpoint to unrelated workloads. Scope
217
+ // ingress to just the Blocks Lambda SG so only our functions can reach them.
218
+ const ctx = getVpcContext(scope);
219
+ const endpointSecurityGroup = new ec2.SecurityGroup(scope, 'BlocksVpcEndpointSg', {
220
+ vpc,
221
+ description: 'Blocks interface VPC endpoints — 443 from the Blocks Lambda only',
222
+ allowAllOutbound: true,
223
+ });
224
+ if (ctx) {
225
+ endpointSecurityGroup.addIngressRule(ec2.Peer.securityGroupId(ctx.computeSecurityGroup.securityGroupId), ec2.Port.tcp(443), 'HTTPS from Blocks Lambda');
226
+ }
227
+ // Provision interface endpoints (deduplicated)
228
+ const provisionedInterface = new Set();
229
+ for (const service of interfaceEndpoints) {
230
+ const key = service.name;
231
+ if (provisionedInterface.has(key))
232
+ continue;
233
+ provisionedInterface.add(key);
234
+ const constructId = `VpcIf${key.replace(/[^a-zA-Z0-9]/g, '')}`;
235
+ new ec2.InterfaceVpcEndpoint(scope, constructId, {
236
+ vpc,
237
+ service,
238
+ privateDnsEnabled: true,
239
+ securityGroups: [endpointSecurityGroup],
240
+ // `open: false` suppresses CDK's default "allow 443 from the whole VPC
241
+ // CIDR" ingress rule. Our dedicated SG already allows 443 from just the
242
+ // Blocks Lambda SG; without this, CDK would re-widen access to the entire
243
+ // VPC — the exact broadening this dedicated SG exists to prevent.
244
+ open: false,
245
+ });
246
+ }
247
+ }
248
+ /**
249
+ * Does the runtime's subnet placement provide egress (an outbound internet
250
+ * route)? Returns `true`/`false` when determinable, or `undefined` when it
251
+ * can't be determined at synth (e.g. an imported VPC whose subnets aren't
252
+ * known) — callers should warn rather than assume.
253
+ *
254
+ * Resolved from the actual subnets, not from a subnet *role*, so an explicit
255
+ * subnet **list** (which carries no `subnetType`) is handled correctly. For a
256
+ * multi-subnet selection, egress is only reported when **every** selected
257
+ * subnet has it (a runtime may land in any of them).
258
+ */
259
+ function placementProvidesEgress(vpc, selection) {
260
+ // Default placement (no selection) is PRIVATE_WITH_EGRESS — see initializeVpc.
261
+ if (!selection) {
262
+ return vpc.privateSubnets.length > 0 ? true : undefined;
263
+ }
264
+ // Explicit type: egress iff it's the egress tier; isolated/public are known.
265
+ if (selection.subnetType !== undefined) {
266
+ switch (selection.subnetType) {
267
+ case ec2.SubnetType.PRIVATE_WITH_EGRESS:
268
+ return true;
269
+ case ec2.SubnetType.PUBLIC:
270
+ return true; // public subnets route to an internet gateway
271
+ case ec2.SubnetType.PRIVATE_ISOLATED:
272
+ return false;
273
+ default:
274
+ return undefined;
275
+ }
276
+ }
277
+ // Explicit subnet list (or filter): resolve the concrete subnets and check
278
+ // that all of them are egress-capable. If resolution yields nothing usable,
279
+ // we can't tell.
280
+ try {
281
+ const { subnets: selected } = vpc.selectSubnets(selection);
282
+ if (selected.length === 0)
283
+ return undefined;
284
+ // A subnet is egress-capable if it's one of the VPC's private (with-egress)
285
+ // or public subnets. Isolated subnets are in neither list.
286
+ const egressCapable = new Set([
287
+ ...vpc.privateSubnets.map((s) => s.subnetId),
288
+ ...vpc.publicSubnets.map((s) => s.subnetId),
289
+ ]);
290
+ // If the VPC exposes no subnet inventory (imported VPC), we can't classify.
291
+ if (egressCapable.size === 0 && vpc.isolatedSubnets.length === 0)
292
+ return undefined;
293
+ return selected.every((s) => egressCapable.has(s.subnetId));
294
+ }
295
+ catch {
296
+ return undefined;
297
+ }
298
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=vpc.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vpc.test.d.ts","sourceRoot":"","sources":["../../src/cdk/vpc.test.ts"],"names":[],"mappings":""}