@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
|
@@ -105,6 +105,39 @@ export interface DevServerOptions {
|
|
|
105
105
|
frontendCommand?: string;
|
|
106
106
|
/** Port the frontend dev server listens on. Default: 3100. */
|
|
107
107
|
frontendPort?: number;
|
|
108
|
+
/**
|
|
109
|
+
* Watch `secret()` / `config()` calls and regenerate the type-safe key
|
|
110
|
+
* augmentation (so `getSecret`/`getConfig` autocomplete and reject typos) as you
|
|
111
|
+
* edit — all under this one `npm run dev`, no second command. Auto-detected: a
|
|
112
|
+
* no-op unless the app actually declares a `secret()`/`config()`, and never fatal
|
|
113
|
+
* (a failure only logs a warning). Set `false` to disable. Default: enabled.
|
|
114
|
+
*/
|
|
115
|
+
typegen?: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Bootstrap the type-safe `getSecret`/`getConfig` key generation for the dev
|
|
120
|
+
* session. Auto-detected and non-fatal: scans the app for `secret()`/`config()`
|
|
121
|
+
* calls and, only if it finds any, generates the augmentation `.d.ts` and starts a
|
|
122
|
+
* watcher that regenerates on save — so a single `npm run dev` gives type-safe keys
|
|
123
|
+
* with no second command. Returns a `stop()` (or `undefined` when the app declares
|
|
124
|
+
* no secrets, typegen is unavailable, or it is disabled). The watcher is `unref`'d
|
|
125
|
+
* so it can never block the dev server's shutdown.
|
|
126
|
+
*/
|
|
127
|
+
async function startTypegenWatch(): Promise<(() => void) | undefined> {
|
|
128
|
+
try {
|
|
129
|
+
const { scanValueKeys, watchHostingValues } = await import('@aws-blocks/hosting/scripts');
|
|
130
|
+
const scan = await scanValueKeys();
|
|
131
|
+
if (scan.secretKeys.length === 0 && scan.configKeys.length === 0) {
|
|
132
|
+
return undefined; // app doesn't use secret()/config() → nothing to type, skip silently
|
|
133
|
+
}
|
|
134
|
+
console.log('🔑 Type-safe secret()/config() keys — watching for changes...');
|
|
135
|
+
return await watchHostingValues({ unref: true });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
// Never break the dev server over typegen (e.g. `typescript` not installed).
|
|
138
|
+
console.warn(`⚠️ hosting-typegen skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
108
141
|
}
|
|
109
142
|
|
|
110
143
|
/**
|
|
@@ -953,6 +986,12 @@ export async function startDevServer(options: DevServerOptions) {
|
|
|
953
986
|
await writeClientCode(resolvedPath, clientPath);
|
|
954
987
|
}
|
|
955
988
|
|
|
989
|
+
// Type-safe getSecret/getConfig: generate + watch the key augmentation so the
|
|
990
|
+
// getters autocomplete and reject typos, regenerating on save — all under this
|
|
991
|
+
// one `npm run dev`. Auto-detected (no-op unless the app uses secret()/config())
|
|
992
|
+
// and non-fatal.
|
|
993
|
+
const stopTypegen = options.typegen === false ? undefined : await startTypegenWatch();
|
|
994
|
+
|
|
956
995
|
// ── Startup reclaim ──────────────────────────────────────────────────────
|
|
957
996
|
// Free any port left bound by a crashed / SIGKILL'd predecessor before we bind
|
|
958
997
|
// the front door or spawn the `--strictPort` frontend. tsx-watch only gives the
|
|
@@ -1039,6 +1078,7 @@ export async function startDevServer(options: DevServerOptions) {
|
|
|
1039
1078
|
console.log('\nShutting down...');
|
|
1040
1079
|
|
|
1041
1080
|
if (respawnTimer) { clearTimeout(respawnTimer); respawnTimer = null; }
|
|
1081
|
+
stopTypegen?.(); // tear down the typegen watcher (unref'd, but close it cleanly)
|
|
1042
1082
|
// Detach our own listeners so repeated signals can't pile up handlers.
|
|
1043
1083
|
for (const sig of signals) process.removeListener(sig, cleanup);
|
|
1044
1084
|
|
|
@@ -1,12 +1,12 @@
|
|
|
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';
|
|
5
|
+
import { afterEach, beforeEach, describe, it } from 'node:test';
|
|
6
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
6
9
|
import { generateClientCode } from './generate-client.js';
|
|
7
|
-
import { writeFileSync, mkdirSync, rmSync } from 'fs';
|
|
8
|
-
import { join } from 'path';
|
|
9
|
-
import { tmpdir } from 'os';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Regression test: generateClientCode collects middleware from whatever
|
|
@@ -35,9 +35,12 @@ describe('generateClientCode — middleware collection', () => {
|
|
|
35
35
|
it('includes middleware specifiers registered by the backend', async () => {
|
|
36
36
|
// Create a minimal backend that registers a middleware specifier
|
|
37
37
|
const backendPath = join(tmpDir, 'backend.mjs');
|
|
38
|
-
writeFileSync(
|
|
38
|
+
writeFileSync(
|
|
39
|
+
backendPath,
|
|
40
|
+
`
|
|
39
41
|
globalThis.__BLOCKS_CLIENT_MIDDLEWARE__?.push('@aws-blocks/bb-realtime/aws-middleware');
|
|
40
|
-
|
|
42
|
+
`,
|
|
43
|
+
);
|
|
41
44
|
|
|
42
45
|
const code = await generateClientCode(backendPath);
|
|
43
46
|
assert.ok(
|
|
@@ -52,9 +55,12 @@ describe('generateClientCode — middleware collection', () => {
|
|
|
52
55
|
|
|
53
56
|
it('includes mock-middleware when backend registers it (local dev mode)', async () => {
|
|
54
57
|
const backendPath = join(tmpDir, 'backend.mjs');
|
|
55
|
-
writeFileSync(
|
|
58
|
+
writeFileSync(
|
|
59
|
+
backendPath,
|
|
60
|
+
`
|
|
56
61
|
globalThis.__BLOCKS_CLIENT_MIDDLEWARE__?.push('@aws-blocks/bb-realtime/mock-middleware');
|
|
57
|
-
|
|
62
|
+
`,
|
|
63
|
+
);
|
|
58
64
|
|
|
59
65
|
const code = await generateClientCode(backendPath);
|
|
60
66
|
assert.ok(
|
|
@@ -66,4 +72,33 @@ describe('generateClientCode — middleware collection', () => {
|
|
|
66
72
|
'Generated client must NOT include aws-middleware in local dev mode',
|
|
67
73
|
);
|
|
68
74
|
});
|
|
75
|
+
|
|
76
|
+
it('skips secret()/config() markers — they are values, not API namespaces', async () => {
|
|
77
|
+
// A secret()/config() marker is branded with Symbol.for('@aws-blocks/hosting.ManagedValue');
|
|
78
|
+
// build one inline so the fixture needs no imports. A real export still gets a proxy.
|
|
79
|
+
const backendPath = join(tmpDir, 'backend.mjs');
|
|
80
|
+
writeFileSync(
|
|
81
|
+
backendPath,
|
|
82
|
+
`
|
|
83
|
+
const BRAND = Symbol.for('@aws-blocks/hosting.ManagedValue');
|
|
84
|
+
export const stripeKey = { [BRAND]: true, key: 'STRIPE_KEY', kind: 'secret' };
|
|
85
|
+
export const featureFlags = { [BRAND]: true, key: 'FEATURE_FLAGS', kind: 'config' };
|
|
86
|
+
export function realApi() {}
|
|
87
|
+
`,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const code = await generateClientCode(backendPath);
|
|
91
|
+
assert.ok(
|
|
92
|
+
!code.includes("__BLOCKS_ApiNamespaceClient__('stripeKey')"),
|
|
93
|
+
'a secret() marker must not become an HTTP namespace',
|
|
94
|
+
);
|
|
95
|
+
assert.ok(
|
|
96
|
+
!code.includes("__BLOCKS_ApiNamespaceClient__('featureFlags')"),
|
|
97
|
+
'a config() marker must not become an HTTP namespace',
|
|
98
|
+
);
|
|
99
|
+
assert.ok(
|
|
100
|
+
code.includes("export const realApi = __BLOCKS_ApiNamespaceClient__('realApi')"),
|
|
101
|
+
'a real (non-marker) export still becomes a namespace',
|
|
102
|
+
);
|
|
103
|
+
});
|
|
69
104
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { isManagedValue } from '@aws-blocks/hosting';
|
|
5
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { dirname } from 'node:path';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
7
8
|
import { API_NAMESPACE_MARKER } from '../api.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -15,72 +16,76 @@ import { API_NAMESPACE_MARKER } from '../api.js';
|
|
|
15
16
|
* import statements + ApiNamespace proxy exports.
|
|
16
17
|
*/
|
|
17
18
|
export async function generateClientCode(foundationPath: string): Promise<string> {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
19
|
+
// Set up global collector if not already present (dev server may have already done this)
|
|
20
|
+
const collectorAlreadyActive = Array.isArray((globalThis as any).__BLOCKS_CLIENT_MIDDLEWARE__);
|
|
21
|
+
if (!collectorAlreadyActive) {
|
|
22
|
+
(globalThis as any).__BLOCKS_CLIENT_MIDDLEWARE__ = [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const backend = await import(pathToFileURL(foundationPath).href);
|
|
26
|
+
|
|
27
|
+
// Read collected middleware and clean up
|
|
28
|
+
const middleware: string[] = (globalThis as any).__BLOCKS_CLIENT_MIDDLEWARE__;
|
|
29
|
+
delete (globalThis as any).__BLOCKS_CLIENT_MIDDLEWARE__;
|
|
30
|
+
|
|
31
|
+
const imports = new Set<string>();
|
|
32
|
+
const lines: string[] = [];
|
|
33
|
+
const namespaceNames: string[] = [];
|
|
34
|
+
|
|
35
|
+
imports.add(`import { ApiNamespaceClient as __BLOCKS_ApiNamespaceClient__ } from '@aws-blocks/blocks/client';`);
|
|
36
|
+
|
|
37
|
+
for (const specifier of middleware) {
|
|
38
|
+
imports.add(`import '${specifier}';`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Emit ApiNamespace proxy exports
|
|
42
|
+
for (const [name, value] of Object.entries(backend)) {
|
|
43
|
+
if (name.startsWith('_')) continue;
|
|
44
|
+
|
|
45
|
+
const isApiNamespace = typeof (value as any)?.[API_NAMESPACE_MARKER] === 'string';
|
|
46
|
+
if (isApiNamespace) {
|
|
47
|
+
lines.push(`export const ${name} = __BLOCKS_ApiNamespaceClient__('${name}');`);
|
|
48
|
+
namespaceNames.push(name);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fallback: any exported function/object that isn't a Scope gets a proxy.
|
|
53
|
+
const isScope = typeof (value as any)?.id === 'string' && typeof (value as any)?.fullId === 'string';
|
|
54
|
+
if (isScope) continue;
|
|
55
|
+
|
|
56
|
+
// A secret()/config() marker is a deferred value, not an API — skip it, or
|
|
57
|
+
// `export const k = secret('K')` in index.ts would become a bogus HTTP namespace.
|
|
58
|
+
if (isManagedValue(value)) continue;
|
|
59
|
+
|
|
60
|
+
// Fallback: any exported function/object gets an ApiNamespace proxy.
|
|
61
|
+
if ((typeof value === 'function' || typeof value === 'object') && value !== null) {
|
|
62
|
+
lines.push(`export const ${name} = __BLOCKS_ApiNamespaceClient__('${name}');`);
|
|
63
|
+
namespaceNames.push(name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Emit generateClient — creates new ApiNamespaceClient instances with the given url
|
|
68
|
+
const nsEntries = namespaceNames.map((n) => ` ${n}: __BLOCKS_ApiNamespaceClient__('${n}', config),`).join('\n');
|
|
69
|
+
lines.push('');
|
|
70
|
+
lines.push(`export const generateClient = (config) => ({\n${nsEntries}\n});`);
|
|
71
|
+
|
|
72
|
+
const header = [
|
|
73
|
+
'// ============================================================',
|
|
74
|
+
'// AUTO-GENERATED FILE — DO NOT EDIT',
|
|
75
|
+
'//',
|
|
76
|
+
'// This file is generated by the Blocks dev server / build process.',
|
|
77
|
+
'// Any manual changes will be overwritten on the next build.',
|
|
78
|
+
'// ============================================================',
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
return [...header, '', ...imports, '', ...lines].join('\n');
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
/**
|
|
80
85
|
* Generate client code and write it to the aws-blocks subpackage.
|
|
81
86
|
*/
|
|
82
87
|
export async function writeClientCode(foundationPath: string, outputPath: string): Promise<void> {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
const code = await generateClientCode(foundationPath);
|
|
89
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
90
|
+
writeFileSync(outputPath, code);
|
|
86
91
|
}
|
package/src/scripts/index.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
export { startDevServer, type DevServerOptions } from './dev-server.js';
|
|
5
|
-
export { startSandbox, destroySandbox, type SandboxOptions } from './sandbox.js';
|
|
6
|
-
export { generateClientCode, writeClientCode } from './generate-client.js';
|
|
7
|
-
export { generateSpec, writeSpec } from './generate-spec.js';
|
|
8
|
-
export { validateSpec, type SpecValidationError } from './validate-spec.js';
|
|
9
|
-
export { deploy, type DeployOptions } from './deploy.js';
|
|
10
|
-
export { destroy, type DestroyOptions } from './destroy.js';
|
|
11
|
-
export { openConsole, type ConsoleOptions } from './console.js';
|
|
12
|
-
export { ensureSecrets, loadProductionEnv, loadEnvFile } from './ensure-secrets.js';
|
|
13
4
|
export {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
5
|
+
type BuildAndSendEventOptions,
|
|
6
|
+
buildAndSendEvent,
|
|
7
|
+
type CommandName,
|
|
8
|
+
type CommandState,
|
|
9
|
+
classifyError,
|
|
10
|
+
trackCommand,
|
|
20
11
|
} from '../telemetry/index.js';
|
|
21
|
-
export {
|
|
22
|
-
export {
|
|
12
|
+
export { listConfig, removeConfig, runConfigCli, setConfig } from './config.js';
|
|
13
|
+
export { type ConsoleOptions, openConsole } from './console.js';
|
|
14
|
+
export { type DeployOptions, deploy } from './deploy.js';
|
|
15
|
+
export { type DestroyOptions, destroy } from './destroy.js';
|
|
16
|
+
export { type DevServerOptions, startDevServer } from './dev-server.js';
|
|
17
|
+
export { ensureSecrets, loadEnvFile, loadProductionEnv } from './ensure-secrets.js';
|
|
18
|
+
export { generateClientCode, writeClientCode } from './generate-client.js';
|
|
19
|
+
export { generateSpec, writeSpec } from './generate-spec.js';
|
|
20
|
+
export { destroySandbox, type SandboxOptions, startSandbox } from './sandbox.js';
|
|
21
|
+
export { listSecrets, removeSecret, runSecretCli, setSecret } from './secret.js';
|
|
22
|
+
export { getSandboxId, getStackId, getStackName } from './stack-id.js';
|
|
23
|
+
export { type TelemetryOptions, telemetry } from './telemetry.js';
|
|
24
|
+
export { runTypegenCli } from './typegen.js';
|
|
25
|
+
export { type SpecValidationError, validateSpec } from './validate-spec.js';
|
|
@@ -0,0 +1,110 @@
|
|
|
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, mock } from 'node:test';
|
|
6
|
+
import { assertAwsCredentials, resolveProbeRegion } from './preflight-credentials.js';
|
|
7
|
+
|
|
8
|
+
const REGION_ENV = { AWS_REGION: 'us-east-1' } as NodeJS.ProcessEnv;
|
|
9
|
+
|
|
10
|
+
/** Build an Error with a specific `name`, as the AWS SDK throws. */
|
|
11
|
+
function namedError(name: string, message = 'raw sdk detail'): Error {
|
|
12
|
+
const err = new Error(message);
|
|
13
|
+
err.name = name;
|
|
14
|
+
return err;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('resolveProbeRegion', () => {
|
|
18
|
+
it('prefers AWS_REGION', () => {
|
|
19
|
+
assert.strictEqual(resolveProbeRegion({ AWS_REGION: 'eu-west-1', AWS_DEFAULT_REGION: 'us-east-2' }), 'eu-west-1');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('falls back to AWS_DEFAULT_REGION', () => {
|
|
23
|
+
assert.strictEqual(resolveProbeRegion({ AWS_DEFAULT_REGION: 'ap-south-1' }), 'ap-south-1');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns null when neither is set (caller then skips the probe)', () => {
|
|
27
|
+
assert.strictEqual(resolveProbeRegion({}), null);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('assertAwsCredentials', () => {
|
|
32
|
+
it('resolves silently when the credential probe succeeds', async () => {
|
|
33
|
+
let calledRegion: string | undefined;
|
|
34
|
+
await assert.doesNotReject(() =>
|
|
35
|
+
assertAwsCredentials(
|
|
36
|
+
'sandbox',
|
|
37
|
+
async (region) => {
|
|
38
|
+
calledRegion = region;
|
|
39
|
+
},
|
|
40
|
+
REGION_ENV,
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
assert.strictEqual(calledRegion, 'us-east-1');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('throws actionable guidance on a real credential error, naming the command', async () => {
|
|
47
|
+
await assert.rejects(
|
|
48
|
+
() => assertAwsCredentials('sandbox', async () => { throw namedError('CredentialsProviderError'); }, REGION_ENV),
|
|
49
|
+
(err: Error) => {
|
|
50
|
+
assert.match(err.message, /npm run sandbox/);
|
|
51
|
+
assert.match(err.message, /aws configure|AWS_PROFILE|AWS_ACCESS_KEY_ID/);
|
|
52
|
+
assert.match(err.message, /CredentialsProviderError/); // the name, for debugging
|
|
53
|
+
return true;
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('treats an expired token as a credential error', async () => {
|
|
59
|
+
await assert.rejects(
|
|
60
|
+
() => assertAwsCredentials('deploy', async () => { throw namedError('ExpiredToken'); }, REGION_ENV),
|
|
61
|
+
(err: Error) => {
|
|
62
|
+
assert.match(err.message, /npm run deploy/);
|
|
63
|
+
assert.match(err.message, /aws sso login|AWS_PROFILE/);
|
|
64
|
+
return true;
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('does NOT leak the raw error message (ARN / account id)', async () => {
|
|
70
|
+
const arnMessage = 'User: arn:aws:iam::123456789012:user/alice is not authorized to perform sts:GetCallerIdentity';
|
|
71
|
+
await assert.rejects(
|
|
72
|
+
() => assertAwsCredentials('deploy', async () => { throw namedError('InvalidClientTokenId', arnMessage); }, REGION_ENV),
|
|
73
|
+
(err: Error) => {
|
|
74
|
+
assert.doesNotMatch(err.message, /arn:aws:iam/);
|
|
75
|
+
assert.doesNotMatch(err.message, /123456789012/);
|
|
76
|
+
return true;
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('does not throw on a network/service error — warns and continues', async () => {
|
|
82
|
+
const warn = mock.method(console, 'warn', () => {});
|
|
83
|
+
try {
|
|
84
|
+
await assert.doesNotReject(() =>
|
|
85
|
+
assertAwsCredentials('sandbox', async () => { throw namedError('TimeoutError'); }, REGION_ENV),
|
|
86
|
+
);
|
|
87
|
+
assert.ok(
|
|
88
|
+
warn.mock.calls.some((c) => String(c.arguments[0]).includes('Could not verify')),
|
|
89
|
+
'should warn that credentials could not be verified',
|
|
90
|
+
);
|
|
91
|
+
} finally {
|
|
92
|
+
warn.mock.restore();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('skips the probe (with a warning) when no region is resolvable', async () => {
|
|
97
|
+
const warn = mock.method(console, 'warn', () => {});
|
|
98
|
+
let probed = false;
|
|
99
|
+
try {
|
|
100
|
+
await assertAwsCredentials('sandbox', async () => { probed = true; }, {});
|
|
101
|
+
assert.strictEqual(probed, false, 'probe must not run without a region');
|
|
102
|
+
assert.ok(
|
|
103
|
+
warn.mock.calls.some((c) => String(c.arguments[0]).includes('Skipping the AWS credential pre-check')),
|
|
104
|
+
'should warn that the pre-check was skipped',
|
|
105
|
+
);
|
|
106
|
+
} finally {
|
|
107
|
+
warn.mock.restore();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pre-deploy AWS credential check.
|
|
6
|
+
*
|
|
7
|
+
* `cdk deploy` spends ~10 seconds synthesizing the app before it makes its
|
|
8
|
+
* first AWS call, so a missing or expired credential surfaces only *after* that
|
|
9
|
+
* wasted synth — as an opaque CDK/CloudFormation error that doesn't name the
|
|
10
|
+
* real cause. Calling STS GetCallerIdentity up front turns that into an
|
|
11
|
+
* immediate, actionable message the moment a deploy command starts.
|
|
12
|
+
*
|
|
13
|
+
* The check is intentionally conservative: it fails fast **only** on a real
|
|
14
|
+
* credential problem (missing/expired/invalid). A network or service error
|
|
15
|
+
* (STS unreachable, throttled, disabled in a region) is not treated as a
|
|
16
|
+
* credential failure — it warns and lets the deploy proceed, since blocking on
|
|
17
|
+
* an unverifiable probe would reject a deploy that might have worked.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Error `name`s that mean the credentials themselves are missing, expired, or
|
|
22
|
+
* invalid — the cases where the "configure your credentials" remediation is
|
|
23
|
+
* correct. Anything else (network, throttling, an explicit `AccessDenied` — which
|
|
24
|
+
* actually proves the identity resolved) is surfaced as itself instead.
|
|
25
|
+
*/
|
|
26
|
+
const CREDENTIAL_ERROR_NAMES = new Set([
|
|
27
|
+
'CredentialsProviderError',
|
|
28
|
+
'ExpiredToken',
|
|
29
|
+
'ExpiredTokenException',
|
|
30
|
+
'InvalidClientTokenId',
|
|
31
|
+
'UnrecognizedClientException',
|
|
32
|
+
'SignatureDoesNotMatch',
|
|
33
|
+
'TokenRefreshRequired',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the region for the STS probe from the environment. Returns `null`
|
|
38
|
+
* when neither `AWS_REGION` nor `AWS_DEFAULT_REGION` is set — the caller then
|
|
39
|
+
* skips the probe rather than guessing a region, because a hardcoded guess
|
|
40
|
+
* (e.g. `us-east-1`) belongs to a different **partition** than GovCloud
|
|
41
|
+
* (`us-gov-*`) or China (`cn-*`) and would fail against a partition the user
|
|
42
|
+
* isn't even deploying to.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveProbeRegion(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
45
|
+
return env.AWS_REGION || env.AWS_DEFAULT_REGION || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Probe that resolves when valid AWS credentials are available for `region` and
|
|
50
|
+
* rejects otherwise. Injectable so the guard can be unit-tested without network
|
|
51
|
+
* or real credentials; production callers use the default {@link stsProbe}.
|
|
52
|
+
*/
|
|
53
|
+
export type CredentialProbe = (region: string) => Promise<void>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Default probe: STS GetCallerIdentity via the SDK's standard credential chain
|
|
57
|
+
* (env vars, shared config/credentials, SSO, container/instance roles).
|
|
58
|
+
*
|
|
59
|
+
* Dynamically imported so the STS client is only loaded when a deploy actually
|
|
60
|
+
* runs — never during dev-server startup or a mock unit test. Bounded with a
|
|
61
|
+
* short request timeout and a single attempt (matching `common/config.ts`) so a
|
|
62
|
+
* bad network can't make this hang *longer* than the synth it's replacing.
|
|
63
|
+
*/
|
|
64
|
+
const stsProbe: CredentialProbe = async (region) => {
|
|
65
|
+
const { STSClient, GetCallerIdentityCommand } = await import('@aws-sdk/client-sts');
|
|
66
|
+
const client = new STSClient({
|
|
67
|
+
region,
|
|
68
|
+
maxAttempts: 1,
|
|
69
|
+
requestHandler: { connectionTimeout: 2000, requestTimeout: 3000 },
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
await client.send(new GetCallerIdentityCommand({}));
|
|
73
|
+
} finally {
|
|
74
|
+
client.destroy();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Verify AWS credentials before a deploy command spends time synthesizing, and
|
|
80
|
+
* fail fast with actionable guidance when they're missing, expired, or invalid.
|
|
81
|
+
*
|
|
82
|
+
* Skips silently (with a warning) when no region can be resolved from the
|
|
83
|
+
* environment, and treats network/service errors as non-fatal — see the module
|
|
84
|
+
* doc for the rationale.
|
|
85
|
+
*
|
|
86
|
+
* @param command - The npm script name, used in the messages (e.g. `sandbox`, `deploy`).
|
|
87
|
+
* @param probe - Credential probe; defaults to STS GetCallerIdentity. Override in tests.
|
|
88
|
+
* @param env - Environment to resolve the region from. Defaults to `process.env`.
|
|
89
|
+
* @throws {Error} With actionable guidance when the probe reports a credential error.
|
|
90
|
+
*/
|
|
91
|
+
export async function assertAwsCredentials(
|
|
92
|
+
command: string,
|
|
93
|
+
probe: CredentialProbe = stsProbe,
|
|
94
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const region = resolveProbeRegion(env);
|
|
97
|
+
if (!region) {
|
|
98
|
+
console.warn(
|
|
99
|
+
`⚠️ Skipping the AWS credential pre-check for \`npm run ${command}\`: ` +
|
|
100
|
+
'no region set in AWS_REGION / AWS_DEFAULT_REGION. The deploy will surface any credential error itself.',
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await probe(region);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
// Use the error *name* only — never the raw message, which for an STS
|
|
109
|
+
// authorization failure embeds the caller ARN and account id (and this
|
|
110
|
+
// Error propagates through telemetry).
|
|
111
|
+
const name = error instanceof Error && error.name ? error.name : 'UnknownError';
|
|
112
|
+
|
|
113
|
+
if (CREDENTIAL_ERROR_NAMES.has(name)) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`AWS credentials could not be verified for \`npm run ${command}\` (${name}).\n` +
|
|
116
|
+
'Configure AWS credentials — for example:\n' +
|
|
117
|
+
' • run `aws configure` (or `aws sso login`), or\n' +
|
|
118
|
+
' • set AWS_PROFILE to a configured profile, or\n' +
|
|
119
|
+
' • export AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (and AWS_SESSION_TOKEN for temporary credentials).\n' +
|
|
120
|
+
`Then re-run \`npm run ${command}\`.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Not a credential problem (network, throttling, STS disabled in-region, …).
|
|
125
|
+
// Don't block a deploy that might still work — warn and continue.
|
|
126
|
+
console.warn(
|
|
127
|
+
`⚠️ Could not verify AWS credentials for \`npm run ${command}\` (${name}); ` +
|
|
128
|
+
'continuing — the deploy will report any real error.',
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|