Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
fdc2420238 | |||
e3a76ca577 |
@@ -1,5 +1,14 @@
|
||||
# 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)
|
||||
Bump @push.rocks/qenv to ^6.1.3 and add local Claude settings
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@push.rocks/npmextra",
|
||||
"version": "5.3.2",
|
||||
"version": "5.3.3",
|
||||
"private": false,
|
||||
"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",
|
||||
|
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@@ -244,8 +244,8 @@ packages:
|
||||
'@borewit/text-codec@0.1.1':
|
||||
resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
|
||||
|
||||
'@cloudflare/workers-types@4.20250813.0':
|
||||
resolution: {integrity: sha512-RFFjomDndGR+p7ug1HWDlW21qOJyRZbmI99dUtuR9tmwJbSZhUUnSFmzok9lBYVfkMMrO1O5vmB+IlgiecgLEA==}
|
||||
'@cloudflare/workers-types@4.20250816.0':
|
||||
resolution: {integrity: sha512-R9ADrrINo1CqTwCddH39Tjlsc3grim6KeO7l8yddNbldH3uTkaAXYCzO0WiyLG7irLzLDrZVc4tLhN6BO3tdFw==}
|
||||
|
||||
'@colors/colors@1.6.0':
|
||||
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
|
||||
@@ -722,8 +722,8 @@ packages:
|
||||
'@push.rocks/smarts3@2.2.5':
|
||||
resolution: {integrity: sha512-OZjD0jBCUTJCLnwraxBcyZ3he5buXf2OEM1zipiTBChA2EcKUZWKk/a6KR5WT+NlFCIIuB23UG+U+cxsIWM91Q==}
|
||||
|
||||
'@push.rocks/smartshell@3.2.3':
|
||||
resolution: {integrity: sha512-BWA/DH1H9lG7Er23d4uYgirfYaya5dX4g/WpWm2la7mOzuL9o2FnPIhel52DQUKIh7ty3Ql305ApV8YaAb4+/w==}
|
||||
'@push.rocks/smartshell@3.2.4':
|
||||
resolution: {integrity: sha512-zZEKfRl3qBaII9BJULk4rB/+EelUpgM2/qHCQLui7c4589HTge4o0nWn+olFw/Hge88qUO77OK1sN7hQFZ6zeg==}
|
||||
|
||||
'@push.rocks/smartsitemap@2.0.3':
|
||||
resolution: {integrity: sha512-jIcms8V1b2mt3dS4PKNlLR1DRC8pCDWMRVbnyM/2+snZOJZonQRlQzAyX8No0EfLbfdrfnxv2IjPX13X29Re6g==}
|
||||
@@ -734,8 +734,8 @@ packages:
|
||||
'@push.rocks/smartspawn@3.0.3':
|
||||
resolution: {integrity: sha512-DyrGPV69wwOiJgKkyruk5hS3UEGZ99xFAqBE9O2nM8VXCRLbbty3xt1Ug5Z092ZZmJYaaGMSnMw3ijyZJFCT0Q==}
|
||||
|
||||
'@push.rocks/smartstate@2.0.25':
|
||||
resolution: {integrity: sha512-gWmbDCx5esezHDQnD2nOClxeTiWtvU1wEdP0XbheCcXzaGEv0H8apRjQBksRZJd9FC3ezrJU00GLh0eH9rPyMQ==}
|
||||
'@push.rocks/smartstate@2.0.26':
|
||||
resolution: {integrity: sha512-lMcf0ZWWs9jej9wjapuonuIZiQNiD9NcAcvRDFXq7GtQf/HUyr6zr5K1XxGZaCIGyYrbYnBHBpNU+8DBoarHrA==}
|
||||
|
||||
'@push.rocks/smartstream@2.0.8':
|
||||
resolution: {integrity: sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==}
|
||||
@@ -3890,7 +3890,7 @@ snapshots:
|
||||
'@api.global/typedrequest': 3.1.10
|
||||
'@api.global/typedrequest-interfaces': 3.0.19
|
||||
'@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
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartchok': 1.1.1
|
||||
@@ -4511,7 +4511,7 @@ snapshots:
|
||||
|
||||
'@borewit/text-codec@0.1.1': {}
|
||||
|
||||
'@cloudflare/workers-types@4.20250813.0': {}
|
||||
'@cloudflare/workers-types@4.20250816.0': {}
|
||||
|
||||
'@colors/colors@1.6.0': {}
|
||||
|
||||
@@ -4543,7 +4543,7 @@ snapshots:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrouter': 1.3.3
|
||||
'@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/smarturl': 3.1.0
|
||||
'@push.rocks/webrequest': 3.0.37
|
||||
@@ -4709,14 +4709,14 @@ snapshots:
|
||||
'@push.rocks/smartnpm': 2.0.4
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
'@push.rocks/smartrequest': 4.2.1
|
||||
'@push.rocks/smartshell': 3.2.3
|
||||
'@push.rocks/smartshell': 3.2.4
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@git.zone/tsrun@1.3.3':
|
||||
dependencies:
|
||||
'@push.rocks/smartfile': 11.2.5
|
||||
'@push.rocks/smartshell': 3.2.3
|
||||
'@push.rocks/smartshell': 3.2.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)':
|
||||
@@ -4740,7 +4740,7 @@ snapshots:
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 2.1.0
|
||||
'@push.rocks/smarts3': 2.2.5
|
||||
'@push.rocks/smartshell': 3.2.3
|
||||
'@push.rocks/smartshell': 3.2.4
|
||||
'@push.rocks/smarttime': 4.1.1
|
||||
'@types/ws': 8.18.1
|
||||
figures: 6.1.0
|
||||
@@ -5319,7 +5319,7 @@ snapshots:
|
||||
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@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)
|
||||
tree-kill: 1.2.2
|
||||
transitivePeerDependencies:
|
||||
@@ -5368,7 +5368,7 @@ snapshots:
|
||||
- aws-crt
|
||||
- supports-color
|
||||
|
||||
'@push.rocks/smartshell@3.2.3':
|
||||
'@push.rocks/smartshell@3.2.4':
|
||||
dependencies:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartexit': 1.0.23
|
||||
@@ -5420,7 +5420,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@push.rocks/smartstate@2.0.25':
|
||||
'@push.rocks/smartstate@2.0.26':
|
||||
dependencies:
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smarthash': 3.2.3
|
||||
|
85
test/test.redaction.ts
Normal file
85
test/test.redaction.ts
Normal 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();
|
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
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.'
|
||||
}
|
||||
|
@@ -16,6 +16,48 @@ function getQenv(): plugins.qenv.Qenv {
|
||||
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
|
||||
// ============================================================================
|
||||
@@ -210,12 +252,14 @@ function applyTransforms(value: unknown, transforms: Transform[]): unknown {
|
||||
*/
|
||||
async function processMappingValue(mappingString: string): Promise<unknown> {
|
||||
const spec = parseMappingSpec(mappingString);
|
||||
const keyName = spec.source.type === 'env' ? spec.source.key : 'hardcoded';
|
||||
|
||||
console.log(` 🔍 Processing mapping: "${mappingString}"`);
|
||||
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'}`);
|
||||
|
||||
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) {
|
||||
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);
|
||||
console.log(` Final value: ${JSON.stringify(result)} (type: ${typeof result})`);
|
||||
console.log(` Final value: ${redactSensitiveValue(keyName, result)} (type: ${typeof result})`);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -253,7 +297,7 @@ async function evaluateMappingValue(mappingValue: any): Promise<any> {
|
||||
// Only skip if explicitly undefined
|
||||
if (evaluated !== undefined) {
|
||||
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 {
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -434,7 +479,7 @@ export class AppData<T = any> {
|
||||
const valuePreview = evaluated === null ? 'null' :
|
||||
typeof evaluated === 'object' ?
|
||||
(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})`);
|
||||
} else {
|
||||
console.log(` ⚠️ Key "${key}" evaluated to undefined, skipping`);
|
||||
|
Reference in New Issue
Block a user