diff --git a/.svn/entries b/.svn/entries new file mode 100644 index 0000000..48082f7 --- /dev/null +++ b/.svn/entries @@ -0,0 +1 @@ +12 diff --git a/.svn/format b/.svn/format new file mode 100644 index 0000000..48082f7 --- /dev/null +++ b/.svn/format @@ -0,0 +1 @@ +12 diff --git a/.svn/pristine/02/02f467277a4684fc765135f30597a4883c14f458.svn-base b/.svn/pristine/02/02f467277a4684fc765135f30597a4883c14f458.svn-base new file mode 100644 index 0000000..41d6788 --- /dev/null +++ b/.svn/pristine/02/02f467277a4684fc765135f30597a4883c14f458.svn-base @@ -0,0 +1,2 @@ +module.exports.RAPParser = require('./rap-parser'); +module.exports.AgNavParser = require('./agn-parser'); \ No newline at end of file diff --git a/.svn/pristine/0d/0dd9b2ac2eee44cc222000eede04b59e26254963.svn-base b/.svn/pristine/0d/0dd9b2ac2eee44cc222000eede04b59e26254963.svn-base new file mode 100644 index 0000000..66f35ec --- /dev/null +++ b/.svn/pristine/0d/0dd9b2ac2eee44cc222000eede04b59e26254963.svn-base @@ -0,0 +1,547 @@ +{ + "name": "error-handler", + "version": "2.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "error-handler", + "version": "2.0.0", + "license": "Proprietary", + "dependencies": { + "debug": "^4.4.0", + "key-file-storage": "^2.3.3", + "mailer": "file:../mailer" + } + }, + "../mailer": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "debug": "^4.4.0", + "nodemailer": "^6.10.0" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/is-valid-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/is-valid-path/-/is-valid-path-0.1.2.tgz", + "integrity": "sha512-BsZtkfiPpnzDWFjSZanYllttVW7/46ayPZkcHBCSFBkBqIO9rWrflUvEmT2tF///hnPLwBJU3TJPzbBxpUEqCg==" + }, + "node_modules/@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-invalid-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-0.1.0.tgz", + "integrity": "sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==", + "dependencies": { + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-valid-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz", + "integrity": "sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==", + "dependencies": { + "is-invalid-path": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/key-file-storage": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/key-file-storage/-/key-file-storage-2.3.3.tgz", + "integrity": "sha512-bqFrbE0ifIq5ahrquGewh5nMub8fdH/nXKMg9CJHdCdD5W/6KQea483Qyss0q53tCOC64CN43DQo9TSvohlbKA==", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "@types/is-valid-path": "^0.1.0", + "fs-extra": "^10.0.0", + "is-valid-path": "^0.1.1", + "recur-fs": "^2.2.4" + } + }, + "node_modules/mailer": { + "resolved": "../mailer", + "link": true + }, + "node_modules/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-NyXjqu1IwcqH6nv5vmMtaG3iw7kdV3g6MwlUBZkc3Vn5b5AMIWYKfptvzipoyFfhlfOgBQ9zoTxQMravF1QTnw==", + "dependencies": { + "brace-expansion": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==" + }, + "node_modules/mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/recur-fs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/recur-fs/-/recur-fs-2.2.4.tgz", + "integrity": "sha512-VofuavbR3PNwHAs2uxn2HNcyyXKIUBayVtdhdElJ8qqSmcr2TY71THQjAoF+PT1xEeUnEi57DWT2/+YSRCvr3w==", + "dependencies": { + "minimatch": "3.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.5.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", + "integrity": "sha512-Lw7SHMjssciQb/rRz7JyPIy9+bbUshEucPoLRvWqy09vC5zQixl8Uet+Zl+SROBB/JMWHJRdCk1qdxNWHNMvlQ==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.0.5" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + }, + "dependencies": { + "@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "requires": { + "@types/node": "*" + } + }, + "@types/is-valid-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/is-valid-path/-/is-valid-path-0.1.2.tgz", + "integrity": "sha512-BsZtkfiPpnzDWFjSZanYllttVW7/46ayPZkcHBCSFBkBqIO9rWrflUvEmT2tF///hnPLwBJU3TJPzbBxpUEqCg==" + }, + "@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "requires": { + "undici-types": "~6.21.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "requires": { + "ms": "^2.1.3" + } + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==" + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-invalid-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-0.1.0.tgz", + "integrity": "sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==", + "requires": { + "is-glob": "^2.0.0" + } + }, + "is-valid-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz", + "integrity": "sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==", + "requires": { + "is-invalid-path": "^0.1.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "key-file-storage": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/key-file-storage/-/key-file-storage-2.3.3.tgz", + "integrity": "sha512-bqFrbE0ifIq5ahrquGewh5nMub8fdH/nXKMg9CJHdCdD5W/6KQea483Qyss0q53tCOC64CN43DQo9TSvohlbKA==", + "requires": { + "@types/fs-extra": "^9.0.11", + "@types/is-valid-path": "^0.1.0", + "fs-extra": "^10.0.0", + "is-valid-path": "^0.1.1", + "recur-fs": "^2.2.4" + } + }, + "mailer": { + "version": "file:../mailer", + "requires": { + "debug": "^4.4.0", + "nodemailer": "^6.10.0" + } + }, + "minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-NyXjqu1IwcqH6nv5vmMtaG3iw7kdV3g6MwlUBZkc3Vn5b5AMIWYKfptvzipoyFfhlfOgBQ9zoTxQMravF1QTnw==", + "requires": { + "brace-expansion": "^1.0.0" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "recur-fs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/recur-fs/-/recur-fs-2.2.4.tgz", + "integrity": "sha512-VofuavbR3PNwHAs2uxn2HNcyyXKIUBayVtdhdElJ8qqSmcr2TY71THQjAoF+PT1xEeUnEi57DWT2/+YSRCvr3w==", + "requires": { + "minimatch": "3.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.5.4" + } + }, + "rimraf": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", + "integrity": "sha512-Lw7SHMjssciQb/rRz7JyPIy9+bbUshEucPoLRvWqy09vC5zQixl8Uet+Zl+SROBB/JMWHJRdCk1qdxNWHNMvlQ==", + "requires": { + "glob": "^7.0.5" + } + }, + "undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + } +} diff --git a/.svn/pristine/19/192434ba4f3b985d0c6ead49a69464d1dd72cb7e.svn-base b/.svn/pristine/19/192434ba4f3b985d0c6ead49a69464d1dd72cb7e.svn-base new file mode 100644 index 0000000..63db4dd --- /dev/null +++ b/.svn/pristine/19/192434ba4f3b985d0c6ead49a69464d1dd72cb7e.svn-base @@ -0,0 +1,9 @@ +const common = require('./utils/common'); +const apiErrorHandler = require('./api_err_handler'); +const errorHandler = require('./error_handler'); + +module.exports = { + common: common, + apiErrorHandler: apiErrorHandler, + errorHandler: errorHandler +}; diff --git a/.svn/pristine/1c/1c8509c859b217d0c6c6c5c6940475f3aaea6af5.svn-base b/.svn/pristine/1c/1c8509c859b217d0c6c6c5c6940475f3aaea6af5.svn-base new file mode 100644 index 0000000..0af9105 --- /dev/null +++ b/.svn/pristine/1c/1c8509c859b217d0c6c6c5c6940475f3aaea6af5.svn-base @@ -0,0 +1,25 @@ +'use strict'; + +/** + * Trim the decimal to maximum numbers of digits + * @param {*} value the decimal value + * @param {*} digits maximum number of decimal digits + */ +function fixedTo(value, digits) { + if (!value) return value; + var re = new RegExp('(-?\\d+\\.\\d{' + digits + '})(\\d)'), + m = value.toString().match(re); + return m ? parseFloat(m[1]) : value; +}; + +function padZero(num, size) { + let s = num + ''; + while (s.length < size) { + s = '0' + s; + } + return s; +} + +module.exports = { + fixedTo, padZero +} diff --git a/.svn/pristine/1e/1ebb9cec80105560e1f453c9750101385d778584.svn-base b/.svn/pristine/1e/1ebb9cec80105560e1f453c9750101385d778584.svn-base new file mode 100644 index 0000000..487b0c1 --- /dev/null +++ b/.svn/pristine/1e/1ebb9cec80105560e1f453c9750101385d778584.svn-base @@ -0,0 +1,25 @@ +{ + "name": "binary-package-parser", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "binary-package-parser", + "version": "1.0.0", + "license": "Proprietary", + "dependencies": { + "bit-util": "file:../bit-util" + } + }, + "../bit-util": { + "name": "bit-util", + "version": "1.0.0", + "license": "Proprietary" + }, + "node_modules/bit-util": { + "resolved": "../bit-util", + "link": true + } + } +} diff --git a/.svn/pristine/1f/1f37f37b5fa21ba9627a2a5b23aac2028f55fe04.svn-base b/.svn/pristine/1f/1f37f37b5fa21ba9627a2a5b23aac2028f55fe04.svn-base new file mode 100644 index 0000000..06fb866 --- /dev/null +++ b/.svn/pristine/1f/1f37f37b5fa21ba9627a2a5b23aac2028f55fe04.svn-base @@ -0,0 +1,5 @@ +{ + "name": "bit-util", + "version": "1.0.0", + "lockfileVersion": 1 +} diff --git a/.svn/pristine/1f/1fa94838faf3dd104e8d78a0525f6aa79695e62f.svn-base b/.svn/pristine/1f/1fa94838faf3dd104e8d78a0525f6aa79695e62f.svn-base new file mode 100644 index 0000000..77373aa --- /dev/null +++ b/.svn/pristine/1f/1fa94838faf3dd104e8d78a0525f6aa79695e62f.svn-base @@ -0,0 +1,138 @@ +'use strict'; + +const TCPDevice = require('./tcp-device'); +const numberUtil = require('number-util'); + +const READ_0 = 0; +const READ_1 = 1; +const READ_BLOCKED = 99; + +const START_MARK = Buffer.from('FBFB', 'hex'); +const END_MARK = Buffer.from('0D0A', 'hex'); +const PKG_SIZE = 40; + +class AgNavTCPDevice extends TCPDevice { + + constructor(socket, ops) { + super(socket, ops); + + this.id = null; + this.msgReceived = 0; + + this._startPos = -1; + this._endPos = -1; + this._firstPart = null; + this._secondPart = null; + this._cmdBuf = null; + + this._readState = READ_0; + } + + /** + * _onData is triggered by the "readable" event on the underlying TCP socket. + * It is called each time there is new data * received. It is responsible for reading data from the socket and + * performing the appropriate action given the current read state. + */ + _onData() { + try { + // Loop while there was still data to process on the socket's buffer. + // This will stop when we don't have enough data or encountering a back pressure issue; + let readMore = true; + do { + switch (this._readState) { + case READ_0: + readMore = this._processRead_0(); + break; + case READ_1: + readMore = this._processRead_1(); + break; + case READ_BLOCKED: + readMore = false; + break; + default: + throw new Error('Unknown read state'); + } + } while (readMore); + } catch (err) { + console.log(err); + // Terminate on failures as we won't be able to recovery since data was corrupted and we won't + // be able to any more data without additional errors. + this.destroy(err); + } + } + + _processRead_0() { + this._firstPart = this._socket.read(PKG_SIZE / 2); + if (!this._firstPart) + return false; + + this._startPos = this._firstPart.indexOf(START_MARK); + if (this._startPos === -1) { + this._guardInvalidTries(); + + this._readState = READ_0; + } else { + this._readState = READ_1; + } + return true; + } + + _processRead_1() { + // Read payload data by the length + this._secondPart = this._socket.read(this._startPos + (PKG_SIZE / 2)); + if (!this._secondPart) { + return false; + } + + this._endPos = this._secondPart.indexOf(END_MARK); + + if (this._endPos === -1) { + this._guardInvalidTries(); + + // Could not find the match for end mark => skip the first part to find the next start mark + this.unshift(this._secondPart); + } else { + if ((this._firstPart.length - this._startPos) + this._endPos + END_MARK.length <= PKG_SIZE) { + + this._invalidTry = 0; + this.msgReceived++; + this._cmdBuf = Buffer.alloc(PKG_SIZE - START_MARK.length); + + this._firstPart.copy(this._cmdBuf, 0, this._startPos + START_MARK.length); + this._secondPart.copy(this._cmdBuf, this._firstPart.length - this._startPos - START_MARK.length, 0, this._endPos + END_MARK.length); + + if (!this.id) { + this.id = numberUtil.padZero(this._cmdBuf.readInt32LE(1), 10); + } + + // Check for the case of package size < PKG_SIZE + if (this._secondPart.length > this._endPos + END_MARK.length) { + // return the rest to the internal buffer for the next read + this.unshift(this._secondPart.slice(this._endPos + END_MARK.length)); + } + + // Push the message onto the read buffer for the consumer to read. We are mindful of slow reads by the consumer + // and will respect backpressure signals. + const pushOk = this.push(this._cmdBuf); + if (pushOk) { + this._readState = READ_0; + return true; + } else { + console.log('socket read is blocked'); + this._readState = READ_BLOCKED; + return false; + } + } else { + this._invalidTry++; + if (this._invalidTry > this.ops.maxInvalidPackages) + throw new Error('Reached maximum invalid read try'); + } + } + + this._readState = READ_0; + return true; + } + +} + +module.exports = AgNavTCPDevice; diff --git a/.svn/pristine/2d/2de35d48b8a1bafdb3d2a37e700ef36de0cdeac8.svn-base b/.svn/pristine/2d/2de35d48b8a1bafdb3d2a37e700ef36de0cdeac8.svn-base new file mode 100644 index 0000000..601c9f1 --- /dev/null +++ b/.svn/pristine/2d/2de35d48b8a1bafdb3d2a37e700ef36de0cdeac8.svn-base @@ -0,0 +1,46 @@ +'use strict'; + +/** + * Error handling functions for the REST server applications. + * Notes: This module provides reusable functions to create and handle API errors. + * It includes functions to create application-specific error objects, throw errors, and handle response errors. + * + * However, the error handling logic is just the simplest form of error management. + * @author: Trung Hoang + * @version: 1.0.1 + * @date: 2023-10-01 + * @license: proprietary + * @description: This module provides functions to create and handle API errors. + */ + +const { isAppError } = require('./utils/common'); + +function appError(tag) { + if (!tag) + return { error: { '.tag': 'unknown_error', text: 'Unknown Error' } } + else + return { error: { '.tag': tag } } + // return { error: { '.tag': tag, text: msg ? msg : '' } } +} + +function throwError(tag) { + throw new Error(tag); +} + +function handleResErr(res, err, code = 409) { + if (!res || typeof res !== "object") return; + + const _erCode = code; + const _err = isAppError(err); + if (_err) { + res.status(_erCode || 409).send(appError(_err)); + } else { + res.status(_erCode || 500).send(appError('unknown_error')); + if (err) console.log(err.stack); + } +} + + +module.exports = { + throwError, appError, handleResErr +} diff --git a/.svn/pristine/2d/2df823ca20dfcff5038841358904cf55f186be28.svn-base b/.svn/pristine/2d/2df823ca20dfcff5038841358904cf55f186be28.svn-base new file mode 100644 index 0000000..80f59c3 --- /dev/null +++ b/.svn/pristine/2d/2df823ca20dfcff5038841358904cf55f186be28.svn-base @@ -0,0 +1,20 @@ +{ + "name": "error-handler", + "version": "2.0.0", + "description": "A reusable error handling utility module for Node.js applications.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "Error Handling", + "Utility" + ], + "author": "Trung Hoang", + "license": "Proprietary", + "dependencies": { + "debug": "^4.4.0", + "key-file-storage": "^2.3.3", + "mailer": "file:../mailer" + } +} diff --git a/.svn/pristine/44/44e1fac2641ddf6cfbed1cd4bb04f04e4c667848.svn-base b/.svn/pristine/44/44e1fac2641ddf6cfbed1cd4bb04f04e4c667848.svn-base new file mode 100644 index 0000000..b10eb8a --- /dev/null +++ b/.svn/pristine/44/44e1fac2641ddf6cfbed1cd4bb04f04e4c667848.svn-base @@ -0,0 +1,4 @@ + +module.exports = { + default: require('./mailer.js') +} \ No newline at end of file diff --git a/.svn/pristine/58/58481746c0d1c983d655c4749115639b00ae8b94.svn-base b/.svn/pristine/58/58481746c0d1c983d655c4749115639b00ae8b94.svn-base new file mode 100644 index 0000000..49fb36e --- /dev/null +++ b/.svn/pristine/58/58481746c0d1c983d655c4749115639b00ae8b94.svn-base @@ -0,0 +1,158 @@ +'use strict'; + +const TCPDevice = require('./tcp-device'); + +const READ_0 = 0; +const READ_ID = 1; +const READ_LEN = 2; +const READ_BODY = 3; +const READ_BLOCKED = 99; + +const RESP_ID_HEADER = 0xE1; +const RESP_CMD_HEADER = 0xF1; + +class RAPTCPDevice extends TCPDevice { + + constructor(socket, ops) { + super(socket, ops); + + this.id = null; + this.msgReceived = 0; + + this._idPart = null; + this._lenPart = null; + this._dataLength = 0; + this._bodyPart = null; + this._cmdBuf = null; + + this._readState = READ_0; + } + + /** + * _onData is triggered by the "readable" event on the underlying TCP socket. + * It is called each time there is new data * received. It is responsible for reading data from the socket and + * performing the appropriate action given the current read state. + */ + _onData() { + try { + // Loop while there was still data to process on the socket's buffer. + // This will stop when we don't have enough data or encountering a back pressure issue; + let readMore = true; + do { + switch (this._readState) { + case READ_0: + // this._idPart = null; + // this._lenPart = null; + // this._dataLength = 0; + // this._bodyPart = null; + readMore = this._processRead_0(); + break; + case READ_ID: + readMore = this._processReadId(); + break; + case READ_LEN: + readMore = this._processReadLen(); + break; + case READ_BODY: + readMore = this._processReadBody(); + break; + case READ_BLOCKED: + readMore = false; + break; + default: + throw new Error('Unknown read state'); + } + } while (readMore); + } catch (err) { + // Terminate on failures as we won't be able to recovery since data was corrupted and we won't + // be able to any more data without additional errors. + this.destroy(err); + } + } + + _processRead_0() { + this._firstByte = this._socket.read(1); + if (!this._firstByte) + return false; + + if (this._firstByte[0] === RESP_ID_HEADER) { + this._readState = READ_ID; + } else if (this._firstByte[0] === RESP_CMD_HEADER) { + this._readState = READ_LEN; + } else { + this._readState = READ_0; + this._guardInvalidTries(); + } + + return true; + } + + _getDeviceId(buf) { + buf[7] = 0; // byte 8th is not used in the case ID is IMEI + // Convert this buf to 64 integer number + return buf.readBigUInt64LE(0).toString(); + } + + _processReadId() { + // Try to read the Report Data Length from the 3rd byte of the GPS Response message + // If we cannot read the 8 bytes, the attempt to process the message will abort. + // E1 => [8 byte ID] + this._idPart = this._socket.read(8); + if (!this._idPart) + return false; + + if (!this.id) { + // Parse the Device ID (8bytes) for the IMEI number + const id8byte = this._idPart.slice(0); + if (id8byte[7] === 0x01) + this.id = this._getDeviceId(id8byte); + } + + this._readState = READ_0; + return true; + } + + _processReadLen() { + // E1[8 byte ID]F1 => + this._lenPart = this._socket.read(2); + if (!this._lenPart) + return false; + + this._dataLength = this._lenPart[1]; + + this._readState = READ_BODY; + return true; + } + + _processReadBody() { + // Read payload data by the length + this._bodyPart = this._socket.read(this._dataLength); + if (!this._bodyPart) { + // this._socket.unshift(this._lenPart); // ??? needed + return false; + } + + this._invalidTry = 0; + this.msgReceived++; + this._cmdBuf = Buffer.alloc(this._lenPart.length + this._dataLength); + + // Copy the from read + this._lenPart.copy(this._cmdBuf); + this._bodyPart.copy(this._cmdBuf, this._lenPart.length); + // Push the message onto the read buffer for the consumer to read. We are mindful of slow reads by the consumer + // and will respect backpressure signals. + const pushOk = this.push(this._cmdBuf); + if (pushOk) { + this._readState = READ_0; + return true; + } else { + // debug("socket read is blocked"); + console.log('socket read is blocked'); + this._readState = READ_BLOCKED; + return false; + } + } + +} + +module.exports = RAPTCPDevice; diff --git a/.svn/pristine/6a/6af69bbf2185fb3e059e77851c50a91f67d37865.svn-base b/.svn/pristine/6a/6af69bbf2185fb3e059e77851c50a91f67d37865.svn-base new file mode 100644 index 0000000..7b8608d --- /dev/null +++ b/.svn/pristine/6a/6af69bbf2185fb3e059e77851c50a91f67d37865.svn-base @@ -0,0 +1,18 @@ +{ + "name": "binary-package-parser", + "version": "1.0.0", + "description": "Binary protocol package parsers for parsing binary packages into JSON objects", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "RAP", + "AgNav" + ], + "author": "Trung Hoang", + "license": "Proprietary", + "dependencies": { + "bit-util": "file:../bit-util" + } +} diff --git a/.svn/pristine/6b/6b44a153a408e347197ce33481baa9617ea77dcc.svn-base b/.svn/pristine/6b/6b44a153a408e347197ce33481baa9617ea77dcc.svn-base new file mode 100644 index 0000000..a15a119 --- /dev/null +++ b/.svn/pristine/6b/6b44a153a408e347197ce33481baa9617ea77dcc.svn-base @@ -0,0 +1,3 @@ +exports.TCPDevice = require('./tcp-device'); +exports.AgNavTCPDevice = require('./agnav-device'); +exports.RAPTCPDevice = require('./rap-device'); \ No newline at end of file diff --git a/.svn/pristine/70/70ac0b9a505e464dbe2b071fb9dbc747bcd8a831.svn-base b/.svn/pristine/70/70ac0b9a505e464dbe2b071fb9dbc747bcd8a831.svn-base new file mode 100644 index 0000000..c284b0d --- /dev/null +++ b/.svn/pristine/70/70ac0b9a505e464dbe2b071fb9dbc747bcd8a831.svn-base @@ -0,0 +1,12 @@ +{ + "name": "bit-util", + "version": "1.0.0", + "description": "Bit utilities with bit manipulation functions", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "Trung Hoang", + "license": "Proprietary" +} diff --git a/.svn/pristine/7b/7bef30291ede54b89a09d1a77d9d352b23440bc1.svn-base b/.svn/pristine/7b/7bef30291ede54b89a09d1a77d9d352b23440bc1.svn-base new file mode 100644 index 0000000..d7dfc72 --- /dev/null +++ b/.svn/pristine/7b/7bef30291ede54b89a09d1a77d9d352b23440bc1.svn-base @@ -0,0 +1,87 @@ +'use strict'; + +const util = require('util'), + path = require('path'), + mailer = require('mailer').default, + KeyFileStorage = require("key-file-storage").default, + debug = require('debug')('agm:errHelper'); + +let logFileName = path.basename(__filename); + + +/** + * Report important errors to Admin + * @param {*} error Error message in text or the Error object + */ +async function mailErrorToAdmin(error) { + const errorMsg = (error && error.message) ? error.message : error; + const contentOps = { + subject: '[Agm-Errors] Unexpected Errors', + body: error && error.stack ? errorMsg + '\n' + error.stack : errorMsg, + to: process.env.AGM_ADM_EMAIL || '"AgMission Admin" ', + }; + return await mailer.sendTextMail(contentOps); +} + +async function _handleCriticalError(error, message) { + const exitFunc = (err) => { + debug(err); + process.exit(1); + }; + try { + const baseFilename = path.basename(logFileName); + const kfs = KeyFileStorage(path.dirname(logFileName), false); + + let lastReport = await kfs[baseFilename]; + let curDateTime = new Date(); + + if (kfs[baseFilename] && (lastReport = kfs[baseFilename])) { + if (lastReport.when && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*$/.test(lastReport.when)) { + if (curDateTime - new Date(lastReport.when) < 2 * 60 * 1e3 + && ((error.code && lastReport.code === error.code) || lastReport.message == error.message)) + return exitFunc(error); // Reported the same error two shortly, at least 2 minutes => ignore to avoid flooding with the same emails + } + } + const logObj = { + code: error.code, + message: error.message, + when: curDateTime + }; + await kfs(baseFilename, logObj); + } catch (err) { + debug(err); + } + + try { + await mailErrorToAdmin(message); + } + catch (err) { + debug(err); + } + finally { + return exitFunc(error); + } +} + +/** + * Register the process error handlers + * @param {*} process The process object + * @param {*} fileName The file name to log the error + */ +function registerUnCaughtProcessErrorsHandler(process, fileName) { + if (!process) return; + + if (fileName) logFileName = fileName; + + process + .on('uncaughtException', async (err) => { + await _handleCriticalError(err, util.format('uncaughtException:', err)); + }) + .on('unhandledRejection', async (reason, p) => { + await _handleCriticalError(reason, util.format(reason, 'Unhandled Rejection at Promise', p)); + }); +} + +module.exports = { + mailErrorToAdmin, registerUnCaughtProcessErrorsHandler +} diff --git a/.svn/pristine/8a/8a271fb1896aad596884a89d462fa5c9d8735062.svn-base b/.svn/pristine/8a/8a271fb1896aad596884a89d462fa5c9d8735062.svn-base new file mode 100644 index 0000000..8b665b7 --- /dev/null +++ b/.svn/pristine/8a/8a271fb1896aad596884a89d462fa5c9d8735062.svn-base @@ -0,0 +1,10 @@ +'use restrict'; + +function isAppError(err) { + const _err = err && err.message || err || ''; + return (err && (typeof _err === "string" && _err.includes('_'))) ? _err : ''; +} + +module.exports = { + isAppError +}; \ No newline at end of file diff --git a/.svn/pristine/95/9504a2fcbace36bc66cf9d20b79bf8fdf64aeb9a.svn-base b/.svn/pristine/95/9504a2fcbace36bc66cf9d20b79bf8fdf64aeb9a.svn-base new file mode 100644 index 0000000..8f93d77 --- /dev/null +++ b/.svn/pristine/95/9504a2fcbace36bc66cf9d20b79bf8fdf64aeb9a.svn-base @@ -0,0 +1,93 @@ +'use strict'; + +const assert = require('assert'), + bitUtil = require('bit-util'); + +const GPS_13 = 0x13; + +/** + * RAPParser is responsible for parsing RAP binary records + */ +class RAPParser { + + constructor() { + // Using these local vars to avoid GC for more memory efficency + this._hour = 0; + this._minute = 0; + this._second = 0; + this._year = 0; + this._month = 0; + this._day = 0; + this._byte = 0; + } + + /** + * Parse RAP binary response package + * @param {*} rapPkg The binary package data in Buffer. Structure: , Cmd = 0x13 + * @returns a GPS data object + */ + parse(rapPkg) { + let record; + if (rapPkg && rapPkg[0] >= GPS_13) { + assert.ok(Buffer.isBuffer(rapPkg), 'rapPkg argument must be an instance of Buffer'); // prettier-ignore + + // Byte 19th, if bit 4 is set -> has GPS Fix + if (rapPkg[18] && bitUtil.isBitOn(rapPkg[18], 4)) { + record = {}; + let offset = 2; // Add 2 to skip the data package header to read the data + + record.lat = rapPkg.readInt32LE(offset) * 1e-5; offset += 4; + record.lon = rapPkg.readInt32LE(offset) * 1e-5; offset += 4; + record.speed = rapPkg.readUInt8(offset); offset += 1; + // GPS Direction in 2 Degree Increasements + record.head = rapPkg.readUInt8(offset) << 1; offset += 1; + + this._hour = rapPkg.readUInt8(offset); offset += 1; + this._minute = rapPkg.readUInt8(offset); offset += 1; + this._second = rapPkg.readUInt8(offset); offset += 1; + this._year = 2000 + rapPkg.readUInt8(offset); offset += 1; + this._month = rapPkg.readUInt8(offset); offset += 1; + this._day = rapPkg.readUInt8(offset); offset += 1; + + // Month in JS 0 => 11 + record.gdt = new Date(Date.UTC(this._year, this._month - 1, this._day, this._hour, this._minute, this._second)); + + record.inputs = 0; + + // Cmd 0x13. Byte 19th + this._byte = rapPkg.readUInt8(offset); offset += 1; + + // Cmd 0x13. Byte 19th. Bits 0-3: numbers of GPS setellites + record.sats = bitUtil.extractBits(this._byte, 4, 0); + + // Cmd 0x13. Byte 20th + this._byte = rapPkg.readUInt8(offset); + + // Cmd 0x13. Byte 20th. Bit 3: Inputs+ + if (bitUtil.isBitOn(this._byte, 3)) { + + // Cmd 0x13. Byte 20th. Bit 0: 1 => extra inputs (21-24) + offset += bitUtil.isBitOn(this._byte, 0) ? 4 : 1; + + // Skip reading Odometer data + + // Cmd 0x13. Byte 21th/25th + this._byte = rapPkg.readUInt8(offset); offset += 1; + + if (bitUtil.isBitOn(this._byte, 0)) { + // Cmd 0x13. Byte 22th/25th: Whether Digital input state present + this._byte = rapPkg.readUInt8(offset); offset += 1; + + // Digital inputs (Up to 5, use only input 1 for Spray on/off, 0: spray on and 1 is off) + // Check Input 1 (bit 0) for spray on/off + record.inputs = bitUtil.isBitOn(this._byte, 0) ? 0 : 1; + } + } + + } + } + return record; + } +} + +module.exports = RAPParser; \ No newline at end of file diff --git a/.svn/pristine/a1/a160718f5f7e08a0bf8cc50d7d366109ed77976c.svn-base b/.svn/pristine/a1/a160718f5f7e08a0bf8cc50d7d366109ed77976c.svn-base new file mode 100644 index 0000000..379e55b --- /dev/null +++ b/.svn/pristine/a1/a160718f5f7e08a0bf8cc50d7d366109ed77976c.svn-base @@ -0,0 +1,84 @@ +'use strict'; + +const nodemailer = require('nodemailer'), + debug = require('debug')('app:mailer'); + +const createConfig = (env) => { + const isSecure = env?.SMTP_SECURE || process.env.SMTP_SECURE || false; + const port = env?.SMTP_PORT || process.env.SMTP_PORT || 587; + + const config = { + host: env?.SMTP_HOST || process.env.SMTP_HOST, + port: port, + secure: isSecure, + auth: { + user: env?.SMTP_USR || process.env.SMTP_USR, + pass: env?.SMTP_PWD || process.env.SMTP_PWD, + }, + sender: env?.SMTP_SENDER || process.env.SMTP_SENDER || `"AgNav Inc." ` + }; + + if (!config.host || !config.port || !config.auth.user || !config.auth.pass) { + throw new Error('SMTP configuration is missing required fields (host, port, user, pass).'); + } + return config; +}; + +const createMailOptions = (from, to, subject, body) => ({ + from: from, to: to, subject: subject, text: body +}); + +function createMailer(env) { + const config = createConfig(env); + + const transporter = nodemailer.createTransport(config); + + return { + sendMail: async (to, subject, body) => { + try { + if (!to || !subject || !body) { + throw new Error('Missing required fields: to, subject, or body'); + } + + const mailOptions = createMailOptions(config.sender, to, subject, body); + const info = await transporter.sendMail(mailOptions); + + return { success: true, message: 'Mail sent successfully', messageId: info.messageId }; + } catch (error) { + debug('Error sending mail:', error.message); + return { success: false, error: error.message }; + } + }, + }; +} + +/** + * Send a text email + * @param {Object} options Email options + * @param {string} options.from Sender of the email (Optional) + * @param {string} options.subject Subject of the email + * @param {string} options.to Recipient of the email + * @param {string} options.text Text content of the email + */ +async function sendTextMail(options, env) { + const { from, to, subject, body } = options; + if (!to || !subject || !body) { + throw new Error('Missing required fields: to, subject, or body'); + } + const config = createConfig(env); + + const transporter = nodemailer.createTransport(config); + const mailOptions = createMailOptions(from || config.sender, to, subject, body); + + try { + const info = await transporter.sendMail(mailOptions); + return { success: true, messageId: info.messageId }; + } catch (error) { + debug('Error sending email:', error.message); + return { success: false, error: error.message }; + } +} + +module.exports = { + createMailer, sendTextMail +}; \ No newline at end of file diff --git a/.svn/pristine/b5/b56e38fe97c77ccec58cb50acbd60bd3a8888317.svn-base b/.svn/pristine/b5/b56e38fe97c77ccec58cb50acbd60bd3a8888317.svn-base new file mode 100644 index 0000000..0446c69 --- /dev/null +++ b/.svn/pristine/b5/b56e38fe97c77ccec58cb50acbd60bd3a8888317.svn-base @@ -0,0 +1,19 @@ +{ + "name": "mailer", + "version": "1.0.0", + "description": "Simple reusable mailer utility.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "mailer", + "utility" + ], + "author": "Trung Hoang", + "license": "ISC", + "dependencies": { + "debug": "^4.4.0", + "nodemailer": "^6.10.0" + } +} \ No newline at end of file diff --git a/.svn/pristine/bd/bd21ca0809c8c4c2c5d7524faf6827144cfb06e9.svn-base b/.svn/pristine/bd/bd21ca0809c8c4c2c5d7524faf6827144cfb06e9.svn-base new file mode 100644 index 0000000..67b7e0e --- /dev/null +++ b/.svn/pristine/bd/bd21ca0809c8c4c2c5d7524faf6827144cfb06e9.svn-base @@ -0,0 +1,88 @@ +'use strict'; + +const stream = require('stream'), + Socket = require('net').Socket, + assert = require('assert'); + +/** + * Base TCPDevice class for handling TCP socket from tracking device + * + */ +class TCPDevice extends stream.Duplex { + + constructor(socket, ops) { + super(ops); + + this.ops = Object.assign({}, { + maxInvalidReadTry: 5 + }, ops); + + // Perform type assertions + assert.ok(socket instanceof Socket, 'socket argument must be an instance of Socket'); // prettier-ignore + + this.id = null; + this.msgReceived = 0; + this._invalidTry = 0; + + this._socket = socket; + this._socket.on('close', hadError => this.emit('close', hadError)); + // this._socket.on("connect", () => console.log("Client connected !")); + this._socket.on('drain', () => this.emit('drain')); + this._socket.on('end', () => this.emit('end')); + this._socket.on('error', err => this.emit('error', err)); + this._socket.on('lookup', (e, a, f, h) => this.emit('lookup', e, a, f, h)); + this._socket.on('readable', this._onData.bind(this)); + this._socket.on('timeout', () => this.emit('timeout')); + } + + /** + * Half-closes the socket. It is still possible that the opposite + * side is still sending data. + */ + end() { + this._socket.end(); + return this; + } + + /** + * Destroys the socket and ensures that no more I/O activity happens + * on the socket. When an `err` is included, an 'error' event will + * be emitted and all listeners will receive the error as an + * argument. + * @param err optional error to send + */ + destroy(err) { + this._socket.destroy(err); + return this; + } + + _guardInvalidTries() { + this._invalidTry++; + if (this._invalidTry > this.ops.maxInvalidReadTry) + throw new Error('Reached maximum invalid read try'); + } + + /** + * _onData is triggered by the "readable" event on the underlying TCP socket. + * It is called each time there is new data * received. It is responsible for reading data from the socket and * + * performing the appropriate action given the current read state. + * @virtual Extenders implementation must override this menthod acccording to the detail of the protocol + */ + _onData() { } + + _read() { + + // Trigger a read but wait until the end of the event loop. + // This is necessary when reading in paused mode where + // _read was triggered by stream.read() originating inside + // a "readable" event handler. Attempting to push more data + // synchronously will not trigger another "readable" event. + setImmediate(() => this._onData()); + } + + _write() { } + _final() { } + +} + +module.exports = TCPDevice; \ No newline at end of file diff --git a/.svn/pristine/cd/cde2c25f9e2d816ff040c0c41839082d4caef692.svn-base b/.svn/pristine/cd/cde2c25f9e2d816ff040c0c41839082d4caef692.svn-base new file mode 100644 index 0000000..6ee8fb1 --- /dev/null +++ b/.svn/pristine/cd/cde2c25f9e2d816ff040c0c41839082d4caef692.svn-base @@ -0,0 +1,74 @@ +'use strict'; + +const assert = require('assert'); + +/* Agnav Tracking binary protocol +trk_pos_type = Record {40 bytes} + {Header} + trk_Header1 : byte {0xFB} + trk_Header2 : byte {0xFB} + trk_DataID : byte {0xF7} + trk_ACID : LongInt; {4 bytes signed Integer} + trk_Time : LongInt // Number of seconds since 1970, 1, 1 to current UTC time + trk_Lat : float; {4 byte floating point} + trk_Lon : float; {4 byte floating point} + trk_Alt : integer; {GPS alt, m, 2 byte signed integer} + trk_SprayStat: byte; {0: off, 1: on, unsigned char} + trk_Speed : word; {dm/s, speed * 10 2 bytes signed integer} + trk_Heading : integer; { 0 - 359} + trk_Temp : byte; {C deg, offset by 100, unsigned char} + trk_Humid : byte; {0 - 100%, unsigned char} + trk_AppRate : integer {Application rate L/Ha * 10} + trk_Wspd : integer; {Windspeed * 10 dm/s} + trk_Whdg : integer; {Wind heading, 0-359} + trk_Res : byte; {reserved = 0, unsigned char} + trk_Chksum : word; {Sum of all bytes start from FB} + trk_End : word; {end of record, 0x0D 0x0A} + End; + +** NOTES: +byte : 1-byte unsigned integer +word : 2-byte unsigned int +integer : 2-byte signed int +LongInt : 4-byte signed int +float : 4-byte floating number + */ +class RAPParser { + + /** + * Parse AgNav Tracking binary response package + * @param {*} agnPkg The package data in Buffer. Structure: , Cmd = 0xF7 + * @returns a GPS data object + */ + parse(agnPkg) { + assert.ok(Buffer.isBuffer(agnPkg), 'agnPkg argument must be an instance of Buffer'); // prettier-ignore + assert.ok(agnPkg.length == 38, 'agnPkg must have 40 bytes'); // prettier-ignore + + let record, offset; + + record = {}; + offset = 5; // Skip the first Cmd byte (F7) + 4 bytes Aircraft Id + + record.gdt = new Date(agnPkg.readInt32LE(offset) * 1000); offset += 4; + record.lat = agnPkg.readFloatLE(offset); offset += 4; + record.lon = agnPkg.readFloatLE(offset); offset += 4; + record.alt = agnPkg.readInt16LE(offset); offset += 2; + record.inputs = agnPkg.readUInt8(offset); offset += 1; + + record.speed = agnPkg.readInt16LE(offset); offset += 2; + if (record.speed > 0) + record.speed = record.speed * 0.36; // dm/s to km/h + + record.head = agnPkg.readInt16LE(offset); offset += 2; + record.temp = agnPkg.readUInt8(offset) - 100; offset += 1; + record.humid = agnPkg.readUInt8(offset); offset += 1; + record.appRate = agnPkg.readInt16LE(offset) * 1e-1; offset += 2; + record.windSpd = agnPkg.readInt16LE(offset); offset += 2; + record.windHdg = agnPkg.readInt16LE(offset); offset += 2; + + agnPkg = null; + return record; + } +} + +module.exports = RAPParser; \ No newline at end of file diff --git a/.svn/pristine/d8/d8e06b6576134b8c59ea2e8dda57152ddeb13774.svn-base b/.svn/pristine/d8/d8e06b6576134b8c59ea2e8dda57152ddeb13774.svn-base new file mode 100644 index 0000000..1d81539 --- /dev/null +++ b/.svn/pristine/d8/d8e06b6576134b8c59ea2e8dda57152ddeb13774.svn-base @@ -0,0 +1,14 @@ +{ + "name": "device-tcp-socket", + "version": "1.1.0", + "description": "Device tcp socket implementation", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Trung Hoang ", + "license": "Proprietary", + "dependencies": { + "number-util": "file:../number-util" + } +} diff --git a/.svn/pristine/dc/dcf362ca886adc2f890186ccf09c4b04fba63789.svn-base b/.svn/pristine/dc/dcf362ca886adc2f890186ccf09c4b04fba63789.svn-base new file mode 100644 index 0000000..3e5bb09 --- /dev/null +++ b/.svn/pristine/dc/dcf362ca886adc2f890186ccf09c4b04fba63789.svn-base @@ -0,0 +1,32 @@ +'use strict'; + +/** + * Extract n bits from a position + * @param numVal the number + * @param bits number of bits to extract + * @param from the bit postion right to left to extract. Zero index. + */ +function extractBits(numVal, bits, from) { + let _from = Math.max(0, from); + let _bits = Math.abs(bits); + return (((1 << _bits) - 1) & (numVal >> _from)); +}; + +/** + * Check if k-th bit of a given number is set or not using right shift operator. + * @param {*} n the number value to check + * @param {*} at the bit position. Zero index. + */ +function isBitOn(n, bit) { + let _at = Math.max(0, bit); + return ((n >> _at) & 1); +}; + +function bytesToHex(bytes, delimeter = "") { + return bytes && Array.isArray(bytes) ? Array.from( + bytes, + byte => byte.toString(16).padStart(2, "0").toUpperCase() + ).join(delimeter) : ''; +} + +module.exports = { extractBits, isBitOn, bytesToHex } \ No newline at end of file diff --git a/.svn/pristine/f1/f10beb76363a6f58ee06a5ca34c12c7f4d8dabf8.svn-base b/.svn/pristine/f1/f10beb76363a6f58ee06a5ca34c12c7f4d8dabf8.svn-base new file mode 100644 index 0000000..d9b6003 --- /dev/null +++ b/.svn/pristine/f1/f10beb76363a6f58ee06a5ca34c12c7f4d8dabf8.svn-base @@ -0,0 +1,5 @@ +{ + "name": "number-util", + "version": "1.1.0", + "lockfileVersion": 1 +} diff --git a/.svn/pristine/fb/fb5795118a78540d7c133b2c4ce836611ff74e88.svn-base b/.svn/pristine/fb/fb5795118a78540d7c133b2c4ce836611ff74e88.svn-base new file mode 100644 index 0000000..2b73cb6 --- /dev/null +++ b/.svn/pristine/fb/fb5795118a78540d7c133b2c4ce836611ff74e88.svn-base @@ -0,0 +1,12 @@ +{ + "name": "number-util", + "version": "1.1.0", + "description": "Number utilities", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "Trung Hoang", + "license": "Proprietary" +} diff --git a/.svn/wc.db b/.svn/wc.db new file mode 100644 index 0000000..5af3745 Binary files /dev/null and b/.svn/wc.db differ diff --git a/.svn/wc.db-journal b/.svn/wc.db-journal new file mode 100644 index 0000000..e69de29 diff --git a/binary-package-parser/agn-parser.js b/binary-package-parser/agn-parser.js new file mode 100644 index 0000000..6ee8fb1 --- /dev/null +++ b/binary-package-parser/agn-parser.js @@ -0,0 +1,74 @@ +'use strict'; + +const assert = require('assert'); + +/* Agnav Tracking binary protocol +trk_pos_type = Record {40 bytes} + {Header} + trk_Header1 : byte {0xFB} + trk_Header2 : byte {0xFB} + trk_DataID : byte {0xF7} + trk_ACID : LongInt; {4 bytes signed Integer} + trk_Time : LongInt // Number of seconds since 1970, 1, 1 to current UTC time + trk_Lat : float; {4 byte floating point} + trk_Lon : float; {4 byte floating point} + trk_Alt : integer; {GPS alt, m, 2 byte signed integer} + trk_SprayStat: byte; {0: off, 1: on, unsigned char} + trk_Speed : word; {dm/s, speed * 10 2 bytes signed integer} + trk_Heading : integer; { 0 - 359} + trk_Temp : byte; {C deg, offset by 100, unsigned char} + trk_Humid : byte; {0 - 100%, unsigned char} + trk_AppRate : integer {Application rate L/Ha * 10} + trk_Wspd : integer; {Windspeed * 10 dm/s} + trk_Whdg : integer; {Wind heading, 0-359} + trk_Res : byte; {reserved = 0, unsigned char} + trk_Chksum : word; {Sum of all bytes start from FB} + trk_End : word; {end of record, 0x0D 0x0A} + End; + +** NOTES: +byte : 1-byte unsigned integer +word : 2-byte unsigned int +integer : 2-byte signed int +LongInt : 4-byte signed int +float : 4-byte floating number + */ +class RAPParser { + + /** + * Parse AgNav Tracking binary response package + * @param {*} agnPkg The package data in Buffer. Structure: , Cmd = 0xF7 + * @returns a GPS data object + */ + parse(agnPkg) { + assert.ok(Buffer.isBuffer(agnPkg), 'agnPkg argument must be an instance of Buffer'); // prettier-ignore + assert.ok(agnPkg.length == 38, 'agnPkg must have 40 bytes'); // prettier-ignore + + let record, offset; + + record = {}; + offset = 5; // Skip the first Cmd byte (F7) + 4 bytes Aircraft Id + + record.gdt = new Date(agnPkg.readInt32LE(offset) * 1000); offset += 4; + record.lat = agnPkg.readFloatLE(offset); offset += 4; + record.lon = agnPkg.readFloatLE(offset); offset += 4; + record.alt = agnPkg.readInt16LE(offset); offset += 2; + record.inputs = agnPkg.readUInt8(offset); offset += 1; + + record.speed = agnPkg.readInt16LE(offset); offset += 2; + if (record.speed > 0) + record.speed = record.speed * 0.36; // dm/s to km/h + + record.head = agnPkg.readInt16LE(offset); offset += 2; + record.temp = agnPkg.readUInt8(offset) - 100; offset += 1; + record.humid = agnPkg.readUInt8(offset); offset += 1; + record.appRate = agnPkg.readInt16LE(offset) * 1e-1; offset += 2; + record.windSpd = agnPkg.readInt16LE(offset); offset += 2; + record.windHdg = agnPkg.readInt16LE(offset); offset += 2; + + agnPkg = null; + return record; + } +} + +module.exports = RAPParser; \ No newline at end of file diff --git a/binary-package-parser/index.js b/binary-package-parser/index.js new file mode 100644 index 0000000..41d6788 --- /dev/null +++ b/binary-package-parser/index.js @@ -0,0 +1,2 @@ +module.exports.RAPParser = require('./rap-parser'); +module.exports.AgNavParser = require('./agn-parser'); \ No newline at end of file diff --git a/binary-package-parser/package-lock.json b/binary-package-parser/package-lock.json new file mode 100644 index 0000000..487b0c1 --- /dev/null +++ b/binary-package-parser/package-lock.json @@ -0,0 +1,25 @@ +{ + "name": "binary-package-parser", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "binary-package-parser", + "version": "1.0.0", + "license": "Proprietary", + "dependencies": { + "bit-util": "file:../bit-util" + } + }, + "../bit-util": { + "name": "bit-util", + "version": "1.0.0", + "license": "Proprietary" + }, + "node_modules/bit-util": { + "resolved": "../bit-util", + "link": true + } + } +} diff --git a/binary-package-parser/package.json b/binary-package-parser/package.json new file mode 100644 index 0000000..7b8608d --- /dev/null +++ b/binary-package-parser/package.json @@ -0,0 +1,18 @@ +{ + "name": "binary-package-parser", + "version": "1.0.0", + "description": "Binary protocol package parsers for parsing binary packages into JSON objects", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "RAP", + "AgNav" + ], + "author": "Trung Hoang", + "license": "Proprietary", + "dependencies": { + "bit-util": "file:../bit-util" + } +} diff --git a/binary-package-parser/rap-parser.js b/binary-package-parser/rap-parser.js new file mode 100644 index 0000000..8f93d77 --- /dev/null +++ b/binary-package-parser/rap-parser.js @@ -0,0 +1,93 @@ +'use strict'; + +const assert = require('assert'), + bitUtil = require('bit-util'); + +const GPS_13 = 0x13; + +/** + * RAPParser is responsible for parsing RAP binary records + */ +class RAPParser { + + constructor() { + // Using these local vars to avoid GC for more memory efficency + this._hour = 0; + this._minute = 0; + this._second = 0; + this._year = 0; + this._month = 0; + this._day = 0; + this._byte = 0; + } + + /** + * Parse RAP binary response package + * @param {*} rapPkg The binary package data in Buffer. Structure: , Cmd = 0x13 + * @returns a GPS data object + */ + parse(rapPkg) { + let record; + if (rapPkg && rapPkg[0] >= GPS_13) { + assert.ok(Buffer.isBuffer(rapPkg), 'rapPkg argument must be an instance of Buffer'); // prettier-ignore + + // Byte 19th, if bit 4 is set -> has GPS Fix + if (rapPkg[18] && bitUtil.isBitOn(rapPkg[18], 4)) { + record = {}; + let offset = 2; // Add 2 to skip the data package header to read the data + + record.lat = rapPkg.readInt32LE(offset) * 1e-5; offset += 4; + record.lon = rapPkg.readInt32LE(offset) * 1e-5; offset += 4; + record.speed = rapPkg.readUInt8(offset); offset += 1; + // GPS Direction in 2 Degree Increasements + record.head = rapPkg.readUInt8(offset) << 1; offset += 1; + + this._hour = rapPkg.readUInt8(offset); offset += 1; + this._minute = rapPkg.readUInt8(offset); offset += 1; + this._second = rapPkg.readUInt8(offset); offset += 1; + this._year = 2000 + rapPkg.readUInt8(offset); offset += 1; + this._month = rapPkg.readUInt8(offset); offset += 1; + this._day = rapPkg.readUInt8(offset); offset += 1; + + // Month in JS 0 => 11 + record.gdt = new Date(Date.UTC(this._year, this._month - 1, this._day, this._hour, this._minute, this._second)); + + record.inputs = 0; + + // Cmd 0x13. Byte 19th + this._byte = rapPkg.readUInt8(offset); offset += 1; + + // Cmd 0x13. Byte 19th. Bits 0-3: numbers of GPS setellites + record.sats = bitUtil.extractBits(this._byte, 4, 0); + + // Cmd 0x13. Byte 20th + this._byte = rapPkg.readUInt8(offset); + + // Cmd 0x13. Byte 20th. Bit 3: Inputs+ + if (bitUtil.isBitOn(this._byte, 3)) { + + // Cmd 0x13. Byte 20th. Bit 0: 1 => extra inputs (21-24) + offset += bitUtil.isBitOn(this._byte, 0) ? 4 : 1; + + // Skip reading Odometer data + + // Cmd 0x13. Byte 21th/25th + this._byte = rapPkg.readUInt8(offset); offset += 1; + + if (bitUtil.isBitOn(this._byte, 0)) { + // Cmd 0x13. Byte 22th/25th: Whether Digital input state present + this._byte = rapPkg.readUInt8(offset); offset += 1; + + // Digital inputs (Up to 5, use only input 1 for Spray on/off, 0: spray on and 1 is off) + // Check Input 1 (bit 0) for spray on/off + record.inputs = bitUtil.isBitOn(this._byte, 0) ? 0 : 1; + } + } + + } + } + return record; + } +} + +module.exports = RAPParser; \ No newline at end of file diff --git a/bit-util/index.js b/bit-util/index.js new file mode 100644 index 0000000..3e5bb09 --- /dev/null +++ b/bit-util/index.js @@ -0,0 +1,32 @@ +'use strict'; + +/** + * Extract n bits from a position + * @param numVal the number + * @param bits number of bits to extract + * @param from the bit postion right to left to extract. Zero index. + */ +function extractBits(numVal, bits, from) { + let _from = Math.max(0, from); + let _bits = Math.abs(bits); + return (((1 << _bits) - 1) & (numVal >> _from)); +}; + +/** + * Check if k-th bit of a given number is set or not using right shift operator. + * @param {*} n the number value to check + * @param {*} at the bit position. Zero index. + */ +function isBitOn(n, bit) { + let _at = Math.max(0, bit); + return ((n >> _at) & 1); +}; + +function bytesToHex(bytes, delimeter = "") { + return bytes && Array.isArray(bytes) ? Array.from( + bytes, + byte => byte.toString(16).padStart(2, "0").toUpperCase() + ).join(delimeter) : ''; +} + +module.exports = { extractBits, isBitOn, bytesToHex } \ No newline at end of file diff --git a/bit-util/package-lock.json b/bit-util/package-lock.json new file mode 100644 index 0000000..06fb866 --- /dev/null +++ b/bit-util/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "bit-util", + "version": "1.0.0", + "lockfileVersion": 1 +} diff --git a/bit-util/package.json b/bit-util/package.json new file mode 100644 index 0000000..c284b0d --- /dev/null +++ b/bit-util/package.json @@ -0,0 +1,12 @@ +{ + "name": "bit-util", + "version": "1.0.0", + "description": "Bit utilities with bit manipulation functions", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "Trung Hoang", + "license": "Proprietary" +} diff --git a/device-tcp-socket/agnav-device.js b/device-tcp-socket/agnav-device.js new file mode 100644 index 0000000..77373aa --- /dev/null +++ b/device-tcp-socket/agnav-device.js @@ -0,0 +1,138 @@ +'use strict'; + +const TCPDevice = require('./tcp-device'); +const numberUtil = require('number-util'); + +const READ_0 = 0; +const READ_1 = 1; +const READ_BLOCKED = 99; + +const START_MARK = Buffer.from('FBFB', 'hex'); +const END_MARK = Buffer.from('0D0A', 'hex'); +const PKG_SIZE = 40; + +class AgNavTCPDevice extends TCPDevice { + + constructor(socket, ops) { + super(socket, ops); + + this.id = null; + this.msgReceived = 0; + + this._startPos = -1; + this._endPos = -1; + this._firstPart = null; + this._secondPart = null; + this._cmdBuf = null; + + this._readState = READ_0; + } + + /** + * _onData is triggered by the "readable" event on the underlying TCP socket. + * It is called each time there is new data * received. It is responsible for reading data from the socket and + * performing the appropriate action given the current read state. + */ + _onData() { + try { + // Loop while there was still data to process on the socket's buffer. + // This will stop when we don't have enough data or encountering a back pressure issue; + let readMore = true; + do { + switch (this._readState) { + case READ_0: + readMore = this._processRead_0(); + break; + case READ_1: + readMore = this._processRead_1(); + break; + case READ_BLOCKED: + readMore = false; + break; + default: + throw new Error('Unknown read state'); + } + } while (readMore); + } catch (err) { + console.log(err); + // Terminate on failures as we won't be able to recovery since data was corrupted and we won't + // be able to any more data without additional errors. + this.destroy(err); + } + } + + _processRead_0() { + this._firstPart = this._socket.read(PKG_SIZE / 2); + if (!this._firstPart) + return false; + + this._startPos = this._firstPart.indexOf(START_MARK); + if (this._startPos === -1) { + this._guardInvalidTries(); + + this._readState = READ_0; + } else { + this._readState = READ_1; + } + return true; + } + + _processRead_1() { + // Read payload data by the length + this._secondPart = this._socket.read(this._startPos + (PKG_SIZE / 2)); + if (!this._secondPart) { + return false; + } + + this._endPos = this._secondPart.indexOf(END_MARK); + + if (this._endPos === -1) { + this._guardInvalidTries(); + + // Could not find the match for end mark => skip the first part to find the next start mark + this.unshift(this._secondPart); + } else { + if ((this._firstPart.length - this._startPos) + this._endPos + END_MARK.length <= PKG_SIZE) { + + this._invalidTry = 0; + this.msgReceived++; + this._cmdBuf = Buffer.alloc(PKG_SIZE - START_MARK.length); + + this._firstPart.copy(this._cmdBuf, 0, this._startPos + START_MARK.length); + this._secondPart.copy(this._cmdBuf, this._firstPart.length - this._startPos - START_MARK.length, 0, this._endPos + END_MARK.length); + + if (!this.id) { + this.id = numberUtil.padZero(this._cmdBuf.readInt32LE(1), 10); + } + + // Check for the case of package size < PKG_SIZE + if (this._secondPart.length > this._endPos + END_MARK.length) { + // return the rest to the internal buffer for the next read + this.unshift(this._secondPart.slice(this._endPos + END_MARK.length)); + } + + // Push the message onto the read buffer for the consumer to read. We are mindful of slow reads by the consumer + // and will respect backpressure signals. + const pushOk = this.push(this._cmdBuf); + if (pushOk) { + this._readState = READ_0; + return true; + } else { + console.log('socket read is blocked'); + this._readState = READ_BLOCKED; + return false; + } + } else { + this._invalidTry++; + if (this._invalidTry > this.ops.maxInvalidPackages) + throw new Error('Reached maximum invalid read try'); + } + } + + this._readState = READ_0; + return true; + } + +} + +module.exports = AgNavTCPDevice; diff --git a/device-tcp-socket/index.js b/device-tcp-socket/index.js new file mode 100644 index 0000000..a15a119 --- /dev/null +++ b/device-tcp-socket/index.js @@ -0,0 +1,3 @@ +exports.TCPDevice = require('./tcp-device'); +exports.AgNavTCPDevice = require('./agnav-device'); +exports.RAPTCPDevice = require('./rap-device'); \ No newline at end of file diff --git a/device-tcp-socket/package.json b/device-tcp-socket/package.json new file mode 100644 index 0000000..1d81539 --- /dev/null +++ b/device-tcp-socket/package.json @@ -0,0 +1,14 @@ +{ + "name": "device-tcp-socket", + "version": "1.1.0", + "description": "Device tcp socket implementation", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Trung Hoang ", + "license": "Proprietary", + "dependencies": { + "number-util": "file:../number-util" + } +} diff --git a/device-tcp-socket/rap-device.js b/device-tcp-socket/rap-device.js new file mode 100644 index 0000000..49fb36e --- /dev/null +++ b/device-tcp-socket/rap-device.js @@ -0,0 +1,158 @@ +'use strict'; + +const TCPDevice = require('./tcp-device'); + +const READ_0 = 0; +const READ_ID = 1; +const READ_LEN = 2; +const READ_BODY = 3; +const READ_BLOCKED = 99; + +const RESP_ID_HEADER = 0xE1; +const RESP_CMD_HEADER = 0xF1; + +class RAPTCPDevice extends TCPDevice { + + constructor(socket, ops) { + super(socket, ops); + + this.id = null; + this.msgReceived = 0; + + this._idPart = null; + this._lenPart = null; + this._dataLength = 0; + this._bodyPart = null; + this._cmdBuf = null; + + this._readState = READ_0; + } + + /** + * _onData is triggered by the "readable" event on the underlying TCP socket. + * It is called each time there is new data * received. It is responsible for reading data from the socket and + * performing the appropriate action given the current read state. + */ + _onData() { + try { + // Loop while there was still data to process on the socket's buffer. + // This will stop when we don't have enough data or encountering a back pressure issue; + let readMore = true; + do { + switch (this._readState) { + case READ_0: + // this._idPart = null; + // this._lenPart = null; + // this._dataLength = 0; + // this._bodyPart = null; + readMore = this._processRead_0(); + break; + case READ_ID: + readMore = this._processReadId(); + break; + case READ_LEN: + readMore = this._processReadLen(); + break; + case READ_BODY: + readMore = this._processReadBody(); + break; + case READ_BLOCKED: + readMore = false; + break; + default: + throw new Error('Unknown read state'); + } + } while (readMore); + } catch (err) { + // Terminate on failures as we won't be able to recovery since data was corrupted and we won't + // be able to any more data without additional errors. + this.destroy(err); + } + } + + _processRead_0() { + this._firstByte = this._socket.read(1); + if (!this._firstByte) + return false; + + if (this._firstByte[0] === RESP_ID_HEADER) { + this._readState = READ_ID; + } else if (this._firstByte[0] === RESP_CMD_HEADER) { + this._readState = READ_LEN; + } else { + this._readState = READ_0; + this._guardInvalidTries(); + } + + return true; + } + + _getDeviceId(buf) { + buf[7] = 0; // byte 8th is not used in the case ID is IMEI + // Convert this buf to 64 integer number + return buf.readBigUInt64LE(0).toString(); + } + + _processReadId() { + // Try to read the Report Data Length from the 3rd byte of the GPS Response message + // If we cannot read the 8 bytes, the attempt to process the message will abort. + // E1 => [8 byte ID] + this._idPart = this._socket.read(8); + if (!this._idPart) + return false; + + if (!this.id) { + // Parse the Device ID (8bytes) for the IMEI number + const id8byte = this._idPart.slice(0); + if (id8byte[7] === 0x01) + this.id = this._getDeviceId(id8byte); + } + + this._readState = READ_0; + return true; + } + + _processReadLen() { + // E1[8 byte ID]F1 => + this._lenPart = this._socket.read(2); + if (!this._lenPart) + return false; + + this._dataLength = this._lenPart[1]; + + this._readState = READ_BODY; + return true; + } + + _processReadBody() { + // Read payload data by the length + this._bodyPart = this._socket.read(this._dataLength); + if (!this._bodyPart) { + // this._socket.unshift(this._lenPart); // ??? needed + return false; + } + + this._invalidTry = 0; + this.msgReceived++; + this._cmdBuf = Buffer.alloc(this._lenPart.length + this._dataLength); + + // Copy the from read + this._lenPart.copy(this._cmdBuf); + this._bodyPart.copy(this._cmdBuf, this._lenPart.length); + // Push the message onto the read buffer for the consumer to read. We are mindful of slow reads by the consumer + // and will respect backpressure signals. + const pushOk = this.push(this._cmdBuf); + if (pushOk) { + this._readState = READ_0; + return true; + } else { + // debug("socket read is blocked"); + console.log('socket read is blocked'); + this._readState = READ_BLOCKED; + return false; + } + } + +} + +module.exports = RAPTCPDevice; diff --git a/device-tcp-socket/tcp-device.js b/device-tcp-socket/tcp-device.js new file mode 100644 index 0000000..67b7e0e --- /dev/null +++ b/device-tcp-socket/tcp-device.js @@ -0,0 +1,88 @@ +'use strict'; + +const stream = require('stream'), + Socket = require('net').Socket, + assert = require('assert'); + +/** + * Base TCPDevice class for handling TCP socket from tracking device + * + */ +class TCPDevice extends stream.Duplex { + + constructor(socket, ops) { + super(ops); + + this.ops = Object.assign({}, { + maxInvalidReadTry: 5 + }, ops); + + // Perform type assertions + assert.ok(socket instanceof Socket, 'socket argument must be an instance of Socket'); // prettier-ignore + + this.id = null; + this.msgReceived = 0; + this._invalidTry = 0; + + this._socket = socket; + this._socket.on('close', hadError => this.emit('close', hadError)); + // this._socket.on("connect", () => console.log("Client connected !")); + this._socket.on('drain', () => this.emit('drain')); + this._socket.on('end', () => this.emit('end')); + this._socket.on('error', err => this.emit('error', err)); + this._socket.on('lookup', (e, a, f, h) => this.emit('lookup', e, a, f, h)); + this._socket.on('readable', this._onData.bind(this)); + this._socket.on('timeout', () => this.emit('timeout')); + } + + /** + * Half-closes the socket. It is still possible that the opposite + * side is still sending data. + */ + end() { + this._socket.end(); + return this; + } + + /** + * Destroys the socket and ensures that no more I/O activity happens + * on the socket. When an `err` is included, an 'error' event will + * be emitted and all listeners will receive the error as an + * argument. + * @param err optional error to send + */ + destroy(err) { + this._socket.destroy(err); + return this; + } + + _guardInvalidTries() { + this._invalidTry++; + if (this._invalidTry > this.ops.maxInvalidReadTry) + throw new Error('Reached maximum invalid read try'); + } + + /** + * _onData is triggered by the "readable" event on the underlying TCP socket. + * It is called each time there is new data * received. It is responsible for reading data from the socket and * + * performing the appropriate action given the current read state. + * @virtual Extenders implementation must override this menthod acccording to the detail of the protocol + */ + _onData() { } + + _read() { + + // Trigger a read but wait until the end of the event loop. + // This is necessary when reading in paused mode where + // _read was triggered by stream.read() originating inside + // a "readable" event handler. Attempting to push more data + // synchronously will not trigger another "readable" event. + setImmediate(() => this._onData()); + } + + _write() { } + _final() { } + +} + +module.exports = TCPDevice; \ No newline at end of file diff --git a/error-handler/api_err_handler.js b/error-handler/api_err_handler.js new file mode 100644 index 0000000..601c9f1 --- /dev/null +++ b/error-handler/api_err_handler.js @@ -0,0 +1,46 @@ +'use strict'; + +/** + * Error handling functions for the REST server applications. + * Notes: This module provides reusable functions to create and handle API errors. + * It includes functions to create application-specific error objects, throw errors, and handle response errors. + * + * However, the error handling logic is just the simplest form of error management. + * @author: Trung Hoang + * @version: 1.0.1 + * @date: 2023-10-01 + * @license: proprietary + * @description: This module provides functions to create and handle API errors. + */ + +const { isAppError } = require('./utils/common'); + +function appError(tag) { + if (!tag) + return { error: { '.tag': 'unknown_error', text: 'Unknown Error' } } + else + return { error: { '.tag': tag } } + // return { error: { '.tag': tag, text: msg ? msg : '' } } +} + +function throwError(tag) { + throw new Error(tag); +} + +function handleResErr(res, err, code = 409) { + if (!res || typeof res !== "object") return; + + const _erCode = code; + const _err = isAppError(err); + if (_err) { + res.status(_erCode || 409).send(appError(_err)); + } else { + res.status(_erCode || 500).send(appError('unknown_error')); + if (err) console.log(err.stack); + } +} + + +module.exports = { + throwError, appError, handleResErr +} diff --git a/error-handler/error_handler.js b/error-handler/error_handler.js new file mode 100644 index 0000000..d7dfc72 --- /dev/null +++ b/error-handler/error_handler.js @@ -0,0 +1,87 @@ +'use strict'; + +const util = require('util'), + path = require('path'), + mailer = require('mailer').default, + KeyFileStorage = require("key-file-storage").default, + debug = require('debug')('agm:errHelper'); + +let logFileName = path.basename(__filename); + + +/** + * Report important errors to Admin + * @param {*} error Error message in text or the Error object + */ +async function mailErrorToAdmin(error) { + const errorMsg = (error && error.message) ? error.message : error; + const contentOps = { + subject: '[Agm-Errors] Unexpected Errors', + body: error && error.stack ? errorMsg + '\n' + error.stack : errorMsg, + to: process.env.AGM_ADM_EMAIL || '"AgMission Admin" ', + }; + return await mailer.sendTextMail(contentOps); +} + +async function _handleCriticalError(error, message) { + const exitFunc = (err) => { + debug(err); + process.exit(1); + }; + try { + const baseFilename = path.basename(logFileName); + const kfs = KeyFileStorage(path.dirname(logFileName), false); + + let lastReport = await kfs[baseFilename]; + let curDateTime = new Date(); + + if (kfs[baseFilename] && (lastReport = kfs[baseFilename])) { + if (lastReport.when && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*$/.test(lastReport.when)) { + if (curDateTime - new Date(lastReport.when) < 2 * 60 * 1e3 + && ((error.code && lastReport.code === error.code) || lastReport.message == error.message)) + return exitFunc(error); // Reported the same error two shortly, at least 2 minutes => ignore to avoid flooding with the same emails + } + } + const logObj = { + code: error.code, + message: error.message, + when: curDateTime + }; + await kfs(baseFilename, logObj); + } catch (err) { + debug(err); + } + + try { + await mailErrorToAdmin(message); + } + catch (err) { + debug(err); + } + finally { + return exitFunc(error); + } +} + +/** + * Register the process error handlers + * @param {*} process The process object + * @param {*} fileName The file name to log the error + */ +function registerUnCaughtProcessErrorsHandler(process, fileName) { + if (!process) return; + + if (fileName) logFileName = fileName; + + process + .on('uncaughtException', async (err) => { + await _handleCriticalError(err, util.format('uncaughtException:', err)); + }) + .on('unhandledRejection', async (reason, p) => { + await _handleCriticalError(reason, util.format(reason, 'Unhandled Rejection at Promise', p)); + }); +} + +module.exports = { + mailErrorToAdmin, registerUnCaughtProcessErrorsHandler +} diff --git a/error-handler/index.js b/error-handler/index.js new file mode 100644 index 0000000..63db4dd --- /dev/null +++ b/error-handler/index.js @@ -0,0 +1,9 @@ +const common = require('./utils/common'); +const apiErrorHandler = require('./api_err_handler'); +const errorHandler = require('./error_handler'); + +module.exports = { + common: common, + apiErrorHandler: apiErrorHandler, + errorHandler: errorHandler +}; diff --git a/error-handler/package-lock.json b/error-handler/package-lock.json new file mode 100644 index 0000000..c66bc2b --- /dev/null +++ b/error-handler/package-lock.json @@ -0,0 +1,548 @@ +{ + "name": "error-handler", + "version": "2.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "error-handler", + "version": "2.0.0", + "license": "Proprietary", + "dependencies": { + "debug": "^4.4.0", + "key-file-storage": "^2.3.3", + "mailer": "file:../mailer" + } + }, + "../mailer": { + "name": "mailer", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "debug": "^4.4.0", + "nodemailer": "^6.10.0" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/is-valid-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/is-valid-path/-/is-valid-path-0.1.2.tgz", + "integrity": "sha512-BsZtkfiPpnzDWFjSZanYllttVW7/46ayPZkcHBCSFBkBqIO9rWrflUvEmT2tF///hnPLwBJU3TJPzbBxpUEqCg==" + }, + "node_modules/@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-invalid-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-0.1.0.tgz", + "integrity": "sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==", + "dependencies": { + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-valid-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz", + "integrity": "sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==", + "dependencies": { + "is-invalid-path": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/key-file-storage": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/key-file-storage/-/key-file-storage-2.3.3.tgz", + "integrity": "sha512-bqFrbE0ifIq5ahrquGewh5nMub8fdH/nXKMg9CJHdCdD5W/6KQea483Qyss0q53tCOC64CN43DQo9TSvohlbKA==", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "@types/is-valid-path": "^0.1.0", + "fs-extra": "^10.0.0", + "is-valid-path": "^0.1.1", + "recur-fs": "^2.2.4" + } + }, + "node_modules/mailer": { + "resolved": "../mailer", + "link": true + }, + "node_modules/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-NyXjqu1IwcqH6nv5vmMtaG3iw7kdV3g6MwlUBZkc3Vn5b5AMIWYKfptvzipoyFfhlfOgBQ9zoTxQMravF1QTnw==", + "dependencies": { + "brace-expansion": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==" + }, + "node_modules/mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/recur-fs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/recur-fs/-/recur-fs-2.2.4.tgz", + "integrity": "sha512-VofuavbR3PNwHAs2uxn2HNcyyXKIUBayVtdhdElJ8qqSmcr2TY71THQjAoF+PT1xEeUnEi57DWT2/+YSRCvr3w==", + "dependencies": { + "minimatch": "3.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.5.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", + "integrity": "sha512-Lw7SHMjssciQb/rRz7JyPIy9+bbUshEucPoLRvWqy09vC5zQixl8Uet+Zl+SROBB/JMWHJRdCk1qdxNWHNMvlQ==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.0.5" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + }, + "dependencies": { + "@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "requires": { + "@types/node": "*" + } + }, + "@types/is-valid-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/is-valid-path/-/is-valid-path-0.1.2.tgz", + "integrity": "sha512-BsZtkfiPpnzDWFjSZanYllttVW7/46ayPZkcHBCSFBkBqIO9rWrflUvEmT2tF///hnPLwBJU3TJPzbBxpUEqCg==" + }, + "@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "requires": { + "undici-types": "~6.21.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "requires": { + "ms": "^2.1.3" + } + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==" + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-invalid-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-invalid-path/-/is-invalid-path-0.1.0.tgz", + "integrity": "sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==", + "requires": { + "is-glob": "^2.0.0" + } + }, + "is-valid-path": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz", + "integrity": "sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==", + "requires": { + "is-invalid-path": "^0.1.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "key-file-storage": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/key-file-storage/-/key-file-storage-2.3.3.tgz", + "integrity": "sha512-bqFrbE0ifIq5ahrquGewh5nMub8fdH/nXKMg9CJHdCdD5W/6KQea483Qyss0q53tCOC64CN43DQo9TSvohlbKA==", + "requires": { + "@types/fs-extra": "^9.0.11", + "@types/is-valid-path": "^0.1.0", + "fs-extra": "^10.0.0", + "is-valid-path": "^0.1.1", + "recur-fs": "^2.2.4" + } + }, + "mailer": { + "version": "file:../mailer", + "requires": { + "debug": "^4.4.0", + "nodemailer": "^6.10.0" + } + }, + "minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-NyXjqu1IwcqH6nv5vmMtaG3iw7kdV3g6MwlUBZkc3Vn5b5AMIWYKfptvzipoyFfhlfOgBQ9zoTxQMravF1QTnw==", + "requires": { + "brace-expansion": "^1.0.0" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "recur-fs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/recur-fs/-/recur-fs-2.2.4.tgz", + "integrity": "sha512-VofuavbR3PNwHAs2uxn2HNcyyXKIUBayVtdhdElJ8qqSmcr2TY71THQjAoF+PT1xEeUnEi57DWT2/+YSRCvr3w==", + "requires": { + "minimatch": "3.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.5.4" + } + }, + "rimraf": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", + "integrity": "sha512-Lw7SHMjssciQb/rRz7JyPIy9+bbUshEucPoLRvWqy09vC5zQixl8Uet+Zl+SROBB/JMWHJRdCk1qdxNWHNMvlQ==", + "requires": { + "glob": "^7.0.5" + } + }, + "undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + } +} diff --git a/error-handler/package.json b/error-handler/package.json new file mode 100644 index 0000000..80f59c3 --- /dev/null +++ b/error-handler/package.json @@ -0,0 +1,20 @@ +{ + "name": "error-handler", + "version": "2.0.0", + "description": "A reusable error handling utility module for Node.js applications.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "Error Handling", + "Utility" + ], + "author": "Trung Hoang", + "license": "Proprietary", + "dependencies": { + "debug": "^4.4.0", + "key-file-storage": "^2.3.3", + "mailer": "file:../mailer" + } +} diff --git a/error-handler/utils/common.js b/error-handler/utils/common.js new file mode 100644 index 0000000..8b665b7 --- /dev/null +++ b/error-handler/utils/common.js @@ -0,0 +1,10 @@ +'use restrict'; + +function isAppError(err) { + const _err = err && err.message || err || ''; + return (err && (typeof _err === "string" && _err.includes('_'))) ? _err : ''; +} + +module.exports = { + isAppError +}; \ No newline at end of file diff --git a/mailer/index.js b/mailer/index.js new file mode 100644 index 0000000..b10eb8a --- /dev/null +++ b/mailer/index.js @@ -0,0 +1,4 @@ + +module.exports = { + default: require('./mailer.js') +} \ No newline at end of file diff --git a/mailer/mailer.js b/mailer/mailer.js new file mode 100644 index 0000000..379e55b --- /dev/null +++ b/mailer/mailer.js @@ -0,0 +1,84 @@ +'use strict'; + +const nodemailer = require('nodemailer'), + debug = require('debug')('app:mailer'); + +const createConfig = (env) => { + const isSecure = env?.SMTP_SECURE || process.env.SMTP_SECURE || false; + const port = env?.SMTP_PORT || process.env.SMTP_PORT || 587; + + const config = { + host: env?.SMTP_HOST || process.env.SMTP_HOST, + port: port, + secure: isSecure, + auth: { + user: env?.SMTP_USR || process.env.SMTP_USR, + pass: env?.SMTP_PWD || process.env.SMTP_PWD, + }, + sender: env?.SMTP_SENDER || process.env.SMTP_SENDER || `"AgNav Inc." ` + }; + + if (!config.host || !config.port || !config.auth.user || !config.auth.pass) { + throw new Error('SMTP configuration is missing required fields (host, port, user, pass).'); + } + return config; +}; + +const createMailOptions = (from, to, subject, body) => ({ + from: from, to: to, subject: subject, text: body +}); + +function createMailer(env) { + const config = createConfig(env); + + const transporter = nodemailer.createTransport(config); + + return { + sendMail: async (to, subject, body) => { + try { + if (!to || !subject || !body) { + throw new Error('Missing required fields: to, subject, or body'); + } + + const mailOptions = createMailOptions(config.sender, to, subject, body); + const info = await transporter.sendMail(mailOptions); + + return { success: true, message: 'Mail sent successfully', messageId: info.messageId }; + } catch (error) { + debug('Error sending mail:', error.message); + return { success: false, error: error.message }; + } + }, + }; +} + +/** + * Send a text email + * @param {Object} options Email options + * @param {string} options.from Sender of the email (Optional) + * @param {string} options.subject Subject of the email + * @param {string} options.to Recipient of the email + * @param {string} options.text Text content of the email + */ +async function sendTextMail(options, env) { + const { from, to, subject, body } = options; + if (!to || !subject || !body) { + throw new Error('Missing required fields: to, subject, or body'); + } + const config = createConfig(env); + + const transporter = nodemailer.createTransport(config); + const mailOptions = createMailOptions(from || config.sender, to, subject, body); + + try { + const info = await transporter.sendMail(mailOptions); + return { success: true, messageId: info.messageId }; + } catch (error) { + debug('Error sending email:', error.message); + return { success: false, error: error.message }; + } +} + +module.exports = { + createMailer, sendTextMail +}; \ No newline at end of file diff --git a/mailer/package-lock.json b/mailer/package-lock.json new file mode 100644 index 0000000..d489b7a --- /dev/null +++ b/mailer/package-lock.json @@ -0,0 +1,66 @@ +{ + "name": "mailer", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "mailer", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "debug": "^4.4.0", + "nodemailer": "^6.10.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "engines": { + "node": ">=6.0.0" + } + } + }, + "dependencies": { + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nodemailer": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==" + } + } +} diff --git a/mailer/package.json b/mailer/package.json new file mode 100644 index 0000000..0446c69 --- /dev/null +++ b/mailer/package.json @@ -0,0 +1,19 @@ +{ + "name": "mailer", + "version": "1.0.0", + "description": "Simple reusable mailer utility.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [ + "mailer", + "utility" + ], + "author": "Trung Hoang", + "license": "ISC", + "dependencies": { + "debug": "^4.4.0", + "nodemailer": "^6.10.0" + } +} \ No newline at end of file diff --git a/number-util/index.js b/number-util/index.js new file mode 100644 index 0000000..0af9105 --- /dev/null +++ b/number-util/index.js @@ -0,0 +1,25 @@ +'use strict'; + +/** + * Trim the decimal to maximum numbers of digits + * @param {*} value the decimal value + * @param {*} digits maximum number of decimal digits + */ +function fixedTo(value, digits) { + if (!value) return value; + var re = new RegExp('(-?\\d+\\.\\d{' + digits + '})(\\d)'), + m = value.toString().match(re); + return m ? parseFloat(m[1]) : value; +}; + +function padZero(num, size) { + let s = num + ''; + while (s.length < size) { + s = '0' + s; + } + return s; +} + +module.exports = { + fixedTo, padZero +} diff --git a/number-util/package-lock.json b/number-util/package-lock.json new file mode 100644 index 0000000..d9b6003 --- /dev/null +++ b/number-util/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "number-util", + "version": "1.1.0", + "lockfileVersion": 1 +} diff --git a/number-util/package.json b/number-util/package.json new file mode 100644 index 0000000..2b73cb6 --- /dev/null +++ b/number-util/package.json @@ -0,0 +1,12 @@ +{ + "name": "number-util", + "version": "1.1.0", + "description": "Number utilities", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "Trung Hoang", + "license": "Proprietary" +}