@aws-blocks/core 0.1.13 → 0.1.18
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 +180 -17
- package/dist/cdk/blocks-backend.d.ts +4 -0
- package/dist/cdk/blocks-backend.d.ts.map +1 -1
- package/dist/cdk/blocks-backend.js +23 -1
- package/dist/cdk/blocks-backend.test.js +71 -1
- package/dist/cdk/blocks-stack.test.js +32 -1
- package/dist/cdk/index.d.ts +13 -0
- package/dist/cdk/index.d.ts.map +1 -1
- package/dist/cdk/index.js +24 -0
- package/dist/cors.d.ts +27 -1
- package/dist/cors.d.ts.map +1 -1
- package/dist/cors.js +55 -2
- package/dist/cors.test.js +81 -2
- package/dist/errors.test.js +26 -1
- package/dist/hosting.d.ts.map +1 -1
- package/dist/hosting.js +26 -1
- package/dist/hosting.test.js +73 -0
- package/dist/lambda-handler.d.ts.map +1 -1
- package/dist/lambda-handler.js +4 -17
- package/dist/lambda-handler.test.js +59 -2
- package/dist/rpc.test.js +77 -1
- package/dist/scripts/console.d.ts.map +1 -1
- package/dist/scripts/console.js +30 -2
- package/dist/scripts/deploy-stream.d.ts +181 -0
- package/dist/scripts/deploy-stream.d.ts.map +1 -0
- package/dist/scripts/deploy-stream.js +332 -0
- package/dist/scripts/deploy-stream.test.d.ts +2 -0
- package/dist/scripts/deploy-stream.test.d.ts.map +1 -0
- package/dist/scripts/deploy-stream.test.js +845 -0
- package/dist/scripts/deploy.d.ts.map +1 -1
- package/dist/scripts/deploy.js +16 -9
- package/dist/scripts/dev-server-cors.test.js +19 -1
- package/dist/scripts/dev-server-rpc.test.js +50 -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 +35 -8
- package/dist/scripts/sandbox.js +1 -1
- package/dist/telemetry/client.js +4 -4
- package/dist/telemetry/telemetry-send-worker.js +4 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -1
- package/src/cdk/blocks-backend.test.ts +90 -1
- package/src/cdk/blocks-backend.ts +24 -1
- package/src/cdk/blocks-stack.test.ts +41 -1
- package/src/cdk/index.ts +25 -0
- package/src/cors.test.ts +96 -2
- package/src/cors.ts +59 -2
- package/src/errors.test.ts +29 -1
- package/src/hosting.test.ts +107 -0
- package/src/hosting.ts +27 -1
- package/src/lambda-handler.test.ts +71 -2
- package/src/lambda-handler.ts +4 -20
- package/src/rpc.test.ts +96 -1
- package/src/scripts/console.ts +29 -2
- package/src/scripts/deploy-stream.test.ts +1035 -0
- package/src/scripts/deploy-stream.ts +475 -0
- package/src/scripts/deploy.ts +18 -11
- package/src/scripts/dev-server-cors.test.ts +26 -1
- package/src/scripts/dev-server-rpc.test.ts +54 -0
- package/src/scripts/dev-server.ts +38 -8
- package/src/scripts/sandbox.ts +1 -1
- package/src/telemetry/client.ts +4 -4
- package/src/telemetry/telemetry-send-worker.ts +5 -0
- package/src/version.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/scripts/deploy.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,MAAM,CAAC,OAAO,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../src/scripts/deploy.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,MAAM,CAAC,OAAO,EAAE,aAAa,iBAiGlD"}
|
package/dist/scripts/deploy.js
CHANGED
|
@@ -8,7 +8,7 @@ import { ensureSecrets, loadProductionEnv } from './ensure-secrets.js';
|
|
|
8
8
|
import { applyExternalMigrations } from './external-migrations-step.js';
|
|
9
9
|
import { trackCommand } from '../telemetry/trackCommand.js';
|
|
10
10
|
import { getCdkTelemetryEnv } from './cdk-telemetry-env.js';
|
|
11
|
-
import {
|
|
11
|
+
import { runStreaming, buildCdkDeployArgs } from './deploy-stream.js';
|
|
12
12
|
export async function deploy(options) {
|
|
13
13
|
return trackCommand('deploy', async () => {
|
|
14
14
|
console.log('🏗️ Preparing deployment...');
|
|
@@ -44,14 +44,14 @@ export async function deploy(options) {
|
|
|
44
44
|
console.log(' (This may take a few minutes on first deploy)');
|
|
45
45
|
console.log(' - Backend API (Lambda + API Gateway)');
|
|
46
46
|
console.log(' - Frontend hosting (S3 + CloudFront)');
|
|
47
|
+
console.log(' Streaming CloudFormation events below; the deploy keeps running if this');
|
|
48
|
+
console.log(' process is backgrounded (press Ctrl-C, or send SIGTERM twice, to abort).');
|
|
47
49
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
], {
|
|
54
|
-
stdio: 'inherit',
|
|
50
|
+
await runStreaming("npx", buildCdkDeployArgs({
|
|
51
|
+
projectRoot: options.projectRoot,
|
|
52
|
+
outputsFile: '.blocks-sandbox/outputs.json',
|
|
53
|
+
}), {
|
|
54
|
+
label: 'cdk deploy',
|
|
55
55
|
cwd: options.projectRoot,
|
|
56
56
|
env: {
|
|
57
57
|
...process.env,
|
|
@@ -61,7 +61,14 @@ export async function deploy(options) {
|
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
catch (error) {
|
|
64
|
-
|
|
64
|
+
// Terminal verdict on stdout: a caller that only captures stdout (the case
|
|
65
|
+
// that produced phantom failures) must still be able to tell a failed
|
|
66
|
+
// deploy from a killed process. This banner deliberately moved off stderr,
|
|
67
|
+
// so grepping stderr for this exact string no longer matches — the failure
|
|
68
|
+
// *reason* is still there. The CDK CLI keeps error-level output on stderr
|
|
69
|
+
// even under `--ci`, and the entrypoint prints the error itself with
|
|
70
|
+
// `console.error(error)`.
|
|
71
|
+
console.log('\n❌ Deployment failed.');
|
|
65
72
|
throw error;
|
|
66
73
|
}
|
|
67
74
|
const outputs = JSON.parse(readFileSync(join(options.projectRoot, '.blocks-sandbox', 'outputs.json'), 'utf-8'));
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
import { describe, it } from 'node:test';
|
|
4
4
|
import assert from 'node:assert';
|
|
5
|
-
import { resolveDevCorsOrigin, LOCALHOST_PATTERN } from './dev-server.js';
|
|
5
|
+
import { resolveDevCorsOrigin, LOCALHOST_PATTERN, buildDevCorsHeaders } from './dev-server.js';
|
|
6
|
+
import { CORS_MAX_AGE } from '../cors.js';
|
|
6
7
|
describe('resolveDevCorsOrigin — dev server CORS', () => {
|
|
7
8
|
it('reflects localhost origin back as-is', () => {
|
|
8
9
|
assert.strictEqual(resolveDevCorsOrigin('http://localhost:3000'), 'http://localhost:3000');
|
|
@@ -21,3 +22,20 @@ describe('resolveDevCorsOrigin — dev server CORS', () => {
|
|
|
21
22
|
assert.strictEqual(resolveDevCorsOrigin('http://localhost.evil.com'), 'http://localhost:3000');
|
|
22
23
|
});
|
|
23
24
|
});
|
|
25
|
+
describe('buildDevCorsHeaders — dev server header set', () => {
|
|
26
|
+
it('shares one Max-Age value with the Lambda path so the two cannot drift', () => {
|
|
27
|
+
assert.strictEqual(buildDevCorsHeaders('http://localhost:3000')['Access-Control-Max-Age'], CORS_MAX_AGE);
|
|
28
|
+
});
|
|
29
|
+
it('sets Vary: Origin because Allow-Origin is reflected per request', () => {
|
|
30
|
+
assert.strictEqual(buildDevCorsHeaders('http://localhost:3000')['Vary'], 'Origin');
|
|
31
|
+
assert.strictEqual(buildDevCorsHeaders('https://evil.com')['Vary'], 'Origin');
|
|
32
|
+
});
|
|
33
|
+
it('reflects an allowed localhost origin', () => {
|
|
34
|
+
const headers = buildDevCorsHeaders('http://127.0.0.1:5173');
|
|
35
|
+
assert.strictEqual(headers['Access-Control-Allow-Origin'], 'http://127.0.0.1:5173');
|
|
36
|
+
assert.strictEqual(headers['Access-Control-Allow-Credentials'], 'true');
|
|
37
|
+
});
|
|
38
|
+
it('does not reflect a non-localhost origin', () => {
|
|
39
|
+
assert.strictEqual(buildDevCorsHeaders('https://evil.com')['Access-Control-Allow-Origin'], 'http://localhost:3000');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -104,4 +104,54 @@ startDevServer({ backendPath: '${join(tempDir, 'backend.ts').replace(/\\/g, '/')
|
|
|
104
104
|
assert.ok(stdout.includes('[rpc-ok] testApi.pingVoid'), `Missing verbose success log. stdout: ${stdout}`);
|
|
105
105
|
assert.ok(!stdout.includes('[rpc-err]'), `Unexpected RPC error log. stdout: ${stdout}`);
|
|
106
106
|
});
|
|
107
|
+
it('returns a JSON usage hint (not an empty body) for a GET on the API path', async () => {
|
|
108
|
+
const port = await getAvailablePort();
|
|
109
|
+
tempDir = join(tmpdir(), `dev-404-test-${process.pid}-${Date.now()}`);
|
|
110
|
+
mkdirSync(tempDir, { recursive: true });
|
|
111
|
+
writeFileSync(join(tempDir, 'backend.ts'), `
|
|
112
|
+
export const testApi = {
|
|
113
|
+
pingVoid: async () => undefined,
|
|
114
|
+
};
|
|
115
|
+
`);
|
|
116
|
+
writeFileSync(join(tempDir, 'preload.mjs'), `
|
|
117
|
+
if (!process.loadEnvFile) {
|
|
118
|
+
process.loadEnvFile = () => { throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); };
|
|
119
|
+
}
|
|
120
|
+
`);
|
|
121
|
+
writeFileSync(join(tempDir, 'run-dev.ts'), `
|
|
122
|
+
import { startDevServer } from '${join(__dirname, 'dev-server.js').replace(/\\/g, '/')}';
|
|
123
|
+
startDevServer({ backendPath: '${join(tempDir, 'backend.ts').replace(/\\/g, '/')}', port: ${port} });
|
|
124
|
+
`);
|
|
125
|
+
const tsxBin = join(__dirname, '..', '..', '..', '..', 'node_modules', '.bin', 'tsx');
|
|
126
|
+
devProcess = spawn(tsxBin, ['--import', join(tempDir, 'preload.mjs'), join(tempDir, 'run-dev.ts')], {
|
|
127
|
+
cwd: tempDir,
|
|
128
|
+
env: { ...process.env, AWS_BLOCKS_DISABLE_TELEMETRY: '1', BLOCKS_DEV_QUIET: '1' },
|
|
129
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
130
|
+
});
|
|
131
|
+
let stdout = '';
|
|
132
|
+
let stderr = '';
|
|
133
|
+
devProcess.stdout?.on('data', chunk => { stdout += chunk.toString(); });
|
|
134
|
+
devProcess.stderr?.on('data', chunk => { stderr += chunk.toString(); });
|
|
135
|
+
// A GET on the API path (e.g. opening it in a browser) hits the API handler
|
|
136
|
+
// but not the POST branch — the exact case that used to return an empty 404.
|
|
137
|
+
const deadline = Date.now() + 15_000;
|
|
138
|
+
let response;
|
|
139
|
+
let lastError;
|
|
140
|
+
while (!response && Date.now() < deadline) {
|
|
141
|
+
try {
|
|
142
|
+
response = await fetch(`http://127.0.0.1:${port}/aws-blocks/api`, { method: 'GET' });
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
lastError = error;
|
|
146
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
assert.ok(response, `Dev server did not respond: ${String(lastError)}\nstdout: ${stdout}\nstderr: ${stderr}`);
|
|
150
|
+
assert.strictEqual(response.status, 404);
|
|
151
|
+
assert.strictEqual(response.headers.get('content-type'), 'application/json');
|
|
152
|
+
const payload = await response.json();
|
|
153
|
+
assert.match(payload.error ?? '', /POST/, `404 body should hint at the POST requirement: ${JSON.stringify(payload)}`);
|
|
154
|
+
assert.strictEqual(payload.expected?.method, 'POST');
|
|
155
|
+
assert.strictEqual(payload.expected?.path, '/aws-blocks/api');
|
|
156
|
+
});
|
|
107
157
|
});
|
|
@@ -4,6 +4,14 @@ export declare const LOCALHOST_PATTERN: RegExp;
|
|
|
4
4
|
* Reflects back origins matching localhost/127.0.0.1; otherwise returns the fallback.
|
|
5
5
|
*/
|
|
6
6
|
export declare function resolveDevCorsOrigin(origin: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Build the CORS response headers the dev server sets on every request.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the Lambda path's cache posture — same {@link CORS_MAX_AGE} and the
|
|
11
|
+
* same `Vary: Origin` — so the reflected `Access-Control-Allow-Origin` can't be
|
|
12
|
+
* served across origins by a shared cache, and so the two sites can't drift.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildDevCorsHeaders(requestOrigin: string): Record<string, string>;
|
|
7
15
|
/** Shape of the client runtime config the browser fetches to discover the API URL. */
|
|
8
16
|
export interface BlocksRuntimeConfig {
|
|
9
17
|
apiUrl: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../../src/scripts/dev-server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../../src/scripts/dev-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,6 +12,7 @@ import { ApiError } from '../errors.js';
|
|
|
12
12
|
import { BLOCKS_RPC_PREFIX, BLOCKS_SANDBOX_PREFIX } from '../constants.js';
|
|
13
13
|
import { BLOCKS_SANDBOX_DIR } from '../common/constants.js';
|
|
14
14
|
import { matchRoute, lockRouteRegistry } from '../raw-route.js';
|
|
15
|
+
import { CORS_MAX_AGE } from '../cors.js';
|
|
15
16
|
import { registerBuiltinRoutes } from '../builtin-routes.js';
|
|
16
17
|
import { parseRpcRequest, successResponse, errorResponseFromCatch, methodNotFoundResponse, } from '../rpc.js';
|
|
17
18
|
import { redactToJson } from '../redact.js';
|
|
@@ -36,6 +37,23 @@ export const LOCALHOST_PATTERN = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/;
|
|
|
36
37
|
export function resolveDevCorsOrigin(origin) {
|
|
37
38
|
return LOCALHOST_PATTERN.test(origin) ? origin : 'http://localhost:3000';
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the CORS response headers the dev server sets on every request.
|
|
42
|
+
*
|
|
43
|
+
* Mirrors the Lambda path's cache posture — same {@link CORS_MAX_AGE} and the
|
|
44
|
+
* same `Vary: Origin` — so the reflected `Access-Control-Allow-Origin` can't be
|
|
45
|
+
* served across origins by a shared cache, and so the two sites can't drift.
|
|
46
|
+
*/
|
|
47
|
+
export function buildDevCorsHeaders(requestOrigin) {
|
|
48
|
+
return {
|
|
49
|
+
'Access-Control-Allow-Origin': resolveDevCorsOrigin(requestOrigin),
|
|
50
|
+
'Vary': 'Origin',
|
|
51
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS',
|
|
52
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
53
|
+
'Access-Control-Allow-Credentials': 'true',
|
|
54
|
+
'Access-Control-Max-Age': CORS_MAX_AGE,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
39
57
|
/**
|
|
40
58
|
* Build the runtime config the browser fetches at `${BLOCKS_SANDBOX_PREFIX}/config.json`.
|
|
41
59
|
* In sandbox mode the browser still targets the localhost front door (the dev
|
|
@@ -699,12 +717,9 @@ export async function startDevServer(options) {
|
|
|
699
717
|
const method = req.method || 'GET';
|
|
700
718
|
// CORS headers
|
|
701
719
|
const requestOrigin = req.headers.origin || '';
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
706
|
-
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
707
|
-
res.setHeader('Access-Control-Max-Age', '86400');
|
|
720
|
+
for (const [name, value] of Object.entries(buildDevCorsHeaders(requestOrigin))) {
|
|
721
|
+
res.setHeader(name, value);
|
|
722
|
+
}
|
|
708
723
|
if (method === 'OPTIONS') {
|
|
709
724
|
res.writeHead(200);
|
|
710
725
|
res.end();
|
|
@@ -1066,6 +1081,18 @@ function handleApiRequest(req, res, url, method, apis) {
|
|
|
1066
1081
|
});
|
|
1067
1082
|
return;
|
|
1068
1083
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1084
|
+
// Anything that reaches here didn't match `POST /aws-blocks/api`. Returning a
|
|
1085
|
+
// bare empty 404 leaves a developer poking at the endpoint (e.g. opening it in
|
|
1086
|
+
// a browser, which sends a GET, or trying a REST-style URL) with no feedback.
|
|
1087
|
+
// Reply with a small JSON hint pointing at the one supported shape. When the
|
|
1088
|
+
// path was right but the method wasn't, say so explicitly.
|
|
1089
|
+
const wrongMethodOnApi = url.pathname === BLOCKS_RPC_PREFIX;
|
|
1090
|
+
const message = wrongMethodOnApi
|
|
1091
|
+
? `The AWS Blocks JSON-RPC API expects POST, not ${method}.`
|
|
1092
|
+
: 'Not found. The AWS Blocks JSON-RPC API is served at POST /aws-blocks/api.';
|
|
1093
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1094
|
+
res.end(JSON.stringify({
|
|
1095
|
+
error: message,
|
|
1096
|
+
expected: { method: 'POST', path: BLOCKS_RPC_PREFIX },
|
|
1097
|
+
}));
|
|
1071
1098
|
}
|
package/dist/scripts/sandbox.js
CHANGED
|
@@ -124,7 +124,7 @@ export async function startSandbox(options) {
|
|
|
124
124
|
console.log("🌐 Starting local dev server (proxying to AWS)...");
|
|
125
125
|
console.log(`\n Open http://localhost:${clientPort}\n`);
|
|
126
126
|
const cdkWatch = spawnCommand("npx", [
|
|
127
|
-
"cdk", "watch",
|
|
127
|
+
"cdk", "watch",
|
|
128
128
|
`--outputs-file`, `${outDir}/outputs.json`,
|
|
129
129
|
`--context`, `projectRoot=${process.cwd()}`,
|
|
130
130
|
`--context`, `sandboxMode=true`,
|
package/dist/telemetry/client.js
CHANGED
|
@@ -192,15 +192,15 @@ export function sendEvent(event) {
|
|
|
192
192
|
const workerPath = path.join(dir, 'telemetry-send-worker.js');
|
|
193
193
|
const child = spawn(process.execPath, [workerPath, endpoint], {
|
|
194
194
|
detached: true,
|
|
195
|
-
stdio: ['pipe', 'ignore', 'ignore'],
|
|
195
|
+
stdio: ['pipe', 'ignore', process.env.NODE_DEBUG?.includes('blocks-telemetry') ? 'inherit' : 'ignore'],
|
|
196
196
|
// Clear NODE_OPTIONS so inherited flags (e.g. --conditions=cdk) don't interfere
|
|
197
197
|
env: { ...process.env, NODE_OPTIONS: '' },
|
|
198
198
|
});
|
|
199
|
-
child.stdin
|
|
199
|
+
child.stdin?.on('error', (err) => { debug('stdin write failed: %s', err.message); });
|
|
200
200
|
// Payload is small (<1KB JSON) so it fits in the kernel pipe buffer (~64KB)
|
|
201
201
|
// and survives the parent closing its fd on exit.
|
|
202
|
-
child.stdin
|
|
203
|
-
child.stdin
|
|
202
|
+
child.stdin?.write(payload);
|
|
203
|
+
child.stdin?.end();
|
|
204
204
|
child.on('error', (err) => { debug('spawn failed: %s', err.message); });
|
|
205
205
|
child.unref();
|
|
206
206
|
debug('spawned telemetry subprocess (pid=%d)', child.pid);
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// NOTE: This worker is an intentional near-duplicate of
|
|
4
|
+
// packages/create-blocks-app/src/telemetry-send-worker.ts. It is kept separate
|
|
5
|
+
// because the worker must run with zero project/package imports. Any change
|
|
6
|
+
// here should be mirrored in both files.
|
|
3
7
|
/**
|
|
4
8
|
* Telemetry send worker — spawned as a detached subprocess.
|
|
5
9
|
* Reads JSON payload from stdin, POSTs it to the endpoint (argv[2]).
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CORE_VERSION = "0.1.
|
|
1
|
+
export declare const CORE_VERSION = "0.1.18";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/generate-version.mjs — do not edit manually
|
|
2
|
-
export const CORE_VERSION = '0.1.
|
|
2
|
+
export const CORE_VERSION = '0.1.18';
|
package/package.json
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aws-blocks/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/aws-devtools-labs/aws-blocks.git",
|
|
7
|
+
"directory": "packages/core"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/aws-devtools-labs/aws-blocks/tree/main/packages/core#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/aws-devtools-labs/aws-blocks/issues"
|
|
12
|
+
},
|
|
4
13
|
"author": "Amazon Web Services",
|
|
5
14
|
"license": "Apache-2.0",
|
|
6
15
|
"type": "module",
|
|
@@ -6,8 +6,10 @@ import assert from 'node:assert';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { dirname, join } from 'node:path';
|
|
8
8
|
import * as cdk from 'aws-cdk-lib';
|
|
9
|
-
import { Template } from 'aws-cdk-lib/assertions';
|
|
9
|
+
import { Template, Match } from 'aws-cdk-lib/assertions';
|
|
10
|
+
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
|
|
10
11
|
import { BlocksBackend } from './blocks-backend.js';
|
|
12
|
+
import { Scope } from './index.js';
|
|
11
13
|
|
|
12
14
|
// Simulate the CDK condition being active (tests import CDK files directly)
|
|
13
15
|
before(() => {
|
|
@@ -19,6 +21,7 @@ const handlerPath = join(__dirname, '__fixtures__', 'handler.js');
|
|
|
19
21
|
const sideEffectBackendPath = join(__dirname, '__fixtures__', 'side-effect-backend.js');
|
|
20
22
|
const factoryBackendPath = join(__dirname, '__fixtures__', 'factory-backend.js');
|
|
21
23
|
const fullIdConstructBackendPath = join(__dirname, '__fixtures__', 'fullid-construct-backend.js');
|
|
24
|
+
const EXECUTION_ROLE_MARKER_ACTION = 'blocks-test:MarkerAction';
|
|
22
25
|
|
|
23
26
|
describe('ESM cache-busting (multi-stage)', () => {
|
|
24
27
|
test('BlocksBackend.create() with same backendCDKPath but different IDs produces constructs in each', async () => {
|
|
@@ -89,6 +92,92 @@ describe('synth shape (drop into existing stack)', () => {
|
|
|
89
92
|
});
|
|
90
93
|
});
|
|
91
94
|
|
|
95
|
+
describe('shared execution role', () => {
|
|
96
|
+
test('exposes executionRole on the backend', async () => {
|
|
97
|
+
const app = new cdk.App();
|
|
98
|
+
const parent = new cdk.Stack(app, 'RoleSurfaceStack');
|
|
99
|
+
|
|
100
|
+
const backend = await BlocksBackend.create(parent, 'Blocks', {
|
|
101
|
+
backendHandlerPath: handlerPath,
|
|
102
|
+
backendCDKPath: sideEffectBackendPath,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
assert.ok(backend.executionRole, 'BlocksBackend should expose .executionRole');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('synth produces a Lambda-assumable role with basic execution, and the handler uses it', async () => {
|
|
109
|
+
const app = new cdk.App();
|
|
110
|
+
const parent = new cdk.Stack(app, 'RoleSynthStack');
|
|
111
|
+
|
|
112
|
+
await BlocksBackend.create(parent, 'Blocks', {
|
|
113
|
+
backendHandlerPath: handlerPath,
|
|
114
|
+
backendCDKPath: sideEffectBackendPath,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const template = Template.fromStack(parent);
|
|
118
|
+
|
|
119
|
+
// The shared role (logical id derived from the 'BlocksRole' construct id)
|
|
120
|
+
// is assumable by Lambda and carries AWSLambdaBasicExecutionRole (so
|
|
121
|
+
// CloudWatch Logs keep working after swapping off the auto-role). Other
|
|
122
|
+
// roles exist (API Gateway CloudWatch role, config BucketDeployment role),
|
|
123
|
+
// so we target ours by logical id.
|
|
124
|
+
const roles = template.findResources('AWS::IAM::Role');
|
|
125
|
+
const blocksRoleId = Object.keys(roles).find(k => k.includes('BlocksRole'));
|
|
126
|
+
assert.ok(blocksRoleId, 'expected a role from the BlocksRole construct');
|
|
127
|
+
const blocksRole = roles[blocksRoleId];
|
|
128
|
+
assert.deepStrictEqual(blocksRole.Properties.AssumeRolePolicyDocument.Statement[0], {
|
|
129
|
+
Action: 'sts:AssumeRole',
|
|
130
|
+
Effect: 'Allow',
|
|
131
|
+
Principal: { Service: 'lambda.amazonaws.com' },
|
|
132
|
+
});
|
|
133
|
+
assert.ok(
|
|
134
|
+
JSON.stringify(blocksRole.Properties.ManagedPolicyArns ?? []).includes('AWSLambdaBasicExecutionRole'),
|
|
135
|
+
'BlocksRole should attach AWSLambdaBasicExecutionRole',
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// The Blocks handler references the shared role, not an auto-generated one.
|
|
139
|
+
template.hasResourceProperties('AWS::Lambda::Function', {
|
|
140
|
+
Role: { 'Fn::GetAtt': [blocksRoleId, 'Arn'] },
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('a nested block resolves executionRole via the construct-tree walk', async () => {
|
|
145
|
+
const app = new cdk.App();
|
|
146
|
+
const parent = new cdk.Stack(app, 'RoleResolveStack');
|
|
147
|
+
|
|
148
|
+
const backend = await BlocksBackend.create(parent, 'Blocks', {
|
|
149
|
+
backendHandlerPath: handlerPath,
|
|
150
|
+
backendCDKPath: sideEffectBackendPath,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Build nested Scopes under the backend (outer → inner), the same shape a
|
|
154
|
+
// real Building Block tree has, and grant a uniquely-named marker action to
|
|
155
|
+
// `this.executionRole` from the innermost scope. If the getter's tree-walk
|
|
156
|
+
// failed, it would resolve the wrong role (or throw), and the marker would
|
|
157
|
+
// not land on the backend's shared role.
|
|
158
|
+
// `create()` sets globalThis.CURRENT_BLOCKS_STACK = backend, so a parent-less
|
|
159
|
+
// Scope attaches under the backend (the same way a real backend module's
|
|
160
|
+
// top-level blocks do); `inner` is then nested one level deeper.
|
|
161
|
+
const outer = new Scope('outer');
|
|
162
|
+
const inner = new Scope('inner', { parent: outer });
|
|
163
|
+
|
|
164
|
+
// Resolves to the backend's shared role from two levels deep.
|
|
165
|
+
assert.strictEqual(inner.executionRole, backend.executionRole);
|
|
166
|
+
|
|
167
|
+
inner.executionRole.addToPrincipalPolicy(
|
|
168
|
+
new PolicyStatement({ actions: [EXECUTION_ROLE_MARKER_ACTION], resources: ['*'] }),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// The grant lands on the shared role's default inline policy (AWS::IAM::Policy).
|
|
172
|
+
const template = Template.fromStack(parent);
|
|
173
|
+
template.hasResourceProperties('AWS::IAM::Policy', {
|
|
174
|
+
PolicyDocument: {
|
|
175
|
+
Statement: Match.arrayWith([Match.objectLike({ Action: EXECUTION_ROLE_MARKER_ACTION })]),
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
92
181
|
describe('factory function support', () => {
|
|
93
182
|
test('BlocksBackend.create() calls default export function with the backend instance', async () => {
|
|
94
183
|
const app = new cdk.App();
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import * as cdk from 'aws-cdk-lib';
|
|
5
5
|
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
6
6
|
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
|
|
7
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
7
8
|
import { CfnGroup } from 'aws-cdk-lib/aws-resourcegroups';
|
|
8
9
|
import { Construct } from 'constructs';
|
|
9
10
|
import { pathToFileURL } from 'node:url';
|
|
@@ -48,10 +49,29 @@ export interface BlocksBackendProps {
|
|
|
48
49
|
|
|
49
50
|
/** Shared infra setup — creates Lambda + API Gateway on the given scope. */
|
|
50
51
|
export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id?: string) {
|
|
52
|
+
// ── Shared execution role ──────────────────────────────────────────────
|
|
53
|
+
// A single IAM role that every Building Block grants to. Provisioned here so
|
|
54
|
+
// it exists before the backend module is imported (Building Blocks reach it
|
|
55
|
+
// via `scope.executionRole`). Block grants sit on the role's default (inline)
|
|
56
|
+
// policy, exactly as they did on the auto-generated NodejsFunction role.
|
|
57
|
+
//
|
|
58
|
+
// AWSLambdaBasicExecutionRole is attached explicitly because the auto-role
|
|
59
|
+
// included it by default — omitting it would silently break CloudWatch Logs.
|
|
60
|
+
const executionRole = new iam.Role(scope, 'BlocksRole', {
|
|
61
|
+
// CompositePrincipal (rather than a bare ServicePrincipal) so additional
|
|
62
|
+
// compute types can assume this same shared role as they are introduced
|
|
63
|
+
// (e.g. ECS tasks via ecs-tasks.amazonaws.com), by adding principals here.
|
|
64
|
+
assumedBy: new iam.CompositePrincipal(new iam.ServicePrincipal('lambda.amazonaws.com')),
|
|
65
|
+
managedPolicies: [
|
|
66
|
+
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
|
|
51
70
|
const handler = new lambda.NodejsFunction(scope, 'Handler', {
|
|
52
71
|
entry: props.backendHandlerPath,
|
|
53
72
|
runtime: DEFAULT_NODE_RUNTIME,
|
|
54
73
|
handler: 'handler',
|
|
74
|
+
role: executionRole,
|
|
55
75
|
memorySize: 2048,
|
|
56
76
|
timeout: cdk.Duration.seconds(60 * 15),
|
|
57
77
|
environment: {
|
|
@@ -152,7 +172,7 @@ export function setupBlocksInfra(scope: Construct, props: BlocksBackendProps, id
|
|
|
152
172
|
|
|
153
173
|
registerBuiltinRoutes();
|
|
154
174
|
|
|
155
|
-
return { handler, gateway: api, apiUrl: `${api.url}${BLOCKS_RPC_PREFIX.slice(1)}
|
|
175
|
+
return { handler, gateway: api, apiUrl: `${api.url}${BLOCKS_RPC_PREFIX.slice(1)}`, executionRole };
|
|
156
176
|
}
|
|
157
177
|
|
|
158
178
|
/**
|
|
@@ -177,6 +197,8 @@ export class BlocksBackend extends Construct {
|
|
|
177
197
|
public readonly gateway: apigateway.RestApi;
|
|
178
198
|
public readonly handler: cdk.aws_lambda_nodejs.NodejsFunction;
|
|
179
199
|
public readonly backendHandlerPath: string;
|
|
200
|
+
/** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */
|
|
201
|
+
public readonly executionRole: iam.IRole;
|
|
180
202
|
|
|
181
203
|
/**
|
|
182
204
|
* The fullId used by child Scopes to compute their env var names,
|
|
@@ -221,6 +243,7 @@ export class BlocksBackend extends Construct {
|
|
|
221
243
|
this.handler = infra.handler;
|
|
222
244
|
this.gateway = infra.gateway;
|
|
223
245
|
this.apiUrl = infra.apiUrl;
|
|
246
|
+
this.executionRole = infra.executionRole;
|
|
224
247
|
|
|
225
248
|
// Override BLOCKS_STACK_NAME to include the parent stack name so runtime
|
|
226
249
|
// resource lookups (DynamoDB table names) match the CDK-time fullId
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { dirname, join } from 'node:path';
|
|
8
8
|
import * as cdk from 'aws-cdk-lib';
|
|
9
9
|
import { BlocksBackend } from './blocks-backend.js';
|
|
10
|
-
import { BlocksStack } from './index.js';
|
|
10
|
+
import { BlocksStack, Scope } from './index.js';
|
|
11
11
|
|
|
12
12
|
// Simulate the CDK condition being active (tests import CDK files directly)
|
|
13
13
|
before(() => {
|
|
@@ -78,6 +78,46 @@ describe('legacy side-effect mode (no default export)', () => {
|
|
|
78
78
|
});
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
+
describe('shared execution role (BlocksStack)', () => {
|
|
82
|
+
// The role synth shape and the Scope.executionRole tree-walk are shared code
|
|
83
|
+
// (setupBlocksInfra + the getter), covered in blocks-backend.test.ts. The only
|
|
84
|
+
// BlocksStack-specific behavior is that its own constructor wires
|
|
85
|
+
// executionRole — a separate code path from BlocksBackend's constructor.
|
|
86
|
+
test('BlocksStack wires executionRole via its constructor', async () => {
|
|
87
|
+
const app = new cdk.App();
|
|
88
|
+
const stack = await BlocksStack.create(app, 'StackRoleStack', {
|
|
89
|
+
backendHandlerPath: handlerPath,
|
|
90
|
+
backendCDKPath: sideEffectBackendPath,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
assert.ok(stack.executionRole, 'BlocksStack should expose a populated .executionRole');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('executionRole globalThis fallback', () => {
|
|
98
|
+
test('resolves via globalThis.CURRENT_BLOCKS_STACK when no owner is in the tree', async () => {
|
|
99
|
+
const app = new cdk.App();
|
|
100
|
+
const stack = await BlocksStack.create(app, 'FallbackStack', {
|
|
101
|
+
backendHandlerPath: handlerPath,
|
|
102
|
+
backendCDKPath: sideEffectBackendPath,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// A Scope whose construct-tree ancestry has no BlocksStack/BlocksBackend
|
|
106
|
+
// (parented under a plain cdk.Stack) exhausts the tree-walk and falls back
|
|
107
|
+
// to globalThis.CURRENT_BLOCKS_STACK. The `as any` is test plumbing — a
|
|
108
|
+
// plain Stack isn't a ScopeParent, but it IS a valid Construct parent.
|
|
109
|
+
const plainStack = new cdk.Stack(app, 'PlainStack');
|
|
110
|
+
(globalThis as any).CURRENT_BLOCKS_STACK = stack;
|
|
111
|
+
const orphan = new Scope('orphan', { parent: plainStack as any });
|
|
112
|
+
|
|
113
|
+
assert.strictEqual(
|
|
114
|
+
orphan.executionRole,
|
|
115
|
+
stack.executionRole,
|
|
116
|
+
'fallback resolves to the ambient stack role',
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
81
121
|
describe('assertCdkConditionActive', () => {
|
|
82
122
|
test('BlocksStack.create() throws when --conditions=cdk is missing', async () => {
|
|
83
123
|
const origNodeOptions = process.env.NODE_OPTIONS;
|
package/src/cdk/index.ts
CHANGED
|
@@ -30,6 +30,8 @@ export class BlocksStack extends cdk.Stack implements BaseBlocksStack {
|
|
|
30
30
|
public readonly gateway: cdk.aws_apigateway.RestApi;
|
|
31
31
|
public readonly handler: cdk.aws_lambda_nodejs.NodejsFunction;
|
|
32
32
|
public readonly backendHandlerPath: string;
|
|
33
|
+
/** Shared IAM role assumed by all Blocks compute. Building Blocks grant to this role. */
|
|
34
|
+
public readonly executionRole: cdk.aws_iam.IRole;
|
|
33
35
|
|
|
34
36
|
private constructor(scope: Construct, id: string, props: BlocksStackProps) {
|
|
35
37
|
super(scope, id, props);
|
|
@@ -43,6 +45,7 @@ export class BlocksStack extends cdk.Stack implements BaseBlocksStack {
|
|
|
43
45
|
this.handler = infra.handler;
|
|
44
46
|
this.gateway = infra.gateway;
|
|
45
47
|
this.apiUrl = infra.apiUrl;
|
|
48
|
+
this.executionRole = infra.executionRole;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
static async create(scope: Construct, id: string, props: BlocksStackProps) {
|
|
@@ -103,6 +106,28 @@ export class Scope extends Construct {
|
|
|
103
106
|
return ((globalThis as any).CURRENT_BLOCKS_STACK as { handler: cdk.aws_lambda_nodejs.NodejsFunction }).handler;
|
|
104
107
|
}
|
|
105
108
|
|
|
109
|
+
/**
|
|
110
|
+
* The shared IAM role assumed by all Blocks compute. Building Blocks grant
|
|
111
|
+
* their permissions to this role instead of to an individual function's
|
|
112
|
+
* auto-role. CDK's `grant*()` / `addToPrincipalPolicy()` route those grants
|
|
113
|
+
* to the role's default (inline) policy — exactly where they landed on the
|
|
114
|
+
* auto-generated role before.
|
|
115
|
+
*
|
|
116
|
+
* Resolves the same way as {@link handler}: walk up to the owning
|
|
117
|
+
* BlocksStack/BlocksBackend, falling back to the ambient stack.
|
|
118
|
+
*/
|
|
119
|
+
get executionRole(): cdk.aws_iam.IRole {
|
|
120
|
+
let current: Construct = this;
|
|
121
|
+
while (current.node.scope) {
|
|
122
|
+
current = current.node.scope as Construct;
|
|
123
|
+
if (current instanceof BlocksStack || current instanceof BlocksBackend) {
|
|
124
|
+
return current.executionRole;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Fallback to globalThis for backward compatibility
|
|
128
|
+
return ((globalThis as any).CURRENT_BLOCKS_STACK as { executionRole: cdk.aws_iam.IRole }).executionRole;
|
|
129
|
+
}
|
|
130
|
+
|
|
106
131
|
get fullId(): string {
|
|
107
132
|
return computeScopeFullId(this);
|
|
108
133
|
}
|