From b7519c97dae43c1488782c62d21072609b938d4f Mon Sep 17 00:00:00 2001 From: Victor Ribeiro Date: Mon, 11 Aug 2025 11:30:17 +0200 Subject: [PATCH 01/14] chore: add nodescript microframework deps and bump mesh-ioc and config versions --- package-lock.json | 388 +++++++++++++++++++++++++++++++++++++++------- package.json | 9 +- 2 files changed, 337 insertions(+), 60 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0ba1c91..2c4c770 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,10 @@ "license": "ISC", "dependencies": { "@koa/cors": "^5.0.0", - "@nodescript/logger": "^2.0.3", + "@nodescript/errors": "^1.2.0", + "@nodescript/http-server": "^2.10.1", + "@nodescript/logger": "^2.0.6", + "@nodescript/microframework": "^1.15.3", "@nodescript/pathmatcher": "^1.0.2", "@types/koa": "^2.11.8", "@types/koa__cors": "^3.0.2", @@ -31,8 +34,8 @@ "koa-compress": "^5.1.1", "koa-conditional-get": "^3.0.0", "koa-etag": "^4.0.0", - "mesh-config": "1.1.0", - "mesh-ioc": "^3.2.0", + "mesh-config": "^1.2.1", + "mesh-ioc": "^4.1.0", "node-fetch": "^2.6.0", "reflect-metadata": "^0.1.13", "stoppable": "^1.1.0", @@ -570,6 +573,12 @@ "node": ">= 8" } }, + "node_modules/@nodescript/errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@nodescript/errors/-/errors-1.2.0.tgz", + "integrity": "sha512-QWrH/tFASZtwD3KmSBFPExVbc1cY2pRHKF7IocCj5khgC6HBoPjuOu/3Dnbkm23DWiWbFgW/Y3xtDjrghMPgpQ==", + "license": "ISC" + }, "node_modules/@nodescript/eslint-config": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@nodescript/eslint-config/-/eslint-config-1.0.4.tgz", @@ -584,15 +593,75 @@ "eslint-plugin-vue": "^9.5.1" } }, + "node_modules/@nodescript/http-server": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@nodescript/http-server/-/http-server-2.10.1.tgz", + "integrity": "sha512-LyROIIJJ98eKYRjhIniZERz7M8DcTNnECKcmzVW2XEHEFlh5zXdy5dOOuwQv6vKqQzWcE/LNU0yInzxMdmdEWQ==", + "license": "ISC", + "dependencies": { + "@nodescript/errors": "^1.2.0", + "@nodescript/logger": "^2.0.6", + "@nodescript/metrics": "^1.7.1", + "@nodescript/pathmatcher": "^1.3.0", + "@nodescript/protocomm": "^1.1.0", + "mesh-config": "^1.2.1", + "mesh-decorators": "^1.1.2", + "mesh-ioc": "^4.1.0", + "nanoevent": "^1.0.0" + } + }, "node_modules/@nodescript/logger": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nodescript/logger/-/logger-2.0.3.tgz", - "integrity": "sha512-p7/z7RXb6Vo1UD8UGpGuymCqAIg6Wo33KCFKijU06HUD88DNe9LjByFGBG6IwLjzjUt2wGcKl6KBf6OdGVufYQ==" + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@nodescript/logger/-/logger-2.0.6.tgz", + "integrity": "sha512-jTS4IQtUnr8srL3DtCA7JCxTh7jlx/m0QC7+vI9g7R3RVGmjcMmbQPnsbGEPI8DUG8oAWXFqis7r7Nye6gcvuw==", + "license": "ISC" + }, + "node_modules/@nodescript/metrics": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@nodescript/metrics/-/metrics-1.7.1.tgz", + "integrity": "sha512-MUwJcKL52XRai35WRfNyAQ/egKD/me2zoEfm1QbIGm1epjlB+ZvTs8qtciekTLLxsx/BlJd3becAsXpmZhXguQ==", + "license": "ISC", + "dependencies": { + "mesh-decorators": "^1.1.2", + "mesh-ioc": "^4.1.0" + } + }, + "node_modules/@nodescript/microframework": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@nodescript/microframework/-/microframework-1.15.3.tgz", + "integrity": "sha512-lB/fg7UmYRw2PkA/EiAXqQ38OAPulEY08+cdMu18xp0FVjT1HorAEbgMH5RMdZDaDE+KvlJVSeBqZ8SfVxWI8A==", + "license": "ISC", + "dependencies": { + "@nodescript/errors": "^1.2.0", + "@nodescript/http-server": "^2.10.1", + "@nodescript/logger": "^2.0.6", + "@nodescript/metrics": "^1.7.1", + "airtight": "^5.7.2", + "dotenv": "^16.0.3", + "jsonwebtoken": "^9.0.2", + "mesh-config": "^1.2.1", + "mesh-ioc": "^4.1.0", + "reflect-metadata": "^0.1.13" + } }, "node_modules/@nodescript/pathmatcher": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@nodescript/pathmatcher/-/pathmatcher-1.0.2.tgz", - "integrity": "sha512-CLUKBYfP4TCbivmcpAbZgMTf1zjq/qN5r/3KdkRgC7sTjop/GalZycdAmijtNEkFn5w0QHP2uXJOWpLKBscTVw==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@nodescript/pathmatcher/-/pathmatcher-1.3.0.tgz", + "integrity": "sha512-Jd7kkEkGQXKmkAt0RPTRhrlS3o33GkwX9MuJBMksH5vifalFKLIXzgg0dgfdRGwGKbDZDud+v0MSt4trgZ+2gQ==", + "license": "ISC" + }, + "node_modules/@nodescript/protocomm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@nodescript/protocomm/-/protocomm-1.2.0.tgz", + "integrity": "sha512-mPbSubJ8OvxfHKHnYQtaLDuzNEGTTzDREPxjXuvomcjbvyBtGjl0Kt1e+QAYYF2Fl4dHE0cEn7ik6jzTg7Umsg==", + "license": "ISC", + "dependencies": { + "airtight": "^5.7.2", + "nanoevent": "^1.0.0" + }, + "engines": { + "node": ">=18" + } }, "node_modules/@types/accepts": { "version": "1.3.5", @@ -1194,6 +1263,12 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/airtight": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/airtight/-/airtight-5.7.2.tgz", + "integrity": "sha512-Ho9mp/c2bQhIVyT4qvsiPDIsOGE+JXudQShP8pbFnpgQJ2ufk3Ti1RJIZPPuI+ZkxOY6wRlH/A7rNiw1TjewDQ==", + "license": "ISC" + }, "node_modules/ajv": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", @@ -5458,14 +5533,21 @@ } }, "node_modules/jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", "dependencies": { "jws": "^3.2.2", - "lodash": "^4.17.21", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", "ms": "^2.1.1", - "semver": "^7.3.8" + "semver": "^7.5.4" }, "engines": { "node": ">=12", @@ -5683,7 +5765,44 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -5691,6 +5810,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -5762,18 +5887,45 @@ } }, "node_modules/mesh-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mesh-config/-/mesh-config-1.1.0.tgz", - "integrity": "sha512-ZDrzJJmdjOXKyABcTI/mt+2yQUDH4XiUNRto7DU2zP4pIVGsTrpwArwSmtaVxt7S6ufwTeJJ7vfYIEXKh2DmSg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/mesh-config/-/mesh-config-1.2.1.tgz", + "integrity": "sha512-OSV2iZE/2hTcUEt3Bw0+JNmMwdiHLB9ksJHas2dCEXADVF/57MotpUCtqrWDMUBx88SWmM3Zv2c8P/rCobJyJQ==", + "license": "ISC", + "dependencies": { + "mesh-ioc": "^4.1.0", + "reflect-utils": "^1.1.1" + } + }, + "node_modules/mesh-config/node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/mesh-config/node_modules/reflect-utils": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/reflect-utils/-/reflect-utils-1.1.1.tgz", + "integrity": "sha512-b1AKtKIHIQ9jYzfv5Xgi4/QGIad4qGjEX5vTZM4MQfzuWcEfyS6tDjg8NR1QZzVzMUDPvcxPmDgX5PudZ1dGTw==", + "license": "ISC", + "peerDependencies": { + "reflect-metadata": "^0.2.2" + } + }, + "node_modules/mesh-decorators": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/mesh-decorators/-/mesh-decorators-1.1.2.tgz", + "integrity": "sha512-tbsaiMOj9XaaL9vcvK1Dm/8GMQTQuDJGCz3A89612yEOyC34Bcb1ZFlJQevEn+HFbhTlkVXO3D1qXkO6YsfA+w==", + "license": "ISC", "dependencies": { - "mesh-ioc": "^3.2.0", - "reflect-utils": "^1.0.3" + "mesh-ioc": "^4.1.0" } }, "node_modules/mesh-ioc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mesh-ioc/-/mesh-ioc-3.2.0.tgz", - "integrity": "sha512-kLVoKX8mwTqy8MjlmyrSr+dpsFLSGJlC1HzfRcEdRR+ip4rWjEQHvwxd/2HFljFKoMyvXdB3J02fPbsVNEKZRg==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mesh-ioc/-/mesh-ioc-4.1.0.tgz", + "integrity": "sha512-7iNWDv6lm7u+pUK1xaG0193fnh8iulISUK6gunYtFsyspSpd9t6azRDbU42T6CsjsD51/a08m8d27sngIjnrwg==", + "license": "ISC" }, "node_modules/methods": { "version": "1.1.2", @@ -6017,6 +6169,12 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "node_modules/nanoevent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nanoevent/-/nanoevent-1.0.0.tgz", + "integrity": "sha512-nxipFgI/EuMStQJfL7kW7avXnP0Fyq5U9MIzvGrP7mMTmDNDtHaqtN+Br6uJFXB9AlILCbPPeGK8PEXZ/UKubg==", + "license": "ISC" + }, "node_modules/nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -6865,14 +7023,6 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, - "node_modules/reflect-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/reflect-utils/-/reflect-utils-1.0.3.tgz", - "integrity": "sha512-MzHKBN9UzDswy2kOoyTfwU/3jveH7c7j9jsfLaEqNfJpR3f3dhDJ9xhI7YWnB5LtvwfE5uELti2R6qac2WpyKA==", - "peerDependencies": { - "reflect-metadata": "^0.1.13" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", @@ -8524,6 +8674,11 @@ "fastq": "^1.6.0" } }, + "@nodescript/errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@nodescript/errors/-/errors-1.2.0.tgz", + "integrity": "sha512-QWrH/tFASZtwD3KmSBFPExVbc1cY2pRHKF7IocCj5khgC6HBoPjuOu/3Dnbkm23DWiWbFgW/Y3xtDjrghMPgpQ==" + }, "@nodescript/eslint-config": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@nodescript/eslint-config/-/eslint-config-1.0.4.tgz", @@ -8538,15 +8693,66 @@ "eslint-plugin-vue": "^9.5.1" } }, + "@nodescript/http-server": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@nodescript/http-server/-/http-server-2.10.1.tgz", + "integrity": "sha512-LyROIIJJ98eKYRjhIniZERz7M8DcTNnECKcmzVW2XEHEFlh5zXdy5dOOuwQv6vKqQzWcE/LNU0yInzxMdmdEWQ==", + "requires": { + "@nodescript/errors": "^1.2.0", + "@nodescript/logger": "^2.0.6", + "@nodescript/metrics": "^1.7.1", + "@nodescript/pathmatcher": "^1.3.0", + "@nodescript/protocomm": "^1.1.0", + "mesh-config": "^1.2.1", + "mesh-decorators": "^1.1.2", + "mesh-ioc": "^4.1.0", + "nanoevent": "^1.0.0" + } + }, "@nodescript/logger": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nodescript/logger/-/logger-2.0.3.tgz", - "integrity": "sha512-p7/z7RXb6Vo1UD8UGpGuymCqAIg6Wo33KCFKijU06HUD88DNe9LjByFGBG6IwLjzjUt2wGcKl6KBf6OdGVufYQ==" + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@nodescript/logger/-/logger-2.0.6.tgz", + "integrity": "sha512-jTS4IQtUnr8srL3DtCA7JCxTh7jlx/m0QC7+vI9g7R3RVGmjcMmbQPnsbGEPI8DUG8oAWXFqis7r7Nye6gcvuw==" + }, + "@nodescript/metrics": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@nodescript/metrics/-/metrics-1.7.1.tgz", + "integrity": "sha512-MUwJcKL52XRai35WRfNyAQ/egKD/me2zoEfm1QbIGm1epjlB+ZvTs8qtciekTLLxsx/BlJd3becAsXpmZhXguQ==", + "requires": { + "mesh-decorators": "^1.1.2", + "mesh-ioc": "^4.1.0" + } + }, + "@nodescript/microframework": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@nodescript/microframework/-/microframework-1.15.3.tgz", + "integrity": "sha512-lB/fg7UmYRw2PkA/EiAXqQ38OAPulEY08+cdMu18xp0FVjT1HorAEbgMH5RMdZDaDE+KvlJVSeBqZ8SfVxWI8A==", + "requires": { + "@nodescript/errors": "^1.2.0", + "@nodescript/http-server": "^2.10.1", + "@nodescript/logger": "^2.0.6", + "@nodescript/metrics": "^1.7.1", + "airtight": "^5.7.2", + "dotenv": "^16.0.3", + "jsonwebtoken": "^9.0.2", + "mesh-config": "^1.2.1", + "mesh-ioc": "^4.1.0", + "reflect-metadata": "^0.1.13" + } }, "@nodescript/pathmatcher": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@nodescript/pathmatcher/-/pathmatcher-1.0.2.tgz", - "integrity": "sha512-CLUKBYfP4TCbivmcpAbZgMTf1zjq/qN5r/3KdkRgC7sTjop/GalZycdAmijtNEkFn5w0QHP2uXJOWpLKBscTVw==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@nodescript/pathmatcher/-/pathmatcher-1.3.0.tgz", + "integrity": "sha512-Jd7kkEkGQXKmkAt0RPTRhrlS3o33GkwX9MuJBMksH5vifalFKLIXzgg0dgfdRGwGKbDZDud+v0MSt4trgZ+2gQ==" + }, + "@nodescript/protocomm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@nodescript/protocomm/-/protocomm-1.2.0.tgz", + "integrity": "sha512-mPbSubJ8OvxfHKHnYQtaLDuzNEGTTzDREPxjXuvomcjbvyBtGjl0Kt1e+QAYYF2Fl4dHE0cEn7ik6jzTg7Umsg==", + "requires": { + "airtight": "^5.7.2", + "nanoevent": "^1.0.0" + } }, "@types/accepts": { "version": "1.3.5", @@ -9029,6 +9235,11 @@ "dev": true, "requires": {} }, + "airtight": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/airtight/-/airtight-5.7.2.tgz", + "integrity": "sha512-Ho9mp/c2bQhIVyT4qvsiPDIsOGE+JXudQShP8pbFnpgQJ2ufk3Ti1RJIZPPuI+ZkxOY6wRlH/A7rNiw1TjewDQ==" + }, "ajv": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", @@ -12236,14 +12447,20 @@ } }, "jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "requires": { "jws": "^3.2.2", - "lodash": "^4.17.21", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", "ms": "^2.1.1", - "semver": "^7.3.8" + "semver": "^7.5.4" }, "dependencies": { "lru-cache": { @@ -12423,7 +12640,38 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, "lodash.merge": { "version": "4.6.2", @@ -12431,6 +12679,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -12484,18 +12737,40 @@ "dev": true }, "mesh-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mesh-config/-/mesh-config-1.1.0.tgz", - "integrity": "sha512-ZDrzJJmdjOXKyABcTI/mt+2yQUDH4XiUNRto7DU2zP4pIVGsTrpwArwSmtaVxt7S6ufwTeJJ7vfYIEXKh2DmSg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/mesh-config/-/mesh-config-1.2.1.tgz", + "integrity": "sha512-OSV2iZE/2hTcUEt3Bw0+JNmMwdiHLB9ksJHas2dCEXADVF/57MotpUCtqrWDMUBx88SWmM3Zv2c8P/rCobJyJQ==", + "requires": { + "mesh-ioc": "^4.1.0", + "reflect-utils": "^1.1.1" + }, + "dependencies": { + "reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "peer": true + }, + "reflect-utils": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/reflect-utils/-/reflect-utils-1.1.1.tgz", + "integrity": "sha512-b1AKtKIHIQ9jYzfv5Xgi4/QGIad4qGjEX5vTZM4MQfzuWcEfyS6tDjg8NR1QZzVzMUDPvcxPmDgX5PudZ1dGTw==", + "requires": {} + } + } + }, + "mesh-decorators": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/mesh-decorators/-/mesh-decorators-1.1.2.tgz", + "integrity": "sha512-tbsaiMOj9XaaL9vcvK1Dm/8GMQTQuDJGCz3A89612yEOyC34Bcb1ZFlJQevEn+HFbhTlkVXO3D1qXkO6YsfA+w==", "requires": { - "mesh-ioc": "^3.2.0", - "reflect-utils": "^1.0.3" + "mesh-ioc": "^4.1.0" } }, "mesh-ioc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mesh-ioc/-/mesh-ioc-3.2.0.tgz", - "integrity": "sha512-kLVoKX8mwTqy8MjlmyrSr+dpsFLSGJlC1HzfRcEdRR+ip4rWjEQHvwxd/2HFljFKoMyvXdB3J02fPbsVNEKZRg==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mesh-ioc/-/mesh-ioc-4.1.0.tgz", + "integrity": "sha512-7iNWDv6lm7u+pUK1xaG0193fnh8iulISUK6gunYtFsyspSpd9t6azRDbU42T6CsjsD51/a08m8d27sngIjnrwg==" }, "methods": { "version": "1.1.2", @@ -12659,6 +12934,11 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nanoevent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nanoevent/-/nanoevent-1.0.0.tgz", + "integrity": "sha512-nxipFgI/EuMStQJfL7kW7avXnP0Fyq5U9MIzvGrP7mMTmDNDtHaqtN+Br6uJFXB9AlILCbPPeGK8PEXZ/UKubg==" + }, "nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -13296,12 +13576,6 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, - "reflect-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/reflect-utils/-/reflect-utils-1.0.3.tgz", - "integrity": "sha512-MzHKBN9UzDswy2kOoyTfwU/3jveH7c7j9jsfLaEqNfJpR3f3dhDJ9xhI7YWnB5LtvwfE5uELti2R6qac2WpyKA==", - "requires": {} - }, "regexp.prototype.flags": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", diff --git a/package.json b/package.json index 48f50ca..b624458 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,10 @@ }, "dependencies": { "@koa/cors": "^5.0.0", - "@nodescript/logger": "^2.0.3", + "@nodescript/errors": "^1.2.0", + "@nodescript/http-server": "^2.10.1", + "@nodescript/logger": "^2.0.6", + "@nodescript/microframework": "^1.15.3", "@nodescript/pathmatcher": "^1.0.2", "@types/koa": "^2.11.8", "@types/koa__cors": "^3.0.2", @@ -80,8 +83,8 @@ "koa-compress": "^5.1.1", "koa-conditional-get": "^3.0.0", "koa-etag": "^4.0.0", - "mesh-config": "1.1.0", - "mesh-ioc": "^3.2.0", + "mesh-config": "^1.2.1", + "mesh-ioc": "^4.1.0", "node-fetch": "^2.6.0", "reflect-metadata": "^0.1.13", "stoppable": "^1.1.0", From 9965821b3351f780d55727bbb91addf0bb29ad89 Mon Sep 17 00:00:00 2001 From: Victor Ribeiro Date: Mon, 11 Aug 2025 13:06:54 +0200 Subject: [PATCH 02/14] feat(application): add extending base app from @nodescript/microframework --- src/main/application.ts | 76 +++++++++++++---------------------------- 1 file changed, 23 insertions(+), 53 deletions(-) diff --git a/src/main/application.ts b/src/main/application.ts index 6ecb104..380564f 100644 --- a/src/main/application.ts +++ b/src/main/application.ts @@ -1,58 +1,44 @@ import { Logger } from '@nodescript/logger'; -import dotenv from 'dotenv'; -import { Config, config, ConfigError, getMeshConfigs, ProcessEnvConfig } from 'mesh-config'; +import { AuxHttpServer, BaseApp } from '@nodescript/microframework'; +import { Config, config, ConfigError, getMeshConfigs } from 'mesh-config'; import { dep, Mesh } from 'mesh-ioc'; import { HttpRequestLogger, HttpServer } from './http.js'; -import { StandardLogger } from './logger.js'; import { getGlobalMetrics } from './metrics/global.js'; -import { MetricsRouter } from './metrics/route.js'; -import { - AcAuthProvider, - AutomationCloudJwtService, - DefaultAcAuthProvider, - JwtService, -} from './services/index.js'; +import { AcAuthProvider, DefaultAcAuthProvider } from './services/ac-auth-provider.js'; +import { AutomationCloudJwtService, JwtService } from './services/jwt.js'; + /** * Application is an IoC composition root where all modules should be registered * and provides minimal lifecycle framework (start, stop, beforeStart, afterStop). */ -export class Application { - - mesh = new Mesh('Global'); +export class Application extends BaseApp { @config({ default: false }) ASSERT_CONFIGS_ON_START!: boolean; @dep() httpServer!: HttpServer; - @dep() logger!: Logger; + @dep() auxHttpServer!: AuxHttpServer; constructor() { - // Some default implementations are bound for convenience but can be replaced as fit - this.mesh = this.createGlobalScope(); - this.mesh.connect(this); + super(new Mesh('App')); + this.createGlobalScope(); } createGlobalScope(): Mesh { - const mesh = new Mesh('Global'); - mesh.constant('httpRequestScope', () => this.createHttpRequestScope()); - mesh.service(Logger, StandardLogger); - mesh.alias('AppLogger', Logger); - mesh.service(ProcessEnvConfig); - mesh.alias(Config, ProcessEnvConfig); - mesh.service(HttpServer); - mesh.service(AutomationCloudJwtService); - mesh.service(AcAuthProvider, DefaultAcAuthProvider); - mesh.alias(JwtService, AutomationCloudJwtService); - mesh.constant('GlobalMetrics', getGlobalMetrics()); - return mesh; + this.mesh.constant('httpRequestScope', () => this.createHttpRequestScope()); + this.mesh.alias('AppLogger', Logger); + this.mesh.service(HttpServer); + this.mesh.service(AutomationCloudJwtService); + this.mesh.service(AcAuthProvider, DefaultAcAuthProvider); + this.mesh.alias(JwtService, AutomationCloudJwtService); + this.mesh.constant('GlobalMetrics', getGlobalMetrics()); + return this.mesh; } createHttpRequestScope(): Mesh { - const mesh = new Mesh('HttpRequest'); - mesh.parent = this.mesh; + const mesh = new Mesh('HttpRequestScope', this.mesh); mesh.service(Logger, HttpRequestLogger); - mesh.service(MetricsRouter); return mesh; } @@ -60,33 +46,17 @@ export class Application { async afterStop(): Promise {} - async start() { - dotenv.config({ path: '.env' }); - if (process.env.NODE_ENV === 'development') { - dotenv.config({ path: '.env.dev' }); - } - if (process.env.NODE_ENV === 'test') { - dotenv.config({ path: '.env.test' }); - } - process.on('uncaughtException', error => { - this.logger.error('uncaughtException', { error }); - }); - process.on('unhandledRejection', error => { - this.logger.error('unhandledRejection', { error }); - }); - process.on('SIGTERM', () => this.logger.info('Received SIGTERM')); - process.on('SIGINT', () => this.logger.info('Received SIGINT')); - process.on('SIGTERM', () => this.stop()); - process.on('SIGINT', () => this.stop()); + override async start() { + await super.start(); if (this.ASSERT_CONFIGS_ON_START) { this.assertConfigs(); } + await this.auxHttpServer.start(); await this.beforeStart(); } - async stop() { - // TODO uninstall process signals handlers better - process.removeAllListeners(); + override async stop() { + await super.stop(); await this.afterStop(); } From ead24b2c18c24ced76c0ede93bb0e8783636b814 Mon Sep 17 00:00:00 2001 From: Victor Ribeiro Date: Mon, 11 Aug 2025 13:08:01 +0200 Subject: [PATCH 03/14] feat(application): add configuration option to start aux metrics/status HTTP server on app start --- .env.test | 1 + src/main/application.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.env.test b/.env.test index 5d0e32a..0e7857e 100644 --- a/.env.test +++ b/.env.test @@ -2,3 +2,4 @@ PORT=3000 LOG_LEVEL=mute AC_JWKS_URL=http://auth.example.com ASSERT_CONFIGS_ON_START=false +START_AUX_HTTP_SERVER_ON_START=false \ No newline at end of file diff --git a/src/main/application.ts b/src/main/application.ts index 380564f..651a856 100644 --- a/src/main/application.ts +++ b/src/main/application.ts @@ -16,6 +16,7 @@ import { AutomationCloudJwtService, JwtService } from './services/jwt.js'; export class Application extends BaseApp { @config({ default: false }) ASSERT_CONFIGS_ON_START!: boolean; + @config({ default: true }) START_AUX_HTTP_SERVER_ON_START!: boolean; @dep() httpServer!: HttpServer; @dep() auxHttpServer!: AuxHttpServer; @@ -51,7 +52,9 @@ export class Application extends BaseApp { if (this.ASSERT_CONFIGS_ON_START) { this.assertConfigs(); } - await this.auxHttpServer.start(); + if (this.START_AUX_HTTP_SERVER_ON_START) { + await this.auxHttpServer.start(); + } await this.beforeStart(); } From 40fb88505d63c58a661f24dda4a81c3cf8e95589 Mon Sep 17 00:00:00 2001 From: Victor Ribeiro Date: Mon, 11 Aug 2025 13:11:04 +0200 Subject: [PATCH 04/14] feat(WIP/auth): adapt http auth middleware to work with opt-in auth provider --- src/main/application.ts | 5 ----- src/main/http.ts | 14 ++++++++------ src/test/specs/jwt.test.ts | 4 +++- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/application.ts b/src/main/application.ts index 651a856..0716966 100644 --- a/src/main/application.ts +++ b/src/main/application.ts @@ -5,8 +5,6 @@ import { dep, Mesh } from 'mesh-ioc'; import { HttpRequestLogger, HttpServer } from './http.js'; import { getGlobalMetrics } from './metrics/global.js'; -import { AcAuthProvider, DefaultAcAuthProvider } from './services/ac-auth-provider.js'; -import { AutomationCloudJwtService, JwtService } from './services/jwt.js'; /** @@ -30,9 +28,6 @@ export class Application extends BaseApp { this.mesh.constant('httpRequestScope', () => this.createHttpRequestScope()); this.mesh.alias('AppLogger', Logger); this.mesh.service(HttpServer); - this.mesh.service(AutomationCloudJwtService); - this.mesh.service(AcAuthProvider, DefaultAcAuthProvider); - this.mesh.alias(JwtService, AutomationCloudJwtService); this.mesh.constant('GlobalMetrics', getGlobalMetrics()); return this.mesh; } diff --git a/src/main/http.ts b/src/main/http.ts index f9f3bff..46b9fed 100644 --- a/src/main/http.ts +++ b/src/main/http.ts @@ -101,8 +101,8 @@ export class HttpServer extends Koa { }) }, { - name: 'acAuth', - middleware: this.createAcAuthMiddleware(), + name: 'auth', + middleware: this.createAuthMiddleware(), }, { name: 'routing', @@ -171,12 +171,14 @@ export class HttpServer extends Koa { }; } - protected createAcAuthMiddleware(): Middleware { + protected createAuthMiddleware(): Middleware { return async (ctx: Koa.Context, next: Koa.Next) => { const mesh: Mesh = ctx.mesh; - const provider = mesh.resolve(AcAuthProvider); - const acAuth = await provider.provide(ctx.headers); - mesh.constant(AcAuth, acAuth); + const provider = mesh.tryResolve(AcAuthProvider); + if (provider) { + const acAuth = await provider.provide(ctx.headers); + mesh.constant(AcAuth, acAuth); + } return next(); }; } diff --git a/src/test/specs/jwt.test.ts b/src/test/specs/jwt.test.ts index 28b3e62..e1693ba 100644 --- a/src/test/specs/jwt.test.ts +++ b/src/test/specs/jwt.test.ts @@ -3,7 +3,7 @@ import crypto from 'crypto'; import jsonwebtoken from 'jsonwebtoken'; import { v4 as uuid } from 'uuid'; -import { Application, AutomationCloudJwtService } from '../../main/index.js'; +import { Application, AutomationCloudJwtService, JwtService } from '../../main/index.js'; describe('AutomationCloudJwt', () => { @@ -23,6 +23,8 @@ describe('AutomationCloudJwt', () => { }; const app = new Application(); + app.mesh.service(AutomationCloudJwtService); + app.mesh.alias(JwtService, AutomationCloudJwtService); beforeEach(async () => { jwtService = app.mesh.resolve(AutomationCloudJwtService); From 42bcf229aad7b30efffe3e0b3eea6fd123f5b565 Mon Sep 17 00:00:00 2001 From: Victor Ribeiro Date: Tue, 12 Aug 2025 18:47:28 +0200 Subject: [PATCH 05/14] feat(auth): make ac auth (and ac jwt service) optional and add generic auth provider/context to auth middleware --- src/main/ac-auth.ts | 15 ------ src/main/http.ts | 8 +-- src/main/services/ac-auth-provider.ts | 44 ++++++++-------- src/main/services/auth-context.ts | 32 ++++++++++++ src/main/services/auth-provider.ts | 7 +++ src/main/services/index.ts | 2 + src/main/util.ts | 12 ++++- src/test/integration/ac-auth-mocking.test.ts | 12 ++--- src/test/routes/access.ts | 4 +- src/test/specs/services/ac-auth.test.ts | 52 +++++++++---------- .../services/bypass-auth-provider.test.ts | 4 +- 11 files changed, 112 insertions(+), 80 deletions(-) create mode 100644 src/main/services/auth-context.ts create mode 100644 src/main/services/auth-provider.ts diff --git a/src/main/ac-auth.ts b/src/main/ac-auth.ts index 67c23ae..6347b0d 100644 --- a/src/main/ac-auth.ts +++ b/src/main/ac-auth.ts @@ -58,16 +58,6 @@ export class AcAuth { this.actor = spec.jwtContext == null ? null : this.parseActor(spec.jwtContext); } - isAuthenticated() { - return this.actor != null; - } - - checkAuthenticated(): void { - if (!this.isAuthenticated()) { - throw new AuthenticationError(); - } - } - getOrganisationId(): string | null { return this.actor?.organisationId ?? null; } @@ -168,11 +158,6 @@ export class AcAuth { } } -export class AuthenticationError extends ClientError { - override status = 401; - override message = 'Authentication is required'; -} - export class AccessForbidden extends ClientError { override status = 403; } diff --git a/src/main/http.ts b/src/main/http.ts index 46b9fed..afa6ed2 100644 --- a/src/main/http.ts +++ b/src/main/http.ts @@ -16,7 +16,7 @@ import { AcAuth } from './ac-auth.js'; import { ClientError } from './exception.js'; import { standardMiddleware } from './middleware.js'; import { Router } from './router.js'; -import { AcAuthProvider } from './services/index.js'; +import { AuthContext, AuthProvider } from './services/index.js'; import { findMeshInstances } from './util.js'; interface MiddlewareSpec { @@ -174,10 +174,10 @@ export class HttpServer extends Koa { protected createAuthMiddleware(): Middleware { return async (ctx: Koa.Context, next: Koa.Next) => { const mesh: Mesh = ctx.mesh; - const provider = mesh.tryResolve(AcAuthProvider); + const provider = mesh.tryResolve(AuthProvider); if (provider) { - const acAuth = await provider.provide(ctx.headers); - mesh.constant(AcAuth, acAuth); + const authContext = await provider.provide(ctx.headers); + mesh.constant(AuthContext, authContext); } return next(); }; diff --git a/src/main/services/ac-auth-provider.ts b/src/main/services/ac-auth-provider.ts index 1441c93..73717b7 100644 --- a/src/main/services/ac-auth-provider.ts +++ b/src/main/services/ac-auth-provider.ts @@ -3,16 +3,13 @@ import { Request } from '@ubio/request'; import { config } from 'mesh-config'; import { dep } from 'mesh-ioc'; -import { AcAuth, AuthenticationError } from '../ac-auth.js'; +import { AcAuth } from '../ac-auth.js'; +import { getSingleValue } from '../util.js'; +import { AuthContext, AuthenticationError } from './auth-context.js'; +import { AuthHeaders, AuthProvider } from './auth-provider.js'; import { JwtService } from './jwt.js'; -export type AuthHeaders = Record; - -export abstract class AcAuthProvider { - abstract provide(headers: AuthHeaders): Promise; -} - -export class DefaultAcAuthProvider extends AcAuthProvider { +export class AcAuthProvider extends AuthProvider { clientRequest: Request; static middlewareCacheTtl: number = 60000; @@ -32,12 +29,13 @@ export class DefaultAcAuthProvider extends AcAuthProvider { }); } - async provide(headers: AuthHeaders): Promise { - const token = await this.getToken(headers as any); + async provide(headers: AuthHeaders) { + const token = await this.getToken(headers); if (token) { - return await this.createAuthFromToken(headers, token); + const acAuth = await this.createAuthFromToken(headers, token); + return new AuthContext(acAuth); } - return new AcAuth(); + return new AuthContext(null); } protected async createAuthFromToken(headers: AuthHeaders, token: string): Promise { @@ -57,9 +55,9 @@ export class DefaultAcAuthProvider extends AcAuthProvider { } } - protected async getToken(headers: { [name: string]: string | undefined }) { + protected async getToken(headers: AuthHeaders) { const authHeaderName = this.AC_AUTH_HEADER_NAME; - const upstreamAuth = headers[authHeaderName]; + const upstreamAuth = getSingleValue(headers[authHeaderName]); if (upstreamAuth) { const [prefix, token] = upstreamAuth.split(' '); if (prefix !== 'Bearer' || !token) { @@ -70,15 +68,15 @@ export class DefaultAcAuthProvider extends AcAuthProvider { } return token; } - const authorization = headers['authorization']; + const authorization = getSingleValue(headers['authorization']); if (authorization) { return await this.getTokenFromAuthMiddleware(authorization); } } protected async getTokenFromAuthMiddleware(authorization: string): Promise { - const cached = DefaultAcAuthProvider.middlewareTokensCache.get(authorization) || { authorisedAt: 0, token: '' }; - const invalid = cached.authorisedAt + DefaultAcAuthProvider.middlewareCacheTtl < Date.now(); + const cached = AcAuthProvider.middlewareTokensCache.get(authorization) || { authorisedAt: 0, token: '' }; + const invalid = cached.authorisedAt + AcAuthProvider.middlewareCacheTtl < Date.now(); if (invalid) { try { const url = this.AC_AUTH_VERIFY_URL; @@ -86,7 +84,7 @@ export class DefaultAcAuthProvider extends AcAuthProvider { headers: { authorization }, }; const { token } = await this.clientRequest.get(url, options); - DefaultAcAuthProvider.middlewareTokensCache.set(authorization, { + AcAuthProvider.middlewareTokensCache.set(authorization, { token, authorisedAt: Date.now(), }); @@ -102,19 +100,19 @@ export class DefaultAcAuthProvider extends AcAuthProvider { pruneCache() { const now = Date.now(); - const entries = DefaultAcAuthProvider.middlewareTokensCache.entries(); + const entries = AcAuthProvider.middlewareTokensCache.entries(); for (const [k, v] of entries) { - if (v.authorisedAt + DefaultAcAuthProvider.middlewareCacheTtl < now) { - DefaultAcAuthProvider.middlewareTokensCache.delete(k); + if (v.authorisedAt + AcAuthProvider.middlewareCacheTtl < now) { + AcAuthProvider.middlewareTokensCache.delete(k); } } } } -export class BypassAcAuthProvider extends AcAuthProvider { +export class BypassAcAuthProvider extends AuthProvider { async provide() { - return new AcAuth(); + return new AuthContext(null); } } diff --git a/src/main/services/auth-context.ts b/src/main/services/auth-context.ts new file mode 100644 index 0000000..e6f922a --- /dev/null +++ b/src/main/services/auth-context.ts @@ -0,0 +1,32 @@ +import { ClientError } from '@nodescript/errors'; + +export type AuthToken = object; + +export class AuthContext { + + constructor(private authToken: T) {} + + isAuthenticated() { + return this.authToken != null; + } + + checkAuthenticated(): void { + if (!this.isAuthenticated()) { + throw new AuthenticationError(); + } + } + + getAuthToken(): T { + return this.authToken; + } + + setAuthToken(authToken: T): void { + this.authToken = authToken; + } +} + + +export class AuthenticationError extends ClientError { + override status = 401; + override message = 'Authentication is required'; +} diff --git a/src/main/services/auth-provider.ts b/src/main/services/auth-provider.ts new file mode 100644 index 0000000..2cf6c9f --- /dev/null +++ b/src/main/services/auth-provider.ts @@ -0,0 +1,7 @@ +import { AuthContext, AuthToken } from './auth-context.js'; + +export type AuthHeaders = Record; + +export abstract class AuthProvider { + abstract provide(headers?: AuthHeaders): Promise>; +} diff --git a/src/main/services/index.ts b/src/main/services/index.ts index 34f7b16..397a731 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -1,3 +1,5 @@ export * from './ac-auth-provider.js'; +export * from './auth-provider.js'; +export * from './auth-context.js'; export * from './job-timeline.js'; export * from './jwt.js'; diff --git a/src/main/util.ts b/src/main/util.ts index f50ec71..88f6fb8 100644 --- a/src/main/util.ts +++ b/src/main/util.ts @@ -11,7 +11,7 @@ import { Exception } from './exception.js'; export type Constructor = new (...args: any[]) => T; export type AnyConstructor = new (...args: any[]) => {}; -export function deepClone(data: T): T { +export function deepClone(data: T): T | null { return data == null ? null : JSON.parse(JSON.stringify(data)); } @@ -105,3 +105,13 @@ export function findMeshInstances(mesh: Mesh, ctor: ServiceConstructor): T } return instances; } + +export function getSingleValue(value: T | T[] | undefined): T | undefined { + if (typeof value === 'string') { + return value; + } + if (Array.isArray(value)) { + return value[0]; + } + return; +} diff --git a/src/test/integration/ac-auth-mocking.test.ts b/src/test/integration/ac-auth-mocking.test.ts index a891b43..4877639 100644 --- a/src/test/integration/ac-auth-mocking.test.ts +++ b/src/test/integration/ac-auth-mocking.test.ts @@ -2,19 +2,19 @@ import assert from 'assert'; import { dep } from 'mesh-ioc'; import supertest from 'supertest'; -import { AcAuth, AcAuthProvider, Application, Get, Router } from '../../main/index.js'; +import { AcAuth, Application, AuthContext, AuthProvider, Get, Router } from '../../main/index.js'; describe('Mocking AcAuth', () => { class MyRouter extends Router { - @dep() protected auth!: AcAuth; + @dep() protected auth!: AuthContext; @Get({ path: '/foo' }) foo() { - return { ...this.auth }; + return { ...this.auth.getAuthToken() }; } } @@ -24,15 +24,15 @@ describe('Mocking AcAuth', () => { override createHttpRequestScope() { const mesh = super.createHttpRequestScope(); mesh.service(MyRouter); - mesh.constant(AcAuthProvider, { + mesh.constant(AuthProvider, { async provide() { - return new AcAuth({ + return new AuthContext(new AcAuth({ jwtContext: { organisation_id: 'foo', service_account_id: 'service-account-worker', service_account_name: 'Bot', } - }); + })); } }); return mesh; diff --git a/src/test/routes/access.ts b/src/test/routes/access.ts index 13291c6..2584442 100644 --- a/src/test/routes/access.ts +++ b/src/test/routes/access.ts @@ -1,10 +1,10 @@ import { dep } from 'mesh-ioc'; -import { AcAuth, Get, Router } from '../../main/index.js'; +import { AcAuth, AuthContext, Get, Router } from '../../main/index.js'; export class AccessRouter extends Router { - @dep() protected auth!: AcAuth; + @dep() protected auth!: AuthContext; @Get({ path: '/public', diff --git a/src/test/specs/services/ac-auth.test.ts b/src/test/specs/services/ac-auth.test.ts index 22b99a4..ad80ec6 100644 --- a/src/test/specs/services/ac-auth.test.ts +++ b/src/test/specs/services/ac-auth.test.ts @@ -7,7 +7,6 @@ import { Mesh } from 'mesh-ioc'; import { AcAuthProvider, AuthenticationError, - DefaultAcAuthProvider, JwtService, StandardLogger, } from '../../../main/index.js'; @@ -16,15 +15,14 @@ describe('AcAuthProvider', () => { let mesh: Mesh; let fetchMock: request.FetchMock; - let authProvider: DefaultAcAuthProvider; + let authProvider: AcAuthProvider; let headers: any = {}; let jwt: any = {}; beforeEach(() => { mesh = new Mesh(); mesh.service(Logger, StandardLogger); - mesh.service(DefaultAcAuthProvider); - mesh.alias(AcAuthProvider, DefaultAcAuthProvider); + mesh.service(AcAuthProvider); mesh.service(Config, ProcessEnvConfig); mesh.constant(JwtService, { async decodeAndVerify(token: string) { @@ -35,7 +33,7 @@ describe('AcAuthProvider', () => { } }); fetchMock = request.fetchMock({ status: 200 }, { token: 'jwt-token-here' }); - authProvider = mesh.resolve(DefaultAcAuthProvider); + authProvider = mesh.resolve(AcAuthProvider); authProvider.clientRequest.config.fetch = fetchMock; jwt = { context: { @@ -48,12 +46,12 @@ describe('AcAuthProvider', () => { afterEach(() => { jwt = {}; headers = {}; - DefaultAcAuthProvider.middlewareTokensCache = new Map(); + AcAuthProvider.middlewareTokensCache = new Map(); }); describe('x-ubio-auth header exists', () => { beforeEach(() => { - const authHeader = mesh.resolve(DefaultAcAuthProvider).AC_AUTH_HEADER_NAME; + const authHeader = mesh.resolve(AcAuthProvider).AC_AUTH_HEADER_NAME; headers[authHeader] = 'Bearer jwt-token-here'; }); @@ -68,7 +66,7 @@ describe('AcAuthProvider', () => { }); it('throws when jwt is not valid', async () => { - const authHeader = mesh.resolve(DefaultAcAuthProvider).AC_AUTH_HEADER_NAME; + const authHeader = mesh.resolve(AcAuthProvider).AC_AUTH_HEADER_NAME; headers[authHeader] = 'Bearer unknown-jwt-token'; try { await authProvider.provide(headers); @@ -94,8 +92,8 @@ describe('AcAuthProvider', () => { it('does not send request if Authorization is cached', async () => { const ttl = 60000; const margin = 1000; - DefaultAcAuthProvider.middlewareCacheTtl = ttl; - DefaultAcAuthProvider.middlewareTokensCache.set('AUTH', { + AcAuthProvider.middlewareCacheTtl = ttl; + AcAuthProvider.middlewareTokensCache.set('AUTH', { token: 'jwt-token-here', authorisedAt: Date.now() - ttl + margin }); @@ -109,8 +107,8 @@ describe('AcAuthProvider', () => { it('sends request if cache has expired', async () => { const ttl = 60000; const margin = 1000; - DefaultAcAuthProvider.middlewareCacheTtl = ttl; - DefaultAcAuthProvider.middlewareTokensCache.set('AUTH', { token: 'jwt-token-here', authorisedAt: Date.now() - ttl - margin }); + AcAuthProvider.middlewareCacheTtl = ttl; + AcAuthProvider.middlewareTokensCache.set('AUTH', { token: 'jwt-token-here', authorisedAt: Date.now() - ttl - margin }); headers['authorization'] = 'AUTH'; assert.strictEqual(fetchMock.spy.called, false); const auth = await authProvider.provide(headers); @@ -140,7 +138,7 @@ describe('AcAuthProvider', () => { describe('acAuth', () => { beforeEach(() => { - const authHeader = mesh.resolve(DefaultAcAuthProvider).AC_AUTH_HEADER_NAME; + const authHeader = mesh.resolve(AcAuthProvider).AC_AUTH_HEADER_NAME; headers[authHeader] = 'Bearer jwt-token-here'; }); @@ -149,7 +147,7 @@ describe('AcAuthProvider', () => { it('sets auth.organisationId', async () => { jwt.context.organisation_id = 'some-user-org-id'; const auth = await authProvider.provide(headers); - const organisationId = auth.getOrganisationId(); + const organisationId = auth.getAuthToken()?.getOrganisationId(); assert.strictEqual(organisationId, 'some-user-org-id'); }); }); @@ -158,7 +156,7 @@ describe('AcAuthProvider', () => { it('sets auth.organisationId', async () => { headers['x-ubio-organisation-id'] = 'org-id-from-header'; const auth = await authProvider.provide(headers); - const organisationId = auth.getOrganisationId(); + const organisationId = auth.getAuthToken()?.getOrganisationId(); assert.strictEqual(organisationId, 'org-id-from-header'); }); }); @@ -168,7 +166,7 @@ describe('AcAuthProvider', () => { jwt.context['organisation_id'] = 'org-id-from-jwt'; headers['x-ubio-organisation-id'] = 'org-id-from-header'; const auth = await authProvider.provide(headers); - const organisationId = auth.getOrganisationId(); + const organisationId = auth.getAuthToken()?.getOrganisationId(); assert.strictEqual(organisationId, 'org-id-from-jwt'); }); }); @@ -182,7 +180,7 @@ describe('AcAuthProvider', () => { service_account_name: 'Bot' }; const auth = await authProvider.provide(headers); - const serviceAccount = auth.actor; + const serviceAccount = auth.getAuthToken()?.actor; assert.ok(serviceAccount?.type === 'ServiceAccount'); assert.strictEqual(serviceAccount.id, 'some-service-account-id'); assert.strictEqual(serviceAccount.name, 'Bot'); @@ -195,7 +193,7 @@ describe('AcAuthProvider', () => { organisation_id: 'ubio-organisation-id', }; const auth = await authProvider.provide(headers); - const serviceAccount = auth.actor; + const serviceAccount = auth.getAuthToken()?.actor; assert.ok(serviceAccount?.type === 'ServiceAccount'); assert.strictEqual(serviceAccount.id, 'some-service-account-id'); assert.strictEqual(serviceAccount.name, 'Bot'); @@ -209,7 +207,7 @@ describe('AcAuthProvider', () => { client_name: 'Ron Swanson', }; const auth = await authProvider.provide(headers); - const serviceAccount = auth.actor; + const serviceAccount = auth.getAuthToken()?.actor; assert.ok(serviceAccount?.type === 'ServiceAccount'); assert.strictEqual(serviceAccount.clientId, 'ClientA'); assert.strictEqual(serviceAccount.clientName, 'Ron Swanson'); @@ -226,7 +224,7 @@ describe('AcAuthProvider', () => { organisation_id: 'ubio-organisation-id', }; const auth = await authProvider.provide(headers); - const client = auth.actor; + const client = auth.getAuthToken()?.actor; assert.ok(client?.type === 'Client'); assert.strictEqual(client.id, 'some-client-id'); assert.strictEqual(client.name, 'UbioAir'); @@ -241,7 +239,7 @@ describe('AcAuthProvider', () => { client_name: 'UbioAir', }; const auth = await authProvider.provide(headers); - const actor = auth.actor; + const actor = auth.getAuthToken()?.actor; assert.ok(actor?.type === 'JobAccessToken'); }); }); @@ -256,7 +254,7 @@ describe('AcAuthProvider', () => { organisation_id: 'ubio-organisation-id', }; const auth = await authProvider.provide(headers); - const user = auth.actor; + const user = auth.getAuthToken()?.actor; assert.ok(user?.type === 'User'); assert.strictEqual(user.id, 'some-user-id'); assert.strictEqual(user.name, 'Travel Aggregator'); @@ -269,7 +267,7 @@ describe('AcAuthProvider', () => { organisation_id: 'ubio-organisation-id', }; const auth = await authProvider.provide(headers); - const user = auth.actor; + const user = auth.getAuthToken()?.actor; assert.ok(user?.type === 'User'); assert.strictEqual(user.id, 'some-user-id'); assert.strictEqual(user.name, ''); @@ -284,7 +282,7 @@ describe('AcAuthProvider', () => { user_name: 'some-user-name' }; const auth = await authProvider.provide(headers); - const user = auth.actor; + const user = auth.getAuthToken()?.actor; assert.ok(user == null); }); }); @@ -300,7 +298,7 @@ describe('AcAuthProvider', () => { organisation_id: 'ubio-organisation-id', }; const auth = await authProvider.provide(headers); - const jobAccessToken = auth.actor; + const jobAccessToken = auth.getAuthToken()?.actor; assert.ok(jobAccessToken?.type === 'JobAccessToken'); assert.strictEqual(jobAccessToken.jobId, 'some-job-id'); assert.strictEqual(jobAccessToken.clientId, 'some-client-id'); @@ -315,7 +313,7 @@ describe('AcAuthProvider', () => { organisation_id: 'ubio-organisation-id', }; const auth = await authProvider.provide(headers); - const jobAccessToken = auth.actor; + const jobAccessToken = auth.getAuthToken()?.actor; assert.ok(jobAccessToken == null); }); @@ -326,7 +324,7 @@ describe('AcAuthProvider', () => { client_name: 'Travel Aggregator', }; const auth = await authProvider.provide(headers); - const jobAccessToken = auth.actor; + const jobAccessToken = auth.getAuthToken()?.actor; assert.ok(jobAccessToken == null); }); }); diff --git a/src/test/specs/services/bypass-auth-provider.test.ts b/src/test/specs/services/bypass-auth-provider.test.ts index 6300c86..56b9da5 100644 --- a/src/test/specs/services/bypass-auth-provider.test.ts +++ b/src/test/specs/services/bypass-auth-provider.test.ts @@ -2,8 +2,8 @@ import assert from 'assert'; import supertest from 'supertest'; import { - AcAuthProvider, Application, + AuthProvider, BypassAcAuthProvider, } from '../../../main/index.js'; import { AccessRouter } from '../../routes/access.js'; @@ -14,7 +14,7 @@ describe('BypassAuthProvider', () => { override createHttpRequestScope() { const mesh = super.createHttpRequestScope(); - mesh.service(AcAuthProvider, BypassAcAuthProvider); + mesh.service(AuthProvider, BypassAcAuthProvider); mesh.service(AccessRouter); return mesh; } From c3672e1b7fe5ca9bb862f7a2ee2c5ea0ca4b19a0 Mon Sep 17 00:00:00 2001 From: Victor Ribeiro Date: Wed, 13 Aug 2025 11:12:42 +0200 Subject: [PATCH 06/14] feat(auth): add auth middleward only if AuthProvider is available in container --- src/main/http.ts | 19 +++++++++++++------ src/test/integration/ac-auth-mocking.test.ts | 11 ++++++++--- .../services/bypass-auth-provider.test.ts | 7 ++++++- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/main/http.ts b/src/main/http.ts index afa6ed2..3d091c5 100644 --- a/src/main/http.ts +++ b/src/main/http.ts @@ -40,6 +40,7 @@ export class HttpServer extends Koa { @dep() protected logger!: Logger; @dep({ key: 'httpRequestScope' }) protected createRequestScope!: () => Mesh; + @dep() protected mesh!: Mesh; protected middlewares: MiddlewareSpec[] = [ { @@ -117,7 +118,15 @@ export class HttpServer extends Koa { } addStandardMiddleware(): this { - this.middlewares.forEach(m => this.use(m.middleware)); + this.middlewares.forEach(m => { + if (m.name === 'auth') { + if (this.mesh.tryResolve(AuthProvider) !== undefined) { + this.use(m.middleware); + } + } else { + this.use(m.middleware); + } + }); return this; } @@ -174,11 +183,9 @@ export class HttpServer extends Koa { protected createAuthMiddleware(): Middleware { return async (ctx: Koa.Context, next: Koa.Next) => { const mesh: Mesh = ctx.mesh; - const provider = mesh.tryResolve(AuthProvider); - if (provider) { - const authContext = await provider.provide(ctx.headers); - mesh.constant(AuthContext, authContext); - } + const provider = mesh.resolve(AuthProvider); + const authContext = await provider.provide(ctx.headers); + mesh.constant(AuthContext, authContext); return next(); }; } diff --git a/src/test/integration/ac-auth-mocking.test.ts b/src/test/integration/ac-auth-mocking.test.ts index 4877639..f345a3e 100644 --- a/src/test/integration/ac-auth-mocking.test.ts +++ b/src/test/integration/ac-auth-mocking.test.ts @@ -21,9 +21,8 @@ describe('Mocking AcAuth', () => { class App extends Application { - override createHttpRequestScope() { - const mesh = super.createHttpRequestScope(); - mesh.service(MyRouter); + override createGlobalScope() { + const mesh = super.createGlobalScope(); mesh.constant(AuthProvider, { async provide() { return new AuthContext(new AcAuth({ @@ -38,6 +37,12 @@ describe('Mocking AcAuth', () => { return mesh; } + override createHttpRequestScope() { + const mesh = super.createHttpRequestScope(); + mesh.service(MyRouter); + return mesh; + } + } const app = new App(); diff --git a/src/test/specs/services/bypass-auth-provider.test.ts b/src/test/specs/services/bypass-auth-provider.test.ts index 56b9da5..c277916 100644 --- a/src/test/specs/services/bypass-auth-provider.test.ts +++ b/src/test/specs/services/bypass-auth-provider.test.ts @@ -12,9 +12,14 @@ describe('BypassAuthProvider', () => { class App extends Application { + override createGlobalScope() { + const mesh = super.createGlobalScope(); + mesh.service(AuthProvider, BypassAcAuthProvider); + return mesh; + } + override createHttpRequestScope() { const mesh = super.createHttpRequestScope(); - mesh.service(AuthProvider, BypassAcAuthProvider); mesh.service(AccessRouter); return mesh; } From 26acbeed8d319f5ec05582ef5fe738921d080621 Mon Sep 17 00:00:00 2001 From: Victor Ribeiro Date: Wed, 13 Aug 2025 11:22:36 +0200 Subject: [PATCH 07/14] fix(util): remove unnecessary return statement in getSingleValue function --- src/main/util.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/util.ts b/src/main/util.ts index 88f6fb8..da25dbe 100644 --- a/src/main/util.ts +++ b/src/main/util.ts @@ -113,5 +113,4 @@ export function getSingleValue(value: T | T[] | undefined): T | undefined { if (Array.isArray(value)) { return value[0]; } - return; } From 52dd13b974d0c6b2b45cd045b89cd8dd3a9259cc Mon Sep 17 00:00:00 2001 From: Victor Ribeiro Date: Wed, 13 Aug 2025 11:24:52 +0200 Subject: [PATCH 08/14] fix(util): remove redundant string chekc in getSingleValue function --- src/main/util.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/util.ts b/src/main/util.ts index da25dbe..603a06b 100644 --- a/src/main/util.ts +++ b/src/main/util.ts @@ -107,10 +107,8 @@ export function findMeshInstances(mesh: Mesh, ctor: ServiceConstructor): T } export function getSingleValue(value: T | T[] | undefined): T | undefined { - if (typeof value === 'string') { - return value; - } if (Array.isArray(value)) { return value[0]; } + return value; } From 971c2c76b55135bf38061007f62e1f1785fbd212 Mon Sep 17 00:00:00 2001 From: Victor Ribeiro Date: Wed, 13 Aug 2025 15:35:24 +0200 Subject: [PATCH 09/14] fix(http): remove specific AcAuth type from AuthContext in HttpServer --- src/main/http.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/http.ts b/src/main/http.ts index 3d091c5..c628638 100644 --- a/src/main/http.ts +++ b/src/main/http.ts @@ -12,7 +12,6 @@ import { dep, Mesh } from 'mesh-ioc'; import stoppable, { StoppableServer } from 'stoppable'; import { constants } from 'zlib'; -import { AcAuth } from './ac-auth.js'; import { ClientError } from './exception.js'; import { standardMiddleware } from './middleware.js'; import { Router } from './router.js'; @@ -185,7 +184,7 @@ export class HttpServer extends Koa { const mesh: Mesh = ctx.mesh; const provider = mesh.resolve(AuthProvider); const authContext = await provider.provide(ctx.headers); - mesh.constant(AuthContext, authContext); + mesh.constant(AuthContext, authContext); return next(); }; } From 81869181396c13d2704061b68bd7e2c73dce32b7 Mon Sep 17 00:00:00 2001 From: Victor Ribeiro Date: Mon, 18 Aug 2025 11:17:05 +0200 Subject: [PATCH 10/14] docs(auth): update instructions to auth setup and references --- docs/application.md | 2 +- docs/auth.md | 42 ++++++++++++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/docs/application.md b/docs/application.md index 2a67d67..46e147a 100644 --- a/docs/application.md +++ b/docs/application.md @@ -75,6 +75,6 @@ Note: scope simply refers to and individual `Mesh` instance. For example, when r When application is processing HTTP requests, a number of request-scoped components can be bound to Router classes: -- `AcAuth` (Automation Cloud identity and authorisation data) +- `AuthContext` (Automation Cloud identity and authorisation data) - `KoaContext` (bound by string `"KoaContext"` service identifier) — [Koa](https://koajs.org) context object - `Logger` (rebound to `RequestLogger`) which includes request-specific data diff --git a/docs/auth.md b/docs/auth.md index 113f02d..8d3dd7d 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -6,12 +6,28 @@ Automation Cloud infrastructure features "highly sophisticated" request authenti Node Framework does its best to abstract away all the complexity, allowing apps to focus on what they are supposed to be doing. -`AcAuth` object exposes identity information of current request, as well as convenience methods for request authorisation. +The framework exports a default implementation of auth provider for Automation Cloud. Although, (starting from version 16.x) this service is optional and needs explicity installation (including its dependencies, i.e. a JWT service). + +```ts +class App extends Application { + + override createGlobalScope() { + // starting from version 16.x, the following lines are required to use AC auth (default) service implementation + const mesh = super.createGlobalScope(); + mesh.service(AuthProvider, AcAuthProvider); + mesh.service(JwtService, AutomationCloudJwtService); + } +} +``` + +`AuthContext` object exposes identity information of current request, as well as convenience methods for request authorisation. + +`AuthToken` object type is based on the implementation of the `AuthProvider` registered. You may specify any auth token object type, i.e `AuthContext`. In order to retrieve specific data from a auth token type, you need to retrieve it from the auth context object with `authContenxt.getToken()`. ```ts export class MyRouter extends Router { - @dep() auth!: AcAuth; + @dep() auth!: AuthContext; @Middleware() async authorise() { @@ -25,7 +41,7 @@ export class MyRouter extends Router { }) async helloOrg() { // throws 403 when organisationId cannot be extracted from request details - const organisationId = this.auth.requireOrganisationId(); + const organisationId = this.auth.getAuthToken().requireOrganisationId(); return { message: '👋 Hello ' + organisationId }; } @@ -35,7 +51,7 @@ export class MyRouter extends Router { }) async helloServiceAccount() { // throws 403 when serviceAccount info cannot be extracted from request details - const serviceAccountId = this.auth.requireServiceAccountId(); + const serviceAccountId = this.auth.getAuthToken().requireServiceAccountId(); return { message: '👋 Hello ' + serviceAccountId }; } } @@ -43,20 +59,22 @@ export class MyRouter extends Router { ## Mocking auth in tests -In integration tests it is useful to mock `AcAuth` by providing a custom implementation of `AcAuthProvider`: +In integration tests it is useful to mock `AuthContext` by providing a custom implementation of `AcAuthProvider`: ```ts class App extends Application { - override createHttpRequestScope() { - const mesh = super.createHttpRequestScope(); + override createGlobalScope() { + const mesh = super.createGlobalScope(); mesh.constant(AcAuthProvider, { async provide() { - return new AcAuth({ - authenticated: true, - organisationId: 'my-fake-org-id', - serviceAccountId: 'my-face-service-account-id', - }); + return new AuthContext( + new AcAuth({ + authenticated: true, + organisationId: 'my-fake-org-id', + serviceAccountId: 'my-face-service-account-id', + }) + ); } }); return mesh; From ab7f46678df7dbb76bad026acf5502b74f0fb93f Mon Sep 17 00:00:00 2001 From: Victor Ribeiro Date: Mon, 18 Aug 2025 12:23:23 +0200 Subject: [PATCH 11/14] chore(deps): update/remove deprecated --- docs/structure.md | 2 +- package-lock.json | 129 +++++++++++++++++++++++++++++----------------- package.json | 16 +++--- 3 files changed, 88 insertions(+), 59 deletions(-) diff --git a/docs/structure.md b/docs/structure.md index d149e1f..b5830f7 100644 --- a/docs/structure.md +++ b/docs/structure.md @@ -53,7 +53,7 @@ export class App extends Application { await this.mongoDb.client.connect(); // Add other code to execute on application startup await this.httpServer.startServer(); - }); + }; override async afterStop() { await this.httpServer.stopServer(); diff --git a/package-lock.json b/package-lock.json index 2c4c770..457d73e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,17 +15,9 @@ "@nodescript/logger": "^2.0.6", "@nodescript/microframework": "^1.15.3", "@nodescript/pathmatcher": "^1.0.2", - "@types/koa": "^2.11.8", - "@types/koa__cors": "^3.0.2", - "@types/koa-conditional-get": "^2.0.0", - "@types/koa-etag": "^3.0.0", - "@types/node-fetch": "^2.5.8", - "@types/stoppable": "^1.1.0", - "@types/uuid": "^8.3.0", "@ubio/request": "^3.5.0", "ajv": "^8.1.0", "ajv-formats": "^2.0.2", - "chalk": "^4.1.0", "commander": "^7.2.0", "dotenv": "^16.3.1", "jsonwebtoken": "^9.0.0", @@ -36,7 +28,6 @@ "koa-etag": "^4.0.0", "mesh-config": "^1.2.1", "mesh-ioc": "^4.1.0", - "node-fetch": "^2.6.0", "reflect-metadata": "^0.1.13", "stoppable": "^1.1.0", "uuid": "^8.3.2" @@ -48,12 +39,17 @@ }, "devDependencies": { "@nodescript/eslint-config": "^1.0.4", - "@types/chalk": "^2.2.0", "@types/jsonwebtoken": "^8.5.0", + "@types/koa": "^2.11.8", + "@types/koa__cors": "^3.0.2", "@types/koa-compress": "^4.0.1", + "@types/koa-conditional-get": "^2.0.0", + "@types/koa-etag": "^3.0.0", "@types/mocha": "^8.2.0", "@types/node": "^14.14.32", + "@types/stoppable": "^1.1.0", "@types/supertest": "^2.0.10", + "@types/uuid": "^8.3.0", "eslint": "^8.24.0", "mocha": "^10.0.0", "mongodb": "^6.10.0", @@ -667,6 +663,7 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -675,25 +672,17 @@ "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" } }, - "node_modules/@types/chalk": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/chalk/-/chalk-2.2.0.tgz", - "integrity": "sha512-1zzPV9FDe1I/WHhRkf9SNgqtRJWZqrBWgu7JGveuHmmyR9CnAPCie2N/x+iHrgnpYBIcCJWHBoMRv2TRWktsvw==", - "deprecated": "This is a stub types definition for chalk (https://github.com/chalk/chalk). chalk provides its own type definitions, so you don't need @types/chalk installed!", - "dev": true, - "dependencies": { - "chalk": "*" - } - }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -701,7 +690,8 @@ "node_modules/@types/content-disposition": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.5.tgz", - "integrity": "sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==" + "integrity": "sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==", + "dev": true }, "node_modules/@types/cookiejar": { "version": "2.1.2", @@ -713,6 +703,7 @@ "version": "0.7.7", "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.7.tgz", "integrity": "sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==", + "dev": true, "dependencies": { "@types/connect": "*", "@types/express": "*", @@ -724,6 +715,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/@types/etag/-/etag-1.8.1.tgz", "integrity": "sha512-bsKkeSqN7HYyYntFRAmzcwx/dKW4Wa+KVMTInANlI72PWLQmOpZu96j0OqHZGArW4VQwCmJPteQlXaUDeOB0WQ==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -732,6 +724,7 @@ "version": "4.17.14", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", + "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.18", @@ -743,6 +736,7 @@ "version": "4.17.31", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", + "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -760,12 +754,14 @@ "node_modules/@types/http-assert": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.3.tgz", - "integrity": "sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==" + "integrity": "sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==", + "dev": true }, "node_modules/@types/http-errors": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.2.tgz", - "integrity": "sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==" + "integrity": "sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==", + "dev": true }, "node_modules/@types/json-schema": { "version": "7.0.11", @@ -791,12 +787,14 @@ "node_modules/@types/keygrip": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", - "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==" + "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==", + "dev": true }, "node_modules/@types/koa": { "version": "2.13.5", "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.13.5.tgz", "integrity": "sha512-HSUOdzKz3by4fnqagwthW/1w/yJspTgppyyalPVbgZf8jQWvdIXcVW5h2DGtw4zYntOaeRGx49r1hxoPWrD4aA==", + "dev": true, "dependencies": { "@types/accepts": "*", "@types/content-disposition": "*", @@ -812,6 +810,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/@types/koa__cors/-/koa__cors-3.3.0.tgz", "integrity": "sha512-FUN8YxcBakIs+walVe3+HcNP+Bxd0SB8BJHBWkglZ5C1XQWljlKcEFDG/dPiCIqwVCUbc5X0nYDlH62uEhdHMA==", + "dev": true, "dependencies": { "@types/koa": "*" } @@ -820,6 +819,7 @@ "version": "3.2.5", "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", "integrity": "sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==", + "dev": true, "dependencies": { "@types/koa": "*" } @@ -838,6 +838,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/koa-conditional-get/-/koa-conditional-get-2.0.0.tgz", "integrity": "sha512-kzEIakwXkkgiJhQPR/IudINqX+xB2gVS+lJWDSEUP3cMYflcX470OOWrUEqOXlc2cCAOyznMo7fFmwfnvd9wyw==", + "dev": true, "dependencies": { "@types/koa": "*" } @@ -846,6 +847,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/koa-etag/-/koa-etag-3.0.0.tgz", "integrity": "sha512-gXQUtKGEnCy0sZLG+uE3wL4mvY1CBPcb6ECjpAoD8RGYy/8ACY1B084k8LTFPIdVcmy7GD6Y4n3up3jnupofcQ==", + "dev": true, "dependencies": { "@types/etag": "*", "@types/koa": "*" @@ -854,7 +856,8 @@ "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "dev": true }, "node_modules/@types/mocha": { "version": "8.2.3", @@ -879,17 +882,20 @@ "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true }, "node_modules/@types/range-parser": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true }, "node_modules/@types/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "dev": true, "dependencies": { "@types/mime": "*", "@types/node": "*" @@ -899,6 +905,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.1.tgz", "integrity": "sha512-b8N+fCADRIYYrGZOcmOR8ZNBOqhktWTB/bMUl5LvGtT201QKJZOOH5UsFyI3qtteM6ZAJbJqZoBcLqqxKIwjhw==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -925,7 +932,8 @@ "node_modules/@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", @@ -1349,6 +1357,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1618,6 +1627,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1730,6 +1740,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1740,7 +1751,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/combined-stream": { "version": "1.0.8", @@ -4985,6 +4997,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -7687,6 +7700,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -8758,6 +8772,7 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "dev": true, "requires": { "@types/node": "*" } @@ -8766,24 +8781,17 @@ "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, "requires": { "@types/connect": "*", "@types/node": "*" } }, - "@types/chalk": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/chalk/-/chalk-2.2.0.tgz", - "integrity": "sha512-1zzPV9FDe1I/WHhRkf9SNgqtRJWZqrBWgu7JGveuHmmyR9CnAPCie2N/x+iHrgnpYBIcCJWHBoMRv2TRWktsvw==", - "dev": true, - "requires": { - "chalk": "*" - } - }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, "requires": { "@types/node": "*" } @@ -8791,7 +8799,8 @@ "@types/content-disposition": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.5.tgz", - "integrity": "sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==" + "integrity": "sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==", + "dev": true }, "@types/cookiejar": { "version": "2.1.2", @@ -8803,6 +8812,7 @@ "version": "0.7.7", "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.7.tgz", "integrity": "sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==", + "dev": true, "requires": { "@types/connect": "*", "@types/express": "*", @@ -8814,6 +8824,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/@types/etag/-/etag-1.8.1.tgz", "integrity": "sha512-bsKkeSqN7HYyYntFRAmzcwx/dKW4Wa+KVMTInANlI72PWLQmOpZu96j0OqHZGArW4VQwCmJPteQlXaUDeOB0WQ==", + "dev": true, "requires": { "@types/node": "*" } @@ -8822,6 +8833,7 @@ "version": "4.17.14", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", + "dev": true, "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.18", @@ -8833,6 +8845,7 @@ "version": "4.17.31", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", + "dev": true, "requires": { "@types/node": "*", "@types/qs": "*", @@ -8850,12 +8863,14 @@ "@types/http-assert": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.3.tgz", - "integrity": "sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==" + "integrity": "sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==", + "dev": true }, "@types/http-errors": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.2.tgz", - "integrity": "sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==" + "integrity": "sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==", + "dev": true }, "@types/json-schema": { "version": "7.0.11", @@ -8881,12 +8896,14 @@ "@types/keygrip": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", - "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==" + "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==", + "dev": true }, "@types/koa": { "version": "2.13.5", "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.13.5.tgz", "integrity": "sha512-HSUOdzKz3by4fnqagwthW/1w/yJspTgppyyalPVbgZf8jQWvdIXcVW5h2DGtw4zYntOaeRGx49r1hxoPWrD4aA==", + "dev": true, "requires": { "@types/accepts": "*", "@types/content-disposition": "*", @@ -8902,6 +8919,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/@types/koa__cors/-/koa__cors-3.3.0.tgz", "integrity": "sha512-FUN8YxcBakIs+walVe3+HcNP+Bxd0SB8BJHBWkglZ5C1XQWljlKcEFDG/dPiCIqwVCUbc5X0nYDlH62uEhdHMA==", + "dev": true, "requires": { "@types/koa": "*" } @@ -8910,6 +8928,7 @@ "version": "3.2.5", "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", "integrity": "sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==", + "dev": true, "requires": { "@types/koa": "*" } @@ -8928,6 +8947,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/koa-conditional-get/-/koa-conditional-get-2.0.0.tgz", "integrity": "sha512-kzEIakwXkkgiJhQPR/IudINqX+xB2gVS+lJWDSEUP3cMYflcX470OOWrUEqOXlc2cCAOyznMo7fFmwfnvd9wyw==", + "dev": true, "requires": { "@types/koa": "*" } @@ -8936,6 +8956,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/koa-etag/-/koa-etag-3.0.0.tgz", "integrity": "sha512-gXQUtKGEnCy0sZLG+uE3wL4mvY1CBPcb6ECjpAoD8RGYy/8ACY1B084k8LTFPIdVcmy7GD6Y4n3up3jnupofcQ==", + "dev": true, "requires": { "@types/etag": "*", "@types/koa": "*" @@ -8944,7 +8965,8 @@ "@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" + "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "dev": true }, "@types/mocha": { "version": "8.2.3", @@ -8969,17 +8991,20 @@ "@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true }, "@types/range-parser": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true }, "@types/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "dev": true, "requires": { "@types/mime": "*", "@types/node": "*" @@ -8989,6 +9014,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/stoppable/-/stoppable-1.1.1.tgz", "integrity": "sha512-b8N+fCADRIYYrGZOcmOR8ZNBOqhktWTB/bMUl5LvGtT201QKJZOOH5UsFyI3qtteM6ZAJbJqZoBcLqqxKIwjhw==", + "dev": true, "requires": { "@types/node": "*" } @@ -9015,7 +9041,8 @@ "@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true }, "@types/webidl-conversions": { "version": "7.0.3", @@ -9292,6 +9319,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -9493,6 +9521,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -9577,6 +9606,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -9584,7 +9614,8 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "combined-stream": { "version": "1.0.8", @@ -12055,7 +12086,8 @@ "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "has-property-descriptors": { "version": "1.0.0", @@ -14075,6 +14107,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } diff --git a/package.json b/package.json index b624458..1085df9 100644 --- a/package.json +++ b/package.json @@ -42,12 +42,17 @@ "homepage": "https://github.com/ubio/node-framework#readme", "devDependencies": { "@nodescript/eslint-config": "^1.0.4", - "@types/chalk": "^2.2.0", "@types/jsonwebtoken": "^8.5.0", "@types/koa-compress": "^4.0.1", "@types/mocha": "^8.2.0", "@types/node": "^14.14.32", "@types/supertest": "^2.0.10", + "@types/koa": "^2.11.8", + "@types/koa__cors": "^3.0.2", + "@types/koa-conditional-get": "^2.0.0", + "@types/koa-etag": "^3.0.0", + "@types/stoppable": "^1.1.0", + "@types/uuid": "^8.3.0", "eslint": "^8.24.0", "mocha": "^10.0.0", "mongodb": "^6.10.0", @@ -64,17 +69,9 @@ "@nodescript/logger": "^2.0.6", "@nodescript/microframework": "^1.15.3", "@nodescript/pathmatcher": "^1.0.2", - "@types/koa": "^2.11.8", - "@types/koa__cors": "^3.0.2", - "@types/koa-conditional-get": "^2.0.0", - "@types/koa-etag": "^3.0.0", - "@types/node-fetch": "^2.5.8", - "@types/stoppable": "^1.1.0", - "@types/uuid": "^8.3.0", "@ubio/request": "^3.5.0", "ajv": "^8.1.0", "ajv-formats": "^2.0.2", - "chalk": "^4.1.0", "commander": "^7.2.0", "dotenv": "^16.3.1", "jsonwebtoken": "^9.0.0", @@ -85,7 +82,6 @@ "koa-etag": "^4.0.0", "mesh-config": "^1.2.1", "mesh-ioc": "^4.1.0", - "node-fetch": "^2.6.0", "reflect-metadata": "^0.1.13", "stoppable": "^1.1.0", "uuid": "^8.3.2" From bc03273d1838dd8f3911b69ae3d42e311f1b51cc Mon Sep 17 00:00:00 2001 From: Victor Ribeiro Date: Mon, 18 Aug 2025 17:15:37 +0200 Subject: [PATCH 12/14] feat(metrics): add nodescript metrics library and drops `MetricsRegistry` --- package-lock.json | 2 +- package.json | 9 +- src/main/metrics/counter.ts | 39 -------- src/main/metrics/gauge.ts | 39 -------- src/main/metrics/global.ts | 14 ++- src/main/metrics/histogram.ts | 97 -------------------- src/main/metrics/index.ts | 6 -- src/main/metrics/metric.ts | 42 --------- src/main/metrics/registry.ts | 46 ---------- src/main/metrics/route.ts | 30 ------- src/test/specs/metrics.test.ts | 158 +-------------------------------- 11 files changed, 16 insertions(+), 466 deletions(-) delete mode 100644 src/main/metrics/counter.ts delete mode 100644 src/main/metrics/gauge.ts delete mode 100644 src/main/metrics/histogram.ts delete mode 100644 src/main/metrics/metric.ts delete mode 100644 src/main/metrics/registry.ts delete mode 100644 src/main/metrics/route.ts diff --git a/package-lock.json b/package-lock.json index 457d73e..189efcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@nodescript/errors": "^1.2.0", "@nodescript/http-server": "^2.10.1", "@nodescript/logger": "^2.0.6", + "@nodescript/metrics": "^1.7.1", "@nodescript/microframework": "^1.15.3", "@nodescript/pathmatcher": "^1.0.2", "@ubio/request": "^3.5.0", @@ -616,7 +617,6 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@nodescript/metrics/-/metrics-1.7.1.tgz", "integrity": "sha512-MUwJcKL52XRai35WRfNyAQ/egKD/me2zoEfm1QbIGm1epjlB+ZvTs8qtciekTLLxsx/BlJd3becAsXpmZhXguQ==", - "license": "ISC", "dependencies": { "mesh-decorators": "^1.1.2", "mesh-ioc": "^4.1.0" diff --git a/package.json b/package.json index 1085df9..7cd54f4 100644 --- a/package.json +++ b/package.json @@ -43,15 +43,15 @@ "devDependencies": { "@nodescript/eslint-config": "^1.0.4", "@types/jsonwebtoken": "^8.5.0", - "@types/koa-compress": "^4.0.1", - "@types/mocha": "^8.2.0", - "@types/node": "^14.14.32", - "@types/supertest": "^2.0.10", "@types/koa": "^2.11.8", "@types/koa__cors": "^3.0.2", + "@types/koa-compress": "^4.0.1", "@types/koa-conditional-get": "^2.0.0", "@types/koa-etag": "^3.0.0", + "@types/mocha": "^8.2.0", + "@types/node": "^14.14.32", "@types/stoppable": "^1.1.0", + "@types/supertest": "^2.0.10", "@types/uuid": "^8.3.0", "eslint": "^8.24.0", "mocha": "^10.0.0", @@ -67,6 +67,7 @@ "@nodescript/errors": "^1.2.0", "@nodescript/http-server": "^2.10.1", "@nodescript/logger": "^2.0.6", + "@nodescript/metrics": "^1.7.1", "@nodescript/microframework": "^1.15.3", "@nodescript/pathmatcher": "^1.0.2", "@ubio/request": "^3.5.0", diff --git a/src/main/metrics/counter.ts b/src/main/metrics/counter.ts deleted file mode 100644 index d467e65..0000000 --- a/src/main/metrics/counter.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Metric, MetricDatum } from './metric.js'; - -export class CounterMetric extends Metric { - protected data: Map> = new Map(); - - getType() { - return 'counter'; - } - - get(labels: Partial = {}) { - return this.data.get(this.createMetricLabelsKey(labels)); - } - - incr(value: number = 1, labels: Partial = {}, timestamp?: number) { - const key = this.createMetricLabelsKey(labels); - const datum = this.data.get(key); - if (datum) { - datum.value += value; - datum.timestamp = timestamp; - } else { - this.data.set(key, { - value, - timestamp, - labels, - }); - } - } - - *generateReportLines() { - for (const datum of this.data.values()) { - yield [ - this.getMetricLineName(datum.labels), - datum.value, - datum.timestamp, - ].filter(x => x != null).join(' '); - } - } - -} diff --git a/src/main/metrics/gauge.ts b/src/main/metrics/gauge.ts deleted file mode 100644 index 0a57e6e..0000000 --- a/src/main/metrics/gauge.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Metric, MetricDatum } from './metric.js'; - -export class GaugeMetric extends Metric { - protected data: Map> = new Map(); - - getType() { - return 'gauge'; - } - - *generateReportLines() { - for (const datum of this.data.values()) { - yield [ - this.getMetricLineName(datum.labels), - datum.value, - datum.timestamp, - ].filter(x => x != null).join(' '); - } - } - - get(labels: Partial = {}) { - return this.data.get(this.createMetricLabelsKey(labels)); - } - - set(value: number, labels: Partial = {}, timestamp?: number) { - const key = this.createMetricLabelsKey(labels); - const datum = this.data.get(key); - if (datum) { - datum.value = value; - datum.timestamp = timestamp; - } else { - this.data.set(key, { - value, - timestamp, - labels, - }); - } - } - -} diff --git a/src/main/metrics/global.ts b/src/main/metrics/global.ts index 462be4a..82c3068 100644 --- a/src/main/metrics/global.ts +++ b/src/main/metrics/global.ts @@ -1,17 +1,15 @@ -import { MetricsRegistry } from './registry.js'; +import { CounterMetric, GaugeMetric, HistogramMetric, metric } from '@nodescript/metrics'; const METRICS_GLOBAL_KEY = Symbol.for('@ubio/framework:globalMetrics'); -export class GlobalMetricsRegistry extends MetricsRegistry { - methodDuration = this.histogram('app_method_duration_seconds', +export class GlobalMetricsRegistry { + @metric() methodDuration = new HistogramMetric('app_method_duration_seconds', 'Performance measurements taken for a particular class method'); - handlerDuration = this.histogram('app_handler_duration_seconds', + @metric() handlerDuration = new HistogramMetric('app_handler_duration_seconds', 'Application performance measurements'); - - mongoDocumentsTotal = this.gauge('mongo_documents_total', + @metric() mongoDocumentsTotal = new GaugeMetric('mongo_documents_total', 'Estimated count of MongoDB documents, per collection'); - - appLogsTotal = this.counter('app_logs_total', + @metric() appLogsTotal = new CounterMetric('app_logs_total', 'Total count of log lines by severity'); } diff --git a/src/main/metrics/histogram.ts b/src/main/metrics/histogram.ts deleted file mode 100644 index 92af396..0000000 --- a/src/main/metrics/histogram.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Metric } from './metric.js'; - -const DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]; - -export class HistogramMetric extends Metric { - protected buckets: number[]; - data: Map> = new Map(); - - constructor( - name: string, - help: string, - buckets: number[] = DEFAULT_BUCKETS, - ) { - super(name, help); - this.buckets = buckets.slice().sort((a, b) => a > b ? 1 : -1); - } - - getType() { - return 'histogram'; - } - - add(value: number, labels: Partial = {}, timestamp: number = Date.now()) { - const key = this.createMetricLabelsKey(labels); - const datum = this.data.get(key); - if (datum) { - datum.timestamp = timestamp; - datum.buckets = this.calcBuckets(datum.buckets, value); - datum.count += 1; - datum.sum += value; - } else { - this.data.set(key, { - labels, - timestamp, - buckets: this.calcBuckets([], value), - count: 1, - sum: value, - }); - } - } - - timer(labels: Partial = {}) { - const startedAt = process.hrtime(); - return () => { - const [sec, nanosec] = process.hrtime(startedAt); - const value = sec + nanosec * 1e-9; - this.add(value, labels); - }; - } - - async measure(fn: () => Promise, labels: Partial = {}): Promise { - const stop = this.timer(labels); - try { - return await fn(); - } finally { - stop(); - } - } - - protected calcBuckets(existingValues: number[], newValue: number) { - return this.buckets - .map(n => newValue <= n ? 1 : 0) - .map((incr, index) => { - return incr + (existingValues[index] || 0); - }); - } - - protected *generateReportLines() { - for (const datum of this.data.values()) { - for (const [i, le] of this.buckets.entries()) { - const prefix = this.getMetricLineName({ ...datum.labels, le: String(le) }, '_bucket'); - const sample = datum.buckets[i]; - yield [prefix, sample].join(' '); - } - yield [ - this.getMetricLineName({ ...datum.labels, le: '+Inf' }, '_bucket'), - datum.count - ].join(' '); - yield [ - this.getMetricLineName(datum.labels, '_sum'), - datum.sum - ].join(' '); - yield [ - this.getMetricLineName(datum.labels, '_count'), - datum.count - ].join(' '); - } - } - -} - -export interface HistogramDatum { - labels: Partial; - timestamp: number; - buckets: number[]; // correspond to configured buckets, w/o +Inf - count: number; - sum: number; -} diff --git a/src/main/metrics/index.ts b/src/main/metrics/index.ts index b3c70cf..f056117 100644 --- a/src/main/metrics/index.ts +++ b/src/main/metrics/index.ts @@ -1,7 +1 @@ -export * from './counter.js'; -export * from './gauge.js'; export * from './global.js'; -export * from './histogram.js'; -export * from './metric.js'; -export * from './registry.js'; -export * from './route.js'; diff --git a/src/main/metrics/metric.ts b/src/main/metrics/metric.ts deleted file mode 100644 index 535b206..0000000 --- a/src/main/metrics/metric.ts +++ /dev/null @@ -1,42 +0,0 @@ -export interface MetricDatum { - labels: Partial; - timestamp?: number; - value: number; -} - -export abstract class Metric { - - constructor( - public name: string, - public help: string, - ) { - } - - abstract getType(): string; - - protected abstract generateReportLines(): Iterable; - - getMetricLineName(labels: Partial, suffix: string = '') { - const fields = this.createMetricLabelsKey(labels); - return fields ? `${this.name}${suffix}{${fields}}` : this.name; - } - - report() { - const report = []; - report.push(`# HELP ${this.name} ${this.help}`); - report.push(`# TYPE ${this.name} ${this.getType()}`); - report.push(...this.generateReportLines()); - return report.join('\n'); - } - - protected createMetricLabelsKey(labels: Partial = {}) { - return Object.keys(labels) - .sort() - .map(k => { - const v = (labels as any)[k]; - return `${k}=${JSON.stringify(String(v))}`; - }) - .join(','); - } - -} diff --git a/src/main/metrics/registry.ts b/src/main/metrics/registry.ts deleted file mode 100644 index 7ddf427..0000000 --- a/src/main/metrics/registry.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Exception } from '../exception.js'; -import { CounterMetric } from './counter.js'; -import { GaugeMetric } from './gauge.js'; -import { HistogramMetric } from './histogram.js'; -import { Metric } from './metric.js'; - -export class MetricsRegistry { - protected registry: Map = new Map(); - - counter(name: string, help: string) { - const counter = new CounterMetric(name, help); - this.register(counter); - return counter; - } - - gauge(name: string, help: string) { - const gauge = new GaugeMetric(name, help); - this.register(gauge); - return gauge; - } - - histogram(name: string, help: string, buckets?: number[]) { - const histogram = new HistogramMetric(name, help, buckets); - this.register(histogram); - return histogram; - } - - protected register(metric: Metric) { - // It is prohibited to register a metric with same name twice - const existing = this.registry.get(metric.name); - if (existing) { - throw new MetricAlreadyDefined(metric.name); - } - this.registry.set(metric.name, metric); - } - - report() { - return [...this.registry.values()].map(_ => _.report()).join('\n\n'); - } -} - -export class MetricAlreadyDefined extends Exception { - constructor(metricName: string) { - super(`Metric ${metricName} is already defined; please store a reference to a metric instance`); - } -} diff --git a/src/main/metrics/route.ts b/src/main/metrics/route.ts deleted file mode 100644 index fad335d..0000000 --- a/src/main/metrics/route.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { dep, Mesh } from 'mesh-ioc'; - -import { Get, Router } from '../router.js'; -import { findMeshInstances } from '../util.js'; -import { MetricsRegistry } from './registry.js'; - -export class MetricsRouter extends Router { - - @dep() protected mesh!: Mesh; - - @Get({ - path: '/metrics', - summary: '(internal) Get current process metrics', - responses: { - 200: { - description: 'Prometheus metrics in text-based format', - contentType: 'text/plain', - } - } - }) - async metrics() { - this.ctx.type = 'text/plain; version=0.0.4'; - return this.getRegistries().map(_ => _.report()).join('\n\n'); - } - - protected getRegistries(): MetricsRegistry[] { - return findMeshInstances(this.mesh, MetricsRegistry); - } - -} diff --git a/src/test/specs/metrics.test.ts b/src/test/specs/metrics.test.ts index f13fd1e..eff7edc 100644 --- a/src/test/specs/metrics.test.ts +++ b/src/test/specs/metrics.test.ts @@ -1,161 +1,11 @@ +import { generateMetricsReport } from '@nodescript/metrics'; import assert from 'assert'; import supertest from 'supertest'; -import theredoc from 'theredoc'; -import { - Application, - CounterMetric, - GaugeMetric, - getGlobalMetrics, - HistogramMetric -} from '../../main/index.js'; +import { Application } from '../../main/index.js'; import { FooRouter } from '../routes/foo.js'; -describe('CounterMetric', () => { - - it('increments an new counter', () => { - const counter = new CounterMetric('foo', 'Foo help'); - counter.incr(1, { foo: 'one' }); - const datum = counter.get({ foo: 'one' }); - assert.ok(datum); - assert.strictEqual(datum?.value, 1); - }); - - it('increments an existing counter', () => { - const counter = new CounterMetric('foo', 'Foo help'); - counter.incr(1, { foo: 'one' }); - counter.incr(1, { foo: 'one' }); - const datum = counter.get({ foo: 'one' }); - assert.ok(datum); - assert.strictEqual(datum?.value, 2); - }); - - it('increments without labels', () => { - const counter = new CounterMetric('foo', 'Foo help'); - counter.incr(); - const datum = counter.get(); - assert.ok(datum); - assert.strictEqual(datum?.value, 1); - }); - - it('compiles a report', () => { - const counter = new CounterMetric('foo', 'Foo help'); - counter.incr(1, {}, 123123123); - counter.incr(1, { lbl: 'one' }, 123123123); - counter.incr(2, { lbl: 'two' }); - counter.incr(3, { lbl: 'three', foo: '1' }, 123123123); - assert.strictEqual(counter.report().trim(), theredoc` - # HELP foo Foo help - # TYPE foo counter - foo 1 123123123 - foo{lbl="one"} 1 123123123 - foo{lbl="two"} 2 - foo{foo="1",lbl="three"} 3 123123123 - `.trim()); - }); - -}); - -describe('GaugeMetric', () => { - - it('sets a new value', () => { - const gauge = new GaugeMetric('foo', 'Foo help'); - gauge.set(1, { foo: 'one' }); - const datum = gauge.get({ foo: 'one' }); - assert.ok(datum); - assert.strictEqual(datum?.value, 1); - }); - - it('overwrites a existing value', () => { - const counter = new GaugeMetric('foo', 'Foo help'); - counter.set(2, { foo: 'one' }); - counter.set(5, { foo: 'one' }); - const datum = counter.get({ foo: 'one' }); - assert.ok(datum); - assert.strictEqual(datum?.value, 5); - }); - - it('sets without labels', () => { - const counter = new GaugeMetric('foo', 'Foo help'); - counter.set(5); - const datum = counter.get(); - assert.ok(datum); - assert.strictEqual(datum?.value, 5); - }); - - it('compiles a report', () => { - const counter = new GaugeMetric('foo', 'Foo help'); - counter.set(1, {}, 123123123); - counter.set(1, { lbl: 'one' }, 123123123); - counter.set(2, { lbl: 'two' }, 123123123); - counter.set(3, { lbl: 'three', foo: '1' }, 123123123); - assert.strictEqual(counter.report().trim(), theredoc` - # HELP foo Foo help - # TYPE foo gauge - foo 1 123123123 - foo{lbl="one"} 1 123123123 - foo{lbl="two"} 2 123123123 - foo{foo="1",lbl="three"} 3 123123123 - `.trim()); - }); - -}); - -describe('HistogramMetric', () => { - - it('compiles a report', () => { - const histogram = new HistogramMetric('foo', 'Foo help'); - histogram.add(0.16, { lbl: 'one' }); - histogram.add(0.15, { lbl: 'one' }); - histogram.add(0.24, { lbl: 'one' }); - histogram.add(0.11, { lbl: 'one' }); - histogram.add(0.55, { lbl: 'one' }); - - histogram.add(0.1, { lbl: 'two' }); - histogram.add(0.6, { lbl: 'two' }); - histogram.add(0.9, { lbl: 'two' }); - histogram.add(1.0, { lbl: 'two' }); - histogram.add(1.05, { lbl: 'two' }); - histogram.add(2, { lbl: 'two' }); - histogram.add(3, { lbl: 'two' }); - - assert.strictEqual(histogram.report().trim(), theredoc` - # HELP foo Foo help - # TYPE foo histogram - foo_bucket{lbl="one",le="0.005"} 0 - foo_bucket{lbl="one",le="0.01"} 0 - foo_bucket{lbl="one",le="0.025"} 0 - foo_bucket{lbl="one",le="0.05"} 0 - foo_bucket{lbl="one",le="0.1"} 0 - foo_bucket{lbl="one",le="0.25"} 4 - foo_bucket{lbl="one",le="0.5"} 4 - foo_bucket{lbl="one",le="1"} 5 - foo_bucket{lbl="one",le="2.5"} 5 - foo_bucket{lbl="one",le="5"} 5 - foo_bucket{lbl="one",le="10"} 5 - foo_bucket{lbl="one",le="+Inf"} 5 - foo_sum{lbl="one"} 1.21 - foo_count{lbl="one"} 5 - foo_bucket{lbl="two",le="0.005"} 0 - foo_bucket{lbl="two",le="0.01"} 0 - foo_bucket{lbl="two",le="0.025"} 0 - foo_bucket{lbl="two",le="0.05"} 0 - foo_bucket{lbl="two",le="0.1"} 1 - foo_bucket{lbl="two",le="0.25"} 1 - foo_bucket{lbl="two",le="0.5"} 1 - foo_bucket{lbl="two",le="1"} 4 - foo_bucket{lbl="two",le="2.5"} 6 - foo_bucket{lbl="two",le="5"} 7 - foo_bucket{lbl="two",le="10"} 7 - foo_bucket{lbl="two",le="+Inf"} 7 - foo_sum{lbl="two"} 8.65 - foo_count{lbl="two"} 7 - `.trim()); - }); - -}); - -describe('Routes execution histogram metric', () => { +describe('Routes execution metrics', () => { class App extends Application { @@ -185,7 +35,7 @@ describe('Routes execution histogram metric', () => { await request.get('/foo/1'); await request.get('/foo/2'); - const metricsReport = getGlobalMetrics().report(); + const metricsReport = generateMetricsReport(app.mesh); assert.match(metricsReport, /path="\/foo\/{fooId}"/); assert.doesNotMatch(metricsReport, /path="\/foo\/1"/); From 45c2d08a12ce0acd4f58bfa17f688d95c041dd0b Mon Sep 17 00:00:00 2001 From: Victor Ribeiro Date: Mon, 18 Aug 2025 17:45:11 +0200 Subject: [PATCH 13/14] feat(metrics): replate GlobalMetrics from global object and add it to mesh container as a service --- src/main/application.ts | 4 ++-- src/main/logger.ts | 7 +++++-- src/main/metrics/global.ts | 20 +++++--------------- src/main/router.ts | 5 +++-- src/modules/mongodb.ts | 6 +++--- 5 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/main/application.ts b/src/main/application.ts index 0716966..3ea5de0 100644 --- a/src/main/application.ts +++ b/src/main/application.ts @@ -4,7 +4,7 @@ import { Config, config, ConfigError, getMeshConfigs } from 'mesh-config'; import { dep, Mesh } from 'mesh-ioc'; import { HttpRequestLogger, HttpServer } from './http.js'; -import { getGlobalMetrics } from './metrics/global.js'; +import { GlobalMetrics } from './metrics/global.js'; /** @@ -28,7 +28,7 @@ export class Application extends BaseApp { this.mesh.constant('httpRequestScope', () => this.createHttpRequestScope()); this.mesh.alias('AppLogger', Logger); this.mesh.service(HttpServer); - this.mesh.constant('GlobalMetrics', getGlobalMetrics()); + this.mesh.service(GlobalMetrics); return this.mesh; } diff --git a/src/main/logger.ts b/src/main/logger.ts index a5ce9db..a1af4b4 100644 --- a/src/main/logger.ts +++ b/src/main/logger.ts @@ -1,7 +1,8 @@ import { ConsoleLogger, DefaultLogFormatter, LOG_LEVELS, LogfmtFormatter, Logger, LogLevel, LogPayload, StructuredLogFormatter } from '@nodescript/logger'; import { config } from 'mesh-config'; +import { dep } from 'mesh-ioc'; -import { getGlobalMetrics } from './metrics/global.js'; +import { GlobalMetrics } from './metrics/global.js'; export { LOG_LEVELS, @@ -19,6 +20,8 @@ export class StandardLogger extends ConsoleLogger { @config({ default: false }) LOG_LOGFMT!: boolean; + @dep() protected globalMetrics!: GlobalMetrics; + constructor() { super(); this.formatter = this.LOG_PRETTY ? new DefaultLogFormatter() : @@ -27,7 +30,7 @@ export class StandardLogger extends ConsoleLogger { } override write(payload: LogPayload) { - getGlobalMetrics().appLogsTotal.incr(1, { severity: payload.level }); + this.globalMetrics.appLogsTotal.incr(1, { severity: payload.level }); super.write(payload); } diff --git a/src/main/metrics/global.ts b/src/main/metrics/global.ts index 82c3068..0c9f8a4 100644 --- a/src/main/metrics/global.ts +++ b/src/main/metrics/global.ts @@ -1,10 +1,10 @@ import { CounterMetric, GaugeMetric, HistogramMetric, metric } from '@nodescript/metrics'; -const METRICS_GLOBAL_KEY = Symbol.for('@ubio/framework:globalMetrics'); +const methodDurationMetric = new HistogramMetric('app_method_duration_seconds', + 'Performance measurements taken for a particular class method'); -export class GlobalMetricsRegistry { - @metric() methodDuration = new HistogramMetric('app_method_duration_seconds', - 'Performance measurements taken for a particular class method'); +export class GlobalMetrics { + @metric() methodDuration = methodDurationMetric; @metric() handlerDuration = new HistogramMetric('app_handler_duration_seconds', 'Application performance measurements'); @metric() mongoDocumentsTotal = new GaugeMetric('mongo_documents_total', @@ -13,21 +13,11 @@ export class GlobalMetricsRegistry { 'Total count of log lines by severity'); } -export function getGlobalMetrics(): GlobalMetricsRegistry { - let registry = (global as any)[METRICS_GLOBAL_KEY]; - if (!(registry instanceof GlobalMetricsRegistry)) { - registry = new GlobalMetricsRegistry(); - (global as any)[METRICS_GLOBAL_KEY] = registry; - } - return registry; -} - export function MeasureAsync() { return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { - const globalMetrics = getGlobalMetrics(); const originalMethod = descriptor.value; descriptor.value = async function (this: any): Promise { - const end = globalMetrics.methodDuration.timer({ + const end = methodDurationMetric.timer({ class: target.constructor.name, method: propertyKey, }); diff --git a/src/main/router.ts b/src/main/router.ts index dda0cb4..0a2cb3c 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -7,7 +7,7 @@ import { config } from 'mesh-config'; import { dep } from 'mesh-ioc'; import { ClientError, Exception } from './exception.js'; -import { getGlobalMetrics } from './metrics/global.js'; +import { GlobalMetrics } from './metrics/global.js'; import { ajvErrorToMessage, AnyConstructor, Constructor, deepClone } from './util.js'; const ROUTES_KEY = Symbol('Route'); @@ -134,6 +134,7 @@ export class Router { @dep() protected logger!: Logger; @dep({ key: 'KoaContext' }) protected ctx!: koa.Context; + @dep() protected globalMetrics!: GlobalMetrics; @config({ default: false }) HTTP_VALIDATE_RESPONSES!: boolean; @@ -152,7 +153,7 @@ export class Router { route, ...getAfterHookRoutes(this.constructor as Constructor), ]; - await getGlobalMetrics().handlerDuration.measure(async () => { + await this.globalMetrics.handlerDuration.measure(async () => { // Route matched, now execute all middleware first, then execute the route itself const response = await this.handleRoutes(routes); this.ctx.body = response ?? this.ctx.body ?? {}; diff --git a/src/modules/mongodb.ts b/src/modules/mongodb.ts index e3212fb..8e7cc78 100644 --- a/src/modules/mongodb.ts +++ b/src/modules/mongodb.ts @@ -3,7 +3,7 @@ import { config } from 'mesh-config'; import { dep } from 'mesh-ioc'; import { Db, MongoClient } from 'mongodb'; -import { getGlobalMetrics } from '../main/index.js'; +import { GlobalMetrics } from '../main/index.js'; export class MongoDb { client: MongoClient; @@ -15,6 +15,7 @@ export class MongoDb { @config({ default: 10000 }) MONGO_METRICS_REFRESH_INTERVAL!: number; @dep() protected logger!: Logger; + @dep() protected globalMetrics!: GlobalMetrics; constructor() { this.client = new MongoClient(this.MONGO_URL, { @@ -52,12 +53,11 @@ export class MongoDb { protected async refreshMetricsLoop() { while (this.refreshingMetrics) { try { - const metrics = getGlobalMetrics(); const collections = await this.db.listCollections().toArray(); const counts = await Promise.all( collections.map(col => this.db.collection(col.name).estimatedDocumentCount())); for (const [i, col] of collections.entries()) { - metrics.mongoDocumentsTotal.set(counts[i], { + this.globalMetrics.mongoDocumentsTotal.set(counts[i], { collection: col.name, db: this.db.databaseName, }); From 0ccab9c3b500f306a890b102e69740b8e645409c Mon Sep 17 00:00:00 2001 From: Victor Ribeiro Date: Mon, 18 Aug 2025 18:05:35 +0200 Subject: [PATCH 14/14] docs(metrics): update metrics documentation to reflect changes and add reference to usage of `@nodescript/metrics` --- docs/metrics.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/metrics.md b/docs/metrics.md index a3669c6..142d976 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -1,6 +1,6 @@ ## Metrics -Framework provides a standard facility to collect and expose Prometheus metrics. +Framework provides (global) standard metrics and that can be extended with `@nodescript/metrics`, to collect and expose Prometheus metrics. Following main use cases are supported: @@ -12,15 +12,15 @@ Following main use cases are supported: Create a `Metrics` class in `src/main/metrics.ts` and define your counters, histograms and gauges. ```ts -import { MetricsRegistry } from '@ubio/framework'; +import { metric, CounterMetric } from '@nodescript/metrics'; -export class Metrics extends MetricsRegistry { +export class Metrics { - dataCacheKeysRequested = this.counter('api_dataset_cache_keys_requested_total', + @metric() dataCacheKeysRequested = new CounterMetric('api_dataset_cache_keys_requested_total', 'Data Cache: Keys Requested'); - dataCacheKeysRetrieved = this.counter('api_dataset_cache_keys_retrieved_total', + @metric() dataCacheKeysRetrieved = new CounterMetric('api_dataset_cache_keys_retrieved_total', 'Data Cache: Keys Retrieved'); - dataCacheKeysStored = this.counter('api_dataset_cache_keys_stored_total', + @metric() dataCacheKeysStored = new CounterMetric('api_dataset_cache_keys_stored_total', 'Data Cache: Keys Stored'); } @@ -43,13 +43,13 @@ export class App extends Application { This will do two things: 1. bind `Metrics` to self in singleton scope -2. bind that instance to `MetricsRegistry` which is used to aggregate mutliple registries (including the global registry) +2. make it available to `@nodescript/metrics` so the app can expose the reports over http. Finally, for observing the metrics in your classes, simply `@dep() metrics: Metrics` and start using the counters, gauges and histograms you have defined. ### Global metrics -Global metrics are collected in `GlobalMetricsRegistry` which is automatically bound to all applications (you don't have to specify anything). +Global metrics are collected in `GlobalMetrics` which is automatically bound to all applications (you don't have to specify anything). Global metrics include `@MeasureAsync` decorator which can be used to annotate any Promise-returning method so that its performance can be reported and analyse. @@ -69,4 +69,10 @@ In this example, `app_method_latencies_seconds{class="MyClass",method="doWork"}` ### Metrics endpoint -MetricsRouter is automatically bound to all applications (you don't have to do anything). It serves `GET /metrics` endpoint and reports metrics from all registered registries. +By default, the application will run a http server (in a different port) that handles requests to `GET /metrics` endpoint, unless configured otherwise (i.e. `START_AUX_HTTP_SERVER_ON_START=false` is set). So, you don't have to do anything to expose the metrics reports. + +This endpoint reports metrics from all registered ones, which are: + +- class member decorated with `@metric()`; +- metric type instantiated (i.e. `new CounterMetric()`); +- and its class is bound to mesh container as a service (i.e `mesh.service(Metrics)`).