@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.
- package/README.md +13 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +32 -0
- package/dist/api.test.js +22 -0
- package/dist/bb-utils.d.ts +1 -0
- package/dist/bb-utils.d.ts.map +1 -1
- package/dist/bb-utils.js +3 -0
- package/dist/cdk/apigateway-account.d.ts +33 -0
- package/dist/cdk/apigateway-account.d.ts.map +1 -0
- package/dist/cdk/apigateway-account.js +60 -0
- package/dist/cdk/blocks-backend.d.ts +43 -9
- package/dist/cdk/blocks-backend.d.ts.map +1 -1
- package/dist/cdk/blocks-backend.js +69 -73
- package/dist/cdk/blocks-backend.test.js +78 -69
- package/dist/cdk/blocks-defaults.d.ts +92 -2
- package/dist/cdk/blocks-defaults.d.ts.map +1 -1
- package/dist/cdk/blocks-defaults.js +22 -2
- package/dist/cdk/blocks-defaults.test.js +21 -2
- package/dist/cdk/blocks-stack.test.js +94 -37
- package/dist/cdk/compute/compute-registry.d.ts +19 -0
- package/dist/cdk/compute/compute-registry.d.ts.map +1 -0
- package/dist/cdk/compute/compute-registry.js +38 -0
- package/dist/cdk/compute/compute.d.ts +2 -0
- package/dist/cdk/compute/compute.d.ts.map +1 -1
- package/dist/cdk/compute/compute.js +8 -0
- package/dist/cdk/compute/default-compute-factory.d.ts +34 -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/config-registry.d.ts +34 -4
- package/dist/cdk/config-registry.d.ts.map +1 -1
- package/dist/cdk/config-registry.js +83 -25
- package/dist/cdk/config-registry.test.d.ts +2 -0
- package/dist/cdk/config-registry.test.d.ts.map +1 -0
- package/dist/cdk/config-registry.test.js +115 -0
- package/dist/cdk/index.d.ts +76 -7
- package/dist/cdk/index.d.ts.map +1 -1
- package/dist/cdk/index.js +99 -9
- package/dist/cdk/internal.d.ts +1 -0
- package/dist/cdk/internal.d.ts.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/common/config.d.ts +34 -0
- package/dist/common/config.d.ts.map +1 -1
- package/dist/common/config.js +45 -3
- package/dist/common/config.test.js +19 -0
- package/dist/common/index.d.ts +8 -0
- package/dist/common/index.d.ts.map +1 -1
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +20 -0
- 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 +97 -6
- package/dist/hosting.d.ts.map +1 -1
- package/dist/hosting.js +91 -23
- package/dist/hosting.test.js +182 -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.d.ts +17 -0
- package/dist/lambda-handler.d.ts.map +1 -1
- package/dist/lambda-handler.js +52 -3
- package/dist/lambda-handler.test.js +196 -1
- 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-empty-buckets.test.d.ts +2 -0
- package/dist/scripts/sandbox-empty-buckets.test.d.ts.map +1 -0
- package/dist/scripts/sandbox-empty-buckets.test.js +171 -0
- package/dist/scripts/sandbox.d.ts +106 -0
- package/dist/scripts/sandbox.d.ts.map +1 -1
- package/dist/scripts/sandbox.js +255 -40
- 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 +4 -1
- package/src/api.test.ts +25 -0
- package/src/api.ts +39 -0
- package/src/bb-utils.ts +3 -0
- package/src/cdk/apigateway-account.ts +66 -0
- package/src/cdk/blocks-backend.test.ts +291 -272
- package/src/cdk/blocks-backend.ts +88 -83
- package/src/cdk/blocks-defaults.test.ts +25 -2
- package/src/cdk/blocks-defaults.ts +94 -2
- package/src/cdk/blocks-stack.test.ts +186 -118
- package/src/cdk/compute/compute-registry.ts +45 -0
- package/src/cdk/compute/compute.ts +10 -0
- package/src/cdk/compute/default-compute-factory.ts +38 -0
- package/src/cdk/config-registry.test.ts +135 -0
- package/src/cdk/config-registry.ts +92 -34
- package/src/cdk/index.ts +129 -10
- package/src/cdk/internal.ts +1 -0
- package/src/client/index.ts +1 -1
- package/src/common/config.test.ts +21 -0
- package/src/common/config.ts +47 -3
- package/src/common/index.ts +8 -0
- package/src/errors.ts +21 -0
- package/src/hosting-secrets.test.ts +106 -0
- package/src/hosting-secrets.ts +106 -0
- package/src/hosting.test.ts +272 -63
- package/src/hosting.ts +200 -68
- package/src/index.cdk.ts +80 -38
- package/src/index.ts +35 -16
- package/src/lambda-handler.test.ts +215 -1
- package/src/lambda-handler.ts +54 -2
- 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-empty-buckets.test.ts +191 -0
- package/src/scripts/sandbox.ts +287 -39
- 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
|
@@ -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
|
+
}
|
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
|
);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
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
|
-
|
|
1552
|
-
|
|
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
|
});
|