diff --git a/electron/database/init.cjs b/electron/database/init.cjs index bea9b3e..32f957f 100644 --- a/electron/database/init.cjs +++ b/electron/database/init.cjs @@ -154,29 +154,30 @@ function getEncryptionKey() { * SQLCipher databases start with different magic bytes than regular SQLite */ function isDatabaseEncrypted(dbPath) { - if (!fs.existsSync(dbPath)) { - return null; // Database doesn't exist + let fd; + try { + fd = fs.openSync(dbPath, 'r'); + } catch (openErr) { + if (openErr.code === 'ENOENT') return null; + return null; } - + try { - // Read first 16 bytes of the file - const fd = fs.openSync(dbPath, 'r'); const buffer = Buffer.alloc(16); fs.readSync(fd, buffer, 0, 16, 0); fs.closeSync(fd); - + // SQLite3 magic header: "SQLite format 3\0" const sqliteMagic = Buffer.from('SQLite format 3\0'); - - // If file starts with SQLite magic, it's unencrypted + if (buffer.compare(sqliteMagic, 0, 16, 0, 16) === 0) { - return false; // Unencrypted + return false; } - - // Otherwise, assume encrypted (or corrupted) + return true; - } catch (e) { - return null; // Unable to determine + } catch { + try { fs.closeSync(fd); } catch { /* already closed */ } + return null; } } @@ -689,13 +690,13 @@ async function seedDefaultData(defaultOrgId) { console.log(' Account : admin@transtrack.local'); console.log(' Source : ' + passwordSource); if (setupTokenFilePath) { - console.log(' Token : ' + defaultPassword); + console.log(' Token : (see file below)'); console.log(' File : ' + setupTokenFilePath); console.log(' (mode 0o600 on POSIX; ACL inherited on Windows)'); } else if (envPassword) { console.log(' Token : (supplied by env; not echoed)'); } else { - console.log(' Token : ' + defaultPassword); + console.log(' Token : (could not persist to file; set TRANSTRACK_INITIAL_ADMIN_PASSWORD env and restart)'); } console.log(' Must change password on first sign-in: yes'); console.log('================================================================'); diff --git a/electron/ipc/handlers/operations.cjs b/electron/ipc/handlers/operations.cjs index 63cdc46..0e81234 100644 --- a/electron/ipc/handlers/operations.cjs +++ b/electron/ipc/handlers/operations.cjs @@ -288,12 +288,20 @@ function register() { const ext = path.extname(importPath).toLowerCase(); const MAX_IMPORT_SIZE = 50 * 1024 * 1024; // 50 MB - const stat = fs.statSync(importPath); - if (stat.size > MAX_IMPORT_SIZE) { - throw new Error(`File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB). Maximum import size is 50 MB.`); + const fd = fs.openSync(importPath, 'r'); + let raw; + try { + const stat = fs.fstatSync(fd); + if (stat.size > MAX_IMPORT_SIZE) { + fs.closeSync(fd); + throw new Error(`File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB). Maximum import size is 50 MB.`); + } + raw = fs.readFileSync(fd, 'utf8'); + fs.closeSync(fd); + } catch (fdErr) { + try { fs.closeSync(fd); } catch { /* already closed */ } + throw fdErr; } - - const raw = fs.readFileSync(importPath, 'utf8'); let parsed; if (ext === '.json') { diff --git a/electron/services/logger.cjs b/electron/services/logger.cjs index a68d285..a6425c5 100644 --- a/electron/services/logger.cjs +++ b/electron/services/logger.cjs @@ -89,26 +89,45 @@ const REMOTE_LOG_LEVELS = new Set( .split(',').map((s) => s.trim().toLowerCase()).filter(Boolean) ); +const SAFE_META_KEYS = new Set(['error', 'code', 'component', 'action', 'duration']); +const MAX_REMOTE_MSG_LEN = 256; + +function _buildRemotePayload(level, message, meta) { + const safeMsg = typeof message === 'string' + ? message.slice(0, MAX_REMOTE_MSG_LEN) + : String(message || '').slice(0, MAX_REMOTE_MSG_LEN); + + const safeMeta = {}; + if (meta && typeof meta === 'object') { + for (const key of Object.keys(meta)) { + if (!SAFE_META_KEYS.has(key)) continue; + const val = meta[key]; + if (typeof val === 'string') safeMeta[key] = val.slice(0, 128); + else if (typeof val === 'number' || typeof val === 'boolean') safeMeta[key] = val; + } + } + + return { + timestamp: new Date().toISOString(), + level: String(level), + message: safeMsg, + meta: Object.keys(safeMeta).length > 0 ? safeMeta : undefined, + product: 'TransTrack', + platform: process.platform, + pid: process.pid, + }; +} + function _shipRemote(level, message, meta) { if (!REMOTE_LOG_URL || !REMOTE_LOG_LEVELS.has(level)) return; if (typeof fetch !== 'function') return; // Fire-and-forget; never throw out of the logger. try { + const payload = _buildRemotePayload(level, message, meta); fetch(REMOTE_LOG_URL, { method: 'POST', headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ - timestamp: new Date().toISOString(), - level, - message, - meta: meta || null, - product: 'TransTrack', - version: (() => { - try { return app.getVersion(); } catch { return null; } - })(), - platform: process.platform, - pid: process.pid, - }), + body: JSON.stringify(payload), }).catch(() => { /* swallow */ }); } catch { /* swallow */ } } diff --git a/package-lock.json b/package-lock.json index d15eb15..060229d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -192,6 +192,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -564,6 +565,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -612,6 +614,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -722,6 +725,7 @@ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1090,7 +1094,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1112,7 +1115,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1129,7 +1131,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -1144,7 +1145,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -4554,8 +4554,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4726,6 +4725,7 @@ "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -4737,6 +4737,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4797,6 +4798,7 @@ "integrity": "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.5", @@ -4975,6 +4977,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5002,60 +5005,6 @@ "node": ">= 14" } }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats-draft2019": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ajv-formats-draft2019/-/ajv-formats-draft2019-1.6.1.tgz", - "integrity": "sha512-JQPvavpkWDvIsBp2Z33UkYCtXCSpW4HD3tAZ+oL4iEFOk9obQZffx0yANwECt6vzr6ET+7HN5czRyqXbnq/u0Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "punycode": "^2.1.1", - "schemes": "^1.4.0", - "smtp-address-parser": "^1.0.3", - "uri-js": "^4.4.1" - }, - "peerDependencies": { - "ajv": "*" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -5819,6 +5768,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -6474,8 +6424,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -6628,6 +6577,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -6857,6 +6807,7 @@ "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", @@ -6977,8 +6928,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dotenv": { "version": "16.6.1", @@ -7276,7 +7226,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -7297,7 +7246,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -7643,6 +7591,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9141,9 +9090,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "dev": true, "license": "MIT", "optional": true, @@ -9982,103 +9931,6 @@ "node": ">= 0.8.0" } }, - "node_modules/libxmljs2": { - "version": "0.37.0", - "resolved": "https://registry.npmjs.org/libxmljs2/-/libxmljs2-0.37.0.tgz", - "integrity": "sha512-Xb78V8GZouoZFrq8cCwx7+G3WYOcJG0xb3YUbweSyE4z2EIrQCZMr3Ye/dHn4mESs6YxUMeQeUZm5IXg+iLHog==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "bindings": "~1.5.0", - "nan": "~2.22.2", - "node-gyp": "^11.2.0", - "prebuild-install": "^7.1.3" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/libxmljs2/node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", - "dev": true, - "license": "ISC", - "optional": true, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/libxmljs2/node_modules/node-gyp": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", - "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "tar": "^7.4.3", - "tinyglobby": "^0.2.12", - "which": "^5.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/libxmljs2/node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", - "dev": true, - "license": "ISC", - "optional": true, - "dependencies": { - "abbrev": "^3.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/libxmljs2/node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", - "dev": true, - "license": "ISC", - "optional": true, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/libxmljs2/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -10187,7 +10039,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -10582,7 +10433,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -11222,7 +11072,8 @@ "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-2.0.1.tgz", "integrity": "sha512-N5ixXjzTy4QDQH0Q9YFjqIWd6zH6936Djpl2m9QNFmDv5Fum8q8BjkpAcHNMzOFE0IwQrFhJWex3AN6kS0OSwg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/parent-module": { "version": "1.0.1", @@ -11550,6 +11401,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11715,7 +11567,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -11733,7 +11584,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -11805,7 +11655,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -11821,7 +11670,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -12009,6 +11857,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12035,6 +11884,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12064,8 +11914,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-refresh": { "version": "0.17.0", @@ -12424,7 +12273,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -13109,6 +12957,7 @@ "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -13468,6 +13317,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -13606,7 +13456,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -14232,6 +14081,7 @@ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -14322,6 +14172,7 @@ "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.1.5", "@vitest/mocker": "4.1.5", @@ -14676,6 +14527,7 @@ "integrity": "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@oozcitak/dom": "^2.0.2", "@oozcitak/infra": "^2.0.2", @@ -14785,6 +14637,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 90d4165..4f50663 100644 --- a/package.json +++ b/package.json @@ -164,7 +164,8 @@ }, "overrides": { "picomatch": "^4.0.4", - "axios": "^1.15.0" + "axios": "^1.15.0", + "ip-address": "^10.1.1" }, "build": { "appId": "com.transtrack.medical", diff --git a/scripts/epic-sandbox-test.mjs b/scripts/epic-sandbox-test.mjs index cd0feb1..23bbf8a 100644 --- a/scripts/epic-sandbox-test.mjs +++ b/scripts/epic-sandbox-test.mjs @@ -114,10 +114,15 @@ async function fhirGet(token, path) { } (async () => { + function redact(val) { + if (!val || val.length <= 6) return '***'; + return val.slice(0, 3) + '***' + val.slice(-3); + } + console.log('--- Epic sandbox SMART Backend Services test ---'); - console.log('Token URL :', TOKEN_URL); - console.log('FHIR base :', FHIR_BASE); - console.log('Patient :', PATIENT_ID); + console.log('Token URL :', TOKEN_URL.replace(/\/\/[^/]+/, '//')); + console.log('FHIR base :', FHIR_BASE.replace(/\/\/[^/]+/, '//')); + console.log('Patient :', redact(PATIENT_ID)); console.log(''); console.log('Step 1 - request access token (JWT bearer)...'); @@ -127,7 +132,7 @@ async function fhirGet(token, path) { ); console.log(''); - console.log('Step 2 - GET Patient/' + PATIENT_ID); + console.log('Step 2 - GET Patient/' + redact(PATIENT_ID)); const patient = await fhirGet(tok.access_token, `Patient/${PATIENT_ID}`); const name = patient.name?.[0]; console.log( diff --git a/server/src/integrations/epic/registry.js b/server/src/integrations/epic/registry.js index 02a0250..aae97bc 100644 --- a/server/src/integrations/epic/registry.js +++ b/server/src/integrations/epic/registry.js @@ -75,23 +75,31 @@ function resetCache() { function _loadFile(filePath) { if (!filePath) return null; try { - const stat = fs.statSync(filePath); - if ( - _fileCache && - _fileCachePath === filePath && - _fileCacheMtimeMs === stat.mtimeMs - ) { - return _fileCache; - } - const raw = fs.readFileSync(filePath, 'utf8'); - const parsed = JSON.parse(raw); - if (!parsed || typeof parsed !== 'object') { - throw new Error('config file did not parse to an object'); + const fd = fs.openSync(filePath, 'r'); + try { + const stat = fs.fstatSync(fd); + if ( + _fileCache && + _fileCachePath === filePath && + _fileCacheMtimeMs === stat.mtimeMs + ) { + fs.closeSync(fd); + return _fileCache; + } + const raw = fs.readFileSync(fd, 'utf8'); + fs.closeSync(fd); + const parsed = JSON.parse(raw); + if (!parsed || typeof parsed !== 'object') { + throw new Error('config file did not parse to an object'); + } + _fileCache = parsed; + _fileCachePath = filePath; + _fileCacheMtimeMs = stat.mtimeMs; + return parsed; + } catch (innerErr) { + fs.closeSync(fd); + throw innerErr; } - _fileCache = parsed; - _fileCachePath = filePath; - _fileCacheMtimeMs = stat.mtimeMs; - return parsed; } catch (e) { throw new Error(`Epic registry: failed to load ${filePath}: ${e.message}`); } diff --git a/server/src/middleware/auth.js b/server/src/middleware/auth.js index 381c5d2..36f4312 100644 --- a/server/src/middleware/auth.js +++ b/server/src/middleware/auth.js @@ -23,6 +23,9 @@ const { errors } = require('../util/errors'); function makeAuthHook(config) { return async function authHook(req) { if (req.routeOptions?.config?.public) return; + if (typeof req.rateLimit === 'function') { + await req.rateLimit(); + } const header = req.headers['authorization'] || ''; if (!header.toLowerCase().startsWith('bearer ')) { throw errors.unauthorized('Missing Bearer token'); diff --git a/server/src/routes/auth.js b/server/src/routes/auth.js index 974b9c6..7fc691a 100644 --- a/server/src/routes/auth.js +++ b/server/src/routes/auth.js @@ -58,7 +58,7 @@ module.exports = async function authRoutes(app, opts) { }); // ----- POST /auth/logout ----- - app.post('/auth/logout', async (req) => { + app.post('/auth/logout', { config: { rateLimit: { max: 30, timeWindow: '1 minute' } } }, async (req) => { const body = z.object({ refresh: z.string().optional() }).parse(req.body || {}); await withTransaction({}, async (client) => { await authService.revoke(client, body.refresh); @@ -67,7 +67,7 @@ module.exports = async function authRoutes(app, opts) { }); // ----- POST /auth/mfa/enroll/begin ----- - app.post('/auth/mfa/enroll/begin', async (req) => { + app.post('/auth/mfa/enroll/begin', { config: { rateLimit: { max: 10, timeWindow: '1 minute' } } }, async (req) => { if (!req.auth) throw errors.unauthorized(); const secret = mfa.generateSecret(); const otpauth = mfa.buildOtpauthUrl({ @@ -93,7 +93,7 @@ module.exports = async function authRoutes(app, opts) { }); // ----- POST /auth/mfa/enroll/confirm ----- - app.post('/auth/mfa/enroll/confirm', async (req) => { + app.post('/auth/mfa/enroll/confirm', { config: { rateLimit: { max: 10, timeWindow: '1 minute' } } }, async (req) => { if (!req.auth) throw errors.unauthorized(); const body = z.object({ code: z.string().min(6).max(10) }).parse(req.body); return withTransaction({ orgId: req.auth.orgId, userId: req.auth.userId }, async (client) => { @@ -117,7 +117,7 @@ module.exports = async function authRoutes(app, opts) { }); // ----- POST /auth/password/change ----- - app.post('/auth/password/change', async (req) => { + app.post('/auth/password/change', { config: { rateLimit: { max: 5, timeWindow: '1 minute' } } }, async (req) => { if (!req.auth) throw errors.unauthorized(); const body = z.object({ current: z.string().min(1), @@ -260,7 +260,7 @@ module.exports = async function authRoutes(app, opts) { } // ----- GET /auth/me ----- - app.get('/auth/me', async (req) => { + app.get('/auth/me', { config: { rateLimit: { max: 60, timeWindow: '1 minute' } } }, async (req) => { if (!req.auth) throw errors.unauthorized(); return withTransaction({ orgId: req.auth.orgId, userId: req.auth.userId }, async (client) => { const r = await client.query( diff --git a/server/src/routes/calculators.js b/server/src/routes/calculators.js index b15d363..6b1cef4 100644 --- a/server/src/routes/calculators.js +++ b/server/src/routes/calculators.js @@ -4,13 +4,22 @@ const { z } = require('zod'); const calc = require('../../../electron/services/calculators/index.cjs'); module.exports = async function calculatorRoutes(app) { - app.get('/calculators', async () => ({ + const perRouteRateLimit = { + config: { + rateLimit: { + max: 200, + timeWindow: '1 minute', + }, + }, + }; + + app.get('/calculators', perRouteRateLimit, async () => ({ formulas: calc.ALL_FORMULAS, requiredFields: calc.REQUIRED_FIELDS, disclaimer: calc.DISCLAIMER, })); - app.post('/calculators/meld', async (req) => { + app.post('/calculators/meld', perRouteRateLimit, async (req) => { const body = z.object({ bilirubin: z.number(), inr: z.number(), @@ -21,10 +30,10 @@ module.exports = async function calculatorRoutes(app) { return calc.calculateMELD(body); }); - app.post('/calculators/meld-na', async (req) => calc.calculateMELDNa(req.body)); - app.post('/calculators/meld-3', async (req) => calc.calculateMELD3(req.body)); - app.post('/calculators/peld', async (req) => calc.calculatePELD(req.body)); - app.post('/calculators/las', async (req) => calc.calculateLAS(req.body)); - app.post('/calculators/kdpi', async (req) => calc.calculateKDPI(req.body)); - app.post('/calculators/epts', async (req) => calc.calculateEPTS(req.body)); + app.post('/calculators/meld-na', perRouteRateLimit, async (req) => calc.calculateMELDNa(req.body)); + app.post('/calculators/meld-3', perRouteRateLimit, async (req) => calc.calculateMELD3(req.body)); + app.post('/calculators/peld', perRouteRateLimit, async (req) => calc.calculatePELD(req.body)); + app.post('/calculators/las', perRouteRateLimit, async (req) => calc.calculateLAS(req.body)); + app.post('/calculators/kdpi', perRouteRateLimit, async (req) => calc.calculateKDPI(req.body)); + app.post('/calculators/epts', perRouteRateLimit, async (req) => calc.calculateEPTS(req.body)); }; diff --git a/server/src/routes/smart.js b/server/src/routes/smart.js index 82c435e..710b3a3 100644 --- a/server/src/routes/smart.js +++ b/server/src/routes/smart.js @@ -229,8 +229,17 @@ module.exports = async function smartRoutes(app, opts) { reply.header('Cache-Control', 'no-store'); reply.header('Pragma', 'no-cache'); + const SUPPORTED_GRANT_TYPES = /** @type {const} */ ([ + 'authorization_code', + 'refresh_token', + 'client_credentials', + 'urn:ietf:params:oauth:grant-type:jwt-bearer', + ]); + const body = req.body || {}; - const grantType = body.grant_type; + const grantParsed = z.enum(SUPPORTED_GRANT_TYPES).safeParse(body.grant_type); + if (!grantParsed.success) throw errors.badRequest('unsupported_grant_type'); + const grantType = grantParsed.data; // ---------- Auth header parsing (basic) ------------------------------- let basicClientId = null; @@ -351,7 +360,7 @@ module.exports = async function smartRoutes(app, opts) { }); } - throw errors.badRequest('unsupported_grant_type'); + // unreachable: grant_type validated by z.enum above }); // ----- Dynamic client registration (admin only) ---------------------------