@aws-blocks/core 0.2.0 → 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 (123) hide show
  1. package/dist/cdk/blocks-backend.d.ts +35 -9
  2. package/dist/cdk/blocks-backend.d.ts.map +1 -1
  3. package/dist/cdk/blocks-backend.js +52 -69
  4. package/dist/cdk/blocks-backend.test.js +61 -69
  5. package/dist/cdk/blocks-defaults.d.ts +27 -1
  6. package/dist/cdk/blocks-defaults.d.ts.map +1 -1
  7. package/dist/cdk/blocks-defaults.js +5 -1
  8. package/dist/cdk/blocks-defaults.test.js +4 -2
  9. package/dist/cdk/blocks-stack.test.js +73 -37
  10. package/dist/cdk/compute/default-compute-factory.d.ts +33 -0
  11. package/dist/cdk/compute/default-compute-factory.d.ts.map +1 -0
  12. package/dist/cdk/compute/default-compute-factory.js +3 -0
  13. package/dist/cdk/index.d.ts +46 -5
  14. package/dist/cdk/index.d.ts.map +1 -1
  15. package/dist/cdk/index.js +57 -7
  16. package/dist/cdk/internal.d.ts +1 -0
  17. package/dist/cdk/internal.d.ts.map +1 -1
  18. package/dist/hosting-secrets.d.ts +48 -0
  19. package/dist/hosting-secrets.d.ts.map +1 -0
  20. package/dist/hosting-secrets.js +57 -0
  21. package/dist/hosting-secrets.test.d.ts +2 -0
  22. package/dist/hosting-secrets.test.d.ts.map +1 -0
  23. package/dist/hosting-secrets.test.js +81 -0
  24. package/dist/hosting.d.ts +88 -6
  25. package/dist/hosting.d.ts.map +1 -1
  26. package/dist/hosting.js +83 -24
  27. package/dist/hosting.test.js +158 -10
  28. package/dist/index.cdk.d.ts +13 -12
  29. package/dist/index.cdk.d.ts.map +1 -1
  30. package/dist/index.cdk.js +13 -9
  31. package/dist/index.d.ts +8 -7
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +9 -5
  34. package/dist/lambda-handler.test.js +66 -0
  35. package/dist/pipeline/index.d.ts +24 -2
  36. package/dist/pipeline/index.d.ts.map +1 -1
  37. package/dist/pipeline/index.js +41 -1
  38. package/dist/pipeline.test.d.ts +2 -0
  39. package/dist/pipeline.test.d.ts.map +1 -0
  40. package/dist/pipeline.test.js +47 -0
  41. package/dist/rpc.d.ts +21 -0
  42. package/dist/rpc.d.ts.map +1 -1
  43. package/dist/rpc.js +34 -0
  44. package/dist/rpc.test.js +38 -1
  45. package/dist/scripts/config.d.ts +15 -0
  46. package/dist/scripts/config.d.ts.map +1 -0
  47. package/dist/scripts/config.js +29 -0
  48. package/dist/scripts/deploy.d.ts.map +1 -1
  49. package/dist/scripts/deploy.js +4 -0
  50. package/dist/scripts/dev-server.d.ts +8 -0
  51. package/dist/scripts/dev-server.d.ts.map +1 -1
  52. package/dist/scripts/dev-server.js +31 -0
  53. package/dist/scripts/generate-client.d.ts.map +1 -1
  54. package/dist/scripts/generate-client.js +9 -4
  55. package/dist/scripts/generate-client.test.js +19 -4
  56. package/dist/scripts/index.d.ts +13 -10
  57. package/dist/scripts/index.d.ts.map +1 -1
  58. package/dist/scripts/index.js +11 -8
  59. package/dist/scripts/preflight-credentials.d.ts +30 -0
  60. package/dist/scripts/preflight-credentials.d.ts.map +1 -0
  61. package/dist/scripts/preflight-credentials.js +108 -0
  62. package/dist/scripts/preflight-credentials.test.d.ts +2 -0
  63. package/dist/scripts/preflight-credentials.test.d.ts.map +1 -0
  64. package/dist/scripts/preflight-credentials.test.js +77 -0
  65. package/dist/scripts/sandbox-args.test.d.ts +2 -0
  66. package/dist/scripts/sandbox-args.test.d.ts.map +1 -0
  67. package/dist/scripts/sandbox-args.test.js +77 -0
  68. package/dist/scripts/sandbox.d.ts +52 -0
  69. package/dist/scripts/sandbox.d.ts.map +1 -1
  70. package/dist/scripts/sandbox.js +92 -15
  71. package/dist/scripts/secret.d.ts +15 -0
  72. package/dist/scripts/secret.d.ts.map +1 -0
  73. package/dist/scripts/secret.js +29 -0
  74. package/dist/scripts/secret.test.d.ts +2 -0
  75. package/dist/scripts/secret.test.d.ts.map +1 -0
  76. package/dist/scripts/secret.test.js +28 -0
  77. package/dist/scripts/typegen.d.ts +15 -0
  78. package/dist/scripts/typegen.d.ts.map +1 -0
  79. package/dist/scripts/typegen.js +16 -0
  80. package/dist/secret-naming.d.ts +58 -0
  81. package/dist/secret-naming.d.ts.map +1 -0
  82. package/dist/secret-naming.js +67 -0
  83. package/dist/secret-naming.test.d.ts +2 -0
  84. package/dist/secret-naming.test.d.ts.map +1 -0
  85. package/dist/secret-naming.test.js +40 -0
  86. package/dist/version.d.ts +1 -1
  87. package/dist/version.js +1 -1
  88. package/package.json +2 -1
  89. package/src/cdk/blocks-backend.test.ts +267 -272
  90. package/src/cdk/blocks-backend.ts +71 -79
  91. package/src/cdk/blocks-defaults.test.ts +4 -2
  92. package/src/cdk/blocks-defaults.ts +27 -1
  93. package/src/cdk/blocks-stack.test.ts +161 -118
  94. package/src/cdk/compute/default-compute-factory.ts +37 -0
  95. package/src/cdk/index.ts +84 -8
  96. package/src/cdk/internal.ts +1 -0
  97. package/src/hosting-secrets.test.ts +106 -0
  98. package/src/hosting-secrets.ts +106 -0
  99. package/src/hosting.test.ts +244 -63
  100. package/src/hosting.ts +180 -68
  101. package/src/index.cdk.ts +77 -38
  102. package/src/index.ts +35 -16
  103. package/src/lambda-handler.test.ts +74 -0
  104. package/src/pipeline/index.ts +64 -11
  105. package/src/pipeline.test.ts +55 -0
  106. package/src/rpc.test.ts +44 -1
  107. package/src/rpc.ts +41 -0
  108. package/src/scripts/config.ts +35 -0
  109. package/src/scripts/deploy.ts +5 -0
  110. package/src/scripts/dev-server.ts +40 -0
  111. package/src/scripts/generate-client.test.ts +43 -8
  112. package/src/scripts/generate-client.ts +70 -65
  113. package/src/scripts/index.ts +20 -17
  114. package/src/scripts/preflight-credentials.test.ts +110 -0
  115. package/src/scripts/preflight-credentials.ts +131 -0
  116. package/src/scripts/sandbox-args.test.ts +92 -0
  117. package/src/scripts/sandbox.ts +102 -15
  118. package/src/scripts/secret.test.ts +35 -0
  119. package/src/scripts/secret.ts +35 -0
  120. package/src/scripts/typegen.ts +18 -0
  121. package/src/secret-naming.test.ts +59 -0
  122. package/src/secret-naming.ts +99 -0
  123. package/src/version.ts +1 -1
@@ -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
  );
@@ -512,7 +548,11 @@ describe('Hosting', () => {
512
548
  const cacheBehaviors = distConfig.CacheBehaviors ?? [];
513
549
 
514
550
  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`);
551
+ assert.strictEqual(
552
+ apiPattern,
553
+ undefined,
554
+ `Should NOT have ${BLOCKS_RPC_PREFIX}/* behavior when api is omitted`,
555
+ );
516
556
  });
517
557
 
518
558
  // Regression: /api/* is the framework SSR namespace; Blocks must
@@ -536,16 +576,12 @@ describe('Hosting', () => {
536
576
  const apiExact = cacheBehaviors.find((b: any) => b.PathPattern === '/api');
537
577
  const apiWildcard = cacheBehaviors.find((b: any) => b.PathPattern === '/api/*');
538
578
 
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
- );
579
+ assert.strictEqual(apiExact, undefined, '/api must not be claimed by Blocks (framework SSR API namespace)');
580
+ assert.strictEqual(
581
+ apiWildcard,
582
+ undefined,
583
+ '/api/* must not be claimed by Blocks (framework SSR API namespace)',
584
+ );
549
585
  });
550
586
 
551
587
  it('works without api prop (static-only site)', () => {
@@ -603,10 +639,7 @@ describe('Hosting', () => {
603
639
  // Should still deploy config (with backendConfig), just without apiUrl
604
640
  const template = Template.fromStack(stack);
605
641
  const customResources = template.findResources('Custom::CDKBucketDeployment');
606
- assert.ok(
607
- Object.keys(customResources).length >= 1,
608
- 'Should have BucketDeployment even without api',
609
- );
642
+ assert.ok(Object.keys(customResources).length >= 1, 'Should have BucketDeployment even without api');
610
643
  });
611
644
 
612
645
  it('does not inject BLOCKS_API_URL into SSR Lambda when api is omitted', () => {
@@ -632,11 +665,11 @@ describe('Hosting', () => {
632
665
  for (const [, fn] of Object.entries(lambdaFns)) {
633
666
  const env = (fn as any).Properties?.Environment?.Variables;
634
667
  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
- );
668
+ assert.strictEqual(
669
+ env.BLOCKS_API_URL,
670
+ undefined,
671
+ 'BLOCKS_API_URL should not be set when api is omitted',
672
+ );
640
673
  }
641
674
  }
642
675
  });
@@ -736,7 +769,11 @@ describe('Hosting', () => {
736
769
  const cacheBehaviors = distConfig.CacheBehaviors ?? [];
737
770
  const webhookPatterns = cacheBehaviors.filter((b: any) => b.PathPattern === '/webhooks/*');
738
771
 
739
- assert.strictEqual(webhookPatterns.length, 1, 'Should have exactly one /webhooks/* behavior despite two routes');
772
+ assert.strictEqual(
773
+ webhookPatterns.length,
774
+ 1,
775
+ 'Should have exactly one /webhooks/* behavior despite two routes',
776
+ );
740
777
  });
741
778
 
742
779
  it('strips {param} segments from mixed param+wildcard routes for CloudFront patterns', () => {
@@ -764,7 +801,10 @@ describe('Hosting', () => {
764
801
  const patterns = cacheBehaviors.map((b: any) => b.PathPattern);
765
802
 
766
803
  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');
804
+ assert.ok(
805
+ !patterns.includes('/proxy/{version}/*'),
806
+ 'Should NOT have literal {version} in CloudFront pattern',
807
+ );
768
808
  });
769
809
 
770
810
  it('converts param-only routes to wildcard patterns for CloudFront', () => {
@@ -1179,11 +1219,11 @@ describe('Hosting', () => {
1179
1219
  const distributions = template.findResources('AWS::CloudFront::Distribution');
1180
1220
  const distKeys = Object.keys(distributions);
1181
1221
  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
- );
1222
+ assert.strictEqual(
1223
+ distConfig.WebACLId,
1224
+ existingAclArn,
1225
+ 'Distribution should reference the provided WebACL ARN',
1226
+ );
1187
1227
  });
1188
1228
  });
1189
1229
 
@@ -1260,9 +1300,7 @@ describe('Hosting', () => {
1260
1300
  // When errorPages is set, CloudFront CustomErrorResponses should be configured
1261
1301
  const errorResponses = distConfig.CustomErrorResponses;
1262
1302
  assert.ok(errorResponses, 'Distribution should have CustomErrorResponses');
1263
- const has404 = errorResponses.some(
1264
- (r: any) => r.ErrorCode === 404,
1265
- );
1303
+ const has404 = errorResponses.some((r: any) => r.ErrorCode === 404);
1266
1304
  assert.ok(has404, 'Should configure 404 error response');
1267
1305
  template.resourceCountIs('AWS::CloudFront::Distribution', 1);
1268
1306
  });
@@ -1302,11 +1340,10 @@ describe('Hosting', () => {
1302
1340
  // Verify no duplicate 404 error responses
1303
1341
  const errorResponses = distConfig.CustomErrorResponses;
1304
1342
  assert.ok(errorResponses, 'Distribution should have CustomErrorResponses');
1305
- const error404Responses = errorResponses.filter(
1306
- (r: any) => r.ErrorCode === 404,
1307
- );
1343
+ const error404Responses = errorResponses.filter((r: any) => r.ErrorCode === 404);
1308
1344
  assert.strictEqual(
1309
- error404Responses.length, 1,
1345
+ error404Responses.length,
1346
+ 1,
1310
1347
  `Expected exactly one 404 error response but got ${error404Responses.length}`,
1311
1348
  );
1312
1349
  });
@@ -1450,9 +1487,7 @@ describe('Hosting', () => {
1450
1487
  Resources: Record<string, { Type: string; Properties?: any }>;
1451
1488
  };
1452
1489
  const kvKeys = Object.entries(tpl.Resources).find(
1453
- ([id, r]) =>
1454
- r.Type === 'AWS::CloudFormation::CustomResource' &&
1455
- /RouteStoreKeys/.test(id),
1490
+ ([id, r]) => r.Type === 'AWS::CloudFormation::CustomResource' && /RouteStoreKeys/.test(id),
1456
1491
  );
1457
1492
  assert.ok(kvKeys, 'expected a RouteStoreKeys custom resource');
1458
1493
  const entries = JSON.parse(kvKeys![1].Properties.Entries);
@@ -1508,15 +1543,166 @@ describe('Hosting', () => {
1508
1543
  });
1509
1544
  });
1510
1545
 
1546
+ // ── value() integration ─────────────────────────────────────────
1547
+ //
1548
+ // Runtime values: the store LOCATOR (never the value) is injected as a Lambda
1549
+ // env var, and the role is granted read + kms:Decrypt. The store is derived from
1550
+ // the marker's type — secret → Secrets Manager, config → SSM Parameter Store.
1551
+ // Domain markers resolve at synth time via Hosting.create().
1552
+
1553
+ describe('secret() / config() integration', () => {
1554
+ it('secret() → Secrets Manager: injects the locator (not the value) + grants read+decrypt', () => {
1555
+ createNextjsBuildOutput(tmpDir);
1556
+
1557
+ const app = new App();
1558
+ const stack = new Stack(app, 'SecretEnvStack');
1559
+
1560
+ new Hosting(stack, 'Hosting', {
1561
+ root: tmpDir,
1562
+ customAdapter: createNextjsFixtureAdapter(tmpDir),
1563
+ api: MOCK_API,
1564
+ environment: { STRIPE_KEY: secret('STRIPE_KEY') },
1565
+ });
1566
+
1567
+ const template = Template.fromStack(stack);
1568
+
1569
+ // secret → Secrets Manager: slash-free locator under the Blocks /blocks/secrets namespace.
1570
+ template.hasResourceProperties('AWS::Lambda::Function', {
1571
+ Environment: {
1572
+ Variables: Match.objectLike({ HOSTING_SECRET_PARAM_STRIPE_KEY: 'blocks/secrets/STRIPE_KEY' }),
1573
+ },
1574
+ });
1575
+
1576
+ // The plaintext value must NOT appear anywhere in the template.
1577
+ const json = JSON.stringify(template.toJSON());
1578
+ assert.ok(!json.includes('STRIPE_KEY_VALUE'), 'no value should be in the template');
1579
+
1580
+ // The role has secretsmanager:GetSecretValue scoped to the secret + kms:Decrypt.
1581
+ template.hasResourceProperties('AWS::IAM::Policy', {
1582
+ PolicyDocument: {
1583
+ Statement: Match.arrayWith([
1584
+ Match.objectLike({ Action: 'secretsmanager:GetSecretValue' }),
1585
+ Match.objectLike({
1586
+ Action: 'kms:Decrypt',
1587
+ Condition: { StringEquals: Match.objectLike({ 'kms:ViaService': Match.anyValue() }) },
1588
+ }),
1589
+ ]),
1590
+ },
1591
+ });
1592
+ });
1593
+
1594
+ it('threads secretStore { prefix, stage, cacheTtlSeconds } from core HostingProps to the wiring (B3)', () => {
1595
+ createNextjsBuildOutput(tmpDir);
1596
+
1597
+ const app = new App();
1598
+ const stack = new Stack(app, 'SecretStoreOverrideStack');
1599
+
1600
+ new Hosting(stack, 'Hosting', {
1601
+ root: tmpDir,
1602
+ customAdapter: createNextjsFixtureAdapter(tmpDir),
1603
+ api: MOCK_API,
1604
+ environment: { STRIPE_KEY: secret('STRIPE_KEY') },
1605
+ // Custom prefix + stage + warm-cache TTL must all reach the leaf wiring.
1606
+ secretStore: { prefix: '/myapp/secrets', stage: 'prod', cacheTtlSeconds: 30 },
1607
+ });
1608
+
1609
+ const template = Template.fromStack(stack);
1610
+ template.hasResourceProperties('AWS::Lambda::Function', {
1611
+ Environment: {
1612
+ Variables: Match.objectLike({
1613
+ // custom prefix + stage (Secrets Manager → slash-free)
1614
+ HOSTING_SECRET_PARAM_STRIPE_KEY: 'myapp/secrets/prod/STRIPE_KEY',
1615
+ // shared fallback (no stage)
1616
+ HOSTING_SECRET_PARAM_STRIPE_KEY_FALLBACK: 'myapp/secrets/STRIPE_KEY',
1617
+ // warm-cache TTL for rotation without a cold start
1618
+ HOSTING_SECRET_CACHE_TTL: '30',
1619
+ }),
1620
+ },
1621
+ });
1622
+ });
1623
+
1624
+ it('config() → SSM: HOSTING_CONFIG_PARAM_* (leading-slash) + ssm:GetParameter', () => {
1625
+ createNextjsBuildOutput(tmpDir);
1626
+
1627
+ const app = new App();
1628
+ const stack = new Stack(app, 'ConfigEnvStack');
1629
+
1630
+ new Hosting(stack, 'Hosting', {
1631
+ root: tmpDir,
1632
+ customAdapter: createNextjsFixtureAdapter(tmpDir),
1633
+ api: MOCK_API,
1634
+ environment: { FEATURE_FLAGS: config('FEATURE_FLAGS') },
1635
+ });
1636
+
1637
+ const template = Template.fromStack(stack);
1638
+ template.hasResourceProperties('AWS::Lambda::Function', {
1639
+ Environment: {
1640
+ Variables: Match.objectLike({ HOSTING_CONFIG_PARAM_FEATURE_FLAGS: '/blocks/config/FEATURE_FLAGS' }),
1641
+ },
1642
+ });
1643
+ template.hasResourceProperties('AWS::IAM::Policy', {
1644
+ PolicyDocument: { Statement: Match.arrayWith([Match.objectLike({ Action: 'ssm:GetParameter' })]) },
1645
+ });
1646
+ });
1647
+
1648
+ it('does not inject a plaintext env var for a runtime value', () => {
1649
+ createNextjsBuildOutput(tmpDir);
1650
+
1651
+ const app = new App();
1652
+ const stack = new Stack(app, 'SecretNoPlaintextStack');
1653
+
1654
+ new Hosting(stack, 'Hosting', {
1655
+ root: tmpDir,
1656
+ customAdapter: createNextjsFixtureAdapter(tmpDir),
1657
+ api: MOCK_API,
1658
+ environment: { FLAG: 'on', STRIPE_KEY: secret('STRIPE_KEY') },
1659
+ });
1660
+
1661
+ const template = Template.fromStack(stack);
1662
+ const fns = template.findResources('AWS::Lambda::Function');
1663
+ for (const fn of Object.values(fns)) {
1664
+ const vars = ((fn as any).Properties?.Environment?.Variables ?? {}) as Record<string, unknown>;
1665
+ assert.strictEqual(vars.STRIPE_KEY, undefined, 'value key must not be a plaintext env var');
1666
+ }
1667
+ });
1668
+
1669
+ it('rejects a runtime value whose key mismatches the env var name', () => {
1670
+ createNextjsBuildOutput(tmpDir);
1671
+ const app = new App();
1672
+ const stack = new Stack(app, 'SecretMismatchStack');
1673
+ assert.throws(
1674
+ () =>
1675
+ new Hosting(stack, 'Hosting', {
1676
+ root: tmpDir,
1677
+ customAdapter: createNextjsFixtureAdapter(tmpDir),
1678
+ api: MOCK_API,
1679
+ environment: { STRIPE_KEY: secret('OTHER') },
1680
+ }),
1681
+ /must match the environment variable name/,
1682
+ );
1683
+ });
1684
+
1685
+ it('throws if a domain marker is used with the sync constructor', () => {
1686
+ createSpaBuildOutput(tmpDir);
1687
+ const app = new App();
1688
+ const stack = new Stack(app, 'SecretDomainSyncStack');
1689
+ assert.throws(
1690
+ () =>
1691
+ new Hosting(stack, 'Hosting', {
1692
+ root: tmpDir,
1693
+ api: MOCK_API,
1694
+ domain: { domainName: config('DOMAIN_PROD') },
1695
+ }),
1696
+ /requires async resolution/,
1697
+ );
1698
+ });
1699
+ });
1700
+
1511
1701
  describe('config.json stale-placeholder guard (#173)', () => {
1512
1702
  // Helper: pull every BucketDeployment custom resource's Properties.
1513
1703
  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
- );
1704
+ const crs = Template.fromStack(stack).findResources('Custom::CDKBucketDeployment');
1705
+ return Object.values(crs).map((cr) => (cr as { Properties: Record<string, unknown> }).Properties);
1520
1706
  };
1521
1707
 
1522
1708
  it('uploads the placeholder config.json with a no-cache directive, never the 1-year mutable cache-control', () => {
@@ -1534,22 +1720,20 @@ describe('Hosting', () => {
1534
1720
  const deployments = bucketDeployments(stack);
1535
1721
  const cc = (p: Record<string, unknown>) =>
1536
1722
  (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) ?? [];
1723
+ const includes = (p: Record<string, unknown>) => (p.Include as string[] | undefined) ?? [];
1724
+ const excludes = (p: Record<string, unknown>) => (p.Exclude as string[] | undefined) ?? [];
1541
1725
 
1542
1726
  // A deployment must upload `.blocks-sandbox/config.json` with the
1543
1727
  // no-cache directive (this is the placeholder upload).
1544
1728
  const noCacheDeploy = deployments.find(
1545
- (p) =>
1546
- includes(p).includes('.blocks-sandbox/config.json') &&
1547
- cc(p) === 'no-cache, no-store, must-revalidate',
1729
+ (p) =>
1730
+ includes(p).includes('.blocks-sandbox/config.json') &&
1731
+ cc(p) === 'no-cache, no-store, must-revalidate',
1548
1732
  );
1549
1733
  assert.ok(
1550
1734
  noCacheDeploy,
1551
- 'placeholder .blocks-sandbox/config.json must be uploaded with ' +
1552
- '"no-cache, no-store, must-revalidate"',
1735
+ 'placeholder .blocks-sandbox/config.json must be uploaded with ' +
1736
+ '"no-cache, no-store, must-revalidate"',
1553
1737
  );
1554
1738
 
1555
1739
  // No deployment may cover the placeholder with the 1-year mutable
@@ -1565,8 +1749,7 @@ describe('Hosting', () => {
1565
1749
  assert.strictEqual(
1566
1750
  leaks.length,
1567
1751
  0,
1568
- 'no mutable-tier deployment may apply s-maxage=31536000 to ' +
1569
- '.blocks-sandbox/config.json',
1752
+ 'no mutable-tier deployment may apply s-maxage=31536000 to ' + '.blocks-sandbox/config.json',
1570
1753
  );
1571
1754
  });
1572
1755
 
@@ -1607,9 +1790,7 @@ describe('Hosting', () => {
1607
1790
  Template.fromStack(stack).hasResourceProperties(
1608
1791
  'Custom::CDKBucketDeployment',
1609
1792
  Match.objectLike({
1610
- DistributionPaths: Match.arrayWith([
1611
- Match.stringLikeRegexp('^/builds/.+/\\.blocks-sandbox/\\*$'),
1612
- ]),
1793
+ DistributionPaths: Match.arrayWith([Match.stringLikeRegexp('^/builds/.+/\\.blocks-sandbox/\\*$')]),
1613
1794
  }),
1614
1795
  );
1615
1796
  });