@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.
- package/dist/cdk/blocks-backend.d.ts +35 -9
- package/dist/cdk/blocks-backend.d.ts.map +1 -1
- package/dist/cdk/blocks-backend.js +52 -69
- package/dist/cdk/blocks-backend.test.js +61 -69
- package/dist/cdk/blocks-defaults.d.ts +27 -1
- package/dist/cdk/blocks-defaults.d.ts.map +1 -1
- package/dist/cdk/blocks-defaults.js +5 -1
- package/dist/cdk/blocks-defaults.test.js +4 -2
- package/dist/cdk/blocks-stack.test.js +73 -37
- package/dist/cdk/compute/default-compute-factory.d.ts +33 -0
- package/dist/cdk/compute/default-compute-factory.d.ts.map +1 -0
- package/dist/cdk/compute/default-compute-factory.js +3 -0
- package/dist/cdk/index.d.ts +46 -5
- package/dist/cdk/index.d.ts.map +1 -1
- package/dist/cdk/index.js +57 -7
- package/dist/cdk/internal.d.ts +1 -0
- package/dist/cdk/internal.d.ts.map +1 -1
- package/dist/hosting-secrets.d.ts +48 -0
- package/dist/hosting-secrets.d.ts.map +1 -0
- package/dist/hosting-secrets.js +57 -0
- package/dist/hosting-secrets.test.d.ts +2 -0
- package/dist/hosting-secrets.test.d.ts.map +1 -0
- package/dist/hosting-secrets.test.js +81 -0
- package/dist/hosting.d.ts +88 -6
- package/dist/hosting.d.ts.map +1 -1
- package/dist/hosting.js +83 -24
- package/dist/hosting.test.js +158 -10
- package/dist/index.cdk.d.ts +13 -12
- package/dist/index.cdk.d.ts.map +1 -1
- package/dist/index.cdk.js +13 -9
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/lambda-handler.test.js +66 -0
- package/dist/pipeline/index.d.ts +24 -2
- package/dist/pipeline/index.d.ts.map +1 -1
- package/dist/pipeline/index.js +41 -1
- package/dist/pipeline.test.d.ts +2 -0
- package/dist/pipeline.test.d.ts.map +1 -0
- package/dist/pipeline.test.js +47 -0
- package/dist/rpc.d.ts +21 -0
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +34 -0
- package/dist/rpc.test.js +38 -1
- package/dist/scripts/config.d.ts +15 -0
- package/dist/scripts/config.d.ts.map +1 -0
- package/dist/scripts/config.js +29 -0
- package/dist/scripts/deploy.d.ts.map +1 -1
- package/dist/scripts/deploy.js +4 -0
- package/dist/scripts/dev-server.d.ts +8 -0
- package/dist/scripts/dev-server.d.ts.map +1 -1
- package/dist/scripts/dev-server.js +31 -0
- package/dist/scripts/generate-client.d.ts.map +1 -1
- package/dist/scripts/generate-client.js +9 -4
- package/dist/scripts/generate-client.test.js +19 -4
- package/dist/scripts/index.d.ts +13 -10
- package/dist/scripts/index.d.ts.map +1 -1
- package/dist/scripts/index.js +11 -8
- package/dist/scripts/preflight-credentials.d.ts +30 -0
- package/dist/scripts/preflight-credentials.d.ts.map +1 -0
- package/dist/scripts/preflight-credentials.js +108 -0
- package/dist/scripts/preflight-credentials.test.d.ts +2 -0
- package/dist/scripts/preflight-credentials.test.d.ts.map +1 -0
- package/dist/scripts/preflight-credentials.test.js +77 -0
- package/dist/scripts/sandbox-args.test.d.ts +2 -0
- package/dist/scripts/sandbox-args.test.d.ts.map +1 -0
- package/dist/scripts/sandbox-args.test.js +77 -0
- package/dist/scripts/sandbox.d.ts +52 -0
- package/dist/scripts/sandbox.d.ts.map +1 -1
- package/dist/scripts/sandbox.js +92 -15
- package/dist/scripts/secret.d.ts +15 -0
- package/dist/scripts/secret.d.ts.map +1 -0
- package/dist/scripts/secret.js +29 -0
- package/dist/scripts/secret.test.d.ts +2 -0
- package/dist/scripts/secret.test.d.ts.map +1 -0
- package/dist/scripts/secret.test.js +28 -0
- package/dist/scripts/typegen.d.ts +15 -0
- package/dist/scripts/typegen.d.ts.map +1 -0
- package/dist/scripts/typegen.js +16 -0
- package/dist/secret-naming.d.ts +58 -0
- package/dist/secret-naming.d.ts.map +1 -0
- package/dist/secret-naming.js +67 -0
- package/dist/secret-naming.test.d.ts +2 -0
- package/dist/secret-naming.test.d.ts.map +1 -0
- package/dist/secret-naming.test.js +40 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
- package/src/cdk/blocks-backend.test.ts +267 -272
- package/src/cdk/blocks-backend.ts +71 -79
- package/src/cdk/blocks-defaults.test.ts +4 -2
- package/src/cdk/blocks-defaults.ts +27 -1
- package/src/cdk/blocks-stack.test.ts +161 -118
- package/src/cdk/compute/default-compute-factory.ts +37 -0
- package/src/cdk/index.ts +84 -8
- package/src/cdk/internal.ts +1 -0
- package/src/hosting-secrets.test.ts +106 -0
- package/src/hosting-secrets.ts +106 -0
- package/src/hosting.test.ts +244 -63
- package/src/hosting.ts +180 -68
- package/src/index.cdk.ts +77 -38
- package/src/index.ts +35 -16
- package/src/lambda-handler.test.ts +74 -0
- package/src/pipeline/index.ts +64 -11
- package/src/pipeline.test.ts +55 -0
- package/src/rpc.test.ts +44 -1
- package/src/rpc.ts +41 -0
- package/src/scripts/config.ts +35 -0
- package/src/scripts/deploy.ts +5 -0
- package/src/scripts/dev-server.ts +40 -0
- package/src/scripts/generate-client.test.ts +43 -8
- package/src/scripts/generate-client.ts +70 -65
- package/src/scripts/index.ts +20 -17
- package/src/scripts/preflight-credentials.test.ts +110 -0
- package/src/scripts/preflight-credentials.ts +131 -0
- package/src/scripts/sandbox-args.test.ts +92 -0
- package/src/scripts/sandbox.ts +102 -15
- package/src/scripts/secret.test.ts +35 -0
- package/src/scripts/secret.ts +35 -0
- package/src/scripts/typegen.ts +18 -0
- package/src/secret-naming.test.ts +59 -0
- package/src/secret-naming.ts +99 -0
- package/src/version.ts +1 -1
package/src/hosting.test.ts
CHANGED
|
@@ -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 {
|
|
13
|
+
import { Match, Template } from 'aws-cdk-lib/assertions';
|
|
12
14
|
import { BLOCKS_RPC_PREFIX } from './constants.js';
|
|
13
|
-
import {
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
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
|
-
|
|
1552
|
-
|
|
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
|
});
|