Compare commits

...

2 Commits

Author SHA1 Message Date
fdc2420238 5.3.3
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Successful in 1m3s
Default (tags) / release (push) Failing after 1m4s
Default (tags) / metadata (push) Successful in 1m13s
2025-08-16 13:15:32 +00:00
e3a76ca577 fix(appdata): Redact sensitive values in AppData logs and add redaction tests 2025-08-16 13:15:32 +00:00
6 changed files with 161 additions and 22 deletions

View File

@@ -1,5 +1,14 @@
# Changelog # Changelog
## 2025-08-16 - 5.3.3 - fix(appdata)
Redact sensitive values in AppData logs and add redaction tests
- Add redactSensitiveValue helper in AppData to mask secrets (API keys, tokens, passwords, JWTs, etc.) during logging.
- Use redaction when logging raw and final mapping values in processMappingValue and nested key logging to avoid leaking sensitive data.
- Improve log output for long or special values (JWT/base64 detection, length-aware previews) while preserving actual stored values.
- Add test/test.redaction.ts to verify sensitive environment values are redacted in console output but still stored correctly in the kv store.
- Add local config .claude/settings.local.json (editor/CI permissions/settings).
## 2025-08-16 - 5.3.2 - fix(dependencies) ## 2025-08-16 - 5.3.2 - fix(dependencies)
Bump @push.rocks/qenv to ^6.1.3 and add local Claude settings Bump @push.rocks/qenv to ^6.1.3 and add local Claude settings

View File

@@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/npmextra", "name": "@push.rocks/npmextra",
"version": "5.3.2", "version": "5.3.3",
"private": false, "private": false,
"description": "A utility to enhance npm with additional configuration, tool management capabilities, and a key-value store for project setups.", "description": "A utility to enhance npm with additional configuration, tool management capabilities, and a key-value store for project setups.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",

30
pnpm-lock.yaml generated
View File

@@ -244,8 +244,8 @@ packages:
'@borewit/text-codec@0.1.1': '@borewit/text-codec@0.1.1':
resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
'@cloudflare/workers-types@4.20250813.0': '@cloudflare/workers-types@4.20250816.0':
resolution: {integrity: sha512-RFFjomDndGR+p7ug1HWDlW21qOJyRZbmI99dUtuR9tmwJbSZhUUnSFmzok9lBYVfkMMrO1O5vmB+IlgiecgLEA==} resolution: {integrity: sha512-R9ADrrINo1CqTwCddH39Tjlsc3grim6KeO7l8yddNbldH3uTkaAXYCzO0WiyLG7irLzLDrZVc4tLhN6BO3tdFw==}
'@colors/colors@1.6.0': '@colors/colors@1.6.0':
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
@@ -722,8 +722,8 @@ packages:
'@push.rocks/smarts3@2.2.5': '@push.rocks/smarts3@2.2.5':
resolution: {integrity: sha512-OZjD0jBCUTJCLnwraxBcyZ3he5buXf2OEM1zipiTBChA2EcKUZWKk/a6KR5WT+NlFCIIuB23UG+U+cxsIWM91Q==} resolution: {integrity: sha512-OZjD0jBCUTJCLnwraxBcyZ3he5buXf2OEM1zipiTBChA2EcKUZWKk/a6KR5WT+NlFCIIuB23UG+U+cxsIWM91Q==}
'@push.rocks/smartshell@3.2.3': '@push.rocks/smartshell@3.2.4':
resolution: {integrity: sha512-BWA/DH1H9lG7Er23d4uYgirfYaya5dX4g/WpWm2la7mOzuL9o2FnPIhel52DQUKIh7ty3Ql305ApV8YaAb4+/w==} resolution: {integrity: sha512-zZEKfRl3qBaII9BJULk4rB/+EelUpgM2/qHCQLui7c4589HTge4o0nWn+olFw/Hge88qUO77OK1sN7hQFZ6zeg==}
'@push.rocks/smartsitemap@2.0.3': '@push.rocks/smartsitemap@2.0.3':
resolution: {integrity: sha512-jIcms8V1b2mt3dS4PKNlLR1DRC8pCDWMRVbnyM/2+snZOJZonQRlQzAyX8No0EfLbfdrfnxv2IjPX13X29Re6g==} resolution: {integrity: sha512-jIcms8V1b2mt3dS4PKNlLR1DRC8pCDWMRVbnyM/2+snZOJZonQRlQzAyX8No0EfLbfdrfnxv2IjPX13X29Re6g==}
@@ -734,8 +734,8 @@ packages:
'@push.rocks/smartspawn@3.0.3': '@push.rocks/smartspawn@3.0.3':
resolution: {integrity: sha512-DyrGPV69wwOiJgKkyruk5hS3UEGZ99xFAqBE9O2nM8VXCRLbbty3xt1Ug5Z092ZZmJYaaGMSnMw3ijyZJFCT0Q==} resolution: {integrity: sha512-DyrGPV69wwOiJgKkyruk5hS3UEGZ99xFAqBE9O2nM8VXCRLbbty3xt1Ug5Z092ZZmJYaaGMSnMw3ijyZJFCT0Q==}
'@push.rocks/smartstate@2.0.25': '@push.rocks/smartstate@2.0.26':
resolution: {integrity: sha512-gWmbDCx5esezHDQnD2nOClxeTiWtvU1wEdP0XbheCcXzaGEv0H8apRjQBksRZJd9FC3ezrJU00GLh0eH9rPyMQ==} resolution: {integrity: sha512-lMcf0ZWWs9jej9wjapuonuIZiQNiD9NcAcvRDFXq7GtQf/HUyr6zr5K1XxGZaCIGyYrbYnBHBpNU+8DBoarHrA==}
'@push.rocks/smartstream@2.0.8': '@push.rocks/smartstream@2.0.8':
resolution: {integrity: sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==} resolution: {integrity: sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==}
@@ -3890,7 +3890,7 @@ snapshots:
'@api.global/typedrequest': 3.1.10 '@api.global/typedrequest': 3.1.10
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 3.0.1 '@api.global/typedsocket': 3.0.1
'@cloudflare/workers-types': 4.20250813.0 '@cloudflare/workers-types': 4.20250816.0
'@design.estate/dees-comms': 1.0.27 '@design.estate/dees-comms': 1.0.27
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartchok': 1.1.1 '@push.rocks/smartchok': 1.1.1
@@ -4511,7 +4511,7 @@ snapshots:
'@borewit/text-codec@0.1.1': {} '@borewit/text-codec@0.1.1': {}
'@cloudflare/workers-types@4.20250813.0': {} '@cloudflare/workers-types@4.20250816.0': {}
'@colors/colors@1.6.0': {} '@colors/colors@1.6.0': {}
@@ -4543,7 +4543,7 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrouter': 1.3.3 '@push.rocks/smartrouter': 1.3.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smartstate': 2.0.25 '@push.rocks/smartstate': 2.0.26
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.0.15
'@push.rocks/smarturl': 3.1.0 '@push.rocks/smarturl': 3.1.0
'@push.rocks/webrequest': 3.0.37 '@push.rocks/webrequest': 3.0.37
@@ -4709,14 +4709,14 @@ snapshots:
'@push.rocks/smartnpm': 2.0.4 '@push.rocks/smartnpm': 2.0.4
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartrequest': 4.2.1 '@push.rocks/smartrequest': 4.2.1
'@push.rocks/smartshell': 3.2.3 '@push.rocks/smartshell': 3.2.4
transitivePeerDependencies: transitivePeerDependencies:
- aws-crt - aws-crt
'@git.zone/tsrun@1.3.3': '@git.zone/tsrun@1.3.3':
dependencies: dependencies:
'@push.rocks/smartfile': 11.2.5 '@push.rocks/smartfile': 11.2.5
'@push.rocks/smartshell': 3.2.3 '@push.rocks/smartshell': 3.2.4
tsx: 4.20.4 tsx: 4.20.4
'@git.zone/tstest@2.3.2(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7)(typescript@5.8.3)': '@git.zone/tstest@2.3.2(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7)(typescript@5.8.3)':
@@ -4740,7 +4740,7 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 2.1.0 '@push.rocks/smartrequest': 2.1.0
'@push.rocks/smarts3': 2.2.5 '@push.rocks/smarts3': 2.2.5
'@push.rocks/smartshell': 3.2.3 '@push.rocks/smartshell': 3.2.4
'@push.rocks/smarttime': 4.1.1 '@push.rocks/smarttime': 4.1.1
'@types/ws': 8.18.1 '@types/ws': 8.18.1
figures: 6.1.0 figures: 6.1.0
@@ -5319,7 +5319,7 @@ snapshots:
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.8.3)': '@push.rocks/smartpuppeteer@2.0.5(typescript@5.8.3)':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartshell': 3.2.3 '@push.rocks/smartshell': 3.2.4
puppeteer: 24.16.2(typescript@5.8.3) puppeteer: 24.16.2(typescript@5.8.3)
tree-kill: 1.2.2 tree-kill: 1.2.2
transitivePeerDependencies: transitivePeerDependencies:
@@ -5368,7 +5368,7 @@ snapshots:
- aws-crt - aws-crt
- supports-color - supports-color
'@push.rocks/smartshell@3.2.3': '@push.rocks/smartshell@3.2.4':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartexit': 1.0.23 '@push.rocks/smartexit': 1.0.23
@@ -5420,7 +5420,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@push.rocks/smartstate@2.0.25': '@push.rocks/smartstate@2.0.26':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smarthash': 3.2.3 '@push.rocks/smarthash': 3.2.3

85
test/test.redaction.ts Normal file
View File

@@ -0,0 +1,85 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as npmextra from '../ts/index.js';
// Test that sensitive values are properly redacted in logs
tap.test('should redact sensitive values in console output', async () => {
// Capture console.log output
const originalLog = console.log;
const logOutput: string[] = [];
console.log = (...args: any[]) => {
logOutput.push(args.join(' '));
};
try {
// Set up environment variables with sensitive data
process.env['API_KEY'] = 'super-secret-api-key-12345';
process.env['DATABASE_PASSWORD'] = 'myP@ssw0rd123';
process.env['AUTH_TOKEN'] = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ';
process.env['PUBLIC_URL'] = 'https://example.com';
process.env['DEBUG_MODE'] = 'true';
// Create AppData with sensitive environment mappings
const appData = await npmextra.AppData.createAndInit({
ephemeral: true,
envMapping: {
apiKey: 'API_KEY',
dbPassword: 'DATABASE_PASSWORD',
authToken: 'AUTH_TOKEN',
publicUrl: 'PUBLIC_URL',
debugMode: 'boolean:DEBUG_MODE',
nestedConfig: {
secretKey: 'API_KEY',
nonSecret: 'PUBLIC_URL'
}
}
});
// Restore console.log
console.log = originalLog;
// Check that sensitive values were redacted in logs
const combinedOutput = logOutput.join('\n');
// API_KEY should be redacted
expect(combinedOutput).toContain('sup...[26 chars]');
expect(combinedOutput).not.toContain('super-secret-api-key-12345');
// DATABASE_PASSWORD should be redacted
expect(combinedOutput).toContain('myP...[13 chars]');
expect(combinedOutput).not.toContain('myP@ssw0rd123');
// AUTH_TOKEN should be redacted (JWT tokens starting with eyJ)
expect(combinedOutput).toContain('eyJ...[');
expect(combinedOutput).not.toContain('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9');
// PUBLIC_URL should not be redacted (not sensitive)
expect(combinedOutput).toContain('https://example.com');
// DEBUG_MODE should not be redacted (not sensitive)
expect(combinedOutput).toContain('true');
// Verify data is still stored correctly (not redacted in actual storage)
const kvStore = await appData.getKvStore();
const apiKey = await kvStore.readKey('apiKey');
const dbPassword = await kvStore.readKey('dbPassword');
const publicUrl = await kvStore.readKey('publicUrl');
// Actual values should be stored correctly
expect(apiKey).toEqual('super-secret-api-key-12345');
expect(dbPassword).toEqual('myP@ssw0rd123');
expect(publicUrl).toEqual('https://example.com');
} finally {
// Restore console.log in case of test failure
console.log = originalLog;
// Clean up environment variables
delete process.env['API_KEY'];
delete process.env['DATABASE_PASSWORD'];
delete process.env['AUTH_TOKEN'];
delete process.env['PUBLIC_URL'];
delete process.env['DEBUG_MODE'];
}
});
export default tap.start();

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/npmextra', name: '@push.rocks/npmextra',
version: '5.3.2', version: '5.3.3',
description: 'A utility to enhance npm with additional configuration, tool management capabilities, and a key-value store for project setups.' description: 'A utility to enhance npm with additional configuration, tool management capabilities, and a key-value store for project setups.'
} }

View File

@@ -16,6 +16,48 @@ function getQenv(): plugins.qenv.Qenv {
return sharedQenv; return sharedQenv;
} }
// ============================================================================
// Security - Redaction for sensitive data
// ============================================================================
/**
* Redacts sensitive values in logs to prevent exposure of secrets
*/
function redactSensitiveValue(key: string, value: unknown): string {
// List of patterns that indicate sensitive data
const sensitivePatterns = [
/secret/i, /token/i, /key/i, /password/i, /pass/i,
/api/i, /credential/i, /auth/i, /private/i, /jwt/i,
/cert/i, /signature/i, /bearer/i
];
// Check if key contains sensitive pattern
const isSensitive = sensitivePatterns.some(pattern => pattern.test(key));
if (isSensitive) {
if (typeof value === 'string') {
// Show first 3 chars and length for debugging
return value.length > 3
? `${value.substring(0, 3)}...[${value.length} chars]`
: '[redacted]';
}
return '[redacted]';
}
// Check if value looks like a JWT token or base64 secret
if (typeof value === 'string') {
// JWT tokens start with eyJ
if (value.startsWith('eyJ')) {
return `eyJ...[${value.length} chars]`;
}
// Very long strings might be encoded secrets
if (value.length > 100) {
return `${value.substring(0, 50)}...[${value.length} chars total]`;
}
}
return JSON.stringify(value);
}
// ============================================================================ // ============================================================================
// Type Converters - Centralized conversion logic // Type Converters - Centralized conversion logic
// ============================================================================ // ============================================================================
@@ -210,12 +252,14 @@ function applyTransforms(value: unknown, transforms: Transform[]): unknown {
*/ */
async function processMappingValue(mappingString: string): Promise<unknown> { async function processMappingValue(mappingString: string): Promise<unknown> {
const spec = parseMappingSpec(mappingString); const spec = parseMappingSpec(mappingString);
const keyName = spec.source.type === 'env' ? spec.source.key : 'hardcoded';
console.log(` 🔍 Processing mapping: "${mappingString}"`); console.log(` 🔍 Processing mapping: "${mappingString}"`);
console.log(` Source: ${spec.source.type === 'env' ? `env:${spec.source.key}` : `hard:${spec.source.value}`}`); console.log(` Source: ${spec.source.type === 'env' ? `env:${spec.source.key}` : `hard:${spec.source.value}`}`);
console.log(` Transforms: ${spec.transforms.length > 0 ? spec.transforms.join(', ') : 'none'}`); console.log(` Transforms: ${spec.transforms.length > 0 ? spec.transforms.join(', ') : 'none'}`);
const rawValue = await resolveSource(spec.source); const rawValue = await resolveSource(spec.source);
console.log(` Raw value: ${JSON.stringify(rawValue)} (type: ${typeof rawValue})`); console.log(` Raw value: ${redactSensitiveValue(keyName, rawValue)} (type: ${typeof rawValue})`);
if (rawValue === undefined || rawValue === null) { if (rawValue === undefined || rawValue === null) {
console.log(` ⚠️ Raw value is undefined/null, returning undefined`); console.log(` ⚠️ Raw value is undefined/null, returning undefined`);
@@ -223,7 +267,7 @@ async function processMappingValue(mappingString: string): Promise<unknown> {
} }
const result = applyTransforms(rawValue, spec.transforms); const result = applyTransforms(rawValue, spec.transforms);
console.log(` Final value: ${JSON.stringify(result)} (type: ${typeof result})`); console.log(` Final value: ${redactSensitiveValue(keyName, result)} (type: ${typeof result})`);
return result; return result;
} }
@@ -253,7 +297,7 @@ async function evaluateMappingValue(mappingValue: any): Promise<any> {
// Only skip if explicitly undefined // Only skip if explicitly undefined
if (evaluated !== undefined) { if (evaluated !== undefined) {
result[key] = evaluated; result[key] = evaluated;
console.log(` ✓ Nested key "${key}" = ${JSON.stringify(evaluated)} (type: ${typeof evaluated})`); console.log(` ✓ Nested key "${key}" = ${redactSensitiveValue(key, evaluated)} (type: ${typeof evaluated})`);
} else { } else {
console.log(` ⚠️ Nested key "${key}" evaluated to undefined, skipping`); console.log(` ⚠️ Nested key "${key}" evaluated to undefined, skipping`);
} }
@@ -262,7 +306,8 @@ async function evaluateMappingValue(mappingValue: any): Promise<any> {
} }
// For any other type (numbers, booleans, etc.), return as-is // For any other type (numbers, booleans, etc.), return as-is
console.log(` 📎 Returning value as-is: ${JSON.stringify(mappingValue)} (type: ${typeof mappingValue})`); // Note: We don't have key context here, so we'll just indicate the type
console.log(` 📎 Returning value as-is: [value] (type: ${typeof mappingValue})`);
return mappingValue; return mappingValue;
} }
@@ -434,7 +479,7 @@ export class AppData<T = any> {
const valuePreview = evaluated === null ? 'null' : const valuePreview = evaluated === null ? 'null' :
typeof evaluated === 'object' ? typeof evaluated === 'object' ?
(Array.isArray(evaluated) ? `[${evaluated.length} items]` : `{${Object.keys(evaluated).length} keys}`) : (Array.isArray(evaluated) ? `[${evaluated.length} items]` : `{${Object.keys(evaluated).length} keys}`) :
JSON.stringify(evaluated); redactSensitiveValue(key, evaluated);
console.log(` ✅ Successfully processed key "${key}" = ${valuePreview} (type: ${valueType})`); console.log(` ✅ Successfully processed key "${key}" = ${valuePreview} (type: ${valueType})`);
} else { } else {
console.log(` ⚠️ Key "${key}" evaluated to undefined, skipping`); console.log(` ⚠️ Key "${key}" evaluated to undefined, skipping`);