@aws-blocks/core 0.2.0 → 0.4.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 (178) hide show
  1. package/README.md +13 -0
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +32 -0
  4. package/dist/api.test.js +22 -0
  5. package/dist/bb-utils.d.ts +1 -0
  6. package/dist/bb-utils.d.ts.map +1 -1
  7. package/dist/bb-utils.js +3 -0
  8. package/dist/cdk/apigateway-account.d.ts +33 -0
  9. package/dist/cdk/apigateway-account.d.ts.map +1 -0
  10. package/dist/cdk/apigateway-account.js +60 -0
  11. package/dist/cdk/blocks-backend.d.ts +43 -9
  12. package/dist/cdk/blocks-backend.d.ts.map +1 -1
  13. package/dist/cdk/blocks-backend.js +69 -73
  14. package/dist/cdk/blocks-backend.test.js +78 -69
  15. package/dist/cdk/blocks-defaults.d.ts +92 -2
  16. package/dist/cdk/blocks-defaults.d.ts.map +1 -1
  17. package/dist/cdk/blocks-defaults.js +22 -2
  18. package/dist/cdk/blocks-defaults.test.js +21 -2
  19. package/dist/cdk/blocks-stack.test.js +94 -37
  20. package/dist/cdk/compute/compute-registry.d.ts +19 -0
  21. package/dist/cdk/compute/compute-registry.d.ts.map +1 -0
  22. package/dist/cdk/compute/compute-registry.js +38 -0
  23. package/dist/cdk/compute/compute.d.ts +2 -0
  24. package/dist/cdk/compute/compute.d.ts.map +1 -1
  25. package/dist/cdk/compute/compute.js +8 -0
  26. package/dist/cdk/compute/default-compute-factory.d.ts +34 -0
  27. package/dist/cdk/compute/default-compute-factory.d.ts.map +1 -0
  28. package/dist/cdk/compute/default-compute-factory.js +3 -0
  29. package/dist/cdk/config-registry.d.ts +34 -4
  30. package/dist/cdk/config-registry.d.ts.map +1 -1
  31. package/dist/cdk/config-registry.js +83 -25
  32. package/dist/cdk/config-registry.test.d.ts +2 -0
  33. package/dist/cdk/config-registry.test.d.ts.map +1 -0
  34. package/dist/cdk/config-registry.test.js +115 -0
  35. package/dist/cdk/index.d.ts +76 -7
  36. package/dist/cdk/index.d.ts.map +1 -1
  37. package/dist/cdk/index.js +99 -9
  38. package/dist/cdk/internal.d.ts +1 -0
  39. package/dist/cdk/internal.d.ts.map +1 -1
  40. package/dist/client/index.d.ts +1 -1
  41. package/dist/client/index.d.ts.map +1 -1
  42. package/dist/client/index.js +1 -1
  43. package/dist/common/config.d.ts +34 -0
  44. package/dist/common/config.d.ts.map +1 -1
  45. package/dist/common/config.js +45 -3
  46. package/dist/common/config.test.js +19 -0
  47. package/dist/common/index.d.ts +8 -0
  48. package/dist/common/index.d.ts.map +1 -1
  49. package/dist/errors.d.ts +16 -0
  50. package/dist/errors.d.ts.map +1 -1
  51. package/dist/errors.js +20 -0
  52. package/dist/hosting-secrets.d.ts +48 -0
  53. package/dist/hosting-secrets.d.ts.map +1 -0
  54. package/dist/hosting-secrets.js +57 -0
  55. package/dist/hosting-secrets.test.d.ts +2 -0
  56. package/dist/hosting-secrets.test.d.ts.map +1 -0
  57. package/dist/hosting-secrets.test.js +81 -0
  58. package/dist/hosting.d.ts +97 -6
  59. package/dist/hosting.d.ts.map +1 -1
  60. package/dist/hosting.js +91 -23
  61. package/dist/hosting.test.js +182 -10
  62. package/dist/index.cdk.d.ts +13 -12
  63. package/dist/index.cdk.d.ts.map +1 -1
  64. package/dist/index.cdk.js +13 -9
  65. package/dist/index.d.ts +8 -7
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +9 -5
  68. package/dist/lambda-handler.d.ts +17 -0
  69. package/dist/lambda-handler.d.ts.map +1 -1
  70. package/dist/lambda-handler.js +52 -3
  71. package/dist/lambda-handler.test.js +196 -1
  72. package/dist/pipeline/index.d.ts +24 -2
  73. package/dist/pipeline/index.d.ts.map +1 -1
  74. package/dist/pipeline/index.js +41 -1
  75. package/dist/pipeline.test.d.ts +2 -0
  76. package/dist/pipeline.test.d.ts.map +1 -0
  77. package/dist/pipeline.test.js +47 -0
  78. package/dist/rpc.d.ts +21 -0
  79. package/dist/rpc.d.ts.map +1 -1
  80. package/dist/rpc.js +34 -0
  81. package/dist/rpc.test.js +38 -1
  82. package/dist/scripts/config.d.ts +15 -0
  83. package/dist/scripts/config.d.ts.map +1 -0
  84. package/dist/scripts/config.js +29 -0
  85. package/dist/scripts/deploy.d.ts.map +1 -1
  86. package/dist/scripts/deploy.js +4 -0
  87. package/dist/scripts/dev-server.d.ts +8 -0
  88. package/dist/scripts/dev-server.d.ts.map +1 -1
  89. package/dist/scripts/dev-server.js +31 -0
  90. package/dist/scripts/generate-client.d.ts.map +1 -1
  91. package/dist/scripts/generate-client.js +9 -4
  92. package/dist/scripts/generate-client.test.js +19 -4
  93. package/dist/scripts/index.d.ts +13 -10
  94. package/dist/scripts/index.d.ts.map +1 -1
  95. package/dist/scripts/index.js +11 -8
  96. package/dist/scripts/preflight-credentials.d.ts +30 -0
  97. package/dist/scripts/preflight-credentials.d.ts.map +1 -0
  98. package/dist/scripts/preflight-credentials.js +108 -0
  99. package/dist/scripts/preflight-credentials.test.d.ts +2 -0
  100. package/dist/scripts/preflight-credentials.test.d.ts.map +1 -0
  101. package/dist/scripts/preflight-credentials.test.js +77 -0
  102. package/dist/scripts/sandbox-args.test.d.ts +2 -0
  103. package/dist/scripts/sandbox-args.test.d.ts.map +1 -0
  104. package/dist/scripts/sandbox-args.test.js +77 -0
  105. package/dist/scripts/sandbox-empty-buckets.test.d.ts +2 -0
  106. package/dist/scripts/sandbox-empty-buckets.test.d.ts.map +1 -0
  107. package/dist/scripts/sandbox-empty-buckets.test.js +171 -0
  108. package/dist/scripts/sandbox.d.ts +106 -0
  109. package/dist/scripts/sandbox.d.ts.map +1 -1
  110. package/dist/scripts/sandbox.js +255 -40
  111. package/dist/scripts/secret.d.ts +15 -0
  112. package/dist/scripts/secret.d.ts.map +1 -0
  113. package/dist/scripts/secret.js +29 -0
  114. package/dist/scripts/secret.test.d.ts +2 -0
  115. package/dist/scripts/secret.test.d.ts.map +1 -0
  116. package/dist/scripts/secret.test.js +28 -0
  117. package/dist/scripts/typegen.d.ts +15 -0
  118. package/dist/scripts/typegen.d.ts.map +1 -0
  119. package/dist/scripts/typegen.js +16 -0
  120. package/dist/secret-naming.d.ts +58 -0
  121. package/dist/secret-naming.d.ts.map +1 -0
  122. package/dist/secret-naming.js +67 -0
  123. package/dist/secret-naming.test.d.ts +2 -0
  124. package/dist/secret-naming.test.d.ts.map +1 -0
  125. package/dist/secret-naming.test.js +40 -0
  126. package/dist/version.d.ts +1 -1
  127. package/dist/version.js +1 -1
  128. package/package.json +4 -1
  129. package/src/api.test.ts +25 -0
  130. package/src/api.ts +39 -0
  131. package/src/bb-utils.ts +3 -0
  132. package/src/cdk/apigateway-account.ts +66 -0
  133. package/src/cdk/blocks-backend.test.ts +291 -272
  134. package/src/cdk/blocks-backend.ts +88 -83
  135. package/src/cdk/blocks-defaults.test.ts +25 -2
  136. package/src/cdk/blocks-defaults.ts +94 -2
  137. package/src/cdk/blocks-stack.test.ts +186 -118
  138. package/src/cdk/compute/compute-registry.ts +45 -0
  139. package/src/cdk/compute/compute.ts +10 -0
  140. package/src/cdk/compute/default-compute-factory.ts +38 -0
  141. package/src/cdk/config-registry.test.ts +135 -0
  142. package/src/cdk/config-registry.ts +92 -34
  143. package/src/cdk/index.ts +129 -10
  144. package/src/cdk/internal.ts +1 -0
  145. package/src/client/index.ts +1 -1
  146. package/src/common/config.test.ts +21 -0
  147. package/src/common/config.ts +47 -3
  148. package/src/common/index.ts +8 -0
  149. package/src/errors.ts +21 -0
  150. package/src/hosting-secrets.test.ts +106 -0
  151. package/src/hosting-secrets.ts +106 -0
  152. package/src/hosting.test.ts +272 -63
  153. package/src/hosting.ts +200 -68
  154. package/src/index.cdk.ts +80 -38
  155. package/src/index.ts +35 -16
  156. package/src/lambda-handler.test.ts +215 -1
  157. package/src/lambda-handler.ts +54 -2
  158. package/src/pipeline/index.ts +64 -11
  159. package/src/pipeline.test.ts +55 -0
  160. package/src/rpc.test.ts +44 -1
  161. package/src/rpc.ts +41 -0
  162. package/src/scripts/config.ts +35 -0
  163. package/src/scripts/deploy.ts +5 -0
  164. package/src/scripts/dev-server.ts +40 -0
  165. package/src/scripts/generate-client.test.ts +43 -8
  166. package/src/scripts/generate-client.ts +70 -65
  167. package/src/scripts/index.ts +20 -17
  168. package/src/scripts/preflight-credentials.test.ts +110 -0
  169. package/src/scripts/preflight-credentials.ts +131 -0
  170. package/src/scripts/sandbox-args.test.ts +92 -0
  171. package/src/scripts/sandbox-empty-buckets.test.ts +191 -0
  172. package/src/scripts/sandbox.ts +287 -39
  173. package/src/scripts/secret.test.ts +35 -0
  174. package/src/scripts/secret.ts +35 -0
  175. package/src/scripts/typegen.ts +18 -0
  176. package/src/secret-naming.test.ts +59 -0
  177. package/src/secret-naming.ts +99 -0
  178. package/src/version.ts +1 -1
@@ -0,0 +1,106 @@
1
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Resolution glue between the dependency-free `secret()` / `config()` markers
6
+ * (defined in `@aws-blocks/hosting`) and the `Hosting` CDK block.
7
+ *
8
+ * Delegates to the shared `@aws-blocks/hosting` engine, pinning only the Blocks
9
+ * namespaces (`/blocks/secrets`, `/blocks/config`). The store is derived from the
10
+ * marker's kind (`secret` → Secrets Manager, `config` → SSM), and BYO
11
+ * `ISecret`/`IParameter` handles are wired identically.
12
+ *
13
+ * @module
14
+ */
15
+
16
+ import type { ManagedValue } from '@aws-blocks/hosting';
17
+ import {
18
+ type ByoBinding,
19
+ type DomainNameInput as LeafDomainNameInput,
20
+ type EnvValue as LeafEnvValue,
21
+ assertMarkersExistAtSynth as leafAssertMarkersExistAtSynth,
22
+ collectSynthMarkers as leafCollectSynthMarkers,
23
+ partitionEnvironment as leafPartitionEnvironment,
24
+ resolveDomainNames as leafResolveDomainNames,
25
+ resolveSecretsAtSynth as leafResolveSecretsAtSynth,
26
+ type SecretFetcher,
27
+ type StoreConfig,
28
+ wireByo as leafWireByo,
29
+ wireManagedValue as leafWireManagedValue,
30
+ } from '@aws-blocks/hosting/constructs';
31
+ import type * as cdk from 'aws-cdk-lib';
32
+ import { blocksStoreConfig } from './secret-naming.js';
33
+
34
+ /**
35
+ * Layer a caller's per-kind store config over the pinned Blocks defaults. Shallow
36
+ * and per-kind: the Blocks `/blocks/*` prefix stays the default, but any field the
37
+ * app sets (`prefix`, `stage`, `cacheTtlSeconds`) wins.
38
+ */
39
+ function mergeBlocksStoreConfig(override?: StoreConfig): StoreConfig {
40
+ const base = blocksStoreConfig();
41
+ if (!override) return base;
42
+ return {
43
+ secretStore: { ...base.secretStore, ...override.secretStore },
44
+ configStore: { ...base.configStore, ...override.configStore },
45
+ };
46
+ }
47
+
48
+ /** A `compute.environment` value: a literal, a managed marker, or a BYO CDK handle. */
49
+ export type EnvValue = LeafEnvValue;
50
+
51
+ /** A custom-domain name: a literal, a managed marker, or a mix in an array. */
52
+ export type DomainNameInput = LeafDomainNameInput;
53
+
54
+ /** Split an environment map into plain literals, managed markers, and BYO handles. */
55
+ export function partitionEnvironment(environment: Record<string, EnvValue> | undefined): {
56
+ plain: Record<string, string>;
57
+ managed: ManagedValue[];
58
+ byo: ByoBinding[];
59
+ } {
60
+ return leafPartitionEnvironment(environment);
61
+ }
62
+
63
+ /** Every managed marker requiring a synth-time fetch (domain markers only). */
64
+ export function collectSynthMarkers(domainName: DomainNameInput | undefined): ManagedValue[] {
65
+ return leafCollectSynthMarkers(domainName);
66
+ }
67
+
68
+ /**
69
+ * Resolve managed markers to plaintext at synth time under the Blocks namespaces.
70
+ * Throws a clear, actionable error if a referenced value was never set. An optional
71
+ * `storeConfig` overrides the pinned Blocks prefix/stage per kind.
72
+ */
73
+ export async function resolveSecretsAtSynth(
74
+ markers: ManagedValue[],
75
+ fetcher?: SecretFetcher,
76
+ storeConfig?: StoreConfig,
77
+ ): Promise<Map<string, string>> {
78
+ return leafResolveSecretsAtSynth(markers, { ...mergeBlocksStoreConfig(storeConfig), fetcher });
79
+ }
80
+
81
+ /** Resolve domain markers to literals using the synth-resolved value map. */
82
+ export function resolveDomainNames(domainName: DomainNameInput, resolved: Map<string, string>): string | string[] {
83
+ return leafResolveDomainNames(domainName, resolved);
84
+ }
85
+
86
+ /**
87
+ * Fail synth (deploy) when a referenced `environment` marker has no value set —
88
+ * existence only, never fetching the value. Uses the Blocks namespaces (with an
89
+ * optional per-kind `storeConfig` override), matching how the values are wired.
90
+ */
91
+ export async function assertMarkersExistAtSynth(markers: ManagedValue[], storeConfig?: StoreConfig): Promise<void> {
92
+ return leafAssertMarkersExistAtSynth(markers, mergeBlocksStoreConfig(storeConfig));
93
+ }
94
+
95
+ /**
96
+ * Wire a runtime managed marker under the Blocks namespace (store from kind). An
97
+ * optional `storeConfig` overrides the pinned prefix/stage/cacheTtlSeconds per kind.
98
+ */
99
+ export function wireManagedValue(fn: cdk.aws_lambda.Function, marker: ManagedValue, storeConfig?: StoreConfig): void {
100
+ leafWireManagedValue(fn, marker, mergeBlocksStoreConfig(storeConfig));
101
+ }
102
+
103
+ /** Wire a runtime BYO handle (grant + inject locator). Honors an optional `storeConfig`. */
104
+ export function wireByo(fn: cdk.aws_lambda.Function, binding: ByoBinding, storeConfig?: StoreConfig): void {
105
+ leafWireByo(fn, binding, mergeBlocksStoreConfig(storeConfig));
106
+ }
@@ -1,16 +1,18 @@
1
1
  // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
- import { describe, it, beforeEach, afterEach } from 'node:test';
5
4
  import assert from 'node:assert';
6
5
  import * as fs from 'node:fs';
7
- import * as path from 'node:path';
8
6
  import * as os from 'node:os';
7
+ import * as path from 'node:path';
8
+ import { afterEach, beforeEach, describe, it } from 'node:test';
9
+ import { config, secret } from '@aws-blocks/hosting';
10
+ import { _setSynthExistsChecker } from '@aws-blocks/hosting/constructs';
9
11
  import * as cdk from 'aws-cdk-lib';
10
12
  import { App, Duration, Stack } from 'aws-cdk-lib';
11
- import { Template, Match } from 'aws-cdk-lib/assertions';
13
+ import { Match, Template } from 'aws-cdk-lib/assertions';
12
14
  import { BLOCKS_RPC_PREFIX } from './constants.js';
13
- import { Hosting, type BlocksStackApi } from './hosting.js';
15
+ import { type BlocksStackApi, Hosting } from './hosting.js';
14
16
  import { clearRouteRegistry, registerRoute } from './raw-route.js';
15
17
 
16
18
  // ================================================================
@@ -104,6 +106,35 @@ describe('Hosting', () => {
104
106
  clearRouteRegistry();
105
107
  });
106
108
 
109
+ // ── Deploy-time existence check (key change 4) ───────────────
110
+
111
+ describe('environment marker existence check (Hosting.create)', () => {
112
+ it('rejects at deploy when a declared environment secret has no value', async () => {
113
+ createSpaBuildOutput(tmpDir);
114
+ const app = new App();
115
+ const stack = new Stack(app, 'MissingEnvSecretStack');
116
+ // Stub the synth existence probe to report "not set" — rejects before any
117
+ // CDK construction, so this needs no assets and no AWS.
118
+ _setSynthExistsChecker(async () => {
119
+ const e = new Error('missing') as Error & { name: string };
120
+ e.name = 'ResourceNotFoundException';
121
+ throw e;
122
+ });
123
+ try {
124
+ await assert.rejects(
125
+ Hosting.create(stack, 'Hosting', {
126
+ root: tmpDir,
127
+ api: MOCK_API,
128
+ environment: { MISSING_SECRET: secret('MISSING_SECRET') },
129
+ }),
130
+ /MISSING_SECRET.*not set/s,
131
+ );
132
+ } finally {
133
+ _setSynthExistsChecker(null);
134
+ }
135
+ });
136
+ });
137
+
107
138
  // ── SPA tests ────────────────────────────────────────────────
108
139
 
109
140
  describe('SPA mode', () => {
@@ -180,7 +211,9 @@ describe('Hosting', () => {
180
211
  assert.ok(crKeys.length >= 1, 'Should have at least one BucketDeployment custom resource');
181
212
 
182
213
  const hasBlocksSandboxPrefix = crKeys.some((key) => {
183
- const props = (customResources[key] as Record<string, unknown>).Properties as Record<string, unknown> | undefined;
214
+ const props = (customResources[key] as Record<string, unknown>).Properties as
215
+ | Record<string, unknown>
216
+ | undefined;
184
217
  const prefix = props?.DestinationBucketKeyPrefix as string | undefined;
185
218
  return typeof prefix === 'string' && prefix.endsWith('.blocks-sandbox');
186
219
  });
@@ -205,7 +238,10 @@ describe('Hosting', () => {
205
238
  });
206
239
  },
207
240
  (err: Error) => {
208
- assert.ok(err.message.includes('Build output directory not found'), `Expected build dir error, got: ${err.message}`);
241
+ assert.ok(
242
+ err.message.includes('Build output directory not found'),
243
+ `Expected build dir error, got: ${err.message}`,
244
+ );
209
245
  return true;
210
246
  },
211
247
  );
@@ -448,6 +484,34 @@ describe('Hosting', () => {
448
484
  }
449
485
  assert.ok(foundRetain, 'At least one bucket should have Retain deletion policy');
450
486
  });
487
+
488
+ it('forwards buildRetentionDays to the hosting bucket lifecycle rule (#480)', () => {
489
+ createSpaBuildOutput(tmpDir);
490
+
491
+ const app = new App();
492
+ const stack = new Stack(app, 'RetentionStack');
493
+
494
+ new Hosting(stack, 'Hosting', {
495
+ root: tmpDir,
496
+ api: MOCK_API,
497
+ buildRetentionDays: 90,
498
+ });
499
+
500
+ const template = Template.fromStack(stack);
501
+ // Before the fix, core dropped buildRetentionDays (only retainOnDelete was
502
+ // forwarded), so this asserted 30. It must now reach the DeleteOldBuilds
503
+ // rule as 90.
504
+ template.hasResourceProperties('AWS::S3::Bucket', {
505
+ LifecycleConfiguration: Match.objectLike({
506
+ Rules: Match.arrayWith([
507
+ Match.objectLike({
508
+ Id: 'DeleteOldBuilds',
509
+ ExpirationInDays: 90,
510
+ }),
511
+ ]),
512
+ }),
513
+ });
514
+ });
451
515
  });
452
516
 
453
517
  // ── API integration tests ────────────────────────────────────
@@ -512,7 +576,11 @@ describe('Hosting', () => {
512
576
  const cacheBehaviors = distConfig.CacheBehaviors ?? [];
513
577
 
514
578
  const apiPattern = cacheBehaviors.find((b: any) => b.PathPattern === `${BLOCKS_RPC_PREFIX}/*`);
515
- assert.strictEqual(apiPattern, undefined, `Should NOT have ${BLOCKS_RPC_PREFIX}/* behavior when api is omitted`);
579
+ assert.strictEqual(
580
+ apiPattern,
581
+ undefined,
582
+ `Should NOT have ${BLOCKS_RPC_PREFIX}/* behavior when api is omitted`,
583
+ );
516
584
  });
517
585
 
518
586
  // Regression: /api/* is the framework SSR namespace; Blocks must
@@ -536,16 +604,12 @@ describe('Hosting', () => {
536
604
  const apiExact = cacheBehaviors.find((b: any) => b.PathPattern === '/api');
537
605
  const apiWildcard = cacheBehaviors.find((b: any) => b.PathPattern === '/api/*');
538
606
 
539
- assert.strictEqual(
540
- apiExact,
541
- undefined,
542
- '/api must not be claimed by Blocks (framework SSR API namespace)',
543
- );
544
- assert.strictEqual(
545
- apiWildcard,
546
- undefined,
547
- '/api/* must not be claimed by Blocks (framework SSR API namespace)',
548
- );
607
+ assert.strictEqual(apiExact, undefined, '/api must not be claimed by Blocks (framework SSR API namespace)');
608
+ assert.strictEqual(
609
+ apiWildcard,
610
+ undefined,
611
+ '/api/* must not be claimed by Blocks (framework SSR API namespace)',
612
+ );
549
613
  });
550
614
 
551
615
  it('works without api prop (static-only site)', () => {
@@ -603,10 +667,7 @@ describe('Hosting', () => {
603
667
  // Should still deploy config (with backendConfig), just without apiUrl
604
668
  const template = Template.fromStack(stack);
605
669
  const customResources = template.findResources('Custom::CDKBucketDeployment');
606
- assert.ok(
607
- Object.keys(customResources).length >= 1,
608
- 'Should have BucketDeployment even without api',
609
- );
670
+ assert.ok(Object.keys(customResources).length >= 1, 'Should have BucketDeployment even without api');
610
671
  });
611
672
 
612
673
  it('does not inject BLOCKS_API_URL into SSR Lambda when api is omitted', () => {
@@ -632,11 +693,11 @@ describe('Hosting', () => {
632
693
  for (const [, fn] of Object.entries(lambdaFns)) {
633
694
  const env = (fn as any).Properties?.Environment?.Variables;
634
695
  if (env) {
635
- assert.strictEqual(
636
- env.BLOCKS_API_URL,
637
- undefined,
638
- 'BLOCKS_API_URL should not be set when api is omitted',
639
- );
696
+ assert.strictEqual(
697
+ env.BLOCKS_API_URL,
698
+ undefined,
699
+ 'BLOCKS_API_URL should not be set when api is omitted',
700
+ );
640
701
  }
641
702
  }
642
703
  });
@@ -736,7 +797,11 @@ describe('Hosting', () => {
736
797
  const cacheBehaviors = distConfig.CacheBehaviors ?? [];
737
798
  const webhookPatterns = cacheBehaviors.filter((b: any) => b.PathPattern === '/webhooks/*');
738
799
 
739
- assert.strictEqual(webhookPatterns.length, 1, 'Should have exactly one /webhooks/* behavior despite two routes');
800
+ assert.strictEqual(
801
+ webhookPatterns.length,
802
+ 1,
803
+ 'Should have exactly one /webhooks/* behavior despite two routes',
804
+ );
740
805
  });
741
806
 
742
807
  it('strips {param} segments from mixed param+wildcard routes for CloudFront patterns', () => {
@@ -764,7 +829,10 @@ describe('Hosting', () => {
764
829
  const patterns = cacheBehaviors.map((b: any) => b.PathPattern);
765
830
 
766
831
  assert.ok(patterns.includes('/proxy/*'), 'Should strip {version} and produce /proxy/*');
767
- assert.ok(!patterns.includes('/proxy/{version}/*'), 'Should NOT have literal {version} in CloudFront pattern');
832
+ assert.ok(
833
+ !patterns.includes('/proxy/{version}/*'),
834
+ 'Should NOT have literal {version} in CloudFront pattern',
835
+ );
768
836
  });
769
837
 
770
838
  it('converts param-only routes to wildcard patterns for CloudFront', () => {
@@ -1179,11 +1247,11 @@ describe('Hosting', () => {
1179
1247
  const distributions = template.findResources('AWS::CloudFront::Distribution');
1180
1248
  const distKeys = Object.keys(distributions);
1181
1249
  const distConfig = (distributions[distKeys[0]] as any).Properties.DistributionConfig;
1182
- assert.strictEqual(
1183
- distConfig.WebACLId,
1184
- existingAclArn,
1185
- 'Distribution should reference the provided WebACL ARN',
1186
- );
1250
+ assert.strictEqual(
1251
+ distConfig.WebACLId,
1252
+ existingAclArn,
1253
+ 'Distribution should reference the provided WebACL ARN',
1254
+ );
1187
1255
  });
1188
1256
  });
1189
1257
 
@@ -1260,9 +1328,7 @@ describe('Hosting', () => {
1260
1328
  // When errorPages is set, CloudFront CustomErrorResponses should be configured
1261
1329
  const errorResponses = distConfig.CustomErrorResponses;
1262
1330
  assert.ok(errorResponses, 'Distribution should have CustomErrorResponses');
1263
- const has404 = errorResponses.some(
1264
- (r: any) => r.ErrorCode === 404,
1265
- );
1331
+ const has404 = errorResponses.some((r: any) => r.ErrorCode === 404);
1266
1332
  assert.ok(has404, 'Should configure 404 error response');
1267
1333
  template.resourceCountIs('AWS::CloudFront::Distribution', 1);
1268
1334
  });
@@ -1302,11 +1368,10 @@ describe('Hosting', () => {
1302
1368
  // Verify no duplicate 404 error responses
1303
1369
  const errorResponses = distConfig.CustomErrorResponses;
1304
1370
  assert.ok(errorResponses, 'Distribution should have CustomErrorResponses');
1305
- const error404Responses = errorResponses.filter(
1306
- (r: any) => r.ErrorCode === 404,
1307
- );
1371
+ const error404Responses = errorResponses.filter((r: any) => r.ErrorCode === 404);
1308
1372
  assert.strictEqual(
1309
- error404Responses.length, 1,
1373
+ error404Responses.length,
1374
+ 1,
1310
1375
  `Expected exactly one 404 error response but got ${error404Responses.length}`,
1311
1376
  );
1312
1377
  });
@@ -1450,9 +1515,7 @@ describe('Hosting', () => {
1450
1515
  Resources: Record<string, { Type: string; Properties?: any }>;
1451
1516
  };
1452
1517
  const kvKeys = Object.entries(tpl.Resources).find(
1453
- ([id, r]) =>
1454
- r.Type === 'AWS::CloudFormation::CustomResource' &&
1455
- /RouteStoreKeys/.test(id),
1518
+ ([id, r]) => r.Type === 'AWS::CloudFormation::CustomResource' && /RouteStoreKeys/.test(id),
1456
1519
  );
1457
1520
  assert.ok(kvKeys, 'expected a RouteStoreKeys custom resource');
1458
1521
  const entries = JSON.parse(kvKeys![1].Properties.Entries);
@@ -1508,15 +1571,166 @@ describe('Hosting', () => {
1508
1571
  });
1509
1572
  });
1510
1573
 
1574
+ // ── value() integration ─────────────────────────────────────────
1575
+ //
1576
+ // Runtime values: the store LOCATOR (never the value) is injected as a Lambda
1577
+ // env var, and the role is granted read + kms:Decrypt. The store is derived from
1578
+ // the marker's type — secret → Secrets Manager, config → SSM Parameter Store.
1579
+ // Domain markers resolve at synth time via Hosting.create().
1580
+
1581
+ describe('secret() / config() integration', () => {
1582
+ it('secret() → Secrets Manager: injects the locator (not the value) + grants read+decrypt', () => {
1583
+ createNextjsBuildOutput(tmpDir);
1584
+
1585
+ const app = new App();
1586
+ const stack = new Stack(app, 'SecretEnvStack');
1587
+
1588
+ new Hosting(stack, 'Hosting', {
1589
+ root: tmpDir,
1590
+ customAdapter: createNextjsFixtureAdapter(tmpDir),
1591
+ api: MOCK_API,
1592
+ environment: { STRIPE_KEY: secret('STRIPE_KEY') },
1593
+ });
1594
+
1595
+ const template = Template.fromStack(stack);
1596
+
1597
+ // secret → Secrets Manager: slash-free locator under the Blocks /blocks/secrets namespace.
1598
+ template.hasResourceProperties('AWS::Lambda::Function', {
1599
+ Environment: {
1600
+ Variables: Match.objectLike({ HOSTING_SECRET_PARAM_STRIPE_KEY: 'blocks/secrets/STRIPE_KEY' }),
1601
+ },
1602
+ });
1603
+
1604
+ // The plaintext value must NOT appear anywhere in the template.
1605
+ const json = JSON.stringify(template.toJSON());
1606
+ assert.ok(!json.includes('STRIPE_KEY_VALUE'), 'no value should be in the template');
1607
+
1608
+ // The role has secretsmanager:GetSecretValue scoped to the secret + kms:Decrypt.
1609
+ template.hasResourceProperties('AWS::IAM::Policy', {
1610
+ PolicyDocument: {
1611
+ Statement: Match.arrayWith([
1612
+ Match.objectLike({ Action: 'secretsmanager:GetSecretValue' }),
1613
+ Match.objectLike({
1614
+ Action: 'kms:Decrypt',
1615
+ Condition: { StringEquals: Match.objectLike({ 'kms:ViaService': Match.anyValue() }) },
1616
+ }),
1617
+ ]),
1618
+ },
1619
+ });
1620
+ });
1621
+
1622
+ it('threads secretStore { prefix, stage, cacheTtlSeconds } from core HostingProps to the wiring (B3)', () => {
1623
+ createNextjsBuildOutput(tmpDir);
1624
+
1625
+ const app = new App();
1626
+ const stack = new Stack(app, 'SecretStoreOverrideStack');
1627
+
1628
+ new Hosting(stack, 'Hosting', {
1629
+ root: tmpDir,
1630
+ customAdapter: createNextjsFixtureAdapter(tmpDir),
1631
+ api: MOCK_API,
1632
+ environment: { STRIPE_KEY: secret('STRIPE_KEY') },
1633
+ // Custom prefix + stage + warm-cache TTL must all reach the leaf wiring.
1634
+ secretStore: { prefix: '/myapp/secrets', stage: 'prod', cacheTtlSeconds: 30 },
1635
+ });
1636
+
1637
+ const template = Template.fromStack(stack);
1638
+ template.hasResourceProperties('AWS::Lambda::Function', {
1639
+ Environment: {
1640
+ Variables: Match.objectLike({
1641
+ // custom prefix + stage (Secrets Manager → slash-free)
1642
+ HOSTING_SECRET_PARAM_STRIPE_KEY: 'myapp/secrets/prod/STRIPE_KEY',
1643
+ // shared fallback (no stage)
1644
+ HOSTING_SECRET_PARAM_STRIPE_KEY_FALLBACK: 'myapp/secrets/STRIPE_KEY',
1645
+ // warm-cache TTL for rotation without a cold start
1646
+ HOSTING_SECRET_CACHE_TTL: '30',
1647
+ }),
1648
+ },
1649
+ });
1650
+ });
1651
+
1652
+ it('config() → SSM: HOSTING_CONFIG_PARAM_* (leading-slash) + ssm:GetParameter', () => {
1653
+ createNextjsBuildOutput(tmpDir);
1654
+
1655
+ const app = new App();
1656
+ const stack = new Stack(app, 'ConfigEnvStack');
1657
+
1658
+ new Hosting(stack, 'Hosting', {
1659
+ root: tmpDir,
1660
+ customAdapter: createNextjsFixtureAdapter(tmpDir),
1661
+ api: MOCK_API,
1662
+ environment: { FEATURE_FLAGS: config('FEATURE_FLAGS') },
1663
+ });
1664
+
1665
+ const template = Template.fromStack(stack);
1666
+ template.hasResourceProperties('AWS::Lambda::Function', {
1667
+ Environment: {
1668
+ Variables: Match.objectLike({ HOSTING_CONFIG_PARAM_FEATURE_FLAGS: '/blocks/config/FEATURE_FLAGS' }),
1669
+ },
1670
+ });
1671
+ template.hasResourceProperties('AWS::IAM::Policy', {
1672
+ PolicyDocument: { Statement: Match.arrayWith([Match.objectLike({ Action: 'ssm:GetParameter' })]) },
1673
+ });
1674
+ });
1675
+
1676
+ it('does not inject a plaintext env var for a runtime value', () => {
1677
+ createNextjsBuildOutput(tmpDir);
1678
+
1679
+ const app = new App();
1680
+ const stack = new Stack(app, 'SecretNoPlaintextStack');
1681
+
1682
+ new Hosting(stack, 'Hosting', {
1683
+ root: tmpDir,
1684
+ customAdapter: createNextjsFixtureAdapter(tmpDir),
1685
+ api: MOCK_API,
1686
+ environment: { FLAG: 'on', STRIPE_KEY: secret('STRIPE_KEY') },
1687
+ });
1688
+
1689
+ const template = Template.fromStack(stack);
1690
+ const fns = template.findResources('AWS::Lambda::Function');
1691
+ for (const fn of Object.values(fns)) {
1692
+ const vars = ((fn as any).Properties?.Environment?.Variables ?? {}) as Record<string, unknown>;
1693
+ assert.strictEqual(vars.STRIPE_KEY, undefined, 'value key must not be a plaintext env var');
1694
+ }
1695
+ });
1696
+
1697
+ it('rejects a runtime value whose key mismatches the env var name', () => {
1698
+ createNextjsBuildOutput(tmpDir);
1699
+ const app = new App();
1700
+ const stack = new Stack(app, 'SecretMismatchStack');
1701
+ assert.throws(
1702
+ () =>
1703
+ new Hosting(stack, 'Hosting', {
1704
+ root: tmpDir,
1705
+ customAdapter: createNextjsFixtureAdapter(tmpDir),
1706
+ api: MOCK_API,
1707
+ environment: { STRIPE_KEY: secret('OTHER') },
1708
+ }),
1709
+ /must match the environment variable name/,
1710
+ );
1711
+ });
1712
+
1713
+ it('throws if a domain marker is used with the sync constructor', () => {
1714
+ createSpaBuildOutput(tmpDir);
1715
+ const app = new App();
1716
+ const stack = new Stack(app, 'SecretDomainSyncStack');
1717
+ assert.throws(
1718
+ () =>
1719
+ new Hosting(stack, 'Hosting', {
1720
+ root: tmpDir,
1721
+ api: MOCK_API,
1722
+ domain: { domainName: config('DOMAIN_PROD') },
1723
+ }),
1724
+ /requires async resolution/,
1725
+ );
1726
+ });
1727
+ });
1728
+
1511
1729
  describe('config.json stale-placeholder guard (#173)', () => {
1512
1730
  // Helper: pull every BucketDeployment custom resource's Properties.
1513
1731
  const bucketDeployments = (stack: Stack) => {
1514
- const crs = Template.fromStack(stack).findResources(
1515
- 'Custom::CDKBucketDeployment',
1516
- );
1517
- return Object.values(crs).map(
1518
- (cr) => (cr as { Properties: Record<string, unknown> }).Properties,
1519
- );
1732
+ const crs = Template.fromStack(stack).findResources('Custom::CDKBucketDeployment');
1733
+ return Object.values(crs).map((cr) => (cr as { Properties: Record<string, unknown> }).Properties);
1520
1734
  };
1521
1735
 
1522
1736
  it('uploads the placeholder config.json with a no-cache directive, never the 1-year mutable cache-control', () => {
@@ -1534,22 +1748,20 @@ describe('Hosting', () => {
1534
1748
  const deployments = bucketDeployments(stack);
1535
1749
  const cc = (p: Record<string, unknown>) =>
1536
1750
  (p.SystemMetadata as Record<string, string> | undefined)?.['cache-control'];
1537
- const includes = (p: Record<string, unknown>) =>
1538
- (p.Include as string[] | undefined) ?? [];
1539
- const excludes = (p: Record<string, unknown>) =>
1540
- (p.Exclude as string[] | undefined) ?? [];
1751
+ const includes = (p: Record<string, unknown>) => (p.Include as string[] | undefined) ?? [];
1752
+ const excludes = (p: Record<string, unknown>) => (p.Exclude as string[] | undefined) ?? [];
1541
1753
 
1542
1754
  // A deployment must upload `.blocks-sandbox/config.json` with the
1543
1755
  // no-cache directive (this is the placeholder upload).
1544
1756
  const noCacheDeploy = deployments.find(
1545
- (p) =>
1546
- includes(p).includes('.blocks-sandbox/config.json') &&
1547
- cc(p) === 'no-cache, no-store, must-revalidate',
1757
+ (p) =>
1758
+ includes(p).includes('.blocks-sandbox/config.json') &&
1759
+ cc(p) === 'no-cache, no-store, must-revalidate',
1548
1760
  );
1549
1761
  assert.ok(
1550
1762
  noCacheDeploy,
1551
- 'placeholder .blocks-sandbox/config.json must be uploaded with ' +
1552
- '"no-cache, no-store, must-revalidate"',
1763
+ 'placeholder .blocks-sandbox/config.json must be uploaded with ' +
1764
+ '"no-cache, no-store, must-revalidate"',
1553
1765
  );
1554
1766
 
1555
1767
  // No deployment may cover the placeholder with the 1-year mutable
@@ -1565,8 +1777,7 @@ describe('Hosting', () => {
1565
1777
  assert.strictEqual(
1566
1778
  leaks.length,
1567
1779
  0,
1568
- 'no mutable-tier deployment may apply s-maxage=31536000 to ' +
1569
- '.blocks-sandbox/config.json',
1780
+ 'no mutable-tier deployment may apply s-maxage=31536000 to ' + '.blocks-sandbox/config.json',
1570
1781
  );
1571
1782
  });
1572
1783
 
@@ -1607,9 +1818,7 @@ describe('Hosting', () => {
1607
1818
  Template.fromStack(stack).hasResourceProperties(
1608
1819
  'Custom::CDKBucketDeployment',
1609
1820
  Match.objectLike({
1610
- DistributionPaths: Match.arrayWith([
1611
- Match.stringLikeRegexp('^/builds/.+/\\.blocks-sandbox/\\*$'),
1612
- ]),
1821
+ DistributionPaths: Match.arrayWith([Match.stringLikeRegexp('^/builds/.+/\\.blocks-sandbox/\\*$')]),
1613
1822
  }),
1614
1823
  );
1615
1824
  });