@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.
Files changed (65) hide show
  1. package/README.md +180 -17
  2. package/dist/cdk/blocks-backend.d.ts +4 -0
  3. package/dist/cdk/blocks-backend.d.ts.map +1 -1
  4. package/dist/cdk/blocks-backend.js +23 -1
  5. package/dist/cdk/blocks-backend.test.js +71 -1
  6. package/dist/cdk/blocks-stack.test.js +32 -1
  7. package/dist/cdk/index.d.ts +13 -0
  8. package/dist/cdk/index.d.ts.map +1 -1
  9. package/dist/cdk/index.js +24 -0
  10. package/dist/cors.d.ts +27 -1
  11. package/dist/cors.d.ts.map +1 -1
  12. package/dist/cors.js +55 -2
  13. package/dist/cors.test.js +81 -2
  14. package/dist/errors.test.js +26 -1
  15. package/dist/hosting.d.ts.map +1 -1
  16. package/dist/hosting.js +26 -1
  17. package/dist/hosting.test.js +73 -0
  18. package/dist/lambda-handler.d.ts.map +1 -1
  19. package/dist/lambda-handler.js +4 -17
  20. package/dist/lambda-handler.test.js +59 -2
  21. package/dist/rpc.test.js +77 -1
  22. package/dist/scripts/console.d.ts.map +1 -1
  23. package/dist/scripts/console.js +30 -2
  24. package/dist/scripts/deploy-stream.d.ts +181 -0
  25. package/dist/scripts/deploy-stream.d.ts.map +1 -0
  26. package/dist/scripts/deploy-stream.js +332 -0
  27. package/dist/scripts/deploy-stream.test.d.ts +2 -0
  28. package/dist/scripts/deploy-stream.test.d.ts.map +1 -0
  29. package/dist/scripts/deploy-stream.test.js +845 -0
  30. package/dist/scripts/deploy.d.ts.map +1 -1
  31. package/dist/scripts/deploy.js +16 -9
  32. package/dist/scripts/dev-server-cors.test.js +19 -1
  33. package/dist/scripts/dev-server-rpc.test.js +50 -0
  34. package/dist/scripts/dev-server.d.ts +8 -0
  35. package/dist/scripts/dev-server.d.ts.map +1 -1
  36. package/dist/scripts/dev-server.js +35 -8
  37. package/dist/scripts/sandbox.js +1 -1
  38. package/dist/telemetry/client.js +4 -4
  39. package/dist/telemetry/telemetry-send-worker.js +4 -0
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/package.json +10 -1
  43. package/src/cdk/blocks-backend.test.ts +90 -1
  44. package/src/cdk/blocks-backend.ts +24 -1
  45. package/src/cdk/blocks-stack.test.ts +41 -1
  46. package/src/cdk/index.ts +25 -0
  47. package/src/cors.test.ts +96 -2
  48. package/src/cors.ts +59 -2
  49. package/src/errors.test.ts +29 -1
  50. package/src/hosting.test.ts +107 -0
  51. package/src/hosting.ts +27 -1
  52. package/src/lambda-handler.test.ts +71 -2
  53. package/src/lambda-handler.ts +4 -20
  54. package/src/rpc.test.ts +96 -1
  55. package/src/scripts/console.ts +29 -2
  56. package/src/scripts/deploy-stream.test.ts +1035 -0
  57. package/src/scripts/deploy-stream.ts +475 -0
  58. package/src/scripts/deploy.ts +18 -11
  59. package/src/scripts/dev-server-cors.test.ts +26 -1
  60. package/src/scripts/dev-server-rpc.test.ts +54 -0
  61. package/src/scripts/dev-server.ts +38 -8
  62. package/src/scripts/sandbox.ts +1 -1
  63. package/src/telemetry/client.ts +4 -4
  64. package/src/telemetry/telemetry-send-worker.ts +5 -0
  65. 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,iBA0FlD"}
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"}
@@ -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 { runSync } from './run-command.js';
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
- runSync("npx", [
49
- "cdk", "deploy",
50
- "--require-approval", "never",
51
- "--outputs-file", ".blocks-sandbox/outputs.json",
52
- "--context", `projectRoot=${options.projectRoot}`,
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
- console.error('\n❌ Deployment failed.');
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":"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"}
1
+ {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../../src/scripts/dev-server.ts"],"names":[],"mappings":"AAsCA,eAAO,MAAM,iBAAiB,QAAiD,CAAC;AAEhF;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASjF;AAED,sFAAsF;AACtF,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,OAAO,GAAG,SAAS,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,mBAAmB,CAKvF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE/E;AAED,MAAM,WAAW,gBAAgB;IAC/B,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAiBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,SAAS,SAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa/F;AAYD,+DAA+D;AAC/D,MAAM,WAAW,qBAAqB;IACpC,oFAAoF;IACpF,WAAW,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,uFAAuF;AACvF,eAAO,MAAM,+BAA+B,EAAE,qBAK7C,CAAC;AAEF,kDAAkD;AAClD,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,OAAO,EAAE,OAAO,CAAC;IACjB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,MAAM,EAAE,EACxB,GAAG,EAAE,MAAM,EACX,MAAM,GAAE,qBAAuD,GAC9D,eAAe,CAOjB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAOnF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,EAC5E,OAAO,EAAE,OAAO,GACf,OAAO,CAOT;AAID,sCAAsC;AACtC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,OAAO,CAAC;IACjB,oFAAoF;IACpF,SAAS,EAAE,OAAO,CAAC;IACnB,yGAAyG;IACzG,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,4CAA4C;IAC5C,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,gDAAgD;IAChD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IACrC,6EAA6E;IAC7E,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,KAAK,IAAI,CAAC;IACxD,kDAAkD;IAClD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,OAAO,CAAC,eAAe,CAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAwB3G;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAW3F;AAED,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,oFAAoF;AACpF,eAAO,MAAM,8BAA8B,EAAE,mBAG5C,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,mBAAoD,GAC3D;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAGrC;AAED,4GAA4G;AAC5G,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;IAClD,wEAAwE;IACxE,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,wEAAwE;IACxE,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,+EAA+E;IAC/E,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,oDAAoD;IACpD,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,aAAa,EACnB,MAAM,GAAE,mBAAoD,GAC3D,MAAM,IAAI,CAwBZ;AAID,8EAA8E;AAC9E,MAAM,WAAW,kBAAkB;IACjC,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,mGAAmG;AACnG,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAUtE;AAED,wGAAwG;AACxG,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAmC,GAAG,OAAO,CAQ3H;AAED,8GAA8G;AAC9G,MAAM,MAAM,iBAAiB,GAAG;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3F;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,kBAAkB,GAAG,IAAI,EACnC,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACnC,SAAS,EAAE,OAAO,EAClB,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,GAChC,iBAAiB,CASnB;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,gBAAgB,iBAmiB7D"}
@@ -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 allowedOrigin = resolveDevCorsOrigin(requestOrigin);
703
- res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
704
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS');
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
- res.writeHead(404);
1070
- res.end();
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
  }
@@ -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", "--hotswap",
127
+ "cdk", "watch",
128
128
  `--outputs-file`, `${outDir}/outputs.json`,
129
129
  `--context`, `projectRoot=${process.cwd()}`,
130
130
  `--context`, `sandboxMode=true`,
@@ -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.on('error', (err) => { debug('stdin write failed: %s', err.message); });
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.write(payload);
203
- child.stdin.end();
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.13";
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.13';
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.13",
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
  }