@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
|
@@ -4,14 +4,19 @@
|
|
|
4
4
|
import * as cdk from 'aws-cdk-lib';
|
|
5
5
|
import * as s3 from 'aws-cdk-lib/aws-s3';
|
|
6
6
|
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
|
|
7
|
-
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
8
7
|
import type { Construct } from 'constructs';
|
|
8
|
+
import type { Compute } from './compute/compute.js';
|
|
9
9
|
|
|
10
10
|
const REGISTRY_KEY = Symbol.for('BLOCKS_CONFIG_REGISTRY');
|
|
11
11
|
|
|
12
|
+
/** The object key of the config JSON under {@link getConfigLocation}'s bucket. */
|
|
13
|
+
const CONFIG_KEY = 'blocks-config.json';
|
|
14
|
+
|
|
12
15
|
interface ConfigRegistryState {
|
|
13
16
|
entries: Map<string, unknown>;
|
|
14
17
|
finalized: boolean;
|
|
18
|
+
/** The shared config bucket, created once per stack by {@link getConfigLocation}. */
|
|
19
|
+
bucket?: s3.Bucket;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
/**
|
|
@@ -45,62 +50,115 @@ export function registerConfig(scope: Construct, key: string, value: unknown): v
|
|
|
45
50
|
registry.entries.set(key, value);
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Ensure the shared config bucket exists and return where the config JSON lives
|
|
55
|
+
* (`{ bucketName, key }`). The bucket is created **once per stack** (memoized on
|
|
56
|
+
* the registry) and this is idempotent — the first caller creates it, later
|
|
57
|
+
* callers get the same bucket regardless of order.
|
|
58
|
+
*
|
|
59
|
+
* Any compute that loads config at runtime (`loadConfigToProcessEnv()`) injects
|
|
60
|
+
* these two values as `BLOCKS_CONFIG_BUCKET` / `BLOCKS_CONFIG_KEY`. The Lambda
|
|
61
|
+
* handler gets them from {@link finalizeConfigRegistry}; other compute that runs
|
|
62
|
+
* as the shared execution role (e.g. the Agent BB's AgentCore Runtime) calls this
|
|
63
|
+
* at construction to inject them too, so it loads the same app config the handler
|
|
64
|
+
* does. IAM is not granted here — `finalizeConfigRegistry` grants read on the config
|
|
65
|
+
* object to the shared execution role, which such compute inherits.
|
|
66
|
+
*
|
|
67
|
+
* @param scope - Any construct in the stack; the bucket is created under the stack.
|
|
68
|
+
*/
|
|
69
|
+
export function getConfigLocation(scope: Construct): { bucketName: string; key: string } {
|
|
70
|
+
return { bucketName: ensureConfigBucket(scope).bucketName, key: CONFIG_KEY };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create-or-return the shared config bucket (memoized on the per-stack registry). Created under the
|
|
75
|
+
* owning `BlocksStack`/`BlocksBackend` (`globalThis.CURRENT_BLOCKS_STACK` — the construct finalize
|
|
76
|
+
* historically used), so its logical ID is stable regardless of which caller creates it first: a
|
|
77
|
+
* co-located BB (e.g. the AgentCore Runtime, a deep construct) may be the first to call it, and a
|
|
78
|
+
* `BlocksBackend` embedded in a customer stack must keep `Blocks/BlocksConfigBucket` (no replacement).
|
|
79
|
+
* Falls back to the stack when no owner is registered (isolated unit tests). Returns a concrete
|
|
80
|
+
* `s3.Bucket` so callers don't need a non-null assertion.
|
|
81
|
+
*/
|
|
82
|
+
function ensureConfigBucket(scope: Construct): s3.Bucket {
|
|
83
|
+
const stack = cdk.Stack.of(scope);
|
|
84
|
+
const registry = getRegistry(stack);
|
|
85
|
+
if (!registry.bucket) {
|
|
86
|
+
const owner = ((globalThis as any).CURRENT_BLOCKS_STACK as Construct | undefined) ?? stack;
|
|
87
|
+
registry.bucket = new s3.Bucket(owner, 'BlocksConfigBucket', {
|
|
88
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
89
|
+
autoDeleteObjects: true,
|
|
90
|
+
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
91
|
+
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
92
|
+
lifecycleRules: [
|
|
93
|
+
{ noncurrentVersionExpiration: cdk.Duration.days(1) },
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return registry.bucket;
|
|
98
|
+
}
|
|
99
|
+
|
|
48
100
|
/**
|
|
49
101
|
* Finalize the config registry: create an S3 bucket, upload the config JSON,
|
|
50
|
-
*
|
|
102
|
+
* grant read to the shared execution role, and stamp the config coordinates
|
|
103
|
+
* (`BLOCKS_CONFIG_BUCKET` / `BLOCKS_CONFIG_KEY`) onto every compute.
|
|
104
|
+
*
|
|
105
|
+
* Read access is granted once to the shared role (`root.executionRole`) rather
|
|
106
|
+
* than to a single function, so every compute that assumes the role can read
|
|
107
|
+
* the object. The bucket/key coordinates can't live on a role (env vars are
|
|
108
|
+
* per-compute), so they are set on each compute via `setEnv`.
|
|
51
109
|
*
|
|
52
110
|
* Must be called after all BBs are constructed (i.e., after the backendCDKPath
|
|
53
111
|
* import completes in BlocksStack.create() / BlocksBackend.create()).
|
|
54
112
|
*
|
|
55
|
-
* @param
|
|
56
|
-
*
|
|
113
|
+
* @param root - The construct to create the config resources under (also used
|
|
114
|
+
* to locate the owning stack).
|
|
115
|
+
* @param executionRole - The shared role every compute assumes; config read is
|
|
116
|
+
* granted to it once.
|
|
117
|
+
* @param computes - The computes to stamp `BLOCKS_CONFIG_BUCKET` / `BLOCKS_CONFIG_KEY` on.
|
|
57
118
|
*/
|
|
58
119
|
export function finalizeConfigRegistry(
|
|
59
|
-
|
|
60
|
-
|
|
120
|
+
root: Construct,
|
|
121
|
+
executionRole: cdk.aws_iam.IRole,
|
|
122
|
+
computes: readonly Compute[],
|
|
61
123
|
): void {
|
|
62
|
-
const stack = cdk.Stack.of(
|
|
124
|
+
const stack = cdk.Stack.of(root);
|
|
63
125
|
const registry = getRegistry(stack);
|
|
64
126
|
|
|
65
127
|
if (registry.finalized) return;
|
|
66
128
|
registry.finalized = true;
|
|
67
129
|
|
|
68
|
-
if
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
autoDeleteObjects: true,
|
|
73
|
-
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
74
|
-
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
75
|
-
lifecycleRules: [
|
|
76
|
-
{ noncurrentVersionExpiration: cdk.Duration.days(1) },
|
|
77
|
-
],
|
|
78
|
-
});
|
|
130
|
+
// Nothing to do only if no config was registered AND no bucket was created (via
|
|
131
|
+
// getConfigLocation). If a co-located BB created the bucket, still upload (even an empty {}) and
|
|
132
|
+
// wire the handler so that compute's loadConfigToProcessEnv() resolves instead of 404-ing forever.
|
|
133
|
+
if (registry.entries.size === 0 && !registry.bucket) return;
|
|
79
134
|
|
|
80
|
-
|
|
135
|
+
// Ensure the bucket exists (a co-located BB may already have created it via getConfigLocation).
|
|
136
|
+
const configBucket = ensureConfigBucket(root);
|
|
137
|
+
const configKey = CONFIG_KEY;
|
|
81
138
|
|
|
82
139
|
const configObject = cdk.Lazy.any({
|
|
83
140
|
produce: () => Object.fromEntries(registry.entries),
|
|
84
141
|
});
|
|
85
142
|
|
|
86
|
-
|
|
143
|
+
new s3deploy.BucketDeployment(root, 'BlocksConfigDeployment', {
|
|
87
144
|
sources: [s3deploy.Source.jsonData(configKey, configObject)],
|
|
88
145
|
destinationBucket: configBucket,
|
|
89
146
|
prune: false,
|
|
90
147
|
});
|
|
91
148
|
|
|
92
|
-
(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
149
|
+
// Grant read once to the shared role (scoped to the config key), so every
|
|
150
|
+
// compute assuming the role can read it. We intentionally use `grantRead`
|
|
151
|
+
// (which also adds s3:GetBucket*/s3:List* alongside s3:GetObject*) rather
|
|
152
|
+
// than a hand-rolled GetObject-only statement: this is a dedicated,
|
|
153
|
+
// block-all-public, config-only bucket, so the broader action set carries
|
|
154
|
+
// negligible exposure, and grantRead stays correct automatically if the
|
|
155
|
+
// bucket ever moves to KMS encryption (it would add kms:Decrypt).
|
|
156
|
+
configBucket.grantRead(executionRole, configKey);
|
|
157
|
+
|
|
158
|
+
// Stamp the config coordinates on every compute — env vars can't live on a
|
|
159
|
+
// role, so each compute needs them to locate the object at runtime.
|
|
160
|
+
for (const compute of computes) {
|
|
161
|
+
compute.setEnv('BLOCKS_CONFIG_BUCKET', configBucket.bucketName);
|
|
162
|
+
compute.setEnv('BLOCKS_CONFIG_KEY', configKey);
|
|
163
|
+
}
|
|
106
164
|
}
|
package/src/cdk/index.ts
CHANGED
|
@@ -16,48 +16,101 @@ import { setupBlocksInfra, BlocksBackend, assertCdkConditionActive } from './blo
|
|
|
16
16
|
import { addBlocksStackMetadata } from './stack-metadata.js';
|
|
17
17
|
import { finalizeConfigRegistry } from './config-registry.js';
|
|
18
18
|
import { type BlocksDefaults, BlocksPresets } from './blocks-defaults.js';
|
|
19
|
+
import type { Compute } from './compute/compute.js';
|
|
20
|
+
import { getComputes } from './compute/compute-registry.js';
|
|
21
|
+
import type { DefaultComputeFactory, LambdaShapedCompute } from './compute/default-compute-factory.js';
|
|
19
22
|
|
|
20
|
-
export {
|
|
23
|
+
export {
|
|
24
|
+
BlocksBackend,
|
|
25
|
+
type BlocksBackendProps,
|
|
26
|
+
type CoreBlocksBackendProps,
|
|
27
|
+
SHARED_HANDLER_TIMEOUT_SECONDS,
|
|
28
|
+
} from './blocks-backend.js';
|
|
21
29
|
export { DEFAULT_NODE_RUNTIME } from './node-version.js';
|
|
22
30
|
export { blocksNodejsBundling } from './bundling.js';
|
|
23
31
|
export { SandboxDisableDeletionProtection } from './mixins.js';
|
|
24
|
-
export { registerConfig, finalizeConfigRegistry } from './config-registry.js';
|
|
32
|
+
export { registerConfig, finalizeConfigRegistry, getConfigLocation } from './config-registry.js';
|
|
33
|
+
export { ensureApiGatewayAccount } from './apigateway-account.js';
|
|
25
34
|
export {
|
|
26
35
|
type BlocksDefaults,
|
|
36
|
+
type BlocksThrottling,
|
|
27
37
|
BlocksPresets,
|
|
28
38
|
} from './blocks-defaults.js';
|
|
29
39
|
export { synthGuard } from './synth-guard.js';
|
|
30
40
|
export type { ScopeOptions } from '../index.js';
|
|
31
41
|
export { ApiError, isBlocksError, hasAuthError, DEFAULT_API_ERROR_NAME } from '../errors.js';
|
|
32
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Core's `create()` props: the public {@link BlocksStackProps} plus the required
|
|
45
|
+
* `defaultComputeFactory`. The umbrella (`@aws-blocks/blocks`) supplies the
|
|
46
|
+
* factory (which builds a `LambdaCompute`) by spreading it onto the customer's
|
|
47
|
+
* props; customers use {@link BlocksStackProps} and never set the factory.
|
|
48
|
+
*
|
|
49
|
+
* Kept separate (rather than a `create()` argument) so the factory travels with
|
|
50
|
+
* the props object and core stays free of any concrete compute class.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export interface CoreBlocksStackProps extends BlocksStackProps {
|
|
55
|
+
/** Builds the stack's default compute. Injected by `@aws-blocks/blocks`. */
|
|
56
|
+
defaultComputeFactory: DefaultComputeFactory;
|
|
57
|
+
}
|
|
58
|
+
|
|
33
59
|
export class BlocksStack extends cdk.Stack implements BaseBlocksStack {
|
|
34
60
|
public readonly id: string;
|
|
35
|
-
public readonly apiUrl: string;
|
|
36
|
-
public readonly gateway: cdk.aws_apigateway.RestApi;
|
|
37
|
-
public readonly handler: cdk.aws_lambda_nodejs.NodejsFunction;
|
|
38
61
|
public readonly backendHandlerPath: string;
|
|
62
|
+
/**
|
|
63
|
+
* Path to the app's backend module (`props.backendCDKPath`). Exposed so Building Blocks that
|
|
64
|
+
* co-bundle the backend at synth (e.g. the Agent BB's AgentCore Runtime) can discover it via
|
|
65
|
+
* `globalThis.CURRENT_BLOCKS_STACK.backendModulePath`.
|
|
66
|
+
*/
|
|
67
|
+
public readonly backendModulePath: string;
|
|
39
68
|
/** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */
|
|
40
69
|
public readonly executionRole: cdk.aws_iam.IRole;
|
|
41
70
|
/** Infrastructure defaults for Building Blocks created under this stack. */
|
|
42
71
|
public readonly defaults: BlocksDefaults;
|
|
72
|
+
/** The default compute (owns the Lambda function + API Gateway); set in `create()`. @internal */
|
|
73
|
+
_defaultCompute?: Compute;
|
|
74
|
+
|
|
75
|
+
/** The default compute's Lambda function. To be removed once consumers move to the multi-compute model. */
|
|
76
|
+
get handler(): cdk.aws_lambda_nodejs.NodejsFunction {
|
|
77
|
+
return this.requireDefaultCompute().fn;
|
|
78
|
+
}
|
|
79
|
+
/** The default compute's API Gateway REST API. To be removed once consumers move to the multi-compute model. */
|
|
80
|
+
get gateway(): cdk.aws_apigateway.RestApi {
|
|
81
|
+
return this.requireDefaultCompute().apiGateway;
|
|
82
|
+
}
|
|
83
|
+
/** The default compute's RPC endpoint URL. To be removed once consumers move to the multi-compute model. */
|
|
84
|
+
get apiUrl(): string {
|
|
85
|
+
return this.requireDefaultCompute().apiUrl;
|
|
86
|
+
}
|
|
87
|
+
/** The default compute's handler CloudWatch log group. `bb-logger` reconfigures its retention. */
|
|
88
|
+
get handlerLogGroup(): cdk.aws_logs.ILogGroup {
|
|
89
|
+
return this.requireDefaultCompute().logGroup;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private requireDefaultCompute(): LambdaShapedCompute {
|
|
93
|
+
if (!this._defaultCompute) {
|
|
94
|
+
throw new Error('Blocks stack not fully initialized — access .handler/.gateway/.apiUrl after BlocksStack.create() resolves.');
|
|
95
|
+
}
|
|
96
|
+
return this._defaultCompute as LambdaShapedCompute;
|
|
97
|
+
}
|
|
43
98
|
|
|
44
99
|
private constructor(scope: Construct, id: string, props: BlocksStackProps) {
|
|
45
100
|
super(scope, id, props);
|
|
46
101
|
this.id = id;
|
|
47
102
|
this.backendHandlerPath = props.backendHandlerPath;
|
|
48
103
|
this.defaults = props.defaults;
|
|
104
|
+
this.backendModulePath = props.backendCDKPath;
|
|
49
105
|
|
|
50
106
|
// Set globalThis so Building Blocks attach directly to this stack
|
|
51
107
|
(globalThis as any).CURRENT_BLOCKS_STACK = this;
|
|
52
108
|
|
|
53
109
|
const infra = setupBlocksInfra(this, props, id);
|
|
54
|
-
this.handler = infra.handler;
|
|
55
|
-
this.gateway = infra.gateway;
|
|
56
|
-
this.apiUrl = infra.apiUrl;
|
|
57
110
|
this.executionRole = infra.executionRole;
|
|
58
111
|
}
|
|
59
112
|
|
|
60
|
-
static async create(scope: Construct, id: string, props:
|
|
113
|
+
static async create(scope: Construct, id: string, props: CoreBlocksStackProps) {
|
|
61
114
|
assertCdkConditionActive();
|
|
62
115
|
|
|
63
116
|
// Detect ambient pipeline stage scope set by Pipeline appFile imports
|
|
@@ -65,6 +118,13 @@ export class BlocksStack extends cdk.Stack implements BaseBlocksStack {
|
|
|
65
118
|
const actualScope = pipelineScope || scope;
|
|
66
119
|
|
|
67
120
|
const stack = new BlocksStack(actualScope, id, props);
|
|
121
|
+
// Create the default compute before importing the backend: it OWNS the
|
|
122
|
+
// Lambda function + API Gateway (which back .handler/.gateway/.apiUrl), and
|
|
123
|
+
// a block reading `this.compute` in its constructor (during that import)
|
|
124
|
+
// must resolve to it. The factory is supplied by the umbrella
|
|
125
|
+
// @aws-blocks/blocks (which injects LambdaCompute) via props, so core never
|
|
126
|
+
// imports the concrete compute class.
|
|
127
|
+
stack._defaultCompute = props.defaultComputeFactory(stack);
|
|
68
128
|
// file:// URL (not a raw path) so the cache-busting query works on Windows,
|
|
69
129
|
// where an absolute path like `D:\...` is rejected as URL scheme `d:`.
|
|
70
130
|
const backendUrl = pathToFileURL(props.backendCDKPath);
|
|
@@ -78,7 +138,7 @@ export class BlocksStack extends cdk.Stack implements BaseBlocksStack {
|
|
|
78
138
|
}
|
|
79
139
|
}
|
|
80
140
|
// Finalize BB config → S3 (after all BBs have registered their config)
|
|
81
|
-
finalizeConfigRegistry(stack, stack.
|
|
141
|
+
finalizeConfigRegistry(stack, stack.executionRole, getComputes(stack));
|
|
82
142
|
|
|
83
143
|
new cdk.CfnOutput(stack, 'ApiUrl', { value: stack.apiUrl });
|
|
84
144
|
|
|
@@ -104,6 +164,15 @@ export class Scope extends Construct {
|
|
|
104
164
|
*/
|
|
105
165
|
private readonly root: BlocksStack | BlocksBackend;
|
|
106
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Compute assigned at this node. Applies to this block and is inherited by
|
|
169
|
+
* descendants (a nearer assignment wins). Covers both a handler assigned to a
|
|
170
|
+
* specific compute and a scope-level default for its subtree. Internal until
|
|
171
|
+
* the customer-facing surface exists.
|
|
172
|
+
* @internal
|
|
173
|
+
*/
|
|
174
|
+
_compute?: Compute;
|
|
175
|
+
|
|
107
176
|
constructor(id: string, options?: ScopeOptions) {
|
|
108
177
|
const parent = options?.parent || (globalThis as any).CURRENT_BLOCKS_STACK;
|
|
109
178
|
super(parent, id);
|
|
@@ -144,6 +213,45 @@ export class Scope extends Construct {
|
|
|
144
213
|
return this.root.executionRole;
|
|
145
214
|
}
|
|
146
215
|
|
|
216
|
+
/**
|
|
217
|
+
* The compute this block runs on: the nearest `_compute` assigned on this
|
|
218
|
+
* block or an ancestor scope, else the owning stack/backend's default compute.
|
|
219
|
+
*
|
|
220
|
+
* For any app that doesn't assign a compute, this always resolves to the
|
|
221
|
+
* default — so reads are a no-op refactor. `_compute` is internal
|
|
222
|
+
* (test/framework) until the customer-facing surface exists; there is no
|
|
223
|
+
* public option to set it yet.
|
|
224
|
+
*/
|
|
225
|
+
get compute(): Compute {
|
|
226
|
+
for (let current: ScopeParent | undefined = this; current; current = (current as Scope).parent) {
|
|
227
|
+
const assigned = (current as Scope)._compute;
|
|
228
|
+
if (assigned) return assigned;
|
|
229
|
+
}
|
|
230
|
+
const defaultCompute = this.root._defaultCompute;
|
|
231
|
+
if (!defaultCompute) {
|
|
232
|
+
throw new Error('Default compute not initialized — BlocksStack/BlocksBackend.create() must run before resolving `compute`.');
|
|
233
|
+
}
|
|
234
|
+
return defaultCompute;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The stack's default compute, **ignoring** any per-scope `_compute`
|
|
239
|
+
* assignment (unlike {@link compute}, which resolves the nearest assigned
|
|
240
|
+
* one). Use when a resource is a stack-level singleton that must bind to one
|
|
241
|
+
* deterministic compute regardless of the block's resolved compute — e.g.
|
|
242
|
+
* Realtime's shared WebSocket route integration, where one WebSocket API
|
|
243
|
+
* integrates to a single target and connection bookkeeping is compute-agnostic.
|
|
244
|
+
*
|
|
245
|
+
* @internal Not a customer surface; for framework/BB singleton infra only.
|
|
246
|
+
*/
|
|
247
|
+
get defaultCompute(): Compute {
|
|
248
|
+
const defaultCompute = this.root._defaultCompute;
|
|
249
|
+
if (!defaultCompute) {
|
|
250
|
+
throw new Error('Default compute not initialized — BlocksStack/BlocksBackend.create() must run before resolving `defaultCompute`.');
|
|
251
|
+
}
|
|
252
|
+
return defaultCompute;
|
|
253
|
+
}
|
|
254
|
+
|
|
147
255
|
/**
|
|
148
256
|
* The backend entry file the owning BlocksStack/BlocksBackend runs — the
|
|
149
257
|
* single handler entry shared across the whole app.
|
|
@@ -168,6 +276,17 @@ export class Scope extends Construct {
|
|
|
168
276
|
return name;
|
|
169
277
|
}
|
|
170
278
|
|
|
279
|
+
/**
|
|
280
|
+
* The shared handler Lambda's CloudWatch log group (the default compute's).
|
|
281
|
+
* Resolves the same way as {@link handler} — via the owning
|
|
282
|
+
* BlocksStack/BlocksBackend. `bb-logger` uses this to reconfigure retention on
|
|
283
|
+
* the single, framework-owned group rather than creating a second one that
|
|
284
|
+
* would collide on the log-group name.
|
|
285
|
+
*/
|
|
286
|
+
get handlerLogGroup(): cdk.aws_logs.ILogGroup {
|
|
287
|
+
return this.root.handlerLogGroup;
|
|
288
|
+
}
|
|
289
|
+
|
|
171
290
|
get fullId(): string {
|
|
172
291
|
return computeScopeFullId(this);
|
|
173
292
|
}
|
package/src/cdk/internal.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
export { Compute } from './compute/compute.js';
|
|
27
|
+
export type { DefaultComputeFactory } from './compute/default-compute-factory.js';
|
|
27
28
|
// Reserved `/aws-blocks` path segment, needed by concrete computes (e.g.
|
|
28
29
|
// LambdaCompute in @aws-blocks/bb-lambda-compute) to build their API route tree.
|
|
29
30
|
export { BLOCKS_NAMESPACE } from '../constants.js';
|
package/src/client/index.ts
CHANGED
|
@@ -316,5 +316,5 @@ export function ApiNamespaceClient<T extends Record<string, (...args: any[]) =>
|
|
|
316
316
|
});
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
-
export { ApiError, isBlocksError, hasAuthError, DEFAULT_API_ERROR_NAME } from '../errors.js';
|
|
319
|
+
export { ApiError, blocksError, isBlocksError, hasAuthError, DEFAULT_API_ERROR_NAME } from '../errors.js';
|
|
320
320
|
export { Scope, type ScopeOptions } from '../common/index.js';
|
|
@@ -131,6 +131,27 @@ describe('config — S3 errors', () => {
|
|
|
131
131
|
assert.strictEqual(result, '', 'Should return empty string (empty config) on 404');
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
it('does not cache a not-found result — a later call re-fetches once the config exists', async () => {
|
|
135
|
+
process.env.BLOCKS_CONFIG_BUCKET = 'test-bucket';
|
|
136
|
+
process.env.BLOCKS_CONFIG_KEY = 'blocks-config.json';
|
|
137
|
+
|
|
138
|
+
// Simulate the transient post-deploy window: the object isn't written yet on the first
|
|
139
|
+
// fetch, then becomes available. A not-found result must NOT be cached, or the container
|
|
140
|
+
// would be poisoned with empty config for its lifetime.
|
|
141
|
+
let call = 0;
|
|
142
|
+
const notFound = new Error('The specified key does not exist.');
|
|
143
|
+
(notFound as any).name = 'NoSuchKey';
|
|
144
|
+
_setS3Fetcher(async () => {
|
|
145
|
+
call++;
|
|
146
|
+
if (call === 1) throw notFound;
|
|
147
|
+
return JSON.stringify({ READY_KEY: 'ready-value' });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
assert.strictEqual(await getConfig('READY_KEY'), '', 'first call sees the not-yet-written config as empty');
|
|
151
|
+
assert.strictEqual(await getConfig('READY_KEY'), 'ready-value', 'later call re-fetches once the config exists');
|
|
152
|
+
assert.strictEqual(call, 2, 'a not-found result must not be cached — S3 is retried');
|
|
153
|
+
});
|
|
154
|
+
|
|
134
155
|
it('throws with clear message on S3 network error', async () => {
|
|
135
156
|
process.env.BLOCKS_CONFIG_BUCKET = 'test-bucket';
|
|
136
157
|
process.env.BLOCKS_CONFIG_KEY = 'blocks-config.json';
|
package/src/common/config.ts
CHANGED
|
@@ -60,9 +60,13 @@ async function loadConfigFromS3(): Promise<Record<string, string>> {
|
|
|
60
60
|
|| error?.Code === 'NoSuchKey'
|
|
61
61
|
|| error?.$metadata?.httpStatusCode === 404;
|
|
62
62
|
if (isNotFound) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
// Do NOT cache the empty result: a 404 right after deploy is usually the config file
|
|
64
|
+
// not being readable yet (the transient window before the BucketDeployment settles), not
|
|
65
|
+
// a genuinely config-less app. Caching {} here would poison the container for its whole
|
|
66
|
+
// lifetime. Returning without caching lets the next invocation re-fetch and pick up the
|
|
67
|
+
// real config once it's present. (Apps that truly have no config just re-check cheaply.)
|
|
68
|
+
console.warn(`[Blocks] Config file not found in S3 (${bucket}/${key}), proceeding with empty config (will retry on next request)`);
|
|
69
|
+
return {};
|
|
66
70
|
}
|
|
67
71
|
const msg = error instanceof Error ? error.message : String(error);
|
|
68
72
|
throw new Error(`[Blocks] Failed to load config from S3 (${bucket}/${key}): ${msg}`);
|
|
@@ -152,6 +156,46 @@ export async function loadConfigToProcessEnv(): Promise<void> {
|
|
|
152
156
|
}
|
|
153
157
|
}
|
|
154
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Whether a config load has resolved and cached a result.
|
|
161
|
+
*
|
|
162
|
+
* Returns `true` once `loadConfigFromS3()` has cached a config object — either a
|
|
163
|
+
* successful S3 load, a genuinely-empty `{}` config, or the no-bucket local-dev
|
|
164
|
+
* path (both of which cache `{}`). Returns `false` in the initial state AND
|
|
165
|
+
* after a transient not-found miss: the 404 path deliberately does NOT cache its
|
|
166
|
+
* empty result (see `loadConfigFromS3`), so a `false` return immediately after
|
|
167
|
+
* awaiting a load uniquely identifies the post-deploy S3 window where
|
|
168
|
+
* blocks-config.json isn't readable yet.
|
|
169
|
+
*
|
|
170
|
+
* Callers use this to tell a transient-empty load (retry on the next request)
|
|
171
|
+
* apart from a legitimately-config-less app (do nothing) without reaching into
|
|
172
|
+
* the module's private cache.
|
|
173
|
+
*/
|
|
174
|
+
export function isConfigResolved(): boolean {
|
|
175
|
+
return configCache !== null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Sanitize a Blocks identifier into a valid config/env-var key segment:
|
|
180
|
+
* uppercase, with every non-alphanumeric character replaced by `_`.
|
|
181
|
+
*
|
|
182
|
+
* Config entries are loaded into `process.env` at runtime (see
|
|
183
|
+
* {@link loadConfigToProcessEnv}), so a key must be a valid env-var name. This
|
|
184
|
+
* is the single rule both sides of a key share: whoever *writes* a config key
|
|
185
|
+
* (`registerConfig(\`PREFIX_${sanitizeConfigKey(id)}\`, …)`) and whoever *reads*
|
|
186
|
+
* it (`getConfigSync(\`PREFIX_${sanitizeConfigKey(id)}\`)`) MUST go through this
|
|
187
|
+
* function so they reconstruct a byte-identical key — a hand-inlined variant
|
|
188
|
+
* that drifts silently misses the lookup at runtime.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```typescript
|
|
192
|
+
* registerConfig(this, `BLOCKS_QUEUE_URL_${sanitizeConfigKey(this.fullId)}`, url);
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
export function sanitizeConfigKey(id: string): string {
|
|
196
|
+
return id.toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
|
197
|
+
}
|
|
198
|
+
|
|
155
199
|
/**
|
|
156
200
|
* Reset the config cache. **For testing only.**
|
|
157
201
|
*/
|
package/src/common/index.ts
CHANGED
|
@@ -338,6 +338,14 @@ export interface BlocksStackProps extends StackProps {
|
|
|
338
338
|
|
|
339
339
|
export class BlocksStack {
|
|
340
340
|
public readonly id: string;
|
|
341
|
+
/**
|
|
342
|
+
* Path to the app's backend module (`props.backendCDKPath`). A typed contract for Building
|
|
343
|
+
* Blocks that co-bundle the backend at synth time and read it off
|
|
344
|
+
* `globalThis.CURRENT_BLOCKS_STACK` (the CDK `BlocksStack` / `BlocksBackend` set it). Declared
|
|
345
|
+
* here so those consumers can type against this shared `BlocksStack` contract — which the CDK
|
|
346
|
+
* `BlocksStack` / `BlocksBackend` implements — instead of `any`.
|
|
347
|
+
*/
|
|
348
|
+
declare readonly backendModulePath: string;
|
|
341
349
|
constructor(scope: Construct, id: string, props: BlocksStackProps) {
|
|
342
350
|
this.id = id;
|
|
343
351
|
}
|
package/src/errors.ts
CHANGED
|
@@ -78,6 +78,27 @@ export function isBlocksError<N extends string>(e: unknown, name: N): e is Error
|
|
|
78
78
|
return e instanceof Error && e.name === name;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Build a named `Error` whose `name` is a BB error constant, so it is matchable
|
|
83
|
+
* with {@link isBlocksError} on both server and client. The name is also
|
|
84
|
+
* prefixed into the message for readable logs.
|
|
85
|
+
*
|
|
86
|
+
* This is the producer half of the {@link isBlocksError} contract: throw via
|
|
87
|
+
* this helper so the `name` a consumer matches on is set consistently. It has
|
|
88
|
+
* no runtime dependencies, so it is safe to use in every bundle — mock,
|
|
89
|
+
* aws-runtime, and CDK synth.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* throw blocksError(KVStoreErrors.ConditionalCheckFailed, 'Key already exists');
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export function blocksError(name: string, message: string): Error {
|
|
97
|
+
const err = new Error(`${name}: ${message}`);
|
|
98
|
+
err.name = name;
|
|
99
|
+
return err;
|
|
100
|
+
}
|
|
101
|
+
|
|
81
102
|
/**
|
|
82
103
|
* Type guard for branching on a failed `AuthState` (the recommended
|
|
83
104
|
* `setAuthState` client path) by its structured `errorName`.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import assert from 'node:assert';
|
|
5
|
+
import { describe, it } from 'node:test';
|
|
6
|
+
import { config, secret } from '@aws-blocks/hosting';
|
|
7
|
+
import {
|
|
8
|
+
collectSynthMarkers,
|
|
9
|
+
partitionEnvironment,
|
|
10
|
+
resolveDomainNames,
|
|
11
|
+
resolveSecretsAtSynth,
|
|
12
|
+
} from './hosting-secrets.js';
|
|
13
|
+
|
|
14
|
+
void describe('partitionEnvironment()', () => {
|
|
15
|
+
void it('splits plain / managed (secret+config)', () => {
|
|
16
|
+
const { plain, managed } = partitionEnvironment({
|
|
17
|
+
FLAG: 'on',
|
|
18
|
+
STRIPE_KEY: secret('STRIPE_KEY'),
|
|
19
|
+
FEATURE_FLAGS: config('FEATURE_FLAGS'),
|
|
20
|
+
});
|
|
21
|
+
assert.deepStrictEqual(plain, { FLAG: 'on' });
|
|
22
|
+
assert.deepStrictEqual(managed.map((m) => `${m.key}:${m.kind}`).sort(), [
|
|
23
|
+
'FEATURE_FLAGS:config',
|
|
24
|
+
'STRIPE_KEY:secret',
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
void it('rejects env key / marker key mismatch', () => {
|
|
29
|
+
assert.throws(
|
|
30
|
+
() => partitionEnvironment({ STRIPE_KEY: secret('OTHER') }),
|
|
31
|
+
/must match the environment variable name/,
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
void it('handles undefined', () => {
|
|
36
|
+
const { plain, managed, byo } = partitionEnvironment(undefined);
|
|
37
|
+
assert.deepStrictEqual(plain, {});
|
|
38
|
+
assert.strictEqual(managed.length, 0);
|
|
39
|
+
assert.strictEqual(byo.length, 0);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
void describe('collectSynthMarkers()', () => {
|
|
44
|
+
void it('gathers domain markers, deduped by key', () => {
|
|
45
|
+
const markers = collectSynthMarkers(['example.com', config('DOMAIN_PROD'), config('DOMAIN_PROD')]);
|
|
46
|
+
assert.deepStrictEqual(
|
|
47
|
+
markers.map((m) => m.key),
|
|
48
|
+
['DOMAIN_PROD'],
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
void it('returns nothing when no markers', () => {
|
|
52
|
+
assert.deepStrictEqual(collectSynthMarkers('example.com'), []);
|
|
53
|
+
assert.deepStrictEqual(collectSynthMarkers(undefined), []);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
void describe('resolveSecretsAtSynth() — Blocks namespaces', () => {
|
|
58
|
+
void it('config marker resolves under /blocks/config (SSM leading-slash)', async () => {
|
|
59
|
+
const seen: string[] = [];
|
|
60
|
+
const resolved = await resolveSecretsAtSynth([config('DOMAIN_PROD')], async (name) => {
|
|
61
|
+
seen.push(name);
|
|
62
|
+
return 'prod.example.com';
|
|
63
|
+
});
|
|
64
|
+
assert.deepStrictEqual(seen, ['/blocks/config/DOMAIN_PROD']);
|
|
65
|
+
assert.strictEqual(resolved.get('DOMAIN_PROD'), 'prod.example.com');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
void it('secret marker resolves under /blocks/secrets (Secrets Manager slash-free)', async () => {
|
|
69
|
+
const seen: string[] = [];
|
|
70
|
+
await resolveSecretsAtSynth([secret('API_KEY')], async (name) => {
|
|
71
|
+
seen.push(name);
|
|
72
|
+
return 'k';
|
|
73
|
+
});
|
|
74
|
+
assert.deepStrictEqual(seen, ['blocks/secrets/API_KEY']);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
void it('throws an actionable error when a value is not set', async () => {
|
|
78
|
+
await assert.rejects(
|
|
79
|
+
resolveSecretsAtSynth([config('DOMAIN_PROD')], async () => {
|
|
80
|
+
const e = new Error('not found');
|
|
81
|
+
e.name = 'ParameterNotFound';
|
|
82
|
+
throw e;
|
|
83
|
+
}),
|
|
84
|
+
/config 'DOMAIN_PROD' is referenced/,
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
void describe('resolveDomainNames()', () => {
|
|
90
|
+
void it('replaces markers with resolved literals, preserving shape', () => {
|
|
91
|
+
const resolved = new Map([['DOMAIN_PROD', 'prod.example.com']]);
|
|
92
|
+
assert.strictEqual(resolveDomainNames(config('DOMAIN_PROD'), resolved), 'prod.example.com');
|
|
93
|
+
assert.deepStrictEqual(resolveDomainNames(['www.example.com', config('DOMAIN_PROD')], resolved), [
|
|
94
|
+
'www.example.com',
|
|
95
|
+
'prod.example.com',
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
void it('throws if a marker reached the sync path unresolved', () => {
|
|
100
|
+
assert.throws(() => resolveDomainNames(config('DOMAIN_PROD'), new Map()), /requires async resolution/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
void it('passes literal domains through untouched', () => {
|
|
104
|
+
assert.strictEqual(resolveDomainNames('example.com', new Map()), 'example.com');
|
|
105
|
+
});
|
|
106
|
+
});
|