@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 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 a safe placeholder instead of throwing if serialization
48
- * fails (e.g. a BigInt slips through), so logging can never crash a request.
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
@@ -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;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAMnD"}
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 a safe placeholder instead of throwing if serialization
125
- * fails (e.g. a BigInt slips through), so logging can never crash a request.
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]';
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=dev-server-rpc.test.d.ts.map
@@ -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.12";
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.12';
2
+ export const CORE_VERSION = '0.1.13';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws-blocks/core",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "author": "Amazon Web Services",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -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 a safe placeholder instead of throwing if serialization
135
- * fails (e.g. a BigInt slips through), so logging can never crash a request.
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.12';
2
+ export const CORE_VERSION = '0.1.13';