Compare commits

...

10 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
2cc0da4462 5.3.2
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 4m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-16 12:42:51 +00:00
b1e4ab09db fix(dependencies): Bump @push.rocks/qenv to ^6.1.3 and add local Claude settings 2025-08-16 12:42:51 +00:00
e21815b0e2 5.3.1
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 4m4s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-15 18:14:57 +00:00
5f1090dd62 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. 2025-08-15 18:14:57 +00:00
b0df896a14 5.3.0
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 4m3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-15 13:17:18 +00:00
ed969cee47 feat(AppData): Refactor AppData class for declarative env mapping and enhanced static helpers 2025-08-15 13:17:18 +00:00
61fafd2c8f 5.1.4
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 4m4s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-15 12:26:50 +00:00
33a5b6b11c fix(AppData, dev dependencies, settings): Improve boolean conversion in AppData, update @types/node dependency, and add local settings file. 2025-08-15 12:26:50 +00:00
9 changed files with 1073 additions and 323 deletions

View File

@@ -1,5 +1,76 @@
# 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)
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
- Introduced a singleton Qenv provider to optimize environment variable resolution.
- Centralized type conversion logic with utility functions for boolean, JSON, base64, number, and string conversions.
- Replaced complex switch statements with a composable, declarative mapping pipeline for processing envMapping.
- Enhanced logging during AppData initialization to clearly report key processing and overwrite operations.
- Added new static helper methods for environment variable access (valueAsBoolean, valueAsJson, valueAsBase64, valueAsString, valueAsNumber).
- Fixed boolean conversion issues and ensured backward compatibility with the deprecated 'ephermal' option.
## 2025-08-15 - 5.2.0 - feat(AppData)
Major refactoring of AppData class for improved elegance and maintainability
- **New Features:**
- Added static helper methods for direct environment variable access:
- `AppData.valueAsBoolean()` - Convert env vars to boolean
- `AppData.valueAsJson()` - Parse env vars as JSON
- `AppData.valueAsBase64()` - Decode base64 env vars
- `AppData.valueAsString()` - Get env vars as string
- `AppData.valueAsNumber()` - Parse env vars as number
- Enhanced logging for AppData initialization and key processing:
- Shows which storage type is being used (custom, ephemeral, auto-selected)
- Logs each key being processed with its spec type
- Reports success/failure for each key with type information
- Provides summary statistics of processed keys
- **Architecture Improvements:**
- Replaced 100+ line switch statement with declarative pipeline architecture
- Introduced centralized type converters and transform registry
- Implemented composable transform pipeline: `parseMappingSpec()``resolveSource()``applyTransforms()`
- Added singleton Qenv provider to reduce allocations
- Reduced code complexity by ~70% while maintaining 100% backward compatibility
- **Bug Fixes:**
- Fixed boolean conversion to properly handle both string and boolean inputs
- Added `ephemeral` option (correctly spelled) while maintaining backward compatibility with deprecated `ephermal`
- **Performance:**
- Optimized environment variable resolution with shared Qenv instance
- Reduced object allocations in static helpers
## 2025-08-15 - 5.1.4 - fix(AppData, dev dependencies, settings)
Improve boolean conversion in AppData, update @types/node dependency, and add local settings file.
- Fixed env var boolean conversion to properly handle non-string values in AppData.
- Updated @types/node from ^20.14.5 to ^22 in package.json.
- Added .claude/settings.local.json to configure project permissions locally.
## 2025-08-15 - 5.1.3 - fix(appdata) ## 2025-08-15 - 5.1.3 - fix(appdata)
Fix iteration over overwriteObject in AppData and update configuration for dependency and path handling Fix iteration over overwriteObject in AppData and update configuration for dependency and path handling

View File

@@ -1,12 +1,12 @@
{ {
"name": "@push.rocks/npmextra", "name": "@push.rocks/npmextra",
"version": "5.1.3", "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",
"typings": "dist_ts/index.d.ts", "typings": "dist_ts/index.d.ts",
"scripts": { "scripts": {
"test": "(tstest test/ --verbose --logfile --timeout 60)", "test": "(tstest test/ --verbose --logfile --timeout 20)",
"build": "(tsbuild --web --allowimplicitany)", "build": "(tsbuild --web --allowimplicitany)",
"buildDocs": "tsdoc" "buildDocs": "tsdoc"
}, },
@@ -21,7 +21,7 @@
}, },
"homepage": "https://code.foss.global/push.rocks/npmextra#readme", "homepage": "https://code.foss.global/push.rocks/npmextra#readme",
"dependencies": { "dependencies": {
"@push.rocks/qenv": "^6.1.2", "@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartfile": "^11.2.5", "@push.rocks/smartfile": "^11.2.5",
"@push.rocks/smartjson": "^5.0.20", "@push.rocks/smartjson": "^5.0.20",
"@push.rocks/smartlog": "^3.1.8", "@push.rocks/smartlog": "^3.1.8",
@@ -35,7 +35,7 @@
"@git.zone/tsbuild": "^2.6.4", "@git.zone/tsbuild": "^2.6.4",
"@git.zone/tsrun": "^1.3.3", "@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^2.3.2", "@git.zone/tstest": "^2.3.2",
"@types/node": "^20.14.5" "@types/node": "^22"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",

262
pnpm-lock.yaml generated
View File

@@ -9,8 +9,8 @@ importers:
.: .:
dependencies: dependencies:
'@push.rocks/qenv': '@push.rocks/qenv':
specifier: ^6.1.2 specifier: ^6.1.3
version: 6.1.2 version: 6.1.3
'@push.rocks/smartfile': '@push.rocks/smartfile':
specifier: ^11.2.5 specifier: ^11.2.5
version: 11.2.5 version: 11.2.5
@@ -46,8 +46,8 @@ importers:
specifier: ^2.3.2 specifier: ^2.3.2
version: 2.3.2(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7)(typescript@5.8.3) version: 2.3.2(@aws-sdk/credential-providers@3.864.0)(socks@2.8.7)(typescript@5.8.3)
'@types/node': '@types/node':
specifier: ^20.14.5 specifier: ^22
version: 20.14.5 version: 22.17.2
packages: packages:
@@ -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==}
@@ -572,17 +572,14 @@ packages:
'@push.rocks/levelcache@3.1.1': '@push.rocks/levelcache@3.1.1':
resolution: {integrity: sha512-+JpDNEt+EuvmbtADGH9SkODxBy+slHDDzs43mAbuMbwpVvi6uNuMK0Mkhrfz9UFpxUSp+cJE/jl/OxdpD0xL1A==} resolution: {integrity: sha512-+JpDNEt+EuvmbtADGH9SkODxBy+slHDDzs43mAbuMbwpVvi6uNuMK0Mkhrfz9UFpxUSp+cJE/jl/OxdpD0xL1A==}
'@push.rocks/lik@6.0.15':
resolution: {integrity: sha512-rZxln6l4NAU931MTxnsjy1pue+S3AXtDCidHH/tbkqBtrWIzWuXduo6Nz3zYkndbD64Knyta7F60JRvcOe4XqA==}
'@push.rocks/lik@6.2.2': '@push.rocks/lik@6.2.2':
resolution: {integrity: sha512-j64FFPPyMXeeUorjKJVF6PWaJUfiIrF3pc41iJH4lOh0UUpBAHpcNzHVxTR58orwbVA/h3Hz+DQd4b1Rq0dFDQ==} resolution: {integrity: sha512-j64FFPPyMXeeUorjKJVF6PWaJUfiIrF3pc41iJH4lOh0UUpBAHpcNzHVxTR58orwbVA/h3Hz+DQd4b1Rq0dFDQ==}
'@push.rocks/mongodump@1.0.8': '@push.rocks/mongodump@1.0.8':
resolution: {integrity: sha512-oDufyjNBg8I50OaJvbHhc0RnRpJQ544dr9her0G6sA8JmI3hD2/amTdcPLVIX1kzYf5GsTUKeWuRaZgdNqz3ew==} resolution: {integrity: sha512-oDufyjNBg8I50OaJvbHhc0RnRpJQ544dr9her0G6sA8JmI3hD2/amTdcPLVIX1kzYf5GsTUKeWuRaZgdNqz3ew==}
'@push.rocks/qenv@6.1.2': '@push.rocks/qenv@6.1.3':
resolution: {integrity: sha512-epb5Ey7E3jVCjxvNmQ5bcjPs9+7d1z/5bV/V8+qwrPqZrbgXnslOnsQWOh9usAatO0VJqqZmSvLYSpjnm3NEcA==} resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
'@push.rocks/smartarchive@3.0.8': '@push.rocks/smartarchive@3.0.8':
resolution: {integrity: sha512-1jPmR0b7hXmjYQoRiTlRXrIbZcdcFmSdGOfznufjcDpGPe86Km0d8TBnzqghTx4dTihzKC67IxAaz/DM3lvxpA==} resolution: {integrity: sha512-1jPmR0b7hXmjYQoRiTlRXrIbZcdcFmSdGOfznufjcDpGPe86Km0d8TBnzqghTx4dTihzKC67IxAaz/DM3lvxpA==}
@@ -617,9 +614,6 @@ packages:
'@push.rocks/smartdelay@3.0.5': '@push.rocks/smartdelay@3.0.5':
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==} resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
'@push.rocks/smartenv@5.0.12':
resolution: {integrity: sha512-tDEFwywzq0FNzRYc9qY2dRl2pgQuZG0G2/yml2RLWZWSW+Fn1EHshnKOGHz8o77W7zvu4hTgQQX42r/JY5XHTg==}
'@push.rocks/smartenv@5.0.13': '@push.rocks/smartenv@5.0.13':
resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==} resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==}
@@ -728,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==}
@@ -740,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==}
@@ -752,9 +746,6 @@ packages:
'@push.rocks/smartstring@4.0.15': '@push.rocks/smartstring@4.0.15':
resolution: {integrity: sha512-NTNeOjWyg+aHtBTiQEyXamr7oTvYZ3wS1fudHo9ua7CLrykpK+i+RxFyJaLg1zB5x9xQF3NLEQecB14HPFX8Cg==} resolution: {integrity: sha512-NTNeOjWyg+aHtBTiQEyXamr7oTvYZ3wS1fudHo9ua7CLrykpK+i+RxFyJaLg1zB5x9xQF3NLEQecB14HPFX8Cg==}
'@push.rocks/smarttime@4.0.6':
resolution: {integrity: sha512-1whOow0YJw/TbN758TedRRxApoZbsvyxCVpoGjXh7DE/fEEgs7RCr4vVF5jYpyXNQuNMLpKJcTsSfyQ6RvH4Aw==}
'@push.rocks/smarttime@4.1.1': '@push.rocks/smarttime@4.1.1':
resolution: {integrity: sha512-Ha/3J/G+zfTl4ahpZgF6oUOZnUjpLhrBja0OQ2cloFxF9sKT8I1COaSqIfBGDtoK2Nly4UD4aTJ3JcJNOg/kgA==} resolution: {integrity: sha512-Ha/3J/G+zfTl4ahpZgF6oUOZnUjpLhrBja0OQ2cloFxF9sKT8I1COaSqIfBGDtoK2Nly4UD4aTJ3JcJNOg/kgA==}
@@ -1379,8 +1370,8 @@ packages:
'@types/node-forge@1.3.13': '@types/node-forge@1.3.13':
resolution: {integrity: sha512-zePQJSW5QkwSHKRApqWCVKeKoSOt4xvEnLENZPjyvm9Ezdf/EyDeJM7jqLzOwjVICQQzvLZ63T55MKdJB5H6ww==} resolution: {integrity: sha512-zePQJSW5QkwSHKRApqWCVKeKoSOt4xvEnLENZPjyvm9Ezdf/EyDeJM7jqLzOwjVICQQzvLZ63T55MKdJB5H6ww==}
'@types/node@20.14.5': '@types/node@22.17.2':
resolution: {integrity: sha512-aoRR+fJkZT2l0aGOJhuA8frnCSoNX6W7U2mpNq63+BxBIj5BQFt8rHy627kijCmm63ijdSdwvGgpUsU6MBsZZA==} resolution: {integrity: sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==}
'@types/ping@0.4.4': '@types/ping@0.4.4':
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==} resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
@@ -1643,8 +1634,8 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
call-bind@1.0.7: call-bind@1.0.8:
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
call-bound@1.0.4: call-bound@1.0.4:
@@ -1814,10 +1805,6 @@ packages:
resolution: {integrity: sha512-9pSLe+tDJnmNak2JeMkz6ZmTCXP5p6vCxSd4kvDqrTJkqAP62j2uAEIZjf8cPDZIakStujqVzh5Y5MIWH3yYAw==} resolution: {integrity: sha512-9pSLe+tDJnmNak2JeMkz6ZmTCXP5p6vCxSd4kvDqrTJkqAP62j2uAEIZjf8cPDZIakStujqVzh5Y5MIWH3yYAw==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
croner@7.0.7:
resolution: {integrity: sha512-05wALDHKjt9zG1JbpziNnWPCwwv9fUKbNf6q0dWaDMJ/eDxW0394Q2R1VAzKvDgoEZBT9FhWSHHFIcgwLgXjcQ==}
engines: {node: '>=6.0'}
croner@9.1.0: croner@9.1.0:
resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==} resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==}
engines: {node: '>=18.0'} engines: {node: '>=18.0'}
@@ -1837,9 +1824,6 @@ packages:
date-fns@4.1.0: date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
dayjs@1.11.11:
resolution: {integrity: sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==}
dayjs@1.11.13: dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
@@ -2002,10 +1986,6 @@ packages:
error-ex@1.3.2: error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
es-define-property@1.0.0:
resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==}
engines: {node: '>= 0.4'}
es-define-property@1.0.1: es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2243,10 +2223,6 @@ packages:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*} engines: {node: 6.* || 8.* || >= 10.*}
get-intrinsic@1.2.4:
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
engines: {node: '>= 0.4'}
get-intrinsic@1.3.0: get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2286,9 +2262,6 @@ packages:
glob@7.2.3: glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
gopd@1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
gopd@1.2.0: gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2322,14 +2295,6 @@ packages:
has-property-descriptors@1.0.2: has-property-descriptors@1.0.2:
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
has-proto@1.0.3:
resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==}
engines: {node: '>= 0.4'}
has-symbols@1.0.3:
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
engines: {node: '>= 0.4'}
has-symbols@1.1.0: has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2529,9 +2494,6 @@ packages:
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
js-base64@3.7.7:
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
js-base64@3.7.8: js-base64@3.7.8:
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
@@ -3024,9 +2986,6 @@ packages:
resolution: {integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=} resolution: {integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
object-inspect@1.13.1:
resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==}
object-inspect@1.13.4: object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -3267,10 +3226,6 @@ packages:
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
qs@6.12.1:
resolution: {integrity: sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==}
engines: {node: '>=0.6'}
qs@6.13.0: qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'} engines: {node: '>=0.6'}
@@ -3461,10 +3416,6 @@ packages:
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
side-channel@1.0.6:
resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
engines: {node: '>= 0.4'}
side-channel@1.1.0: side-channel@1.1.0:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -3727,8 +3678,8 @@ packages:
resolution: {integrity: sha512-+NWHrac9dvilNgme+gP4YrBSumsaMZP0fNBtXXFIf33RLLKEcBUKaQZ7ULUbS0sBfcjxIZ4V96OTRkCbM7hxpw==} resolution: {integrity: sha512-+NWHrac9dvilNgme+gP4YrBSumsaMZP0fNBtXXFIf33RLLKEcBUKaQZ7ULUbS0sBfcjxIZ4V96OTRkCbM7hxpw==}
engines: {node: '>=18'} engines: {node: '>=18'}
undici-types@5.26.5: undici-types@6.21.0:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
unified@11.0.5: unified@11.0.5:
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
@@ -3766,9 +3717,6 @@ packages:
upper-case@1.1.3: upper-case@1.1.3:
resolution: {integrity: sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=} resolution: {integrity: sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=}
url@0.11.3:
resolution: {integrity: sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==}
url@0.11.4: url@0.11.4:
resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -3942,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
@@ -4563,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': {}
@@ -4595,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
@@ -4761,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)':
@@ -4777,7 +4725,7 @@ snapshots:
'@git.zone/tsbundle': 2.5.1 '@git.zone/tsbundle': 2.5.1
'@git.zone/tsrun': 1.3.3 '@git.zone/tsrun': 1.3.3
'@push.rocks/consolecolor': 2.0.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/smartbrowser': 2.0.8(typescript@5.8.3)
'@push.rocks/smartchok': 1.1.1 '@push.rocks/smartchok': 1.1.1
'@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartcrypto': 2.0.4
@@ -4792,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
@@ -4982,17 +4930,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- aws-crt - aws-crt
'@push.rocks/lik@6.0.15':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartmatch': 2.0.0
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smarttime': 4.0.6
'@types/minimatch': 5.1.2
'@types/symbol-tree': 3.2.5
symbol-tree: 3.2.4
'@push.rocks/lik@6.2.2': '@push.rocks/lik@6.2.2':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
@@ -5016,7 +4953,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- aws-crt - aws-crt
'@push.rocks/qenv@6.1.2': '@push.rocks/qenv@6.1.3':
dependencies: dependencies:
'@api.global/typedrequest': 3.1.10 '@api.global/typedrequest': 3.1.10
'@configvault.io/interfaces': 1.0.17 '@configvault.io/interfaces': 1.0.17
@@ -5139,10 +5076,6 @@ snapshots:
dependencies: dependencies:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartenv@5.0.12':
dependencies:
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartenv@5.0.13': '@push.rocks/smartenv@5.0.13':
dependencies: dependencies:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
@@ -5221,7 +5154,7 @@ snapshots:
'@push.rocks/smartjson@5.0.20': '@push.rocks/smartjson@5.0.20':
dependencies: dependencies:
'@push.rocks/smartenv': 5.0.12 '@push.rocks/smartenv': 5.0.13
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.0.15
fast-json-stable-stringify: 2.1.0 fast-json-stable-stringify: 2.1.0
lodash.clonedeep: 4.5.0 lodash.clonedeep: 4.5.0
@@ -5386,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:
@@ -5435,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
@@ -5487,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
@@ -5515,23 +5448,13 @@ snapshots:
'@push.rocks/smartstring@4.0.15': '@push.rocks/smartstring@4.0.15':
dependencies: dependencies:
'@push.rocks/isounique': 1.0.5 '@push.rocks/isounique': 1.0.5
'@push.rocks/smartenv': 5.0.12 '@push.rocks/smartenv': 5.0.13
'@types/randomatic': 3.1.5 '@types/randomatic': 3.1.5
crypto-random-string: 5.0.0 crypto-random-string: 5.0.0
js-base64: 3.7.7 js-base64: 3.7.8
randomatic: 3.1.1 randomatic: 3.1.1
strip-indent: 4.0.0 strip-indent: 4.0.0
url: 0.11.3 url: 0.11.4
'@push.rocks/smarttime@4.0.6':
dependencies:
'@push.rocks/lik': 6.0.15
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartpromise': 4.2.3
croner: 7.0.7
dayjs: 1.11.11
is-nan: 1.3.2
pretty-ms: 8.0.0
'@push.rocks/smarttime@4.1.1': '@push.rocks/smarttime@4.1.1':
dependencies: dependencies:
@@ -5569,12 +5492,12 @@ snapshots:
'@push.rocks/taskbuffer@3.1.7': '@push.rocks/taskbuffer@3.1.7':
dependencies: dependencies:
'@push.rocks/lik': 6.0.15 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.8 '@push.rocks/smartlog': 3.1.8
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smarttime': 4.0.6 '@push.rocks/smarttime': 4.1.1
'@push.rocks/smartunique': 3.0.9 '@push.rocks/smartunique': 3.0.9
'@push.rocks/webrequest@3.0.37': '@push.rocks/webrequest@3.0.37':
@@ -6244,22 +6167,22 @@ snapshots:
'@types/body-parser@1.19.6': '@types/body-parser@1.19.6':
dependencies: dependencies:
'@types/connect': 3.4.38 '@types/connect': 3.4.38
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/buffer-json@2.0.3': {} '@types/buffer-json@2.0.3': {}
'@types/clean-css@4.2.11': '@types/clean-css@4.2.11':
dependencies: dependencies:
'@types/node': 20.14.5 '@types/node': 22.17.2
source-map: 0.6.1 source-map: 0.6.1
'@types/connect@3.4.38': '@types/connect@3.4.38':
dependencies: dependencies:
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/cors@2.8.19': '@types/cors@2.8.19':
dependencies: dependencies:
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/debug@4.1.12': '@types/debug@4.1.12':
dependencies: dependencies:
@@ -6269,7 +6192,7 @@ snapshots:
'@types/express-serve-static-core@5.0.7': '@types/express-serve-static-core@5.0.7':
dependencies: dependencies:
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/qs': 6.14.0 '@types/qs': 6.14.0
'@types/range-parser': 1.2.7 '@types/range-parser': 1.2.7
'@types/send': 0.17.5 '@types/send': 0.17.5
@@ -6286,30 +6209,30 @@ snapshots:
'@types/from2@2.3.5': '@types/from2@2.3.5':
dependencies: dependencies:
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/fs-extra@11.0.4': '@types/fs-extra@11.0.4':
dependencies: dependencies:
'@types/jsonfile': 6.1.4 '@types/jsonfile': 6.1.4
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/fs-extra@9.0.13': '@types/fs-extra@9.0.13':
dependencies: dependencies:
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/glob@7.2.0': '@types/glob@7.2.0':
dependencies: dependencies:
'@types/minimatch': 6.0.0 '@types/minimatch': 6.0.0
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/glob@8.1.0': '@types/glob@8.1.0':
dependencies: dependencies:
'@types/minimatch': 5.1.2 '@types/minimatch': 5.1.2
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/gunzip-maybe@1.4.2': '@types/gunzip-maybe@1.4.2':
dependencies: dependencies:
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/hast@3.0.4': '@types/hast@3.0.4':
dependencies: dependencies:
@@ -6331,7 +6254,7 @@ snapshots:
'@types/jsonfile@6.1.4': '@types/jsonfile@6.1.4':
dependencies: dependencies:
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/mdast@4.0.4': '@types/mdast@4.0.4':
dependencies: dependencies:
@@ -6353,11 +6276,11 @@ snapshots:
'@types/node-forge@1.3.13': '@types/node-forge@1.3.13':
dependencies: dependencies:
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/node@20.14.5': '@types/node@22.17.2':
dependencies: dependencies:
undici-types: 5.26.5 undici-types: 6.21.0
'@types/ping@0.4.4': {} '@types/ping@0.4.4': {}
@@ -6371,30 +6294,30 @@ snapshots:
'@types/s3rver@3.7.4': '@types/s3rver@3.7.4':
dependencies: dependencies:
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/semver@7.7.0': {} '@types/semver@7.7.0': {}
'@types/send@0.17.5': '@types/send@0.17.5':
dependencies: dependencies:
'@types/mime': 1.3.5 '@types/mime': 1.3.5
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/serve-static@1.15.8': '@types/serve-static@1.15.8':
dependencies: dependencies:
'@types/http-errors': 2.0.5 '@types/http-errors': 2.0.5
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/send': 0.17.5 '@types/send': 0.17.5
'@types/symbol-tree@3.2.5': {} '@types/symbol-tree@3.2.5': {}
'@types/tar-stream@2.2.3': '@types/tar-stream@2.2.3':
dependencies: dependencies:
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/through2@2.0.41': '@types/through2@2.0.41':
dependencies: dependencies:
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/triple-beam@1.3.5': {} '@types/triple-beam@1.3.5': {}
@@ -6418,18 +6341,18 @@ snapshots:
'@types/whatwg-url@8.2.2': '@types/whatwg-url@8.2.2':
dependencies: dependencies:
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/webidl-conversions': 7.0.3 '@types/webidl-conversions': 7.0.3
'@types/which@3.0.4': {} '@types/which@3.0.4': {}
'@types/ws@8.18.1': '@types/ws@8.18.1':
dependencies: dependencies:
'@types/node': 20.14.5 '@types/node': 22.17.2
'@types/yauzl@2.10.3': '@types/yauzl@2.10.3':
dependencies: dependencies:
'@types/node': 20.14.5 '@types/node': 22.17.2
optional: true optional: true
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
@@ -6628,12 +6551,11 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
function-bind: 1.1.2 function-bind: 1.1.2
call-bind@1.0.7: call-bind@1.0.8:
dependencies: dependencies:
es-define-property: 1.0.0 call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0 es-define-property: 1.0.1
function-bind: 1.1.2 get-intrinsic: 1.3.0
get-intrinsic: 1.2.4
set-function-length: 1.2.2 set-function-length: 1.2.2
call-bound@1.0.4: call-bound@1.0.4:
@@ -6785,8 +6707,6 @@ snapshots:
croner@5.7.0: {} croner@5.7.0: {}
croner@7.0.7: {}
croner@9.1.0: {} croner@9.1.0: {}
cross-spawn@7.0.6: cross-spawn@7.0.6:
@@ -6803,8 +6723,6 @@ snapshots:
date-fns@4.1.0: {} date-fns@4.1.0: {}
dayjs@1.11.11: {}
dayjs@1.11.13: {} dayjs@1.11.13: {}
debug@2.6.9: debug@2.6.9:
@@ -6835,9 +6753,9 @@ snapshots:
define-data-property@1.1.4: define-data-property@1.1.4:
dependencies: dependencies:
es-define-property: 1.0.0 es-define-property: 1.0.1
es-errors: 1.3.0 es-errors: 1.3.0
gopd: 1.0.1 gopd: 1.2.0
define-lazy-prop@2.0.0: {} define-lazy-prop@2.0.0: {}
@@ -6931,7 +6849,7 @@ snapshots:
engine.io@6.6.4: engine.io@6.6.4:
dependencies: dependencies:
'@types/cors': 2.8.19 '@types/cors': 2.8.19
'@types/node': 20.14.5 '@types/node': 22.17.2
accepts: 1.3.8 accepts: 1.3.8
base64id: 2.0.0 base64id: 2.0.0
cookie: 0.7.2 cookie: 0.7.2
@@ -6954,10 +6872,6 @@ snapshots:
dependencies: dependencies:
is-arrayish: 0.2.1 is-arrayish: 0.2.1
es-define-property@1.0.0:
dependencies:
get-intrinsic: 1.2.4
es-define-property@1.0.1: {} es-define-property@1.0.1: {}
es-errors@1.3.0: {} es-errors@1.3.0: {}
@@ -7269,14 +7183,6 @@ snapshots:
get-caller-file@2.0.5: {} get-caller-file@2.0.5: {}
get-intrinsic@1.2.4:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
has-proto: 1.0.3
has-symbols: 1.0.3
hasown: 2.0.2
get-intrinsic@1.3.0: get-intrinsic@1.3.0:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@@ -7345,10 +7251,6 @@ snapshots:
once: 1.4.0 once: 1.4.0
path-is-absolute: 1.0.1 path-is-absolute: 1.0.1
gopd@1.0.1:
dependencies:
get-intrinsic: 1.2.4
gopd@1.2.0: {} gopd@1.2.0: {}
got@12.6.1: got@12.6.1:
@@ -7402,11 +7304,7 @@ snapshots:
has-property-descriptors@1.0.2: has-property-descriptors@1.0.2:
dependencies: dependencies:
es-define-property: 1.0.0 es-define-property: 1.0.1
has-proto@1.0.3: {}
has-symbols@1.0.3: {}
has-symbols@1.1.0: {} has-symbols@1.1.0: {}
@@ -7560,7 +7458,7 @@ snapshots:
is-nan@1.3.2: is-nan@1.3.2:
dependencies: dependencies:
call-bind: 1.0.7 call-bind: 1.0.8
define-properties: 1.2.1 define-properties: 1.2.1
is-number@4.0.0: {} is-number@4.0.0: {}
@@ -7610,8 +7508,6 @@ snapshots:
dependencies: dependencies:
'@isaacs/cliui': 8.0.2 '@isaacs/cliui': 8.0.2
js-base64@3.7.7: {}
js-base64@3.7.8: {} js-base64@3.7.8: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
@@ -8291,8 +8187,6 @@ snapshots:
object-assign@4.1.1: {} object-assign@4.1.1: {}
object-inspect@1.13.1: {}
object-inspect@1.13.4: {} object-inspect@1.13.4: {}
object-keys@1.1.1: {} object-keys@1.1.1: {}
@@ -8546,10 +8440,6 @@ snapshots:
- typescript - typescript
- utf-8-validate - utf-8-validate
qs@6.12.1:
dependencies:
side-channel: 1.0.6
qs@6.13.0: qs@6.13.0:
dependencies: dependencies:
side-channel: 1.1.0 side-channel: 1.1.0
@@ -8814,8 +8704,8 @@ snapshots:
define-data-property: 1.1.4 define-data-property: 1.1.4
es-errors: 1.3.0 es-errors: 1.3.0
function-bind: 1.1.2 function-bind: 1.1.2
get-intrinsic: 1.2.4 get-intrinsic: 1.3.0
gopd: 1.0.1 gopd: 1.2.0
has-property-descriptors: 1.0.2 has-property-descriptors: 1.0.2
setprototypeof@1.2.0: {} setprototypeof@1.2.0: {}
@@ -8846,13 +8736,6 @@ snapshots:
object-inspect: 1.13.4 object-inspect: 1.13.4
side-channel-map: 1.0.1 side-channel-map: 1.0.1
side-channel@1.0.6:
dependencies:
call-bind: 1.0.7
es-errors: 1.3.0
get-intrinsic: 1.2.4
object-inspect: 1.13.1
side-channel@1.1.0: side-channel@1.1.0:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
@@ -9152,7 +9035,7 @@ snapshots:
uint8array-extras@1.4.1: {} uint8array-extras@1.4.1: {}
undici-types@5.26.5: {} undici-types@6.21.0: {}
unified@11.0.5: unified@11.0.5:
dependencies: dependencies:
@@ -9197,11 +9080,6 @@ snapshots:
upper-case@1.1.3: {} upper-case@1.1.3: {}
url@0.11.3:
dependencies:
punycode: 1.4.1
qs: 6.12.1
url@0.11.4: url@0.11.4:
dependencies: dependencies:
punycode: 1.4.1 punycode: 1.4.1

View File

@@ -259,6 +259,44 @@ AppData intelligently handles boolean conversions:
} }
``` ```
### Static Helper Functions
AppData provides convenient static methods for directly accessing and converting environment variables without creating an instance:
```typescript
import { AppData } from '@push.rocks/npmextra';
// Get environment variable as boolean
const isEnabled = await AppData.valueAsBoolean('FEATURE_ENABLED');
// Returns: true if "true", false otherwise
// Get environment variable as parsed JSON
interface Config {
timeout: number;
retries: number;
}
const config = await AppData.valueAsJson<Config>('SERVICE_CONFIG');
// Returns: Parsed object or undefined
// Get environment variable as base64 decoded string
const secret = await AppData.valueAsBase64('ENCODED_SECRET');
// Returns: Decoded string or undefined
// Get environment variable as string
const apiUrl = await AppData.valueAsString('API_URL');
// Returns: String value or undefined
// Get environment variable as number
const port = await AppData.valueAsNumber('PORT');
// Returns: Number value or undefined
```
These static methods are perfect for:
- Quick environment variable access without setup
- Simple type conversions in utility functions
- One-off configuration checks
- Scenarios where you don't need the full AppData instance
## Advanced Patterns 🎨 ## Advanced Patterns 🎨
### Reactive Configuration ### Reactive Configuration

225
readme.plan.md Normal file
View File

@@ -0,0 +1,225 @@
# AppData Refactoring Plan
## Overview
Refactor the AppData class to improve elegance, maintainability, and extensibility while maintaining 100% backward compatibility.
## Current Issues
- 100+ lines of nested switch statements in processEnvMapping
- Static helpers recreate Qenv instances on every call
- Complex boolean conversion logic scattered across multiple places
- Typo: "ephermal" should be "ephemeral"
- Difficult to test and extend with new transformations
## Architecture Improvements
### 1. Singleton Qenv Provider
Create a shared Qenv instance to avoid repeated instantiation:
```typescript
let sharedQenv: plugins.qenv.Qenv | undefined;
function getQenv(): plugins.qenv.Qenv {
if (!sharedQenv) {
sharedQenv = new plugins.qenv.Qenv(
process.cwd(),
plugins.path.join(process.cwd(), '.nogit')
);
}
return sharedQenv;
}
```
### 2. Centralized Type Converters
Extract all conversion logic into pure utility functions:
```typescript
function toBoolean(value: unknown): boolean {
if (typeof value === 'boolean') return value;
if (value == null) return false;
const s = String(value).toLowerCase();
return s === 'true';
}
function toJson<T>(value: unknown): T | undefined {
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch {
return undefined;
}
}
return value as T;
}
function fromBase64(value: unknown): string {
if (value == null) return '';
return Buffer.from(String(value), 'base64').toString('utf8');
}
function toNumber(value: unknown): number | undefined {
if (value == null) return undefined;
const num = Number(value);
return Number.isNaN(num) ? undefined : num;
}
function toString(value: unknown): string | undefined {
if (value == null) return undefined;
return String(value);
}
```
### 3. Declarative Pipeline Architecture
Replace the giant switch statement with a composable pipeline:
#### Data Structures
```typescript
type MappingSpec = {
source:
| { type: 'env', key: string }
| { type: 'hard', value: string };
transforms: Transform[];
}
type Transform = 'boolean' | 'json' | 'base64' | 'number';
```
#### Pipeline Functions
```typescript
// Parse mapping string into spec
function parseMappingSpec(input: string): MappingSpec
// Resolve the source value
async function resolveSource(source: MappingSpec['source']): Promise<unknown>
// Apply transformations
function applyTransforms(value: unknown, transforms: Transform[]): unknown
// Complete pipeline
async function processMappingValue(mappingString: string): Promise<unknown>
```
### 4. Transform Registry
Enable easy extension with new transforms:
```typescript
const transformRegistry: Record<string, (v: unknown) => unknown> = {
boolean: toBoolean,
json: toJson,
base64: fromBase64,
number: toNumber,
};
```
### 5. Simplified processEnvMapping
Build pure object tree first, then write to kvStore:
```typescript
async function evaluateMappingValue(mappingValue: any): Promise<any> {
if (typeof mappingValue === 'string') {
return processMappingValue(mappingValue);
}
if (mappingValue && typeof mappingValue === 'object') {
const out: any = {};
for (const [k, v] of Object.entries(mappingValue)) {
out[k] = await evaluateMappingValue(v);
}
return out;
}
return undefined;
}
// Main loop becomes:
for (const key in this.options.envMapping) {
const evaluated = await evaluateMappingValue(this.options.envMapping[key]);
if (evaluated !== undefined) {
await this.kvStore.writeKey(key as keyof T, evaluated);
}
}
```
## Backward Compatibility
### Supported Prefixes (Maintained)
- `hard:` - Hardcoded value
- `hard_boolean:` - Hardcoded boolean
- `hard_json:` - Hardcoded JSON
- `hard_base64:` - Hardcoded base64
- `boolean:` - Environment variable as boolean
- `json:` - Environment variable as JSON
- `base64:` - Environment variable as base64
### Supported Suffixes (Maintained)
- `_JSON` - Auto-parse as JSON
- `_BASE64` - Auto-decode from base64
### Typo Fix Strategy
- Add `ephemeral` option to interface
- Keep reading `ephermal` for backward compatibility
- Log deprecation warning when old spelling is used
## Implementation Steps
1. **Add utility functions** at the top of the file
2. **Implement pipeline functions** (parseMappingSpec, resolveSource, applyTransforms)
3. **Refactor processEnvMapping** to use the pipeline
4. **Update static helpers** to use shared utilities
5. **Fix typo** with compatibility shim
6. **Add error boundaries** for better error reporting
7. **Test** to ensure backward compatibility
## Benefits
### Code Quality
- **70% reduction** in processEnvMapping complexity
- **Better separation** of concerns
- **Easier testing** - each function is pure and testable
- **Cleaner error handling** with boundaries
### Performance
- **Shared Qenv instance** reduces allocations
- **Optional parallelization** with Promise.all
- **Fewer repeated operations**
### Maintainability
- **Extensible** - Easy to add new transforms
- **Readable** - Clear pipeline flow
- **Debuggable** - Each step can be logged
- **Type-safe** - Better TypeScript support
## Testing Strategy
1. **Unit tests** for each utility function
2. **Integration tests** for the full pipeline
3. **Backward compatibility tests** for all existing prefixes/suffixes
4. **Edge case tests** for error conditions
## Future Extensions
With the transform registry, adding new features becomes trivial:
```typescript
// Add YAML support
transformRegistry['yaml'] = (v) => YAML.parse(String(v));
// Add integer parsing
transformRegistry['int'] = (v) => parseInt(String(v), 10);
// Add custom transformers
transformRegistry['uppercase'] = (v) => String(v).toUpperCase();
```
## Migration Path
1. Implement new architecture alongside existing code
2. Gradually migrate internal usage
3. Mark old patterns as deprecated (with warnings)
4. Remove deprecated code in next major version
## Success Metrics
- All existing tests pass
- No breaking changes for users
- Reduced code complexity (measurable via cyclomatic complexity)
- Improved test coverage
- Better performance (fewer allocations, optional parallelization)

139
test/test.boolean-false.ts Normal file
View 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
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.1.3', 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

@@ -1,13 +1,330 @@
import * as plugins from './npmextra.plugins.js'; import * as plugins from './npmextra.plugins.js';
import * as paths from './npmextra.paths.js';
import { KeyValueStore } from './npmextra.classes.keyvaluestore.js'; import { KeyValueStore } from './npmextra.classes.keyvaluestore.js';
// ============================================================================
// Singleton Qenv Provider
// ============================================================================
let sharedQenv: plugins.qenv.Qenv | undefined;
function getQenv(): plugins.qenv.Qenv {
if (!sharedQenv) {
sharedQenv = new plugins.qenv.Qenv(
process.cwd(),
plugins.path.join(process.cwd(), '.nogit')
);
}
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 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 {
if (value == null) return undefined;
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch {
return undefined;
}
}
return value as T;
}
function fromBase64(value: unknown): string | undefined {
if (value == null) return undefined;
try {
return Buffer.from(String(value), 'base64').toString('utf8');
} catch {
return String(value);
}
}
function toNumber(value: unknown): number | undefined {
if (value == null) return undefined;
const num = Number(value);
return Number.isNaN(num) ? undefined : num;
}
function toString(value: unknown): string | undefined {
if (value == null) return undefined;
return String(value);
}
// ============================================================================
// Declarative Pipeline Architecture
// ============================================================================
type Transform = 'boolean' | 'json' | 'base64' | 'number';
type MappingSpec = {
source:
| { type: 'env'; key: string }
| { type: 'hard'; value: string };
transforms: Transform[];
};
// Transform registry for extensibility
const transformRegistry: Record<string, (v: unknown) => unknown> = {
boolean: toBoolean,
json: toJson,
base64: fromBase64,
number: toNumber,
};
/**
* Parse a mapping string into a declarative spec
*/
function parseMappingSpec(input: string): MappingSpec {
const transforms: Transform[] = [];
let remaining = input;
// Check for hardcoded prefixes with type conversion
if (remaining.startsWith('hard_boolean:')) {
return {
source: { type: 'hard', value: remaining.slice(13) },
transforms: ['boolean']
};
}
if (remaining.startsWith('hard_json:')) {
return {
source: { type: 'hard', value: remaining.slice(10) },
transforms: ['json']
};
}
if (remaining.startsWith('hard_base64:')) {
return {
source: { type: 'hard', value: remaining.slice(12) },
transforms: ['base64']
};
}
// Check for generic hard: prefix
if (remaining.startsWith('hard:')) {
remaining = remaining.slice(5);
// Check for legacy suffixes on hardcoded values
if (remaining.endsWith('_JSON')) {
transforms.push('json');
remaining = remaining.slice(0, -5);
} else if (remaining.endsWith('_BASE64')) {
transforms.push('base64');
remaining = remaining.slice(0, -7);
}
return {
source: { type: 'hard', value: remaining },
transforms
};
}
// Check for env var prefixes
if (remaining.startsWith('boolean:')) {
transforms.push('boolean');
remaining = remaining.slice(8);
} else if (remaining.startsWith('json:')) {
transforms.push('json');
remaining = remaining.slice(5);
} else if (remaining.startsWith('base64:')) {
transforms.push('base64');
remaining = remaining.slice(7);
}
// Check for legacy suffixes on env vars
if (remaining.endsWith('_JSON')) {
transforms.push('json');
remaining = remaining.slice(0, -5);
} else if (remaining.endsWith('_BASE64')) {
transforms.push('base64');
remaining = remaining.slice(0, -7);
}
return {
source: { type: 'env', key: remaining },
transforms
};
}
/**
* Resolve the source value (env var or hardcoded)
*/
async function resolveSource(source: MappingSpec['source']): Promise<unknown> {
if (source.type === 'hard') {
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);
}
/**
* Apply transformations in sequence
*/
function applyTransforms(value: unknown, transforms: Transform[]): unknown {
return transforms.reduce((acc, transform) => {
const fn = transformRegistry[transform];
return fn ? fn(acc) : acc;
}, value);
}
/**
* Process a mapping value through the complete pipeline
*/
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;
}
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)) {
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;
}
// 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;
}
// ============================================================================
// AppData Interface and Class
// ============================================================================
export interface IAppDataOptions<T = any> { export interface IAppDataOptions<T = any> {
dirPath?: string; dirPath?: string;
requiredKeys?: Array<keyof T>; requiredKeys?: Array<keyof T>;
/** /**
* wether keys should be persisted on disk or not * Whether keys should be persisted on disk or not
*/
ephemeral?: boolean;
/**
* @deprecated Use 'ephemeral' instead
*/ */
ephermal?: boolean; ephermal?: boolean;
@@ -33,6 +350,56 @@ export class AppData<T = any> {
return appData; return appData;
} }
/**
* Static helper to get an environment variable as a boolean
* @param envVarName The name of the environment variable
* @returns boolean value (true if env var is "true", false otherwise)
*/
public static async valueAsBoolean(envVarName: string): Promise<boolean> {
const value = await getQenv().getEnvVarOnDemand(envVarName);
return toBoolean(value);
}
/**
* Static helper to get an environment variable as parsed JSON
* @param envVarName The name of the environment variable
* @returns Parsed JSON object/array
*/
public static async valueAsJson<R = any>(envVarName: string): Promise<R | undefined> {
const value = await getQenv().getEnvVarOnDemand(envVarName);
return toJson<R>(value);
}
/**
* Static helper to get an environment variable as base64 decoded string
* @param envVarName The name of the environment variable
* @returns Decoded string
*/
public static async valueAsBase64(envVarName: string): Promise<string | undefined> {
const value = await getQenv().getEnvVarOnDemand(envVarName);
return fromBase64(value);
}
/**
* Static helper to get an environment variable as a string
* @param envVarName The name of the environment variable
* @returns String value
*/
public static async valueAsString(envVarName: string): Promise<string | undefined> {
const value = await getQenv().getEnvVarOnDemand(envVarName);
return toString(value);
}
/**
* Static helper to get an environment variable as a number
* @param envVarName The name of the environment variable
* @returns Number value
*/
public static async valueAsNumber(envVarName: string): Promise<number | undefined> {
const value = await getQenv().getEnvVarOnDemand(envVarName);
return toNumber(value);
}
// instance // instance
public readyDeferred = plugins.smartpromise.defer<void>(); public readyDeferred = plugins.smartpromise.defer<void>();
public options: IAppDataOptions<T>; public options: IAppDataOptions<T>;
@@ -45,11 +412,20 @@ export class AppData<T = any> {
/** /**
* inits app data * inits app data
* @param pathArg
*/ */
private async init(pathArg?: string) { private async init() {
if (this.options.dirPath || this.options.ephermal) { console.log('🚀 Initializing AppData...');
// ok, nothing to do here;
// Handle backward compatibility for typo
const isEphemeral = this.options.ephemeral ?? this.options.ephermal ?? false;
if (this.options.ephermal && !this.options.ephemeral) {
console.warn('⚠️ Option "ephermal" is deprecated, use "ephemeral" instead.');
}
if (this.options.dirPath) {
console.log(` 📁 Using custom directory: ${this.options.dirPath}`);
} else if (isEphemeral) {
console.log(` 💨 Using ephemeral storage (in-memory only)`);
} else { } else {
const appDataDir = '/app/data'; const appDataDir = '/app/data';
const dataDir = '/data'; const dataDir = '/data';
@@ -58,147 +434,85 @@ export class AppData<T = any> {
const dataExists = plugins.smartfile.fs.isDirectory(dataDir); const dataExists = plugins.smartfile.fs.isDirectory(dataDir);
if (appDataExists) { if (appDataExists) {
this.options.dirPath = appDataDir; this.options.dirPath = appDataDir;
console.log(` 📁 Auto-selected container directory: ${appDataDir}`);
} else if (dataExists) { } else if (dataExists) {
this.options.dirPath = dataDir; this.options.dirPath = dataDir;
console.log(` 📁 Auto-selected data directory: ${dataDir}`);
} else { } else {
await plugins.smartfile.fs.ensureDir(nogitAppData); await plugins.smartfile.fs.ensureDir(nogitAppData);
this.options.dirPath = nogitAppData; this.options.dirPath = nogitAppData;
console.log(` 📁 Auto-selected local directory: ${nogitAppData}`);
} }
} }
this.kvStore = new KeyValueStore<T>({ this.kvStore = new KeyValueStore<T>({
typeArg: this.options.ephermal ? 'ephemeral' : 'custom', typeArg: isEphemeral ? 'ephemeral' : 'custom',
identityArg: 'appkv', identityArg: 'appkv',
customPath: this.options.dirPath, customPath: this.options.dirPath,
mandatoryKeys: this.options.requiredKeys as Array<keyof T>, mandatoryKeys: this.options.requiredKeys as Array<keyof T>,
}); });
if (this.options.envMapping) { if (this.options.envMapping) {
const qenvInstance = new plugins.qenv.Qenv( console.log(`📦 Processing envMapping for AppData...`);
process.cwd(), const totalKeys = Object.keys(this.options.envMapping).length;
plugins.path.join(process.cwd(), '.nogit'), let processedCount = 0;
);
// Recursive function to handle nested objects, now includes key parameter
const processEnvMapping = async (
key: keyof T,
mappingValue: any,
parentKey: keyof T | '' = '',
): Promise<any> => {
if (typeof mappingValue === 'string') {
let envValue: string | boolean | T[keyof T];
let convert: 'none' | 'json' | 'base64' | 'boolean' = 'none';
switch (true) {
case mappingValue.startsWith('hard:'):
envValue = mappingValue.replace('hard:', '') as T[keyof T];
break;
case mappingValue.startsWith('hard_boolean:'):
envValue = mappingValue.replace('hard_boolean:', '') === 'true';
convert = 'boolean';
break;
case mappingValue.startsWith('hard_json:'):
envValue = JSON.parse(
mappingValue.replace('hard_json:', ''),
) as T[keyof T];
convert = 'json';
break;
case mappingValue.startsWith('hard_base64:'):
envValue = Buffer.from(
mappingValue.replace('hard_base64:', ''),
'base64',
).toString() as T[keyof T];
convert = 'base64';
break;
case mappingValue.startsWith('boolean:'):
envValue = (await qenvInstance.getEnvVarOnDemand(
mappingValue.replace('boolean:', ''),
)) as T[keyof T];
convert = 'boolean';
break;
case mappingValue.startsWith('json:'):
envValue = (await qenvInstance.getEnvVarOnDemand(
mappingValue.replace('json:', ''),
)) as T[keyof T];
convert = 'json';
break;
case mappingValue.startsWith('base64:'):
envValue = (await qenvInstance.getEnvVarOnDemand(
mappingValue.replace('base64:', ''),
)) as T[keyof T];
convert = 'base64';
break;
default:
envValue = (await qenvInstance.getEnvVarOnDemand(
mappingValue,
)) as T[keyof T];
break;
}
// lets format the env value
if (envValue) {
if (typeof envValue === 'string' && convert === 'boolean') {
envValue = envValue === 'true';
}
if (
typeof envValue === 'string' &&
(mappingValue.endsWith('_JSON') || convert === 'json')
) {
envValue = JSON.parse(envValue as string) as T[keyof T];
}
if (
typeof envValue === 'string' &&
(mappingValue.endsWith('_BASE64') || convert === 'base64')
) {
envValue = Buffer.from(envValue as string, 'base64').toString();
}
if (!parentKey) {
await this.kvStore.writeKey(key, envValue as any);
} else {
return envValue;
}
} else {
return undefined;
}
} else if (typeof mappingValue === 'object' && mappingValue !== null) {
const resultObject: Partial<T> = {};
for (const innerKey in mappingValue) {
const nestedValue = mappingValue[innerKey];
// For nested objects, call recursively but do not immediately write to kvStore
const nestedResult = await processEnvMapping(
innerKey as keyof T,
nestedValue,
key,
);
resultObject[innerKey as keyof T] = nestedResult;
}
if (parentKey === '') {
// Only write to kvStore if at the top level
await this.kvStore.writeKey(key, resultObject as T[keyof T]);
} else {
// For nested objects, return the constructed object instead of writing to kvStore
return resultObject;
}
}
};
// Process each top-level key in envMapping
for (const key in this.options.envMapping) { for (const key in this.options.envMapping) {
await processEnvMapping(key as keyof T, this.options.envMapping[key]); try {
const mappingSpec = this.options.envMapping[key];
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 = 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`);
}
} catch (err) {
console.error(` ❌ Failed to evaluate envMapping for key "${key}":`, err);
}
} }
console.log(`📊 EnvMapping complete: ${processedCount}/${totalKeys} keys successfully processed`);
}
// Apply overwrite object after env mapping
if (this.options.overwriteObject) { if (this.options.overwriteObject) {
for (const key of Object.keys(this.options.overwriteObject)) { const overwriteKeys = Object.keys(this.options.overwriteObject);
console.log( console.log(`🔄 Applying overwriteObject with ${overwriteKeys.length} key(s)...`);
`-> heads up: overwriting key ${key} from options.overwriteObject`,
); for (const key of overwriteKeys) {
const value = this.options.overwriteObject[key];
const valueType = Array.isArray(value) ? 'array' : typeof value;
console.log(` 🔧 Overwriting key "${key}" with ${valueType} value`);
await this.kvStore.writeKey( await this.kvStore.writeKey(
key as keyof T, key as keyof T,
this.options.overwriteObject[key], value,
); );
} }
}
console.log(`✅ OverwriteObject complete: ${overwriteKeys.length} key(s) overwritten`);
} }
this.readyDeferred.resolve(); this.readyDeferred.resolve();
console.log('✨ AppData initialization complete!');
} }
/** /**