@aws-blocks/core 0.1.12 → 0.1.13
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/redact.d.ts +3 -2
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +4 -3
- package/dist/redact.test.js +9 -0
- package/dist/scripts/dev-server-rpc.test.d.ts +2 -0
- package/dist/scripts/dev-server-rpc.test.d.ts.map +1 -0
- package/dist/scripts/dev-server-rpc.test.js +107 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/redact.test.ts +12 -0
- package/src/redact.ts +4 -3
- package/src/scripts/dev-server-rpc.test.ts +115 -0
- package/src/version.ts +1 -1
package/dist/redact.d.ts
CHANGED
|
@@ -44,8 +44,9 @@ export declare const REDACTED: "[REDACTED]";
|
|
|
44
44
|
export declare function redactForLogging(value: unknown, seen?: WeakSet<object>): unknown;
|
|
45
45
|
/**
|
|
46
46
|
* Convenience for log call sites: redact `value` and serialize it to a JSON
|
|
47
|
-
* string. Returns
|
|
48
|
-
*
|
|
47
|
+
* string. Returns `'undefined'` when JSON serialization produces no output,
|
|
48
|
+
* or a safe placeholder if serialization throws (e.g. a BigInt slips through),
|
|
49
|
+
* so logging can never crash a request.
|
|
49
50
|
*/
|
|
50
51
|
export declare function redactToJson(value: unknown): string;
|
|
51
52
|
//# sourceMappingURL=redact.d.ts.map
|
package/dist/redact.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,sDAAsD;AACtD,eAAO,MAAM,QAAQ,EAAG,YAAqB,CAAC;AAwC9C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,GAAE,OAAO,CAAC,MAAM,CAAiB,GAAG,OAAO,CA2C/F;AAED
|
|
1
|
+
{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,sDAAsD;AACtD,eAAO,MAAM,QAAQ,EAAG,YAAqB,CAAC;AAwC9C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,GAAE,OAAO,CAAC,MAAM,CAAiB,GAAG,OAAO,CA2C/F;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAMnD"}
|
package/dist/redact.js
CHANGED
|
@@ -121,12 +121,13 @@ export function redactForLogging(value, seen = new WeakSet()) {
|
|
|
121
121
|
}
|
|
122
122
|
/**
|
|
123
123
|
* Convenience for log call sites: redact `value` and serialize it to a JSON
|
|
124
|
-
* string. Returns
|
|
125
|
-
*
|
|
124
|
+
* string. Returns `'undefined'` when JSON serialization produces no output,
|
|
125
|
+
* or a safe placeholder if serialization throws (e.g. a BigInt slips through),
|
|
126
|
+
* so logging can never crash a request.
|
|
126
127
|
*/
|
|
127
128
|
export function redactToJson(value) {
|
|
128
129
|
try {
|
|
129
|
-
return JSON.stringify(redactForLogging(value));
|
|
130
|
+
return JSON.stringify(redactForLogging(value)) ?? 'undefined';
|
|
130
131
|
}
|
|
131
132
|
catch {
|
|
132
133
|
return '[unserializable]';
|
package/dist/redact.test.js
CHANGED
|
@@ -179,6 +179,15 @@ describe('redactToJson', () => {
|
|
|
179
179
|
// BigInt is not JSON-serializable and survives redaction (not a sensitive key).
|
|
180
180
|
assert.equal(redactToJson({ big: 10n }), '[unserializable]');
|
|
181
181
|
});
|
|
182
|
+
it('returns a string for undefined', () => {
|
|
183
|
+
assert.equal(redactToJson(undefined), 'undefined');
|
|
184
|
+
});
|
|
185
|
+
it('returns a string for function values', () => {
|
|
186
|
+
assert.equal(redactToJson(() => { }), 'undefined');
|
|
187
|
+
});
|
|
188
|
+
it('returns a string for symbol values', () => {
|
|
189
|
+
assert.equal(redactToJson(Symbol('x')), 'undefined');
|
|
190
|
+
});
|
|
182
191
|
it('does not leak secrets even when truncation would apply downstream', () => {
|
|
183
192
|
const json = redactToJson([{ action: 'signIn', username: 'u', password: 'p'.repeat(50) }]);
|
|
184
193
|
assert.ok(!json.includes('pppp'));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-server-rpc.test.d.ts","sourceRoot":"","sources":["../../src/scripts/dev-server-rpc.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { afterEach, describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
async function getAvailablePort() {
|
|
11
|
+
const server = createServer();
|
|
12
|
+
await new Promise((resolve, reject) => {
|
|
13
|
+
server.once('error', reject);
|
|
14
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
15
|
+
});
|
|
16
|
+
const address = server.address();
|
|
17
|
+
assert.ok(address && typeof address === 'object');
|
|
18
|
+
const { port } = address;
|
|
19
|
+
// TOCTOU: brief window between closing this probe and the dev server binding
|
|
20
|
+
// the port; port: 0 would require the dev server to expose its assigned port.
|
|
21
|
+
await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
|
|
22
|
+
return port;
|
|
23
|
+
}
|
|
24
|
+
describe('dev-server RPC integration', () => {
|
|
25
|
+
let devProcess = null;
|
|
26
|
+
let tempDir = null;
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
if (devProcess && devProcess.exitCode === null) {
|
|
29
|
+
devProcess.kill('SIGTERM');
|
|
30
|
+
await new Promise((resolve) => {
|
|
31
|
+
const timeout = setTimeout(() => {
|
|
32
|
+
devProcess?.kill('SIGKILL');
|
|
33
|
+
resolve();
|
|
34
|
+
}, 2_000);
|
|
35
|
+
devProcess?.once('exit', () => {
|
|
36
|
+
clearTimeout(timeout);
|
|
37
|
+
resolve();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (tempDir)
|
|
42
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
it('returns success for a void handler in verbose mode', async () => {
|
|
45
|
+
const port = await getAvailablePort();
|
|
46
|
+
tempDir = join(tmpdir(), `dev-rpc-test-${process.pid}-${Date.now()}`);
|
|
47
|
+
mkdirSync(tempDir, { recursive: true });
|
|
48
|
+
writeFileSync(join(tempDir, 'backend.ts'), `
|
|
49
|
+
export const testApi = {
|
|
50
|
+
pingVoid: async () => undefined,
|
|
51
|
+
};
|
|
52
|
+
`);
|
|
53
|
+
// Polyfill process.loadEnvFile for Node <20.6; ENOENT means no .env file.
|
|
54
|
+
writeFileSync(join(tempDir, 'preload.mjs'), `
|
|
55
|
+
if (!process.loadEnvFile) {
|
|
56
|
+
process.loadEnvFile = () => { throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); };
|
|
57
|
+
}
|
|
58
|
+
`);
|
|
59
|
+
writeFileSync(join(tempDir, 'run-dev.ts'), `
|
|
60
|
+
import { startDevServer } from '${join(__dirname, 'dev-server.js').replace(/\\/g, '/')}';
|
|
61
|
+
startDevServer({ backendPath: '${join(tempDir, 'backend.ts').replace(/\\/g, '/')}', port: ${port} });
|
|
62
|
+
`);
|
|
63
|
+
const tsxBin = join(__dirname, '..', '..', '..', '..', 'node_modules', '.bin', 'tsx');
|
|
64
|
+
devProcess = spawn(tsxBin, ['--import', join(tempDir, 'preload.mjs'), join(tempDir, 'run-dev.ts')], {
|
|
65
|
+
cwd: tempDir,
|
|
66
|
+
env: {
|
|
67
|
+
...process.env,
|
|
68
|
+
AWS_BLOCKS_DISABLE_TELEMETRY: '1',
|
|
69
|
+
// Empty is falsy, keeping verbose logging on even if the parent sets quiet mode.
|
|
70
|
+
BLOCKS_DEV_QUIET: '',
|
|
71
|
+
},
|
|
72
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
73
|
+
});
|
|
74
|
+
let stdout = '';
|
|
75
|
+
let stderr = '';
|
|
76
|
+
devProcess.stdout?.on('data', chunk => { stdout += chunk.toString(); });
|
|
77
|
+
devProcess.stderr?.on('data', chunk => { stderr += chunk.toString(); });
|
|
78
|
+
const deadline = Date.now() + 15_000;
|
|
79
|
+
let response;
|
|
80
|
+
let lastError;
|
|
81
|
+
while (!response && Date.now() < deadline) {
|
|
82
|
+
try {
|
|
83
|
+
response = await fetch(`http://127.0.0.1:${port}/aws-blocks/api`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({ jsonrpc: '2.0', method: 'testApi.pingVoid', params: [], id: 1 }),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
lastError = error;
|
|
91
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
assert.ok(response, `Dev server did not respond: ${String(lastError)}\nstdout: ${stdout}\nstderr: ${stderr}`);
|
|
95
|
+
assert.strictEqual(response.status, 200);
|
|
96
|
+
const payload = await response.json();
|
|
97
|
+
assert.strictEqual(payload.jsonrpc, '2.0');
|
|
98
|
+
assert.strictEqual(payload.id, 1);
|
|
99
|
+
assert.ok(!('error' in payload), `Unexpected RPC error: ${JSON.stringify(payload.error)}`);
|
|
100
|
+
const logDeadline = Date.now() + 1_000;
|
|
101
|
+
while (!stdout.includes('[rpc-ok] testApi.pingVoid') && Date.now() < logDeadline) {
|
|
102
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
103
|
+
}
|
|
104
|
+
assert.ok(stdout.includes('[rpc-ok] testApi.pingVoid'), `Missing verbose success log. stdout: ${stdout}`);
|
|
105
|
+
assert.ok(!stdout.includes('[rpc-err]'), `Unexpected RPC error log. stdout: ${stdout}`);
|
|
106
|
+
});
|
|
107
|
+
});
|
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.13";
|
|
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.13';
|
package/package.json
CHANGED
package/src/redact.test.ts
CHANGED
|
@@ -201,6 +201,18 @@ describe('redactToJson', () => {
|
|
|
201
201
|
assert.equal(redactToJson({ big: 10n }), '[unserializable]');
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
+
it('returns a string for undefined', () => {
|
|
205
|
+
assert.equal(redactToJson(undefined), 'undefined');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('returns a string for function values', () => {
|
|
209
|
+
assert.equal(redactToJson(() => {}), 'undefined');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('returns a string for symbol values', () => {
|
|
213
|
+
assert.equal(redactToJson(Symbol('x')), 'undefined');
|
|
214
|
+
});
|
|
215
|
+
|
|
204
216
|
it('does not leak secrets even when truncation would apply downstream', () => {
|
|
205
217
|
const json = redactToJson([{ action: 'signIn', username: 'u', password: 'p'.repeat(50) }]);
|
|
206
218
|
assert.ok(!json.includes('pppp'));
|
package/src/redact.ts
CHANGED
|
@@ -131,12 +131,13 @@ export function redactForLogging(value: unknown, seen: WeakSet<object> = new Wea
|
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
133
|
* Convenience for log call sites: redact `value` and serialize it to a JSON
|
|
134
|
-
* string. Returns
|
|
135
|
-
*
|
|
134
|
+
* string. Returns `'undefined'` when JSON serialization produces no output,
|
|
135
|
+
* or a safe placeholder if serialization throws (e.g. a BigInt slips through),
|
|
136
|
+
* so logging can never crash a request.
|
|
136
137
|
*/
|
|
137
138
|
export function redactToJson(value: unknown): string {
|
|
138
139
|
try {
|
|
139
|
-
return JSON.stringify(redactForLogging(value));
|
|
140
|
+
return JSON.stringify(redactForLogging(value)) ?? 'undefined';
|
|
140
141
|
} catch {
|
|
141
142
|
return '[unserializable]';
|
|
142
143
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { afterEach, describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
async function getAvailablePort(): Promise<number> {
|
|
13
|
+
const server = createServer();
|
|
14
|
+
await new Promise<void>((resolve, reject) => {
|
|
15
|
+
server.once('error', reject);
|
|
16
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
17
|
+
});
|
|
18
|
+
const address = server.address();
|
|
19
|
+
assert.ok(address && typeof address === 'object');
|
|
20
|
+
const { port } = address;
|
|
21
|
+
// TOCTOU: brief window between closing this probe and the dev server binding
|
|
22
|
+
// the port; port: 0 would require the dev server to expose its assigned port.
|
|
23
|
+
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
|
|
24
|
+
return port;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('dev-server RPC integration', () => {
|
|
28
|
+
let devProcess: ChildProcess | null = null;
|
|
29
|
+
let tempDir: string | null = null;
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
if (devProcess && devProcess.exitCode === null) {
|
|
33
|
+
devProcess.kill('SIGTERM');
|
|
34
|
+
await new Promise<void>((resolve) => {
|
|
35
|
+
const timeout = setTimeout(() => {
|
|
36
|
+
devProcess?.kill('SIGKILL');
|
|
37
|
+
resolve();
|
|
38
|
+
}, 2_000);
|
|
39
|
+
devProcess?.once('exit', () => {
|
|
40
|
+
clearTimeout(timeout);
|
|
41
|
+
resolve();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (tempDir) rmSync(tempDir, { recursive: true, force: true });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns success for a void handler in verbose mode', async () => {
|
|
49
|
+
const port = await getAvailablePort();
|
|
50
|
+
tempDir = join(tmpdir(), `dev-rpc-test-${process.pid}-${Date.now()}`);
|
|
51
|
+
mkdirSync(tempDir, { recursive: true });
|
|
52
|
+
writeFileSync(join(tempDir, 'backend.ts'), `
|
|
53
|
+
export const testApi = {
|
|
54
|
+
pingVoid: async () => undefined,
|
|
55
|
+
};
|
|
56
|
+
`);
|
|
57
|
+
// Polyfill process.loadEnvFile for Node <20.6; ENOENT means no .env file.
|
|
58
|
+
writeFileSync(join(tempDir, 'preload.mjs'), `
|
|
59
|
+
if (!process.loadEnvFile) {
|
|
60
|
+
process.loadEnvFile = () => { throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }); };
|
|
61
|
+
}
|
|
62
|
+
`);
|
|
63
|
+
writeFileSync(join(tempDir, 'run-dev.ts'), `
|
|
64
|
+
import { startDevServer } from '${join(__dirname, 'dev-server.js').replace(/\\/g, '/')}';
|
|
65
|
+
startDevServer({ backendPath: '${join(tempDir, 'backend.ts').replace(/\\/g, '/')}', port: ${port} });
|
|
66
|
+
`);
|
|
67
|
+
|
|
68
|
+
const tsxBin = join(__dirname, '..', '..', '..', '..', 'node_modules', '.bin', 'tsx');
|
|
69
|
+
devProcess = spawn(tsxBin, ['--import', join(tempDir, 'preload.mjs'), join(tempDir, 'run-dev.ts')], {
|
|
70
|
+
cwd: tempDir,
|
|
71
|
+
env: {
|
|
72
|
+
...process.env,
|
|
73
|
+
AWS_BLOCKS_DISABLE_TELEMETRY: '1',
|
|
74
|
+
// Empty is falsy, keeping verbose logging on even if the parent sets quiet mode.
|
|
75
|
+
BLOCKS_DEV_QUIET: '',
|
|
76
|
+
},
|
|
77
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
let stdout = '';
|
|
81
|
+
let stderr = '';
|
|
82
|
+
devProcess.stdout?.on('data', chunk => { stdout += chunk.toString(); });
|
|
83
|
+
devProcess.stderr?.on('data', chunk => { stderr += chunk.toString(); });
|
|
84
|
+
|
|
85
|
+
const deadline = Date.now() + 15_000;
|
|
86
|
+
let response: Response | undefined;
|
|
87
|
+
let lastError: unknown;
|
|
88
|
+
while (!response && Date.now() < deadline) {
|
|
89
|
+
try {
|
|
90
|
+
response = await fetch(`http://127.0.0.1:${port}/aws-blocks/api`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
body: JSON.stringify({ jsonrpc: '2.0', method: 'testApi.pingVoid', params: [], id: 1 }),
|
|
94
|
+
});
|
|
95
|
+
} catch (error) {
|
|
96
|
+
lastError = error;
|
|
97
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
assert.ok(response, `Dev server did not respond: ${String(lastError)}\nstdout: ${stdout}\nstderr: ${stderr}`);
|
|
102
|
+
assert.strictEqual(response.status, 200);
|
|
103
|
+
const payload = await response.json() as Record<string, unknown>;
|
|
104
|
+
assert.strictEqual(payload.jsonrpc, '2.0');
|
|
105
|
+
assert.strictEqual(payload.id, 1);
|
|
106
|
+
assert.ok(!('error' in payload), `Unexpected RPC error: ${JSON.stringify(payload.error)}`);
|
|
107
|
+
|
|
108
|
+
const logDeadline = Date.now() + 1_000;
|
|
109
|
+
while (!stdout.includes('[rpc-ok] testApi.pingVoid') && Date.now() < logDeadline) {
|
|
110
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
111
|
+
}
|
|
112
|
+
assert.ok(stdout.includes('[rpc-ok] testApi.pingVoid'), `Missing verbose success log. stdout: ${stdout}`);
|
|
113
|
+
assert.ok(!stdout.includes('[rpc-err]'), `Unexpected RPC error log. stdout: ${stdout}`);
|
|
114
|
+
});
|
|
115
|
+
});
|
package/src/version.ts
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.13';
|