Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
fdc2420238 | |||
e3a76ca577 | |||
2cc0da4462 | |||
b1e4ab09db | |||
e21815b0e2 | |||
5f1090dd62 |
23
changelog.md
23
changelog.md
@@ -1,5 +1,28 @@
|
||||
# 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
|
||||
|
||||
- Bumped dependency @push.rocks/qenv from ^6.1.2 to ^6.1.3 in package.json
|
||||
- Added .claude/settings.local.json to provide local Claude permissions and tooling allowances for development/CI helpers
|
||||
|
||||
## 2025-08-15 - 5.3.1 - fix(AppData/Conversion)
|
||||
Improve boolean conversion and mapping evaluation in AppData, ensuring falsy values (like false, 0, and empty strings) are correctly handled and logged. Also, reduce test timeout and add local permissions settings for development.
|
||||
|
||||
- Enhanced toBoolean and evaluateMappingValue functions to properly preserve and log falsy values.
|
||||
- Added detailed logging for mapping spec processing and nested key evaluations.
|
||||
- Reduced test timeout in package.json for faster CI feedback.
|
||||
- Introduced .claude/settings.local.json with updated permissions for local development.
|
||||
|
||||
## 2025-08-15 - 5.3.0 - feat(AppData)
|
||||
Refactor AppData class for declarative env mapping and enhanced static helpers
|
||||
|
||||
|
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@push.rocks/npmextra",
|
||||
"version": "5.3.0",
|
||||
"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",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --verbose --logfile --timeout 60)",
|
||||
"test": "(tstest test/ --verbose --logfile --timeout 20)",
|
||||
"build": "(tsbuild --web --allowimplicitany)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"homepage": "https://code.foss.global/push.rocks/npmextra#readme",
|
||||
"dependencies": {
|
||||
"@push.rocks/qenv": "^6.1.2",
|
||||
"@push.rocks/qenv": "^6.1.3",
|
||||
"@push.rocks/smartfile": "^11.2.5",
|
||||
"@push.rocks/smartjson": "^5.0.20",
|
||||
"@push.rocks/smartlog": "^3.1.8",
|
||||
|
42
pnpm-lock.yaml
generated
42
pnpm-lock.yaml
generated
@@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@push.rocks/qenv':
|
||||
specifier: ^6.1.2
|
||||
version: 6.1.2
|
||||
specifier: ^6.1.3
|
||||
version: 6.1.3
|
||||
'@push.rocks/smartfile':
|
||||
specifier: ^11.2.5
|
||||
version: 11.2.5
|
||||
@@ -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==}
|
||||
@@ -578,8 +578,8 @@ packages:
|
||||
'@push.rocks/mongodump@1.0.8':
|
||||
resolution: {integrity: sha512-oDufyjNBg8I50OaJvbHhc0RnRpJQ544dr9her0G6sA8JmI3hD2/amTdcPLVIX1kzYf5GsTUKeWuRaZgdNqz3ew==}
|
||||
|
||||
'@push.rocks/qenv@6.1.2':
|
||||
resolution: {integrity: sha512-epb5Ey7E3jVCjxvNmQ5bcjPs9+7d1z/5bV/V8+qwrPqZrbgXnslOnsQWOh9usAatO0VJqqZmSvLYSpjnm3NEcA==}
|
||||
'@push.rocks/qenv@6.1.3':
|
||||
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
|
||||
|
||||
'@push.rocks/smartarchive@3.0.8':
|
||||
resolution: {integrity: sha512-1jPmR0b7hXmjYQoRiTlRXrIbZcdcFmSdGOfznufjcDpGPe86Km0d8TBnzqghTx4dTihzKC67IxAaz/DM3lvxpA==}
|
||||
@@ -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)':
|
||||
@@ -4725,7 +4725,7 @@ snapshots:
|
||||
'@git.zone/tsbundle': 2.5.1
|
||||
'@git.zone/tsrun': 1.3.3
|
||||
'@push.rocks/consolecolor': 2.0.3
|
||||
'@push.rocks/qenv': 6.1.2
|
||||
'@push.rocks/qenv': 6.1.3
|
||||
'@push.rocks/smartbrowser': 2.0.8(typescript@5.8.3)
|
||||
'@push.rocks/smartchok': 1.1.1
|
||||
'@push.rocks/smartcrypto': 2.0.4
|
||||
@@ -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
|
||||
@@ -4953,7 +4953,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- aws-crt
|
||||
|
||||
'@push.rocks/qenv@6.1.2':
|
||||
'@push.rocks/qenv@6.1.3':
|
||||
dependencies:
|
||||
'@api.global/typedrequest': 3.1.10
|
||||
'@configvault.io/interfaces': 1.0.17
|
||||
@@ -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
|
||||
|
139
test/test.boolean-false.ts
Normal file
139
test/test.boolean-false.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as npmextra from '../ts/index.js';
|
||||
|
||||
// Test environment variable with boolean false value
|
||||
tap.test('should handle boolean false values in nested objects correctly', async () => {
|
||||
// Set up test environment variable
|
||||
process.env['S3_USESSL'] = 'false';
|
||||
process.env['S3_ENDPOINT'] = 'https://s3.example.com';
|
||||
process.env['S3_REGION'] = 'us-east-1';
|
||||
process.env['S3_ACCESSKEY'] = 'test-key';
|
||||
process.env['S3_ACCESSSECRET'] = 'test-secret';
|
||||
|
||||
// Create AppData with nested object structure similar to CloudlyConfig
|
||||
const appData = await npmextra.AppData.createAndInit({
|
||||
ephemeral: true, // Use in-memory storage for testing
|
||||
envMapping: {
|
||||
s3Descriptor: {
|
||||
endpoint: 'S3_ENDPOINT',
|
||||
region: 'S3_REGION',
|
||||
accessKey: 'S3_ACCESSKEY',
|
||||
accessSecret: 'S3_ACCESSSECRET',
|
||||
useSsl: 'boolean:S3_USESSL'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Get the kvStore and read the configuration
|
||||
const kvStore = await appData.getKvStore();
|
||||
const s3Descriptor = await kvStore.readKey('s3Descriptor');
|
||||
|
||||
console.log('\n=== Test Results ===');
|
||||
console.log('S3 Descriptor:', JSON.stringify(s3Descriptor, null, 2));
|
||||
console.log('useSsl value:', s3Descriptor?.useSsl);
|
||||
console.log('useSsl type:', typeof s3Descriptor?.useSsl);
|
||||
console.log('useSsl === false:', (s3Descriptor?.useSsl as any) === false);
|
||||
|
||||
// Verify the values
|
||||
expect(s3Descriptor).toBeTruthy();
|
||||
expect(s3Descriptor.endpoint).toEqual('https://s3.example.com');
|
||||
expect(s3Descriptor.region).toEqual('us-east-1');
|
||||
expect(s3Descriptor.accessKey).toEqual('test-key');
|
||||
expect(s3Descriptor.accessSecret).toEqual('test-secret');
|
||||
|
||||
// Critical test: useSsl should be false, not undefined
|
||||
expect(s3Descriptor.useSsl).toEqual(false);
|
||||
expect(typeof s3Descriptor.useSsl).toEqual('boolean');
|
||||
expect(s3Descriptor.useSsl).not.toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should handle various boolean representations correctly', async () => {
|
||||
// Test different boolean representations
|
||||
const testCases = [
|
||||
{ env: 'false', expected: false },
|
||||
{ env: 'FALSE', expected: false },
|
||||
{ env: '0', expected: false },
|
||||
{ env: 'no', expected: false },
|
||||
{ env: 'NO', expected: false },
|
||||
{ env: 'n', expected: false },
|
||||
{ env: 'off', expected: false },
|
||||
{ env: 'true', expected: true },
|
||||
{ env: 'TRUE', expected: true },
|
||||
{ env: '1', expected: true },
|
||||
{ env: 'yes', expected: true },
|
||||
{ env: 'YES', expected: true },
|
||||
{ env: 'y', expected: true },
|
||||
{ env: 'on', expected: true },
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
process.env['TEST_BOOL'] = testCase.env;
|
||||
|
||||
const appData = await npmextra.AppData.createAndInit({
|
||||
ephemeral: true,
|
||||
envMapping: {
|
||||
testBool: 'boolean:TEST_BOOL'
|
||||
}
|
||||
});
|
||||
|
||||
const kvStore = await appData.getKvStore();
|
||||
const testBool = await kvStore.readKey('testBool');
|
||||
|
||||
console.log(`Input "${testCase.env}" => ${testBool} (expected: ${testCase.expected})`);
|
||||
expect(testBool).toEqual(testCase.expected);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should handle hardcoded boolean false values', async () => {
|
||||
// Test with hardcoded boolean false value
|
||||
const appData = await npmextra.AppData.createAndInit({
|
||||
ephemeral: true,
|
||||
envMapping: {
|
||||
boolValue: 'hard_boolean:false'
|
||||
}
|
||||
});
|
||||
|
||||
const kvStore = await appData.getKvStore();
|
||||
const boolValue = await kvStore.readKey('boolValue');
|
||||
|
||||
console.log('\n=== Hardcoded Boolean Test ===');
|
||||
console.log('boolValue:', boolValue);
|
||||
console.log('type:', typeof boolValue);
|
||||
console.log('is false:', (boolValue as any) === false);
|
||||
|
||||
expect(boolValue).toEqual(false);
|
||||
expect(typeof boolValue).toEqual('boolean');
|
||||
});
|
||||
|
||||
tap.test('should not filter out other falsy values', async () => {
|
||||
process.env['ZERO_VALUE'] = '0';
|
||||
process.env['EMPTY_STRING'] = ''; // This should be preserved as empty string
|
||||
|
||||
const appData = await npmextra.AppData.createAndInit({
|
||||
ephemeral: true,
|
||||
envMapping: {
|
||||
nested: {
|
||||
zeroAsNumber: 'ZERO_VALUE', // Should preserve "0" as string
|
||||
zeroAsBoolean: 'boolean:ZERO_VALUE', // Should convert to false
|
||||
emptyString: 'EMPTY_STRING', // Should preserve empty string
|
||||
hardcodedFalse: 'hard_boolean:false', // Should be false
|
||||
hardcodedZero: 'hard:0', // Should be "0" string
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const kvStore = await appData.getKvStore();
|
||||
const nested = await kvStore.readKey('nested');
|
||||
|
||||
console.log('\n=== Falsy Values Test ===');
|
||||
console.log('nested:', JSON.stringify(nested, null, 2));
|
||||
|
||||
expect(nested).toBeTruthy();
|
||||
expect(nested.zeroAsNumber).toEqual('0');
|
||||
expect(nested.zeroAsBoolean).toEqual(false);
|
||||
expect(nested.emptyString).toEqual('');
|
||||
expect(nested.hardcodedFalse).toEqual(false);
|
||||
expect(nested.hardcodedZero).toEqual('0');
|
||||
});
|
||||
|
||||
export default tap.start();
|
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.0',
|
||||
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,14 +16,83 @@ 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
|
||||
// ============================================================================
|
||||
function toBoolean(value: unknown): boolean {
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (value == null) return false;
|
||||
const s = String(value).toLowerCase();
|
||||
return s === 'true';
|
||||
// If already boolean, return as-is
|
||||
if (typeof value === 'boolean') {
|
||||
console.log(` 🔹 toBoolean: value is already boolean: ${value}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Handle null/undefined
|
||||
if (value == null) {
|
||||
console.log(` 🔹 toBoolean: value is null/undefined, returning false`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle string representations
|
||||
const s = String(value).toLowerCase().trim();
|
||||
|
||||
// True values: "true", "1", "yes", "y", "on"
|
||||
if (['true', '1', 'yes', 'y', 'on'].includes(s)) {
|
||||
console.log(` 🔹 toBoolean: converting "${value}" to true`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// False values: "false", "0", "no", "n", "off"
|
||||
if (['false', '0', 'no', 'n', 'off'].includes(s)) {
|
||||
console.log(` 🔹 toBoolean: converting "${value}" to false`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Default: non-empty string = true, empty = false
|
||||
const result = s.length > 0;
|
||||
console.log(` 🔹 toBoolean: defaulting "${value}" to ${result}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function toJson<T = any>(value: unknown): T | undefined {
|
||||
@@ -159,6 +228,12 @@ async function resolveSource(source: MappingSpec['source']): Promise<unknown> {
|
||||
return source.value;
|
||||
}
|
||||
// source.type === 'env'
|
||||
// Workaround for Qenv bug where empty strings are treated as undefined
|
||||
// Check process.env directly first to preserve empty strings
|
||||
if (Object.prototype.hasOwnProperty.call(process.env, source.key)) {
|
||||
return process.env[source.key];
|
||||
}
|
||||
// Fall back to Qenv for other sources (env.json, docker secrets, etc.)
|
||||
return await getQenv().getEnvVarOnDemand(source.key);
|
||||
}
|
||||
|
||||
@@ -177,32 +252,63 @@ 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: ${redactSensitiveValue(keyName, rawValue)} (type: ${typeof rawValue})`);
|
||||
|
||||
if (rawValue === undefined || rawValue === null) {
|
||||
console.log(` ⚠️ Raw value is undefined/null, returning undefined`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return applyTransforms(rawValue, spec.transforms);
|
||||
const result = applyTransforms(rawValue, spec.transforms);
|
||||
console.log(` Final value: ${redactSensitiveValue(keyName, result)} (type: ${typeof result})`);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively evaluate mapping values (strings or nested objects)
|
||||
*/
|
||||
async function evaluateMappingValue(mappingValue: any): Promise<any> {
|
||||
// Handle null explicitly - it should return null, not be treated as object
|
||||
if (mappingValue === null) {
|
||||
console.log(` 📌 Value is null, returning null`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle strings (mapping specs)
|
||||
if (typeof mappingValue === 'string') {
|
||||
return processMappingValue(mappingValue);
|
||||
}
|
||||
|
||||
// Handle objects (but not arrays or null)
|
||||
if (mappingValue && typeof mappingValue === 'object' && !Array.isArray(mappingValue)) {
|
||||
console.log(` 📂 Processing nested object with ${Object.keys(mappingValue).length} keys`);
|
||||
const result: any = {};
|
||||
for (const [key, value] of Object.entries(mappingValue)) {
|
||||
result[key] = await evaluateMappingValue(value);
|
||||
console.log(` → Processing nested key "${key}"`);
|
||||
const evaluated = await evaluateMappingValue(value);
|
||||
// Important: Don't filter out false or other falsy values!
|
||||
// Only skip if explicitly undefined
|
||||
if (evaluated !== undefined) {
|
||||
result[key] = evaluated;
|
||||
console.log(` ✓ Nested key "${key}" = ${redactSensitiveValue(key, evaluated)} (type: ${typeof evaluated})`);
|
||||
} else {
|
||||
console.log(` ⚠️ Nested key "${key}" evaluated to undefined, skipping`);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
// For any other type (numbers, booleans, etc.), return as-is
|
||||
// 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;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -355,14 +461,26 @@ export class AppData<T = any> {
|
||||
for (const key in this.options.envMapping) {
|
||||
try {
|
||||
const mappingSpec = this.options.envMapping[key];
|
||||
console.log(` → Processing key "${key}" with spec:`, typeof mappingSpec === 'string' ? mappingSpec : 'nested object');
|
||||
const specType = mappingSpec === null ? 'null' :
|
||||
typeof mappingSpec === 'string' ? mappingSpec :
|
||||
typeof mappingSpec === 'object' ? 'nested object' :
|
||||
typeof mappingSpec;
|
||||
console.log(` → Processing key "${key}" with spec: ${specType}`);
|
||||
|
||||
const evaluated = await evaluateMappingValue(mappingSpec);
|
||||
// Important: Don't skip false, 0, empty string, or null values!
|
||||
// Only skip if explicitly undefined
|
||||
if (evaluated !== undefined) {
|
||||
await this.kvStore.writeKey(key as keyof T, evaluated);
|
||||
processedCount++;
|
||||
const valueType = Array.isArray(evaluated) ? 'array' : typeof evaluated;
|
||||
console.log(` ✅ Successfully processed key "${key}" (type: ${valueType})`);
|
||||
const valueType = evaluated === null ? 'null' :
|
||||
Array.isArray(evaluated) ? 'array' :
|
||||
typeof evaluated;
|
||||
const valuePreview = evaluated === null ? 'null' :
|
||||
typeof evaluated === 'object' ?
|
||||
(Array.isArray(evaluated) ? `[${evaluated.length} items]` : `{${Object.keys(evaluated).length} keys}`) :
|
||||
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