From c1ad46c02caf1a846a0762e556bbd3f8f3ad6e78 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Mar 2026 20:23:55 +0000 Subject: [PATCH 01/12] =?UTF-8?q?fix:=20resolve=20merge=20conflicts=20?= =?UTF-8?q?=E2=80=94=20squash=20all=20branch=20changes=20onto=20current=20?= =?UTF-8?q?main?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Combines all changes from this PR into a single clean commit: - backend.js: store raw buffer on req.rawBody before express.json() runs so HMAC verification uses the original bytes (not JSON.stringify output); guard log stream creation in try-catch with .on('error') fallback to console so filesystem failures never crash the process - package.json: broaden engine range from >=20 <22 to >=20 (Node v24 compat) - package-lock.json: regenerate with express@4.x and all transitive deps (was missing, causing npm ci to fail on VPS) - runewager-endpoint.service: User/Group → root (VPS runs as root) - runewager.service: User/Group → root (VPS runs as root) https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4 --- backend.js | 29 +- package-lock.json | 865 ++++++++++++++++++++++++++++++++++++- package.json | 2 +- runewager-endpoint.service | 4 +- runewager.service | 4 +- 5 files changed, 886 insertions(+), 18 deletions(-) diff --git a/backend.js b/backend.js index e7c1996..1b44f8e 100644 --- a/backend.js +++ b/backend.js @@ -38,22 +38,28 @@ const LOG_DIR = path.join(__dirname, 'logs'); const LOG_FILE = path.join(LOG_DIR, 'backend.log'); const ERR_FILE = path.join(LOG_DIR, 'backend-error.log'); +let _logStream = null; +let _errStream = null; + try { if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true }); + _logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' }); + _errStream = fs.createWriteStream(ERR_FILE, { flags: 'a' }); + _logStream.on('error', (e) => { _logStream = null; console.error(`[backend] log stream error: ${e.message}`); }); + _errStream.on('error', (e) => { _errStream = null; console.error(`[backend] err stream error: ${e.message}`); }); } catch (e) { - console.error(`[backend] Cannot create log directory ${LOG_DIR}: ${e.message}`); + console.error(`[backend] Cannot open log files in ${LOG_DIR}: ${e.message}`); } -const _logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' }); -const _errStream = fs.createWriteStream(ERR_FILE, { flags: 'a' }); - const logger = { _entry(level, eventType, msg, extra) { return { ts: new Date().toISOString(), level, eventType, msg, ...extra }; }, _write(streams, obj) { const line = JSON.stringify(obj) + '\n'; - for (const s of streams) s.write(line); + for (const s of streams) { + if (s) s.write(line); else console.log(line.trimEnd()); + } }, info(eventType, msg, extra = {}) { this._write([_logStream], this._entry('info', eventType, msg, extra)); @@ -133,8 +139,15 @@ app.use((req, _res, next) => { next(); }); -// Capture raw body for HMAC verification on /autofix routes before JSON parsing -app.use('/autofix', express.raw({ type: '*/*', limit: '1mb' })); +// Capture raw body for HMAC verification on /autofix routes. +// Store it on req.rawBody before express.json() can overwrite req.body. +app.use('/autofix', (req, res, next) => { + express.raw({ type: '*/*', limit: '1mb' })(req, res, (err) => { + if (err) return next(err); + if (Buffer.isBuffer(req.body)) req.rawBody = req.body; + next(); + }); +}); app.use(express.json({ limit: '1mb' })); // ─── GET /health ────────────────────────────────────────────────────────────── @@ -191,7 +204,7 @@ app.post('/autofix/webhook', async (req, res) => { const requestId = req.id; try { const sig = req.headers['x-autofix-signature'] ?? req.headers['x-hub-signature-256'] ?? ''; - const rawBody = Buffer.isBuffer(req.body) ? req.body : Buffer.from(JSON.stringify(req.body ?? {})); + const rawBody = req.rawBody ?? Buffer.alloc(0); if (!AUTOFIX_SECRET || !verifySignature(rawBody, sig)) { const reason = !AUTOFIX_SECRET ? 'AUTOFIX_SECRET not configured' : 'Signature mismatch'; diff --git a/package-lock.json b/package-lock.json index 59e9720..9cbb515 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,19 @@ { - "name": "runewager-telegram-bot", - "version": "1.0.0", + "name": "runewager-bot", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "runewager-telegram-bot", - "version": "1.0.0", + "name": "runewager-bot", + "version": "3.0.0", "dependencies": { - "dotenv": "^16.4.7", + "dotenv": "^16.6.1", + "express": "^4.21.2", "telegraf": "^4.16.3" + }, + "engines": { + "node": ">=20" } }, "node_modules/@telegraf/types": { @@ -30,6 +34,64 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/buffer-alloc": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", @@ -52,6 +114,80 @@ "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", "license": "MIT" }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -69,6 +205,25 @@ } } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -81,6 +236,80 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -90,6 +319,316 @@ "node": ">=6" } }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -105,6 +644,15 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -125,6 +673,30 @@ } } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/p-timeout": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-4.1.0.tgz", @@ -134,6 +706,93 @@ "node": ">=10" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-compare": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/safe-compare/-/safe-compare-1.1.4.tgz", @@ -143,6 +802,12 @@ "buffer-alloc": "^1.2.0" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/sandwich-stream": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/sandwich-stream/-/sandwich-stream-2.0.2.tgz", @@ -152,6 +817,147 @@ "node": ">= 0.10" } }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/telegraf": { "version": "4.16.3", "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.16.3.tgz", @@ -174,12 +980,61 @@ "node": "^12.20.0 || >=14.13.1" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 019bb71..fca3afb 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "index.js", "type": "commonjs", "engines": { - "node": ">=20 <22" + "node": ">=20" }, "scripts": { "start": "node index.js", diff --git a/runewager-endpoint.service b/runewager-endpoint.service index eaf2e61..5e13753 100644 --- a/runewager-endpoint.service +++ b/runewager-endpoint.service @@ -22,8 +22,8 @@ Wants=network-online.target Type=simple WorkingDirectory=/var/www/html/Runewager ExecStart=/usr/bin/node /var/www/html/Runewager/backend.js -User=runewager -Group=runewager +User=root +Group=root EnvironmentFile=/var/www/html/Runewager/.env Environment=NODE_ENV=production diff --git a/runewager.service b/runewager.service index ab0cc19..7b79f78 100644 --- a/runewager.service +++ b/runewager.service @@ -25,8 +25,8 @@ Wants=network-online.target Type=simple WorkingDirectory=/var/www/html/Runewager ExecStart=/usr/bin/node /var/www/html/Runewager/index.js -User=runewager -Group=runewager +User=root +Group=root EnvironmentFile=/var/www/html/Runewager/.env Environment=NODE_ENV=production From 722c7a08a269019988984c58109251a1c17358a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Mar 2026 21:04:00 +0000 Subject: [PATCH 02/12] fix: prevent EADDRINUSE crash-loops and lock Express version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pin express to 4.22.1 (remove ^ semver range) to match the explicitly installed version and prevent future version drift that caused the missing-dependency failure on Node 21. - Add ExecStartPre to runewager-endpoint.service: kills any stale process holding port 3001 before each systemd start, eliminating the EADDRINUSE → instant-crash → restart loop. Also tightened StartLimitBurst=4 / RestartSec=10 / StartLimitIntervalSec=120 to reduce CPU spike if the service still fails repeatedly. - Add section 9d to prod-run.sh: installs/refreshes the endpoint service unit from the repo copy, clears port 3001 before restart, restarts via systemd with a nohup fallback — so a full prod-run now brings both services up cleanly without manual intervention. https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4 --- package.json | 2 +- prod-run.sh | 55 ++++++++++++++++++++++++++++++++++++++ runewager-endpoint.service | 8 +++--- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index fca3afb..5e35135 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "dotenv": "^16.6.1", - "express": "^4.21.2", + "express": "4.22.1", "telegraf": "^4.16.3" } } diff --git a/prod-run.sh b/prod-run.sh index c4325d5..025a21e 100755 --- a/prod-run.sh +++ b/prod-run.sh @@ -390,6 +390,61 @@ if is_port_listening "$PORT_PRESTART"; then sleep 1 fi +# --------------------------------------------------------- +# 9d) Install / restart runewager-endpoint (backend.js on port 3001) +ENDPOINT_SERVICE_NAME="runewager-endpoint" +ENDPOINT_SERVICE_SRC="$PROJECT_DIR/runewager-endpoint.service" +ENDPOINT_SERVICE_DEST="/etc/systemd/system/${ENDPOINT_SERVICE_NAME}.service" +ENDPOINT_PORT="$(read_env_value ENDPOINT_PORT || echo 3001)" +ENDPOINT_PORT="${ENDPOINT_PORT:-3001}" +ENDPOINT_LOG_DIR="$LOG_DIR" +ENDPOINT_LOG="$ENDPOINT_LOG_DIR/backend.log" +ENDPOINT_ERR="$ENDPOINT_LOG_DIR/backend-error.log" +touch "$ENDPOINT_LOG" "$ENDPOINT_ERR" 2>/dev/null || true + +if command -v systemctl >/dev/null 2>&1; then + # Install/refresh the service unit from repo copy + if [[ -f "$ENDPOINT_SERVICE_SRC" ]]; then + if ! cmp -s "$ENDPOINT_SERVICE_SRC" "$ENDPOINT_SERVICE_DEST" 2>/dev/null; then + cp "$ENDPOINT_SERVICE_SRC" "$ENDPOINT_SERVICE_DEST" + systemctl daemon-reload + systemctl enable "${ENDPOINT_SERVICE_NAME}.service" 2>/dev/null || true + say "Endpoint service unit installed/refreshed: $ENDPOINT_SERVICE_DEST" + fi + fi + + # Clear stale port-3001 holders before restarting + if is_port_listening "$ENDPOINT_PORT"; then + say "Port $ENDPOINT_PORT in use — freeing before endpoint restart..." + free_port_if_conflicted "$ENDPOINT_PORT" "" || true + sleep 1 + fi + + if systemctl restart "${ENDPOINT_SERVICE_NAME}.service" 2>/dev/null; then + say "runewager-endpoint restarted via systemd" + else + warn "systemctl restart runewager-endpoint failed — falling back to nohup" + pkill -f "node .*backend\.js" 2>/dev/null || true + sleep 1 + nohup node "$PROJECT_DIR/backend.js" \ + >> "$ENDPOINT_LOG" 2>> "$ENDPOINT_ERR" < /dev/null & + disown || true + say "runewager-endpoint started via nohup fallback" + fi +else + # No systemd — direct nohup + pkill -f "node .*backend\.js" 2>/dev/null || true + sleep 1 + if is_port_listening "$ENDPOINT_PORT"; then + free_port_if_conflicted "$ENDPOINT_PORT" "" || true + sleep 1 + fi + nohup node "$PROJECT_DIR/backend.js" \ + >> "$ENDPOINT_LOG" 2>> "$ENDPOINT_ERR" < /dev/null & + disown || true + say "runewager-endpoint started via nohup (no systemd)" +fi + # --------------------------------------------------------- # 10) Safe restart if [[ -n "$PID" ]]; then diff --git a/runewager-endpoint.service b/runewager-endpoint.service index 5e13753..0504337 100644 --- a/runewager-endpoint.service +++ b/runewager-endpoint.service @@ -21,6 +21,8 @@ Wants=network-online.target [Service] Type=simple WorkingDirectory=/var/www/html/Runewager +# Clear any stale process holding port 3001 before each start to prevent EADDRINUSE crash-loops. +ExecStartPre=/bin/sh -c 'port=${ENDPOINT_PORT:-3001}; pid=$(lsof -t -i:"$port" 2>/dev/null || fuser "$port"/tcp 2>/dev/null || true); [ -n "$pid" ] && echo "[endpoint-prestart] killing stale PID $pid on port $port" && kill -9 $pid && sleep 1 || true' ExecStart=/usr/bin/node /var/www/html/Runewager/backend.js User=root Group=root @@ -28,9 +30,9 @@ EnvironmentFile=/var/www/html/Runewager/.env Environment=NODE_ENV=production Restart=always -RestartSec=5 -StartLimitIntervalSec=60 -StartLimitBurst=5 +RestartSec=10 +StartLimitIntervalSec=120 +StartLimitBurst=4 KillSignal=SIGTERM TimeoutStopSec=15 From 66322f4b3a27f044849e4e3be55ed42e108c21dc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Mar 2026 21:17:54 +0000 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20add=20gcz=5Fcpu=5Fguard.sh=20?= =?UTF-8?q?=E2=80=94=20passive=20GCZ=20stack=20CPU=20monitor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Monitoring-only companion to rw_cpu_guard.sh. Generates timestamped Markdown reports under /root/CPU_LOGS for /var/www/html/gcz and /var/www/html/Runewager stacks, rotates to keep last 10 reports, and symlinks the latest as CPU_report.md. Never kills, renice-s, or restarts any process — purely observational. Coexists safely with rw_cpu_guard.sh (separate log path, no shared state). Features: - System-wide CPU summary via top - High-CPU process table (>85% threshold) - GCZ stack process listing with ⚠️ flag on hot processes - Best-effort dpkg package attribution for flagged pids - --status, --install-cron, --dry-run modes - Self-exclusion and kernel-thread exclusion from reports https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4 --- scripts/gcz_cpu_guard.sh | 247 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100755 scripts/gcz_cpu_guard.sh diff --git a/scripts/gcz_cpu_guard.sh b/scripts/gcz_cpu_guard.sh new file mode 100755 index 0000000..1b98b8e --- /dev/null +++ b/scripts/gcz_cpu_guard.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env bash +# ============================================================================= +# gcz_cpu_guard.sh — GCZ Stack CPU Monitor (reporting only) +# +# Purpose: +# Passive CPU monitoring for GCZ stacks (/var/www/html/gcz and +# /var/www/html/Runewager). Generates a timestamped Markdown report, +# symlinks it as CPU_report.md, and rotates older reports. +# +# This script NEVER kills, renice-s, or restarts any process. +# It coexists safely alongside rw_cpu_guard.sh (which handles active +# throttling of non-Runewager hogs). Both guards operate independently +# on separate log paths with no shared state. +# +# Usage: +# ./gcz_cpu_guard.sh # generate one report and exit +# ./gcz_cpu_guard.sh --status # print log dir summary and exit +# +# Cron (every 15 minutes — install via: gcz_cpu_guard.sh --install-cron): +# */15 * * * * /usr/local/bin/gcz_cpu_guard.sh >> /root/CPU_LOGS/cron.log 2>&1 +# +# Output: +# /root/CPU_LOGS/CPU_.md — timestamped report +# /root/CPU_LOGS/CPU_report.md — symlink to latest report +# /root/CPU_LOGS/cron.log — cron stdout/stderr +# Last 10 reports are kept; older ones are deleted automatically. +# ============================================================================= + +set -uo pipefail + +# ─── Config ────────────────────────────────────────────────────────────────── + +readonly LOG_DIR="/root/CPU_LOGS" +readonly REPORT_LINK="${LOG_DIR}/CPU_report.md" +readonly KEEP_REPORTS=10 +readonly CPU_LIMIT=85 # % threshold for flagging a GCZ-stack process +readonly GCZ_PATHS=( + "/var/www/html/gcz" + "/var/www/html/Runewager" +) + +# Processes to exclude from high-CPU listings (monitoring infra + kernel) +readonly EXCLUDE_PATTERNS=( + "gcz_cpu_guard" + "rw_cpu_guard" + "CPU.sh" + "[init]" + "[systemd]" + "systemd" + "[kthreadd]" + "[ksoftirqd" + "[kworker" + "[rcu_" + "[migration" + "[watchdog" + "[kdevtmpfs" + "[netns" +) + +# ─── Helpers ───────────────────────────────────────────────────────────────── + +ts() { date '+%Y-%m-%d %H:%M:%S'; } +log() { printf '[gcz_cpu_guard] [%s] %s\n' "$(ts)" "$*"; } + +is_excluded() { + local cmd="$1" + local pat + for pat in "${EXCLUDE_PATTERNS[@]}"; do + [[ "$cmd" == *"$pat"* ]] && return 0 + done + return 1 +} + +# ─── Status mode ───────────────────────────────────────────────────────────── + +if [[ "${1:-}" == "--status" ]]; then + echo "=== GCZ CPU Guard — Log Directory Status ===" + echo "Log dir : $LOG_DIR" + echo "Reports : $(ls -1 "${LOG_DIR}"/CPU_*.md 2>/dev/null | wc -l) file(s)" + ls -lht "${LOG_DIR}"/CPU_*.md 2>/dev/null | head -5 || echo " (none found)" + echo "Symlink : $(readlink -f "$REPORT_LINK" 2>/dev/null || echo '(not set)')" + exit 0 +fi + +# ─── Install-cron mode ─────────────────────────────────────────────────────── + +if [[ "${1:-}" == "--install-cron" ]]; then + SELF="$(readlink -f "${BASH_SOURCE[0]}")" + CRON_MARK="gcz_cpu_guard_cron" + CRON_LINE="*/15 * * * * ${SELF} >> ${LOG_DIR}/cron.log 2>&1 # ${CRON_MARK}" + current_cron="$(crontab -l 2>/dev/null || true)" + if printf '%s\n' "$current_cron" | grep -Fq "$CRON_MARK"; then + log "Cron already installed — nothing to do." + else + { printf '%s\n' "$current_cron"; printf '%s\n' "$CRON_LINE"; } | crontab - + log "Cron installed: $CRON_LINE" + fi + exit 0 +fi + +# ─── Ensure log directory ──────────────────────────────────────────────────── + +mkdir -p "$LOG_DIR" + +# Remove any sub-directories that may have accumulated (keep it flat) +find "$LOG_DIR" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} + 2>/dev/null || true + +# ─── Build report ──────────────────────────────────────────────────────────── + +STAMP="$(date '+%Y%m%d_%H%M%S')" +REPORT="${LOG_DIR}/CPU_${STAMP}.md" + +{ +printf '# GCZ CPU Report — %s\n\n' "$(ts)" + +# ── 1. System-wide CPU summary (via top, one iteration) ────────────────────── +printf '## System CPU Summary\n\n' +printf '```\n' +top -b -n 1 2>/dev/null \ + | head -5 \ + | grep -E "^(%Cpu|Cpu|top|Tasks|MiB|KiB)" || top -b -n 1 2>/dev/null | head -5 +printf '```\n\n' + +# ── 2. All processes above CPU_LIMIT (system-wide, excluding infra) ─────────── +printf '## High-CPU Processes (> %d%% system-wide)\n\n' "$CPU_LIMIT" +printf '| PID | CPU%% | Process |\n' +printf '|-----|------|---------|\n' + +high_cpu_found=0 +while IFS= read -r line; do + pid=$(printf '%s' "$line" | awk '{print $1}') + cpu=$(printf '%s' "$line" | awk '{print $2}') + cmd=$(printf '%s' "$line" | awk '{$1=$2=""; sub(/^ +/,""); print}') + + # Skip excluded patterns + is_excluded "$cmd" && continue + + # Parse CPU as integer for comparison (strip decimals) + cpu_int="${cpu%%.*}" + [[ -z "$cpu_int" || "$cpu_int" -lt "$CPU_LIMIT" ]] 2>/dev/null && continue + + printf '| %s | %s | `%s` |\n' "$pid" "$cpu" "${cmd:0:80}" + high_cpu_found=1 +done < <(ps -eo pid,pcpu,args --sort=-pcpu --no-headers 2>/dev/null | head -30) + +[[ "$high_cpu_found" -eq 0 ]] && printf '_No processes exceed %d%% CPU._\n' "$CPU_LIMIT" +printf '\n' + +# ── 3. GCZ-stack processes (all, regardless of CPU) ────────────────────────── +printf '## GCZ Stack Processes\n\n' +printf '| PID | CPU%% | Stack | Process |\n' +printf '|-----|------|-------|---------|\n' + +gcz_found=0 +while IFS= read -r line; do + pid=$(printf '%s' "$line" | awk '{print $1}') + cpu=$(printf '%s' "$line" | awk '{print $2}') + cmd=$(printf '%s' "$line" | awk '{$1=$2=""; sub(/^ +/,""); print}') + + is_excluded "$cmd" && continue + + for stack_path in "${GCZ_PATHS[@]}"; do + if [[ "$cmd" == *"$stack_path"* ]]; then + stack_label="$(basename "$stack_path")" + cpu_int="${cpu%%.*}" + + # Flag visually if over threshold + flag="" + { [[ -n "$cpu_int" ]] && [[ "$cpu_int" -ge "$CPU_LIMIT" ]]; } 2>/dev/null && flag=" ⚠️" + + printf '| %s | %s%s | %s | `%s` |\n' \ + "$pid" "$cpu" "$flag" "$stack_label" "${cmd:0:80}" + gcz_found=1 + break + fi + done +done < <(ps -eo pid,pcpu,args --sort=-pcpu --no-headers 2>/dev/null) + +[[ "$gcz_found" -eq 0 ]] && printf '_No GCZ stack processes found._\n' +printf '\n' + +# ── 4. GCZ-stack processes ABOVE threshold ──────────────────────────────────── +printf '## GCZ Stack — Over %d%% CPU Threshold\n\n' "$CPU_LIMIT" +printf '| PID | CPU%% | Stack | Process | Package |\n' +printf '|-----|------|-------|---------|--------|\n' + +gcz_hot_found=0 +while IFS= read -r line; do + pid=$(printf '%s' "$line" | awk '{print $1}') + cpu=$(printf '%s' "$line" | awk '{print $2}') + cmd=$(printf '%s' "$line" | awk '{$1=$2=""; sub(/^ +/,""); print}') + + is_excluded "$cmd" && continue + + cpu_int="${cpu%%.*}" + { [[ -z "$cpu_int" ]] || [[ "$cpu_int" -lt "$CPU_LIMIT" ]]; } 2>/dev/null && continue + + for stack_path in "${GCZ_PATHS[@]}"; do + if [[ "$cmd" == *"$stack_path"* ]]; then + stack_label="$(basename "$stack_path")" + + # Best-effort package identification + pkg="unknown" + if command -v dpkg >/dev/null 2>&1; then + bin_path="$(ls -l /proc/"$pid"/exe 2>/dev/null | awk '{print $NF}')" || true + [[ -n "$bin_path" ]] && pkg="$(dpkg -S "$bin_path" 2>/dev/null | cut -d: -f1 || echo 'unknown')" + fi + + printf '| %s | %s ⚠️ | %s | `%s` | %s |\n' \ + "$pid" "$cpu" "$stack_label" "${cmd:0:60}" "$pkg" + gcz_hot_found=1 + break + fi + done +done < <(ps -eo pid,pcpu,args --sort=-pcpu --no-headers 2>/dev/null) + +[[ "$gcz_hot_found" -eq 0 ]] && printf '_No GCZ stack processes exceed %d%% CPU. ✅_\n' "$CPU_LIMIT" +printf '\n' + +# ── 5. Load averages ────────────────────────────────────────────────────────── +printf '## Load Averages\n\n' +printf '```\n%s\n```\n\n' "$(uptime 2>/dev/null)" + +printf '_Report generated by gcz\\_cpu\\_guard.sh — monitoring only, no process actions taken._\n' + +} > "$REPORT" + +log "Report written: $REPORT" + +# ─── Symlink latest ────────────────────────────────────────────────────────── + +ln -sf "$REPORT" "$REPORT_LINK" +log "Symlink updated: $REPORT_LINK -> $REPORT" + +# ─── Rotate: keep only last KEEP_REPORTS reports ───────────────────────────── + +mapfile -t old_reports < <( + ls -1t "${LOG_DIR}"/CPU_*.md 2>/dev/null | tail -n +"$((KEEP_REPORTS + 1))" +) +if [[ "${#old_reports[@]}" -gt 0 ]]; then + for f in "${old_reports[@]}"; do + rm -f "$f" + log "Rotated old report: $f" + done +fi + +log "Done. Reports retained: $(ls -1 "${LOG_DIR}"/CPU_*.md 2>/dev/null | wc -l)/${KEEP_REPORTS}" From 7250d44e03872ba2f76b39a20bae916542d05daf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Mar 2026 21:18:46 +0000 Subject: [PATCH 04/12] fix: scope pkill pattern to PROJECT_DIR to avoid killing unrelated node processes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pkill -f "node .*backend.js"` was too broad — on a shared host it could kill any Node process whose cmdline contains "backend.js". Narrowed both pkill calls (systemd fallback + no-systemd path) to `node .*/backend.js` so only the Runewager endpoint process is targeted. https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4 --- prod-run.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prod-run.sh b/prod-run.sh index 025a21e..6a06079 100755 --- a/prod-run.sh +++ b/prod-run.sh @@ -424,7 +424,7 @@ if command -v systemctl >/dev/null 2>&1; then say "runewager-endpoint restarted via systemd" else warn "systemctl restart runewager-endpoint failed — falling back to nohup" - pkill -f "node .*backend\.js" 2>/dev/null || true + pkill -f "node .*${PROJECT_DIR}/backend\.js" 2>/dev/null || true sleep 1 nohup node "$PROJECT_DIR/backend.js" \ >> "$ENDPOINT_LOG" 2>> "$ENDPOINT_ERR" < /dev/null & @@ -433,7 +433,7 @@ if command -v systemctl >/dev/null 2>&1; then fi else # No systemd — direct nohup - pkill -f "node .*backend\.js" 2>/dev/null || true + pkill -f "node .*${PROJECT_DIR}/backend\.js" 2>/dev/null || true sleep 1 if is_port_listening "$ENDPOINT_PORT"; then free_port_if_conflicted "$ENDPOINT_PORT" "" || true From 18f959fc2e03685ae23d452f92b49281314591b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Mar 2026 21:19:53 +0000 Subject: [PATCH 05/12] chore: remove gcz_cpu_guard.sh from repo; whitelist it in rw_cpu_guard gcz_cpu_guard.sh lives on the VPS directly, not in this repo. Added "gcz_cpu_guard" to WHITELIST_PATTERNS in rw_cpu_guard.sh so the Runewager CPU governor never renice/kills/stops the GCZ monitoring script regardless of its CPU usage. https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4 --- scripts/gcz_cpu_guard.sh | 247 --------------------------------------- scripts/rw_cpu_guard.sh | 3 +- 2 files changed, 2 insertions(+), 248 deletions(-) delete mode 100755 scripts/gcz_cpu_guard.sh diff --git a/scripts/gcz_cpu_guard.sh b/scripts/gcz_cpu_guard.sh deleted file mode 100755 index 1b98b8e..0000000 --- a/scripts/gcz_cpu_guard.sh +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================= -# gcz_cpu_guard.sh — GCZ Stack CPU Monitor (reporting only) -# -# Purpose: -# Passive CPU monitoring for GCZ stacks (/var/www/html/gcz and -# /var/www/html/Runewager). Generates a timestamped Markdown report, -# symlinks it as CPU_report.md, and rotates older reports. -# -# This script NEVER kills, renice-s, or restarts any process. -# It coexists safely alongside rw_cpu_guard.sh (which handles active -# throttling of non-Runewager hogs). Both guards operate independently -# on separate log paths with no shared state. -# -# Usage: -# ./gcz_cpu_guard.sh # generate one report and exit -# ./gcz_cpu_guard.sh --status # print log dir summary and exit -# -# Cron (every 15 minutes — install via: gcz_cpu_guard.sh --install-cron): -# */15 * * * * /usr/local/bin/gcz_cpu_guard.sh >> /root/CPU_LOGS/cron.log 2>&1 -# -# Output: -# /root/CPU_LOGS/CPU_.md — timestamped report -# /root/CPU_LOGS/CPU_report.md — symlink to latest report -# /root/CPU_LOGS/cron.log — cron stdout/stderr -# Last 10 reports are kept; older ones are deleted automatically. -# ============================================================================= - -set -uo pipefail - -# ─── Config ────────────────────────────────────────────────────────────────── - -readonly LOG_DIR="/root/CPU_LOGS" -readonly REPORT_LINK="${LOG_DIR}/CPU_report.md" -readonly KEEP_REPORTS=10 -readonly CPU_LIMIT=85 # % threshold for flagging a GCZ-stack process -readonly GCZ_PATHS=( - "/var/www/html/gcz" - "/var/www/html/Runewager" -) - -# Processes to exclude from high-CPU listings (monitoring infra + kernel) -readonly EXCLUDE_PATTERNS=( - "gcz_cpu_guard" - "rw_cpu_guard" - "CPU.sh" - "[init]" - "[systemd]" - "systemd" - "[kthreadd]" - "[ksoftirqd" - "[kworker" - "[rcu_" - "[migration" - "[watchdog" - "[kdevtmpfs" - "[netns" -) - -# ─── Helpers ───────────────────────────────────────────────────────────────── - -ts() { date '+%Y-%m-%d %H:%M:%S'; } -log() { printf '[gcz_cpu_guard] [%s] %s\n' "$(ts)" "$*"; } - -is_excluded() { - local cmd="$1" - local pat - for pat in "${EXCLUDE_PATTERNS[@]}"; do - [[ "$cmd" == *"$pat"* ]] && return 0 - done - return 1 -} - -# ─── Status mode ───────────────────────────────────────────────────────────── - -if [[ "${1:-}" == "--status" ]]; then - echo "=== GCZ CPU Guard — Log Directory Status ===" - echo "Log dir : $LOG_DIR" - echo "Reports : $(ls -1 "${LOG_DIR}"/CPU_*.md 2>/dev/null | wc -l) file(s)" - ls -lht "${LOG_DIR}"/CPU_*.md 2>/dev/null | head -5 || echo " (none found)" - echo "Symlink : $(readlink -f "$REPORT_LINK" 2>/dev/null || echo '(not set)')" - exit 0 -fi - -# ─── Install-cron mode ─────────────────────────────────────────────────────── - -if [[ "${1:-}" == "--install-cron" ]]; then - SELF="$(readlink -f "${BASH_SOURCE[0]}")" - CRON_MARK="gcz_cpu_guard_cron" - CRON_LINE="*/15 * * * * ${SELF} >> ${LOG_DIR}/cron.log 2>&1 # ${CRON_MARK}" - current_cron="$(crontab -l 2>/dev/null || true)" - if printf '%s\n' "$current_cron" | grep -Fq "$CRON_MARK"; then - log "Cron already installed — nothing to do." - else - { printf '%s\n' "$current_cron"; printf '%s\n' "$CRON_LINE"; } | crontab - - log "Cron installed: $CRON_LINE" - fi - exit 0 -fi - -# ─── Ensure log directory ──────────────────────────────────────────────────── - -mkdir -p "$LOG_DIR" - -# Remove any sub-directories that may have accumulated (keep it flat) -find "$LOG_DIR" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} + 2>/dev/null || true - -# ─── Build report ──────────────────────────────────────────────────────────── - -STAMP="$(date '+%Y%m%d_%H%M%S')" -REPORT="${LOG_DIR}/CPU_${STAMP}.md" - -{ -printf '# GCZ CPU Report — %s\n\n' "$(ts)" - -# ── 1. System-wide CPU summary (via top, one iteration) ────────────────────── -printf '## System CPU Summary\n\n' -printf '```\n' -top -b -n 1 2>/dev/null \ - | head -5 \ - | grep -E "^(%Cpu|Cpu|top|Tasks|MiB|KiB)" || top -b -n 1 2>/dev/null | head -5 -printf '```\n\n' - -# ── 2. All processes above CPU_LIMIT (system-wide, excluding infra) ─────────── -printf '## High-CPU Processes (> %d%% system-wide)\n\n' "$CPU_LIMIT" -printf '| PID | CPU%% | Process |\n' -printf '|-----|------|---------|\n' - -high_cpu_found=0 -while IFS= read -r line; do - pid=$(printf '%s' "$line" | awk '{print $1}') - cpu=$(printf '%s' "$line" | awk '{print $2}') - cmd=$(printf '%s' "$line" | awk '{$1=$2=""; sub(/^ +/,""); print}') - - # Skip excluded patterns - is_excluded "$cmd" && continue - - # Parse CPU as integer for comparison (strip decimals) - cpu_int="${cpu%%.*}" - [[ -z "$cpu_int" || "$cpu_int" -lt "$CPU_LIMIT" ]] 2>/dev/null && continue - - printf '| %s | %s | `%s` |\n' "$pid" "$cpu" "${cmd:0:80}" - high_cpu_found=1 -done < <(ps -eo pid,pcpu,args --sort=-pcpu --no-headers 2>/dev/null | head -30) - -[[ "$high_cpu_found" -eq 0 ]] && printf '_No processes exceed %d%% CPU._\n' "$CPU_LIMIT" -printf '\n' - -# ── 3. GCZ-stack processes (all, regardless of CPU) ────────────────────────── -printf '## GCZ Stack Processes\n\n' -printf '| PID | CPU%% | Stack | Process |\n' -printf '|-----|------|-------|---------|\n' - -gcz_found=0 -while IFS= read -r line; do - pid=$(printf '%s' "$line" | awk '{print $1}') - cpu=$(printf '%s' "$line" | awk '{print $2}') - cmd=$(printf '%s' "$line" | awk '{$1=$2=""; sub(/^ +/,""); print}') - - is_excluded "$cmd" && continue - - for stack_path in "${GCZ_PATHS[@]}"; do - if [[ "$cmd" == *"$stack_path"* ]]; then - stack_label="$(basename "$stack_path")" - cpu_int="${cpu%%.*}" - - # Flag visually if over threshold - flag="" - { [[ -n "$cpu_int" ]] && [[ "$cpu_int" -ge "$CPU_LIMIT" ]]; } 2>/dev/null && flag=" ⚠️" - - printf '| %s | %s%s | %s | `%s` |\n' \ - "$pid" "$cpu" "$flag" "$stack_label" "${cmd:0:80}" - gcz_found=1 - break - fi - done -done < <(ps -eo pid,pcpu,args --sort=-pcpu --no-headers 2>/dev/null) - -[[ "$gcz_found" -eq 0 ]] && printf '_No GCZ stack processes found._\n' -printf '\n' - -# ── 4. GCZ-stack processes ABOVE threshold ──────────────────────────────────── -printf '## GCZ Stack — Over %d%% CPU Threshold\n\n' "$CPU_LIMIT" -printf '| PID | CPU%% | Stack | Process | Package |\n' -printf '|-----|------|-------|---------|--------|\n' - -gcz_hot_found=0 -while IFS= read -r line; do - pid=$(printf '%s' "$line" | awk '{print $1}') - cpu=$(printf '%s' "$line" | awk '{print $2}') - cmd=$(printf '%s' "$line" | awk '{$1=$2=""; sub(/^ +/,""); print}') - - is_excluded "$cmd" && continue - - cpu_int="${cpu%%.*}" - { [[ -z "$cpu_int" ]] || [[ "$cpu_int" -lt "$CPU_LIMIT" ]]; } 2>/dev/null && continue - - for stack_path in "${GCZ_PATHS[@]}"; do - if [[ "$cmd" == *"$stack_path"* ]]; then - stack_label="$(basename "$stack_path")" - - # Best-effort package identification - pkg="unknown" - if command -v dpkg >/dev/null 2>&1; then - bin_path="$(ls -l /proc/"$pid"/exe 2>/dev/null | awk '{print $NF}')" || true - [[ -n "$bin_path" ]] && pkg="$(dpkg -S "$bin_path" 2>/dev/null | cut -d: -f1 || echo 'unknown')" - fi - - printf '| %s | %s ⚠️ | %s | `%s` | %s |\n' \ - "$pid" "$cpu" "$stack_label" "${cmd:0:60}" "$pkg" - gcz_hot_found=1 - break - fi - done -done < <(ps -eo pid,pcpu,args --sort=-pcpu --no-headers 2>/dev/null) - -[[ "$gcz_hot_found" -eq 0 ]] && printf '_No GCZ stack processes exceed %d%% CPU. ✅_\n' "$CPU_LIMIT" -printf '\n' - -# ── 5. Load averages ────────────────────────────────────────────────────────── -printf '## Load Averages\n\n' -printf '```\n%s\n```\n\n' "$(uptime 2>/dev/null)" - -printf '_Report generated by gcz\\_cpu\\_guard.sh — monitoring only, no process actions taken._\n' - -} > "$REPORT" - -log "Report written: $REPORT" - -# ─── Symlink latest ────────────────────────────────────────────────────────── - -ln -sf "$REPORT" "$REPORT_LINK" -log "Symlink updated: $REPORT_LINK -> $REPORT" - -# ─── Rotate: keep only last KEEP_REPORTS reports ───────────────────────────── - -mapfile -t old_reports < <( - ls -1t "${LOG_DIR}"/CPU_*.md 2>/dev/null | tail -n +"$((KEEP_REPORTS + 1))" -) -if [[ "${#old_reports[@]}" -gt 0 ]]; then - for f in "${old_reports[@]}"; do - rm -f "$f" - log "Rotated old report: $f" - done -fi - -log "Done. Reports retained: $(ls -1 "${LOG_DIR}"/CPU_*.md 2>/dev/null | wc -l)/${KEEP_REPORTS}" diff --git a/scripts/rw_cpu_guard.sh b/scripts/rw_cpu_guard.sh index 687f5f3..bc9c63a 100755 --- a/scripts/rw_cpu_guard.sh +++ b/scripts/rw_cpu_guard.sh @@ -104,8 +104,9 @@ DRY_RUN="${DRY_RUN:-0}" # Any process whose *parent* matches a pattern is also protected. # WHITELIST_PATTERNS=( - # Guard itself + # Guard itself + sibling monitoring scripts — never touch these "rw_cpu_guard" + "gcz_cpu_guard" # Runewager bot — all known launch forms "runewager" From 5e9d1fa884b6890fc16bdb357972f9e28ea209ec Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Mar 2026 21:24:26 +0000 Subject: [PATCH 06/12] =?UTF-8?q?fix(security):=20drop=20root=20privileges?= =?UTF-8?q?=20=E2=80=94=20run=20services=20as=20runewager=20user?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both static service files were hardcoding User=root/Group=root, meaning any RCE in index.js or backend.js yielded full system control and made ProtectSystem/NoNewPrivileges sandboxing meaningless. Changes: - runewager.service: User/Group → runewager (prod-run.sh already creates this system user and chowns logs/ + data/ to it) - runewager-endpoint.service: User/Group → runewager; ExecStartPre prefixed with '+' so systemd runs only that port-clear step as root (needed to kill stale root-owned processes), while backend.js itself runs unprivileged https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4 --- runewager-endpoint.service | 7 ++++--- runewager.service | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/runewager-endpoint.service b/runewager-endpoint.service index 0504337..47ca82d 100644 --- a/runewager-endpoint.service +++ b/runewager-endpoint.service @@ -22,10 +22,11 @@ Wants=network-online.target Type=simple WorkingDirectory=/var/www/html/Runewager # Clear any stale process holding port 3001 before each start to prevent EADDRINUSE crash-loops. -ExecStartPre=/bin/sh -c 'port=${ENDPOINT_PORT:-3001}; pid=$(lsof -t -i:"$port" 2>/dev/null || fuser "$port"/tcp 2>/dev/null || true); [ -n "$pid" ] && echo "[endpoint-prestart] killing stale PID $pid on port $port" && kill -9 $pid && sleep 1 || true' +# The '+' prefix runs this step as root so it can kill processes owned by any user. +ExecStartPre=+/bin/sh -c 'port=${ENDPOINT_PORT:-3001}; pid=$(lsof -t -i:"$port" 2>/dev/null || fuser "$port"/tcp 2>/dev/null || true); [ -n "$pid" ] && echo "[endpoint-prestart] killing stale PID $pid on port $port" && kill -9 $pid && sleep 1 || true' ExecStart=/usr/bin/node /var/www/html/Runewager/backend.js -User=root -Group=root +User=runewager +Group=runewager EnvironmentFile=/var/www/html/Runewager/.env Environment=NODE_ENV=production diff --git a/runewager.service b/runewager.service index 7b79f78..ab0cc19 100644 --- a/runewager.service +++ b/runewager.service @@ -25,8 +25,8 @@ Wants=network-online.target Type=simple WorkingDirectory=/var/www/html/Runewager ExecStart=/usr/bin/node /var/www/html/Runewager/index.js -User=root -Group=root +User=runewager +Group=runewager EnvironmentFile=/var/www/html/Runewager/.env Environment=NODE_ENV=production From 613da2fce709b841952392a3f349fe0b9cf1c53f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Mar 2026 21:29:51 +0000 Subject: [PATCH 07/12] fix: address code review issues in backend.js and prod-run.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit backend.js — _write double-logging: Filter to writable streams first; fall back to a single console.log only when none are available. Previously logger.error() with two null streams would print the same line twice to stdout. backend.js — rawBody guard on webhook: Treat missing/non-Buffer req.rawBody as a 500 (middleware misconfiguration) rather than silently falling back to Buffer.alloc(0), which was indistinguishable from a genuine signature mismatch. prod-run.sh — port race in nohup fallback: After a failed systemctl restart, re-check and free ENDPOINT_PORT before launching the nohup fallback. The original cleanup ran before the systemd attempt, leaving a race window where another process could bind the port. Note: log paths are NOT a conflict — prod-run.sh LOG_DIR resolves to $PROJECT_DIR/logs, same as backend.js path.join(__dirname, 'logs'). https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4 --- backend.js | 14 +++++++++++--- prod-run.sh | 6 ++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/backend.js b/backend.js index 1b44f8e..593a35f 100644 --- a/backend.js +++ b/backend.js @@ -57,8 +57,11 @@ const logger = { }, _write(streams, obj) { const line = JSON.stringify(obj) + '\n'; - for (const s of streams) { - if (s) s.write(line); else console.log(line.trimEnd()); + const writable = streams.filter(Boolean); + if (writable.length > 0) { + for (const s of writable) s.write(line); + } else { + console.log(line.trimEnd()); } }, info(eventType, msg, extra = {}) { @@ -204,7 +207,12 @@ app.post('/autofix/webhook', async (req, res) => { const requestId = req.id; try { const sig = req.headers['x-autofix-signature'] ?? req.headers['x-hub-signature-256'] ?? ''; - const rawBody = req.rawBody ?? Buffer.alloc(0); + const rawBody = req.rawBody; + + if (!rawBody || !Buffer.isBuffer(rawBody)) { + logger.error('autofix.webhook', 'Missing rawBody — middleware misconfiguration', { requestId }); + return res.status(500).json({ ok: false, error: 'Internal server error', requestId }); + } if (!AUTOFIX_SECRET || !verifySignature(rawBody, sig)) { const reason = !AUTOFIX_SECRET ? 'AUTOFIX_SECRET not configured' : 'Signature mismatch'; diff --git a/prod-run.sh b/prod-run.sh index 6a06079..1ca91ff 100755 --- a/prod-run.sh +++ b/prod-run.sh @@ -426,6 +426,12 @@ if command -v systemctl >/dev/null 2>&1; then warn "systemctl restart runewager-endpoint failed — falling back to nohup" pkill -f "node .*${PROJECT_DIR}/backend\.js" 2>/dev/null || true sleep 1 + # Re-check port: another process may have bound it during the failed systemd restart window + if is_port_listening "$ENDPOINT_PORT"; then + say "Port $ENDPOINT_PORT still in use — freeing before nohup fallback..." + free_port_if_conflicted "$ENDPOINT_PORT" "" || true + sleep 1 + fi nohup node "$PROJECT_DIR/backend.js" \ >> "$ENDPOINT_LOG" 2>> "$ENDPOINT_ERR" < /dev/null & disown || true From ab189aa1ee10146252575515b1f878733956a084 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Mar 2026 21:39:43 +0000 Subject: [PATCH 08/12] fix: address remaining code review items (kill escalation, stream guards, log fallback) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit runewager-endpoint.service — aggressive prestart kill: Replace direct SIGKILL with SIGTERM → 3s wait → SIGKILL escalation. Gives the port holder a chance to shut down gracefully before forcing. backend.js — stream write guards: Filter streams by !destroyed && writable before writing, and wrap each write in try/catch so a mid-flight stream error is logged to stderr rather than crashing or silently corrupting output. prod-run.sh — silent log creation: Replace `touch ... 2>/dev/null || true` with an explicit failure check; when the log directory is not writable, warn and fall back to /tmp so the nohup backend process always has somewhere to log. https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4 --- backend.js | 6 ++++-- prod-run.sh | 7 ++++++- runewager-endpoint.service | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/backend.js b/backend.js index 593a35f..293053b 100644 --- a/backend.js +++ b/backend.js @@ -57,9 +57,11 @@ const logger = { }, _write(streams, obj) { const line = JSON.stringify(obj) + '\n'; - const writable = streams.filter(Boolean); + const writable = streams.filter((s) => s && !s.destroyed && s.writable); if (writable.length > 0) { - for (const s of writable) s.write(line); + for (const s of writable) { + try { s.write(line); } catch (e) { console.error(`[backend] stream write error: ${e.message}`); } + } } else { console.log(line.trimEnd()); } diff --git a/prod-run.sh b/prod-run.sh index 1ca91ff..78e5124 100755 --- a/prod-run.sh +++ b/prod-run.sh @@ -400,7 +400,12 @@ ENDPOINT_PORT="${ENDPOINT_PORT:-3001}" ENDPOINT_LOG_DIR="$LOG_DIR" ENDPOINT_LOG="$ENDPOINT_LOG_DIR/backend.log" ENDPOINT_ERR="$ENDPOINT_LOG_DIR/backend-error.log" -touch "$ENDPOINT_LOG" "$ENDPOINT_ERR" 2>/dev/null || true +if ! touch "$ENDPOINT_LOG" "$ENDPOINT_ERR" 2>/dev/null; then + warn "Cannot write to $ENDPOINT_LOG_DIR — falling back to /tmp for endpoint logs" + ENDPOINT_LOG="/tmp/runewager-backend.log" + ENDPOINT_ERR="/tmp/runewager-backend-error.log" + touch "$ENDPOINT_LOG" "$ENDPOINT_ERR" || true +fi if command -v systemctl >/dev/null 2>&1; then # Install/refresh the service unit from repo copy diff --git a/runewager-endpoint.service b/runewager-endpoint.service index 469ed2d..fd29a68 100644 --- a/runewager-endpoint.service +++ b/runewager-endpoint.service @@ -23,7 +23,8 @@ Type=simple WorkingDirectory=/var/www/html/Runewager # Clear any stale process holding port 3001 before each start to prevent EADDRINUSE crash-loops. # The '+' prefix runs this step as root so it can kill processes owned by any user. -ExecStartPre=+/bin/sh -c 'port=${ENDPOINT_PORT:-3001}; pid=$(lsof -t -i:"$port" 2>/dev/null || fuser "$port"/tcp 2>/dev/null || true); [ -n "$pid" ] && echo "[endpoint-prestart] killing stale PID $pid on port $port" && kill -9 $pid && sleep 1 || true' +# Graceful escalation: SIGTERM → 3s wait → SIGKILL (only if still alive). +ExecStartPre=+/bin/sh -c 'port=${ENDPOINT_PORT:-3001}; pid=$(lsof -t -i:"$port" 2>/dev/null || fuser "$port"/tcp 2>/dev/null || true); [ -z "$pid" ] && exit 0; echo "[endpoint-prestart] SIGTERM PID $pid on port $port"; kill -TERM "$pid" 2>/dev/null || true; sleep 3; kill -0 "$pid" 2>/dev/null && { echo "[endpoint-prestart] escalating to SIGKILL"; kill -KILL "$pid" 2>/dev/null || true; sleep 1; } || true' ExecStart=/usr/bin/node /var/www/html/Runewager/backend.js User=root Group=root From 0e300b62427832e30789fcda0416ea40cec1dd0b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Mar 2026 21:43:08 +0000 Subject: [PATCH 09/12] fix: escape PROJECT_DIR in pkill pattern; return 400 on missing rawBody MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit prod-run.sh — pkill regex safety: Escape PROJECT_DIR with sed before interpolating into the pkill -f pattern. Spaces or regex metacharacters in the path (e.g. dots in /var/www) can cause unexpected matches or silent pattern failure. backend.js — rawBody missing response code: Change 500 → 400 when req.rawBody is absent. 500 caused webhook senders to treat the rejection as transient and retry; 400 signals a permanent bad-request so retries stop. The server-side misconfiguration is still logged as an error for visibility. https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4 --- backend.js | 5 +++-- prod-run.sh | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/backend.js b/backend.js index 293053b..7b7c120 100644 --- a/backend.js +++ b/backend.js @@ -212,8 +212,9 @@ app.post('/autofix/webhook', async (req, res) => { const rawBody = req.rawBody; if (!rawBody || !Buffer.isBuffer(rawBody)) { - logger.error('autofix.webhook', 'Missing rawBody — middleware misconfiguration', { requestId }); - return res.status(500).json({ ok: false, error: 'Internal server error', requestId }); + // Log as server-side misconfiguration but return 400 so senders don't retry indefinitely. + logger.error('autofix.webhook', 'Missing rawBody — raw-body middleware did not run', { requestId }); + return res.status(400).json({ ok: false, error: 'Bad request: missing body', requestId }); } if (!AUTOFIX_SECRET || !verifySignature(rawBody, sig)) { diff --git a/prod-run.sh b/prod-run.sh index 78e5124..47c4981 100755 --- a/prod-run.sh +++ b/prod-run.sh @@ -429,7 +429,8 @@ if command -v systemctl >/dev/null 2>&1; then say "runewager-endpoint restarted via systemd" else warn "systemctl restart runewager-endpoint failed — falling back to nohup" - pkill -f "node .*${PROJECT_DIR}/backend\.js" 2>/dev/null || true + _esc_dir="$(printf '%s' "$PROJECT_DIR" | sed 's/[.[\*^$()+?{|]/\\&/g')" + pkill -f "node .*${_esc_dir}/backend\.js" 2>/dev/null || true sleep 1 # Re-check port: another process may have bound it during the failed systemd restart window if is_port_listening "$ENDPOINT_PORT"; then @@ -444,7 +445,8 @@ if command -v systemctl >/dev/null 2>&1; then fi else # No systemd — direct nohup - pkill -f "node .*${PROJECT_DIR}/backend\.js" 2>/dev/null || true + _esc_dir="$(printf '%s' "$PROJECT_DIR" | sed 's/[.[\*^$()+?{|]/\\&/g')" + pkill -f "node .*${_esc_dir}/backend\.js" 2>/dev/null || true sleep 1 if is_port_listening "$ENDPOINT_PORT"; then free_port_if_conflicted "$ENDPOINT_PORT" "" || true From 63bfc3b96f52f0f494a5d85219c74b5eb8c99a83 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Mar 2026 21:43:50 +0000 Subject: [PATCH 10/12] fix: add -t flag to fuser so port-clear prestart gets bare PIDs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fuser without -t outputs "3001/tcp: 1234" — the full string was being passed to kill, causing it to fail silently and leaving the port holder alive (EADDRINUSE crash loop when lsof is unavailable). fuser -t outputs only the bare PID, matching lsof -t behaviour. https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4 --- runewager-endpoint.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runewager-endpoint.service b/runewager-endpoint.service index fd29a68..5d74b4b 100644 --- a/runewager-endpoint.service +++ b/runewager-endpoint.service @@ -24,7 +24,7 @@ WorkingDirectory=/var/www/html/Runewager # Clear any stale process holding port 3001 before each start to prevent EADDRINUSE crash-loops. # The '+' prefix runs this step as root so it can kill processes owned by any user. # Graceful escalation: SIGTERM → 3s wait → SIGKILL (only if still alive). -ExecStartPre=+/bin/sh -c 'port=${ENDPOINT_PORT:-3001}; pid=$(lsof -t -i:"$port" 2>/dev/null || fuser "$port"/tcp 2>/dev/null || true); [ -z "$pid" ] && exit 0; echo "[endpoint-prestart] SIGTERM PID $pid on port $port"; kill -TERM "$pid" 2>/dev/null || true; sleep 3; kill -0 "$pid" 2>/dev/null && { echo "[endpoint-prestart] escalating to SIGKILL"; kill -KILL "$pid" 2>/dev/null || true; sleep 1; } || true' +ExecStartPre=+/bin/sh -c 'port=${ENDPOINT_PORT:-3001}; pid=$(lsof -t -i:"$port" 2>/dev/null || fuser -t "$port"/tcp 2>/dev/null || true); [ -z "$pid" ] && exit 0; echo "[endpoint-prestart] SIGTERM PID $pid on port $port"; kill -TERM "$pid" 2>/dev/null || true; sleep 3; kill -0 "$pid" 2>/dev/null && { echo "[endpoint-prestart] escalating to SIGKILL"; kill -KILL "$pid" 2>/dev/null || true; sleep 1; } || true' ExecStart=/usr/bin/node /var/www/html/Runewager/backend.js User=root Group=root From 4f3dd8af7bdce24d387bfcff51e1d98ad92b5542 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Mar 2026 21:53:08 +0000 Subject: [PATCH 11/12] =?UTF-8?q?fix:=20add=20qa/=20to=20ReadWritePaths=20?= =?UTF-8?q?=E2=80=94=20ProtectSystem=3Dstrict=20blocked=20repo=5Finfo=20wr?= =?UTF-8?q?ite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ProtectSystem=strict makes the entire FS read-only; only explicitly listed paths are writable. qa/context/repo_info.json write on startup was hitting EROFS because qa/ was not in ReadWritePaths. https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4 --- runewager.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runewager.service b/runewager.service index 7b79f78..08a845d 100644 --- a/runewager.service +++ b/runewager.service @@ -43,7 +43,7 @@ TimeoutStopSec=30 # Logging: append mode — survives log rotation without restart StandardOutput=append:/var/www/html/Runewager/logs/runewager.log StandardError=append:/var/www/html/Runewager/logs/runewager-error.log -ReadWritePaths=/var/www/html/Runewager/logs /var/www/html/Runewager/data +ReadWritePaths=/var/www/html/Runewager/logs /var/www/html/Runewager/data /var/www/html/Runewager/qa ProtectSystem=strict ProtectHome=true PrivateTmp=true From 5375bba190b243f8ba63777d8699664f92759c72 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Mar 2026 21:57:50 +0000 Subject: [PATCH 12/12] fix(prod-run): robust _esc_dir helper, deduplicate, silence fallback touch - Add esc_regex_str() helper that escapes all non-alphanumeric/slash chars via sed; replaces fragile [.[\*^+?{|] character class that missed ] and - and mis-ordered chars after ] - Compute _esc_dir once before the systemctl block instead of duplicating the inline expression in both pkill branches - Add 2>/dev/null to fallback touch in /tmp so locked-down envs (chroot, ProtectSystem=strict) don't emit spurious stderr under set -e - Add ${PROJECT_DIR}/qa to ReadWritePaths in ensure_systemd_service (mirrors same fix already applied to runewager.service) https://claude.ai/code/session_01WpEqAiDbpqnBywMjYXJHf4 --- prod-run.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/prod-run.sh b/prod-run.sh index 47c4981..23c268b 100755 --- a/prod-run.sh +++ b/prod-run.sh @@ -33,6 +33,10 @@ say() { printf '[%s] %s\n' "$APP_NAME" "$*"; } warn() { printf '[%s][warn] %s\n' "$APP_NAME" "$*" >&2; } err() { printf '[%s][error] %s\n' "$APP_NAME" "$*" >&2; } +# Escape a string for safe use as a literal substring in an ERE/BRE regex. +# Escapes every character that is not alphanumeric, underscore, or forward-slash. +esc_regex_str() { printf '%s' "$1" | sed 's/[^[:alnum:]_/]/\\&/g'; } + SERVICE_USER="$APP_NAME" SERVICE_GROUP="$APP_NAME" @@ -318,7 +322,7 @@ KillSignal=SIGTERM TimeoutStopSec=20 StandardOutput=append:${MAIN_LOG} StandardError=append:${ERROR_LOG} -ReadWritePaths=${PROJECT_DIR}/logs ${PROJECT_DIR}/data +ReadWritePaths=${PROJECT_DIR}/logs ${PROJECT_DIR}/data ${PROJECT_DIR}/qa ProtectSystem=strict ProtectHome=true PrivateTmp=true @@ -404,9 +408,11 @@ if ! touch "$ENDPOINT_LOG" "$ENDPOINT_ERR" 2>/dev/null; then warn "Cannot write to $ENDPOINT_LOG_DIR — falling back to /tmp for endpoint logs" ENDPOINT_LOG="/tmp/runewager-backend.log" ENDPOINT_ERR="/tmp/runewager-backend-error.log" - touch "$ENDPOINT_LOG" "$ENDPOINT_ERR" || true + touch "$ENDPOINT_LOG" "$ENDPOINT_ERR" 2>/dev/null || true fi +_esc_dir="$(esc_regex_str "$PROJECT_DIR")" + if command -v systemctl >/dev/null 2>&1; then # Install/refresh the service unit from repo copy if [[ -f "$ENDPOINT_SERVICE_SRC" ]]; then @@ -429,7 +435,6 @@ if command -v systemctl >/dev/null 2>&1; then say "runewager-endpoint restarted via systemd" else warn "systemctl restart runewager-endpoint failed — falling back to nohup" - _esc_dir="$(printf '%s' "$PROJECT_DIR" | sed 's/[.[\*^$()+?{|]/\\&/g')" pkill -f "node .*${_esc_dir}/backend\.js" 2>/dev/null || true sleep 1 # Re-check port: another process may have bound it during the failed systemd restart window @@ -445,7 +450,6 @@ if command -v systemctl >/dev/null 2>&1; then fi else # No systemd — direct nohup - _esc_dir="$(printf '%s' "$PROJECT_DIR" | sed 's/[.[\*^$()+?{|]/\\&/g')" pkill -f "node .*${_esc_dir}/backend\.js" 2>/dev/null || true sleep 1 if is_port_listening "$ENDPOINT_PORT"; then