@aws-blocks/core 0.1.4 → 0.1.10
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/dist/cdk/index.d.ts +1 -1
- package/dist/cdk/index.d.ts.map +1 -1
- package/dist/cdk/index.js +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/db-naming.d.ts +17 -5
- package/dist/db-naming.d.ts.map +1 -1
- package/dist/db-naming.js +18 -6
- package/dist/db-naming.test.js +44 -3
- package/dist/errors.d.ts +28 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +27 -1
- package/dist/errors.test.d.ts +2 -0
- package/dist/errors.test.d.ts.map +1 -0
- package/dist/errors.test.js +47 -0
- package/dist/hosting.d.ts +71 -1
- package/dist/hosting.d.ts.map +1 -1
- package/dist/hosting.js +47 -8
- package/dist/hosting.test.js +60 -0
- package/dist/index.cdk.d.ts +1 -1
- package/dist/index.cdk.d.ts.map +1 -1
- package/dist/index.cdk.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/scripts/deploy.d.ts.map +1 -1
- package/dist/scripts/deploy.js +4 -2
- package/dist/scripts/dev-server-supervisor.test.d.ts +2 -0
- package/dist/scripts/dev-server-supervisor.test.d.ts.map +1 -0
- package/dist/scripts/dev-server-supervisor.test.js +551 -0
- package/dist/scripts/dev-server.d.ts +73 -0
- package/dist/scripts/dev-server.d.ts.map +1 -1
- package/dist/scripts/dev-server.js +279 -29
- package/dist/scripts/ensure-secrets.d.ts +5 -2
- package/dist/scripts/ensure-secrets.d.ts.map +1 -1
- package/dist/scripts/ensure-secrets.js +14 -6
- package/dist/scripts/external-migrations-step.d.ts.map +1 -1
- package/dist/scripts/external-migrations-step.js +5 -1
- package/dist/scripts/index.d.ts +1 -0
- package/dist/scripts/index.d.ts.map +1 -1
- package/dist/scripts/index.js +1 -0
- package/dist/scripts/process-tree.d.ts +126 -0
- package/dist/scripts/process-tree.d.ts.map +1 -0
- package/dist/scripts/process-tree.js +198 -0
- package/dist/scripts/sandbox.d.ts.map +1 -1
- package/dist/scripts/sandbox.js +51 -4
- package/dist/scripts/stack-id.d.ts +37 -0
- package/dist/scripts/stack-id.d.ts.map +1 -0
- package/dist/scripts/stack-id.js +79 -0
- package/dist/scripts/stack-id.test.d.ts +2 -0
- package/dist/scripts/stack-id.test.d.ts.map +1 -0
- package/dist/scripts/stack-id.test.js +104 -0
- package/dist/telemetry/client.d.ts +3 -1
- package/dist/telemetry/client.d.ts.map +1 -1
- package/dist/telemetry/client.js +20 -24
- package/dist/telemetry/telemetry-send-worker.d.ts +2 -0
- package/dist/telemetry/telemetry-send-worker.d.ts.map +1 -0
- package/dist/telemetry/telemetry-send-worker.js +58 -0
- package/dist/telemetry/telemetry.test.js +77 -1
- package/dist/telemetry/trackCommand.d.ts +1 -1
- package/dist/telemetry/trackCommand.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/cdk/index.ts +1 -1
- package/src/client/index.ts +1 -1
- package/src/db-naming.test.ts +50 -5
- package/src/db-naming.ts +18 -6
- package/src/errors.test.ts +55 -0
- package/src/errors.ts +32 -1
- package/src/hosting.test.ts +79 -0
- package/src/hosting.ts +127 -13
- package/src/index.cdk.ts +1 -1
- package/src/index.ts +1 -1
- package/src/scripts/deploy.ts +4 -2
- package/src/scripts/dev-server-supervisor.test.ts +621 -0
- package/src/scripts/dev-server.ts +316 -27
- package/src/scripts/ensure-secrets.ts +17 -6
- package/src/scripts/external-migrations-step.ts +5 -1
- package/src/scripts/index.ts +1 -0
- package/src/scripts/process-tree.ts +245 -0
- package/src/scripts/sandbox.ts +50 -4
- package/src/scripts/stack-id.test.ts +123 -0
- package/src/scripts/stack-id.ts +87 -0
- package/src/telemetry/client.ts +22 -30
- package/src/telemetry/telemetry-send-worker.ts +60 -0
- package/src/telemetry/telemetry.test.ts +91 -1
- package/src/telemetry/trackCommand.ts +3 -3
- package/src/version.ts +1 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { describe, it, afterEach } from 'node:test';
|
|
4
|
+
import assert from 'node:assert';
|
|
5
|
+
import { mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { getStackId, getSandboxId, getStackName } from './stack-id.js';
|
|
9
|
+
describe('getStackId', () => {
|
|
10
|
+
let tmpDir;
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
if (tmpDir)
|
|
13
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
it('reads stackId from .blocks/config.json', () => {
|
|
16
|
+
tmpDir = join(tmpdir(), `stack-id-test-${Date.now()}`);
|
|
17
|
+
mkdirSync(join(tmpDir, '.blocks'), { recursive: true });
|
|
18
|
+
writeFileSync(join(tmpDir, '.blocks', 'config.json'), JSON.stringify({ stackId: 'test-abc123' }));
|
|
19
|
+
assert.strictEqual(getStackId(tmpDir), 'test-abc123');
|
|
20
|
+
});
|
|
21
|
+
it('throws actionable error when config is missing', () => {
|
|
22
|
+
tmpDir = join(tmpdir(), `stack-id-test-missing-${Date.now()}`);
|
|
23
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
24
|
+
assert.throws(() => getStackId(tmpDir), /\.blocks\/config\.json not found/);
|
|
25
|
+
});
|
|
26
|
+
it('throws actionable error when stackId key is missing', () => {
|
|
27
|
+
tmpDir = join(tmpdir(), `stack-id-test-nokey-${Date.now()}`);
|
|
28
|
+
mkdirSync(join(tmpDir, '.blocks'), { recursive: true });
|
|
29
|
+
writeFileSync(join(tmpDir, '.blocks', 'config.json'), JSON.stringify({ other: 'value' }));
|
|
30
|
+
assert.throws(() => getStackId(tmpDir), /\.blocks\/config\.json not found/);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('getSandboxId', () => {
|
|
34
|
+
let tmpDir;
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
if (tmpDir)
|
|
37
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
it('generates and persists a sandbox id', () => {
|
|
40
|
+
tmpDir = join(tmpdir(), `sandbox-id-test-${Date.now()}`);
|
|
41
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
42
|
+
const id = getSandboxId(tmpDir);
|
|
43
|
+
assert.match(id, /^[a-z0-9]+-[a-f0-9]{6}$/);
|
|
44
|
+
// Verify persisted
|
|
45
|
+
const stored = readFileSync(join(tmpDir, '.blocks-sandbox', 'sandbox-id.txt'), 'utf-8').trim();
|
|
46
|
+
assert.strictEqual(stored, id);
|
|
47
|
+
});
|
|
48
|
+
it('returns existing id on subsequent calls', () => {
|
|
49
|
+
tmpDir = join(tmpdir(), `sandbox-id-test-idem-${Date.now()}`);
|
|
50
|
+
mkdirSync(join(tmpDir, '.blocks-sandbox'), { recursive: true });
|
|
51
|
+
writeFileSync(join(tmpDir, '.blocks-sandbox', 'sandbox-id.txt'), 'alice-abc123');
|
|
52
|
+
assert.strictEqual(getSandboxId(tmpDir), 'alice-abc123');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('getStackName', () => {
|
|
56
|
+
let tmpDir;
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
if (tmpDir)
|
|
59
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
it('production is <stackId>-prod', () => {
|
|
62
|
+
tmpDir = join(tmpdir(), `stack-name-prod-${Date.now()}`);
|
|
63
|
+
mkdirSync(join(tmpDir, '.blocks'), { recursive: true });
|
|
64
|
+
writeFileSync(join(tmpDir, '.blocks', 'config.json'), JSON.stringify({ stackId: 'my-app-k7x2mf' }));
|
|
65
|
+
assert.strictEqual(getStackName({ sandbox: false, projectRoot: tmpDir }), 'my-app-k7x2mf-prod');
|
|
66
|
+
});
|
|
67
|
+
it('sandbox is <stackId>-<sandboxId>', () => {
|
|
68
|
+
tmpDir = join(tmpdir(), `stack-name-sbx-${Date.now()}`);
|
|
69
|
+
mkdirSync(join(tmpDir, '.blocks'), { recursive: true });
|
|
70
|
+
writeFileSync(join(tmpDir, '.blocks', 'config.json'), JSON.stringify({ stackId: 'my-app-k7x2mf' }));
|
|
71
|
+
mkdirSync(join(tmpDir, '.blocks-sandbox'), { recursive: true });
|
|
72
|
+
writeFileSync(join(tmpDir, '.blocks-sandbox', 'sandbox-id.txt'), 'alice-0d7e1c');
|
|
73
|
+
assert.strictEqual(getStackName({ sandbox: true, projectRoot: tmpDir }), 'my-app-k7x2mf-alice-0d7e1c');
|
|
74
|
+
});
|
|
75
|
+
it('throws actionable error when config is missing (fail fast, no silent fallback)', () => {
|
|
76
|
+
tmpDir = join(tmpdir(), `stack-name-missing-${Date.now()}`);
|
|
77
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
78
|
+
assert.throws(() => getStackName({ sandbox: false, projectRoot: tmpDir }), /\.blocks\/config\.json not found/);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('getStackName sandbox id (get-or-create)', () => {
|
|
82
|
+
let tmpDir;
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
if (tmpDir)
|
|
85
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
it('creates and persists sandbox-id.txt on first call when missing', () => {
|
|
88
|
+
tmpDir = join(tmpdir(), `stack-name-getorcreate-${Date.now()}`);
|
|
89
|
+
mkdirSync(join(tmpDir, '.blocks'), { recursive: true });
|
|
90
|
+
writeFileSync(join(tmpDir, '.blocks', 'config.json'), JSON.stringify({ stackId: 'test-app' }));
|
|
91
|
+
// No .blocks-sandbox dir yet — getStackName creates the id rather than throwing.
|
|
92
|
+
const name = getStackName({ sandbox: true, projectRoot: tmpDir });
|
|
93
|
+
assert.match(name, /^test-app-[a-z0-9]+-[a-f0-9]{6}$/);
|
|
94
|
+
// Persisted so later callers/processes resolve the identical name.
|
|
95
|
+
const stored = readFileSync(join(tmpDir, '.blocks-sandbox', 'sandbox-id.txt'), 'utf-8').trim();
|
|
96
|
+
assert.strictEqual(name, `test-app-${stored}`);
|
|
97
|
+
});
|
|
98
|
+
it('reuses the same id across calls', () => {
|
|
99
|
+
tmpDir = join(tmpdir(), `stack-name-getorcreate-idem-${Date.now()}`);
|
|
100
|
+
mkdirSync(join(tmpDir, '.blocks'), { recursive: true });
|
|
101
|
+
writeFileSync(join(tmpDir, '.blocks', 'config.json'), JSON.stringify({ stackId: 'test-app' }));
|
|
102
|
+
assert.strictEqual(getStackName({ sandbox: true, projectRoot: tmpDir }), getStackName({ sandbox: true, projectRoot: tmpDir }));
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -52,7 +52,9 @@ export declare function buildAndSendEvent(opts: BuildAndSendEventOptions): void;
|
|
|
52
52
|
/**
|
|
53
53
|
* Send a pre-built telemetry event to the collection endpoint.
|
|
54
54
|
*
|
|
55
|
-
*
|
|
55
|
+
* Spawns a detached subprocess that performs the HTTPS POST independently of
|
|
56
|
+
* the parent CLI process. This ensures the request completes even when the
|
|
57
|
+
* parent exits on failure paths before an in-process request would flush.
|
|
56
58
|
* Debug output available via `NODE_DEBUG=blocks-telemetry`.
|
|
57
59
|
*/
|
|
58
60
|
export declare function sendEvent(event: BlocksTelemetryEvent): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/telemetry/client.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/telemetry/client.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAYjF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,SAAS,CAWzD;AAwBD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,wBAAwB,GAAG,oBAAoB,CAwD/E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,wBAAwB,GAAG,IAAI,CAatE;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI,CAgC3D"}
|
package/dist/telemetry/client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { request as httpsRequest } from 'node:https';
|
|
2
|
-
import { request as httpRequest } from 'node:http';
|
|
3
1
|
import { existsSync, readFileSync, mkdirSync, openSync, writeSync, closeSync, writeFileSync, constants } from 'node:fs';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
4
3
|
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { debuglog } from 'node:util';
|
|
6
6
|
import { CORE_VERSION } from '../version.js';
|
|
7
7
|
import { Scope } from '../common/index.js';
|
|
@@ -11,7 +11,6 @@ import { getInstallationId, getProjectId, generateEventId } from './identifiers.
|
|
|
11
11
|
// Debug log: writes to stderr via NODE_DEBUG=blocks-telemetry (developer-facing, for troubleshooting)
|
|
12
12
|
const debug = debuglog('blocks-telemetry');
|
|
13
13
|
const DEFAULT_ENDPOINT = 'https://blocks-telemetry.us-east-1.api.aws/metrics';
|
|
14
|
-
const TIMEOUT_MS = 500;
|
|
15
14
|
const TELEMETRY_VERSION = '1.0.0';
|
|
16
15
|
function getEndpoint() {
|
|
17
16
|
return process.env.BLOCKS_TELEMETRY_ENDPOINT || DEFAULT_ENDPOINT;
|
|
@@ -172,7 +171,9 @@ export function buildAndSendEvent(opts) {
|
|
|
172
171
|
/**
|
|
173
172
|
* Send a pre-built telemetry event to the collection endpoint.
|
|
174
173
|
*
|
|
175
|
-
*
|
|
174
|
+
* Spawns a detached subprocess that performs the HTTPS POST independently of
|
|
175
|
+
* the parent CLI process. This ensures the request completes even when the
|
|
176
|
+
* parent exits on failure paths before an in-process request would flush.
|
|
176
177
|
* Debug output available via `NODE_DEBUG=blocks-telemetry`.
|
|
177
178
|
*/
|
|
178
179
|
export function sendEvent(event) {
|
|
@@ -187,27 +188,22 @@ export function sendEvent(event) {
|
|
|
187
188
|
catch { }
|
|
188
189
|
}
|
|
189
190
|
debug('sending event to %s (%d bytes)', endpoint, Buffer.byteLength(payload));
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
method: 'POST',
|
|
198
|
-
headers: {
|
|
199
|
-
'Content-Type': 'application/json',
|
|
200
|
-
'Content-Length': Buffer.byteLength(payload),
|
|
201
|
-
},
|
|
202
|
-
timeout: TIMEOUT_MS,
|
|
203
|
-
}, (res) => {
|
|
204
|
-
debug('event sent (status=%d)', res.statusCode);
|
|
205
|
-
res.resume();
|
|
191
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
192
|
+
const workerPath = path.join(dir, 'telemetry-send-worker.js');
|
|
193
|
+
const child = spawn(process.execPath, [workerPath, endpoint], {
|
|
194
|
+
detached: true,
|
|
195
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
196
|
+
// Clear NODE_OPTIONS so inherited flags (e.g. --conditions=cdk) don't interfere
|
|
197
|
+
env: { ...process.env, NODE_OPTIONS: '' },
|
|
206
198
|
});
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
199
|
+
child.stdin.on('error', (err) => { debug('stdin write failed: %s', err.message); });
|
|
200
|
+
// Payload is small (<1KB JSON) so it fits in the kernel pipe buffer (~64KB)
|
|
201
|
+
// and survives the parent closing its fd on exit.
|
|
202
|
+
child.stdin.write(payload);
|
|
203
|
+
child.stdin.end();
|
|
204
|
+
child.on('error', (err) => { debug('spawn failed: %s', err.message); });
|
|
205
|
+
child.unref();
|
|
206
|
+
debug('spawned telemetry subprocess (pid=%d)', child.pid);
|
|
211
207
|
}
|
|
212
208
|
catch {
|
|
213
209
|
// Telemetry must never throw or affect the user's command
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-send-worker.d.ts","sourceRoot":"","sources":["../../src/telemetry/telemetry-send-worker.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Telemetry send worker — spawned as a detached subprocess.
|
|
5
|
+
* Reads JSON payload from stdin, POSTs it to the endpoint (argv[2]).
|
|
6
|
+
*
|
|
7
|
+
* Uses only Node built-ins — no project imports — so the compiled .js
|
|
8
|
+
* runs with bare `node` (no tsx needed).
|
|
9
|
+
*/
|
|
10
|
+
import { request as httpsRequest } from 'node:https';
|
|
11
|
+
import { request as httpRequest } from 'node:http';
|
|
12
|
+
const TIMEOUT_MS = 500;
|
|
13
|
+
const debug = (process.env.NODE_DEBUG || '').includes('blocks-telemetry');
|
|
14
|
+
const endpoint = process.argv[2];
|
|
15
|
+
if (!endpoint)
|
|
16
|
+
process.exit(1);
|
|
17
|
+
let payload = '';
|
|
18
|
+
process.stdin.setEncoding('utf-8');
|
|
19
|
+
process.stdin.on('data', (chunk) => { payload += chunk; });
|
|
20
|
+
process.stdin.on('end', () => {
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(endpoint);
|
|
23
|
+
const isHttps = url.protocol === 'https:';
|
|
24
|
+
const requestFn = isHttps ? httpsRequest : httpRequest;
|
|
25
|
+
const req = requestFn({
|
|
26
|
+
hostname: url.hostname,
|
|
27
|
+
port: url.port || (isHttps ? '443' : '80'),
|
|
28
|
+
path: url.pathname + url.search,
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
33
|
+
},
|
|
34
|
+
timeout: TIMEOUT_MS,
|
|
35
|
+
}, (res) => {
|
|
36
|
+
res.resume();
|
|
37
|
+
if (debug)
|
|
38
|
+
process.stderr.write(`BLOCKS-TELEMETRY: sent (status=${res.statusCode})\n`);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
});
|
|
41
|
+
req.on('error', (e) => {
|
|
42
|
+
if (debug)
|
|
43
|
+
process.stderr.write(`BLOCKS-TELEMETRY: error: ${e.message}\n`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
});
|
|
46
|
+
req.on('timeout', () => {
|
|
47
|
+
if (debug)
|
|
48
|
+
process.stderr.write(`BLOCKS-TELEMETRY: timed out\n`);
|
|
49
|
+
req.destroy();
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
|
52
|
+
req.write(payload);
|
|
53
|
+
req.end();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
@@ -10,7 +10,7 @@ import { isCI, detectOS, detectNodeVersion, detectPackageManager, detectAgent, c
|
|
|
10
10
|
import { trackCommand, classifyError } from './trackCommand.js';
|
|
11
11
|
import { buildAndSendEvent, buildEvent, sendEvent, getTelemetryFilePath } from './client.js';
|
|
12
12
|
import { getInstallationId, getProjectId, generateEventId } from './identifiers.js';
|
|
13
|
-
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import { spawnSync, spawn as spawnChild } from 'node:child_process';
|
|
14
14
|
import { Scope } from '../common/index.js';
|
|
15
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
describe('telemetry/consent', () => {
|
|
@@ -453,6 +453,82 @@ describe('telemetry/client', () => {
|
|
|
453
453
|
process.env = { ...originalEnv };
|
|
454
454
|
});
|
|
455
455
|
});
|
|
456
|
+
describe('telemetry/send-worker', () => {
|
|
457
|
+
it('worker POSTs payload from stdin to endpoint', async () => {
|
|
458
|
+
const received = [];
|
|
459
|
+
const server = await new Promise((resolve) => {
|
|
460
|
+
const s = createServer((req, res) => {
|
|
461
|
+
let body = '';
|
|
462
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
463
|
+
req.on('end', () => {
|
|
464
|
+
received.push(body);
|
|
465
|
+
res.writeHead(200);
|
|
466
|
+
res.end();
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
s.listen(0, '127.0.0.1', () => resolve(s));
|
|
470
|
+
});
|
|
471
|
+
const addr = server.address();
|
|
472
|
+
const endpoint = `http://127.0.0.1:${addr.port}/collect`;
|
|
473
|
+
const payload = JSON.stringify({ test: true, command: 'dev' });
|
|
474
|
+
const workerPath = join(__dirname, 'telemetry-send-worker.js');
|
|
475
|
+
const exitCode = await new Promise((resolve) => {
|
|
476
|
+
const proc = spawnChild(process.execPath, [workerPath, endpoint], {
|
|
477
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
478
|
+
env: { ...process.env, NODE_OPTIONS: '' },
|
|
479
|
+
});
|
|
480
|
+
proc.stdin.write(payload);
|
|
481
|
+
proc.stdin.end();
|
|
482
|
+
proc.on('close', (code) => resolve(code));
|
|
483
|
+
});
|
|
484
|
+
assert.strictEqual(exitCode, 0);
|
|
485
|
+
assert.strictEqual(received.length, 1);
|
|
486
|
+
assert.deepStrictEqual(JSON.parse(received[0]), { test: true, command: 'dev' });
|
|
487
|
+
server.close();
|
|
488
|
+
});
|
|
489
|
+
it('worker exits with 1 on unreachable endpoint', async () => {
|
|
490
|
+
const payload = JSON.stringify({ test: true });
|
|
491
|
+
const workerPath = join(__dirname, 'telemetry-send-worker.js');
|
|
492
|
+
const exitCode = await new Promise((resolve) => {
|
|
493
|
+
const proc = spawnChild(process.execPath, [workerPath, 'http://127.0.0.1:1/unreachable'], {
|
|
494
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
495
|
+
env: { ...process.env, NODE_OPTIONS: '' },
|
|
496
|
+
});
|
|
497
|
+
proc.stdin.write(payload);
|
|
498
|
+
proc.stdin.end();
|
|
499
|
+
proc.on('close', (code) => resolve(code));
|
|
500
|
+
});
|
|
501
|
+
assert.strictEqual(exitCode, 1);
|
|
502
|
+
});
|
|
503
|
+
it('worker writes debug output to stderr when NODE_DEBUG is set', async () => {
|
|
504
|
+
const server = await new Promise((resolve) => {
|
|
505
|
+
const s = createServer((req, res) => {
|
|
506
|
+
let body = '';
|
|
507
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
508
|
+
req.on('end', () => { res.writeHead(200); res.end(); });
|
|
509
|
+
});
|
|
510
|
+
s.listen(0, '127.0.0.1', () => resolve(s));
|
|
511
|
+
});
|
|
512
|
+
const addr = server.address();
|
|
513
|
+
const endpoint = `http://127.0.0.1:${addr.port}/collect`;
|
|
514
|
+
const payload = JSON.stringify({ test: true });
|
|
515
|
+
const workerPath = join(__dirname, 'telemetry-send-worker.js');
|
|
516
|
+
const result = await new Promise((resolve) => {
|
|
517
|
+
const proc = spawnChild(process.execPath, [workerPath, endpoint], {
|
|
518
|
+
stdio: ['pipe', 'ignore', 'pipe'],
|
|
519
|
+
env: { ...process.env, NODE_OPTIONS: '', NODE_DEBUG: 'blocks-telemetry' },
|
|
520
|
+
});
|
|
521
|
+
let stderr = '';
|
|
522
|
+
proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
523
|
+
proc.stdin.write(payload);
|
|
524
|
+
proc.stdin.end();
|
|
525
|
+
proc.on('close', (code) => resolve({ code, stderr }));
|
|
526
|
+
});
|
|
527
|
+
assert.strictEqual(result.code, 0);
|
|
528
|
+
assert.ok(result.stderr.includes('BLOCKS-TELEMETRY: sent (status=200)'), `Expected debug output, got: ${result.stderr}`);
|
|
529
|
+
server.close();
|
|
530
|
+
});
|
|
531
|
+
});
|
|
456
532
|
describe('telemetry/trackCommand integration', () => {
|
|
457
533
|
const originalEnv = { ...process.env };
|
|
458
534
|
afterEach(() => {
|
|
@@ -12,7 +12,7 @@ export declare function classifyError(error: unknown): {
|
|
|
12
12
|
*
|
|
13
13
|
* Measures wall-clock duration, classifies errors, and sends a single telemetry event
|
|
14
14
|
* after the command completes (success or failure). The telemetry send is fire-and-forget
|
|
15
|
-
* and bounded by a
|
|
15
|
+
* and bounded by a 500ms timeout — it will never delay the command or affect its exit code.
|
|
16
16
|
*
|
|
17
17
|
* If telemetry is disabled (via env var or config), the function is executed directly
|
|
18
18
|
* with zero overhead.
|
|
@@ -20,10 +20,10 @@ export function classifyError(error) {
|
|
|
20
20
|
if (msg.includes('cdk synth') || msg.includes('synthesis')) {
|
|
21
21
|
return { code: 'CDK_SYNTH_FAILED', phase: 'synth' };
|
|
22
22
|
}
|
|
23
|
-
if (msg.includes('cdk deploy') || msg.includes('deployment failed')) {
|
|
23
|
+
if (msg.includes('cdk deploy') || msg.includes('deployment failed') || (msg.includes('cdk') && msg.includes('deploy') && msg.includes('exited with code'))) {
|
|
24
24
|
return { code: 'CDK_DEPLOY_FAILED', phase: 'deploy' };
|
|
25
25
|
}
|
|
26
|
-
if (msg.includes('cdk destroy') || msg.includes('destroy failed')) {
|
|
26
|
+
if (msg.includes('cdk destroy') || msg.includes('destroy failed') || (msg.includes('cdk') && msg.includes('destroy') && msg.includes('exited with code'))) {
|
|
27
27
|
return { code: 'CDK_DESTROY_FAILED', phase: 'destroy' };
|
|
28
28
|
}
|
|
29
29
|
if (msg.includes('npm install') || msg.includes('npm err')) {
|
|
@@ -48,7 +48,7 @@ export function classifyError(error) {
|
|
|
48
48
|
*
|
|
49
49
|
* Measures wall-clock duration, classifies errors, and sends a single telemetry event
|
|
50
50
|
* after the command completes (success or failure). The telemetry send is fire-and-forget
|
|
51
|
-
* and bounded by a
|
|
51
|
+
* and bounded by a 500ms timeout — it will never delay the command or affect its exit code.
|
|
52
52
|
*
|
|
53
53
|
* If telemetry is disabled (via env var or config), the function is executed directly
|
|
54
54
|
* with zero overhead.
|
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.10";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,YAAY,
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,YAAY,WAAW,CAAC"}
|
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.10';
|
package/package.json
CHANGED
package/src/cdk/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ export { SandboxDisableDeletionProtection } from './mixins.js';
|
|
|
22
22
|
export { registerConfig, finalizeConfigRegistry } from './config-registry.js';
|
|
23
23
|
export { synthGuard } from './synth-guard.js';
|
|
24
24
|
export type { ScopeOptions } from '../index.js';
|
|
25
|
-
export { ApiError, isBlocksError } from '../errors.js';
|
|
25
|
+
export { ApiError, isBlocksError, hasAuthError, DEFAULT_API_ERROR_NAME } from '../errors.js';
|
|
26
26
|
|
|
27
27
|
export class BlocksStack extends cdk.Stack implements BaseBlocksStack {
|
|
28
28
|
public readonly id: string;
|
package/src/client/index.ts
CHANGED
|
@@ -316,5 +316,5 @@ export function ApiNamespaceClient<T extends Record<string, (...args: any[]) =>
|
|
|
316
316
|
});
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
-
export { ApiError, isBlocksError } from '../errors.js';
|
|
319
|
+
export { ApiError, isBlocksError, hasAuthError, DEFAULT_API_ERROR_NAME } from '../errors.js';
|
|
320
320
|
export { Scope, type ScopeOptions } from '../common/index.js';
|
package/src/db-naming.test.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import { test, describe } from 'node:test';
|
|
4
|
+
import { test, describe, afterEach } from 'node:test';
|
|
5
5
|
import assert from 'node:assert';
|
|
6
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
6
9
|
import { extractDbRef, dbConnectionParameterName } from './db-naming.js';
|
|
10
|
+
import { getStackName } from './scripts/stack-id.js';
|
|
7
11
|
|
|
8
12
|
describe('extractDbRef', () => {
|
|
9
13
|
test('pooler form (postgres.{ref}@) yields ref', () => {
|
|
@@ -39,10 +43,51 @@ describe('extractDbRef', () => {
|
|
|
39
43
|
});
|
|
40
44
|
|
|
41
45
|
describe('dbConnectionParameterName', () => {
|
|
42
|
-
test('
|
|
43
|
-
assert.strictEqual(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
test('formats stack name into parameter path', () => {
|
|
47
|
+
assert.strictEqual(dbConnectionParameterName('my-app-k7x2mf-prod'), '/my-app-k7x2mf-prod-db-url');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('two distinct stack names produce distinct parameter names', () => {
|
|
51
|
+
assert.notStrictEqual(
|
|
52
|
+
dbConnectionParameterName('app-a-111111-prod'),
|
|
53
|
+
dbConnectionParameterName('app-b-222222-prod'),
|
|
46
54
|
);
|
|
47
55
|
});
|
|
48
56
|
});
|
|
57
|
+
|
|
58
|
+
describe('cross-site invariant: write name == read name', () => {
|
|
59
|
+
let tmpDir: string;
|
|
60
|
+
let originalCwd: string;
|
|
61
|
+
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
if (originalCwd) process.chdir(originalCwd);
|
|
64
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function setupProject(stackId: string, sandboxId: string): string {
|
|
68
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'cross-site-'));
|
|
69
|
+
mkdirSync(join(tmpDir, '.blocks'), { recursive: true });
|
|
70
|
+
writeFileSync(join(tmpDir, '.blocks', 'config.json'), JSON.stringify({ stackId }));
|
|
71
|
+
mkdirSync(join(tmpDir, '.blocks-sandbox'), { recursive: true });
|
|
72
|
+
writeFileSync(join(tmpDir, '.blocks-sandbox', 'sandbox-id.txt'), sandboxId);
|
|
73
|
+
return tmpDir;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test('write-side name == read-side name (sandbox)', () => {
|
|
77
|
+
const root = setupProject('my-app-k7x2mf', 'alice-0d7e1c');
|
|
78
|
+
const writeName = dbConnectionParameterName(getStackName({ sandbox: true, projectRoot: root }));
|
|
79
|
+
originalCwd = process.cwd();
|
|
80
|
+
process.chdir(root);
|
|
81
|
+
const readName = dbConnectionParameterName(getStackName({ sandbox: true }));
|
|
82
|
+
assert.strictEqual(writeName, readName);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('write-side name == read-side name (production)', () => {
|
|
86
|
+
const root = setupProject('my-app-k7x2mf', 'alice-0d7e1c');
|
|
87
|
+
const writeName = dbConnectionParameterName(getStackName({ sandbox: false, projectRoot: root }));
|
|
88
|
+
originalCwd = process.cwd();
|
|
89
|
+
process.chdir(root);
|
|
90
|
+
const readName = dbConnectionParameterName(getStackName({ sandbox: false }));
|
|
91
|
+
assert.strictEqual(writeName, readName);
|
|
92
|
+
});
|
|
93
|
+
});
|
package/src/db-naming.ts
CHANGED
|
@@ -6,9 +6,15 @@
|
|
|
6
6
|
* database connection string, and for the project ref derived from a Postgres
|
|
7
7
|
* connection string.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The connection-string parameter name is **stack-scoped** (embeds the
|
|
10
|
+
* deployment's stack name), so two Blocks apps in the same account + region +
|
|
11
|
+
* stage get distinct names and cannot overwrite each other's credentials.
|
|
12
|
+
*
|
|
13
|
+
* Two call sites compute the name: the pre-deploy writer (`ensure-secrets`) and
|
|
14
|
+
* the `db pull` generated wiring at synth. Both pass the result of
|
|
15
|
+
* `getStackName({ sandbox, projectRoot })` into this function, so they produce
|
|
16
|
+
* the same name by construction. The runtime Lambda does not call this function;
|
|
17
|
+
* it reads the name recorded at synth.
|
|
12
18
|
*/
|
|
13
19
|
|
|
14
20
|
/**
|
|
@@ -35,7 +41,13 @@ export function extractDbRef(connectionString: string): string {
|
|
|
35
41
|
throw new Error('Cannot extract database identifier from connection string.');
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
/**
|
|
45
|
+
* SSM SecureString parameter name for a deployment's external database
|
|
46
|
+
* connection string.
|
|
47
|
+
*
|
|
48
|
+
* Pure string transform: `/<stackName>-db-url`. The caller is responsible for
|
|
49
|
+
* computing the stack name via `getStackName({ sandbox, projectRoot })`.
|
|
50
|
+
*/
|
|
51
|
+
export function dbConnectionParameterName(stackName: string): string {
|
|
52
|
+
return `/${stackName}-db-url`;
|
|
41
53
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { describe, it } from 'node:test';
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
import { ApiError, isBlocksError, hasAuthError } from './errors.js';
|
|
7
|
+
|
|
8
|
+
describe('isBlocksError', () => {
|
|
9
|
+
it('matches a thrown ApiError by name', () => {
|
|
10
|
+
const e = new ApiError('nope', 401, { name: 'InvalidCredentialsException' });
|
|
11
|
+
assert.ok(isBlocksError(e, 'InvalidCredentialsException'));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('does not match a different name', () => {
|
|
15
|
+
const e = new ApiError('nope', 401, { name: 'InvalidCredentialsException' });
|
|
16
|
+
assert.ok(!isBlocksError(e, 'SomeOtherException'));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('does not match a plain object (not an Error)', () => {
|
|
20
|
+
assert.ok(!isBlocksError({ name: 'InvalidCredentialsException' }, 'InvalidCredentialsException'));
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('hasAuthError', () => {
|
|
25
|
+
it('matches a state carrying the given errorName', () => {
|
|
26
|
+
const state = { state: 'signedOut', errorName: 'InvalidCredentialsException' } as const;
|
|
27
|
+
assert.ok(hasAuthError(state, 'InvalidCredentialsException'));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('does not match a different errorName', () => {
|
|
31
|
+
const state = { errorName: 'InvalidCredentialsException' };
|
|
32
|
+
assert.ok(!hasAuthError(state, 'UserAlreadyExistsException'));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('does not match a state with no errorName', () => {
|
|
36
|
+
const state: { errorName?: string } = {};
|
|
37
|
+
assert.ok(!hasAuthError(state, 'InvalidCredentialsException'));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('is safe on null / undefined', () => {
|
|
41
|
+
assert.ok(!hasAuthError(null, 'InvalidCredentialsException'));
|
|
42
|
+
assert.ok(!hasAuthError(undefined, 'InvalidCredentialsException'));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('narrows the errorName to the matched literal', () => {
|
|
46
|
+
const state: { errorName?: string } = { errorName: 'InvalidCredentialsException' };
|
|
47
|
+
if (hasAuthError(state, 'InvalidCredentialsException')) {
|
|
48
|
+
// Type-level: state.errorName is narrowed to the literal.
|
|
49
|
+
const name: 'InvalidCredentialsException' = state.errorName;
|
|
50
|
+
assert.strictEqual(name, 'InvalidCredentialsException');
|
|
51
|
+
} else {
|
|
52
|
+
assert.fail('expected match');
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
package/src/errors.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The `name` an `ApiError` falls back to when no structured error name is
|
|
6
|
+
* given. A name equal to this carries no BB-level meaning, so consumers
|
|
7
|
+
* branching on the structured identity should treat it as "no name".
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_API_ERROR_NAME = 'ApiError';
|
|
10
|
+
|
|
4
11
|
/**
|
|
5
12
|
* Error subclass for errors that cross the wire between server and client.
|
|
6
13
|
*
|
|
@@ -46,7 +53,7 @@ export class ApiError extends Error {
|
|
|
46
53
|
|
|
47
54
|
constructor(message: string, status: number, options?: { name?: string; cause?: unknown; retriable?: boolean }) {
|
|
48
55
|
super(message, options?.cause ? { cause: options.cause } : undefined);
|
|
49
|
-
this.name = options?.name ??
|
|
56
|
+
this.name = options?.name ?? DEFAULT_API_ERROR_NAME;
|
|
50
57
|
this.status = status;
|
|
51
58
|
this.retriable = options?.retriable ?? false;
|
|
52
59
|
}
|
|
@@ -70,3 +77,27 @@ export class ApiError extends Error {
|
|
|
70
77
|
export function isBlocksError<N extends string>(e: unknown, name: N): e is Error & { name: N } {
|
|
71
78
|
return e instanceof Error && e.name === name;
|
|
72
79
|
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Type guard for branching on a failed `AuthState` (the recommended
|
|
83
|
+
* `setAuthState` client path) by its structured `errorName`.
|
|
84
|
+
*
|
|
85
|
+
* The returned state is a plain object, not a thrown `Error`, so
|
|
86
|
+
* `isBlocksError` does not apply — use this on the value returned by
|
|
87
|
+
* `setAuthState`/`getAuthState`. Match on the BB error constant, never on
|
|
88
|
+
* the human-facing `error` string.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* const next = await authApi.setAuthState({ action: 'signIn', username, password });
|
|
93
|
+
* if (hasAuthError(next, AuthBasicErrors.InvalidCredentials)) {
|
|
94
|
+
* // unknown user → fall back to sign-up
|
|
95
|
+
* }
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export function hasAuthError<T extends { errorName?: string }, N extends string>(
|
|
99
|
+
state: T | null | undefined,
|
|
100
|
+
name: N,
|
|
101
|
+
): state is T & { errorName: N } {
|
|
102
|
+
return state?.errorName === name;
|
|
103
|
+
}
|