copy of the @agn repository from as of April 23 2026

This commit is contained in:
Devin Major 2026-04-23 09:05:02 -04:00
parent d6e6e8164f
commit 3362073a35
55 changed files with 3129 additions and 0 deletions

1
.svn/entries Normal file
View File

@ -0,0 +1 @@
12

1
.svn/format Normal file
View File

@ -0,0 +1 @@
12

View File

@ -0,0 +1,2 @@
module.exports.RAPParser = require('./rap-parser');
module.exports.AgNavParser = require('./agn-parser');

View File

@ -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=="
}
}
}

View File

@ -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
};

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -0,0 +1,5 @@
{
"name": "bit-util",
"version": "1.0.0",
"lockfileVersion": 1
}

View File

@ -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;

View File

@ -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
}

View File

@ -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"
}
}

View File

@ -0,0 +1,4 @@
module.exports = {
default: require('./mailer.js')
}

View File

@ -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 => <Cmd><Len>
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 <Cmd> from read <Cmd><Len>
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;

View File

@ -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"
}
}

View File

@ -0,0 +1,3 @@
exports.TCPDevice = require('./tcp-device');
exports.AgNavTCPDevice = require('./agnav-device');
exports.RAPTCPDevice = require('./rap-device');

View File

@ -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"
}

View File

@ -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" <agm_admin@agnav.com>',
};
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
}

View File

@ -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
};

View File

@ -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><Len><Data>, 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 <Cmd><Len> 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;

View File

@ -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." <noreply@agnav.com>`
};
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
};

View File

@ -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"
}
}

View File

@ -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;

View File

@ -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><Data>, 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;

View File

@ -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 <trungh@agnav.com>",
"license": "Proprietary",
"dependencies": {
"number-util": "file:../number-util"
}
}

View File

@ -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 }

View File

@ -0,0 +1,5 @@
{
"name": "number-util",
"version": "1.1.0",
"lockfileVersion": 1
}

View File

@ -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"
}

BIN
.svn/wc.db Normal file

Binary file not shown.

0
.svn/wc.db-journal Normal file
View File

View File

@ -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><Data>, 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;

View File

@ -0,0 +1,2 @@
module.exports.RAPParser = require('./rap-parser');
module.exports.AgNavParser = require('./agn-parser');

25
binary-package-parser/package-lock.json generated Normal file
View File

@ -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
}
}
}

View File

@ -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"
}
}

View File

@ -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><Len><Data>, 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 <Cmd><Len> 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;

32
bit-util/index.js Normal file
View File

@ -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 }

5
bit-util/package-lock.json generated Normal file
View File

@ -0,0 +1,5 @@
{
"name": "bit-util",
"version": "1.0.0",
"lockfileVersion": 1
}

12
bit-util/package.json Normal file
View File

@ -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"
}

View File

@ -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;

View File

@ -0,0 +1,3 @@
exports.TCPDevice = require('./tcp-device');
exports.AgNavTCPDevice = require('./agnav-device');
exports.RAPTCPDevice = require('./rap-device');

View File

@ -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 <trungh@agnav.com>",
"license": "Proprietary",
"dependencies": {
"number-util": "file:../number-util"
}
}

View File

@ -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 => <Cmd><Len>
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 <Cmd> from read <Cmd><Len>
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;

View File

@ -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;

View File

@ -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
}

View File

@ -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" <agm_admin@agnav.com>',
};
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
}

9
error-handler/index.js Normal file
View File

@ -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
};

548
error-handler/package-lock.json generated Normal file
View File

@ -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=="
}
}
}

View File

@ -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"
}
}

View File

@ -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
};

4
mailer/index.js Normal file
View File

@ -0,0 +1,4 @@
module.exports = {
default: require('./mailer.js')
}

84
mailer/mailer.js Normal file
View File

@ -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." <noreply@agnav.com>`
};
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
};

66
mailer/package-lock.json generated Normal file
View File

@ -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=="
}
}
}

19
mailer/package.json Normal file
View File

@ -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"
}
}

25
number-util/index.js Normal file
View File

@ -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
}

5
number-util/package-lock.json generated Normal file
View File

@ -0,0 +1,5 @@
{
"name": "number-util",
"version": "1.1.0",
"lockfileVersion": 1
}

12
number-util/package.json Normal file
View File

@ -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"
}